Skip to content

fix(conversations): broadcast new chat to list live (#155)#255

Open
Ovaculos wants to merge 2 commits into
NimbleBrainInc:mainfrom
Ovaculos:fix/conversations-list-live-update
Open

fix(conversations): broadcast new chat to list live (#155)#255
Ovaculos wants to merge 2 commits into
NimbleBrainInc:mainfrom
Ovaculos:fix/conversations-list-live-update

Conversation

@Ovaculos
Copy link
Copy Markdown
Contributor

Summary

Fixes #155 — new conversations now appear in the Conversations list without a manual refresh.

Root cause: the runtime emitted data.changed onto the per-request chat sink, which never reaches the /v1/events SSE channel useDataSync consumes. The fix routes the emit through the runtime's default sink. Two broadcasts per new conversation — once right after the user message is persisted (label = first-message preview) and once after auto-title generation settles (label flips to the generated title).

Follow-up issues filed during this work

Two adjacent bugs surfaced while wiring the live-update path. Both are pre-existing and out of scope for #155:

Commits

  • refactor(conversations): switch ConversationIndex to pull-on-demand — replaces fs.watch + 500ms debounce with mtime-based reconciliation per call. Removes the timing-race class that the live broadcast would otherwise hit. get(id) keeps an id → filePath map so handleGet / handleFork / handleUpdate stat one file instead of walking.
  • fix(conversations): broadcast new chat to list live — the routing fix itself, plus hardening that keeps the broadcast path correct end-to-end: FaultIsolatedSink so a logger throw can't abort the SSE-broadcast wrap, AbortSignal.timeout so a hung fast-model call can't leave the title-block .finally pending, dead chat-stream data.changed branch removed from handlers.ts.

Known scope notes

  • data.changed is scope: "global" in SSE_ROUTES. Pre-existing; a new conversation in workspace A causes every workspace-B iframe on /v1/events to refetch (each gets its own dir's data, so no leak — wasted bytes only). Worth a follow-up when the payload grows wsId.
  • No broadcast on conversation resume. Deliberate — the conversation is already in the list. A "bump to top on resume" UX is a separate feature.

Test plan

  • bun run verify — unit / web 249 / integration 482 / smoke 17, 0 fail, 0 errors
  • ConversationIndex reflects new + deleted + modified files immediately (no debounce)
  • Pre-turn + post-title broadcasts reach the default (SSE-bound) sink for new conversations; no broadcast on resume
  • Manual: new chat appears in list instantly with its first-message preview, then label flips to the generated title

🤖 Generated with Claude Code

Ovaculos and others added 2 commits May 20, 2026 11:55
Replace the fs.watch + 500ms-debounce model with a pull-on-demand index.
Each `list` / `get` / `search` reconciles in-memory state with the
directory: stat every conversation file, reuse cached entries when
mtime is unchanged, re-read the header for new or modified files, drop
entries whose files have disappeared.

Removes a class of timing races: `fs.watch` delivery is variable
(especially under FSEvents on macOS), the 500ms debounce window let
write-then-list refetches return stale data, and the prior
`flushPending` shortcut had a concurrent-caller gap (caller B could
share caller A's in-flight snapshot and miss a filename queued
mid-flush).

`get(id)` keeps an `id → filePath` map so the common callers
(`handleGet`, `handleFork`, `handleUpdate`) stat one file instead of
walking the directory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A brand-new conversation did not appear in the Conversations list until
the user refreshed or switched tabs (NimbleBrainInc#155). The runtime emitted the
data.changed signal onto the per-request chat sink, which never reaches
the /v1/events SSE channel that useDataSync consumes. Route the emit
through the runtime's default sink (the one api/server.ts wraps for
/v1/events). Fire once right after the user message is persisted so the
conversation surfaces with its first-message preview as the label, and
once more after auto-title generation settles so the label flips to the
generated title.

Two adjacent issues surfaced during investigation and are filed
separately:

  - NimbleBrainInc#253: auto-generated titles often contain assistant response content
          instead of a short summary (pre-existing prompt-shape bug)
  - NimbleBrainInc#254: mid-turn conversation switch bleeds the streaming response
          into the destination chat (pre-existing client-side state
          contamination)

Supporting hardening in this commit, in service of the broadcast both
reaching its destination and surviving along the way:

  - FaultIsolatedSink wraps each sink in the defaultEvents fan-out so
    a logger throw can't abort the SSE-broadcast wrap downstream.
    Engine-time sink chain stays loud.
  - generateTitle uses AbortSignal.timeout to hard-cap a hung fast-model
    call so the title-block .finally always fires.
  - Dead chat-stream data.changed branch removed from handlers.ts
    (nothing relays it now; confirmed via grep).

Closes NimbleBrainInc#155

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

New chats do not appear in Conversations list until refresh or tab switch

1 participant