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
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.
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.
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}/message succeeds, but returns no messages.
The original prompt_async HTTP call returned 204 even though startup failed.
session.error is 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 direct session.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.
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_async returns 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_async returns 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
Treat event.seq == null as missing, not just undefined.
Ensure every session_message insert gets a valid per-session seq.
Add a regression test around session.next.agent.switched and any appendMessage paths 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() with parentID, 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 during prompt_async startup 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}/message does 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.
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.seqinsertion bug, but the user-visible failure is broader:POST /session/{id}/prompt_asynccan fail during startup, publish/logsession.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}/messagereturns 200 with an empty list, and monitors/tools keep waiting for messages that will never be written.Related existing reports/fix:
opencode runand HTTPPOST /session/{id}/messagefail withNOT NULL constraint failed: session_message.seq(1.15.13) #31413Those cover the
session_message.seqcrash 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
1.14.48oh-my-openagent~/.local/share/opencode/opencode.db~/.local/share/opencode/log/2026-06-22T143351.logWhat 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:
DB state for those sessions:
The logs show the important failure mode. For each child session, OpenCode logged
prompt_async failed [object Object]and publishedsession.error, but the HTTP request still returned 204:After that, the server/UI continued polling:
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:
GET /session/{child}/messagesucceeds, but returns no messages.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.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:
Both obscure the real failure.
Root cause observed
There appear to be two layers:
1. Immediate trigger:
session_message.seqnull/NOT NULL failureThe empty child sessions were created seconds before applying a local workaround for the known
session_message.seqissue. 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_asyncstartup 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:
Currently, a third state is possible:
That is the frozen-empty state.
Expected behavior
If
prompt_asyncfails before persisting the initial user message, one of these should happen:POST /session/{id}/prompt_asyncreturns a non-2xx error with the underlying failure, orAny of these would be much better than a 204 plus an empty message stream.
Actual behavior
prompt_asynclogs/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.seqprojection bugMerge or extend #31419:
event.seq == nullas missing, not justundefined.session_messageinsert gets a valid per-sessionseq.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_asyncstartup atomic or visibly failedAdd a guard around the startup path:
The key contract should be:
Do not let the API return accepted while leaving no observable message/error state.
C. Expose child session existence consistently
If
session.get(id)andsession.messages(id)can address child sessions, list-based existence checks need a reliable way to discover them too. Options:session.list()withparentID, orincludeChildren=true, orsession.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:
appendMessage/projection insert failure duringprompt_asyncstartup and assert the HTTP caller does not receive a successful accepted response without a durable session error./session/{id}/messagedoes not remain silently empty forever without an error/status signal.session.get(id)/session.messages(id)and the intended listing/discovery API agree on its existence.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:
session.messages(id)briefly;session.messages(id)/session.get(id)instead of rejecting child sessions based only onsession.list().That workaround made the symptoms explicit:
The durable fix should be in OpenCode core/API so plugin/tooling authors do not each need to implement their own bootstrap-detection workaround.