Skip to content

ai-bot: add AI_BOT_STREAMING_MODE=off to skip mid-stream Matrix edits#5556

Merged
lukemelia merged 1 commit into
mainfrom
cs-12260-ai-bot-streaming-mode-off
Jul 21, 2026
Merged

ai-bot: add AI_BOT_STREAMING_MODE=off to skip mid-stream Matrix edits#5556
lukemelia merged 1 commit into
mainfrom
cs-12260-ai-bot-streaming-mode-off

Conversation

@lukemelia

Copy link
Copy Markdown
Contributor

Summary

  • Adds AI_BOT_STREAMING_MODE env var to packages/ai-bot/lib/responder.ts. Default is room-edits (current behavior); off skips the throttled per-chunk m.replace edit and only emits the initial thinking placeholder plus the final consolidated event at stream end.
  • Reduces Matrix room events per turn from ~600 to 2 in off mode.
  • Cancellation and final-event semantics (isStreamingFinished, isCanceled, continuation chaining for >16kB bodies) route through the same finalize/flush path in both modes — only the onChunk call is gated.

Context

Phase 1 of CS-12124, the plan for the Improve Synapse Performance by Killing Streaming project. Synapse was observed at 125% CPU with a single active AI Assistant user, driven by ~600 m.replace edits per response. This PR ships the lowest-risk mode — pure "no streaming" — so we can measure the immediate CPU relief and dogfood the fallback UX while CS-12263 adds streaming back over Matrix's sendToDevice transport.

The to-device value is accepted by the getter but behaves like room-edits today; the substitution comes in CS-12263.

Test plan

  • New Responding test: in off mode, 10 rapid chunks + finalize() produce exactly 2 events (thinking + final) with isStreamingFinished=true and m.replace pointing at the placeholder.
  • All 20 existing Responding tests pass.
  • All 5 Responder Cancellation tests pass — finalize({isCanceled:true}) still emits the final isCanceled event through the same path.
  • Staging: set AI_BOT_STREAMING_MODE=off on the ai-bot task, run a chat turn, verify only 2 room events land per turn and Synapse CPU drops on the dashboard.

In `off` mode the responder emits only the initial thinking placeholder
and the final consolidated `m.replace` edit at stream end, cutting room
events per response from ~600 to 2. Default (`room-edits`) is unchanged.
Cancellation and final-event semantics (isStreamingFinished, isCanceled,
continuation chaining) route through the same path in both modes — only
the throttled onChunk send is gated.

Phase 1 of CS-12124.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lukemelia
lukemelia marked this pull request as ready for review July 20, 2026 22:22
@lukemelia
lukemelia requested review from a team and jurgenwerk July 20, 2026 22:22
@habdelra
habdelra requested a review from Copilot July 20, 2026 22:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an AI_BOT_STREAMING_MODE environment variable in the ai-bot responder to allow disabling mid-stream Matrix room edits, aiming to reduce the volume of m.replace events during a response.

Changes:

  • Add AI_BOT_STREAMING_MODE parsing in Responder (defaulting to current “room-edits” behavior).
  • Gate per-chunk throttled sends in off mode so intermediate m.replace edits are skipped.
  • Add a new Responding test asserting off mode produces only the thinking placeholder plus a single final consolidated event.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
packages/ai-bot/lib/responder.ts Adds streamingMode env var handling and gates per-chunk message sends in off mode.
packages/ai-bot/tests/responding-test.ts Adds a regression test for off mode ensuring no intermediate sends occur and the final event is consolidated.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +172 to 174
if (responseStateChanged && this.streamingMode !== 'off') {
await this.sendMessageEventWithThrottling();
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Confirmed and fixed in 5cf06f8. The mid-turn gate at this line was correct; the bug was upstream — onChunk was flipping responseState.isStreamingFinished from the terminal chunks finish_reason: "stop", so finalize saw no transition and its send path was skipped too. Fix defers that flag transition to finalize()whenshouldStreamMidTurn` is false, so the final consolidated event always lands via the flush path.

Comment on lines +272 to +274
for (let i = 0; i < 10; i++) {
await responder.onChunk({} as any, snapshotWithContent('content ' + i));
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Good catch — updated in 5cf06f8. The 10th onChunk now carries choices: [{ finish_reason: "stop", ... }], which reproduces the missing-final-event bug without the fix and passes with it.

@jurgenwerk jurgenwerk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks ok but the copilot review seems important to address

@lukemelia
lukemelia merged commit f517c98 into main Jul 21, 2026
24 checks passed
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.

3 participants