FE-534: Walking skeleton SDK to SSE to React#10
Conversation
lunelson
commented
Mar 30, 2026
- Add memory/SPEC.md with full v2 product spec
- Add memory/ROADMAP.md with 12 sliced issues (FE-534–FE-545)
- Update AGENTS.md with git/linear/graphite conventions
- Move REFACTORS.md, REMODEL.md to docs/design/
- Move TEST_PROBLEMS.md to docs/test-scenarios/
- Add memory/SPEC.md with full v2 product spec - Add memory/ROADMAP.md with 12 sliced issues (FE-534–FE-545) - Update AGENTS.md with git/linear/graphite conventions - Move REFACTORS.md, REMODEL.md to docs/design/ - Move TEST_PROBLEMS.md to docs/test-scenarios/
FE-534 Walking skeleton: SDK → SSE → React
Prove the integration seam end-to-end. New Express route, pipeSDKStream adapter (Design A), React + Vite + useChat. Thinking, text, tool-use events visible in browser. No DB, no domain logic. |
…ation - Define canonical section schemas for SPEC.md (concept, constraints, requirements, assumptions, decisions, lexicon, verification design, acceptance criteria) and PLAN.md (phases with slices/spikes, cross-referencing SPEC.md) - Extract templates to resources/ subdirs (ln-spec, ln-plan), matching ln-handoff pattern - Add traceability hygiene to ln-spec, ln-plan, ln-build, ln-spike, ln-scope - Expand routing tables across all execution skills to include ln-spec/ln-plan escalation - Add lexicon alignment survey to ln-review (feeds into ln-refactor) - Add routing table to ln-sync - Make ln-spec and ln-plan re-runnable (read-first, update semantics) - Migrate memory/SPEC.md to new schema: surface 10 implicit assumptions, build 19-term lexicon, consolidate acceptance criteria, fold Q&A into decision rationale - Rename memory/ROADMAP.md → PLAN.md: organize 12 slices into 4 phases + horizon, add requirement/assumption cross-references to every slice - Update templates bidirectionally: add issue IDs, branches, acceptance, dependencies sections from existing docs back into plan-template - Add HTML comments throughout templates and docs as section-level guidance - Update AGENTS.md with protocol docs (two-document system, skill flow, symlinks) - Add .claude/skills → .agents/skills symlink for Claude Code compatibility Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ln-grill: add anti-pattern naming and lexicon sharpening behaviors - ln-plan: add epistemic horizon and spike economics concepts - AGENTS.md: tie git/Linear/planning into one workflow (PLAN.md → Linear issue → Graphite stacked branch) - Delete docs/design/REFACTORS.md (absorbed into REMODEL.md and SPEC.md) - Add cli-graphite, cli-linear skill references Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prove the integration seam: Claude Agent SDK query() streams through an Express SSE adapter emitting AI SDK UI Message Stream protocol events, consumed by @ai-sdk/react useChat hook rendering thinking and text in a React UI. - SSE adapter (pure function): translates SDK stream events to AI SDK protocol (text-start/delta/end, reasoning-start/delta/end, start, finish-step, finish, [DONE]) - Express POST /api/chat handler with SDK integration - React + Vite client with useChat, thinking/text rendering - 17 tests (10 unit, 5 integration, 2 format) - TypeScript throughout, strict mode - Replaced Preact with React, removed v1 deps (mysql2, dnd-kit, etc.) Validates assumptions A1, A2, A8, A10. Establishes invariants I1-I4 (SSE conformance, stream lifecycle, thinking/text separation, Vite proxy routing). Also: refine ln-* skills (scope-before-build routing, mandatory traceability, invariants as first-class ledger in spec template). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| type SDKMessage = SDKStreamEvent | SDKOtherMessage; | ||
|
|
||
| // Track content block types by index | ||
| const thinkingBlocks = new Set<number>(); |
There was a problem hiding this comment.
src/server/sse-adapter.ts:39-41 — thinkingBlocks/textBlocks are module-level mutable state, so concurrent /api/chat requests can interfere with each other. In particular, one request’s resetAdapter() can clear another request’s in-flight block tracking and cause incorrect *-end events or missing events.
Severity: high
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
|
|
||
| switch (event.type) { | ||
| case 'message_start': | ||
| return [{ type: 'start', messageId: event.message!.id }]; |
There was a problem hiding this comment.
src/server/sse-adapter.ts:61-87 — Several non-null assertions (event.message!, event.content_block!, event.delta!, event.index!, delta.text!/delta.thinking!) will throw if the SDK ever emits an unexpected/partial shape. That would terminate the SSE stream and can also generate protocol ids like reasoning-undefined / text-undefined if index is absent.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| }, | ||
| }); | ||
|
|
||
| for await (const sdkMessage of stream) { |
There was a problem hiding this comment.
src/server/app.ts:36-43 — If the client disconnects mid-stream, the for await loop will likely keep consuming from query() and attempting res.write(...) on a closed response. This can waste tokens/compute and may surface as noisy errors unless the stream is cancelled on req/res close.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| ?? lastMessage?.parts?.filter((p: { type: string }) => p.type === 'text') | ||
| .map((p: { text: string }) => p.text).join('') ?? ''; | ||
|
|
||
| console.log('POST /api/chat — prompt:', JSON.stringify(prompt).substring(0, 100)); |
There was a problem hiding this comment.
src/server/app.ts:17 — Logging the user prompt (even truncated) can unintentionally persist sensitive user input/PII into server logs. If this endpoint is used beyond local dev, it’s worth ensuring the logging policy matches your privacy/security requirements.
Severity: low
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
| }); | ||
| }); | ||
|
|
||
| describe('translateEvent', () => { |
There was a problem hiding this comment.
src/server/sse-adapter.test.ts:16 — translateEvent depends on module-level adapter state (thinkingBlocks/textBlocks), but these tests don’t reset that state between cases. This can make the suite order-dependent or flaky if tests are run concurrently or re-ordered.
Severity: medium
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
