fix(workspace): foreman chat streams reactively + audit spec #573
No reviewers
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks!573
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/workspace-chat-streaming"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Two commits.
Streaming fix. The workspace foreman chat used to require a hard reload to see the assistant response. Three races layered together caused this:
assistant/resultframes were broadcast to no subscriber. Fixed by replaying buffered task events to a late-joining subscriber in/foreman/stream/:task_id.resultevent broadcast beforeappendMessagecommitted the assistant text to SQLite. The client's post-stream refetch raced the DB write and the asst flashed empty. Fixed by buffering theresultevent inrunForemanTurnuntil afterappendMessagereturns.phase=donetriggered, before react-query's refetch returned. Fixed by promoting?task=<id>to a URL search param (re-subscribable on reload) and only clearing it afteractiveSession.messages.lengthactually grows past its baseline.The legacy redirects (
/planner,/specs/*, the/quick-jump inuse-global-keymap) are patched to includetask: undefinedso the new search shape typechecks.Audit spec. Adds
specs/workspace-chat-overhaul.mdcapturing the chat surface gap against Claude Code CLI + t3code. Nine sections, each sized to one Forgejo ticket — issues #564–#572 already filed and labelled.Test plan
?session=new— assistant text streams and persists, no flash.?session=<id>— same behaviour.curl /foreman/stream/<id>— replay returns full event sequence.assistantevent timestamps precederesultevent by the appendMessage window (~470ms in tests).?task=<id>set, hard-reload — transcript fills via replay buffer.🤖 Generated with Claude Code
CI still pending at review time (run #1099, sha
130e257). Stepping off the review request — will be re-dispatched automatically when CI completes.CI still pending at review time (run #1099, sha
130e2576). Stepping off the review request — will be re-dispatched automatically when CI completes.CI green (run #2585). Three races correctly identified and fixed.
foreman.ts— bufferingpendingResultuntil afterappendMessagereturns is the right fix. The event is pushed torecord.eventsat the correct point (after the DB write), so the replay buffer also reflects the ordering.main.ts— replayingrecord.eventsbefore registering the SSE subscriber is correct. TheReadableStream.start()callback is synchronous, so there is no window between the replay tail and the subscriber registration where a live event can slip through.workspace.index.tsx— promotingstreamingTaskIdto a URL param is the right abstraction. The two-effect pattern (snapshot baseline → watch for growth → clear?task=) avoids the flash correctly.Nit: if
assistantTextis empty (foreman turn produced only tool calls, no text),appendMessageis skipped andpersistedMsgCountnever grows past baseline, so?task=<id>is permanently stranded in the URL. On every reload the replay deliversresult, phase goes todone, the refetch fires, count stays flat, and the param stays. Not a regression from pre-PR behaviour (local state was just lost on reload), and the Polish sweep spec already tracks the cleanup.?task=on tool-only turns via fallback timerAddressed the tool-only-turn nit in
c1413e6— added a 2 s fallback timer that strips?task=whenappendMessageis skipped server-side (and sopersistedMsgCountnever grows). Fast path (text-bearing turn) still clears the param the moment the persisted asst lands; fallback only fires when the message count stayed flat. Timer is cleared on the message-count path so the two never race.The polish-sweep cleanup (#572) is therefore a no-op for this specific case; updating the AC there to drop it.