118 lines
6.2 KiB
Markdown
118 lines
6.2 KiB
Markdown
---
|
|
phase: 22-agent-streaming
|
|
plan: "01"
|
|
subsystem: chat-streaming
|
|
tags: [sse, streaming, chat, websockets-alternative, abort]
|
|
dependency_graph:
|
|
requires: [22-00]
|
|
provides: [chat-streaming-endpoint, useStreamingChat-hook, chat-edit-truncate-api]
|
|
affects: [server/src/routes/chat.ts, server/src/services/chat.ts, ui/src/hooks/useStreamingChat.ts, ui/src/api/chat.ts]
|
|
tech_stack:
|
|
added: ["@testing-library/react ^16.0.0 (devDep)"]
|
|
patterns:
|
|
- "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)"
|
|
key_files:
|
|
created:
|
|
- ui/src/hooks/useStreamingChat.ts
|
|
- ui/src/hooks/useStreamingChat.test.ts
|
|
modified:
|
|
- server/src/services/chat.ts
|
|
- server/src/routes/chat.ts
|
|
- ui/src/api/chat.ts
|
|
- ui/package.json
|
|
decisions:
|
|
- "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)"
|
|
metrics:
|
|
duration: "~6 minutes"
|
|
completed: "2026-04-01"
|
|
tasks: 2
|
|
files: 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
|