Skip to content

Single-source the history wire types shared by server and web - #981

Merged
selfcontained merged 1 commit into
mainfrom
tech-debt/history-wire-types
Aug 19, 2026
Merged

Single-source the history wire types shared by server and web#981
selfcontained merged 1 commit into
mainfrom
tech-debt/history-wire-types

Conversation

@selfcontained

Copy link
Copy Markdown
Owner

Sub-item (d) of the server↔web wire-type duplication cluster found by the 2026-08-14 audit's duplicate-block scan (5 mirrored 7-line windows — the largest remainder after #965, #971 and #976).

What was duplicated

apps/web/src/hooks/use-agent-history.ts hand-wrote five response shapes that apps/server/src/routes/activity/history-routes.ts restated inline as deps.pool.query<{...}> generics:

shape server (before) web (before)
child agent local type ChildAgent HistoryChildAgent
event row inline generic on the events query HistoryEvent
token totals + by-model two inline generics HistoryTokenUsage
media row inline generic HistoryMedia
feedback row inline generic HistoryFeedbackItem

What changed

New dependency-free leaf apps/server/src/routes/activity/history-wire.ts (73 lines, zero imports, no classes) declares them once. history-routes.ts uses them as its pool.query<> generics; use-agent-history.ts type-imports them and re-exports every name it exported before, so no consumer of either module changed.

HistoryLatestEvent is named because the same {type, message, updatedAt, metadata} object was restated three times (server ChildAgent, web HistoryChildAgent, web HistoryAgent).

3 files, +32/−119.

Why Shape B (leaf extraction) and not a direct import

history-routes.ts imports ./shared.js, whose ActivityRouteDeps pulls in AgentManager — i.e. most of the server graph would land in web's TS program. The class probe (grep -n '^export class\|^class') came back empty, but the service-layer graph settles it on its own. The leaf was verified against web's TS program with apps/server/src/generated/ absent (pnpm run check:web before pnpm run check, which creates that directory as a side effect).

Drift

None. All five shapes were field-for-field identical; only declaration order differed (web listed totalTokens before latestEvent on the child agent). No web field turned out to be an un-mirrored wire field this time.

Deliberately excluded

  • HistoryAgent / HistoryAgentsResponse / HistoryAgentDetail stay web-local. The server's agents-list and agent-detail queries carry no type generic at all, so there is nothing to share — typing those rows would be adding new server-side typing, not removing a duplicate.
  • messages: AgentMessage[] keeps pointing at @/hooks/use-agent-messages. The message row is sub-item (g) of the same cluster (messages/store.ts vs use-agent-messages.ts) and belongs to its own run.
  • TokenStats in apps/web/src/hooks/use-activity.ts:168 looks like HistoryTokenTotals but carries an extra total_sessions, and is produced by a different route (token-routes.ts). Not merged — it is a separate duplication pair (backlogged), and merging would widen the history payload's contract.
  • TokenByModel (use-activity.ts:209) vs the new HistoryTokenByModel: same lookalike situation — five fields vs three, different route, different SQL. Left alone.
  • size_bytes: in apps/server/src/media/store.ts:18 and server/mcp-handlers.ts:1058 — same field name, different domain row (the media store's own shape, not the history payload's media entry).
  • handleHistoryProjects's projectOptions return type stays anonymous; web's useHistoryProjects only reads projects: string[], so there is no second declaration to remove.

Completeness check

Content-grep on middle fields, counting declaration sites: total_cache_creation and mediaRef and size_bytes: each resolve to exactly one declaration for the history payload (history-wire.ts) outside test fixtures. The remaining hits are the token/activity pair and the media-store row listed above.

Checks

pnpm run check:web (with generated/ absent) · pnpm run check · pnpm run finalize:web · web vitest 1000/1000 · server vitest 2797 passed / 9 skipped · pnpm run test:e2e 181 passed / 12 skipped.

Queued next

Sub-item (e) of the cluster: apps/server/src/release-info.ts vs apps/web/src/hooks/use-cached-release-info.ts (3 windows).

🤖 Generated with Claude Code

apps/web/src/hooks/use-agent-history.ts hand-mirrored five response
shapes that apps/server/src/routes/activity/history-routes.ts restated
inline as pool.query<> generics. Both sides now derive from a new
dependency-free leaf, apps/server/src/routes/activity/history-wire.ts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@selfcontained
selfcontained merged commit d889ff4 into main Aug 19, 2026
1 check passed
@selfcontained
selfcontained deleted the tech-debt/history-wire-types branch August 19, 2026 09:14
selfcontained added a commit that referenced this pull request Aug 21, 2026
apps/web/src/hooks/use-agent-diff.ts hand-restated four types that
apps/server/src/shared/git/agent-diff.ts already declares. This is
sub-item (f) of the server<->web wire-type duplication cluster;
(a)-(e) landed in #965, #971, #976, #981 and #985.

DiffFileStatus, DiffFile and FileDiffResponse were byte-identical and
are now type-only imports re-exported under their existing names, so
every consumer import is unchanged.

DiffResponse had genuinely drifted — the first real drift this cluster
has turned up in six consolidations. The web copy declared
`baseRef: string | null` and omitted the server's
`truncatedFileCount?: number`. Both halves of that are explained by the
route rather than by a mistake in either type: the GET
/api/v1/agents/:id/diff handler substitutes `{ baseRef: null, files: [] }`
when getAgentDiff returns null, so nullability is real on the wire even
though the server's own DiffResponse always carries a merge-base SHA.
The web type is now derived — Omit<DiffResponse, "baseRef"> plus the
nullable baseRef — so the nullability stays documented while
truncatedFileCount and any future field come across for free.

Type-only change; esbuild erases the imports, so nothing from the
server reaches the web bundle.


Claude-Session: https://claude.ai/code/session_014PHD7HNfqtZHTXTLWwxxWo

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to 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