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
This commit is contained in:
Harsh 2026-03-06 18:44:13 +05:30 committed by Harsh
parent 6768c931cb
commit 42cc0cfa1a
2 changed files with 25 additions and 6 deletions

View File

@ -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({

View File

@ -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) {