mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 01:09:43 +02:00
fix(omp): resolve and pin the respawn session id only at actual respawn time
findLatestOmpSessionId()'s newest-mtime pin ran eagerly inside _buildRespawnPaneOptions(), which startInteractive() calls unconditionally on every boot-recovery reattach — before anything checks whether the pane is actually dead. With two omp tabs in the same case dir, this could pin an ALIVE pane's session onto whichever sibling's file happened to be newest on disk, purely as a side effect of building options that might never lead to a respawn (reported in Ark0N/Codeman#353 review). Move resolution out of the eager builder into _pinOmpRespawnId(), called explicitly only where a respawn is actually confirmed: the dead-pane branch in _setupOrAttachMuxSession() and reattachRemote(). Add resolveAndClaimOmpSessionId(), which verifies each candidate's own file header (cwd) rather than trusting the mangled-directory match alone, and tracks claimed ids in a process-wide registry so two ambiguous resolutions can't both pick the same sibling's conversation.
This commit is contained in:
+43
-44
@@ -57,7 +57,7 @@ import {
|
|||||||
type SessionRemote,
|
type SessionRemote,
|
||||||
type SessionDocker,
|
type SessionDocker,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { findLatestOmpSessionId } from './utils/omp-session-resolver.js';
|
import { resolveAndClaimOmpSessionId } from './utils/omp-session-resolver.js';
|
||||||
import { probeDockerCliVersion } from './docker-hosts.js';
|
import { probeDockerCliVersion } from './docker-hosts.js';
|
||||||
import { probeRemoteCliVersion } from './remote-hosts.js';
|
import { probeRemoteCliVersion } from './remote-hosts.js';
|
||||||
import type { TerminalMultiplexer, MuxSession } from './mux-interface.js';
|
import type { TerminalMultiplexer, MuxSession } from './mux-interface.js';
|
||||||
@@ -1514,7 +1514,11 @@ export class Session extends EventEmitter {
|
|||||||
let needsNewSession = false;
|
let needsNewSession = false;
|
||||||
if (this._muxSession && mux.isPaneDead(this._muxSession.muxName)) {
|
if (this._muxSession && mux.isPaneDead(this._muxSession.muxName)) {
|
||||||
console.log('[Session] Dead pane detected, respawning:', this._muxSession.muxName);
|
console.log('[Session] Dead pane detected, respawning:', this._muxSession.muxName);
|
||||||
const newPid = await mux.respawnPane(options.respawnPaneOptions);
|
// Confirmed dead — safe to resolve/pin now (see `_pinOmpRespawnId()`).
|
||||||
|
// `options.respawnPaneOptions` was built eagerly before this dead-pane
|
||||||
|
// check ran, so it still carries the pre-pin ompConfig; rebuild it.
|
||||||
|
this._pinOmpRespawnId();
|
||||||
|
const newPid = await mux.respawnPane(this._buildRespawnPaneOptions());
|
||||||
if (!newPid) {
|
if (!newPid) {
|
||||||
console.error('[Session] Failed to respawn pane, will create new session');
|
console.error('[Session] Failed to respawn pane, will create new session');
|
||||||
needsNewSession = true;
|
needsNewSession = true;
|
||||||
@@ -1605,6 +1609,9 @@ export class Session extends EventEmitter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Confirmed the mux session (and thus the pane) exists but this reattach
|
||||||
|
// is about to respawn it — safe to resolve/pin now.
|
||||||
|
this._pinOmpRespawnId();
|
||||||
const newPid = await mux.respawnPane(this._buildRespawnPaneOptions());
|
const newPid = await mux.respawnPane(this._buildRespawnPaneOptions());
|
||||||
if (!newPid) {
|
if (!newPid) {
|
||||||
console.error('[Session] reattachRemote: respawnPane failed for', this._muxSession.muxName);
|
console.error('[Session] reattachRemote: respawnPane failed for', this._muxSession.muxName);
|
||||||
@@ -1637,21 +1644,16 @@ export class Session extends EventEmitter {
|
|||||||
piConfig: this._piConfig,
|
piConfig: this._piConfig,
|
||||||
grokConfig: this._grokConfig,
|
grokConfig: this._grokConfig,
|
||||||
deepSeekConfig: this._deepSeekConfig,
|
deepSeekConfig: this._deepSeekConfig,
|
||||||
// Respawning a dead pane means the CLI process exited (crash, idle
|
// OMP resolution/pinning does NOT happen here. This object is built
|
||||||
// respawn, or the user's own /exit) but this is still the same
|
// EAGERLY — including on every boot-recovery reattach, before anyone
|
||||||
// conversation from the user's perspective — unlike a brand-new
|
// knows whether the pane is actually dead — so resolving here mutated
|
||||||
// `createSession` call, defaulting to continuation here is the honest
|
// `_ompConfig`/`_claudeSessionId` even for a pane that was simply being
|
||||||
// behavior. `--continue` alone is ambiguous the moment ANY other omp
|
// reattached to, not respawned; with two omp tabs in the same case dir
|
||||||
// conversation has touched this directory more recently (ours resumed
|
// that mis-pinned the ALIVE session onto whichever file happened to be
|
||||||
// elsewhere, a second Codeman session opened here, ...) since it just
|
// newest on disk (reported live in the Ark0N/Codeman#353 review). The
|
||||||
// picks the newest session file — so resolve and PIN the exact id the
|
// real pin now happens in `_pinOmpRespawnId()`, called by callers ONLY
|
||||||
// pane that just died was writing to, once. The dead pane's file is
|
// once they've confirmed an actual respawn is about to happen.
|
||||||
// already fully flushed at this point, so "newest file" here is
|
ompConfig: this._ompConfig,
|
||||||
// unambiguous by construction; every later respawn then reuses the
|
|
||||||
// pinned id instead of re-guessing. Only when the session already
|
|
||||||
// carries an explicit resumeSessionId does this skip straight past it
|
|
||||||
// (that one always wins in buildOmpCommand regardless).
|
|
||||||
ompConfig: this._resolvedOmpRespawnConfig(),
|
|
||||||
resumeSessionId: this._resumeSessionId,
|
resumeSessionId: this._resumeSessionId,
|
||||||
envOverrides: this._envOverrides,
|
envOverrides: this._envOverrides,
|
||||||
effort: this._effort,
|
effort: this._effort,
|
||||||
@@ -1671,23 +1673,18 @@ export class Session extends EventEmitter {
|
|||||||
* "newest file on disk" is safe here specifically. Non-omp modes and a
|
* "newest file on disk" is safe here specifically. Non-omp modes and a
|
||||||
* session that already carries an explicit id pass through untouched.
|
* session that already carries an explicit id pass through untouched.
|
||||||
*/
|
*/
|
||||||
private _resolvedOmpRespawnConfig(): OmpConfig | undefined {
|
private _pinOmpRespawnId(): void {
|
||||||
if (this.mode !== 'omp') return this._ompConfig;
|
if (this.mode !== 'omp') return;
|
||||||
if (this._ompConfig?.resumeSessionId) return this._ompConfig;
|
if (this._ompConfig?.resumeSessionId) return;
|
||||||
// Resolving-and-pinning is only correct when a mux session ALREADY exists for
|
// Callers MUST call this only immediately before an ACTUAL respawn (a
|
||||||
// this Session object — a dead-pane respawn, or a boot-recovery reattach (the
|
// confirmed-dead pane, or a genuine remote reattach) — never while merely
|
||||||
// constructor sets _muxSession from persisted state before startInteractive()
|
// building options that might not lead to a respawn. A fresh "Run OMP"
|
||||||
// ever runs there). A genuinely brand-new session (Run OMP -> POST
|
// click has no _muxSession yet and must never inherit whatever omp
|
||||||
// /api/quick-start -> a fresh Session with no muxSession in its create config)
|
// conversation happens to be newest on disk for this working directory
|
||||||
// has _muxSession still null at this point. Without this guard, the eager
|
// (reported live 2026-08-27, fixed in 13a19f79); this guard keeps that
|
||||||
// `respawnPaneOptions: this._buildRespawnPaneOptions()` in startInteractive()
|
// fix intact now that resolution has moved out of the eager options build.
|
||||||
// mutates this._ompConfig via the side effect below BEFORE
|
if (!this._muxSession) return;
|
||||||
// createSessionOptions.ompConfig is even read in the SAME object literal, so a
|
const resolvedId = resolveAndClaimOmpSessionId(this.workingDir);
|
||||||
// fresh "Run OMP" click silently inherited whatever omp conversation happened
|
|
||||||
// to be newest on disk for this working directory instead of starting clean
|
|
||||||
// (reported live 2026-08-27).
|
|
||||||
if (!this._muxSession) return this._ompConfig;
|
|
||||||
const resolvedId = findLatestOmpSessionId(this.workingDir);
|
|
||||||
if (resolvedId) {
|
if (resolvedId) {
|
||||||
this._ompConfig = { ...this._ompConfig, resumeSessionId: resolvedId };
|
this._ompConfig = { ...this._ompConfig, resumeSessionId: resolvedId };
|
||||||
// Alias omp's own session uuid to this Codeman id — see the
|
// Alias omp's own session uuid to this Codeman id — see the
|
||||||
@@ -1695,14 +1692,15 @@ export class Session extends EventEmitter {
|
|||||||
// (generically-named) mechanism that folds a Past-Sessions row back
|
// (generically-named) mechanism that folds a Past-Sessions row back
|
||||||
// into its live/persisted session instead of duplicating it.
|
// into its live/persisted session instead of duplicating it.
|
||||||
this._claudeSessionId = resolvedId;
|
this._claudeSessionId = resolvedId;
|
||||||
return this._ompConfig;
|
return;
|
||||||
}
|
}
|
||||||
// Nothing on disk yet (the dying process never got far enough to write a
|
// Nothing unclaimed on disk (the dying process never got far enough to
|
||||||
// session file) — fall back to the CLI's own "most recent" heuristic.
|
// write a session file, or a sibling already claimed the only candidate)
|
||||||
|
// — fall back to the CLI's own "most recent" heuristic.
|
||||||
console.warn(
|
console.warn(
|
||||||
`[Session] OMP: no session file found under ${this.workingDir} to pin --resume on respawn; falling back to ambiguous --continue`
|
`[Session] OMP: no session file found under ${this.workingDir} to pin --resume on respawn; falling back to ambiguous --continue`
|
||||||
);
|
);
|
||||||
return { ...this._ompConfig, continueSession: true };
|
this._ompConfig = { ...this._ompConfig, continueSession: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1968,10 +1966,11 @@ export class Session extends EventEmitter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Set claudeSessionId — when resuming, the Claude conversation ID is the
|
// Set claudeSessionId — when resuming, the Claude conversation ID is the
|
||||||
// resumed one. `_resolvedOmpRespawnConfig()` (called above while building
|
// resumed one. `_pinOmpRespawnId()` (called just above, inside
|
||||||
// respawnPaneOptions) may have JUST aliased this to omp's own session
|
// `_setupOrAttachMuxSession()`'s dead-pane branch) may have JUST aliased
|
||||||
// uuid — that already-resolved id must win over the generic
|
// this to omp's own session uuid — that already-resolved id must win
|
||||||
// `this.id` fallback, or this line clobbers it back to the Codeman id
|
// over the generic `this.id` fallback, or this line clobbers it back
|
||||||
|
// to the Codeman id
|
||||||
// on every single respawn.
|
// on every single respawn.
|
||||||
this._claudeSessionId = this._resumeSessionId || this._ompConfig?.resumeSessionId || this.id;
|
this._claudeSessionId = this._resumeSessionId || this._ompConfig?.resumeSessionId || this.id;
|
||||||
|
|
||||||
@@ -2394,7 +2393,7 @@ export class Session extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A brand-new omp session (never yet respawned, so
|
* A brand-new omp session (never yet respawned, so
|
||||||
* {@link _resolvedOmpRespawnConfig} has never run) has no captured
|
* {@link _pinOmpRespawnId} has never run) has no captured
|
||||||
* omp-native session id: `_claudeSessionId` still defaults to this
|
* omp-native session id: `_claudeSessionId` still defaults to this
|
||||||
* session's OWN Codeman id from the constructor. Until something aliases
|
* session's OWN Codeman id from the constructor. Until something aliases
|
||||||
* it, the omp history scan's row for this exact conversation (keyed by
|
* it, the omp history scan's row for this exact conversation (keyed by
|
||||||
@@ -2407,7 +2406,7 @@ export class Session extends EventEmitter {
|
|||||||
private _maybeCaptureOmpSessionId(): void {
|
private _maybeCaptureOmpSessionId(): void {
|
||||||
if (this.mode !== 'omp' || this._claudeSessionId !== this.id) return;
|
if (this.mode !== 'omp' || this._claudeSessionId !== this.id) return;
|
||||||
try {
|
try {
|
||||||
const resolvedId = findLatestOmpSessionId(this.workingDir);
|
const resolvedId = resolveAndClaimOmpSessionId(this.workingDir);
|
||||||
if (resolvedId) {
|
if (resolvedId) {
|
||||||
this._claudeSessionId = resolvedId;
|
this._claudeSessionId = resolvedId;
|
||||||
this._ompConfig = { ...this._ompConfig, resumeSessionId: resolvedId };
|
this._ompConfig = { ...this._ompConfig, resumeSessionId: resolvedId };
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
* @module utils/omp-session-resolver
|
* @module utils/omp-session-resolver
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readdirSync, statSync } from 'node:fs';
|
import { closeSync, openSync, readdirSync, readSync, statSync } from 'node:fs';
|
||||||
import { homedir } from 'node:os';
|
import { homedir } from 'node:os';
|
||||||
import { join, sep } from 'node:path';
|
import { join, sep } from 'node:path';
|
||||||
|
|
||||||
@@ -92,3 +92,95 @@ export function findLatestOmpSessionId(workingDir: string): string | null {
|
|||||||
}
|
}
|
||||||
return newestId;
|
return newestId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The session header line is always near the top of the file (the
|
||||||
|
* transcript's own "second line" — see omp-transcript.ts), so identifying a
|
||||||
|
* file never needs reading the whole thing (up to multi-MB, per that same
|
||||||
|
* module's size cap). Bounded read only.
|
||||||
|
*/
|
||||||
|
const HEADER_READ_BYTES = 8 * 1024;
|
||||||
|
|
||||||
|
function readOmpSessionHeader(filePath: string): { id: string; cwd: string } | null {
|
||||||
|
let raw: string;
|
||||||
|
try {
|
||||||
|
const fd = openSync(filePath, 'r');
|
||||||
|
try {
|
||||||
|
const buf = Buffer.alloc(HEADER_READ_BYTES);
|
||||||
|
const bytesRead = readSync(fd, buf, 0, HEADER_READ_BYTES, 0);
|
||||||
|
raw = buf.toString('utf-8', 0, bytesRead);
|
||||||
|
} finally {
|
||||||
|
closeSync(fd);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (const line of raw.split('\n')) {
|
||||||
|
if (!line) continue;
|
||||||
|
let entry: unknown;
|
||||||
|
try {
|
||||||
|
entry = JSON.parse(line);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!entry || typeof entry !== 'object') continue;
|
||||||
|
const e = entry as Record<string, unknown>;
|
||||||
|
if (e.type === 'session' && typeof e.id === 'string' && typeof e.cwd === 'string') {
|
||||||
|
return { id: e.id, cwd: e.cwd };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process-wide registry of OMP session ids already pinned to a live Codeman
|
||||||
|
* session. Two omp tabs in the same case dir (`w1-foo`, `w2-foo`) resolve
|
||||||
|
* against the SAME directory on disk — without this, both could pick the
|
||||||
|
* newest file and alias onto each other's conversation (found in upstream PR
|
||||||
|
* review, Ark0N/Codeman#353). Never released: this holds at most a handful of
|
||||||
|
* short ids per real omp conversation ever pinned in this process's lifetime,
|
||||||
|
* immaterial memory even after weeks of uptime — correctness here matters
|
||||||
|
* more than reclaiming it.
|
||||||
|
*/
|
||||||
|
const claimedOmpSessionIds = new Set<string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safe variant of {@link findLatestOmpSessionId} for callers where two omp
|
||||||
|
* sessions CAN share the same case directory — a dead-pane respawn, a
|
||||||
|
* boot-recovery reattach, or a first-idle capture — instead of the narrower
|
||||||
|
* cases where "newest file" is unambiguous by construction. Verifies each
|
||||||
|
* candidate's own header `cwd` against `workingDir` (mangling is a lossy
|
||||||
|
* one-way transform — see {@link mangleOmpWorkingDir} — so trusting the
|
||||||
|
* filename-derived id alone isn't enough) and skips any id a sibling session
|
||||||
|
* has already claimed. Claims the id it returns so a concurrent caller
|
||||||
|
* resolving the same directory in the same tick can't double-claim it.
|
||||||
|
*/
|
||||||
|
export function resolveAndClaimOmpSessionId(workingDir: string): string | null {
|
||||||
|
const dir = join(resolveOmpHome(), 'agent', 'sessions', mangleOmpWorkingDir(workingDir));
|
||||||
|
let entries: string[];
|
||||||
|
try {
|
||||||
|
entries = readdirSync(dir);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newestMtime = -Infinity;
|
||||||
|
let newestId: string | null = null;
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (!OMP_SESSION_FILE_PATTERN.test(entry)) continue;
|
||||||
|
const filePath = join(dir, entry);
|
||||||
|
let mtimeMs: number;
|
||||||
|
try {
|
||||||
|
mtimeMs = statSync(filePath).mtimeMs;
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (mtimeMs <= newestMtime) continue;
|
||||||
|
const header = readOmpSessionHeader(filePath);
|
||||||
|
if (!header || header.cwd !== workingDir || claimedOmpSessionIds.has(header.id)) continue;
|
||||||
|
newestMtime = mtimeMs;
|
||||||
|
newestId = header.id;
|
||||||
|
}
|
||||||
|
if (newestId) claimedOmpSessionIds.add(newestId);
|
||||||
|
return newestId;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
/**
|
/**
|
||||||
* @fileoverview Pins the "Run OMP always resumes" bug found live 2026-08-27.
|
* @fileoverview Pins the "Run OMP always resumes" bug found live 2026-08-27,
|
||||||
|
* and its follow-on fix for the sibling-aliasing bug found in upstream PR
|
||||||
|
* review (Ark0N/Codeman#353).
|
||||||
*
|
*
|
||||||
* Session._resolvedOmpRespawnConfig() resolves-and-pins the newest on-disk omp
|
* Session._pinOmpRespawnId() resolves-and-pins the newest on-disk omp
|
||||||
* conversation as a side effect on `this._ompConfig`. That is correct when
|
* conversation as a side effect on `this._ompConfig`. That is correct ONLY
|
||||||
* reattaching to an ALREADY-TRACKED mux session (a dead-pane respawn, or a
|
* immediately before an ACTUAL respawn (a confirmed-dead pane, or a genuine
|
||||||
* boot-recovery reattach — the constructor sets `_muxSession` from persisted
|
* remote reattach) — never while merely building options that might not
|
||||||
* state before startInteractive() ever runs there). It is wrong for a
|
* lead to one. It used to run eagerly inside `_buildRespawnPaneOptions()`,
|
||||||
* genuinely brand-new session: startInteractive() computes
|
* which startInteractive() calls unconditionally (including for a genuinely
|
||||||
* `respawnPaneOptions: this._buildRespawnPaneOptions()` EAGERLY in the same
|
* brand-new session, and for a boot-recovery reattach to a pane that turns
|
||||||
* object literal that builds `createSessionOptions.ompConfig: this._ompConfig`,
|
* out to still be alive): a fresh "Run OMP" click in a working directory
|
||||||
* so the resolve-and-pin side effect ran and poisoned `this._ompConfig` before
|
* with any prior omp history silently launched `--resume <old-id>` instead
|
||||||
* that field was even read — a fresh "Run OMP" click in a working directory
|
* of a clean `omp` invocation, and — with two omp tabs in the same case dir
|
||||||
* with any prior omp history silently launched `--resume <old-id>` instead of
|
* — a live pane's `_ompConfig`/`claudeSessionId` could get mis-pinned to
|
||||||
* a clean `omp` invocation.
|
* whichever sibling's file happened to be newest on disk, even though
|
||||||
|
* nothing was actually being respawned. Resolution now happens only inside
|
||||||
|
* `_pinOmpRespawnId()`, called by a caller that has already confirmed a
|
||||||
|
* real respawn is happening.
|
||||||
*/
|
*/
|
||||||
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
import { homedir } from 'node:os';
|
import { homedir } from 'node:os';
|
||||||
@@ -35,7 +40,11 @@ describe('OMP: fresh session vs. reattach must not share resumeSessionId resolut
|
|||||||
function seedOmpSessionFile(id: string) {
|
function seedOmpSessionFile(id: string) {
|
||||||
mkdirSync(workingDir, { recursive: true });
|
mkdirSync(workingDir, { recursive: true });
|
||||||
mkdirSync(sessionDir, { recursive: true });
|
mkdirSync(sessionDir, { recursive: true });
|
||||||
writeFileSync(join(sessionDir, `2026-08-27T17-31-08-001Z_${id}.jsonl`), '{}');
|
// resolveAndClaimOmpSessionId() verifies the file's own header (not just
|
||||||
|
// the filename), mirroring the real `omp` session-file shape — the
|
||||||
|
// header's `cwd` must match `workingDir` for the candidate to count.
|
||||||
|
const header = `${JSON.stringify({ type: 'session', id, cwd: workingDir })}\n`;
|
||||||
|
writeFileSync(join(sessionDir, `2026-08-27T17-31-08-001Z_${id}.jsonl`), header);
|
||||||
}
|
}
|
||||||
|
|
||||||
it('a brand-new session (no prior mux session) never inherits an on-disk conversation', async () => {
|
it('a brand-new session (no prior mux session) never inherits an on-disk conversation', async () => {
|
||||||
@@ -56,8 +65,13 @@ describe('OMP: fresh session vs. reattach must not share resumeSessionId resolut
|
|||||||
expect(session.claudeSessionId).toBe(session.id);
|
expect(session.claudeSessionId).toBe(session.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('a reattach to an existing tracked mux session still resolves and pins the real id', async () => {
|
it('a plain reattach to an existing mux session (pane still alive) does NOT pin', async () => {
|
||||||
seedOmpSessionFile('real-omp-uuid');
|
// Regression for the sibling-aliasing bug: pinning must never be a side
|
||||||
|
// effect of merely building respawn options for a pane that might still
|
||||||
|
// be alive (isPaneDead is unconditionally false under IS_TEST_MODE,
|
||||||
|
// which is what a real "just reattaching, nothing died" boot recovery
|
||||||
|
// looks like from Session's perspective).
|
||||||
|
seedOmpSessionFile('sibling-conversation-id');
|
||||||
|
|
||||||
const muxSession: MuxSession = {
|
const muxSession: MuxSession = {
|
||||||
sessionId: 'placeholder',
|
sessionId: 'placeholder',
|
||||||
@@ -81,7 +95,35 @@ describe('OMP: fresh session vs. reattach must not share resumeSessionId resolut
|
|||||||
await session.startInteractive();
|
await session.startInteractive();
|
||||||
const state = session.toState();
|
const state = session.toState();
|
||||||
|
|
||||||
expect(state.ompConfig?.resumeSessionId).toBe('real-omp-uuid');
|
expect(state.ompConfig?.resumeSessionId).toBeUndefined();
|
||||||
|
expect(session.claudeSessionId).toBe(session.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('_pinOmpRespawnId() resolves and pins the real id once a respawn is confirmed', () => {
|
||||||
|
seedOmpSessionFile('real-omp-uuid');
|
||||||
|
|
||||||
|
const muxSession: MuxSession = {
|
||||||
|
sessionId: 'placeholder',
|
||||||
|
muxName: 'codeman-deadbeef',
|
||||||
|
pid: 1,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
workingDir,
|
||||||
|
mode: 'omp',
|
||||||
|
attached: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const session = new Session({
|
||||||
|
workingDir,
|
||||||
|
mode: 'omp',
|
||||||
|
mux: new TmuxManager(),
|
||||||
|
useMux: true,
|
||||||
|
muxSession,
|
||||||
|
});
|
||||||
|
sessions.push(session);
|
||||||
|
|
||||||
|
(session as unknown as { _pinOmpRespawnId(): void })._pinOmpRespawnId();
|
||||||
|
|
||||||
|
expect(session.toState().ompConfig?.resumeSessionId).toBe('real-omp-uuid');
|
||||||
expect(session.claudeSessionId).toBe('real-omp-uuid');
|
expect(session.claudeSessionId).toBe('real-omp-uuid');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user