Skip to content

Scope live chat to watched threads with overlay-based streaming - #23

Merged
soorya-u merged 7 commits into
mainfrom
scoped-thread-live-stream
Jul 10, 2026
Merged

soorya-u merged 7 commits into
mainfrom
scoped-thread-live-stream

Conversation

@soorya-u

@soorya-u soorya-u commented Jul 10, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Replace global peer broadcast with a ThreadEventBus that fans out ChatChunks only to peers watching a thread, with active-turn replay on watchThread
  • Change chat to a unary { threadId, turnId } RPC; all live chunks flow through subscribe() into a Zustand overlay merged with the getConversations snapshot
  • Refactor client hooks: sendMessage awaits waitForTurnEnd, remove conversation-cache, add client-side token batching via requestAnimationFrame

Test plan

  • Single device: send message, tokens stream via subscribe, sendMessage resolves on turn_completed
  • Two devices on same thread: both see live stream; device on different thread receives nothing
  • Refresh mid-stream: partial tokens replay via watchThread active-turn log
  • Navigate away and back mid-stream: replay restores in-flight content
  • Stop/cancel: sendMessage rejects, overlay clears turn
  • bun check and bun check:types pass

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Chat responses now start turns immediately and stream updates through thread-specific subscriptions.
    • Added thread watch and unwatch controls with replay of active conversation updates.
    • Chat feeds now display assistant thinking states, loading indicators, and live progress more clearly.
    • Added resilient clipboard copying with automatic copied-state feedback.
    • Improved login authentication flow with the updated CLI identity.
  • Bug Fixes

    • Improved cancellation and stopping-state handling for active chat turns.
    • Enhanced conversation synchronization and recovery when turns complete or are interrupted.
    • Improved scrolling behavior during live chat updates.

soorya-u and others added 2 commits July 10, 2026 02:35
Replace global peer broadcast with ThreadEventBus, unary chat RPC, and watchThread/unwatchThread so subscribe only fans out to interested peers. Clients merge durable snapshots with a Zustand overlay and await turn completion via waitForTurnEnd instead of mutating the query cache.

Co-authored-by: Cursor <cursoragent@cursor.com>
Point the queue bus at @cyrus/connections/rtc/bus, remove unused stdin helper, align OAuth client id with cyrusd, and tighten small style nits across CLI and web.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cyrus Ready Ready Preview, Comment Jul 10, 2026 5:04pm

@coderabbitai

coderabbitai Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR replaces peer-wide chat streaming with thread-scoped event delivery and replay, changes chat to a unary turn-start RPC, adds thread watch operations, synchronizes client conversation state through live chunks, and adds thought/loading rendering.

Changes

Thread-scoped live chat

Layer / File(s) Summary
Contracts and event-bus architecture
shared/schemas/..., shared/connections/..., shared/database/..., shared/constants/...
Defines unary chat output, thread watch/unwatch schemas, event-bus typing, operation keys, and snapshot high-water mark retrieval.
Server delivery and worker wiring
apps/cli/src/handlers/controller/..., apps/cli/src/queue/bus.ts, shared/connections/src/rtc/..., apps/cli/src/commands/service/worker.ts
Runs turns asynchronously, publishes persisted and streaming chunks through ThreadEventBus, supports replay and thread watches, and replaces broadcaster lifecycle handling.
Client synchronization and conversation merging
shared/hooks/src/connection/..., shared/utils/src/conversations/...
Applies streamed chunks, merges cached and fetched entries, waits for terminal turn events, and coordinates watch, send, cancel, and subscription behavior.
Thought folding and chat feed
shared/utils/src/fold.ts, shared/hooks/src/use-thread-feed.ts, shared/schemas/src/view/index.ts, apps/web/src/components/chat/...
Adds thought state and deterministic turn folding, loading/thought feed entries, active-turn propagation, stopping controls, and feed scrolling updates.
Specifications and supporting updates
openspec/..., apps/web/src/hooks/..., apps/cli/src/commands/...
Documents the thread-streaming design and applies supporting OAuth, clipboard, directive, utility, and formatting changes.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

  • soorya-u/cyrus#5 — Shares the CLI OAuth CLIENT_ID change.
  • soorya-u/cyrus#6 — Relates to replacing broadcaster-based worker wiring with ThreadEventBus.
  • soorya-u/cyrus#20 — Shares the conversation folding and view-type changes, including thought handling.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.84% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: scoped thread-based live chat streaming with overlay-driven delivery.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch scoped-thread-live-stream

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (5)
apps/web/src/components/connection-error.tsx (1)

