Skip to content

fix(api): defer follow-ups until Zed thread established (no divorced thread) - #2661

Merged
lukemarsden merged 1 commit into
mainfrom
fix/interrupt-boot-race-thread-fork
Jun 19, 2026
Merged

fix(api): defer follow-ups until Zed thread established (no divorced thread)#2661
lukemarsden merged 1 commit into
mainfrom
fix/interrupt-boot-race-thread-fork

Conversation

@lukemarsden

Copy link
Copy Markdown
Collaborator

Follow-up to #2660, which was incomplete. After it merged, the original reproducer (move task to backlog → re-plan from scratch → quick correction during boot) failed identically — the agent answered the correction with "a previous conversation context that I don't have."

Why #2660 missed it

#2660 guarded the interrupt poller decision site. But a second dispatch path — processAnyPendingPrompt → sendQueuedPromptToSession — is exempt from the busy-defer for interrupt prompts, and it dispatched the correction with an empty acp_thread_id concurrently with the initial (also empty-thread). Two empty-thread sends → Zed forked two threads: initial's spec work in A, correction in B. The session bound to B, which never held the initial → context loss.

Also: #2660's "validation" checked message completion + length, not whether the agent retained context, so it passed while the bug was live.

Fix — at the real chokepoint

In sendQueuedPromptToSession, the interrupt's exemption from the busy-defer is only safe once the thread exists. Gate it on threadNotEstablished := session.Metadata.ZedThreadID == "":

threadNotEstablished := session.Metadata.ZedThreadID == ""
if !prompt.Interrupt || threadNotEstablished {
    // busy-defer: don't dispatch a second empty-thread message while the
    // initial is still creating the thread; redeliver once ZedThreadID is set.
}

Until thread_created populates ZedThreadID, every prompt (interrupt included) respects the busy-defer, so only the genuine first message is ever sent empty-thread. Once the thread exists, the interrupt is exempt again and fires into the same thread.

Validation (content-verified, live Zed, meta.helix.ml)

Fresh spec task, initial = "WidgetSync … over Bluetooth", interrupt during boot = "Actually … over WiFi":

  • Session bound to one thread (04101764) — no fork.
  • Zero empty-thread correction sends.
  • Agent's correction response: "The user wants to change the sync mechanism from Bluetooth to WiFi. Let me now write the spec files for WidgetSync that syncs widgets between devices over WiFi…" — retained the feature and applied the correction. No "context I don't have."
  • Unit: TestWebSocketSyncSuite + TestPromptHistoryHandlersSuite green.

Incident report updated with this follow-up: design/2026-06-19-incident-interrupt-during-boot-context-loss.md.

🤖 Generated with Claude Code

…thread)

Follow-up to #2660, which was incomplete. The original reproducer (re-plan from
scratch + a correction during boot) still forked two threads and lost context.

#2660 guarded the interrupt-poller decision site, but a SECOND dispatch path —
processAnyPendingPrompt -> sendQueuedPromptToSession — is exempt from the
busy-defer for interrupt prompts and dispatched the correction with an empty
acp_thread_id concurrently with the initial. Two empty-thread sends -> Zed forked
two threads (initial in A, correction in B); the session bound to B, which never
held the initial, so the agent answered "a previous conversation context that I
don't have".

Fix at the real chokepoint: in sendQueuedPromptToSession the interrupt's
exemption from the busy-defer is gated on the thread being established
(ZedThreadID != ""). Until thread_created lands, every prompt (interrupt
included) respects the busy-defer, so only the genuine first message is ever
sent empty-thread; once the thread exists the interrupt is exempt again and
fires into the SAME thread.

Content-verified on live Zed: initial "WidgetSync over Bluetooth" + interrupt
"over WiFi" -> one thread, zero empty-thread correction sends, agent response
retained the feature AND applied the correction. (The #2660 validation only
checked message completion/length, not retained context — hence the miss.)
See design/2026-06-19-incident-interrupt-during-boot-context-loss.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@lukemarsden
lukemarsden merged commit 1818bbf into main Jun 19, 2026
5 checks passed
@lukemarsden
lukemarsden deleted the fix/interrupt-boot-race-thread-fork branch June 19, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant