diff --git a/packages/core/src/session-manager.ts b/packages/core/src/session-manager.ts index 479b445b0..6396f50d7 100644 --- a/packages/core/src/session-manager.ts +++ b/packages/core/src/session-manager.ts @@ -931,6 +931,22 @@ export function createSessionManager(deps: SessionManagerDeps): SessionManager { } } + if (!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 = plugins.agent.name === "opencode" && orchestratorSessionStrategy === "reuse" ? await resolveOpenCodeSessionReuse({ diff --git a/packages/plugins/agent-opencode/src/index.ts b/packages/plugins/agent-opencode/src/index.ts index 7f5f6f974..feb2394dc 100644 --- a/packages/plugins/agent-opencode/src/index.ts +++ b/packages/plugins/agent-opencode/src/index.ts @@ -98,14 +98,17 @@ 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 continueSession = `"$(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 matches=rows.filter((r)=>r&&r.title===title&&typeof r.id==='string');if(matches.length===0)process.exit(1);process.stdout.write(matches[0].id);});")} ${shellEscape(`AO:${config.sessionId}`)})"`; - const continueCommand = ["opencode", "--session", continueSession, ...sharedOptions].join( + + const captureSessionId = `"$(node -e ${shellEscape("let buf='';process.stdin.on('data',c=>buf+=c).on('end',()=>{const lines=buf.toString().split('\\n');for(const line of lines){if(!line.trim())continue;try{const evt=JSON.parse(line);if(evt.type==='step_start'&&evt.session_id){process.stdout.write(evt.session_id);process.exit(0);}}}process.exit(1);})")})"`; + + 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 matches=rows.filter(r=>r&&r.title===title&&typeof r.id==='string').sort((a,b)=>new Date(b.updated||0)-new Date(a.updated||0));if(matches.length===0)process.exit(1);process.stdout.write(matches[0].id);});")} ${shellEscape(`AO:${config.sessionId}`)})"`; + + const sessionIdCapture = `"$( { ${promptValue ? ["opencode", "run", "--format", "json", ...runOptions, promptValue].join(" ") : ["opencode", "run", "--format", "json", ...runOptions, "--command", "true"].join(" ")} | ${captureSessionId}; } || echo ${fallbackSessionId} )"`; + + const continueCommand = ["opencode", "--session", sessionIdCapture, ...sharedOptions].join( " ", ); - return `${runCommand} && exec ${continueCommand}`; + return `${continueCommand}`; } if (promptValue) {