• v1.13.11-b: convert raw broker.publish call sites to typed publishFrame

    indifferentketchup released this 2026-05-22 15:54:00 +00:00 | 82 commits to main since this release

    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.

    Downloads