Before submitting
Area
apps/server
Summary
On the OpenCode provider, t3code does not durably bind a thread to its OpenCode ses_… id, and the adapter cannot resume. When the in-memory binding is lost — most plausibly when ProviderSessionReaper reaps the session after ~30 min idle (e.g. after a long sub-agent turn), or on an app/server restart — the next follow-up in the same visible thread is sent to a brand-new, empty OpenCode session. t3code keeps rendering the prior conversation from its own projection DB, so the user still sees the history, but the model has no context and behaves as if a new session started.
Steps to reproduce
- Start a thread on the OpenCode provider; have a real exchange (ideally one that spawns sub-agents / runs a while, so the session then goes idle).
- Let it sit idle past the reaper threshold (~30 min), or restart the app, between turns.
- Send a follow-up message in the same thread.
- The agent answers with no awareness of the earlier conversation that is still visible in the chat.
Expected behavior
A follow-up in an existing thread continues the same OpenCode session with full prior context — or, if the session genuinely can't be resumed, fails with a clear explicit error rather than silently starting an empty session under the same visible thread.
Actual behavior
The follow-up runs in a new, empty OpenCode session. No hang and no error — the model just has no context. The visible chat (t3code's own projection) and the OpenCode session the prompt is sent to are desynced.
Root cause (source + data evidence)
1. The adapter always creates a new session and ignores the resume cursor — apps/server/src/provider/Layers/OpenCodeAdapter.ts:
// startSession always creates; input.resumeCursor is ignored
1073: const openCodeSession = yield* runOpenCodeSdk("session.create", () =>
1074: client.session.create({ title: `T3 Code ${input.threadId}`, ... }))
1133: openCodeSessionId: started.openCodeSession.id, // kept in memory only
// follow-up turns target the in-memory id:
1241: context.client.session.promptAsync({ sessionID: context.openCodeSessionId, ... })
The returned ProviderSession carries no resumeCursor, and startSession never reuses one even though ProviderService passes a persisted cursor on recovery (ProviderService.ts ~400-408).
2. Nothing durable is persisted to re-attach. In ~/.t3/userdata/state.sqlite:
provider_session_runtime.resume_cursor_json is NULL for every OpenCode thread.
projection_thread_sessions.provider_session_id / provider_thread_id are empty for OpenCode threads (even currently-running ones).
So there is no stored link from a t3code thread to its OpenCode ses_….
3. The reaper stops idle sessions after 30 min — ProviderSessionReaper.ts (DEFAULT_INACTIVITY_THRESHOLD_MS = 30 * 60 * 1000); last_seen_at is refreshed on turn send/start, not on runtime events, so a long sub-agent turn can finish and then be reaped before the next user message.
Direct proof from OpenCode's DB (~/.local/share/opencode/opencode.db). t3code titles each session T3 Code <threadId>, so a correctly-resumed thread would have exactly one such session. Instead, multiple threads have two independent top-level (parent_id IS NULL) sessions for the same thread id, created ~45–90 min apart:
| T3 thread |
OpenCode session |
created (UTC) |
msgs |
| a72d0ec7… |
ses_0ec30151… |
14:37 |
18 |
| a72d0ec7… |
ses_0ec07e26… |
15:20 |
57 |
| 5d69a12b… |
ses_0ec90f05… |
12:51 |
25 |
| 5d69a12b… |
ses_0ec421e3… |
14:17 |
34 |
| 279f293f… |
ses_0ec8dfba… |
12:54 |
32 |
| 279f293f… |
ses_0ec432a4… |
14:16 |
111 |
Each pair is one visible t3code thread split across two separate OpenCode sessions; the second does not contain the first's history (OpenCode loads context per session_id), so a turn that lands in the new session is contextless. (Sub-agent/Task sessions are excluded — they carry different titles like … (@explorer subagent).)
OpenCode is not at fault: given the same session_id, OpenCode loads prior history for a prompt (packages/opencode/src/session/prompt.ts ~1092-1279; messages selected by session_id in message-v2.ts). The CLI/TUI keeps using the same session id directly, which is why it does not lose context on the same DB.
Suggested fix
- Make
OpenCodeAdapter resumable: persist the OpenCode ses_… (e.g. as the resumeCursor already supported by provider_session_runtime.resume_cursor_json), and on startSession with a cursor, reuse that session id (validate via session.get) instead of session.create.
- If a thread has prior state but no resumable session, surface an explicit recovery error instead of silently creating a new empty session under the same visible thread (
ProviderCommandReactor).
- Harden the reaper: refresh
last_seen_at on runtime events / turn completion, and/or don't reap providers that can't currently resume.
Impact
Major degradation — silent context loss mid-thread; the model answers confidently without the prior conversation, which can lead to wrong actions. Intermittent (tied to idle/reaper/restart), so easy to hit and confusing to diagnose.
Version or commit
T3 Code Nightly (desktop). OpenCode server 1.17.11; @opencode-ai/sdk 1.15.13.
Environment
macOS (Apple Silicon); provider = opencode (t3code-spawned opencode serve).
Logs or stack traces
Corroborating: provider.session.reaped { provider: 'opencode', reason: 'inactivity_threshold', idleDurationMs: ~30+ min } in ~/.t3/userdata/logs/server-child.log, plus the two-sessions-per-thread split above. There is no error at the moment of context loss (a fresh session is created normally), matching "no hang, just contextless."
Relationship to #3601
#3601 (unpaginated readThread -> slow/hung "pull") is a separate potential performance concern and does not cause this context loss (it is not on the send path, and the symptom here is contextless execution, not a hang). Filing this separately as the real root cause.
Before submitting
Area
apps/server
Summary
On the OpenCode provider, t3code does not durably bind a thread to its OpenCode
ses_…id, and the adapter cannot resume. When the in-memory binding is lost — most plausibly whenProviderSessionReaperreaps the session after ~30 min idle (e.g. after a long sub-agent turn), or on an app/server restart — the next follow-up in the same visible thread is sent to a brand-new, empty OpenCode session. t3code keeps rendering the prior conversation from its own projection DB, so the user still sees the history, but the model has no context and behaves as if a new session started.Steps to reproduce
Expected behavior
A follow-up in an existing thread continues the same OpenCode session with full prior context — or, if the session genuinely can't be resumed, fails with a clear explicit error rather than silently starting an empty session under the same visible thread.
Actual behavior
The follow-up runs in a new, empty OpenCode session. No hang and no error — the model just has no context. The visible chat (t3code's own projection) and the OpenCode session the prompt is sent to are desynced.
Root cause (source + data evidence)
1. The adapter always creates a new session and ignores the resume cursor —
apps/server/src/provider/Layers/OpenCodeAdapter.ts:The returned
ProviderSessioncarries noresumeCursor, andstartSessionnever reuses one even thoughProviderServicepasses a persisted cursor on recovery (ProviderService.ts~400-408).2. Nothing durable is persisted to re-attach. In
~/.t3/userdata/state.sqlite:provider_session_runtime.resume_cursor_jsonis NULL for every OpenCode thread.projection_thread_sessions.provider_session_id/provider_thread_idare empty for OpenCode threads (even currently-running ones).So there is no stored link from a t3code thread to its OpenCode
ses_….3. The reaper stops idle sessions after 30 min —
ProviderSessionReaper.ts(DEFAULT_INACTIVITY_THRESHOLD_MS = 30 * 60 * 1000);last_seen_atis refreshed on turn send/start, not on runtime events, so a long sub-agent turn can finish and then be reaped before the next user message.Direct proof from OpenCode's DB (
~/.local/share/opencode/opencode.db). t3code titles each sessionT3 Code <threadId>, so a correctly-resumed thread would have exactly one such session. Instead, multiple threads have two independent top-level (parent_id IS NULL) sessions for the same thread id, created ~45–90 min apart:Each pair is one visible t3code thread split across two separate OpenCode sessions; the second does not contain the first's history (OpenCode loads context per
session_id), so a turn that lands in the new session is contextless. (Sub-agent/Task sessions are excluded — they carry different titles like… (@explorer subagent).)OpenCode is not at fault: given the same
session_id, OpenCode loads prior history for a prompt (packages/opencode/src/session/prompt.ts~1092-1279; messages selected bysession_idinmessage-v2.ts). The CLI/TUI keeps using the same session id directly, which is why it does not lose context on the same DB.Suggested fix
OpenCodeAdapterresumable: persist the OpenCodeses_…(e.g. as theresumeCursoralready supported byprovider_session_runtime.resume_cursor_json), and onstartSessionwith a cursor, reuse that session id (validate viasession.get) instead ofsession.create.ProviderCommandReactor).last_seen_aton runtime events / turn completion, and/or don't reap providers that can't currently resume.Impact
Major degradation — silent context loss mid-thread; the model answers confidently without the prior conversation, which can lead to wrong actions. Intermittent (tied to idle/reaper/restart), so easy to hit and confusing to diagnose.
Version or commit
T3 Code Nightly (desktop). OpenCode server 1.17.11; @opencode-ai/sdk 1.15.13.
Environment
macOS (Apple Silicon); provider = opencode (t3code-spawned
opencode serve).Logs or stack traces
Corroborating:
provider.session.reaped { provider: 'opencode', reason: 'inactivity_threshold', idleDurationMs: ~30+ min }in~/.t3/userdata/logs/server-child.log, plus the two-sessions-per-thread split above. There is no error at the moment of context loss (a fresh session is created normally), matching "no hang, just contextless."Relationship to #3601
#3601 (unpaginated
readThread-> slow/hung "pull") is a separate potential performance concern and does not cause this context loss (it is not on the send path, and the symptom here is contextless execution, not a hang). Filing this separately as the real root cause.