Derive the web AgentMessage type from the server's message row - #992
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.AgentMessagehand-restated the nine fields of the message row thatapps/server/src/messages/store.tsalready declares asStoredMessage. 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.tsdeclares a class andbrain/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;MessageStoreis 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
Omitinstead of directlyAgentMessageserves two endpoints, and they send different projections:GET /api/v1/agents/:id/messages(routes/messages.ts:31) returnsstore.listForAgent()verbatim — all elevenStoredMessagefields, includingsenderRepoRoot/recipientRepoRoot./api/v1/historyagent-detail query (routes/activity/history-routes.ts:343-368) projects the same row without the two repo-root columns, anduse-agent-history.ts:70types itsmessageswith the sameAgentMessage.So a plain
export type AgentMessage = StoredMessagewould have been accurate for one consumer and a latent lie for the other — TypeScript would have permittedmsg.senderRepoRootin the history tab, where the value isundefinedat runtime. It also broke two web test factories, which is what surfaced it. Applying the#989lesson (derive, don't blindly alias), the type is: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/historydetail query restates the same nine fields as an inlinepool.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:31returns an object literal with no named server-side type, so there is nothing to import; same call as#976made for its anonymous return type.e2e/helpers.ts:262-272—seedAgentMessageViaDB'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 theUiEventunion (already tracked separately in the backlog).StoredMessageconstruction sites inmcp-handlers.ts/messaging-tools.tsare producers ofInsertMessageInput, already typed from the store.Verification
pnpm run check:webrun withapps/server/src/generated/still absent — the#863trap, cleared. (pnpm run checkcreates that directory as a side effect, so this was run first.)pnpm run check,pnpm run finalize:web,apps/webVitest (72 files / 1090 tests),pnpm run test:e2e(181 passed, 12 skipped) — all green.recipientName: stringnow has three declaration sites —messages/store.ts:9(canonical), the backloggedhistory-routes.ts:348server projection, and thee2e/helpers.ts:266fixture. 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.