Skip to content

Derive the web AgentMessage type from the server's message row - #992

Merged
selfcontained merged 1 commit into
mainfrom
tech-debt/message-wire-type
Aug 22, 2026
Merged

Derive the web AgentMessage type from the server's message row#992
selfcontained merged 1 commit into
mainfrom
tech-debt/message-wire-type

Conversation

@selfcontained

Copy link
Copy Markdown
Owner

Finishes the server↔web wire-type duplication cluster found by the 2026-08-14 audit (#958). Sub-item (g), the last of seven — after (a) #965, (b) #971, (c) #976, (d) #981, (e) #985, (f) #989.

What changed

One file, apps/web/src/hooks/use-agent-messages.ts. AgentMessage hand-restated the nine fields of the message row that apps/server/src/messages/store.ts already declares as StoredMessage. It now derives from it via a type-only import (../../../server/src/messages/store), which esbuild erases, so nothing from the server reaches the web bundle.

Why it's debt: two hand-maintained copies of one wire shape, with nothing keeping them in step.

Shape A worked, against the recorded prediction

The Brain expected Shape B (leaf extraction) here, because messages/store.ts declares a class and brain/store.ts — the sibling that broke Shape A on sub-item (c) — failed inside web's TS program with TS2401 (useDefineForClassFields: true + target: ES2020). It type-checked clean this time. TS2401 requires a derived class with initialized property members; MessageStore is a plain class whose only member is a constructor parameter property, so the class grep is a warning sign rather than a verdict.

Drift: found, and it is why this aliases with Omit instead of directly

AgentMessage serves two endpoints, and they send different projections:

  • GET /api/v1/agents/:id/messages (routes/messages.ts:31) returns store.listForAgent() verbatim — all eleven StoredMessage fields, including senderRepoRoot / recipientRepoRoot.
  • The /api/v1/history agent-detail query (routes/activity/history-routes.ts:343-368) projects the same row without the two repo-root columns, and use-agent-history.ts:70 types its messages with the same AgentMessage.

So a plain export type AgentMessage = StoredMessage would have been accurate for one consumer and a latent lie for the other — TypeScript would have permitted msg.senderRepoRoot in the history tab, where the value is undefined at runtime. It also broke two web test factories, which is what surfaced it. Applying the #989 lesson (derive, don't blindly alias), the type is:

exporttypeAgentMessage=Omit<StoredMessage,"senderRepoRoot"|"recipientRepoRoot">;

That preserves today's exact semantics for both consumers with zero test churn. This is the run's one arguable design choice and it is flagged to the reviewer explicitly.

Deliberate exclusions

  • history-routes.ts:343-353 — the /history detail query restates the same nine fields as an inline pool.query<{...}> generic. It is a genuinely different projection (server-side, no repo roots) and is not part of sub-item (g), so it is left alone and backlogged rather than folded in here.
  • MessagesPayload (use-agent-messages.ts:17) — the { messages, unreadCount } envelope. routes/messages.ts:31 returns an object literal with no named server-side type, so there is nothing to import; same call as #976 made for its anonymous return type.
  • e2e/helpers.ts:262-272seedAgentMessageViaDB's parameter shape. A test-fixture seed input with optional repo roots, not a wire type.
  • use-sse.ts:85{ type: "message.created"; senderAgentId; recipientAgentId }. An SSE event payload, not the message row; it belongs to the UiEvent union (already tracked separately in the backlog).
  • The four StoredMessage construction sites in mcp-handlers.ts / messaging-tools.ts are producers of InsertMessageInput, already typed from the store.

Verification

  • pnpm run check:web run with apps/server/src/generated/ still absent — the #863 trap, cleared. (pnpm run check creates that directory as a side effect, so this was run first.)
  • pnpm run check, pnpm run finalize:web, apps/web Vitest (72 files / 1090 tests), pnpm run test:e2e (181 passed, 12 skipped) — all green.
  • Completeness grep on a middle field: recipientName: string now has three declaration sites — messages/store.ts:9 (canonical), the backlogged history-routes.ts:348 server projection, and the e2e/helpers.ts:266 fixture. Web is at zero.

Next run

The cluster is retired. Seven runs have passed since the last broad audit (2026-08-14, #958), so the next run or two should be the fresh audit, scoped to duplication and collisions first.

Last sub-item of the server<->web wire-type duplication cluster found by
the 2026-08-14 audit. apps/web/src/hooks/use-agent-messages.ts hand-restated
the nine fields of the message row that apps/server/src/messages/store.ts
already declares as StoredMessage. The web copy now derives from it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBimqehbS1VmatsnaL1VvQ
@selfcontained
selfcontained merged commit b48001f into mainAug 22, 2026
1 check passed
@selfcontained
selfcontained deleted the tech-debt/message-wire-type branch August 22, 2026 09:11
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@selfcontained