Repository navigation
prompt_async returns 204 after startup failure, leaving empty child sessions that appear frozen #33394
Description
Summary
Delegated/subagent sessions can be created successfully, then remain permanently empty while the caller/UI keeps polling them. The immediate trigger I observed was the known session_message.seq insertion bug, but the user-visible failure is broader: POST /session/{id}/prompt_async can fail during startup, publish/log session.error, still return HTTP 204, and leave a valid child session with zero user/assistant messages.
That makes delegation features look frozen: the child session exists, /session/{id}/message returns 200 with an empty list, and monitors/tools keep waiting for messages that will never be written.
Related existing reports/fix:
- BUG: session_message.seq NOT NULL constraint failed on agent-switched sessions #31204
opencode runand HTTPPOST /session/{id}/messagefail withNOT NULL constraint failed: session_message.seq(1.15.13) #31413- fix(core): guard against null event.seq when inserting session_messages #31419
Those cover the session_message.seq crash itself. This issue is about the higher-level API/state behavior that turns a startup failure into a silent, empty, frozen child session.
Environment observed
- OpenCode version reported by CLI:
1.14.48 - OS: macOS / darwin arm64
- Delegation plugin in use:
oh-my-openagent - Local DB:
~/.local/share/opencode/opencode.db - Local server logs:
~/.local/share/opencode/log/2026-06-22T143351.log
What happened
A parent session launched five delegated child sessions. Each child session row was created with a parent ID, title, directory, and model, but no message rows or parts were ever written.
Example affected child sessions:
ses_1103540cfffe3v9MYa17MzZnc1 GDD Angle spine (@NeoAgent (Builder) subagent)
ses_110354087ffeubDsuGjBMTETWX GDD social LLM (@NeoAgent (Builder) subagent)
ses_110354060ffe40fMS6hHJlilGS GDD input feel rooms (@NeoAgent (Builder) subagent)
ses_110354044ffevRr4Bva6HfVaXQ Engine Angle docs (@Odin (Builder) subagent)
ses_110353ffbffe1F9p3vKXuus3Ze Angle spec capture (@Odin (Builder) subagent)
DB state for those sessions:
session row: present
parent_id: present
message rows: 0
part rows: 0
session_message rows: 0
The logs show the important failure mode. For each child session, OpenCode logged prompt_async failed [object Object] and published session.error, but the HTTP request still returned 204:
POST /session/ses_1103540cfffe3v9MYa17MzZnc1/prompt_async
ERROR prompt_async failed [object Object]
service=bus type=session.error publishing
POST /session/ses_1103540cfffe3v9MYa17MzZnc1/prompt_async 204
After that, the server/UI continued polling:
GET /session/ses_1103540cfffe3v9MYa17MzZnc1/message 200
GET /session/ses_110354087ffeubDsuGjBMTETWX/message 200
...
Those message responses were empty indefinitely because the initial user message was never persisted.
Why this is especially confusing
There are several inconsistent surfaces after the startup failure:
- The child session exists in the DB.
GET /session/{child}/messagesucceeds, but returns no messages.- The original
prompt_asyncHTTP call returned 204 even though startup failed. session.erroris only visible through logs/bus, not through the prompt caller as an actionable failure.- Some list-based SDK/plugin code treats child sessions as nonexistent because
session.list()does not include them even though directsession.get(id)/session.messages(id)can work for real child sessions.
So downstream tools see either:
- “session exists but has no messages, keep polling”, or
- “session not found”, depending on whether they use list-based existence checks or direct message lookup.
Both obscure the real failure.
Root cause observed
There appear to be two layers:
1. Immediate trigger: session_message.seq null/NOT NULL failure
The empty child sessions were created seconds before applying a local workaround for the known session_message.seq issue. This matches #31204/#31413/#31419.
In the failing path, message/session projection insertion can fail before any user message is persisted. That leaves the newly-created child session shell without messages.
2. Higher-level bug: prompt startup failure is not propagated to the caller/session state
Even after the internal prompt_async startup failed, the HTTP layer returned 204. That means callers interpret the prompt as accepted and enter their polling loop.
A delegated session needs a startup invariant:
If
prompt_asyncreturns accepted/success, at least the initial user message should be durably visible, or the session should be durably marked failed.
Currently, a third state is possible:
session created
prompt failed before first message persisted
HTTP returned 204
no durable message/error visible to polling clients
That is the frozen-empty state.
Expected behavior
If prompt_async fails before persisting the initial user message, one of these should happen:
POST /session/{id}/prompt_asyncreturns a non-2xx error with the underlying failure, or- the session is durably marked failed/error and exposed through session status/messages, or
- an error message/part is appended to the session so clients polling messages can terminate with a visible failure.
Any of these would be much better than a 204 plus an empty message stream.
Actual behavior
prompt_async logs/publishes an error but returns 204. The child session remains queryable and empty. Delegation monitors continue polling until their own timeout/abort logic, making the feature appear frozen.
Proposed fix
A. Fix the known session_message.seq projection bug
Merge or extend #31419:
- Treat
event.seq == nullas missing, not justundefined. - Ensure every
session_messageinsert gets a valid per-sessionseq. - Add a regression test around
session.next.agent.switchedand anyappendMessagepaths that create projection rows.
I would also consider a migration/backfill/default safeguard so a future missed insert path cannot take down message startup entirely.
B. Make prompt_async startup atomic or visibly failed
Add a guard around the startup path:
- after creating the session and before returning 204, ensure the initial user message was persisted; or
- if the async worker is intentionally fire-and-forget, persist a startup-failed state/error when message creation fails.
The key contract should be:
204/accepted => callers can observe at least one new message or a running state
startup failure => non-2xx response or durable session error
Do not let the API return accepted while leaving no observable message/error state.
C. Expose child session existence consistently
If session.get(id) and session.messages(id) can address child sessions, list-based existence checks need a reliable way to discover them too. Options:
- include child sessions in
session.list()withparentID, or - add/document
includeChildren=true, or - provide/document
session.get(id)as the correct existence check and avoid list-only existence assumptions in SDK helpers.
This is not the primary crash, but it compounds the freeze by causing some tooling to report “Session not found” for real child sessions.
D. Tests that would catch this
Suggested regression tests:
- Force
appendMessage/projection insert failure duringprompt_asyncstartup and assert the HTTP caller does not receive a successful accepted response without a durable session error. - Create a child session, trigger startup failure, and assert
/session/{id}/messagedoes not remain silently empty forever without an error/status signal. - Create a normal child session with messages and assert direct
session.get(id)/session.messages(id)and the intended listing/discovery API agree on its existence. - Regression for BUG: session_message.seq NOT NULL constraint failed on agent-switched sessions #31204/
opencode runand HTTPPOST /session/{id}/messagefail withNOT NULL constraint failed: session_message.seq(1.15.13) #31413:session.next.agent.switchedwritessession_message.seqcorrectly whenevent.seqis null.
Local workaround used for confirmation
I locally worked around the DB constraint issue and added plugin-side bootstrap checks so delegation no longer polls forever:
- after dispatching a delegated prompt, poll
session.messages(id)briefly; - if no new user/assistant message appears, treat startup as failed immediately;
- for session reading, use direct
session.messages(id)/session.get(id)instead of rejecting child sessions based only onsession.list().
That workaround made the symptoms explicit:
- completed delegated children can be read again;
- old zero-message child shells now report “No messages found” instead of appearing frozen/missing;
- new delegation smoke test completes normally.
The durable fix should be in OpenCode core/API so plugin/tooling authors do not each need to implement their own bootstrap-detection workaround.
Activity
aschokinatgmail commentedon Aug 13, 2026
Confirmation: systemic on Windows with oh-my-openagent plugin
We're seeing this on v1.18.18 on Windows with the
oh-my-openagentplugin. The bug is not occasional — it affects every single child/subagent session in our database.Evidence from DB inspection
The opencode SQLite DB (
~/.local/share/opencode/opencode.db, 10.3 GB) contains:parent_id IS NOT NULL) — all 1,529 have 0 rows insession_messagemessagetable has 64,561 rows and theparttable has 251,923 rows — the data IS persisted, just not linked viasession_messageFor example, an Oracle review subagent session
ses_00587af5dffeYxgt0Z7syUy5sp:session_messagerows: 0partrows: 111 (17 distinctmessage_ids)message_ids all exist in themessagetable with correctsession_id— the data is there, just the projection/join table is emptyUser-visible symptom
In the TUI, clicking on any subagent session shows only the handover prompt (the parent agent's message with the model name) — no subagent conversation, tool calls, reasoning, or response. This makes every subagent session look like a failure, even when the work completed successfully and the parent agent received the result.
DB-level workaround
We repaired all 1,529 broken sessions by reconstructing
session_messagerows from existingpartandmessagedata:After this repair, the TUI correctly displays the full conversation history for all subagent sessions.
Key observation
The
session_message.seqcolumn isNOT NULLwith no default. If the projection insert fails (due to a nullseqfrom a race condition or missing event sequence), the entire message-to-session link is lost silently. The message and part data are written independently and survive — only the projection fails.This suggests the fix should ensure either:
seqhas a safe default or is computed before insert (not dependent on an event that may be null), orseq, orpart.message_id → messagedirectly whensession_messageis empty for a session that has partsgithub-actions commentedon Oct 13, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.