4-4: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove or track the dummy TODO before merging.

This leaves the connection-error component explicitly marked as unfinished. Either implement the intended change or replace this with a concrete issue reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/connection-error.tsx` at line 4, Remove the
placeholder TODO in the connection-error component by implementing the intended
behavior, or replace it with a concrete tracked issue reference describing the
remaining work.
openspec/changes/archive/2026-07-10-scoped-thread-live-stream/design.md (1)

39-39: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Specify a language for the fenced code block

The code block starting on line 39 has no language specifier, triggering markdownlint MD040. Add a language tag (e.g., text or plaintext) for proper rendering and lint compliance.

📝 Proposed fix
-```
+```text
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/changes/archive/2026-07-10-scoped-thread-live-stream/design.md` at
line 39, Specify a language identifier, such as text or plaintext, on the fenced
code block beginning near the referenced section of the design document to
satisfy markdownlint MD040.
shared/utils/src/merge-conversation.ts (1)

13-20: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider replacing localeCompare with simple string comparison for createdAt sorting.

createdAt values are ISO 8601 strings (new Date().toISOString()). Using localeCompare may introduce locale-specific behavior. A simple </> comparison is more predictable for ISO 8601 lexicographic ordering.

♻️ Proposed refactor
 	return merged.sort((left, right) => {
 		if (left.seq !== right.seq) {
 			if (left.seq === 0) return 1;
 			if (right.seq === 0) return -1;
 			return left.seq - right.seq;
 		}
-		return left.createdAt.localeCompare(right.createdAt);
+		return left.createdAt < right.createdAt ? -1 : left.createdAt > right.createdAt ? 1 : 0;
 	});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/utils/src/merge-conversation.ts` around lines 13 - 20, In the merge
sorting logic, replace locale-dependent createdAt ordering in the comparator
with direct string comparison using < and >, preserving the existing equality
behavior and seq ordering in the surrounding merge/sort function.
shared/hooks/src/connection/use-worker-conversation-sync.ts (1)

31-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

onSyncError uses console.error instead of the structured log.error from evlog used elsewhere in the codebase.

Other hooks in this layer (e.g., use-thread-conversation.ts) use log.error({ kind: "watch_thread", error, threadId }) from evlog for structured logging. Using console.error here is inconsistent and bypasses the structured logging pipeline.

♻️ Proposed refactor
+import { log } from "evlog";
+
 // ...
 const onSyncError = useEffectEvent((error: unknown) => {
-	console.error("worker conversation sync failed", error);
+	log.error({ kind: "worker_conversation_sync", error });
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/hooks/src/connection/use-worker-conversation-sync.ts` around lines 31
- 33, Replace the console.error call in onSyncError with the structured evlog
log.error API, matching the pattern used by use-thread-conversation.ts; include
an appropriate event kind and the received error in the structured payload.
shared/hooks/src/stores/conversation-overlay.ts (1)

278-282: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

getLiveEntries creates new ConversationEntry objects on every call, causing the liveEntries selector in useThreadConversation to always return a new reference and trigger re-renders on every store change.

