• v1.13.11-a: WS frame schemas + frontend receive validation

    indifferentketchup released this 2026-05-22 15:48:32 +00:00 | 83 commits to main since this release

    First half of the WebSocket-frame-typing batch (split per recon — total
    scope was ~535 LoC, larger than the roadmap's ~300 estimate, so the
    server-side publish-site conversion lands separately in v1.13.11-b).

    Phase A scope:

    (1) apps/server/src/types/ws-frames.ts (NEW) — Zod schemas for all 27
    wire-format WS frame types. Discriminated union (WsFrameSchema) plus
    KNOWN_FRAME_TYPES const for diagnostic lookup. UUIDs are z.string().
    uuid(); model-emitted tool_call_id stays z.string().min(1) since OpenAI-
    compatible APIs emit "call_" not UUID. Per-kind payload narrowing
    (tool args, message_parts payloads) intentionally stays z.unknown() —
    frame-level drift detection is the goal; deep payload validation is
    follow-up work.

    (2) apps/web/src/api/ws-frames.ts (NEW) — byte-identical mirror of the
    authoritative server file. No path alias from web→server in the existing
    tsconfig setup; sync-by-hand was chosen over a new packages/shared/ dir.
    A ws-frames.test.ts test asserts the two files match.

    (3) apps/server/src/services/broker.ts — adds publishFrame() and
    publishUserFrame() methods to the Broker interface. Both validate via
    WsFrameSchema and fail-closed: log + drop on invalid. createBroker now
    accepts an optional FastifyBaseLogger so validation failures land in
    the pino stream (with console.error fallback for unit tests). The
    existing publish() / publishUser() raw methods stay legal — they get
    converted to the typed variants in v1.13.11-b.

    (4) apps/web/src/hooks/useSessionStream.ts + useUserEvents.ts — wrap
    ws.onmessage with WsFrameSchema.safeParse. Fail-closed: invalid frames
    log + return without dispatching. Hand-maintained WsFrame and
    SessionEvent types stay in place; one cast bridges Zod-typed → narrowed
    shape (Zod uses OpaqueObject for nested Message[] / WorkspacePane[] etc.,
    which are dev-time-narrowed via the existing hand-maintained types).

    (5) apps/web/package.json — adds zod ^3.23.8 as a direct dep. Was a
    transitive dep via ai-sdk / postgres; promotion makes the import legal.

    (6) Tests: 15 new in ws-frames.test.ts covering happy-path per major
    frame type, drift-catchers (unknown type, invalid enum, non-UUID, negative
    tokens), parts-authoritative read variants, the mirror-file diff check,
    and four broker fail-closed scenarios. 219/219 server tests pass (was
    204; +15 new).

    Two recon corrections to the dispatch brief, both flagged before
    implementation:

    • No 'parts_appended' frame exists. The brief assumed one; the codebase
      reads parts via the messages_with_parts view after message_complete
      triggers a refetch. MessagePartSchema is therefore unused this batch.
    • No 'tool_running' frame exists. The brief listed it as standalone; it
      is in fact a 'chat_status' variant ({ status: 'tool_running' }), already
      covered by ChatStatusFrame.

    Smoke: clean container boot, no validation errors in the server log. Real
    production frames pass validation (the schemas were derived from the
    existing hand-maintained types in api/types.ts and sessionEvents.ts).

    v1.13.11-b will follow immediately: convert all ~85 raw broker.publish /
    ctx.publish call sites across 11 server files to publishFrame /
    publishUserFrame. Mechanical edit; the wiring done here means the diff
    in -b is just the call-site swaps.

    ~310 LoC across 9 files (4 new + 5 modified).

    Downloads