fix: implement PR325 session capture fallback and spawn race hardening (#366)

* fix: capture exact OpenCode session ID from JSON stream to prevent orphan sessions

- Primary: Extract session_id from opencode run --format json step_start event
- Fallback: Title-based match with newest-first sorting for delayed visibility
- Core: Add atomic reserveSessionId check in spawnOrchestrator to prevent race conditions
- Prevents orphan sessions when multiple spawns use same title or delayed discovery

(cherry picked from commit 42cc0cfa1a)

* fix: capture OpenCode session id from stream output

(cherry picked from commit 42b3bf3ba6)

* fix: respawn orchestrators when stale metadata is left behind

(cherry picked from commit e519628017)

* fix: align respawn guard and opencode test expectation

* fix: remove unused helper from opencode launch path

* fix: remove unused function and align tests with session capture format

- Remove unused buildSessionLookupScript function from opencode agent
- Update integration tests to expect --format json flag in launch commands
- Fix assertions for exec opencode --session wrapper format

* fix: address Bugbot findings on PR #366

- Fix orphaned runtime leak when reuse strategy finds alive runtime but
  get() returns null (now destroys the orphaned runtime)
- Restore session ID format validation in fallback script with
  isValidId regex check
- Add regression tests for both fixes

* fix: address additional Bugbot findings on PR #366

- Add timestamp helper to fallback sort to avoid NaN from invalid dates
- Add session ID type/format validation to primary capture script
- Add tests for both robustness improvements
This commit is contained in:
Harsh Batheja 2026-03-10 08:42:07 +05:30 committed by GitHub
parent a99d37b1d0
commit cf31dee0b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 198 additions and 101 deletions

View File

@ -2545,6 +2545,62 @@ describe("spawnOrchestrator", () => {
expect(existsSync(listLogPath)).toBe(false);
});
it("destroys orphaned runtime when reuse strategy finds alive runtime but get returns null", async () => {
const opencodeAgent: Agent = {
...mockAgent,
name: "opencode",
};
const registryWithOpenCode: PluginRegistry = {
...mockRegistry,
get: vi.fn().mockImplementation((slot: string) => {
if (slot === "runtime") return mockRuntime;
if (slot === "agent") return opencodeAgent;
if (slot === "workspace") return mockWorkspace;
return null;
}),
};
const configWithReuse: OrchestratorConfig = {
...config,
defaults: { ...config.defaults, agent: "opencode" },
projects: {
...config.projects,
"my-app": {
...config.projects["my-app"],
agent: "opencode",
orchestratorSessionStrategy: "reuse",
},
},
};
const orphanedHandle = makeHandle("rt-orphaned");
writeMetadata(sessionsDir, "app-orchestrator", {
worktree: join(tmpDir, "my-app"),
branch: "main",
status: "working",
role: "orchestrator",
project: "my-app",
agent: "opencode",
runtimeHandle: JSON.stringify(orphanedHandle),
createdAt: new Date().toISOString(),
});
vi.mocked(mockRuntime.isAlive).mockImplementation(async (handle: RuntimeHandle) => {
if (handle?.id === "rt-orphaned") {
deleteMetadata(sessionsDir, "app-orchestrator");
return true;
}
return false;
});
const sm = createSessionManager({ config: configWithReuse, registry: registryWithOpenCode });
const session = await sm.spawnOrchestrator({ projectId: "my-app" });
expect(session.id).toBe("app-orchestrator");
expect(mockRuntime.destroy).toHaveBeenCalledWith(orphanedHandle);
expect(mockRuntime.create).toHaveBeenCalled();
});
it("reuses mapped OpenCode session id when strategy is reuse and runtime is restarted", async () => {
const opencodeAgent: Agent = {
...mockAgent,
@ -2846,6 +2902,27 @@ describe("spawnOrchestrator", () => {
expect(meta?.["orchestratorSessionReused"]).toBeUndefined();
});
it("respawns the orchestrator when stale metadata exists but the runtime is dead", async () => {
writeMetadata(sessionsDir, "app-orchestrator", {
worktree: join(tmpDir, "my-app"),
branch: "main",
status: "working",
project: "my-app",
role: "orchestrator",
runtimeHandle: JSON.stringify(makeHandle("rt-stale")),
createdAt: new Date().toISOString(),
});
vi.mocked(mockRuntime.isAlive).mockResolvedValueOnce(false);
const sm = createSessionManager({ config, registry: mockRegistry });
await sm.spawnOrchestrator({ projectId: "my-app" });
expect(mockRuntime.create).toHaveBeenCalledTimes(1);
const meta = readMetadataRaw(sessionsDir, "app-orchestrator");
expect(meta?.["runtimeHandle"]).toBe(JSON.stringify(makeHandle("rt-1")));
});
it("uses orchestratorModel when configured", async () => {
const configWithOrchestratorModel: OrchestratorConfig = {
...config,

View File

@ -1008,18 +1008,38 @@ export function createSessionManager(deps: SessionManagerDeps): OpenCodeSessionM
writeFileSync(systemPromptFile, orchestratorConfig.systemPrompt, "utf-8");
}
const existingOrchestrator = await get(sessionId);
if (existingOrchestrator?.runtimeHandle) {
const existingAlive = await plugins.runtime
.isAlive(existingOrchestrator.runtimeHandle)
.catch(() => false);
const existingOrchestratorMetadata = readMetadataRaw(sessionsDir, sessionId);
const existingRuntimeHandle = existingOrchestratorMetadata?.["runtimeHandle"]
? safeJsonParse<RuntimeHandle>(existingOrchestratorMetadata["runtimeHandle"])
: null;
if (existingRuntimeHandle) {
const existingAlive = await plugins.runtime.isAlive(existingRuntimeHandle).catch(() => false);
if (existingAlive && orchestratorSessionStrategy === "reuse") {
existingOrchestrator.metadata["orchestratorSessionReused"] = "true";
return existingOrchestrator;
const existingOrchestrator = await get(sessionId);
if (existingOrchestrator) {
existingOrchestrator.metadata["orchestratorSessionReused"] = "true";
return existingOrchestrator;
}
await plugins.runtime.destroy(existingRuntimeHandle).catch(() => undefined);
} else if (existingAlive) {
await plugins.runtime.destroy(existingRuntimeHandle).catch(() => undefined);
}
if (existingAlive && orchestratorSessionStrategy !== "reuse") {
await plugins.runtime.destroy(existingOrchestrator.runtimeHandle).catch(() => undefined);
}
if (!existingOrchestratorMetadata && !reserveSessionId(sessionsDir, sessionId)) {
const raceSession = await get(sessionId);
if (raceSession?.runtimeHandle) {
const raceAlive = await plugins.runtime
.isAlive(raceSession.runtimeHandle)
.catch(() => false);
if (raceAlive && orchestratorSessionStrategy === "reuse") {
raceSession.metadata["orchestratorSessionReused"] = "true";
return raceSession;
}
}
throw new Error(
`Failed to reserve orchestrator session ID ${sessionId} (concurrent spawn detected)`,
);
}
const reusableOpenCodeSessionId =

View File

@ -202,10 +202,10 @@ describe("getLaunchCommand (integration)", () => {
prompt: "fix the bug",
});
expect(cmd).toContain("--agent 'sisyphus'");
expect(cmd).toContain("opencode run --title 'AO:test-1'");
expect(cmd).toContain("opencode run --format json --title 'AO:test-1'");
expect(cmd).toContain("'fix the bug'");
expect(cmd).not.toContain("--prompt");
expect(cmd).toContain("&& exec opencode --session");
expect(cmd).toContain("exec opencode --session");
expect(cmd).toContain("--agent 'sisyphus'");
});
@ -215,7 +215,7 @@ describe("getLaunchCommand (integration)", () => {
systemPrompt: "You are an orchestrator",
prompt: "do the task",
});
expect(cmd).toContain("opencode run --title 'AO:test-1'");
expect(cmd).toContain("opencode run --format json --title 'AO:test-1'");
expect(cmd).not.toContain("--prompt");
expect(cmd).toContain("You are an orchestrator");
expect(cmd).toContain("do the task");
@ -227,7 +227,7 @@ describe("getLaunchCommand (integration)", () => {
systemPromptFile: "/tmp/orchestrator-prompt.md",
prompt: "do the task",
});
expect(cmd).toContain("opencode run --title 'AO:test-1'");
expect(cmd).toContain("opencode run --format json --title 'AO:test-1'");
expect(cmd).not.toContain("--prompt");
expect(cmd).toContain(
"$(cat '/tmp/orchestrator-prompt.md'; printf '\\n\\n'; printf %s 'do the task')",
@ -252,7 +252,7 @@ describe("getLaunchCommand (integration)", () => {
prompt: "review this code",
});
expect(cmd).toContain("--agent 'oracle'");
expect(cmd).toContain("opencode run --title 'AO:test-1'");
expect(cmd).toContain("opencode run --format json --title 'AO:test-1'");
expect(cmd).not.toContain("--prompt");
expect(cmd).toContain("You are an expert");
expect(cmd).toContain("review this code");
@ -277,9 +277,9 @@ describe("getLaunchCommand (integration)", () => {
systemPromptFile: "/tmp/orchestrator-prompt.md",
});
expect(cmd).toContain(
"opencode run --title 'AO:test-orchestrator' \"$(cat '/tmp/orchestrator-prompt.md')\"",
"opencode run --format json --title 'AO:test-orchestrator' \"$(cat '/tmp/orchestrator-prompt.md')\"",
);
expect(cmd).toContain("&& exec opencode --session");
expect(cmd).toContain("exec opencode --session");
});
it("escapes single quotes in systemPrompt", () => {
@ -303,7 +303,7 @@ describe("getLaunchCommand (integration)", () => {
...baseConfig,
prompt: "fix and `backtick` and 'quote'",
});
expect(cmd).toContain("opencode run --title 'AO:test-1'");
expect(cmd).toContain("opencode run --format json --title 'AO:test-1'");
expect(cmd).toContain("fix and `backtick`");
expect(cmd).not.toContain("--prompt");
});
@ -313,8 +313,8 @@ describe("getLaunchCommand (integration)", () => {
...baseConfig,
prompt: "",
});
expect(cmd).toContain("opencode run --title 'AO:test-1' --command true");
expect(cmd).toContain("&& exec opencode --session");
expect(cmd).toContain("opencode run --format json --title 'AO:test-1' --command true");
expect(cmd).toContain("exec opencode --session");
expect(cmd).toContain("opencode session list --format json");
expect(cmd).toContain("AO:test-1");
});
@ -324,7 +324,7 @@ describe("getLaunchCommand (integration)", () => {
...baseConfig,
prompt: "line1\nline2",
});
expect(cmd).toContain("opencode run --title 'AO:test-1'");
expect(cmd).toContain("opencode run --format json --title 'AO:test-1'");
expect(cmd).toContain("'line1");
expect(cmd).not.toContain("--prompt");
});
@ -336,8 +336,8 @@ describe("getLaunchCommand (integration)", () => {
});
expect(cmd).toContain("--title 'AO:test-1'");
expect(cmd).not.toContain("--prompt 'start work'");
expect(cmd).toContain("opencode run --title 'AO:test-1' 'start work'");
expect(cmd).toContain("&& exec opencode --session");
expect(cmd).toContain("opencode run --format json --title 'AO:test-1' 'start work'");
expect(cmd).toContain("exec opencode --session");
});
it("uses --session when existing OpenCode session id is provided", () => {

View File

@ -112,17 +112,17 @@ describe("getLaunchCommand", () => {
it("generates base command without prompt", () => {
const cmd = agent.getLaunchCommand(makeLaunchConfig());
expect(cmd).toContain("opencode run --title 'AO:sess-1' --command true");
expect(cmd).toContain("&& exec opencode --session");
expect(cmd).toContain("opencode run --format json --title 'AO:sess-1' --command true");
expect(cmd).toContain("exec opencode --session");
expect(cmd).toContain("opencode session list --format json");
expect(cmd).toContain("AO:sess-1");
expect(cmd).toContain("try { rows = JSON.parse(input); } catch { process.exit(1); }");
expect(cmd).toContain("try{rows=JSON.parse(input)}catch{process.exit(1)}");
});
it("uses --prompt with shell-escaped prompt", () => {
const cmd = agent.getLaunchCommand(makeLaunchConfig({ prompt: "Fix it" }));
expect(cmd).toContain("opencode run --title 'AO:sess-1' 'Fix it'");
expect(cmd).toContain("&& exec opencode --session");
expect(cmd).toContain("opencode run --format json --title 'AO:sess-1' 'Fix it'");
expect(cmd).toContain("exec opencode --session");
});
it("includes --model with shell-escaped value", () => {
@ -135,16 +135,16 @@ describe("getLaunchCommand", () => {
makeLaunchConfig({ prompt: "Go", model: "claude-sonnet-4-5-20250929" }),
);
expect(cmd).toContain(
"opencode run --title 'AO:sess-1' --model 'claude-sonnet-4-5-20250929' 'Go'",
"opencode run --format json --title 'AO:sess-1' --model 'claude-sonnet-4-5-20250929' 'Go'",
);
expect(cmd).toContain("&& exec opencode --session");
expect(cmd).toContain("exec opencode --session");
expect(cmd).toContain("--model 'claude-sonnet-4-5-20250929'");
});
it("escapes single quotes in prompt (POSIX shell escaping)", () => {
const cmd = agent.getLaunchCommand(makeLaunchConfig({ prompt: "it's broken" }));
expect(cmd).toContain("opencode run --title 'AO:sess-1' 'it'\\''s broken'");
expect(cmd).toContain("&& exec opencode --session");
expect(cmd).toContain("opencode run --format json --title 'AO:sess-1' 'it'\\''s broken'");
expect(cmd).toContain("exec opencode --session");
});
it("omits optional flags when not provided", () => {
@ -165,8 +165,10 @@ describe("getLaunchCommand", () => {
const cmd = agent.getLaunchCommand(
makeLaunchConfig({ subagent: "sisyphus", prompt: "fix bug" }),
);
expect(cmd).toContain("opencode run --title 'AO:sess-1' --agent 'sisyphus' 'fix bug'");
expect(cmd).toContain("&& exec opencode --session");
expect(cmd).toContain(
"opencode run --format json --title 'AO:sess-1' --agent 'sisyphus' 'fix bug'",
);
expect(cmd).toContain("exec opencode --session");
expect(cmd).toContain("--agent 'sisyphus'");
});
@ -179,9 +181,9 @@ describe("getLaunchCommand", () => {
}),
);
expect(cmd).toContain(
"opencode run --title 'AO:sess-1' --agent 'sisyphus' --model 'claude-sonnet-4-5-20250929' 'fix the bug'",
"opencode run --format json --title 'AO:sess-1' --agent 'sisyphus' --model 'claude-sonnet-4-5-20250929' 'fix the bug'",
);
expect(cmd).toContain("&& exec opencode --session");
expect(cmd).toContain("exec opencode --session");
expect(cmd).toContain("--agent 'sisyphus'");
expect(cmd).toContain("--model 'claude-sonnet-4-5-20250929'");
});
@ -205,8 +207,8 @@ describe("getLaunchCommand", () => {
it("backward compatible: no agent flag when subagent not provided", () => {
const cmd = agent.getLaunchCommand(makeLaunchConfig({ prompt: "fix it" }));
expect(cmd).not.toContain("--agent");
expect(cmd).toContain("opencode run --title 'AO:sess-1' 'fix it'");
expect(cmd).toContain("&& exec opencode --session");
expect(cmd).toContain("opencode run --format json --title 'AO:sess-1' 'fix it'");
expect(cmd).toContain("exec opencode --session");
});
it("combines model and prompt without agent (backward compatible)", () => {
@ -215,9 +217,9 @@ describe("getLaunchCommand", () => {
);
expect(cmd).not.toContain("--agent");
expect(cmd).toContain(
"opencode run --title 'AO:sess-1' --model 'claude-sonnet-4-5-20250929' 'Go'",
"opencode run --format json --title 'AO:sess-1' --model 'claude-sonnet-4-5-20250929' 'Go'",
);
expect(cmd).toContain("&& exec opencode --session");
expect(cmd).toContain("exec opencode --session");
expect(cmd).toContain("--model 'claude-sonnet-4-5-20250929'");
});
@ -228,7 +230,7 @@ describe("getLaunchCommand", () => {
const cmd = agent.getLaunchCommand(
makeLaunchConfig({ systemPrompt: "You are an orchestrator" }),
);
expect(cmd).toContain("opencode run --title 'AO:sess-1'");
expect(cmd).toContain("opencode run --format json --title 'AO:sess-1'");
expect(cmd).toContain("'You are an orchestrator'");
});
@ -236,7 +238,7 @@ describe("getLaunchCommand", () => {
const cmd = agent.getLaunchCommand(
makeLaunchConfig({ systemPrompt: "You are an orchestrator", prompt: "do the task" }),
);
expect(cmd).toContain("opencode run --title 'AO:sess-1'");
expect(cmd).toContain("opencode run --format json --title 'AO:sess-1'");
expect(cmd).not.toContain("--prompt 'You are an orchestrator");
expect(cmd).toContain("do the task'");
});
@ -249,22 +251,26 @@ describe("getLaunchCommand", () => {
it("handles very long systemPrompt", () => {
const longPrompt = "A".repeat(500);
const cmd = agent.getLaunchCommand(makeLaunchConfig({ systemPrompt: longPrompt }));
expect(cmd).toContain("opencode run --title 'AO:sess-1'");
expect(cmd).toContain("opencode run --format json --title 'AO:sess-1'");
expect(cmd.length).toBeGreaterThan(500);
});
it("generates command with systemPromptFile via shell substitution", () => {
const cmd = agent.getLaunchCommand(makeLaunchConfig({ systemPromptFile: "/tmp/prompt.md" }));
expect(cmd).toContain("opencode run --title 'AO:sess-1' \"$(cat '/tmp/prompt.md')\"");
expect(cmd).toContain("&& exec opencode --session");
expect(cmd).toContain(
"opencode run --format json --title 'AO:sess-1' \"$(cat '/tmp/prompt.md')\"",
);
expect(cmd).toContain("exec opencode --session");
});
it("escapes path in systemPromptFile", () => {
const cmd = agent.getLaunchCommand(
makeLaunchConfig({ systemPromptFile: "/tmp/it's-prompt.md" }),
);
expect(cmd).toContain("opencode run --title 'AO:sess-1' \"$(cat '/tmp/it'\\''s-prompt.md')\"");
expect(cmd).toContain("&& exec opencode --session");
expect(cmd).toContain(
"opencode run --format json --title 'AO:sess-1' \"$(cat '/tmp/it'\\''s-prompt.md')\"",
);
expect(cmd).toContain("exec opencode --session");
});
it("systemPromptFile takes precedence over systemPrompt", () => {
@ -287,9 +293,9 @@ describe("getLaunchCommand", () => {
}),
);
expect(cmd).toContain(
"opencode run --title 'AO:my-orchestrator' \"$(cat '/tmp/orchestrator.md')\"",
"opencode run --format json --title 'AO:my-orchestrator' \"$(cat '/tmp/orchestrator.md')\"",
);
expect(cmd).toContain("&& exec opencode --session");
expect(cmd).toContain("exec opencode --session");
});
it("combines systemPromptFile with subagent and prompt", () => {
@ -301,8 +307,8 @@ describe("getLaunchCommand", () => {
}),
);
expect(cmd).toContain("--agent 'sisyphus'");
expect(cmd).toContain("opencode run --title 'AO:sess-1'");
expect(cmd).toContain("&& exec opencode --session");
expect(cmd).toContain("opencode run --format json --title 'AO:sess-1'");
expect(cmd).toContain("exec opencode --session");
expect(cmd).toContain("--agent 'sisyphus'");
expect(cmd).not.toContain("--prompt");
expect(cmd).toContain(
@ -322,7 +328,7 @@ describe("getLaunchCommand", () => {
it("handles prompt with newlines", () => {
const cmd = agent.getLaunchCommand(makeLaunchConfig({ prompt: "line1\nline2\nline3" }));
expect(cmd).toContain("opencode run --title 'AO:sess-1'");
expect(cmd).toContain("opencode run --format json --title 'AO:sess-1'");
expect(cmd).toContain("'line1");
});
@ -353,12 +359,36 @@ describe("getLaunchCommand", () => {
it("handles empty prompt", () => {
const cmd = agent.getLaunchCommand(makeLaunchConfig({ prompt: "" }));
expect(cmd).toContain("opencode run --title 'AO:sess-1' --command true");
expect(cmd).toContain("&& exec opencode --session");
expect(cmd).toContain("opencode run --format json --title 'AO:sess-1' --command true");
expect(cmd).toContain("exec opencode --session");
expect(cmd).toContain("opencode session list --format json");
expect(cmd).toContain("AO:sess-1");
});
it("validates session ID format in fallback script", () => {
const cmd = agent.getLaunchCommand(makeLaunchConfig({ prompt: "test" }));
expect(cmd).toContain("isValidId=id=>/^ses_[A-Za-z0-9_-]+$/.test(id)");
expect(cmd).toContain("isValidId(r.id)");
});
it("primary capture validates session ID type and format", () => {
const cmd = agent.getLaunchCommand(makeLaunchConfig({ prompt: "test" }));
expect(cmd).toContain("isValidId=id=>typeof id===");
expect(cmd).toContain("isValidId(evt.session_id)");
});
it("fallback sort handles invalid date strings without NaN", () => {
const cmd = agent.getLaunchCommand(makeLaunchConfig({ prompt: "test" }));
expect(cmd).toContain("timestamp=v=>");
expect(cmd).toContain("Number.NEGATIVE_INFINITY");
});
it("pipes JSON output into node instead of treating the session id as a command", () => {
const cmd = agent.getLaunchCommand(makeLaunchConfig({ prompt: "Fix it" }));
expect(cmd).toContain("| node -e");
expect(cmd).not.toContain('| "$(node -e');
});
it("uses existing session id", () => {
const cmd = agent.getLaunchCommand(
makeLaunchConfig({

View File

@ -37,48 +37,6 @@ function parseSessionList(raw: string): OpenCodeSessionListEntry[] {
});
}
function buildSessionLookupScript(): string {
const script = `
let input = '';
process.stdin.on('data', c => input += c).on('end', () => {
const title = process.argv[1];
let rows;
try { rows = JSON.parse(input); } catch { process.exit(1); }
if (!Array.isArray(rows)) process.exit(1);
const isValidId = id => /^ses_[A-Za-z0-9_-]+$/.test(id);
const timestamp = value => {
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'string') {
const parsed = Date.parse(value);
return Number.isNaN(parsed) ? Number.NEGATIVE_INFINITY : parsed;
}
return Number.NEGATIVE_INFINITY;
};
const matches = rows
.filter(r => r && r.title === title && typeof r.id === 'string' && isValidId(r.id))
.sort((a, b) => {
const ta = timestamp(a.updated);
const tb = timestamp(b.updated);
if (ta === tb) return 0;
return tb - ta;
});
if (matches.length === 0) process.exit(1);
process.stdout.write(matches[0].id);
});
`.trim();
return script.replace(/\n/g, " ").replace(/\s+/g, " ");
}
function buildContinueSessionCommand(sessionTitle: string, sharedOptions: string[]): string {
const script = buildSessionLookupScript();
const options = sharedOptions.length > 0 ? ` ${sharedOptions.join(" ")}` : "";
return [
`SES_ID=$(opencode session list --format json | node -e ${shellEscape(script)} ${shellEscape(sessionTitle)})`,
'[ -n "$SES_ID" ]',
`exec opencode --session "$SES_ID"${options}`,
].join(" && ");
}
// =============================================================================
// Plugin Manifest
// =============================================================================
@ -138,13 +96,25 @@ function createOpenCodeAgent(): Agent {
if (!existingSessionId) {
const runOptions = ["--title", shellEscape(`AO:${config.sessionId}`), ...sharedOptions];
const runCommand = promptValue
? ["opencode", "run", ...runOptions, promptValue].join(" ")
: ["opencode", "run", ...runOptions, "--command", "true"].join(" ");
const continueCommand = buildContinueSessionCommand(
`AO:${config.sessionId}`,
sharedOptions,
? ["opencode", "run", "--format", "json", ...runOptions, promptValue].join(" ")
: ["opencode", "run", "--format", "json", ...runOptions, "--command", "true"].join(" ");
const captureSessionId = [
"node",
"-e",
shellEscape(
"let buf='';process.stdin.on('data',c=>buf+=c).on('end',()=>{const lines=buf.toString().split('\\n');const isValidId=id=>typeof id==='string'&&/^ses_[A-Za-z0-9_-]+$/.test(id);for(const line of lines){if(!line.trim())continue;try{const evt=JSON.parse(line);if(evt.type==='step_start'&&isValidId(evt.session_id)){process.stdout.write(evt.session_id);process.exit(0);}}catch{}}process.exit(1);})",
),
].join(" ");
const fallbackSessionId = `opencode session list --format json | node -e ${shellEscape("let input='';process.stdin.on('data',c=>input+=c).on('end',()=>{const title=process.argv[1];let rows;try{rows=JSON.parse(input)}catch{process.exit(1)};if(!Array.isArray(rows))process.exit(1);const isValidId=id=>/^ses_[A-Za-z0-9_-]+$/.test(id);const timestamp=v=>{if(typeof v==='number'&&Number.isFinite(v))return v;if(typeof v==='string'){const p=Date.parse(v);return Number.isNaN(p)?Number.NEGATIVE_INFINITY:p;}return Number.NEGATIVE_INFINITY;};const matches=rows.filter(r=>r&&r.title===title&&typeof r.id==='string'&&isValidId(r.id)).sort((a,b)=>timestamp(b.updated)-timestamp(a.updated));if(matches.length===0)process.exit(1);process.stdout.write(matches[0].id);});")} ${shellEscape(`AO:${config.sessionId}`)}`;
const sessionIdCapture = `"$( { ${runCommand} | ${captureSessionId}; } || ${fallbackSessionId} )"`;
const continueCommand = ["opencode", "--session", sessionIdCapture, ...sharedOptions].join(
" ",
);
return `${runCommand} && ${continueCommand}`;
return `exec ${continueCommand}`;
}
if (promptValue) {