Skip to content

Derive web's Agent type from the server's AgentRecord - #1044

Merged
selfcontained merged 2 commits into
mainfrom
tech-debt/agent-record-web-derivation
Sep 3, 2026
Merged

Derive web's Agent type from the server's AgentRecord#1044
selfcontained merged 2 commits into
mainfrom
tech-debt/agent-record-web-derivation

Conversation

@selfcontained

@selfcontained selfcontained commented Sep 3, 2026

Copy link
Copy Markdown
Owner

What

apps/web/src/components/app/types.ts hand-restated the agent row that the server already declares as AgentRecord. That copy is the last big server↔web wire-type gap left after the UiEvent consolidation (#1016).

The contract now lives in @dispatch/shared, and both sides take it from there:

  • packages/shared/src/agent-record.tsAgentRecord plus its member unions (AgentStatus, AgentRole, AgentPin, AgentLatestEvent, AgentGitContext, SetupPhase, ArchivePhase, WorktreeCleanupMode, AgentLatestEventType).
  • packages/shared/src/agent-types.ts — the AGENT_TYPES / CLI_AGENT_TYPES tables.
  • packages/shared/src/pin-types.ts — the VALID_PIN_TYPES / VALID_PIN_SHORTCUT_VARIANTS tables.

Web derives its lenient view with the same idiom the file already uses for DiffStats:

export type Agent = Omit<AgentRecord, LenientAgentField> &
  Partial<Pick<AgentRecord, LenientAgentField>> & { hasStream?: boolean };

apps/server/src/agents/types.ts, apps/server/src/shared/agent-types.ts and apps/server/src/pins.ts re-export everything they moved, so no server importer changed — the whole server-side diff is those three files' declaration blocks.

Validation logic stays server-side. shared/agent-types.ts keeps isAgentType / isCliAgentType / sanitizeEnabledAgentTypes and the server-only PLUGIN_AGENT_TYPES table; pins.ts keeps every validator, sanitizer and VALID_PIN_SHORTCUT_ICONS. Only the plain tables moved, which the packages/shared charter explicitly allows ("keep those to plain constants that both sides genuinely have to agree on" — DIFF_IMAGE_MAX_BYTES is the existing precedent).

Why it's debt

The web copy had drifted three ways, all silently:

  • Six columns missing entirelysimulatorUdid, archiveCleanupMode, gitContextStale, gitContextUpdatedAt, launchedByAgentId, cliSessionId. The rows go to JSON unmapped, so the server has always sent them; web just couldn't see them.
  • type?: string where the server has type: AgentType. Any typo in an agent-type comparison type-checked.
  • reviewAgentType?: "codex" | "claude" | "opencode" | "cursor" | null — a hand-written copy of the agent-type table. This closes the separate "web half of the agent-type table" backlog entry; the server half landed in Derive the remaining within-server agent-type tables from shared #950.

Widening reviewAgentType to AgentType | null matches the server's own declaration (agents/manager.ts:167, agents/types.ts:127). Its only web consumer is persona-launcher.tsx:38, whose defaultReviewAgentType already returns AgentType.

Review history

The first push imported AgentRecord directly from apps/server/src/agents/types.ts — the established Shape-A pattern used by eleven other web modules — on the reasoning that AgentRecord's closure reached two runtime modules and so could not go into a types-only package. architecture-review 1065 (item #2527) ruled that wrong on both counts: it reverses the intended dependency direction, and the shared package does permit plain constants. This push implements the reviewer's Option B. The stale "cannot move here" claim added to ui-event-types.ts in the first push is gone.

The five fixture edits

Five latestEvent literals in agent-card.test.tsx / child-agent-row.test.tsx stopped compiling because AgentLatestEvent requires metadata while web's copy had it optional. I re-read the producer rather than patching blind: agents/manager.ts:1429 builds the object with COALESCE(latest_event_metadata, '{}'::jsonb), so the field is never absent on the wire — the fixtures were modelling a shape the server cannot produce. They now pass metadata: {}.

Deliberate exclusions

  • PLUGIN_AGENT_TYPES stays in apps/server/src/shared/agent-types.ts. It is a server-only subset (launch-guidance trimming and plugin update detection); web has no use for it, so it does not belong in a shared contract.
  • VALID_PIN_SHORTCUT_ICONS stays in pins.ts. Web mirrors it as an icon map (lib/pin-shortcut-icons.ts) with a guard test asserting lockstep — that is a different relationship from a shared table, and it isn't part of AgentRecord.
  • apps/web/src/lib/agent-types.ts still re-exports from apps/server/src/shared/agent-types.ts. That seam predates this PR and still resolves (the server module re-exports the moved tables); repointing it and the ten other web→server type imports is out of scope here.
  • agent-type-icon.tsx:28 — its "codex" | "claude" | "opencode" | "cursor" | "terminal" | "unknown" return type adds an "unknown" member. That is a normalization contract, not a copy of the table. Left alone.
  • The 22 fields kept optional (LenientAgentField). AgentRecord has them required-nullable; web keeps them optional because dozens of fixtures and optimistic cache entries build partial agents. This PR does not tighten them.
  • hasStream stays declared web-side and optional. The server's UiEvent still declares plain AgentRecord for snapshot/agent.upsert even though withStreamFlag is applied at all 31 publish sites as of fix(templates): publish agent.upsert with the hasStream flag on launch #1038. Tightening that is queued for the next run.

Validation

pnpm run check ✅ · pnpm run finalize:web ✅ · pnpm run test:e2e ✅ (186 passed, 12 skipped) · cd apps/web && pnpm vitest run ✅ (1490 passed, 103 files)

pnpm run test (server Vitest) — 2985 passed, 1 pre-existing environment failure: test/db/agent-manager.test.ts > harvestAgentTokens > should skip session ownership logic for non-claude agents times out at 30s creating a codex agent. Verified pre-existing, not caused by this diff: origin/main checked out in this same worktree fails that test identically, and both commits of this branch pass the full 124-test file (124/124, twice) in a clean throwaway worktree on the same database. The failure tracks the working tree, not the code.

Next run

Tighten apps/server/src/server/ui-events.ts so snapshot/agent.upsert declare AgentRecord & { hasStream: boolean }, and drop the 13 event as UiEvent casts that the loose publishUiEvent: (event: unknown) => void dep type in eight route modules currently requires.

🤖 Generated with Claude Code

selfcontained and others added 2 commits September 3, 2026 03:06
apps/web/src/components/app/types.ts hand-restated the agent row that
apps/server/src/agents/types.ts already declares as AgentRecord — the last
big server<->web wire-type gap left after the UiEvent consolidation (#1016).
The web copy omitted six columns the server has always sent (simulatorUdid,
archiveCleanupMode, gitContextStale, gitContextUpdatedAt, launchedByAgentId,
cliSessionId), typed `type` as a bare string, and hand-wrote
reviewAgentType's member list.

Agent is now `Omit<AgentRecord, LenientAgentField> &
Partial<Pick<AgentRecord, LenientAgentField>> & { hasStream?: boolean }` —
the same derivation idiom the file already uses for DiffStats. AgentStatus,
AgentPin and PinShortcutVariant are re-exported from the server module
instead of restated, so every existing importer is untouched.

Five test fixtures gained `metadata: {}` on their latestEvent literals: the
producer (agents/manager.ts:1429) COALESCEs the column to '{}'::jsonb, so
the wire never omits that field.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to the first commit, per architecture-review 1065: web now takes
`AgentRecord` from `@dispatch/shared` rather than reaching into
`apps/server/src/agents/types.ts`, which reversed the intended dependency
direction and coupled the web build to server-internal file layout.

New shared modules:
  - packages/shared/src/agent-types.ts  — AGENT_TYPES / CLI_AGENT_TYPES
  - packages/shared/src/pin-types.ts    — VALID_PIN_TYPES /
                                          VALID_PIN_SHORTCUT_VARIANTS
  - packages/shared/src/agent-record.ts — AgentRecord and its member unions

The index.ts charter already permits "plain constants that both sides
genuinely have to agree on" (DIFF_IMAGE_MAX_BYTES is the precedent), so the
runtime tables were not a blocker as the first commit's comment claimed.
Validation stays server-side: apps/server/src/shared/agent-types.ts keeps
isAgentType/isCliAgentType/sanitizeEnabledAgentTypes and the server-only
PLUGIN_AGENT_TYPES table; apps/server/src/pins.ts keeps every pin validator.
Both re-export what they moved, so no server importer changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@selfcontained
selfcontained merged commit 0aa7932 into main Sep 3, 2026
2 of 3 checks passed
@selfcontained
selfcontained deleted the tech-debt/agent-record-web-derivation branch September 3, 2026 09:39
selfcontained added a commit that referenced this pull request Sep 3, 2026
Also imports AgentPin/AgentStatus in apps/web/src/components/app/types.ts:
the file used them locally but only re-exported them (a re-export creates
no local binding), so check:web failed on main after #1044 — pushes to
main don't run CI, which is how it slipped through.

Co-Authored-By: Claude Fable 5 <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