Company-mode UIs became unusably slow once work items and transcripts
grew. Three presentation-layer fixes, none touching work progression:
- App.tsx: buffer assistant_delta/thinking_delta store writes per task
and flush every 80ms instead of once per token; any non-delta event
for the same task flushes first so draft/clearDraft ordering is
byte-identical. The unconditional per-event setUiTick (whole-app
re-render per websocket event) is now a 300ms trailing throttle.
- PhaserGame: display:none does not stop requestAnimationFrame, so the
office scene kept burning CPU on every other page. The loop now
sleeps when the office page is hidden and wakes (with a parent-bounds
refresh) on return; bridge writes stay synchronous so no state is lost.
- Kanban collab_sync debounce 0.2s -> 1.5s: the broadcaster is a
trailing coalescer, so the final board state still always ships; each
fire is a full-project snapshot build, which at 5/s dominated backend
CPU on large projects. CommsPanel poll 8s -> 30s (comms_state_dirty
push already drives freshness) and its interval no longer pins a
stale onRefresh closure.
Verified: tsc + vite build, App.test.tsx / workItemSessions structural
tests, backend suites (company_review_flow incl. debounce push test,
kanban_push_runtime, actor_runtime_company_mode, task_mode_contract,
work_item_transition — 119 green), and canvas_smoke e2e against a real
server with zero console errors.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Typed-3-shown-5 forensics (project 000): the WS client queues session_send
payloads while disconnected and flushes the queue after a reconnect, and the
server minted a fresh row id per delivery — so one typed message could land
as several user turns, each dispatched to the engine.
Every send now carries a client-generated ui_message_id (dispatchSessionSend
injects one when the caller didn't). The handler persists the user row under
that id and answers any later delivery in the same channel with an idempotent
ack instead of inserting and dispatching again. Because the row id now equals
the optimistic bubble's ui_message_id, the echo also merges with the local
message even after the transcript sync rewrites row metadata.
Same text intentionally sent again gets a fresh id and still starts a new turn.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>