Skip to content

fix: guard addStreamMessage with isStreaming() to avoid stream-completed race#1208

Open
HaiyiMei wants to merge 1 commit into
cyrusagents:mainfrom
HaiyiMei:fix/isrunning-check-stream-completed
Open

fix: guard addStreamMessage with isStreaming() to avoid stream-completed race#1208
HaiyiMei wants to merge 1 commit into
cyrusagents:mainfrom
HaiyiMei:fix/isrunning-check-stream-completed

Conversation

@HaiyiMei
Copy link
Copy Markdown

Bug

After #1141 began calling streamingPrompt.complete() on the SDK result message under non-warm mode, a brief window opens where:

  • ClaudeRunner.isRunning() still returns true (because sessionInfo.isRunning is flipped after the for-await loop exits — see ClaudeRunner.ts:806)
  • streamingPrompt.completed is already true

Webhook handlers that guarded addStreamMessage(...) with isRunning() alone passed the check, then StreamingPrompt.addMessage() threw "Cannot add message to completed stream". The exception was logged but not caught, so the user's comment was silently dropped — after Cyrus had already posted "I've queued up your message as guidance" in the Linear UI. Confusing failure mode.

Symptom from production logs

2026-05-13T11:53:01.920Z [ERROR] [EdgeWorker] Failed to handle prompted webhook:
Error: Cannot add message to completed stream
at ClaudeRunner.addStreamMessage (.../cyrus-claude-runner/dist/ClaudeRunner.js:299:30)

User's view: they replied to an agent session, saw "queued as guidance", waited 9 minutes for the agent to act on it, nothing happened. The comment had to be re-posted to take effect.

Fix

IAgentRunner.isStreaming?() is already part of the interface — see agent-runner-types.ts:296:

"Use this to guard calls to addStreamMessage()."
ClaudeRunner.isStreaming() correctly checks both streamingPrompt !== null && !streamingPrompt.completed && isRunning(). ChatSessionHandler.ts:141-143 already gates on isStreaming?.(). The four EdgeWorker sites did not — this PR adds && existingRunner.isStreaming?.() to each guard:

  • EdgeWorker.ts:1512 — base-branch change notification streaming
  • EdgeWorker.ts:3521 — issue-update streaming delivery (CYPACK-954 path)
  • EdgeWorker.ts:6619handlePromptWithStreamingCheck (main prompted-webhook entry)
  • EdgeWorker.ts:6706resumeAgentSession early-return when stream is alive
    When the guard fails, callers fall through to the proper query({ resume: ... }) path, preserving the user's message instead of dropping it.

Test changes

EdgeWorker.issue-update-multiple-sessions.test.ts — the mock factory at line 84 was missing isStreaming on the agent runner mock. Other test files already mock both. Added isStreaming: vi.fn().mockReturnValue(isRunning) so the streaming branch is reachable in tests.

Verification

  • All 611 edge-worker tests pass (pnpm --filter cyrus-edge-worker test:run).
  • pnpm --filter cyrus-edge-worker typecheck clean.
  • pnpm build clean across the workspace.
  • Deployed locally to a Cyrus production install for ~30 min before reverting; no regressions observed during the test window.

Related

…ted race

After cyrusagents#1141 started calling streamingPrompt.complete() on result message
under non-warm mode, there is a brief window where isRunning() still
returns true but streamingPrompt.completed is already true. Webhook
handlers that called existingRunner.addStreamMessage(...) on the basis
of isRunning() alone would throw "Cannot add message to completed
stream", silently dropping the user's message after Cyrus had already
posted "I've queued up your message as guidance".

IAgentRunner.isStreaming?() is documented in agent-runner-types.ts as
"Use this to guard calls to addStreamMessage()". ChatSessionHandler.ts
already does this. Add it at the four EdgeWorker.ts sites that did not:

- handlePromptWithStreamingCheck (the main prompted-webhook entry)
- resumeAgentSession (early-return when stream is alive)
- issue-update streaming delivery (CYPACK-954 path)
- base-branch change notification streaming

When the guard fails, callers fall through to query({resume: ...}) and
the user's comment is delivered via the normal resume path instead of
being lost. Test mock factory for the issue-update path updated to
mirror isRunning into isStreaming so the streaming branch is reachable.

Repro: send a comment in an active agent session thread; if it arrives
in the window between SDK result emission and runner teardown (~tens of
ms), Cyrus acknowledges "queued up as guidance" but logs the throw and
drops the message. With this fix the comment falls through to resume.
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