Skip to content

Web client's mirrored WebSocket frame types have drifted from the server union in both directions — 5 payloads, 5 inline casts hiding it (folds in #772) #773

Description

@edspencer

Summary

packages/web/src/lib/types.ts hand-mirrors the server's ServerMessage frames, and the mirror has drifted: three of the queue frames omit the fields #728 added, and chat:killed_task declares two fields the server never sends. The runtime handlers work around the gap with inline as {…} casts, so TypeScript is not checking the attachment plumbing at all.

Evidence

chat:queued_flushed — web (packages/web/src/lib/types.ts:1269):

payload: { projectSlug: string; sessionId: string };

server (packages/server/src/ws-protocol.ts:555-574) additionally sends text?: string and attachments?: Array<{ id; filename; kind? }>.

chat:queued_state — web (:1279-1286) has projectSlug, sessionId, text, qid?, reason? but no attachments; server (ws-protocol.ts:588-617) sends attachments?: Array<{ id; filename; kind? }>.

chat:queued_returned — web (:1296):

payload: { projectSlug: string; sessionId: string; text: string };

server (ws-protocol.ts:633-647) also sends attachments?.

The handlers then cast straight past the declared type:

  • packages/web/src/lib/ws.ts:705-709const { sessionId, text, attachments } = msg.payload as { sessionId?; text?; attachments?: AttachmentRef[] }
  • packages/web/src/lib/ws.ts:734-740 — same shape for chat:queued_state
  • packages/web/src/lib/ws.ts:768-772 — same for chat:queued_returned

So the fields that carry a user's queued files across clients are read entirely through an assertion. If the server renamed attachments or changed the element shape, nothing in the web build would fail.

chat:killed_task — web (:1315) types the payload as Routing & { sessionId; summary; timestamp }. Routing (types.ts:1138-1148) contributes projectSlug, sessionId, jobId: string | null and seq?: number. The server's emit (packages/server/src/ws-turn.ts:848-856) sends only projectSlug, sessionId, summary, timestamp — no jobId (and it is an out-of-band hub.broadcast, so no per-turn seq either). The declared type claims a non-optional jobId that is always undefined at runtime.

Why it matters

The whole point of mirroring the frames client-side is to get a compile error when the wire contract moves. Here the mirror is stale on precisely the fields a recent feature (#728, queued attachments) added, and the casts mean the drift produced no signal. chat:killed_task fails in the other direction — the compiler will happily let code read payload.jobId as a string | null that is never present.

Suggested fix

  1. Bring the three queue payloads in line with ws-protocol.ts (add text? / attachments?), then drop the inline as {…} casts in ws.ts:705-709, :734-740, :768-772 so the destructuring is type-checked.
  2. Type chat:killed_task from what the server sends rather than Routing & — i.e. { projectSlug; sessionId; summary; timestamp }.
  3. Worth considering as the durable fix: generate or share the client union from packages/server/src/ws-protocol.ts instead of maintaining a second hand-written copy. Related: the chat:injected gap filed separately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    SSmallautoSuitable for autonomous agent implementation up to PRbugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions