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
tui: /undo followed by resend keeps the reverted turn in context and duplicates it #54150
After /undo, I edit the restored prompt and resubmit. The reverted turn disappears from the transcript but is still sent to the model: the next reply references content from the erased turn. The erased prompt/turn also shows up duplicated in the session store (same message text and identical time.created), so the model can effectively see the turn twice.
This looks timing-sensitive: it happens when /undo is used right after sending, while the turn may still be finishing.
Steps to reproduce
In a session, send a message and let the assistant reply.
Press /undo, edit the restored prompt, submit again.
Inspect the session: the previous assistant output is gone from the UI, but the new reply refers to it; the underlying messages still contain the old turn (sometimes two copies with the same time.created).
Expected behavior
Committing the staged rollback should remove the reverted turn from what is sent to the model, and resubmitting should not create duplicate messages.
Actual behavior
Reverted turns remain in the model context, and the erased prompt/turn is left duplicated in the message store.
Environment
OpenCode version: 2.0.25
OS: macOS (Darwin 27.2.0, arm64)
Terminal: VS Code integrated terminal (TERM_PROGRAM=vscode, TERM=xterm-256color)
Thanks for the report. I could reproduce this, but only in one timing window.
With the simulated model on 2.0.25, I sent a prompt and ran /undo immediately, before the reply had started streaming. Then I cleared the restored prompt and sent a replacement. The prompt came back into the input, but the turn was not reverted: the original prompt and its reply were still stored in the session and included in the next model request. It still happens on the latest v2 (5cfe3b42). If /undo runs once the reply is streaming or finished, the turn is removed correctly.
In my runs the turn stayed visible in the transcript, and I did not see two copies with the same time.created. If you resend the restored prompt unchanged, it would likely be stored twice. The likely cause is that /undo right after sending cancels the prompt as if it were still waiting to be delivered, even though it has already started running.
The recording below shows it on 2.0.25, and the script below reproduces it.
Related: #37751 (a staged revert racing with the running session), #44357 (fast send after /undo), #41428 (duplicate prompt rows in the app store).
recording.mp4Reproduction script (drive.ts)
// Repro for #54150: /undo right after sending, then edit + resend.// MODE=immediate (default): /undo right after submitting. Other modes: streaming, done, justdone.// Checks whether the next model request still contains the reverted turn.import{Effect,Stream}from"effect"import{Llm,OpenCodeDriver}from"opencode-drive"constrequests: string[]=[]exportdefaultOpenCodeDriver.use({
...(process.env.OC_DEV ? {opencode: {dev: process.env.OC_DEV}} : {}),project: {git: true,files: {"README.md": "hello\n"}},tui: {recording: true,keypressOverlay: true},},({ ui, llm, opencode })=>Effect.gen(function*(){yield*llm.serve((request,index)=>{constbody=JSON.stringify(request)requests.push(body)if(index===0)// a slow reply, so /undo lands while the turn is still runningreturnStream.make(Llm.text("BRAVO first answer, this is the reply that will be undone. ".repeat(4),{delay: process.env.MODE==="justdone" ? 30 : 60,chunkSize: 8,}),)returnStream.make(Llm.text(`Reply number ${index+1}`))})constmode=process.env.MODE??"immediate"yield*ui.submit("ALPHA first prompt")if(mode==="streaming")yield*ui.waitFor("BRAVO")if(mode==="done")yield*ui.waitFor("Simulated Model ·")if(mode==="justdone")yield*Effect.sleep(1150)// undo while the assistant turn is still streamingyield*ui.submit("/undo")yield*Effect.sleep(1500)yield*ui.screenshot("after-undo")// clear the restored prompt and send a replacementyield*ui.press("u",{ctrl: true})yield*ui.submit("CHARLIE replacement prompt")yield*ui.waitFor("Reply number")yield*Effect.sleep(1500)yield*ui.screenshot("after-resend")constlast=requests[requests.length-1]??""constreport={totalRequests: requests.length,lastContainsAlpha: last.includes("ALPHA first prompt"),lastContainsBravo: last.includes("BRAVO first answer"),lastContainsCharlie: last.includes("CHARLIE replacement prompt"),alphaCount: last.split("ALPHA first prompt").length-1,}yield*Effect.log(`REPORT ${JSON.stringify(report)}`)// dump what the server stores for the sessionconstclient=opencodeasanyconstsessions: any=yield*(client.session.list({})asEffect.Effect<any>)yield*Effect.log(`SESSIONS ${JSON.stringify(sessions).slice(0,600)}`)constlist: any[]=sessions?.data??sessions?.items??sessionsconstid=Array.isArray(list) ? list[0]?.id : undefinedif(id){constmsgs: any=yield*(client.message.list({sessionID: id})asEffect.Effect<any>)yield*Effect.log(`MESSAGES ${JSON.stringify(msgs)}`)}}),)
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.
Independent datapoint on the duplication half of this bug, from a released build (2.1.4, cut 2026-10-23).
While auditing a session store I found a signature that looks exactly like this race:
one session contained the same user prompt stored 11 times (plus two more groups of 3x each of a near-identical re-send), all after repeated /undo + resend cycles;
each stored copy was a successful re-send, i.e. the turn that /undo was supposed to remove was still there, so the model saw it once per attempt.
I could not drive the TUI deterministically, so I am not claiming a fresh repro — just that the persisted shape (duplicated admitted turns after undo/resend) matches the analysis above, and that it is observable on 2.1.4.
Also worth noting for release planning: none of the fixes are in a released build. #55394 and #55403 are still open, #54163 was closed unmerged, and no commit on main references this issue.
A cheap guard would already help: refuse (or defer) /undo while the turn is admitted/running, and tell the user, rather than silently leaving the turn in context.
Activity
opencode-agent commentedon Oct 9, 2026
Thanks for the report. I could reproduce this, but only in one timing window.
With the simulated model on 2.0.25, I sent a prompt and ran
/undoimmediately, before the reply had started streaming. Then I cleared the restored prompt and sent a replacement. The prompt came back into the input, but the turn was not reverted: the original prompt and its reply were still stored in the session and included in the next model request. It still happens on the latestv2(5cfe3b42). If/undoruns once the reply is streaming or finished, the turn is removed correctly.In my runs the turn stayed visible in the transcript, and I did not see two copies with the same
time.created. If you resend the restored prompt unchanged, it would likely be stored twice. The likely cause is that/undoright after sending cancels the prompt as if it were still waiting to be delivered, even though it has already started running.The recording below shows it on 2.0.25, and the script below reproduces it.
Related: #37751 (a staged revert racing with the running session), #44357 (fast send after
/undo), #41428 (duplicate prompt rows in the app store).recording.mp4
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.fix(tui): undo prompts the server already accepted
myhsia commentedon Oct 28, 2026
Independent datapoint on the duplication half of this bug, from a released build (2.1.4, cut 2026-10-23).
While auditing a session store I found a signature that looks exactly like this race:
/undo+ resend cycles;/undowas supposed to remove was still there, so the model saw it once per attempt.I could not drive the TUI deterministically, so I am not claiming a fresh repro — just that the persisted shape (duplicated admitted turns after undo/resend) matches the analysis above, and that it is observable on 2.1.4.
Also worth noting for release planning: none of the fixes are in a released build. #55394 and #55403 are still open, #54163 was closed unmerged, and no commit on
mainreferences this issue.A cheap guard would already help: refuse (or defer)
/undowhile the turn is admitted/running, and tell the user, rather than silently leaving the turn in context.fix(tui): undo prompts the server already accepted
fix(tui): undo prompts the server already accepted
fix(tui): undo prompts the server already accepted
fix(tui): undo prompts the server already accepted