Derive web's Agent type from the server's AgentRecord - #1044
Merged
Conversation
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
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>
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 free
to 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.
What
apps/web/src/components/app/types.tshand-restated the agent row that the server already declares asAgentRecord. 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.ts—AgentRecordplus its member unions (AgentStatus,AgentRole,AgentPin,AgentLatestEvent,AgentGitContext,SetupPhase,ArchivePhase,WorktreeCleanupMode,AgentLatestEventType).packages/shared/src/agent-types.ts— theAGENT_TYPES/CLI_AGENT_TYPEStables.packages/shared/src/pin-types.ts— theVALID_PIN_TYPES/VALID_PIN_SHORTCUT_VARIANTStables.Web derives its lenient view with the same idiom the file already uses for
DiffStats:apps/server/src/agents/types.ts,apps/server/src/shared/agent-types.tsandapps/server/src/pins.tsre-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.tskeepsisAgentType/isCliAgentType/sanitizeEnabledAgentTypesand the server-onlyPLUGIN_AGENT_TYPEStable;pins.tskeeps every validator, sanitizer andVALID_PIN_SHORTCUT_ICONS. Only the plain tables moved, which thepackages/sharedcharter explicitly allows ("keep those to plain constants that both sides genuinely have to agree on" —DIFF_IMAGE_MAX_BYTESis the existing precedent).Why it's debt
The web copy had drifted three ways, all silently:
simulatorUdid,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?: stringwhere the server hastype: 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
reviewAgentTypetoAgentType | nullmatches the server's own declaration (agents/manager.ts:167,agents/types.ts:127). Its only web consumer ispersona-launcher.tsx:38, whosedefaultReviewAgentTypealready returnsAgentType.Review history
The first push imported
AgentRecorddirectly fromapps/server/src/agents/types.ts— the established Shape-A pattern used by eleven other web modules — on the reasoning thatAgentRecord'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 toui-event-types.tsin the first push is gone.The five fixture edits
Five
latestEventliterals inagent-card.test.tsx/child-agent-row.test.tsxstopped compiling becauseAgentLatestEventrequiresmetadatawhile web's copy had it optional. I re-read the producer rather than patching blind:agents/manager.ts:1429builds the object withCOALESCE(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 passmetadata: {}.Deliberate exclusions
PLUGIN_AGENT_TYPESstays inapps/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_ICONSstays inpins.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 ofAgentRecord.apps/web/src/lib/agent-types.tsstill re-exports fromapps/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.LenientAgentField).AgentRecordhas them required-nullable; web keeps them optional because dozens of fixtures and optimistic cache entries build partial agents. This PR does not tighten them.hasStreamstays declared web-side and optional. The server'sUiEventstill declares plainAgentRecordforsnapshot/agent.upserteven thoughwithStreamFlagis 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 agentstimes out at 30s creating a codex agent. Verified pre-existing, not caused by this diff:origin/mainchecked 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.tssosnapshot/agent.upsertdeclareAgentRecord & { hasStream: boolean }, and drop the 13event as UiEventcasts that the loosepublishUiEvent: (event: unknown) => voiddep type in eight route modules currently requires.🤖 Generated with Claude Code