You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Session recovery loses client environment and inherits another agent identity #51784
Local session environments set through session.environment are held only in a process-local map. After a server restart, automatic recovery resumes the durable execution but local shells fall back to the new server process environment. Starting that server from agent A can therefore run recovered agent B shells with A’s identity and workspace variables.
Confirmed in packages/core/src/session/environment.ts and shell.ts on v2 (39e1ce5). The live-process claim guard in #51215 does not cover a dead owner.
OpenCode version
2.0.15-based fork; same environment storage/fallback in current v2.
Plugins
Paseo bridge in the original server; no bridge in the recovering background service. The failure occurs in core shell environment selection.
Steps to reproduce
Bind different client environments to two sessions in one directory.
Interrupt their server while execution is active.
Start a successor with a different agent identity in its process environment.
Recover the sessions before their clients reconnect. Shells use the successor’s identity instead of waiting for their own binding.
Expected: recovery must wait for each previously client-bound environment to be reattached; never substitute the server environment. A durable presence marker can enforce this without storing environment values or secrets.
Related to #38897, but this reproduction starts after explicit per-session environment binding already exists: recovery runs before that in-memory binding is restored. #51786 retains only the requirement and waits for reattachment. #44914 concerns MCP configuration interpolation; this patch is limited to local session shells and recovery.
Thanks for the detailed report. I was able to reproduce this.
Setup: the server ran with AGENT_ID=A in its own environment. A session was bound to AGENT_ID=B with session.environment.
The session's first shell command logged AGENT_ID=B.
I stopped the server while that command was still running and started a new one. The client did not reattach its environment.
Recovery resumed the turn, and the next shell command logged AGENT_ID=A, the server's own environment instead of the session's.
This happens on 2.0.15 and still happens on the latest dev (86ce19d). The script below reproduces it with a simulated model. I stopped the server normally rather than crashing it, which recovery handles the same way.
As you pointed out, the per-session environment is only kept in memory, and shells fall back to the server's own environment when it's missing (packages/core/src/shell.ts). #51215 only skips claims held by a process that is still alive, so it doesn't cover this case. I've passed it on to the maintainers.
Reproduction script (drive.ts)
// Repro for #51784: after a server restart, a recovered session's shells use the// new server process environment instead of the environment bound via session.environment.// Run with: AGENT_ID=A OPENCODE_DRIVE_DB=restart.sqlite opencode-drive start --name r51784 --script drive.ts (add --dev <checkout> for source)import{Effect}from"effect"import{readFileSync,existsSync,rmSync}from"node:fs"import{defineScript,Llm}from"opencode-drive"constLOG="/tmp/opencode/agent-51784.log"constread=()=>(existsSync(LOG) ? readFileSync(LOG,"utf8") : "")constwaitFor=(needle: string,ms: number)=>Effect.gen(function*(){constend=Date.now()+mswhile(Date.now()<end){if(read().includes(needle))returntrueyield*Effect.sleep(250)}returnfalse})exportdefaultdefineScript({launch: "manual",run: ({ server, llm })=>Effect.gen(function*(){rmSync(LOG,{force: true})console.log("server process AGENT_ID =",process.env.AGENT_ID)constoc=yield*server.launch()constsession=yield*oc.session.create({title: "agent B session"})// Client binds its own environment (agent B) to the session.yield*oc.session.environment({sessionID: session.id,variables: {PATH: process.env.PATH??"/usr/bin:/bin",HOME: process.env.HOME??"/root",AGENT_ID: "B"},})yield*llm.queue(Llm.toolCall({index: 0,id: "call_1",name: "shell",input: {command: `echo "before-restart AGENT_ID=$AGENT_ID" >> ${LOG}; sleep 120`,description: "long"}}),Llm.finish("tool-calls"),)yield*oc.session.prompt({sessionID: session.id,text: "run the long command"})if(!(yield*waitFor("before-restart",20000)))thrownewError("first shell never ran")console.log("LOG before restart:\n"+read())// Server dies while the execution is active; successor starts (with AGENT_ID=A in its env).yield*server.kill()yield*llm.queue(Llm.toolCall({index: 0,id: "call_2",name: "shell",input: {command: `echo "after-recovery AGENT_ID=$AGENT_ID" >> ${LOG}`,description: "check"}}),Llm.finish("tool-calls"),)yield*llm.queue(Llm.text("done"))yield*server.launch()// Client has NOT reattached its environment.constok=yield*waitFor("after-recovery",30000)console.log("LOG after recovery:\n"+read())if(!ok)thrownewError("recovered shell did not run")}),})
Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH.
Activity
github-actions commentedon Sep 28, 2026
This issue might be a duplicate of existing issues. Please check:
gszep commentedon Sep 28, 2026
Related to #38897, but this reproduction starts after explicit per-session environment binding already exists: recovery runs before that in-memory binding is restored. #51786 retains only the requirement and waits for reattachment. #44914 concerns MCP configuration interpolation; this patch is limited to local session shells and recovery.
opencodejr commentedon Oct 28, 2026
Thanks for the detailed report. I was able to reproduce this.
Setup: the server ran with
AGENT_ID=Ain its own environment. A session was bound toAGENT_ID=Bwithsession.environment.AGENT_ID=B.AGENT_ID=A, the server's own environment instead of the session's.This happens on 2.0.15 and still happens on the latest
dev(86ce19d). The script below reproduces it with a simulated model. I stopped the server normally rather than crashing it, which recovery handles the same way.As you pointed out, the per-session environment is only kept in memory, and shells fall back to the server's own environment when it's missing (
packages/core/src/shell.ts). #51215 only skips claims held by a process that is still alive, so it doesn't cover this case. I've passed it on to the maintainers.Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH.