Skip to content

prompt_async returns 204 after startup failure, leaving empty child sessions that appear frozen #33394

Closed as not planned

Description

@DanRioDev

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:

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:

  1. The child session exists in the DB.
  2. GET /session/{child}/message succeeds, but returns no messages.
  3. The original prompt_async HTTP call returned 204 even though startup failed.
  4. session.error is only visible through logs/bus, not through the prompt caller as an actionable failure.
  5. 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.

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_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:

  1. POST /session/{id}/prompt_async returns a non-2xx error with the underlying failure, or
  2. the session is durably marked failed/error and exposed through session status/messages, or
  3. 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 == 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:

  1. 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.
  2. Create a child session, trigger startup failure, and assert /session/{id}/message does not remain silently empty forever without an error/status signal.
  3. 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.
  4. Regression for BUG: session_message.seq NOT NULL constraint failed on agent-switched sessions #31204/opencode run and HTTP POST /session/{id}/message fail with NOT NULL constraint failed: session_message.seq (1.15.13) #31413: session.next.agent.switched writes session_message.seq correctly when event.seq is 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 on session.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

  1. aschokinatgmail commented on Aug 13, 2026

    @aschokinatgmail

    Confirmation: systemic on Windows with oh-my-openagent plugin

    We're seeing this on v1.18.18 on Windows with the oh-my-openagent plugin. 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:

    • 1,975 total sessions across 5 projects (April 2026 – August 2026)
    • 1,529 child sessions (sessions with parent_id IS NOT NULL) — all 1,529 have 0 rows in session_message
    • The message table has 64,561 rows and the part table has 251,923 rows — the data IS persisted, just not linked via session_message

    For example, an Oracle review subagent session ses_00587af5dffeYxgt0Z7syUy5sp:

    • session_message rows: 0
    • part rows: 111 (17 distinct message_ids)
    • Those 17 message_ids all exist in the message table with correct session_id — the data is there, just the projection/join table is empty

    User-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_message rows from existing part and message data:

    -- For each broken session, get distinct message_ids from parts (ordered by creation time)
    -- and insert session_message rows with sequential seq values
    INSERT OR IGNORE INTO session_message (id, session_id, type, time_created, time_updated, data, seq)
    SELECT p.message_id, p.session_id, 'message', 
           MIN(p.time_created), MAX(p.time_updated), 
           COALESCE(m.data, '{}'), 
           -- seq assigned per-session in application code
    FROM part p
    LEFT JOIN message m ON m.id = p.message_id
    WHERE p.session_id = ? AND p.message_id IS NOT NULL
    GROUP BY p.message_id

    After this repair, the TUI correctly displays the full conversation history for all subagent sessions.

    Key observation

    The session_message.seq column is NOT NULL with no default. If the projection insert fails (due to a null seq from 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:

    1. seq has a safe default or is computed before insert (not dependent on an event that may be null), or
    2. The insert failure is caught and retried with a computed seq, or
    3. The TUI falls back to querying part.message_id → message directly when session_message is empty for a session that has parts
  2. github-actions commented on Oct 13, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions