feat(bichat): resilient run client — request_id, text blocks, active-run SSE#24
Conversation
…run SSE Complements iota-uz/iota-sdk#747 with the client pieces of the resilient generation pipeline. Pure additions — no wire break. All features degrade gracefully when the backend doesn't implement the matching endpoints (older SDK commits). ## Idempotent send via request_id Every POST /bi-chat/stream now carries a client-generated requestId. The backend's SetNX dedupe collapses duplicate sends (double-click / cross-tab) onto a single run within the ~30 min dedupe window. Callers can override via SendMessageOptions.requestId for retry flows; otherwise one is auto-minted per call via crypto.randomUUID (with a Math.random fallback for older WebViews). ## Cursor-based reconnect via native EventSource New MessageTransport.subscribeRunEvents + HttpDataSource.subscribeRunEvents open a native EventSource against GET /bi-chat/stream/events. The browser handles reconnect + Last-Event-ID automatically on wifi drops / tab sleep — we listen for every documented server event type and settle the promise on a terminal (done / error). Callers can abort via a signal to close the EventSource on unmount. ## Intermediate text blocks - Added text_block_end to StreamChunk / StreamEvent / sseParser. - New utils/textBlocks.ts exports splitIntoTextBlocks + readTextBlockOffsets so components can split the accumulated assistant content at server- emitted byte offsets and render text → tool → text → tool → final_text as distinct blocks instead of one merged paragraph. ## Sidebar status fan-out - New ActiveRunDelivery type + subscribeActiveRuns on MessageTransport / HttpDataSource / ChatDataSource. - useActiveRuns hook subscribes on mount, aggregates the initial snapshot batch with a 16ms coalescer, then applies live update deltas (removing entries on terminal status). Components read one per-session status dictionary to render a status dot without polling /stream/status per session. ## Tests - textBlocks.test.ts (8 cases): empty / no-offsets / split / clamp / dedup-offsets / unicode boundary / metadata parser. - MessageTransport.test.ts: auto-generated requestId in POST body, caller-supplied requestId preserved verbatim. - pnpm run typecheck clean, pnpm run build green, eslint zero errors.
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 27 minutes and 17 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughAdds SSE streaming support for run-tail and tenant active-runs, client-side requestId generation for sendMessage, text-block segmentation utilities, a useActiveRuns React hook, and related tests and exports. Changes
Sequence Diagram(s)sequenceDiagram
participant App as App / Client
participant Hook as useActiveRuns Hook
participant Transport as MessageTransport
participant Server as Backend (SSE)
participant State as React State
App->>Hook: mount (dataSource, options)
Hook->>Transport: subscribeActiveRuns({ onEvent, onError, signal })
Transport->>Server: GET /active-runs (open SSE)
Server-->>Transport: SSE: snapshot / update
Transport->>Hook: onEvent(ActiveRunDelivery)
alt snapshot events (batched)
Hook->>Hook: stage snapshots, start coalesce timer
Hook->>State: merge snapshot batch, set ready=true
else update event
Hook->>State: apply update immediately
alt terminal status
Hook->>Hook: schedule prune or remove immediately
end
end
App->>Hook: unmount / abort
Hook->>Transport: signal abort
Transport->>Server: close EventSource
sequenceDiagram
participant App as Application
participant Transport as MessageTransport
participant Server as Backend
participant Dedup as Server Dedup Store
App->>Transport: sendMessage(payload, { requestId? })
alt requestId provided
Transport->>Transport: use provided requestId
else
Transport->>Transport: generate requestId (crypto.randomUUID() or fallback)
end
Transport->>Server: POST /messages { ..., requestId }
Server->>Dedup: check requestId
alt duplicate within window
Dedup-->>Server: return cached response
else
Server-->>Transport: process and stream response
end
Transport-->>App: emit StreamChunk events
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Native EventSource routes events with an `event:` line ONLY via
addEventListener — there is no onmessage fallback. subscribeRunEvents
registered listeners for {done, error, content, …} but not for the
`cancelled` / `failed` terminal names emitted by the backend on
cross-tab Stop or RunReaper stale-run sweeps. Effect: the client
promise never settles, EventSource auto-reconnects forever, users
see a stuck "thinking" indicator.
- Register listeners for cancelled / failed / citation.
- Extend the settle predicate to the full terminal set.
- Add a 500ms initial-connect grace: when onerror fires before the
first event arrives, settle with a typed RunEventsConnectError
instead of spinning the auto-reconnect. Distinguishes 401/404/503
at the boundary from transient mid-run flaps.
- Document the request_id / UUID v4 contract in the generator
jsdoc.
- openManagedEventSource: shared primitive for the two subscribeX functions; centralises listener wiring, abort handling, and the 500ms initial-connect probe so future fan-out helpers only need to map event names to business callbacks. - utils/eventNames.ts: hand-mirrored version of the backend's pkg/httpdto.StreamEventType constants. A drift-guard test reads the Go file at test time (skipped when the sibling tree is absent). - useActiveRuns: retainTerminalMs + emptyStateTimeoutMs options so consumers can render a "completed" pulse before the entry is pruned. - Remove duplicate ActiveRunSidebarEvent type; reuse public ActiveRunDelivery. - Add @testing-library/react + jsdom to devDependencies; fill in the missing hook + subscribeActiveRuns test coverage.
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
- Snapshot rows now clear any pending terminal-removal timer for the same sessionId before merging into staging. Previously, when retainTerminalMs > 0, an in-flight deletion could wipe a newly reconnected streaming snapshot. - useActiveRuns attaches .catch() on the subscribeActiveRuns promise so initial-connect failures (401 / 503) route through the onError callback instead of surfacing as unhandled rejections. - eventNames drift-guard test resolves the sibling iota-sdk path relative to __dirname so it runs anywhere the repos are checked out side-by-side; only skips (with a console.warn) when the sibling file is truly absent.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ui/src/bichat/utils/eventNames.ts`:
- Around line 46-52: The type guard isTerminalEvent currently narrows name to
StreamEventType but only checks membership in TERMINAL_STREAM_EVENT_TYPES;
change the return type to narrow to the specific terminal union (e.g., name is
TerminalStreamEventType or the exact union type represented by
TERMINAL_STREAM_EVENT_TYPES) and update the function signature accordingly so
the implementation and type predicate match (refer to isTerminalEvent,
TERMINAL_STREAM_EVENT_TYPES and StreamEventType); also update any callers that
rely on the broader StreamEventType if they need the wider type.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 181cd3ae-4b16-48cc-a6af-499aaed57460
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (11)
package.jsonui/src/bichat/data/MessageTransport.test.tsui/src/bichat/data/MessageTransport.tsui/src/bichat/data/openManagedEventSource.tsui/src/bichat/data/subscribeActiveRuns.test.tsui/src/bichat/hooks/useActiveRuns.test.tsxui/src/bichat/hooks/useActiveRuns.tsui/src/bichat/index.tsui/src/bichat/utils/eventNames.test.tsui/src/bichat/utils/eventNames.tsvitest.config.ts
✅ Files skipped from review due to trivial changes (2)
- package.json
- vitest.config.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- ui/src/bichat/data/MessageTransport.test.ts
CodeRabbit flagged that the type guard claimed to narrow to the full StreamEventType union while only checking membership in the 4-element terminal subset. Callers receiving `true` now get TerminalStreamEventType, which is the actual constrained set. TERMINAL_STREAM_EVENT_TYPES is retyped via `as const satisfies readonly StreamEventType[]` so the tuple's literal types power the narrower derived type without losing the containment invariant.
Summary
Client companion to iota-uz/iota-sdk#747 (resilient bichat generation pipeline, #732). Pure additions to `@iota-uz/sdk/bichat`; no wire break. All features degrade gracefully when the backend doesn't implement the matching endpoints (older SDK commits).
Changes
1. Idempotent send via `request_id`
2. Cursor-based reconnect via native EventSource
3. Intermediate text blocks
4. Sidebar status fan-out
Public API additions
```ts
// From @iota-uz/sdk/bichat
export type ActiveRunDelivery;
export { splitIntoTextBlocks, readTextBlockOffsets, type AssistantTextBlock };
export {
useActiveRuns,
type UseActiveRunsOptions,
type UseActiveRunsResult,
type ActiveRunSnapshot,
};
// HttpDataSource methods
subscribeRunEvents(sessionId, runId, { lastEventId?, onChunk, onError?, signal? });
subscribeActiveRuns({ onEvent, onError?, signal? });
// SendMessageOptions
requestId?: string;
// ChatDataSource optional methods
subscribeRunEvents?(…);
subscribeActiveRuns?(…);
```
`StreamEvent` gains one variant: `{ type: "text_block_end"; seq: number }`.
Test plan
Links
Summary by CodeRabbit
New Features
Tests