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 fails to recover session history after a transient read error #59542
Resuming a populated session can return to home or leave an empty transcript after a transient read error. An initial session metadata error navigates home; an initial message-history error is ignored. Neither read is retried after the failure, even when subsequent requests would succeed. The stored messages remain intact.
The TUI should recover when the server becomes available, or show a loading error.
Plugins
None (isolated reproduction).
OpenCode version
2.1.4 (dev at d5bb993278)
Steps to reproduce
Resume a populated session with opencode -s <session-id>, allowing the CLI's preflight session lookup to succeed.
Return HTTP 503 for the TUI's next metadata request (/api/session/<id>) or initial history request (/api/session/<id>/message), then restore normal responses.
Keep the connection open: the TUI returns home in the first case, or stays empty in the second.
Reproduced with controlled HTTP failures; no concurrent writer is needed.
Screenshot
Synthetic session; one initial history HTTP 503, then normal responses.
Thanks for the detailed report. I reproduced the empty-transcript case on 2.1.4, and it still happens on the latest dev (f993a89).
What I did:
Created a session with one prompt and reply (simulated model).
Resumed it with opencode -s <id> through a small HTTP proxy.
The proxy returned HTTP 503 once for the TUI's first GET /api/session/<id>/message?limit=20&order=desc, then forwarded everything normally.
The resumed TUI shows the session tab and prompt, but the transcript stays empty. The message list is never requested again and no error is shown, while the server still has the messages. Without the 503, the history renders normally. The recording below shows the empty case, and the script below reproduces it (set MODE=history).
The other case, where a single 503 on the TUI's own GET /api/session/<id> (after the CLI's lookup) sends you home, did not reproduce in my setup: the TUI stayed on the session and showed its history.
Related: #36582 (broader audit of TUI API failures being swallowed) and #39419 (a closed, unmerged PR that would have navigated home only when the session is actually missing).
recording.mp4Reproduction script (drive.ts)
// Repro for anomalyco/opencode#59542: one transient HTTP 503 on the resumed// session's initial history (MODE=history, default) or metadata (MODE=metadata)// read is never retried. Requires opencode-wrapper.sh next to this file.import{mkdirSync,rmSync,writeFileSync,readFileSync}from"node:fs"import{Effect}from"effect"import{Llm,OpenCodeDriver}from"opencode-drive"constMODE=process.env.MODE??"history"constD="/tmp/opencode/repro-59542"rmSync(D,{recursive: true,force: true})mkdirSync(D,{recursive: true})letsessionID=""letmetadataReads=0lethistoryFailed=falseconstlog: string[]=[]constproxy=Bun.serve({port: 0,idleTimeout: 0,asyncfetch(req){consturl=newURL(req.url)constupstream=readFileSync(`${D}/upstream`,"utf8").trim()letfail=falseif(sessionID&&req.method==="GET"){if(url.pathname===`/api/session/${sessionID}`){metadataReads++// read 1 = CLI preflight lookup; read 2 = the TUI's own metadata readif(MODE==="metadata"&&metadataReads===2)fail=true}if(MODE==="history"&&!historyFailed&&url.pathname===`/api/session/${sessionID}/message`){historyFailed=truefail=true}}log.push(`${fail ? "503" : "ok "}${req.method}${url.pathname}${url.search}`)if(fail)returnnewResponse("Service Unavailable",{status: 503})constheaders=newHeaders(req.headers)headers.delete("host")constres=awaitfetch(upstream.replace(/\/$/,"")+url.pathname+url.search,{method: req.method,
headers,body: req.method==="GET"||req.method==="HEAD" ? undefined : awaitreq.arrayBuffer(),redirect: "manual",decompress: false,}asRequestInit)returnnewResponse(res.body,{status: res.status,headers: res.headers})},})writeFileSync(`${D}/proxy`,`http://127.0.0.1:${proxy.port}`)exportdefaultOpenCodeDriver.use({opencode: {command: ["/workspace/repro/opencode-wrapper.sh"]},project: {git: true},tui: {viewport: {cols: 110,rows: 32}},},({ ui, llm, opencode, tuis })=>Effect.gen(function*(){// 1. Populate a session through the normal TUI.// the title request is served first, then the replyyield*llm.queue(Llm.text("Seeded session"))yield*llm.queue(Llm.text("PERSISTED-ANSWER: the stored history is intact."))yield*ui.submit("PERSISTED-QUESTION: remember this")yield*ui.waitFor("PERSISTED-ANSWER")yield*Effect.sleep("3 seconds")constlist: any=yield*opencode.session.list()constitems: any[]=list.items??list.data??list.sessions??listsessionID=items[0].idyield*Effect.log(`session ${sessionID}`)// 2. Resume it in a new TUI (`opencode -s <id>`) through the fault proxy.writeFileSync(`${D}/resume`,sessionID)constresumed=yield*tuis.launch("resumed",{recording: true,viewport: {cols: 110,rows: 32}})yield*Effect.sleep("8 seconds")// proxy serves everything normally after the single 503yield*resumed.ui.screenshot(`resumed-${MODE}`)constframe: any=yield*resumed.ui.capture()consttext=JSON.stringify(frame)consthas=text.includes("PERSISTED-QUESTION")&&text.includes("PERSISTED-ANSWER")yield*Effect.log(`MODE=${MODE} transcript shows stored history: ${has}`)yield*Effect.log(`recording: ${resumed.recording?.path}`)yield*Effect.log("proxy log:\n"+log.join("\n"))// Confirm the history is still stored server-side.constmsgs: any=yield*opencode.message.list({ sessionID }asany)yield*Effect.log(`stored messages: ${JSON.stringify(msgs).includes("PERSISTED-QUESTION")}`)}),)
Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH.
I hit the same bug on v2.2.5, macOS, when the shared background service restarts with about 25 TUI clients attached. The clients reconnect, and a transient failure during the session re-sync on connected (ECONNRESET / ECONNREFUSED) closes the tabs or sends them home.
At 2026-11-01T17:47:53Z, about 25 clients logged event stream disconnected with ECONNRESET within 3 ms, followed a few seconds later by ECONNREFUSED and Timed out connecting to server. The toast is not logged, so I cannot match a specific closed tab to that timestamp.
My report is #61243; PR #59543 retries transient session reads in the same block and would cover this reconnect case too.
We hit this in practice and found a natural trigger, with no injected errors: several TUIs reconnecting together to a session with many subagent sessions.
Reproduction: one root session with 200 child sessions created through the API, 5 TUIs attached to it, the server and all TUIs paused together for 75 s and then resumed. macOS 27.2 arm64, kern.ipc.somaxconn 128.
With 0 children, all TUIs are kept. Each PR fixes this case on its own: #60925 removes the per-subagent read burst, #59543 survives the failed read. We did not trace which request fails.
Rerun recipe
# isolated HOME/XDG, cwd outside any repo, no providersexport OPENCODE_SERVER_PASSWORD=repro # TUIs use OPENCODE_PASSWORD=repro
opencode serve --hostname 127.0.0.1 --port 5196 & SERVER=$!
A="-u opencode:repro -H content-type:application/json"
ROOT=$(curl -s $A -d '{"title":"root"}' http://127.0.0.1:5196/api/session | jq -r .data.id)foriin$(seq 200);do# sequential: a parallel burst here hits the same backlog
curl -s $A -d "{\"parentID\":\"$ROOT\",\"title\":\"child $i\"}" http://127.0.0.1:5196/api/session >/dev/null
done
tmux -L repro new-session -d -s r -x 220 -y 51
foriin 1 2 3 4 5;do# Don't make opencode the pane command itself (or `exec` it): tmux sends SIGCONT# to a stopped pane process at once. Keep a shell in between and stop only its child.
tmux -L repro new-window -d -t r -n tui-$i \
"sh -c 'OPENCODE_PASSWORD=repro opencode --server http://127.0.0.1:5196 -s $ROOT; true'"done
sleep 15 # all 5 show the session
TUIS=$(for p in$(tmux -L repro list-panes -s -t r -F '#{pane_pid}');do pgrep -P $p;done| xargs)kill -STOP $SERVER$TUIS; ps -o stat= -p "$(echo $SERVER$TUIS| tr '' ,)"# every line starts with T
sleep 75
kill -CONT $SERVER;kill -CONT $TUIS
sleep 60
foriin 1 2 3 4 5;do tmux -L repro capture-pane -p -t r:tui-$i| head -1;done# root title in the tab row = kept, a blank line = the session tab is gone
Activity
opencode-agent commentedon Oct 27, 2026
Thanks for the detailed report. I reproduced the empty-transcript case on 2.1.4, and it still happens on the latest
dev(f993a89).What I did:
opencode -s <id>through a small HTTP proxy.GET /api/session/<id>/message?limit=20&order=desc, then forwarded everything normally.The resumed TUI shows the session tab and prompt, but the transcript stays empty. The message list is never requested again and no error is shown, while the server still has the messages. Without the 503, the history renders normally. The recording below shows the empty case, and the script below reproduces it (set
MODE=history).The other case, where a single 503 on the TUI's own
GET /api/session/<id>(after the CLI's lookup) sends you home, did not reproduce in my setup: the TUI stayed on the session and showed its history.Related: #36582 (broader audit of TUI API failures being swallowed) and #39419 (a closed, unmerged PR that would have navigated home only when the session is actually missing).
recording.mp4
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH.RuriRyan commentedon Nov 2, 2026
I hit the same bug on v2.2.5, macOS, when the shared background service restarts with about 25 TUI clients attached. The clients reconnect, and a transient failure during the session re-sync on
connected(ECONNRESET/ECONNREFUSED) closes the tabs or sends them home.At
2026-11-01T17:47:53Z, about 25 clients loggedevent stream disconnectedwithECONNRESETwithin 3 ms, followed a few seconds later byECONNREFUSEDandTimed out connecting to server. The toast is not logged, so I cannot match a specific closed tab to that timestamp.My report is #61243; PR #59543 retries transient session reads in the same block and would cover this reconnect case too.
romainfd-arrakis commentedon Nov 3, 2026
Thanks to both of you for the fixes here.
We hit this in practice and found a natural trigger, with no injected errors: several TUIs reconnecting together to a session with many subagent sessions.
Reproduction: one root session with 200 child sessions created through the API, 5 TUIs attached to it, the server and all TUIs paused together for 75 s and then resumed. macOS 27.2 arm64,
kern.ipc.somaxconn128.67139d682767139d6827+ #59543 (d80f0fab1c)67139d6827+ #60925 (931ed78b26)With 0 children, all TUIs are kept. Each PR fixes this case on its own: #60925 removes the per-subagent read burst, #59543 survives the failed read. We did not trace which request fails.
Rerun recipe