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
Reverting a message restores the whole worktree and discards other sessions' uncommitted changes #54026
Reverting a message restores the entire shared working tree to the snapshot
taken at that message, silently discarding uncommitted changes made by other
sessions. The revert is not scoped to the files the reverted message changed.
Expected Behavior
Reverting a message should only restore the files that message (and any later
ones) changed. It should not touch files the message never wrote, and it should
warn before discarding uncommitted changes from other sessions.
Actual Behavior
The whole working tree was restored to the snapshot at the reverted message.
Six files unrelated to the reverted message were overwritten (undoing Session A's
edits) and a test file was deleted. No warning was shown.
Additional Context
Server log (UTC; local is +02:00): timestamp=2026-10-08T21:25:40.821Z level=INFO run=0b819a15 message=session.revert.stage sessionID=ses_f11c69a3fffesDjJhY4qNeB0KY messageID=msg_11c7818fc0012kExKO5arsoAsj files=undefined http.span=1 role=server
The overwritten files' mtimes fell ~0.1s after that line (23:25:40.9-41.1
local). Those mtimes have since been overwritten by restoring the files.
Revert API surface: POST /api/session/:sessionID/revert/stage, POST /api/session/:sessionID/revert/commit, DELETE /api/session/:sessionID/revert. The stage endpoint accepts an
optional files list; the client call passed none, so the restore was
unscoped.
The reverted message id (msg_11c7818fc0012kExKO5arsoAsj) is no longer present
in the session store after the revert.
Session B is a plan session; every assistant reply in it is agent=plan, and
no edit/write tool appears in its stored messages. It did run shell and subagent tools, so whether the reverted message itself wrote anything could
not be confirmed — but the unrelated files it rolled back prove the restore
was not scoped to that message.
Frequency: observed once, but the mechanism is deterministic.
Plugins
opencode-mem
OpenCode version
2.0.23
Steps to reproduce
Open two sessions on the same repository/worktree:
Session A: working normally, with uncommitted edits.
Session B: a plan-mode session.
In Session B, send a message by mistake, then revert that message via the
client UI so it does not pollute the context.
Thanks for the detailed report. I was able to reproduce this on 2.0.23, and it still happens on the latest v2 (b5c43a44).
What I ran (script below):
A session runs a step that only calls a read-only shell command (ls).
While that step is running, something else (here a separate writer standing in for your Session A) changes a tracked file and creates a new file in the same worktree.
Reverting the message through POST /session/:id/revert/stage, with no files (the same call the web UI makes), puts the edited file back to its old content and deletes the new file, with no warning.
The revert isn't restoring the whole worktree. It only restores the files recorded for each step. The problem is that this file list comes from comparing snapshots of the whole worktree taken at the start and end of the step, so anything another session changed during that step gets counted as this session's change and is then rolled back.
Until this is fixed, the safest option is not to revert in one session while another session is editing the same worktree, or to give each session its own git worktree.
Reproduction script (drive.ts)
// Repro for #54026: reverting a message in session B rolls back edits that// another session (or any writer) made to the shared worktree while B's step ran.import{Effect}from"effect"import*asfsfrom"node:fs"import*aspathfrom"node:path"import{Llm,OpenCodeDriver}from"opencode-drive"exportdefaultOpenCodeDriver.use({
...(process.env.OC_DEV ? {opencode: {dev: process.env.OC_DEV}} : {}),project: {git: true,files: {"a.txt": "session A original\n","untouched.txt": "never changed\n",},},// Drive disables snapshots by default; OpenCode enables them by default.config: {snapshots: true}asany,tools: ["shell"],tui: {recording: true},},({ ui, llm, tools, opencode, artifacts })=>Effect.gen(function*(){constdir=path.join(artifacts,"files")constshells=yield*tools.control("shell")// Session B: a step that only runs a read-only shell command.yield*llm.queue(Llm.toolCall({index: 0,id: "call_ls",name: "shell",input: {command: "ls",description: "list"}}),Llm.finish("tool-calls"),)yield*llm.queue(Llm.text("Done listing files."))yield*ui.submit("list the files (sent by mistake)")constshell=yield*shells.take("call_ls")// Meanwhile "session A" edits a tracked file and creates a new one.fs.writeFileSync(path.join(dir,"a.txt"),"session A EDITED (uncommitted work)\n")fs.writeFileSync(path.join(dir,"a.test.ts"),"// new test written by session A\n")yield*Effect.log("before step end: a.txt="+JSON.stringify(fs.readFileSync(path.join(dir,"a.txt"),"utf8")))yield*shell.succeed({output: "a.txt\nuntouched.txt\n",exit: 0})yield*ui.waitFor("Done listing files.")yield*Effect.sleep(1000)constsessions: any=yield*opencode.session.list()constsession=(sessions.items??sessions.data??sessions)[0]constmessages: any=yield*opencode.message.list({sessionID: session.id}asany)constlist: any[]=messages.items??messages.data??messagesfor(constmoflist)yield*Effect.log(`msg ${m.id}${m.type} snapshot=${JSON.stringify(m.snapshot)}`)constuser=list.find((m)=>m.type==="user")yield*Effect.log(`before revert: a.txt=${JSON.stringify(fs.readFileSync(path.join(dir,"a.txt"),"utf8"))} a.test.ts exists=${fs.existsSync(path.join(dir,"a.test.ts"))}`)// Same call the web UI makes: stage a revert of the mistaken message, no `files` flag.constrevert=yield*opencode.session.revert.stage({sessionID: session.id,messageID: user.id}asany)yield*Effect.log("revert staged: "+JSON.stringify(revert))yield*Effect.sleep(1500)constafter=fs.readFileSync(path.join(dir,"a.txt"),"utf8")constexists=fs.existsSync(path.join(dir,"a.test.ts"))yield*Effect.log(`AFTER revert: a.txt=${JSON.stringify(after)} a.test.ts exists=${exists}`)yield*ui.screenshot("after-revert")yield*Effect.log(after.includes("EDITED")&&exists ? "RESULT: other session's work preserved" : "RESULT: BUG - other session's work discarded")}),)
Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.
I’d like to investigate and implement a fix for this. I plan to reproduce the race between an active turn, an external file edit, and /undo, then add a regression test ensuring user-authored changes are not reverted. I’ll first inspect the undo snapshot/patch tracking logic and propose the smallest conflict-safe fix.
Activity
opencode-agent commentedon Oct 8, 2026
Thanks for the detailed report. I was able to reproduce this on 2.0.23, and it still happens on the latest
v2(b5c43a44).What I ran (script below):
shellcommand (ls).POST /session/:id/revert/stage, with nofiles(the same call the web UI makes), puts the edited file back to its old content and deletes the new file, with no warning.The revert isn't restoring the whole worktree. It only restores the files recorded for each step. The problem is that this file list comes from comparing snapshots of the whole worktree taken at the start and end of the step, so anything another session changed during that step gets counted as this session's change and is then rolled back.
Related issues:
Until this is fixed, the safest option is not to revert in one session while another session is editing the same worktree, or to give each session its own git worktree.
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.AtomicFS commentedon Oct 20, 2026
Related to #55121, where a LLM only reading a file will cause
undocommand to revert user-made changes.Steps to reproduce:
Jackdrakes commentedon Oct 25, 2026
I’d like to investigate and implement a fix for this. I plan to reproduce the race between an active turn, an external file edit, and /undo, then add a regression test ensuring user-authored changes are not reverted. I’ll first inspect the undo snapshot/patch tracking logic and propose the smallest conflict-safe fix.