Each call to getLiveEntries invokes chunkToEntry which generates new IDs (++overlayEntrySeq) and timestamps (new Date().toISOString()). Since Zustand's default Object.is equality check sees a new array reference each time, the selector triggers re-renders even when the relevant thread's overlay data hasn't changed. Consider selecting the raw live array from the store and converting to ConversationEntry[] via useMemo in the consumer, so re-renders only occur when the underlying live array reference actually changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/hooks/src/stores/conversation-overlay.ts` around lines 278 - 282,
Refactor getLiveEntries and its use in useThreadConversation so selectors return
the stable raw live array from the thread overlay, while chunkToEntry conversion
occurs in a useMemo keyed by that array reference. Preserve ConversationEntry
typing and ensure conversion only reruns when the underlying live data changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/cli/src/handlers/controller/chat.ts`:
- Around line 24-49: Update runTurn so terminal notifications are always
delivered independently of persistence failures: ensure turn_completed emission
failures do not convert a successfully completed turn into turn_interrupted, and
when turn_interrupted persistence fails, publish a fallback terminal event
directly to the bus (for example with seq: 0), catching and logging persistence
errors. Use the existing emit flow and bus/logging abstractions available around
runTurn, and verify activeTurnLogs cleanup still occurs.
- Around line 91-102: Add a rejection handler to the promise chain returned by
runTurn in the chat handler, alongside the existing tapError handling. Ensure
errors from initial emit calls or the turn_interrupted emit are logged and that
the handler prevents an unhandled promise rejection; preserve the existing
result.tapError behavior for handled turn errors.

In `@apps/web/src/hooks/use-copy-to-clipboard.ts`:
- Around line 27-31: copy() in use-copy-to-clipboard.ts swallows clipboard
failures by only calling tapError on the Result. Update copy to re-throw the
captured error after setting copied to false, so its Promise rejects on failure
and callers such as message-copy-button.tsx and use-desktop.ts trigger their
error paths instead of reporting success.

In
`@openspec/changes/archive/2026-07-10-scoped-thread-live-stream/specs/thread-live-stream/spec.md`:
- Line 5: Update the live ChatChunk routing specification to reference
ThreadEventBus in apps/cli/src/queue/bus.ts instead of
apps/cli/src/queue/index.ts, and update all matching design and task
documentation references to the same path.

In `@openspec/changes/archive/2026-07-10-scoped-thread-live-stream/tasks.md`:
- Line 11: Update checklist item 2.1 to reference the actual ThreadEventBus
implementation in apps/cli/src/queue/bus.ts, or add a deliberate re-export from
apps/cli/src/queue/index.ts if that is intended as the public entrypoint; keep
the path consistent with the worker import.

In `@shared/hooks/src/stores/conversation-overlay.ts`:
- Around line 141-157: settleTurnWaiter invokes waiter.onAbort() before settling
the waiter, causing every turn to reject as aborted and preventing the intended
outcome. Remove the unconditional onAbort call from settleTurnWaiter; rely on
the resolve/reject wrappers’ cleanup behavior to remove the abort listener, then
preserve the existing completed resolve and interrupted reject logic.
- Around line 125-139: Update getOrCreateOverlay to return a cloned
ThreadOverlay when an entry already exists, including copies of live and
activeTurnIds, so applyChunkToOverlay, applyWatermark, and clearTurn never
mutate prior Zustand state. In commitLiveChunk, when shouldSkipPersistedChunk
returns true, return the previous state unchanged instead of constructing a new
byThread Map.

---

Nitpick comments:
In `@apps/web/src/components/connection-error.tsx`:
- Line 4: Remove the placeholder TODO in the connection-error component by
implementing the intended behavior, or replace it with a concrete tracked issue
reference describing the remaining work.

In `@openspec/changes/archive/2026-07-10-scoped-thread-live-stream/design.md`:
- Line 39: Specify a language identifier, such as text or plaintext, on the
fenced code block beginning near the referenced section of the design document
to satisfy markdownlint MD040.

In `@shared/hooks/src/connection/use-worker-conversation-sync.ts`:
- Around line 31-33: Replace the console.error call in onSyncError with the
structured evlog log.error API, matching the pattern used by
use-thread-conversation.ts; include an appropriate event kind and the received
error in the structured payload.

In `@shared/hooks/src/stores/conversation-overlay.ts`:
- Around line 278-282: Refactor getLiveEntries and its use in
useThreadConversation so selectors return the stable raw live array from the
thread overlay, while chunkToEntry conversion occurs in a useMemo keyed by that
array reference. Preserve ConversationEntry typing and ensure conversion only
reruns when the underlying live data changes.

In `@shared/utils/src/merge-conversation.ts`:
- Around line 13-20: In the merge sorting logic, replace locale-dependent
createdAt ordering in the comparator with direct string comparison using < and
>, preserving the existing equality behavior and seq ordering in the surrounding
merge/sort function.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ff28240a-f896-4434-84f4-20cd835da217

📥 Commits

Reviewing files that changed from the base of the PR and between 6f454c7 and 1911856.

📒 Files selected for processing (47)
  • apps/cli/src/commands/agents/doctor.ts
  • apps/cli/src/commands/agents/rm.ts
  • apps/cli/src/commands/auth/login.ts
  • apps/cli/src/commands/auth/whoami.ts
  • apps/cli/src/commands/service/start.ts
  • apps/cli/src/commands/service/stop.ts
  • apps/cli/src/commands/service/worker.ts
  • apps/cli/src/handlers/controller/chat.ts
  • apps/cli/src/handlers/controller/threads.ts
  • apps/cli/src/lib/env.ts
  • apps/cli/src/queue/bus.ts
  • apps/cli/src/utils/io.ts
  • apps/web/src/components/auth/provider-button.tsx
  • apps/web/src/components/chat/main/thread-workspace.tsx
  • apps/web/src/components/connection-error.tsx
  • apps/web/src/components/sidebar/chat-sidebar-footer-actions.tsx
  • apps/web/src/components/sidebar/projects/thread-search-field.tsx
  • apps/web/src/hooks/use-auto-animate-ref.ts
  • apps/web/src/hooks/use-copy-to-clipboard.ts
  • apps/web/src/hooks/use-media-query.ts
  • apps/web/src/utils/dir.ts
  • openspec/changes/archive/2026-07-10-scoped-thread-live-stream/.openspec.yaml
  • openspec/changes/archive/2026-07-10-scoped-thread-live-stream/design.md
  • openspec/changes/archive/2026-07-10-scoped-thread-live-stream/proposal.md
  • openspec/changes/archive/2026-07-10-scoped-thread-live-stream/specs/conversation-overlay/spec.md
  • openspec/changes/archive/2026-07-10-scoped-thread-live-stream/specs/conversation-view/spec.md
  • openspec/changes/archive/2026-07-10-scoped-thread-live-stream/specs/thread-live-stream/spec.md
  • openspec/changes/archive/2026-07-10-scoped-thread-live-stream/specs/wire-schemas/spec.md
  • openspec/changes/archive/2026-07-10-scoped-thread-live-stream/tasks.md
  • openspec/specs/conversation-overlay/spec.md
  • openspec/specs/conversation-view/spec.md
  • openspec/specs/thread-live-stream/spec.md
  • openspec/specs/wire-schemas/spec.md
  • shared/connections/src/contracts/controller.ts
  • shared/connections/src/rtc/bus.ts
  • shared/connections/src/rtc/peer.ts
  • shared/connections/src/rtc/worker/index.ts
  • shared/constants/src/operation-keys.ts
  • shared/database/src/repositories/conversations.ts
  • shared/hooks/src/connection/use-controller-threads.ts
  • shared/hooks/src/connection/use-thread-conversation.ts
  • shared/hooks/src/connection/use-worker-conversation-sync.ts
  • shared/hooks/src/stores/conversation-overlay.ts
  • shared/schemas/src/rtc/chat.ts
  • shared/schemas/src/rtc/threads.ts
  • shared/utils/src/conversation-cache.ts
  • shared/utils/src/merge-conversation.ts
💤 Files with no reviewable changes (5)
  • apps/web/src/components/sidebar/projects/thread-search-field.tsx
  • apps/cli/src/utils/io.ts
  • shared/utils/src/conversation-cache.ts
  • apps/web/src/components/auth/provider-button.tsx
  • apps/cli/src/lib/env.ts

Comment thread apps/cli/src/handlers/controller/chat.ts
Comment thread apps/cli/src/handlers/controller/chat.ts Outdated
Comment thread apps/web/src/hooks/use-copy-to-clipboard.ts
Comment thread openspec/changes/archive/2026-07-10-scoped-thread-live-stream/tasks.md Outdated
Comment thread shared/hooks/src/stores/conversation-overlay.ts Outdated
Comment thread shared/hooks/src/stores/conversation-overlay.ts Outdated
Ensure terminal chat events always reach the bus, fix waitForTurnEnd settlement, clone overlay state immutably, rethrow clipboard copy failures, and align OpenSpec paths with queue/bus.ts.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@openspec/changes/archive/2026-07-10-scoped-thread-live-stream/specs/thread-live-stream/spec.md`:
- Line 26: Update the ThreadEventBus activeTurnLogs requirement to impose
explicit size and/or age limits, evict buffers when turns are cancelled or fail
without publishing turn_completed or turn_interrupted, and clear all remaining
buffers during worker shutdown. Specify the behavior when replay data is
truncated, including whether clients receive a fresh snapshot afterward.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d28c5257-8ffc-48b5-9bb8-962ae8062cf6

📥 Commits

Reviewing files that changed from the base of the PR and between 1911856 and 796a5f6.

📒 Files selected for processing (8)
  • apps/cli/src/handlers/controller/chat.ts
  • apps/web/src/hooks/use-copy-to-clipboard.ts
  • openspec/changes/archive/2026-07-10-scoped-thread-live-stream/design.md
  • openspec/changes/archive/2026-07-10-scoped-thread-live-stream/proposal.md
  • openspec/changes/archive/2026-07-10-scoped-thread-live-stream/specs/thread-live-stream/spec.md
  • openspec/changes/archive/2026-07-10-scoped-thread-live-stream/tasks.md
  • openspec/specs/thread-live-stream/spec.md
  • shared/hooks/src/stores/conversation-overlay.ts
✅ Files skipped from review due to trivial changes (2)
  • openspec/changes/archive/2026-07-10-scoped-thread-live-stream/proposal.md
  • openspec/changes/archive/2026-07-10-scoped-thread-live-stream/tasks.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/web/src/hooks/use-copy-to-clipboard.ts
  • openspec/changes/archive/2026-07-10-scoped-thread-live-stream/design.md
  • shared/hooks/src/stores/conversation-overlay.ts
  • apps/cli/src/handlers/controller/chat.ts

Specify per-turn chunk limits, shutdown cleanup, and durable snapshot
reconciliation in thread-live-stream specs; replace console.error with
structured evlog in chat and conversation sync paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ow-up UX.

Replace the overlay store with shared conversation cache utilities, turn waiters, and subscribe-driven sync so live chunks and refetches stay consistent. Fix cancel races, message ordering after interrupt, and render collapsible thinking in the feed.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
openspec/specs/thread-live-stream/spec.md (1)

30-36: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve emission order across concurrent turns. watchThread replays buffered chunks per turnId, so interleaved events from concurrent turns on the same thread can come back out of emission order (A1, B1, A2 → A1, A2, B1). Define a single per-thread replay order or prohibit concurrent turns on a thread.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openspec/specs/thread-live-stream/spec.md` around lines 30 - 36, Preserve
global emission order when replaying active turn logs from
ThreadEventBus.watchThread: per-turn buffers cannot independently replay
interleaved chunks without reordering them. Add a per-thread ordered replay
structure (or explicitly enforce one active turn per thread), and update
buffering, replay, cleanup, and closeAll logic so concurrent turns replay in
original emission order.
openspec/changes/archive/2026-07-10-scoped-thread-live-stream/specs/thread-live-stream/spec.md (1)

26-36: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Preserve emission order across overlapping turns on the same thread.

watchThread replays activeTurnLogs turn-by-turn, so concurrent turns on one thread can come back reordered (A1, B1, A2 → A1, A2, B1). Merge replay into a single per-thread order, or forbid overlapping turns per thread.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@openspec/changes/archive/2026-07-10-scoped-thread-live-stream/specs/thread-live-stream/spec.md`
around lines 26 - 36, Preserve chronological emission order when replaying
concurrent active turns on the same thread; the current turn-by-turn replay of
activeTurnLogs can reorder chunks. Update ThreadEventBus.watchThread and its
activeTurnLogs replay logic to maintain a per-thread ordered sequence of
buffered ChatChunks, or explicitly prevent overlapping turns for a thread,
ensuring replay produces A1, B1, A2 rather than grouping by turn.
apps/web/src/components/chat/main/thread-workspace.tsx (1)

46-56: 🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

thread in the effect dependency array causes the effect to run on every render.

thread is created via spread ({ ...baseThread, ...conversation }) on every render, producing a new object reference each time. This makes the effect fire on every render even when nothing relevant changed. The navigation guard prevents incorrect behavior, but the wasted execution is unnecessary.

Depend on the primitive fields actually used inside the effect instead:

♻️ Proposed fix
 	useEffect(() => {
-		if (thread && thread.projectId !== projectId)
+		if (thread && thread.projectId !== projectId)
 			navigate({
 				to: "/workers/$workerId/p/$projectId/t/$threadId",
 				params: {
 					workerId,
 					projectId: thread.projectId,
 					threadId: thread.id,
 				},
 			});
-	}, [navigate, projectId, thread, workerId]);
+	}, [navigate, projectId, thread?.projectId, thread?.id, workerId]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/chat/main/thread-workspace.tsx` around lines 46 - 56,
Update the useEffect dependency array in the thread workspace component to
replace the object dependency thread with the primitive values used by the
effect, specifically thread?.projectId and thread?.id, while retaining navigate,
projectId, and workerId.
🧹 Nitpick comments (1)
shared/utils/src/fold.ts (1)

387-427: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: de-duplicate the turn-order sort comparator.

The leftTurn/rightTurn lookup via turnOrder (Lines 390-396 and 408-413) is duplicated between the thoughts and messages sorts. Extracting a small compareByTurnOrder(turnOrder, left, right) helper would keep the two ordering rules from drifting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shared/utils/src/fold.ts` around lines 387 - 427, Optionally deduplicate the
repeated turn-order comparison logic in the thoughts and messages sorting within
the fold result. Extract a compareByTurnOrder helper that accepts turnOrder and
two items, performs the existing turnId lookup with the MAX_SAFE_INTEGER
fallback, and returns the ordering difference; invoke it from both sort
callbacks while preserving their existing tie-breakers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/cli/src/handlers/controller/chat.ts`:
- Around line 172-188: Filter the snapshotted turn IDs before publishing
interruption events in the cancel handler: after await
runtime.threadCoordinator.cancel, call
context.eventBus.getActiveTurnIdsForThread(input.threadId) again and publish
turn_interrupted only for IDs still present in the current active set,
preserving the existing cancellation flow.

In `@apps/web/src/components/chat/feed/chat-feed.tsx`:
- Around line 39-43: Update the auto-scroll useEffect in the chat feed component
to depend on conversation changes as well as feed length, so streaming content
updates trigger scrolling. Preserve the existing empty-feed guard and
requestAnimationFrame cleanup, and add the conversation reference to the effect
dependency array.

In
`@openspec/changes/archive/2026-07-10-scoped-thread-live-stream/specs/thread-live-stream/spec.md`:
- Line 26: Prevent post-shutdown event publication from recreating state after
ThreadEventBus.closeAll(). Add a shutdown fence in the worker lifecycle to stop
and await in-flight turns before calling closeAll(), or update
ThreadEventBus.publish() to reject once shutdown starts; ensure closeAll() and
the fence coordinate so activeTurnLogs and turnThreads cannot be repopulated.

In `@openspec/specs/thread-live-stream/spec.md`:
- Line 30: Make ThreadEventBus shutdown resistant to late publishes: update
closeAll() and publish() so shutdown sets a closed/stopping state before
clearing activeTurnLogs and turnThreads, and publish() rejects or ignores all
subsequent events; ensure closeAll() remains safe and idempotent.
- Line 30: Update the ThreadEventBus turn interruption flow and related
specification sections so non-delta events remain persist-before-broadcast,
while a failed durable terminal-event write still publishes turn_interrupted
through a non-durable or explicitly exempt path. Ensure the failure path always
evicts the turn’s activeTurnLogs buffer and documents this behavior consistently
with the worker’s cancellation and error handling.

In `@shared/utils/src/conversations/cache.ts`:
- Line 14: Prevent completedTurnKeys from growing indefinitely by removing each
turn’s key when its ephemeral cache entry is pruned. Update
pruneEphemeralTurnEntries to delete the corresponding key from
completedTurnKeys, while preserving removal in removeTurnFromCache and ensuring
terminal-event tracking remains correct.

---

Outside diff comments:
In `@apps/web/src/components/chat/main/thread-workspace.tsx`:
- Around line 46-56: Update the useEffect dependency array in the thread
workspace component to replace the object dependency thread with the primitive
values used by the effect, specifically thread?.projectId and thread?.id, while
retaining navigate, projectId, and workerId.

In
`@openspec/changes/archive/2026-07-10-scoped-thread-live-stream/specs/thread-live-stream/spec.md`:
- Around line 26-36: Preserve chronological emission order when replaying
concurrent active turns on the same thread; the current turn-by-turn replay of
activeTurnLogs can reorder chunks. Update ThreadEventBus.watchThread and its
activeTurnLogs replay logic to maintain a per-thread ordered sequence of
buffered ChatChunks, or explicitly prevent overlapping turns for a thread,
ensuring replay produces A1, B1, A2 rather than grouping by turn.

In `@openspec/specs/thread-live-stream/spec.md`:
- Around line 30-36: Preserve global emission order when replaying active turn
logs from ThreadEventBus.watchThread: per-turn buffers cannot independently
replay interleaved chunks without reordering them. Add a per-thread ordered
replay structure (or explicitly enforce one active turn per thread), and update
buffering, replay, cleanup, and closeAll logic so concurrent turns replay in
original emission order.

---

Nitpick comments:
In `@shared/utils/src/fold.ts`:
- Around line 387-427: Optionally deduplicate the repeated turn-order comparison
logic in the thoughts and messages sorting within the fold result. Extract a
compareByTurnOrder helper that accepts turnOrder and two items, performs the
existing turnId lookup with the MAX_SAFE_INTEGER fallback, and returns the
ordering difference; invoke it from both sort callbacks while preserving their
existing tie-breakers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 365f8e5c-a874-49b9-a455-18ebf8b3d590

📥 Commits

Reviewing files that changed from the base of the PR and between 796a5f6 and 59cb5f1.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (23)
  • apps/cli/package.json
  • apps/cli/src/handlers/controller/chat.ts
  • apps/cli/src/queue/bus.ts
  • apps/web/src/components/chat/composer/index.tsx
  • apps/web/src/components/chat/composer/primary-action.tsx
  • apps/web/src/components/chat/feed/chat-feed.tsx
  • apps/web/src/components/chat/feed/feed-entry-view.tsx
  • apps/web/src/components/chat/main/thread-workspace.tsx
  • apps/web/src/components/chat/messages/assistant-loading.tsx
  • apps/web/src/components/chat/messages/assistant-thinking.tsx
  • openspec/changes/archive/2026-07-10-scoped-thread-live-stream/specs/thread-live-stream/spec.md
  • openspec/specs/thread-live-stream/spec.md
  • shared/connections/src/rtc/bus.ts
  • shared/hooks/src/connection/use-controller-threads.ts
  • shared/hooks/src/connection/use-thread-conversation.ts
  • shared/hooks/src/connection/use-worker-conversation-sync.ts
  • shared/hooks/src/use-thread-feed.ts
  • shared/schemas/src/rtc/chat.ts
  • shared/schemas/src/view/index.ts
  • shared/utils/src/conversations/cache.ts
  • shared/utils/src/conversations/merger.ts
  • shared/utils/src/conversations/turn-waiters.ts
  • shared/utils/src/fold.ts
✅ Files skipped from review due to trivial changes (2)
  • apps/web/src/components/chat/messages/assistant-loading.tsx
  • shared/connections/src/rtc/bus.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/cli/src/queue/bus.ts

Comment thread apps/cli/src/handlers/controller/chat.ts
Comment thread apps/web/src/components/chat/feed/chat-feed.tsx Outdated
Comment thread openspec/specs/thread-live-stream/spec.md Outdated
Comment thread shared/utils/src/conversations/cache.ts
soorya-u and others added 2 commits July 10, 2026 22:32
Re-check active turns after cancel before publishing interrupts, scroll the feed on streaming conversation updates, ignore publishes after bus shutdown, prune completedTurnKeys with ephemeral entries, and clarify terminal-event specs.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use a derived streaming content key instead of the conversation object so scroll updates during token/thought streaming without tripping exhaustive-deps.

Co-authored-by: Cursor <cursoragent@cursor.com>

This branch was successfully deployed

1 active deployment
Preview — f7753fa7 Deployed Jul 10, 2026 by vercel[bot]
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.

Scope live chat broadcast to peers watching that thread, not every connected peer

1 participant