Skip to content

[BUG] Subagent final part lost — SessionProcessor.cleanup races with "removing share" (1.17.18) #36670

Closed as not planned

Description

@mel0nyrame

Description

When a subagent (librarian / observer) session finishes, its final output doesn't show up in the TUI — the content just disappears. Digging into the logs, SessionProcessor.cleanup collides with the "removing share" flow when writing the final part.

The ordering is the same across all 4 affected subagent sessions:

12:34:51 created ses_… subagent session created
12:34:51 loop / process / stream step 0..5 streaming fine
12:35:21 removing share ses_… ← this deletes the session + message rows
12:35:24 ERROR FOREIGN KEY constraint failed
at Session.updatePart
at SessionProcessor.cleanup ← cleanup tries to write part, parent rows already gone

Session.updatePart does an upsert on part, whose FK points to message(id). By the time cleanup runs, the parent message (and session) rows have already been deleted by the share-removal path, so the upsert trips the FK constraint and the whole part write gets rolled back.

Net result: the subagent's last text / reasoning / step-start never lands in the DB, so the TUI has nothing to render for that turn. The DB itself is fine (PRAGMA integrity_check ok, no orphan parts / messages) — the content is just gone.

Plugins

oh-my-opencode-slim (same run also logged Plugin export is not a function for a list plugin, but the FK race reproduces independently of that

OpenCode version

1.17.18

Steps to reproduce

  1. Start an orchestrator session and have it dispatch subagents (librarian / observer) that run a few steps and finish normally.
  2. When the subagent session ends, opencode runs removing share (deletes the session + message rows) and then SessionProcessor.cleanup tries to upsert the final part.
  3. Session.updatePart fails with FOREIGN KEY constraint failed because the parent message row no longer exists.
  4. The subagent's final text / reasoning is never persisted → TUI renders nothing for that turn.

Reproduced 6 FK failures across 4 subagent sessions in a single run today, all deepseek-v4-flash, all under run e21ab353. From ~/.local/share/opencode/log/opencode.log:

level=ERROR message=process session.id=ses_0a486e447ffe… error="Failed query: insert into "part" … (cause: SQLiteError: FOREIGN KEY constraint failed)"
at Session.updatePart
at SessionProcessor.cleanup
at SessionProcessor.process
at SessionPrompt.run

Afterward, all 4 session.ids and messageIDs are missing from the DB; PRAGMA foreign_key_check returns nothing — the failed upserts were rolled back, no dirty data, just lost parts.

Screenshot and/or share link

https://opncd.ai/share/vPVSWz88

The whole terminal fills up with what looks like TypeScript source code, as if someone opened a file in vim. As soon as I drag the mouse cursor across the terminal, the screen "tears" open and the real opencode TUI shows through underneath. Resizing the window fixes it for a while, but the moment I switch to another app/window and switch back, it reverts to the broken TS-code view.

Feels like a redraw / alternate-screen-buffer issue — the TUI isn't repainting on focus regain, so stale terminal buffer content (left over from some tool output or editor) stays on screen until a mouse drag or resize forces a full repaint

Operating System

macOS 26.5.2 (25F84)

Terminal

Ghostty (xterm-ghostty)

Activity

  1. mel0nyrame commented on Jul 13, 2026

    @mel0nyrame
    Author

    Dug deeper into the logs and the DB. The original report says 6 FK failures across 4 subagent sessions — the actual log shows 7 FK failures across 7 subagent sessions, all under run e21ab353. Also the schema's ON DELETE CASCADE makes the race worse than described.

    Schema: both FKs are ON DELETE CASCADE

    -- part
    CONSTRAINT fk_part_message_id_message_id_fk
      FOREIGN KEY (message_id) REFERENCES message(id) ON DELETE CASCADE
    
    -- message
    CONSTRAINT fk_message_session_id_session_id_fk
      FOREIGN KEY (session_id) REFERENCES session(id) ON DELETE CASCADE

    So "removing share" only deletes the session row; message and part get wiped by SQLite's cascade. The original "deletes the session + message rows" is really "deletes the session row, which cascades to message and part" — and since cascade delete is atomic, the cleanup upsert is guaranteed to hit missing parent rows once it lands after the delete.

    7 FK failures with timing

    Every one of them is the same pattern: removing share deletes the session row (cascade removes message/part), then SessionProcessor.cleanup upserts the part/message and hits the FK constraint. Gap ranges from 0.83s to 8.33s.

    # subagent session removing share FK error gap failed upsert ref
    1 ses_0a48e7aa…C3l 12:27:04.227Z 12:27:06.984Z 2.76s insert into "part" type=text err_5ebbc44e
    2 ses_0a486e7d…RG0 12:35:20.556Z 12:35:22.337Z 1.78s insert into "part" type=reasoning err_0c574697
    3 ses_0a486e44…myH 12:35:21.469Z 12:35:24.530Z 3.06s insert into "part" type=text err_cf5159ee
    4 ses_0a486dc1…UjA 12:35:23.560Z 12:35:25.446Z 1.89s insert into "message" (nested part step-start failure) err_8a4ced43
    5 ses_0a4855d1…m5N 12:37:01.615Z 12:37:02.445Z 0.83s insert into "part" type=reasoning err_d0638133
    6 ses_0a4854d8…krK 12:37:05.605Z 12:37:08.474Z 2.87s insert into "part" type=reasoning err_f248f38d
    7 ses_0a4844d8…nTe 12:38:11.131Z 12:38:19.456Z 8.33s insert into "part" type=reasoning err_843ee6d4

    Row 4 is an insert into "message" failure, but error.data.message wraps an insert into "part" (step-start) FK failure — the message upsert rolls back because the child part write failed. Cascaded failure.

    removing share — raw log (all 7)

    timestamp=2026-07-13T12:27:04.227Z level=INFO run=e21ab353 message="removing share" sessionID=ses_0a48e7aa7ffe8eD6S60onn2C3l
    timestamp=2026-07-13T12:35:20.556Z level=INFO run=e21ab353 message="removing share" sessionID=ses_0a486e7ddffe6umtdypxZ38RG0
    timestamp=2026-07-13T12:35:21.469Z level=INFO run=e21ab353 message="removing share" sessionID=ses_0a486e447ffeTJjHVzcaJJEmyH
    timestamp=2026-07-13T12:35:23.560Z level=INFO run=e21ab353 message="removing share" sessionID=ses_0a486dc18ffeaaHr0xsQM4kUjA
    timestamp=2026-07-13T12:37:01.615Z level=INFO run=e21ab353 message="removing share" sessionID=ses_0a4855d15ffeI1IFXwDrzr6m5N
    timestamp=2026-07-13T12:37:05.605Z level=INFO run=e21ab353 message="removing share" sessionID=ses_0a4854d80ffeJT0Re7D8i4UkrK
    timestamp=2026-07-13T12:38:11.131Z level=INFO run=e21ab353 message="removing share" sessionID=ses_0a4844d8dffepMNMtetQTwEnTe
    

    FK error — raw log (row 1, data truncated)

    timestamp=2026-07-13T12:27:06.984Z level=ERROR run=e21ab353 message=failed ref=err_5ebbc44e
    error="EffectDrizzleQueryError: Failed query: insert into \"part\" (\"id\",\"message_id\",\"session_id\",\"time_created\",\"time_updated\",\"data\")
      values (?,?,?,?,?,?) on conflict (\"part\".\"id\") do update set \"time_updated\"=?,\"data\"=?"
    params: prt_f5b71f8a2001tMuQFBq7KoHjNQ, msg_f5b71e543001V5B5BYEGZVYPlP, ses_0a48e7aa7ffe8eD6S60onn2C3l,
            1783945626981, 1783945626982, {"type":"text","text":"根据你提供的 Gitee 页面内容…(subagent final output)"}
    cause: SQLiteError: FOREIGN KEY constraint failed
    stack:
      at Session.updatePart
      at SessionProcessor.cleanup
      at SessionProcessor.process
      at SessionPrompt.run
      at SessionRunState.ensureRunning
      at SessionPrompt.loop
    

    All 7 share the same stack: Session.updatePart → SessionProcessor.cleanup → SessionProcessor.process → SessionPrompt.run → SessionRunState.ensureRunning → SessionPrompt.loop.

    DB verification

    PRAGMA integrity_check;    → ok
    PRAGMA foreign_key_check;  → (empty)
    WAL checkpoint             → 0|153|153 (fully checkpointed, no pending tx)
    

    7 failed sessions — data fully gone, no residue

    failed session message rows part rows
    all 7 0 0

    All 7 messages and 7 parts are gone, wiped by cascade. The failed upserts rolled back — no half-written state.

    Control group — 5 surviving subagents, data intact

    session agent messages parts last part types
    ses_0a499df7b… observer 6 20 step-finish → text → reasoning ✓
    ses_0a48f2d2e… librarian 7 33 step-finish → text → reasoning ✓
    ses_0a48f03f6… librarian 21 130 —
    ses_0a48dcce9… librarian 14 73 —
    ses_0a486e482… librarian 6 32 step-finish → text → reasoning ✓

    Surviving subagents kept their final output (text/reasoning/step-finish) in the part table. So the data loss is strictly tied to the "removing share" path — subagents that didn't trigger it are fine.

    Parent session

    ses_0a4a30990ffeBn6ktWvPVSWz88 (orchestrator)
      slug      = misty-cactus
      share_url = https://opncd.ai/share/vPVSWz88   ← matches the share link in the issue
      messages  = 20, parts = 77
      parent_id = (none, top-level)
    

    This session spawned at least 12 subagents (5 surviving + 7 deleted), not 4.

    Global consistency

    orphan parts (part exists / message missing)      → 0
    orphan messages (message exists / session missing) → 0
    sessions with non-null share_url                   → 1 (only the parent)
    

    Corrections to the original report

    1. Counts: 7 FK failures across 7 subagent sessions, not 6/4. Row 4 (ses_0a486dc18…) is a message upsert cascading from a child part failure — worth calling out separately.
    2. CASCADE is the amplifier: schema is ON DELETE CASCADE, so "removing share" deleting one session row auto-removes message + part. Suggest rewording "deletes the session + message rows" → "deletes the session row, which cascades to message and part".
    3. Race window: 0.83s–8.33s. Row 7 has an 8.3s gap, so cleanup lagging well behind still collides.
    4. Subagent total: parent spawned 12 subagents (5 surviving + 7 deleted). The 5 survivors are a clean control group showing the bug is isolated to the "removing share" path.
    5. share_url: only the parent session has a share_url. The 7 deleted sessions' share_url rows are gone, but "removing share" clearly ran against them — worth checking whether subagent sessions get a transient share_url that then gets cleaned up.
  2. mel0nyrame commented on Jul 14, 2026

    @mel0nyrame
    Author

    Additional screenshots:
    Image

  3. mradwankhalil commented on Aug 19, 2026

    @mradwankhalil
    Contributor

    Follow-up from a related incident on OpenCode Desktop 1.18.18 / Windows 11 (2026-08-19). I now have a reproduction whose first symptom matches this issue: a completed subagent produced no final output, so I clicked another subagent to investigate.

    Parent and child sessions

    The parent was:

    • ses_fece7f378ffesRvg374eKUX5YC — OpenCode SQLite Database Forensic Audit

    It spawned five subagents at approximately 02:08:30Z:

    • ses_fe8390a61ffedKDInjyU722ECz — Map phase gate surface
    • ses_fe8390a2dffeFLT2aHsgoa0pZa — Map LightRAG admission paths
    • ses_fe8390992ffeyNEz37GNhMdwdV — Map production orchestration
    • ses_fe83909dbffeNBuDAB7RoQc2zA — Research LightRAG metadata isolation
    • ses_fe8390a47ffe2Kk8HTtFpB26uV — Map claim audit tooling

    The first four have normal completion evidence in SQLite and the runtime log: persisted final text, a final step-finish, finish: stop, and clean exiting loop / cancel entries.

    The fifth session, Map claim audit tooling, is different:

    • Only 7 messages and 287 parts were persisted.
    • Its last persisted parts are step-start, reasoning, several tool calls, and step-finish for an intermediate step.
    • There is no final results/deliverable text.
    • Its final message metadata is finish: tool-calls, not finish: stop.
    • There is no clean exiting loop or cancel entry for this session.
    • The last persisted part was at 02:14:48Z; the parent exited/pruned at about 02:15:01Z.

    In other words, the session exists and its earlier work is intact, but the final answer that should have followed the last tool calls is absent. This is the same user-visible symptom described in this issue: the subagent appears to finish, but the final part is missing.

    Desktop freeze immediately afterward

    While investigating the completed-subagent entries in the Desktop UI, the renderer produced a burst of:

    ResizeObserver loop completed with undelivered notifications.
    

    The renderer log stopped at 05:14:44.077 local time. The main log was already silent since 05:07:55, and there was no Electron unresponsive event, Crashpad dump, or Windows AppHang/AppCrash. The sidecar continued independently until the app was killed at approximately 05:15:14.

    The DB remained healthy (PRAGMA quick_check=ok, zero foreign-key violations), and the affected subagent row was not deleted. This makes the sequence look like:

    1. A subagent final result is lost or never persisted.
    2. The user navigates/clicks through the subagent tree to investigate.
    3. The Desktop renderer then hangs while the parent session is still active.

    Important limitation

    I did not find an August 19 log line for removing share, SessionProcessor.cleanup, or FOREIGN KEY constraint failed. Therefore I cannot claim that the exact deletion race documented here is proven for this incident. The missing-final-part symptom and abnormal completion state are proven; the precise cleanup mechanism is not.

    Could there be a completion/cleanup path that loses the final part without emitting the FK error, or does the removing share race have a variant where the failed write is rolled back before the error reaches the main log? This incident may be a useful regression case: a subagent with completed tool calls must either persist a final text/step-finish or emit an explicit terminal failure, never remain as a session ending in finish: tool-calls with no final result.

    This is also relevant to #42538, where the same subagent lifecycle is followed by a Desktop-wide freeze:
    #42538

  4. mradwankhalil commented on Oct 10, 2026

    @mradwankhalil
    Contributor

    The follow-up reproduction narrows this but does not yet prove the FK/share-removal race: it ends at finish: tool-calls without a final part, while the log lacks removing share, SessionProcessor.cleanup, and FK errors. Could maintainers confirm whether this is the same lifecycle failure or a separate terminal-state bug? A regression test should require every completed subagent to persist final text/step-finish or record an explicit terminal error.

  5. thdxr commented on Oct 11, 2026

    @thdxr
    Member

    Thanks for the report! OpenCode 2.0 is now out and development is focused on V2, so we're closing issues filed against OpenCode 1. Please upgrade (opencode upgrade --major, details: https://anoma.ly/notes/opencode-2-0), and if this is still relevant on 2.0, reopen this issue or open a new one with your 2.0 version.

    — from 𝕺𝖕𝖊𝖓𝕮𝖔𝖉𝖊

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