nexus/.planning/phases/22-agent-streaming/22-01-SUMMARY.md

6.2 KiB

phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
22-agent-streaming 01 chat-streaming
sse
streaming
chat
websockets-alternative
abort
requires provides affects
22-00
chat-streaming-endpoint
useStreamingChat-hook
chat-edit-truncate-api
server/src/routes/chat.ts
server/src/services/chat.ts
ui/src/hooks/useStreamingChat.ts
ui/src/api/chat.ts
added patterns
@testing-library/react ^16.0.0 (devDep)
SSE via fetch ReadableStream (not EventSource — POST-based stream)
AbortController for stop generation (CHAT-12)
startTransition for non-blocking token accumulation (PERF-02)
res.flushHeaders() before for-await loop for sub-100ms first-token (PERF-02)
res.writable guard on all res.write() calls (prevents write-after-end)
created modified
ui/src/hooks/useStreamingChat.ts
ui/src/hooks/useStreamingChat.test.ts
server/src/services/chat.ts
server/src/routes/chat.ts
ui/src/api/chat.ts
ui/package.json
Used fetch with ReadableStream instead of EventSource because endpoint is POST-based (EventSource only supports GET)
Partial content on stop saved with [stopped] suffix via savePartialMessage (Open Question 3 resolved)
streamEcho is a stub async generator yielding word-by-word with 50ms delay — real LLM in Phase 23
chatMessages schema has no updatedAt column so editMessage only sets content (adapted from plan which assumed updatedAt)
Added @testing-library/react as devDep (plan said already installed but it was not present)
duration completed tasks files
~6 minutes 2026-04-01 2 6

Phase 22 Plan 01: SSE Streaming Endpoint and useStreamingChat Hook Summary

One-liner: SSE streaming endpoint with stub echo generator, AbortController stop, partial-content persistence, and fetch ReadableStream client hook.

Tasks Completed

Task Name Commit Key Files
1 Server SSE streaming endpoint + edit/truncate service methods 2d711c7e server/src/services/chat.ts, server/src/routes/chat.ts
2 useStreamingChat hook, chat API stream method, real unit tests 78742239 ui/src/hooks/useStreamingChat.ts, ui/src/hooks/useStreamingChat.test.ts, ui/src/api/chat.ts

What Was Built

Server (Task 1)

server/src/services/chat.ts — Three new methods added to chatService:

  • editMessage(messageId, content) — Updates message content in DB (adapted: no updatedAt column in chatMessages schema)
  • truncateMessagesAfter(conversationId, messageId) — Deletes all messages after a given createdAt timestamp; imports gt from drizzle-orm
  • streamEcho(content, signal) — Async generator stub yielding words with 50ms delay, respects AbortSignal

server/src/routes/chat.ts — Three new routes:

  • POST /conversations/:id/stream — SSE endpoint; headers flushed (PERF-02) before for-await loop; saves full content as assistant message on completion; handles req.on("close") abort
  • PATCH /conversations/:id/messages/:msgId — Edit message content
  • DELETE /conversations/:id/messages/after/:msgId — Truncate subsequent messages

Client (Task 2)

ui/src/api/chat.ts — Two new methods:

  • postMessageAndStream — Opens POST fetch with ReadableStream, parses SSE data lines, dispatches onToken/onDone/onError callbacks
  • savePartialMessage — Delegates to postMessage to persist partial content on stop

ui/src/hooks/useStreamingChat.ts — New hook:

  • startStream(userMessage, agentId?) — Creates AbortController, calls postMessageAndStream, wraps setStreamingContent in startTransition
  • stop() — Aborts controller, saves partial content with " [stopped]" suffix, invalidates queries
  • Returns: { streamingContent, isStreaming, startStream, stop }

ui/src/hooks/useStreamingChat.test.ts — 5 passing unit tests:

  1. Token accumulation into streamingContent
  2. isStreaming lifecycle (true on start, false on done)
  3. stop() aborts controller and sets isStreaming=false
  4. SSE error sets isStreaming=false
  5. null conversationId guard

Verification Results

  • pnpm --filter @paperclipai/ui exec -- tsc --noEmit — PASS (0 errors)
  • pnpm --filter @paperclipai/server exec -- tsc --noEmit (chat files) — PASS (0 chat errors; pre-existing plugin-sdk errors in unrelated files)
  • vitest: 5/5 tests passing, 0 todos
  • PERF-02: flushHeaders() position (pre-loop) verified via Python script

Deviations from Plan

Auto-fixed Issues

1. [Rule 2 - Missing Dependency] @testing-library/react not installed

  • Found during: Task 2 test execution
  • Issue: Plan stated renderHook from @testing-library/react was "already installed" but the package was not present in ui/package.json or the pnpm lockfile
  • Fix: Added "@testing-library/react": "^16.0.0" to ui/package.json devDependencies and ran pnpm install --filter @paperclipai/ui
  • Files modified: ui/package.json, pnpm-lock.yaml
  • Commit: 78742239

2. [Rule 1 - Bug] chatMessages schema has no updatedAt column

  • Found during: Task 1 — implementing editMessage
  • Issue: Plan's editMessage code included .set({ content, updatedAt: new Date() }) but the chatMessages schema only has id, conversationId, role, content, agentId, createdAt — no updatedAt
  • Fix: Removed updatedAt: new Date() from the update set; only content is updated
  • Files modified: server/src/services/chat.ts
  • Commit: 2d711c7e

3. [Rule 3 - Blocking] Worktree branch was behind phase 22 foundation

  • Found during: Initial setup
  • Issue: The worktree branch worktree-agent-a8157dfc was on nexus/main commits that diverged from phase 22; chat files did not exist
  • Fix: Reset worktree branch to gsd/phase-22-agent-streaming with git reset --hard
  • Impact: Nexus-specific commits (phase 01-04 branding/onboarding work) are not present on this worktree; those exist on nexus/main branch and are not related to phase 22

Known Stubs

  • streamEcho in server/src/services/chat.ts is a stub that echoes back the user message word-by-word with 50ms delay. This is intentional for Phase 22 (no LLM). Phase 23 will replace this with real LLM adapter calls.

Self-Check: PASSED