Repository navigation
Description
Description
Agent switch corrupts the session sequence counter → session permanently wedged (UNIQUE constraint on session_message.seq)
Environment
- OpenCode CLI/server: v2.0.32 (TUI + web-login server,
opencode serve) - Storage: SQLite (
opencode-web-login-v1.db), WAL mode - Schema:
session_messagehas a unique index(session_id, seq); the sequence counter lives inevent_sequence(aggregate_id) -> seq; pending inputs queue insession_inbox - State affected: long-running session (single directory, ~1600 seq messages)
Summary
Switching the agent on an existing long-running session reset the session's sequence counter to 0 instead of continuing past the highest already-used seq. The newly generated messages then march upward from seq 0 until they hit an already-occupied seq, where the insert fails permanently:
SQLiteError: UNIQUE constraint failed: session_message.session_id, session_message.seq
Once this starts, every operation on the session fails with HTTP 500 (agent switch, model switch, inbox promote / drain) and the session becomes unrecoverable via the UI. Restarting the server does not help — the counter is rehydrated from the corrupted low value and the session keeps failing.
Impact
- Session is permanently stuck; every user action returns 500
- Pending user messages can sit in
session_inboxwithout being delivered (or get dropped on enqueue/retry) - Compaction enters a retry loop: it keeps re-attempting, burning one seq per attempt, and wedges at the first occupied seq
- Data itself is not lost, but the session is unusable until the counter is repaired manually
Reproduction
- Use a session that has accumulated many messages (original projection seq space occupied up to N, e.g. N=1605).
- Switch agent (e.g.
build→plan). - (Aggravating factor observed, likely race) a second
opencode serve --serviceinstance running concurrently, sharing the same SQLite DB. - Observe:
agent-switchedmessage is inserted with seq=0 while projection max seq is 1605 (counter reset, not continued)- promoting the next queued message fails with
UNIQUE constraint failed: session_message.session_id, session_message.seq POST /api/session/{id}/model→ 500 with the same error, repeatedly
Evidence
Timeline (all messages logged with exact timestamps):
14:42:57— previous round completes normally (idle, succeeded)17:21:21— user switches agentbuild → plan; recordagent-switchedwritten at seq=0; queued user message getsenqueued_seq=1insession_inbox;SessionRunner.drain → SessionInbox.promote → SessionInbox.publishfails with:
Failed to drain Session
cause: SQLiteError: UNIQUE constraint failed: session_message.session_id, session_message.seq
Session state becomessuspended, message never delivered.17:40:15-17:40:43— three attempts to switch model:
POST /api/session/.../model → 500
at Session.switchModel
UNIQUE constraint failed: session_message.session_id, session_message.seq- Counter state after corruption:
event_sequence.seq = 76 <- cursor points into an already-occupied low range
session_message: min seq 0, max seq 1605 (all seqs 0..1605 occupied by pre-reset history) 18:43:32— server restart does NOT fix it. The runner rehydrates from the low counter and keeps writing LOW seqs (8, 9, 10, 17, 19, 22, 23, 26, 29, 34, 35, ... up to 74), then wedges permanently at the first occupied seq (77):
compaction seq 23, 26, 29, ... 74 -> next attempt 77 -> UNIQUE violation, no progress
Root-cause smoking gun: the agent-switched record exists at seq=0 while other records of the same session occupy seq 0–1605 with strictly increasing timestamps after seq 0 — the counter was reset to 0 at agent-switch time, instead of max(session_message.seq)+1.
Relevant code path (from the stack trace):
SessionRunner.drain -> SessionInbox.promote -> SessionInbox.publish
-> insert into session_message (unique index on (session_id, seq)) FAILS
Session.switchModel -> same insert, same failure
Workaround / Fix that unblocked me
Repairing the counter to be above the occupied range restored the session immediately (no code change needed):
-- event_sequence.seq for the session: 76 -> (MAX(seq)+1) per session_message
UPDATE event_sequence
SET seq = (SELECT MAX(seq)+1 FROM session_message WHERE session_id = ?)
WHERE aggregate_id = ?;
after which a server restart rehydrates the counter correctly and:
- the queued message is delivered normally
- the compaction retry loop completes
- model switching works again
- zero UNIQUE errors in the log after the restart
Suggested fix
Either of:
- On session (re)load / agent switch, recompute the counter as
max(seq)+1oversession_messagefor that session (and assertevent_sequence.seq > max(session_message.seq)when rehydrating — if lower, self-heal instead of failing inserts forever). - Make the projection insert resilient: on UNIQUE(seq) collision, re-derive
seq = COALESCE(MAX(seq), -1) + 1for the session instead of hard-failing the whole runner and wedging the session in an unrecoverable suspended state.
The self-healing on rehydration seems especially important: currently a corrupted counter is reloaded on every restart and the session remains dead forever.
Plugins
No response
OpenCode version
No response
Steps to reproduce
No response
Screenshot and/or share link
No response
Operating System
No response
Terminal
No response
Activity
opencode-agent commentedon Oct 17, 2026
Thanks for the detailed report, timeline and repair query.
I think this is a duplicate of #53146. That issue covers the same failure: the session's sequence counter ends up behind rows already in
session_message, so every later insert fails withUNIQUE constraint failed: session_message.session_id, session_message.seqand the session stays wedged. It also names the same setup as a trigger (anopencode serve --serviceinstance sharing the database with another server process). A follow-up comment there shows the counter going backwards with a single writer too, and the stale value coming back after a restart, which matches what you saw. Theagent-switchedrecord at seq 0 looks like the first write after the counter was reset, not a separate cause.Possible duplicates and closely related issues, ordered from most to least likely:
session_message.seqUNIQUE collision, same shared-service setup, stale counter survives restarteventtable (event.aggregate_id, event.seq)event_sequencecursor falling behind event rows on v2.0.32 with the shared servicesession_messageseq during projectionIt would help to add your timeline and the before/after values (
event_sequence.seq76 vsMAX(seq)1605) to #53146 as a comment.The bot will close this issue as a duplicate in 1 day unless you reply explaining how your problem is different.
opencode-agent commentedon Oct 18, 2026
We haven't heard back, so I'm closing this as a duplicate of #53146. That issue covers the same problem: the session's sequence counter ends up behind rows already in
session_message, every later insert fails withUNIQUE constraint failed: session_message.session_id, session_message.seq, and the stale counter is still there after a restart.Please follow #53146 for updates. Your timeline and before/after values (
event_sequence.seq76 vsMAX(seq)1605) would help there as a comment, and so would the note that this started right after an agent switch. #51411 tracks the same kind of failure on theeventtable.If your problem turns out to be different, reply here and we can reopen it.