v1.13.11-b: convert raw broker.publish call sites to typed publishFrame
Second half of the WebSocket-frame-typing batch. Phase A (8b568b3)
landed the schemas + frontend receive validation + publishFrame /
publishUserFrame wrappers. This commit converts the existing publish
call sites so every server-emitted WS frame now goes through Zod
validation at the broker boundary.
Conversion strategy: change once in the inference / skills adapters in
index.ts (so ctx.publish / ctx.publishUser propagate to publishFrame /
publishUserFrame for ALL ~50 inference + auto_name call sites in one
move), then bulk-replace the ~30 direct broker.publish* call sites in
the routes + compaction.
Files touched:
- index.ts: inference + skills route adapters now call publishFrame /
publishUserFrame internally; raw broker.publishUser('default', ...)
call in the stale-row sweeper also converted.
- routes/projects.ts (7 sites), routes/chats.ts (9 sites),
routes/sessions.ts (8 sites): all broker.publishUser(...) → broker.
publishUserFrame(...).
- services/compaction.ts (3 sites): 2 publishUser, 1 publish.
Real protocol drift surfaced by Zod, fixed in the same commit:
services/compaction.ts:442 was publishing chat_status with status:
'working' — the v1.12.1 chat_status widening (CLAUDE.md:55) dropped
this enum value in favor of streaming|tool_running|waiting_for_input|
idle|error. The compaction.ts site was missed during v1.12.1; the
frame had been published with an unknown enum value ever since (the
frontend useChatStatus quietly ignored it). Corrected to 'streaming'
— compaction's LLM call has the same dot-state semantic as an
inference turn. This is exactly the class of bug v1.13.11 exists to
catch.
Schema relaxation: OpaqueObject (the bag type for nested entities like
Project / Chat / Session / WorkspacePane embedded in WS frames) was
z.object({}).passthrough(), which Zod outputs as {} & {[k:string]:
unknown}. The strict-typed entities don't have index signatures so
TypeScript rejected them at publishFrame call sites. Relaxed to
z.unknown() — runtime validation still accepts the value, dev-time
narrowing happens via the existing hand-maintained types. Trade-off:
frame-level drift detection stays sharp; nested-payload validation
goes to follow-up work as the brief intended.
Schema audit:
grep -rn "broker\.publish(\|broker\.publishUser(" apps/server/src \
--include="*.ts" | grep -v "broker.ts\|__tests__\|.bak"
→ 0 results. Every server publish goes through publishFrame /
publishUserFrame. The remaining ctx.publish / ctx.publishUser sites
in services/inference/* + services/auto_name.ts route through the
index.ts adapter, which calls publishFrame internally.
Tests: 219/219 pass (unchanged from v1.13.11-a; the Phase B conversion
is mechanical and doesn't add test cases).
Smoke: clean container boot, no ws-frame-validation-failed entries
under normal traffic. Sidebar list refresh + agent picker open both
pass through useUserEvents without drops.
~70 LoC across 7 files. v1.13.11 closed.
This commit is contained in:
@@ -46,9 +46,13 @@ const ToolCallShape = z.object({
|
||||
});
|
||||
|
||||
// Free-form bags: opaque to the frame schema; deep validation is out of
|
||||
// scope. passthrough preserves unknown keys so the consumer sees the full
|
||||
// shape even when this schema doesn't enumerate every field.
|
||||
const OpaqueObject = z.object({}).passthrough();
|
||||
// scope for v1.13.11 (frame-level drift detection is the goal; per-kind
|
||||
// payload narrowing is follow-up work). z.unknown() means the consumer
|
||||
// must narrow before reading — TypeScript-side this is fine because every
|
||||
// consumer already operates on the hand-maintained Project / Chat / Session
|
||||
// / WorkspacePane types (the brief's "Don't strip existing types yet"
|
||||
// rule), and the Zod-typed shape is only used at the publishFrame boundary.
|
||||
const OpaqueObject = z.unknown();
|
||||
|
||||
// ---- per-session channel frames --------------------------------------------
|
||||
|
||||
|
||||
Reference in New Issue
Block a user