diff --git a/packages/core/src/__tests__/session-manager.test.ts b/packages/core/src/__tests__/session-manager.test.ts index 48dca8269..ed0649830 100644 --- a/packages/core/src/__tests__/session-manager.test.ts +++ b/packages/core/src/__tests__/session-manager.test.ts @@ -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, diff --git a/packages/core/src/session-manager.ts b/packages/core/src/session-manager.ts index 6a959545f..dfb3b855c 100644 --- a/packages/core/src/session-manager.ts +++ b/packages/core/src/session-manager.ts @@ -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(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 = diff --git a/packages/integration-tests/src/agent-opencode.integration.test.ts b/packages/integration-tests/src/agent-opencode.integration.test.ts index 11d6ef56a..ce2e46d87 100644 --- a/packages/integration-tests/src/agent-opencode.integration.test.ts +++ b/packages/integration-tests/src/agent-opencode.integration.test.ts @@ -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", () => { diff --git a/packages/plugins/agent-opencode/src/index.test.ts b/packages/plugins/agent-opencode/src/index.test.ts index 9a6d2dead..15cbfe04e 100644 --- a/packages/plugins/agent-opencode/src/index.test.ts +++ b/packages/plugins/agent-opencode/src/index.test.ts @@ -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({ diff --git a/packages/plugins/agent-opencode/src/index.ts b/packages/plugins/agent-opencode/src/index.ts index 3e047778b..4d80dac34 100644 --- a/packages/plugins/agent-opencode/src/index.ts +++ b/packages/plugins/agent-opencode/src/index.ts @@ -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) {