agent-relay-server 0.32.3 → 0.32.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.32.3",
3
+ "version": "0.32.4",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "CONTRIBUTING.md"
34
34
  ],
35
35
  "dependencies": {
36
- "agent-relay-sdk": "0.2.19"
36
+ "agent-relay-sdk": "0.2.20"
37
37
  },
38
38
  "scripts": {
39
39
  "prepack": "bun run build:dashboard:bundle >&2",
@@ -1 +1 @@
1
- {"version":3,"file":"display-JI19Vc7L.js","names":[],"sources":["../../dashboard/src/lib/constants.ts","../../sdk/src/types.ts","../../dashboard/src/lib/display.ts"],"sourcesContent":["import type { ComposeState, InboxComposeState, PairMessageState, PairInviteState, ViewName } from '@/types'\n\nconst PREF_PREFIX = 'ar-'\nexport const HUMAN_AGENT_ID = 'user'\nexport const INBOX_OPERATOR_ID = HUMAN_AGENT_ID\nexport const LIVE_REFRESH_MS = 5_000\n// Max live messages retained in memory. Must be >= the largest fetch limit\n// (chat view fetches 300) so SSE appends never trim already-loaded history.\nexport const MESSAGE_BUFFER_CAP = 500\n\nexport const CLOSED_TASK_STATUSES = new Set(['done', 'failed', 'canceled'])\nexport const WAITING_TASK_STATUSES = new Set(['open', 'blocked'])\n\nexport const STATUS_SORT_ORDER: Record<string, number> = { online: 0, idle: 0, busy: 2, offline: 3 }\nexport const CHAT_STATUS_SORT_ORDER: Record<string, number> = { idle: 0, online: 0, busy: 2, offline: 4 }\n\nexport const DEFAULT_COMPOSE: ComposeState = { from: '', to: '', body: '', channel: '', subject: '', claimable: false }\nexport const DEFAULT_INBOX_COMPOSE: InboxComposeState = { toMode: 'agent', to: '', body: '', channel: '', subject: '', claimable: false }\nexport const DEFAULT_PAIR_MESSAGE: PairMessageState = { pairId: '', from: '', body: '', subject: '' }\nexport const DEFAULT_PAIR_INVITE: PairInviteState = { requesterId: '', targetId: '', objective: '' }\n\n\nexport const AGENT_TYPE_ICONS: Record<string, string> = {\n codex: 'Terminal',\n claude: 'Sun',\n user: 'User',\n system: 'Server',\n channel: 'MessagesSquare',\n agent: 'Bot',\n}\n\nexport const AGENT_TYPE_TITLES: Record<string, string> = {\n codex: 'Codex agent',\n claude: 'Claude agent',\n user: 'Human operator',\n system: 'System',\n channel: 'Channel',\n agent: 'Agent',\n}\n\nexport const NAV_ITEMS: Array<{ key: ViewName; label: string; icon: string }> = [\n { key: 'overview', label: 'Overview', icon: 'LayoutDashboard' },\n { key: 'chat', label: 'Chat', icon: 'MessageCircle' },\n { key: 'agents', label: 'Agents', icon: 'Bot' },\n { key: 'managed', label: 'Managed Agents', icon: 'Workflow' },\n { key: 'profiles', label: 'Agent Profiles', icon: 'UserCog' },\n { key: 'orchestrators', label: 'Orchestrators', icon: 'Server' },\n { key: 'workspaces', label: 'Workspaces', icon: 'GitBranch' },\n { key: 'files', label: 'Files', icon: 'FolderTree' },\n { key: 'channels', label: 'Channels', icon: 'MessagesSquare' },\n { key: 'connectors', label: 'Connectors', icon: 'Plug' },\n { key: 'integrations', label: 'Integrations', icon: 'PlugZap' },\n { key: 'security', label: 'Security', icon: 'Shield' },\n { key: 'memory', label: 'Memory', icon: 'BrainCircuit' },\n { key: 'activity', label: 'Activity', icon: 'Activity' },\n { key: 'pairs', label: 'Pairs', icon: 'Link' },\n { key: 'messages', label: 'Messages', icon: 'Mail' },\n { key: 'work', label: 'Work Queue', icon: 'ListChecks' },\n { key: 'tasks', label: 'Tasks', icon: 'ClipboardList' },\n { key: 'automation', label: 'Automation', icon: 'CalendarClock' },\n { key: 'analytics', label: 'Analytics', icon: 'AreaChart' },\n { key: 'insights', label: 'Insights', icon: 'Lightbulb' },\n { key: 'maintenance', label: 'Maintenance', icon: 'Wrench' },\n { key: 'settings', label: 'Settings', icon: 'Settings' },\n]\n\nexport const SEVERITY_COLORS: Record<string, string> = {\n critical: 'text-red-400 bg-red-500/10',\n warning: 'text-yellow-400 bg-yellow-500/10',\n info: 'text-blue-400 bg-blue-500/10',\n}\n\nexport const PAIR_STATUS_COLORS: Record<string, string> = {\n active: 'bg-emerald-500/20 text-emerald-400',\n pending: 'bg-yellow-500/20 text-yellow-400',\n ended: 'bg-zinc-500/20 text-zinc-400',\n rejected: 'bg-red-500/20 text-red-400',\n expired: 'bg-red-500/20 text-red-400',\n}\n","export type AgentKind = \"provider\" | \"channel\" | \"orchestrator\" | \"system\" | \"user\";\n\nexport interface AgentCard {\n id: string;\n name: string;\n kind: AgentKind;\n label?: string; // human-friendly alias; acts as a fan-out target (\"label:foo\")\n tags: string[];\n machine?: string;\n rig?: string;\n capabilities: string[];\n ready: boolean;\n status: AgentStatus;\n instanceId?: string;\n epoch: number;\n providerCapabilities?: ProviderCapabilities;\n context?: ContextState;\n meta?: Record<string, unknown>;\n /** Agent id of the parent that spawned this one (set authoritatively from the child's\n * signed runner token at registration). Absent for top-level/user/system agents. Powers\n * spawn quotas, the `spawnedBy:` search filter, scoped shutdown, and the no-grandchild gate. */\n spawnedBy?: string;\n /** Branch-workspace lifecycle state, server-derived from the agent's owned isolated\n * worktree (#236). Absent for non-branch agents (no live isolated workspace). Lets the\n * dashboard show a \"where is this in the merge-back\" badge without recomputing it. */\n branchState?: BranchState;\n /** Id of the live isolated workspace this agent owns — populated whenever `branchState`\n * is, so the dashboard can deep-link the badge to that workspace row (#236). */\n branchWorkspaceId?: string;\n lastSeen: number;\n createdAt: number;\n}\n\nexport type AgentStatus = \"online\" | \"idle\" | \"busy\" | \"stale\" | \"offline\";\n\n/**\n * At-a-glance branch-workspace state for an isolated-workspace owner (#236). One\n * server-derived projection of `WorkspaceStatus` + claim + git ahead/dirty, so the\n * human can answer \"does this agent have unlanded work, and where is it in the\n * merge-back lifecycle\" from the agent card alone.\n * - `idle` ⚪ active worktree, nothing to land (0 ahead, clean tree)\n * - `changes` 🟡 active worktree with commits/dirty work — human can mark it ready\n * - `ready` 🔵 handed off; the relay auto-merge will land it (~2 min sweep)\n * - `steward` 🟠 under reconciliation (a steward holds it, or conflict/merge in flight)\n * - `blocked` 🔴 escalated/failed — needs human attention\n */\nexport type BranchState = \"idle\" | \"changes\" | \"ready\" | \"steward\" | \"blocked\";\n/** All BranchState values, for exhaustive UI maps and validation. */\nexport const BRANCH_STATE_VALUES = [\"idle\", \"changes\", \"ready\", \"steward\", \"blocked\"] as const satisfies readonly BranchState[];\n\ntype CapabilitySource = \"catalog\" | \"provider\" | \"runtime\" | \"override\" | \"estimate\";\ntype CapabilityConfidence = \"declared\" | \"reported\" | \"verified\" | \"estimated\" | \"unknown\";\n\nexport interface ProviderCapabilities {\n lifecycle: {\n managed: boolean;\n shutdownHard: boolean;\n restartHard: boolean;\n semanticStatus?: boolean;\n reconnect?: boolean;\n hibernate?: boolean;\n resume?: boolean;\n };\n model?: {\n provider?: SpawnProvider | string;\n id?: string;\n alias?: string;\n providerModel?: string;\n effort?: SpawnEffort | string;\n source: CapabilitySource;\n confidence: CapabilityConfidence;\n lastUpdatedAt?: number;\n };\n session?: {\n approvalMode?: SpawnApprovalMode | string;\n fileRead?: boolean;\n fileWrite?: boolean;\n shell?: boolean;\n source: CapabilitySource;\n confidence: CapabilityConfidence;\n lastUpdatedAt?: number;\n };\n context?: {\n stats?: {\n source: \"api\" | \"statusline\" | \"hook\" | \"estimate\";\n confidence: \"exact\" | \"reported\" | \"estimated\";\n };\n compact?: boolean;\n clear?: boolean;\n inject?: boolean;\n fork?: boolean;\n rollback?: boolean;\n archive?: boolean;\n };\n liveSession?: {\n capture?: boolean;\n inject?: boolean;\n /** Provider can be interrupted mid-turn from the dashboard (ESC / turn-interrupt). */\n interrupt?: boolean;\n /** Prompts typed directly into the provider (web terminal/TUI) are mirrored into chat. */\n promptEcho?: boolean;\n /** Reasoning/tool activity steps are surfaced to chat as discreet session events. */\n reasoning?: boolean;\n /** Provider exposes a slash-command catalog for the chat input palette. */\n slashCommands?: boolean;\n };\n terminal?: {\n live?: {\n read?: boolean;\n write?: boolean;\n };\n attach?: {\n create?: boolean;\n read?: boolean;\n write?: boolean;\n detach?: boolean;\n };\n };\n source: CapabilitySource;\n confidence: CapabilityConfidence;\n lastUpdatedAt: number;\n}\n\ntype ContextLifecycleState =\n | \"fresh\"\n | \"primed\"\n | \"working\"\n | \"cooling\"\n | \"compacting\"\n | \"hibernating\";\n\nexport interface ContextProbeMetrics {\n agentId: string;\n contextPercent: number;\n tokensUsed?: number;\n tokensMax?: number;\n quotaUsed?: number;\n quotaLimit?: number;\n quotaResetIn?: number;\n model?: string;\n effort?: string;\n source: \"statusline\" | \"hook\" | \"api\" | \"estimate\";\n confidence: \"exact\" | \"reported\" | \"estimated\";\n timestamp: number;\n}\n\nexport interface ContextState {\n utilization: number;\n tokensUsed?: number;\n tokensMax?: number;\n lifecycleState: ContextLifecycleState;\n warmTopics: string[];\n activeMemories: string[];\n tasksSinceCompact: number;\n lastCompactedAt?: number;\n lastUpdatedAt: number;\n source: \"statusline\" | \"hook\" | \"api\" | \"estimate\";\n confidence: \"exact\" | \"reported\" | \"estimated\";\n}\n\nexport interface ContextSnapshot {\n id: number;\n agentId: string;\n context: ContextState;\n utilization: number;\n lifecycleState: ContextLifecycleState;\n tokensUsed?: number;\n tokensMax?: number;\n source: ContextState[\"source\"];\n confidence: ContextState[\"confidence\"];\n capturedAt: number;\n}\n\nexport type MemoryType = \"organization\" | \"role\" | \"project\" | \"task\" | \"interaction\" | \"agent\";\nexport type MemoryVisibility = \"private\" | \"project\" | \"org\" | \"public\";\nexport type MemorySensitivity = \"public\" | \"normal\" | \"sensitive\" | \"secret\";\nexport type MemoryConfidence = \"reported\" | \"inferred\" | \"verified\";\nexport type MemoryRedactionState = \"raw\" | \"redacted\" | \"rejected\";\n\nexport interface Memory {\n id: string;\n type: MemoryType;\n scope: string;\n title: string;\n content: string;\n tags: string[];\n visibility: MemoryVisibility;\n sensitivity: MemorySensitivity;\n confidence: MemoryConfidence;\n redactionState: MemoryRedactionState;\n relevanceScore: number;\n sourceAgent?: string;\n sourceTask?: number;\n createdBy?: string;\n contentHash?: string;\n metadata: Record<string, unknown>;\n accessCount: number;\n lastAccessedAt?: number;\n createdAt: number;\n updatedAt: number;\n expiresAt?: number;\n}\n\nexport interface CreateMemoryInput {\n type: MemoryType;\n scope: string;\n title: string;\n content: string;\n tags?: string[];\n visibility?: MemoryVisibility;\n sensitivity?: MemorySensitivity;\n confidence?: MemoryConfidence;\n redactionState?: MemoryRedactionState;\n relevanceScore?: number;\n sourceAgent?: string;\n sourceTask?: number;\n createdBy?: string;\n metadata?: Record<string, unknown>;\n ttlMs?: number;\n}\n\nexport interface UpdateMemoryInput {\n title?: string;\n content?: string;\n tags?: string[];\n visibility?: MemoryVisibility;\n sensitivity?: MemorySensitivity;\n confidence?: MemoryConfidence;\n redactionState?: MemoryRedactionState;\n relevanceScore?: number;\n metadata?: Record<string, unknown>;\n expiresAt?: number | null;\n}\n\nexport interface MemoryQuery {\n type?: MemoryType;\n scope?: string;\n tags?: string[];\n minRelevance?: number;\n limit?: number;\n includeExpired?: boolean;\n visibility?: MemoryVisibility;\n includeSensitive?: boolean;\n}\n\nexport interface MemorySearchResult {\n memories: Memory[];\n total: number;\n}\n\nexport interface PackagedMemory {\n memory: Memory;\n reason: string;\n priority: 1 | 2 | 3;\n score?: number;\n}\n\nexport interface TaskRoutingHints {\n id?: number;\n title?: string;\n text?: string;\n scope?: string;\n tags?: string[];\n capabilities?: string[];\n target?: string;\n}\n\nexport interface ContextBudget {\n maxTokens: number;\n maxMemories: number;\n priorityCutoff: 1 | 2 | 3;\n}\n\ninterface TaskHistorySummary {\n taskId: number;\n agentId: string;\n status: string;\n summary?: string;\n completedAt?: number;\n}\n\nexport interface ContextPackage {\n memories: PackagedMemory[];\n rolePrompt?: string;\n recentContext?: string;\n taskHistory?: TaskHistorySummary[];\n estimatedTokens: number;\n}\n\nexport interface ContextPackageRequest {\n task: TaskRoutingHints;\n agent: AgentCard;\n budget: ContextBudget;\n memories?: Memory[];\n}\n\nexport interface MemoryStats {\n total: number;\n byType: Partial<Record<MemoryType, number>>;\n byScope: Record<string, number>;\n bySensitivity: Partial<Record<MemorySensitivity, number>>;\n}\n\nexport type ActiveMemoryClearReason = \"compact\" | \"clearContext\" | \"restart\" | \"shutdown\" | \"manual\";\n\nexport interface MemoryBrokerCapabilities {\n search: true;\n create: boolean;\n update: boolean;\n delete: boolean;\n stats: boolean;\n assemble: boolean;\n activeTracking: boolean;\n activeList: boolean;\n external: boolean;\n}\n\nexport interface MemoryBrokerContext {\n now: number;\n actor: string;\n scopes: string[];\n relayUrl?: string;\n requestId?: string;\n}\n\nexport type MemoryBrokerKind = \"sqlite\" | \"http\" | \"command\";\n\ninterface SqliteMemoryBrokerConfig {\n type: \"sqlite\";\n}\n\nexport interface HttpMemoryBrokerConfig {\n type: \"http\";\n url: string;\n tokenEnv?: string;\n timeoutMs?: number;\n}\n\nexport interface CommandMemoryBrokerConfig {\n type: \"command\";\n command: string;\n args?: string[];\n timeoutMs?: number;\n}\n\nexport type MemoryBrokerConfig = SqliteMemoryBrokerConfig | HttpMemoryBrokerConfig | CommandMemoryBrokerConfig;\n\nexport type MessageKind =\n | \"chat\"\n | \"channel.event\"\n | \"task\"\n | \"pair\"\n | \"control\"\n | \"system\"\n | \"session\";\n\n/**\n * Mechanical message kinds: the relay's own lifecycle/observability lane, not\n * agent-directed messaging. They bypass delivery resolution (never re-delivered\n * into a session) and — when targeting a reserved sink — the recipient-constraint\n * auth check (a managed token's `targets`/`policies`/`agents` constraints gate which\n * *agents* it may message, not a session-mirror capture to the reserved sink; #284).\n * Keep in sync with the `MessageKind` union.\n */\nexport const MECHANICAL_MESSAGE_KINDS: readonly MessageKind[] = [\"system\", \"control\", \"session\"];\n\nexport function isMechanicalMessageKind(kind: string | undefined): boolean {\n return MECHANICAL_MESSAGE_KINDS.includes((kind ?? \"\") as MessageKind);\n}\n\n/**\n * Reserved built-in identities that are not real agents: the `user` chat/mirror sink\n * and the `system` lifecycle sender. They are never registered, polled, or delivered to\n * like agents, so recipient constraints don't apply to mechanical posts addressed to them.\n */\nexport const RESERVED_AGENT_IDS: readonly string[] = [\"user\", \"system\"];\n\nexport function isReservedAgentId(id: string | undefined): boolean {\n return id === \"user\" || id === \"system\";\n}\n\n/**\n * Session-mirror event taxonomy. Every `kind: \"session\"` message carries a\n * `payload.session` of this shape so the dashboard can render the live provider\n * session faithfully regardless of which surface (chat box or web terminal)\n * started the turn. `prompt`/`response` render as chat bubbles; `narration` is\n * the agent's intermediate spoken text between tool calls (the terminal's `●`\n * lines) and renders inline in the turn's activity trace; `reasoning`, `tool`,\n * and `notice` render discreetly (collapsed/inline activity, never bubbles).\n * A legacy session message with no `payload.session` is treated as a `response`.\n */\ntype SessionEventType = \"prompt\" | \"response\" | \"narration\" | \"reasoning\" | \"tool\" | \"notice\";\n\ntype SessionEventOrigin = \"chat\" | \"terminal\" | \"provider\";\n\nexport interface MessageSessionMeta {\n type: SessionEventType;\n /** Where the event originated. `terminal` = typed into the provider directly. */\n origin?: SessionEventOrigin;\n /** Groups reasoning/tool steps and the final response under one provider turn. */\n turnId?: string;\n /** Short label: tool name (\"Bash\"), \"Thinking\", a slash command, etc. */\n label?: string;\n /** Tool/step lifecycle status when type is \"tool\": running | completed | failed. */\n status?: \"running\" | \"completed\" | \"failed\";\n /** True while the step body is still being streamed in (coalesced deltas). */\n streaming?: boolean;\n /** Provider that produced the event. */\n provider?: string;\n}\n\nexport type ArtifactKind = \"image\" | \"audio\" | \"video\" | \"document\" | \"archive\" | \"other\";\nexport type ArtifactSensitivity = \"public\" | \"normal\" | \"sensitive\" | \"secret\";\nexport type ArtifactVisibility = \"private\" | \"project\" | \"org\";\nexport type ArtifactRole = \"media\" | \"patch\" | \"report\" | \"log\" | \"output\" | \"input\";\n\nexport interface ArtifactBlob {\n digest: string;\n storageUri: string;\n mediaType: string;\n size: number;\n createdAt: number;\n}\n\nexport interface ArtifactLink {\n id: string;\n artifactId: string;\n entityType: \"message\" | \"task\" | \"recipeRun\" | \"recipeStep\" | \"channelEvent\";\n entityId: string;\n role?: ArtifactRole;\n title?: string;\n createdBy: string;\n createdAt: number;\n}\n\nexport interface Artifact {\n id: string;\n blobDigest: string;\n mediaType: string;\n kind: ArtifactKind;\n filename?: string;\n size: number;\n digest: string;\n visibility: ArtifactVisibility;\n sensitivity: ArtifactSensitivity;\n createdBy: string;\n createdAt: number;\n expiresAt?: number;\n metadata: Record<string, unknown>;\n links?: ArtifactLink[];\n url?: string;\n}\n\nexport interface AttachmentRef {\n artifactId: string;\n kind?: ArtifactKind;\n role?: ArtifactRole;\n title?: string;\n ref?: AttachmentSourceRef;\n metadata?: Record<string, unknown>;\n}\n\nexport interface ChannelAttachmentRef {\n artifactId?: string;\n kind?: ArtifactKind;\n role?: ArtifactRole;\n title?: string;\n ref?: AttachmentSourceRef;\n metadata?: Record<string, unknown>;\n}\n\nexport type AttachmentSourceRef =\n | { type: \"relay-blob\"; id: string; [key: string]: unknown }\n | { type: \"external-url\"; url: string; [key: string]: unknown }\n | { type: \"channel-file\"; id: string; provider?: string; uniqueId?: string; [key: string]: unknown };\n\nexport interface Message {\n id: number;\n from: string;\n to: string; // agent-id | \"tag:<name>\" | \"broadcast\" | \"cap:<name>\"\n kind: MessageKind;\n channel?: string;\n subject?: string;\n body: string;\n threadId?: number;\n replyTo?: number;\n // Server-owned reply obligation (#283). Absent/true = the message wants a response (default);\n // false = a notification (FYI, merge notice, lifecycle event) that must NOT be replied to.\n // The server keys footer rendering and the reply-obligation tracker off this, not off `from`.\n replyExpected?: boolean;\n claimable?: boolean;\n claimedBy?: string;\n claimedAt?: number;\n claimExpiresAt?: number;\n idempotencyKey?: string;\n deliveryStatus?: MessageDeliveryStatus;\n deliveryAttempts?: number;\n deliveryLastError?: string;\n deliveryNextRetryAt?: number;\n deliveryPoisonReason?: string;\n deliveryUpdatedAt?: number;\n queuedAt?: number;\n maxAgeSeconds?: number;\n resolvedToAgent?: string;\n payload: Record<string, unknown>;\n meta?: Record<string, unknown>;\n reactions?: MessageReaction[];\n readBy: string[];\n createdAt: number;\n // True event time (#196). Equals createdAt for messages posted live; differs only when a\n // Runner backfilled a message queued during an outage. Read `occurredAt ?? createdAt`.\n occurredAt?: number;\n}\n\nexport interface ReplyObligation {\n messageId: number;\n agentId: string;\n from: string;\n kind: MessageKind;\n subject?: string;\n channel?: string;\n bodyPreview: string;\n createdAt: number;\n replyCommand: string;\n}\n\nexport interface MessageReaction {\n messageId: number;\n actorId: string;\n emoji: string;\n createdAt: number;\n updatedAt: number;\n}\n\nexport type RelayNotificationKind =\n | \"message\"\n | \"reply\"\n | \"error\"\n | \"agent.blocked\";\n\nexport type RelayNotificationSeverity = \"info\" | \"warning\" | \"error\";\n\nexport interface RelayNotification {\n id: string;\n kind: RelayNotificationKind;\n severity: RelayNotificationSeverity;\n title: string;\n body: string;\n messageId?: number;\n agentId?: string;\n threadPeer?: string;\n view?: \"chat\" | \"messages\" | \"agents\" | \"work\" | \"tasks\" | \"channels\" | \"overview\";\n createdAt: number;\n}\n\nexport type MessageDeliveryStatus = \"pending\" | \"delivered\" | \"queued\" | \"failed\" | \"dead\";\n\nexport interface MessageDeliveryAttempt {\n id: number;\n messageId: number;\n agentId?: string;\n action: \"attempt\" | \"retry-now\" | \"mark-dead\" | \"clear\";\n status: MessageDeliveryStatus;\n error?: string;\n nextRetryAt?: number;\n poisonReason?: string;\n createdAt: number;\n}\n\nexport interface MessageDeliveryState extends Pick<Message,\n | \"id\"\n | \"to\"\n | \"deliveryStatus\"\n | \"deliveryAttempts\"\n | \"deliveryLastError\"\n | \"deliveryNextRetryAt\"\n | \"deliveryPoisonReason\"\n | \"deliveryUpdatedAt\"\n | \"queuedAt\"\n | \"maxAgeSeconds\"\n | \"resolvedToAgent\"\n> {\n attempts: MessageDeliveryAttempt[];\n}\n\nexport interface SendMessageInput {\n from: string;\n to: string;\n kind?: MessageKind;\n channel?: string;\n subject?: string;\n body: string;\n replyTo?: number;\n // Defaults to true server-side. Set false to mark a notification (no reply wanted) — the\n // server suppresses the reply-scaffold footer and appends a one-line no-reply nudge (#283).\n replyExpected?: boolean;\n claimable?: boolean;\n idempotencyKey?: string;\n maxAgeSeconds?: number;\n attachments?: AttachmentRef[];\n payload?: Record<string, unknown>;\n meta?: Record<string, unknown>;\n // Epoch-ms event time, stamped when the event actually occurred (#196). When a Runner\n // queues a message through its durable outbox during a server outage, this preserves the\n // true time rather than the (later) server receive time. Defaults server-side to created_at.\n occurredAt?: number;\n}\n\nexport type ConnectorKind = \"channel\" | \"event\" | \"provider\" | \"orchestrator\";\nexport type ConnectorAction = \"install\" | \"uninstall\" | \"enable\" | \"disable\" | \"start\" | \"stop\" | \"restart\" | \"status\" | \"doctor\";\n\nexport interface ConnectorManifest {\n schema: \"agent-relay.connector.v1\";\n id: string;\n kind: ConnectorKind;\n packageName?: string;\n binary: string;\n displayName: string;\n description?: string;\n version: string;\n capabilities: string[];\n commands: Partial<Record<ConnectorAction, string[]>>;\n configSchema?: Record<string, unknown>;\n}\n\ninterface ConnectorRuntime {\n installed: boolean;\n enabled?: boolean;\n running?: boolean;\n status?: \"ok\" | \"warn\" | \"error\" | \"unknown\";\n detail?: string;\n updatedAt?: string;\n raw?: unknown;\n}\n\nexport interface ConnectorSummary {\n id: string;\n kind: ConnectorKind;\n displayName: string;\n description?: string;\n version: string;\n packageName?: string;\n binary: string;\n capabilities: string[];\n registryPath: string;\n manifest: ConnectorManifest;\n config?: Record<string, unknown>;\n state?: Record<string, unknown>;\n runtime: ConnectorRuntime;\n}\n\nexport interface ConnectorActionResult {\n connectorId: string;\n action: ConnectorAction;\n command?: string[];\n ok: boolean;\n exitCode?: number | null;\n stdout?: string;\n stderr?: string;\n parsed?: unknown;\n}\n\nexport type PairStatus = \"pending\" | \"active\" | \"ended\" | \"rejected\" | \"expired\";\n\nexport interface PairSession {\n id: string;\n requesterId: string;\n targetId: string;\n status: PairStatus;\n objective?: string;\n createdAt: number;\n updatedAt: number;\n expiresAt: number;\n acceptedAt?: number;\n endedAt?: number;\n endedBy?: string;\n lastMessageAt?: number;\n meta: Record<string, unknown>;\n}\n\nexport interface CreatePairInput {\n from: string;\n target: string;\n objective?: string;\n ttlMs?: number;\n meta?: Record<string, unknown>;\n}\n\nexport interface PairActionInput {\n agentId: string;\n reason?: string;\n}\n\nexport interface PairMessageInput {\n from: string;\n body: string;\n subject?: string;\n}\n\nexport interface PollQuery {\n for: string; // agent-id\n since?: number; // unix ms (createdAt cursor)\n sinceId?: number; // monotonic message id cursor (preferred — avoids same-ms collisions)\n unread?: boolean;\n channel?: string;\n limit?: number;\n}\n\nexport interface RegisterAgentInput {\n id: string;\n name: string;\n kind?: AgentKind;\n label?: string | null;\n tags?: string[];\n machine?: string;\n rig?: string;\n capabilities?: string[];\n ready?: boolean;\n status?: AgentCard[\"status\"];\n instanceId?: string;\n providerCapabilities?: ProviderCapabilities;\n context?: ContextState;\n meta?: Record<string, unknown>;\n /** Parent agent id. Server sets this authoritatively from the registering token's\n * `spawnedBy`/`parentAgents` constraint; any client-supplied value is ignored. */\n spawnedBy?: string;\n}\n\nexport interface AgentSessionGuard {\n instanceId?: string;\n epoch?: number;\n}\n\nexport type TaskSeverity = \"info\" | \"warning\" | \"critical\";\nexport type TaskStatus =\n | \"open\"\n | \"claimed\"\n | \"in_progress\"\n | \"blocked\"\n | \"orphaned\"\n | \"done\"\n | \"failed\"\n | \"canceled\";\n\nexport type CommandStatus =\n | \"pending\"\n | \"accepted\"\n | \"running\"\n | \"succeeded\"\n | \"failed\"\n | \"timed_out\"\n | \"rejected\"\n | \"canceled\";\n\nexport interface Command {\n id: string;\n type: string;\n source: string;\n target: string;\n params: Record<string, unknown>;\n status: CommandStatus;\n result?: Record<string, unknown>;\n error?: string;\n correlationId?: string;\n createdAt: number;\n updatedAt: number;\n expiresAt?: number;\n}\n\nexport interface CreateCommandInput {\n type: string;\n source: string;\n target: string;\n params?: Record<string, unknown>;\n correlationId?: string;\n ttlMs?: number;\n}\n\nexport interface UpdateCommandInput {\n status?: CommandStatus;\n result?: Record<string, unknown>;\n error?: string;\n}\n\nexport interface Task {\n id: number;\n source: string;\n title: string;\n body: string;\n severity: TaskSeverity;\n status: TaskStatus;\n target: string;\n channel?: string;\n dedupeKey?: string;\n externalUrl?: string;\n occurrenceCount: number;\n claimedBy?: string;\n claimedAt?: number;\n claimExpiresAt?: number;\n messageId?: number;\n result?: string;\n metadata: Record<string, unknown>;\n createdAt: number;\n updatedAt: number;\n lastSeenAt: number;\n}\n\nexport interface TaskEvent {\n id: number;\n taskId: number;\n source: string;\n type: string;\n severity: TaskSeverity;\n title: string;\n body: string;\n metadata: Record<string, unknown>;\n createdAt: number;\n}\n\nexport interface IntegrationEventInput {\n source?: string;\n type?: string;\n severity?: TaskSeverity;\n status?: TaskStatus | \"resolved\";\n title: string;\n body: string;\n target: string;\n channel?: string;\n dedupeKey?: string;\n externalUrl?: string;\n attachments?: AttachmentRef[];\n metadata?: Record<string, unknown>;\n}\n\nexport interface IntegrationTaskStats {\n source: string;\n tasks: number;\n openTasks: number;\n waitingTasks: number;\n failedTasks: number;\n lastSeenAt?: number;\n lastUpdatedAt?: number;\n}\n\nexport interface IntegrationSummary {\n name: string;\n displayName?: string;\n description?: string;\n enabled: boolean;\n configured: boolean;\n observed: boolean;\n type?: string;\n icon?: string;\n accentColor?: string;\n tags: string[];\n homepageUrl?: string;\n repositoryUrl?: string;\n docsUrl?: string;\n manifest?: Record<string, unknown>;\n scopes: string[];\n targets: string[];\n channels: string[];\n callbackHost?: string;\n callbackConfigured: boolean;\n rateLimit: {\n limitPerMinute: number;\n currentWindowCount: number;\n windowStartedAt?: number;\n };\n taskStats: IntegrationTaskStats;\n}\n\nexport type RuntimeContractName =\n | \"relayApi\"\n | \"orchestratorProtocol\"\n | \"runnerProtocol\"\n | \"providerPluginProtocol\";\n\nexport type RuntimeContracts = Partial<Record<RuntimeContractName, number>>;\nexport type RuntimeCapabilities = Record<string, boolean>;\n\nexport interface RuntimePackageMetadata {\n name: string;\n version: string;\n}\n\nexport interface ContractCompatibilityIssue {\n contract: string;\n expected: string;\n actual?: number;\n}\n\nexport interface ContractCompatibility {\n status: \"compatible\" | \"incompatible\" | \"unknown\";\n compatible: boolean;\n issues: ContractCompatibilityIssue[];\n}\n\nexport type AutomationKind = \"scheduled_task\";\nexport type AutomationCatchUpPolicy = \"skip\" | \"run_once\" | \"run_all\";\nexport type AutomationConcurrencyPolicy = \"skip\" | \"queue\" | \"replace\";\nexport type AutomationRunStatus =\n | \"scheduled\"\n | \"dispatching\"\n | \"waiting_agent\"\n | \"running\"\n | \"succeeded\"\n | \"failed\"\n | \"canceled\"\n | \"timed_out\";\n\nexport interface AutomationExistingAgentPolicy {\n mode: \"existing_agent\";\n selector: {\n provider?: SpawnProvider;\n label?: string;\n tags?: string[];\n capabilities?: string[];\n };\n ifNoMatch?: \"fail\" | \"spawn\";\n}\n\nexport interface AutomationOnDemandAgentPolicy {\n mode: \"on_demand_agent\";\n provider: SpawnProvider;\n model?: string;\n effort?: SpawnEffort;\n cwd?: string;\n workspaceMode?: WorkspaceMode;\n profile?: string;\n approvalMode?: SpawnApprovalMode;\n keepAlive?: boolean;\n runtimeBudget?: AutomationRuntimeBudget;\n shutdownAfterMs?: number;\n}\n\nexport type AutomationTargetPolicy = AutomationExistingAgentPolicy | AutomationOnDemandAgentPolicy;\n\nexport interface AutomationRuntimeBudget {\n maxRuntimeMs: number;\n warnAtMs?: number;\n warningMessage?: string;\n}\n\nexport interface AutomationTaskTemplate {\n title: string;\n body: string;\n severity?: TaskSeverity;\n dedupeKey?: string;\n externalUrl?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface Automation {\n id: string;\n kind: AutomationKind;\n name: string;\n description?: string;\n enabled: boolean;\n schedule: string;\n timezone: string;\n nextRunAt?: number;\n catchUpPolicy: AutomationCatchUpPolicy;\n concurrencyPolicy: AutomationConcurrencyPolicy;\n orchestratorId: string;\n targetPolicy: AutomationTargetPolicy;\n taskTemplate: AutomationTaskTemplate;\n createdAt: number;\n updatedAt: number;\n}\n\nexport interface AutomationRun {\n id: string;\n automationId: string;\n status: AutomationRunStatus;\n scheduledFor: number;\n startedAt?: number;\n finishedAt?: number;\n orchestratorId: string;\n targetAgentId?: string;\n spawnedAgentId?: string;\n taskId?: number;\n messageId?: number;\n error?: string;\n result?: Record<string, unknown>;\n meta: Record<string, unknown>;\n createdAt: number;\n updatedAt: number;\n shutdownRequestedAt?: number;\n}\n\nexport interface CreateAutomationInput {\n kind?: AutomationKind;\n name: string;\n description?: string;\n enabled?: boolean;\n schedule: string;\n timezone?: string;\n catchUpPolicy?: AutomationCatchUpPolicy;\n concurrencyPolicy?: AutomationConcurrencyPolicy;\n orchestratorId: string;\n targetPolicy: AutomationTargetPolicy;\n taskTemplate: AutomationTaskTemplate;\n}\n\nexport interface UpdateAutomationInput {\n name?: string;\n description?: string | null;\n enabled?: boolean;\n schedule?: string;\n timezone?: string;\n catchUpPolicy?: AutomationCatchUpPolicy;\n concurrencyPolicy?: AutomationConcurrencyPolicy;\n orchestratorId?: string;\n targetPolicy?: AutomationTargetPolicy;\n taskTemplate?: AutomationTaskTemplate;\n}\n\nexport type ChannelDirection = \"inbound\" | \"outbound\" | \"bidirectional\";\n\nexport type ChannelRouteTarget =\n | { type: \"agent\"; id: string }\n | { type: \"label\"; id: string }\n | { type: \"tag\"; id: string }\n | { type: \"capability\"; id: string }\n | { type: \"broadcast\" }\n | { type: \"orchestrator\"; id: string }\n | { type: \"pool\"; id: string }\n | { type: \"policy\"; id: string };\n\nexport type ChannelBindingMode = \"exclusive\" | \"broadcast\";\n\nexport interface ChannelBinding {\n id: string;\n channelId: string;\n conversationId?: string;\n target: ChannelRouteTarget;\n mode: ChannelBindingMode;\n priority: number;\n createdAt: number;\n updatedAt: number;\n poolSelector?: string;\n poolAgentId?: string;\n poolAgentEpoch?: number;\n poolClaimExpiresAt?: number;\n}\n\nexport interface ChannelTargetHealth {\n status: \"ok\" | \"warning\" | \"error\";\n detail: string;\n target: ChannelRouteTarget;\n matches: Array<{\n id: string;\n name: string;\n status: AgentCard[\"status\"];\n ready: boolean;\n lastSeen: number;\n label?: string;\n tags: string[];\n capabilities: string[];\n }>;\n}\n\nexport interface ChannelSummary {\n id: string;\n name: string;\n type: string;\n transport: string;\n agentId: string;\n accountId: string;\n status: AgentCard[\"status\"];\n ready: boolean;\n direction: ChannelDirection;\n target?: string;\n binding?: ChannelBinding;\n targetHealth?: ChannelTargetHealth;\n topicChannels: string[];\n capabilities: string[];\n tags: string[];\n lastSeen: number;\n meta?: Record<string, unknown>;\n}\n\nexport interface ChannelEventInput {\n body?: string;\n payload: Record<string, unknown>;\n attachments?: ChannelAttachmentRef[];\n conversationId?: string;\n idempotencyKey?: string;\n instanceId?: string;\n epoch?: number;\n}\n\nexport interface ChannelEventResult {\n messages: Message[];\n bindings: ChannelBinding[];\n created: boolean;\n}\n\nexport interface TaskStatusInput {\n status: TaskStatus;\n agentId?: string;\n instanceId?: string;\n epoch?: number;\n result?: string;\n body?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface InboxThreadState {\n operatorId: string;\n peerId: string;\n readCursorMessageId?: number;\n archivedAtMessageId?: number;\n updatedAt: number;\n}\n\nexport interface InboxDraft {\n operatorId: string;\n peerId: string;\n body: string;\n subject?: string;\n channel?: string;\n updatedAt: number;\n}\n\nexport interface InboxState {\n operatorId: string;\n threads: InboxThreadState[];\n drafts: InboxDraft[];\n}\n\nexport interface ChatHistoryImportEntry {\n position: number;\n originalMessageId: number;\n originalFrom: string;\n originalTo: string;\n originalCreatedAt: number;\n message: Message;\n}\n\nexport interface ChatHistoryImport {\n id: string;\n targetAgentId?: string;\n targetSpawnRequestId?: string;\n sourcePeerId: string;\n sourceAgentId?: string;\n sourceThreadId?: string;\n sourceAgentLabel?: string;\n importedBy: string;\n importedAt: number;\n entries: ChatHistoryImportEntry[];\n}\n\nexport type ActivityKind = \"message\" | \"reply\" | \"question\" | \"operator\" | \"pair\" | \"task\" | \"state\";\n\nexport interface ActivityEvent {\n id: number;\n operatorId?: string;\n clientId?: string;\n kind: ActivityKind;\n title: string;\n body?: string;\n meta?: string;\n icon?: string;\n view?: string;\n peer?: string;\n messageId?: number;\n pairId?: string;\n taskId?: number;\n agentId?: string;\n metadata: Record<string, unknown>;\n createdAt: number;\n}\n\nexport interface ActivityEventInput {\n operatorId?: string;\n clientId?: string;\n kind: ActivityKind;\n title: string;\n body?: string;\n meta?: string;\n icon?: string;\n view?: string;\n peer?: string;\n messageId?: number;\n pairId?: string;\n taskId?: number;\n agentId?: string;\n metadata?: Record<string, unknown>;\n}\n\n// --- Orchestrators ---\n\nexport type OrchestratorStatus = \"online\" | \"offline\";\n\n/** Spawn providers — the runtime tuple is the single source of truth; the type derives from it. */\nexport const SPAWN_PROVIDERS = [\"claude\", \"codex\"] as const;\nexport type SpawnProvider = (typeof SPAWN_PROVIDERS)[number];\nfunction isSpawnProvider(value: unknown): value is SpawnProvider {\n return typeof value === \"string\" && (SPAWN_PROVIDERS as readonly string[]).includes(value);\n}\n\n/** Approval modes — runtime tuple + derived type. */\nexport const APPROVAL_MODES = [\"open\", \"guarded\", \"read-only\"] as const;\nexport type SpawnApprovalMode = (typeof APPROVAL_MODES)[number];\nfunction isApprovalMode(value: unknown): value is SpawnApprovalMode {\n return typeof value === \"string\" && (APPROVAL_MODES as readonly string[]).includes(value);\n}\n\nexport type SpawnEffort = \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\n\n/** Workspace modes — runtime tuple + derived type + guard. */\nexport const VALID_WORKSPACE_MODES = [\"isolated\", \"shared\", \"inherit\"] as const;\nexport type WorkspaceMode = (typeof VALID_WORKSPACE_MODES)[number];\nexport function isWorkspaceMode(value: unknown): value is WorkspaceMode {\n return typeof value === \"string\" && (VALID_WORKSPACE_MODES as readonly string[]).includes(value);\n}\n/** Narrow an unknown to a WorkspaceMode, or undefined if it isn't one. */\nexport function normalizeWorkspaceMode(value: unknown): WorkspaceMode | undefined {\n return isWorkspaceMode(value) ? value : undefined;\n}\nexport type WorkspaceStatus = \"active\" | \"ready\" | \"conflict\" | \"review_requested\" | \"merge_planned\" | \"merged\" | \"abandoned\" | \"cleanup_requested\" | \"cleaned\";\n/** Terminal workspace statuses shared across server + dashboard filters. */\nexport const TERMINAL_WORKSPACE_STATUS_VALUES = [\"cleaned\", \"merged\", \"abandoned\"] as const satisfies readonly WorkspaceStatus[];\n\n/** How a workspace's work is integrated back into its base branch. */\nexport type WorkspaceMergeStrategy = \"pr\" | \"rebase-ff\" | \"auto\";\n\nexport interface WorkspaceProbeWorktree {\n path: string;\n branch?: string;\n headSha?: string;\n bare?: boolean;\n detached?: boolean;\n prunable?: boolean;\n locked?: boolean;\n reason?: string;\n}\n\nexport interface WorkspaceProbe {\n path: string;\n isGitRepo: boolean;\n repoRoot?: string;\n branch?: string;\n headSha?: string;\n dirty?: boolean;\n detached?: boolean;\n worktrees?: WorkspaceProbeWorktree[];\n error?: string;\n}\n\ninterface WorkspaceGitCommit {\n sha: string;\n message: string;\n at?: number;\n}\n\n/**\n * Live git state of a workspace worktree, computed on the host by the\n * orchestrator. Used to surface whether a worktree contains actual work\n * (commits ahead of base, dirty files) so the dashboard isn't a black box and\n * so exit-time reconcile can decide cleanup-vs-flag.\n */\nexport interface WorkspaceGitState {\n /** Commits on the agent branch not yet in base (raw `rev-list` count). */\n ahead?: number;\n /** Commits still genuinely unmerged after discounting work already present in\n * base via a squash/cherry-pick (git-cherry '+' count). Equals `ahead` for a\n * normal branch; 0 when the work landed but left `ahead` positive. */\n unmergedAhead?: number;\n /** True when `ahead` > 0 but the work is already in base (squash/cherry-pick\n * merge) — the worktree is effectively merged and safe to clean up. */\n landed?: boolean;\n /** Commits in base not in the agent branch (how far base moved on). */\n behind?: number;\n /** True if the worktree has uncommitted/untracked changes. */\n dirty?: boolean;\n /** Number of dirty (porcelain) entries. */\n dirtyCount?: number;\n /** Most recent commit in the worktree. */\n lastCommit?: WorkspaceGitCommit;\n /** Ref/sha the ahead/behind counts were computed against. */\n baseRef?: string;\n /** Branch currently checked out in the worktree (for recorded-vs-live mismatch). */\n branch?: string;\n /** Set when the worktree path no longer exists on disk. */\n missing?: boolean;\n /** Populated when git interrogation failed. */\n error?: string;\n}\n\n/**\n * Pre-flight check for integrating a workspace's work, computed on the host.\n * Reports whether the merge would be clean, conflict, or is a no-op, and which\n * strategy `auto` would pick — so the dashboard can warn before the user acts.\n */\nexport interface WorkspaceMergePreview {\n /** Strategy `auto` would resolve to given the repo's remote/gh state. */\n strategy: \"pr\" | \"rebase-ff\";\n /** Commits on the agent branch not yet in base (raw count). */\n ahead?: number;\n /** Genuinely-unmerged commits after discounting squash/cherry-pick landings. */\n unmergedAhead?: number;\n /** True when the work already landed in base via squash/cherry-pick. */\n landed?: boolean;\n /** Merged PR state for the branch (ground truth when local git can't detect a\n * squash-landing because base has moved on). Only populated when the caller\n * opts in (`checkPr`), since it costs a `gh` network round-trip. */\n prState?: \"merged\" | \"open\";\n /** True when the branch's PR is merged on the remote — the work has landed even\n * if the local commit graph still shows it as ahead. */\n prMerged?: boolean;\n /** URL of the branch's PR, when one was found via `gh`. */\n prUrl?: string;\n /** Commits in base not in the agent branch. */\n behind?: number;\n /** Uncommitted/untracked entries in the worktree. */\n dirtyCount?: number;\n /** True if the three-way merge of base and the branch would conflict. */\n conflict?: boolean;\n /** True if base can fast-forward to the branch with no rebase needed. */\n cleanFastForward?: boolean;\n /** Whether an `origin` remote is configured. */\n hasRemote?: boolean;\n /** Whether the `gh` CLI is available on the host. */\n ghAvailable?: boolean;\n /** Resolved base branch name the work would land on. */\n baseRef?: string;\n /** Human-readable reason a merge can't proceed (no work, dirty, missing). */\n reason?: string;\n /** True when there is nothing to land: ahead=0 with a clean worktree (branch tree\n * already in base — never diverged, or already landed via squash/cherry-pick/PR).\n * A no-op land resolves the workspace to a terminal state instead of parking it in\n * the steward queue (#230). Distinct from a dirty-worktree refusal, which also sets\n * `reason` but is not a no-op. */\n noop?: boolean;\n /** Set when the worktree path no longer exists. */\n missing?: boolean;\n /** Populated when git interrogation failed. */\n error?: string;\n}\n\n/** Outcome of a `workspace.merge` command executed on the host. */\nexport interface WorkspaceMergeResult {\n workspaceId?: string;\n /** Strategy actually used. */\n strategy: \"pr\" | \"rebase-ff\";\n /** True when work was landed locally (rebase-ff into base). */\n merged: boolean;\n /** True when the land was a no-op — nothing to merge (ahead=0, clean worktree),\n * resolved to a terminal `merged` status to clear the steward queue (#230). The\n * worktree/branch are reclaimed too when the owner is gone (`deleteBranch`). */\n noop?: boolean;\n /** True when a merge was prevented by conflicts. */\n conflict?: boolean;\n /** PR URL when the pr strategy opened one. */\n prUrl?: string;\n branch?: string;\n baseRef?: string;\n /** SHA of the landed commit — the agent branch's tip, preserved verbatim (#287).\n * This is the SHA reported in the `branch.landed` notice, so it exists on base\n * exactly as the agent produced it (no rebase rewrite). On a clean fast-forward\n * it equals {@link baseSha}; on a no-ff merge it's the merge's second parent. */\n mergedSha?: string;\n /** SHA base points at after the land (#287): equals {@link mergedSha} on a\n * fast-forward, or the merge commit on a no-ff merge when base had advanced. The\n * relay records this as the recycled workspace's new base_sha. Absent on older\n * orchestrators — consumers fall back to {@link mergedSha}. */\n baseSha?: string;\n /** Subject line of the landed commit (git log -1 --format=%s of mergedSha), for\n * the `branch.landed` notification body (#239). Best-effort; absent on older\n * orchestrators or when the subject can't be read. */\n subject?: string;\n branchDeleted?: boolean;\n worktreeRemoved?: boolean;\n /** Fresh branch the worktree was recycled onto after a land-and-continue merge\n * (#206) — the relay repoints the workspace row at it. Absent for terminal lands. */\n newBranch?: string;\n /** True when the landed base branch was pushed to its upstream (origin). */\n pushed?: boolean;\n /** Resulting workspace status the relay should record. */\n status: WorkspaceStatus;\n /** Deps re-provisioned after a land-and-continue recycle when the advanced base\n * brought new dependencies (issue #51). Absent when nothing was stale. */\n depsRefresh?: WorkspaceDepsRefreshResult;\n /** Populated when the merge could not complete. */\n error?: string;\n}\n\n/**\n * Joined steward briefing for one workspace (#208): the row, owner + orchestrator\n * liveness, live git state, recorded-vs-live branch mismatch, any active steward\n * claim, and a recommended next action — so a steward (or release agent) doesn't\n * reconstruct it from ad-hoc shell + API calls.\n */\nexport interface WorkspaceDiagnostics {\n workspaceId: string;\n status: WorkspaceStatus;\n mode: WorkspaceMode;\n repoRoot: string;\n worktreePath?: string;\n recordedBranch?: string;\n liveBranch?: string;\n baseRef?: string;\n branchMismatch?: boolean;\n owner?: { id?: string; status?: string; online: boolean };\n orchestrator?: { id?: string; online: boolean };\n /** Active claim that auto-merge yields to (#208), if held and unexpired. */\n claim?: { by?: string; purpose?: string; expiresAt?: number };\n /** Live worktree git state (proxied from the host); absent when unavailable. */\n gitState?: WorkspaceGitState;\n /** Why git state is absent (host offline, no worktree, …). */\n gitStateUnavailable?: string;\n recommendation: { action: \"merge\" | \"rebase\" | \"cleanup\" | \"review\" | \"wait\" | \"none\"; confidence: \"high\" | \"medium\" | \"low\"; reason: string };\n}\n\n/** One changed file in a workspace diff against its base. */\nexport interface WorkspaceDiffFile {\n path: string;\n /** Lines added (undefined for binary files). */\n additions?: number;\n /** Lines removed (undefined for binary files). */\n deletions?: number;\n binary?: boolean;\n}\n\n/** Diff of a workspace's committed work against its base, computed on the host. */\nexport interface WorkspaceDiff {\n baseRef?: string;\n ahead?: number;\n files: WorkspaceDiffFile[];\n /** Unified patch text (may be truncated). */\n patch?: string;\n /** True when the patch was capped at the size limit. */\n truncated?: boolean;\n /** Uncommitted entries in the worktree, not part of the committed diff. */\n dirtyCount?: number;\n missing?: boolean;\n error?: string;\n}\n\n/** A worktree found on disk with no matching active workspace row. */\nexport interface WorkspaceOrphan {\n worktreePath: string;\n repoRoot: string;\n branch?: string;\n headSha?: string;\n /** True if a DB row exists for this path but is already terminal (cleaned/merged). */\n hadTerminalRow?: boolean;\n /** Work already merged into base (squash/cherry/PR). Set from the host probe. */\n landed?: boolean;\n /** Raw commits ahead of base (a squash-landed branch still shows >0). */\n ahead?: number;\n /** Commits ahead whose patch isn't already in base — the squash-aware count. */\n unmergedAhead?: number;\n /** Uncommitted working-tree changes in the worktree. */\n dirty?: boolean;\n /**\n * Safe to remove with no loss: clean tree AND (nothing ahead OR already landed).\n * `false` means the worktree holds un-landed work and must be flagged, not reaped.\n * `undefined` means the host couldn't be probed — treat as not safe.\n */\n safeToReap?: boolean;\n}\n\n/**\n * Outcome of provisioning node_modules into a freshly created isolated worktree.\n * Git worktrees do not share gitignored/untracked files, so a new worktree starts\n * with no installed deps — typecheck/test/build would see thousands of phantom\n * missing-module errors. We either symlink the source checkout's node_modules\n * (fast, default) or run a package install (when the source has none to borrow).\n */\nexport interface WorkspaceDepsProvision {\n mode: \"symlink\" | \"install\" | \"none\";\n /** Relative dirs (from repo root) whose node_modules were symlinked. */\n linked?: string[];\n /** Relative dirs where a package install ran successfully. */\n installed?: string[];\n packageManager?: string;\n error?: string;\n}\n\n/** Result of symlinking configured untracked files/dirs from main into an isolated worktree. */\nexport interface WorkspaceSymlinkProvision {\n /** Relative paths (from repo root) that were symlinked from the main checkout. */\n linked: string[];\n /** Per-pattern failures, if any (best-effort: a failure never blocks the spawn). */\n errors?: string[];\n}\n\n/** One project dir's outcome in a workspace deps refresh (issue #51). */\nexport interface WorkspaceDepsRefreshDir {\n /** Relative dir from repo root (`.` for root, e.g. `dashboard`). */\n dir: string;\n /** ok — declared deps already present; stale — missing (check-only, not installed);\n * installed — re-installed in isolation; failed — install errored. */\n status: \"ok\" | \"stale\" | \"installed\" | \"failed\";\n /** Declared deps (dependencies + devDependencies) missing from node_modules, capped sample. */\n missing?: string[];\n /** True when the dir's node_modules was a shared symlink replaced with a real install. */\n wasSymlink?: boolean;\n packageManager?: string;\n error?: string;\n}\n\n/**\n * Outcome of refreshing an isolated worktree's deps (issue #51). The default symlink\n * provisioning shares the source checkout's node_modules, so a dep added to the base\n * AFTER worktree creation is missing in the worktree. Refresh promotes each stale dir\n * to a real, isolated install — never mutating the shared source node_modules.\n */\nexport interface WorkspaceDepsRefreshResult {\n /** True when at least one dir was actually (re)installed. */\n refreshed: boolean;\n /** True (check-only) when any dir is stale, or (refresh) when any install left deps missing. */\n stale?: boolean;\n dirs: WorkspaceDepsRefreshDir[];\n error?: string;\n}\n\nexport interface WorkspaceMetadata {\n id?: string;\n mode: WorkspaceMode;\n requestedMode?: WorkspaceMode;\n repoRoot?: string;\n sourceCwd?: string;\n worktreePath?: string;\n branch?: string;\n baseRef?: string;\n baseSha?: string;\n status?: WorkspaceStatus;\n stewardAgentId?: string;\n deps?: WorkspaceDepsProvision;\n /** Configured untracked files/dirs symlinked from main (see WorkspaceConfig.symlinkPaths). */\n symlinks?: WorkspaceSymlinkProvision;\n probe?: WorkspaceProbe;\n}\n\nexport interface WorkspaceRecord {\n id: string;\n repoRoot: string;\n sourceCwd: string;\n worktreePath: string;\n branch?: string;\n baseRef?: string;\n baseSha?: string;\n mode: WorkspaceMode;\n requestedMode?: WorkspaceMode;\n status: WorkspaceStatus;\n ownerAgentId?: string;\n /** Server-derived from ownerAgentId via the authoritative owner liveness helper. */\n ownerOnline?: boolean;\n ownerPolicyName?: string;\n ownerAutomationRunId?: string;\n stewardAgentId?: string;\n metadata: Record<string, unknown>;\n createdAt: number;\n updatedAt: number;\n readyAt?: number;\n cleanedAt?: number;\n}\n\nexport interface ConfigEntry<T = unknown> {\n namespace: string;\n key: string;\n value: T;\n version: number;\n updatedAt: string;\n updatedBy?: string;\n}\n\nexport interface ConfigHistoryEntry<T = unknown> {\n id: number;\n namespace: string;\n key: string;\n value: T;\n version: number;\n changedAt: string;\n changedBy?: string;\n}\n\nexport type ManagedAgentStatus = \"stopped\" | \"starting\" | \"running\" | \"stopping\" | \"backoff\";\n\nexport interface ManagedAgentState {\n policyName: string;\n status: ManagedAgentStatus;\n agentId?: string;\n orchestratorId: string;\n provider: SpawnProvider;\n tmuxSession?: string;\n spawnRequestId?: string;\n workspaceId?: string;\n workspacePath?: string;\n workspaceBranch?: string;\n lastSpawnAt?: number;\n lastStopAt?: number;\n healthySince?: number;\n restartCount: number;\n consecutiveFailures: number;\n backoffUntil?: number;\n lastError?: string;\n updatedAt: number;\n}\n\ntype SpawnPolicyMode = \"always-on\" | \"on-demand\";\n\nexport type AgentProfileProvider = SpawnProvider | \"any\";\nexport type AgentProfileBase = \"host\" | \"minimal\" | \"isolated\";\nexport type AgentProfileInstructionPolicy = \"allow\" | \"ignore\";\nexport type AgentProfileCategoryMode = \"host\" | \"profile\" | \"repo\" | \"none\";\nexport type AgentProfileFilesystemScope = \"repo\" | \"workspace\" | \"host\";\n\nexport interface AgentProfileAssetRef {\n source: \"relay\" | \"repo\" | \"inline\" | \"provider\";\n ref: string;\n enabled: boolean;\n provider?: AgentProfileProvider;\n meta?: Record<string, unknown>;\n}\n\nexport interface AgentProfileProviderOptions extends Record<string, unknown> {\n codex?: {\n toolOutputTokenLimit?: number | null;\n } & Record<string, unknown>;\n}\n\nexport interface AgentProfile {\n name: string;\n description?: string;\n provider?: AgentProfileProvider;\n base: AgentProfileBase;\n builtIn?: boolean;\n instructions: {\n system?: string;\n append: string[];\n repoInstructions: AgentProfileInstructionPolicy;\n globalInstructions: AgentProfileInstructionPolicy;\n };\n relay: {\n context: boolean;\n skills: boolean;\n plugins: boolean;\n statusLine: boolean;\n // Inject the relay HTTP MCP endpoint into the spawned agent. Default-on for\n // host/minimal; isolated clean-room profiles disable it like they disable\n // plugins. Optional so existing profile literals stay valid; the gate\n // (profileAllowsRelayFeature) treats absent as enabled and config-store\n // resolves a concrete default at write time.\n mcp?: boolean;\n };\n skills: AgentProfileAssetRef[];\n plugins: AgentProfileAssetRef[];\n mcp: {\n mode: AgentProfileCategoryMode;\n servers?: Record<string, unknown>;\n };\n hooks: {\n mode: AgentProfileCategoryMode;\n };\n permissions: {\n mode?: SpawnApprovalMode;\n filesystem: AgentProfileFilesystemScope;\n };\n env: Record<string, string>;\n providerOptions: AgentProfileProviderOptions;\n /** Spawn quota for agents granted this profile: max concurrent LIVE children they may\n * have at once. `0`/undefined = cannot spawn (the strict default). `N` = up to N live\n * children; a child exiting frees a slot. Drives both the minted token's `command:spawn`\n * scope (granted iff `> 0`) and the runtime quota count. Children never inherit it\n * (no grandchildren). */\n maxSpawnedAgents?: number;\n}\n\nexport type AgentProfileProjectionResult = \"applied\" | \"partial\" | \"unsupported\" | \"not-applicable\";\n\nexport interface AgentProfileProjectionEntry {\n capability: string;\n requested: string;\n result: AgentProfileProjectionResult;\n detail: string;\n}\n\nexport interface AgentProfileProjectionReport {\n profileName: string;\n provider: SpawnProvider;\n base: AgentProfileBase;\n generatedAt: number;\n entries: AgentProfileProjectionEntry[];\n warnings: string[];\n unsupported: string[];\n}\n\nexport interface SpawnPolicy {\n name: string;\n description?: string;\n // Desired on/off switch. `false` keeps the policy from (re)spawning even for\n // always-on mode, so a manual stop survives reconcile ticks. Defaults to true.\n enabled?: boolean;\n orchestratorId: string;\n cwd: string;\n provider: SpawnProvider;\n workspaceMode?: WorkspaceMode;\n rig?: string;\n model?: string;\n effort?: SpawnEffort;\n profile?: string;\n providerArgs: string[];\n prompt?: string;\n tags: string[];\n capabilities: string[];\n label?: string;\n mode: SpawnPolicyMode;\n permissionMode: SpawnApprovalMode;\n restartOnUpdate: boolean;\n scheduledDailyRestart: boolean;\n onDemand?: {\n keepaliveSeconds: number;\n idleDefinition: \"no-activity\";\n };\n backoff: {\n schedule: number[];\n resetAfterSeconds: number;\n };\n binding?: {\n type: \"channel\";\n channelId: string;\n };\n}\n\n/**\n * Global, provider-independent configuration for repo steward agents (issue #167).\n * Stewards are auto-provisioned per repo from these settings when one is needed.\n * Editable in the dashboard Settings → Stewards section. Disabled by default —\n * configuring a provider and enabling is the opt-in that also gates auto-spawn.\n */\nexport interface StewardConfig {\n /** When false, no steward is auto-provisioned or woken; the relay falls back to the legacy elected-steward ping. */\n enabled: boolean;\n /** Provider that runs the steward (e.g. claude, codex; future providers extend SpawnProvider). */\n provider: SpawnProvider;\n /** Optional model alias for the chosen provider (e.g. \"gpt-5.5\"). */\n model?: string;\n /** Optional reasoning effort. */\n effort?: SpawnEffort;\n /** Approval mode for the steward — needs merge/command authority, so defaults to \"open\". */\n permissionMode: SpawnApprovalMode;\n /** On-demand idle timeout before the steward is stopped when its queue drains. */\n keepaliveSeconds: number;\n}\n\n/**\n * Global workspace provisioning config for isolated worktrees (the dashboard \"Workspace\"\n * spawn option). A fresh git worktree only contains git-tracked files; node_modules are\n * symlinked from main automatically, and these additional untracked paths are too.\n */\nexport interface WorkspaceConfig {\n /**\n * Files or filename patterns to symlink from the main checkout into each isolated\n * worktree. Plain names (\"AGENTS.md\", \".claude-rig\") match files and directories;\n * entries containing glob metacharacters (*?[]{}) are expanded against main. A path\n * is only linked if it actually exists in main — missing entries are ignored.\n */\n symlinkPaths: string[];\n}\n\n/**\n * Continuous self-improvement (\"Insights\") module config — feature toggles for the\n * dogfooding flywheel (see docs/self-improvement.md, epic #183). Everything here is\n * opt-out: passive, read-only observation, so it defaults on. Each feature is\n * independently togglable so instrumentation can be disabled without losing the rest.\n */\nexport interface InsightsConfig {\n /** Master switch. When false, no signals are recorded regardless of per-feature flags. */\n enabled: boolean;\n /** #184 — server-side context-gathering ratio computed from the session trace at session end. */\n contextRatio: {\n enabled: boolean;\n };\n /** #185 — agent-authored end-of-session introspection artifact (3 fields), gated. */\n introspection: {\n enabled: boolean;\n /** Gate: skip sessions with fewer substantive turns than this (trivial work has nothing to learn). */\n minTurns: number;\n /** Gate: skip when remaining context fraction is below this (poor introspection = noise). 0–1. */\n minContextRemaining: number;\n };\n}\n\n/** Toggle for relay-driven lifecycle push notifications (#239 event bus). These wake\n * recipient agents, so they sit behind a switch the operator can flip off per-event. */\nexport interface NotificationsConfig {\n /** Master switch. When false, no lifecycle push notifications are sent. */\n enabled: boolean;\n /** #239 — push the author \"your branch landed\" + agents-on-main \"merged\" notices at land time. */\n branchLanded: boolean;\n}\n\n/** A single self-improvement datapoint. Generic by design: new instrumentation = new `signal`, not new schema. */\nexport interface InsightObservation {\n id: number;\n /** Session this datapoint belongs to (provider session id). */\n sessionId: string;\n /** Agent that produced the session, when known. */\n agentId?: string;\n /** Project/repo identity — baselines and deltas are scoped per-project. */\n project: string;\n /** Discriminator: which signal this row carries (e.g. \"context_ratio\", \"introspection\"). */\n signal: string;\n /** Signal-specific metric payload. */\n value: Record<string, unknown>;\n /** Paired outcome proxy (rework / operator-correction / success), kept alongside the metric — anti-Goodhart. */\n outcome?: Record<string, unknown>;\n /** Who computed it: \"server\" (relay, from trace) or \"agent\" (session-end hook). */\n source: \"server\" | \"agent\";\n createdAt: number;\n}\n\n/** Per-project + global rollups for the dashboard / API. */\nexport interface InsightsStats {\n signal: string;\n project: string | null; // null = global rollup across projects\n count: number;\n /** Mean of `value.ratio` where present (context_ratio); null for non-numeric signals. */\n avgRatio: number | null;\n lastAt: number | null;\n}\n\nexport interface Orchestrator {\n id: string;\n hostname: string;\n status: OrchestratorStatus;\n agentId: string; // relay agent id for messaging\n providers: SpawnProvider[];\n providerStatus?: ProviderStatusReport[];\n providerCatalog?: ProviderCatalogSummary[];\n baseDir: string;\n apiUrl?: string;\n envKeys: string[]; // names only, never values\n package?: RuntimePackageMetadata;\n contracts?: RuntimeContracts;\n capabilities?: RuntimeCapabilities;\n contractCompatibility?: ContractCompatibility;\n version?: string;\n protocolVersion?: number;\n gitSha?: string;\n health?: OrchestratorHealth;\n // Self-reported host supervision (how the orchestrator's own process is run),\n // used to target a remote self-upgrade at the right unit.\n supervisor?: \"process\" | \"systemd\" | \"launchd\" | \"unknown\";\n selfUnit?: string;\n runtimePrefix?: string;\n // In-flight / last remote upgrade state, reconciled by the relay against the\n // version the orchestrator reports after it restarts.\n upgrade?: OrchestratorUpgradeState;\n meta: Record<string, unknown>;\n managedAgents: ManagedAgent[];\n lastSeen: number;\n createdAt: number;\n}\n\nexport interface OrchestratorUpgradeState {\n desiredVersion: string;\n status: \"pending\" | \"succeeded\" | \"failed\";\n commandId?: string;\n providers?: string[];\n fromVersion?: string;\n requestedBy?: string;\n requestedAt: number;\n settledAt?: number;\n error?: string;\n}\n\nexport interface OrchestratorHealth {\n status: \"ok\" | \"warn\" | \"restart-required\" | \"upgrade-required\" | \"unknown\";\n restartRequired: boolean;\n upgradeRequired?: boolean;\n issues: Array<{\n code: \"missing-version\" | \"package-drift\" | \"missing-contract\" | \"protocol-mismatch\" | \"restart-required\" | \"upgrade-required\";\n detail: string;\n }>;\n}\n\nexport interface ManagedAgent {\n agentId: string;\n provider: SpawnProvider;\n model?: string;\n effort?: SpawnEffort;\n profile?: string;\n workspaceMode?: WorkspaceMode;\n workspace?: WorkspaceMetadata;\n sessionName?: string;\n supervisor?: \"process\" | \"systemd\" | \"launchd\" | \"unknown\";\n systemdUnit?: string;\n terminalSession?: string;\n terminalAvailable?: boolean;\n tmuxSession: string;\n cwd: string;\n label?: string;\n approvalMode: SpawnApprovalMode;\n policyName?: string;\n spawnRequestId?: string;\n automationRunId?: string;\n pid?: number;\n startedAt: number;\n}\n\nexport interface ManagedSessionExitDiagnostics {\n agentId: string;\n provider: SpawnProvider;\n workspaceMode?: WorkspaceMode;\n workspace?: WorkspaceMetadata;\n sessionName?: string;\n tmuxSession: string;\n cwd: string;\n label?: string;\n policyName?: string;\n spawnRequestId?: string;\n automationRunId?: string;\n supervisor: \"process\" | \"systemd\" | \"launchd\" | \"unknown\";\n systemdUnit?: string;\n terminalSession?: string;\n terminalAvailable?: boolean;\n pid?: number;\n currentPid?: number;\n startedAt: number;\n detectedAt: number;\n runtimeMs: number;\n logFile?: string;\n logBytes?: number;\n logEmpty?: boolean;\n logTail?: string[];\n runnerInfoFile?: string;\n runnerInfoPresent?: boolean;\n systemd?: {\n unit: string;\n activeState?: string;\n subState?: string;\n result?: string;\n execMainCode?: string;\n execMainStatus?: string;\n mainPid?: number;\n unavailable?: string;\n };\n unavailable?: string[];\n lastError: string;\n}\n\nexport interface RegisterOrchestratorInput {\n id: string;\n hostname: string;\n providers: SpawnProvider[];\n providerStatus?: ProviderStatusReport[];\n providerCatalog?: ProviderCatalogSummary[];\n baseDir: string;\n apiUrl?: string;\n envKeys?: string[];\n package?: RuntimePackageMetadata;\n contracts?: RuntimeContracts;\n capabilities?: RuntimeCapabilities;\n version?: string;\n protocolVersion?: number;\n gitSha?: string;\n meta?: Record<string, unknown>;\n}\n\nexport interface OrchestratorRuntimeInput {\n package?: RuntimePackageMetadata;\n contracts?: RuntimeContracts;\n capabilities?: RuntimeCapabilities;\n version?: string;\n protocolVersion?: number;\n gitSha?: string;\n providers?: SpawnProvider[];\n providerStatus?: ProviderStatusReport[];\n providerCatalog?: ProviderCatalogSummary[];\n}\n\ninterface OrchestratorSpawnInput {\n provider: SpawnProvider;\n model?: string;\n effort?: SpawnEffort;\n cwd?: string;\n workspaceMode?: WorkspaceMode;\n label?: string;\n approvalMode?: SpawnApprovalMode;\n prompt?: string;\n systemPromptAppend?: string;\n env?: Record<string, string>;\n}\n\nexport interface ProviderStatusReport {\n name: SpawnProvider;\n available: boolean;\n checkedAt: number;\n reason?: string;\n version?: string;\n features?: Record<string, boolean>;\n cli?: {\n command: string;\n path?: string;\n ok: boolean;\n version?: string;\n error?: string;\n };\n runner?: {\n command: string;\n path?: string;\n ok: boolean;\n version?: string;\n error?: string;\n };\n}\n\nexport interface ProviderCatalogSummary {\n provider: SpawnProvider;\n label: string;\n defaultModel?: string;\n models: Array<{\n alias: string;\n label: string;\n providerModel: string;\n efforts: SpawnEffort[];\n defaultEffort?: SpawnEffort;\n limits?: {\n contextWindowTokens?: {\n value: number;\n source: \"catalog\" | \"provider\" | \"runtime\" | \"override\";\n confidence: \"declared\" | \"verified\" | \"estimated\" | \"unknown\";\n lastUpdatedAt?: number;\n };\n maxOutputTokens?: {\n value: number;\n source: \"catalog\" | \"provider\" | \"runtime\" | \"override\";\n confidence: \"declared\" | \"verified\" | \"estimated\" | \"unknown\";\n lastUpdatedAt?: number;\n };\n };\n capabilities?: {\n modalities: {\n input: {\n text: boolean;\n image?: boolean;\n audio?: boolean;\n video?: boolean;\n pdf?: boolean;\n };\n output: {\n text: boolean;\n image?: boolean;\n audio?: boolean;\n video?: boolean;\n };\n };\n tools?: {\n code?: boolean;\n review?: boolean;\n debug?: boolean;\n refactor?: boolean;\n shell?: boolean;\n fileRead?: boolean;\n fileWrite?: boolean;\n webSearch?: boolean;\n imageGeneration?: boolean;\n imageEditing?: boolean;\n };\n source: \"catalog\" | \"provider\" | \"runtime\" | \"override\";\n confidence: \"declared\" | \"verified\" | \"estimated\" | \"unknown\";\n lastUpdatedAt?: number;\n };\n }>;\n}\n\ninterface OrchestratorSpawnResult {\n orchestratorId: string;\n provider: SpawnProvider;\n sessionName?: string;\n supervisor?: \"process\" | \"systemd\" | \"launchd\" | \"unknown\";\n systemdUnit?: string;\n terminalSession?: string;\n terminalAvailable?: boolean;\n tmuxSession: string;\n cwd: string;\n label?: string;\n approvalMode: SpawnApprovalMode;\n pid?: number;\n startedAt: number;\n}\n\nexport interface Recipe {\n name: string;\n description: string;\n version?: string;\n author?: string;\n agents: RecipeAgent[];\n workflow?: RecipeWorkflow;\n lifecycle?: RecipeLifecycle;\n}\n\nexport interface RecipeAgent {\n role: string;\n provider: \"claude\" | \"codex\";\n count?: number;\n capabilities: string[];\n label?: string;\n tags?: string[];\n memoryTags?: string[];\n approvalMode?: \"open\" | \"guarded\" | \"read-only\";\n prompt?: string;\n model?: string;\n effort?: SpawnEffort;\n env?: Record<string, string>;\n}\n\ninterface RecipeWorkflow {\n trigger?: string;\n fanOut?: \"all\" | \"first\";\n collect?: string;\n routing?: RecipeRoute[];\n}\n\ninterface RecipeRoute {\n pattern: string;\n pipeline: string[];\n}\n\ninterface RecipeLifecycle {\n mode?: \"persistent\" | \"ephemeral\";\n idleTimeoutMs?: number;\n memory?: RecipeMemoryPolicy;\n}\n\nexport interface RecipeMemoryPolicy {\n injectOnAssign?: boolean;\n autoCapture?: boolean;\n captureTypes?: MemoryType[];\n memoryTags?: string[];\n alwaysReload?: string[];\n scope?: string;\n maxTokens?: number;\n maxMemories?: number;\n priorityCutoff?: 1 | 2 | 3;\n ttlMs?: number;\n}\n\nexport interface RecipeInstance {\n id: string;\n recipeName: string;\n recipeSource: \"builtin\" | \"user\";\n cwd: string;\n orchestratorId: string;\n status: \"starting\" | \"running\" | \"stopping\" | \"stopped\" | \"failed\";\n agents: RecipeAgentInstance[];\n artifacts?: Artifact[];\n startedAt: number;\n stoppedAt?: number;\n startedBy: string;\n error?: string;\n}\n\nexport interface RecipeAgentInstance {\n role: string;\n agentId: string;\n provider: string;\n status: \"spawning\" | \"running\" | \"stopping\" | \"stopped\" | \"failed\";\n index?: number;\n}\n\nexport interface ComponentToken {\n sub: string;\n role: \"provider\" | \"channel\" | \"orchestrator\" | \"admin\" | \"dashboard\" | string;\n scope: string[];\n constraints?: TokenConstraints;\n iat: number;\n exp?: number;\n jti?: string;\n}\n\nexport interface TokenRecord {\n jti: string;\n sub: string;\n role: string;\n scope: string[];\n constraints?: TokenConstraints;\n profileId?: string;\n issuedAt: number;\n expiresAt?: number;\n revokedAt?: number;\n createdBy?: string;\n}\n\nexport interface TokenProfile {\n id: string;\n name: string;\n description?: string;\n role: string;\n scope: string[];\n constraints?: TokenConstraints;\n ttlSeconds?: number;\n builtIn: boolean;\n createdAt: number;\n updatedAt: number;\n createdBy?: string;\n}\n\nexport interface CreateTokenProfileInput {\n id?: string;\n name: string;\n description?: string;\n role: string;\n scope: string[];\n constraints?: TokenConstraints;\n ttlSeconds?: number;\n createdBy?: string;\n}\n\nexport type UpdateTokenProfileInput = Partial<Omit<CreateTokenProfileInput, \"id\">>;\n\nexport interface TokenConstraints {\n agents?: string[];\n policies?: string[];\n parentAgents?: string[];\n /** Parent agent id stamped on a child's runner token at spawn; read at registration to\n * set the child agent card's `spawnedBy` authoritatively (the child can't forge it). */\n spawnedBy?: string;\n /** Spawn quota (max concurrent live children) baked into a spawn-capable agent's token,\n * resolved from its profile's `maxSpawnedAgents`. The runtime quota check reads it here. */\n maxSpawnedAgents?: number;\n targets?: string[];\n channels?: string[];\n orchestrators?: string[];\n hosts?: string[];\n cwd?: string;\n cwdPrefixes?: string[];\n taskIds?: string[];\n memoryScopes?: string[];\n integrationNames?: string[];\n spawnRequestIds?: string[];\n terminalAttach?: boolean;\n logsRead?: boolean;\n canDelegate?: boolean;\n}\n\ntype TokenScope =\n | \"system:admin\"\n | \"token:read\"\n | \"token:write\"\n | \"agent:read\"\n | \"agent:write\"\n | \"message:read\"\n | \"message:send\"\n | \"task:read\"\n | \"task:write\"\n | \"command:read\"\n | \"command:write\"\n | \"command:*\"\n | \"artifact:read\"\n | \"artifact:write\"\n | \"artifact:admin\"\n | \"memory:read\"\n | \"memory:write\"\n | \"memory:admin\"\n | \"mcp:use\"\n | \"terminal:attach\"\n | \"logs:read\"\n | \"integration:read\"\n | \"integration:write\"\n | \"channel:read\"\n | \"channel:write\"\n | \"stats:read\"\n | \"health:read\"\n | \"events:read\"\n | \"command:spawn\"\n | \"command:shutdown\"\n | \"recipe:start\"\n | \"recipe:stop\"\n | \"admin:*\";\n\nexport interface MaintenanceJob {\n id: string;\n title: string;\n description?: string;\n intervalMs: number;\n timeoutMs: number;\n enabled: boolean;\n runOnStart: boolean;\n lastRunAt?: number;\n nextRunAt?: number;\n lastDurationMs?: number;\n lastStatus: \"idle\" | \"running\" | \"succeeded\" | \"failed\" | \"disabled\";\n lastError?: string;\n lastResult?: Record<string, unknown>;\n consecutiveFailures: number;\n running: boolean;\n leaseUntil?: number;\n updatedAt: number;\n}\n\nexport interface MaintenanceJobRun {\n id: string;\n status: \"succeeded\" | \"failed\" | \"skipped\";\n startedAt: number;\n finishedAt: number;\n durationMs: number;\n result?: Record<string, unknown>;\n error?: string;\n}\n\nexport interface HealthCheck {\n name: string;\n status: \"ok\" | \"warn\" | \"error\";\n detail?: string;\n count?: number;\n subjects?: Array<{ id: string; label?: string; status?: string; detail?: string }>;\n}\n\nexport interface HealthReport {\n status: \"ok\" | \"degraded\" | \"error\";\n version: string;\n generatedAt: number;\n checks: HealthCheck[];\n}\n\n// --- Shared micro-helpers (single source of truth; do not re-declare per file) ---\n\n/** True for a non-null, non-array object. The canonical type guard for the whole repo. */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Narrow `unknown` to a non-empty trimmed string, else `undefined`.\n * Settled semantics: whitespace-only is treated as empty (returns `undefined`).\n */\nexport function stringValue(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\n/** Extract a human-readable message from any thrown value. */\nexport function errMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n// --- Relay connection defaults ---\n\n/** Default port the relay server listens on. */\nconst DEFAULT_RELAY_PORT = 4850;\n\n/** Default relay base URL. Loopback spelling settled on `127.0.0.1` (not `localhost`). */\nexport const DEFAULT_RELAY_URL = `http://127.0.0.1:${DEFAULT_RELAY_PORT}`;\n","import type {\n Agent, Message, PairSession, Task, ChannelSummary, ConnectorSummary,\n IntegrationSummary, Orchestrator, AgentType, PresenceInfo, PresenceBadge,\n AttentionInfo, InboxThread, HealthCheck, HealthDiagnostic, ManagedPolicyHealth,\n} from '@/types'\nimport { HUMAN_AGENT_ID, AGENT_TYPE_ICONS, AGENT_TYPE_TITLES, WAITING_TASK_STATUSES } from './constants'\nimport { isRecord } from 'agent-relay-sdk/types'\n\nexport function toTimestamp(value: unknown): number {\n const ts = typeof value === 'number' ? value : new Date((value as string) || 0).getTime()\n return Number.isFinite(ts) ? ts : 0\n}\n\nconst INTERNAL_CAP_PREFIXES = ['lifecycle.', 'commands.', 'capabilities.']\n\nconst REACTION_EMOJI_ALIASES: Record<string, string> = {\n '+1': '👍',\n ':+1:': '👍',\n thumbsup: '👍',\n ':thumbsup:': '👍',\n 'thumbs-up': '👍',\n ':thumbs-up:': '👍',\n thumbs_up: '👍',\n ':thumbs_up:': '👍',\n thumb_up: '👍',\n ':thumb_up:': '👍',\n like: '👍',\n ':like:': '👍',\n heart: '❤️',\n ':heart:': '❤️',\n redheart: '❤️',\n red_heart: '❤️',\n ':red_heart:': '❤️',\n check: '✅',\n ':check:': '✅',\n checkmark: '✅',\n ':checkmark:': '✅',\n white_check_mark: '✅',\n ':white_check_mark:': '✅',\n eyes: '👀',\n ':eyes:': '👀',\n}\n\nexport function normalizeReactionEmoji(value: string): string {\n const trimmed = value.trim()\n return REACTION_EMOJI_ALIASES[trimmed.toLowerCase()] ?? trimmed\n}\n\nexport function userFacingCapabilities(caps: string[]): string[] {\n return caps.filter((c) => !INTERNAL_CAP_PREFIXES.some((p) => c.startsWith(p)))\n}\n\nexport function shortPath(cwd: string | undefined | null, segments = 2): string {\n if (!cwd) return ''\n const parts = cwd.replace(/\\/+$/, '').split('/')\n return parts.length <= segments ? cwd : parts.slice(-segments).join('/')\n}\n\nexport function normalizePathForCompare(path: string): string {\n return path.trim().replace(/\\\\/g, '/').replace(/\\/+$/, '') || '/'\n}\n\nexport function pathWithinBase(path: string | undefined | null, baseDir: string | undefined | null): boolean {\n if (!path?.trim() || !baseDir?.trim()) return false\n const normalizedPath = normalizePathForCompare(path)\n const normalizedBase = normalizePathForCompare(baseDir)\n if (normalizedBase === '/') return normalizedPath.startsWith('/')\n if (normalizedPath === '/') return false\n return normalizedPath === normalizedBase || normalizedPath.startsWith(normalizedBase + '/')\n}\n\nfunction isBuiltInAgent(agent: Agent | null | undefined): boolean {\n return agent?.meta?.builtin === true || agent?.id === HUMAN_AGENT_ID || agent?.id === 'system'\n}\n\nfunction isChannelAgent(agent: Agent | null | undefined): boolean {\n const tags = agent?.tags || []\n const caps = agent?.capabilities || []\n const policyName = typeof agent?.meta?.policyName === 'string' ? agent.meta.policyName : ''\n return agent?.kind === 'channel' ||\n agent?.meta?.kind === 'channel' ||\n tags.includes('channel') ||\n tags.some((tag) => tag.startsWith('channel:')) ||\n caps.includes('channel') ||\n caps.some((cap) => cap.startsWith('channel:')) ||\n tags.includes('telegram') ||\n caps.includes('telegram') ||\n policyName === 'telegram-main'\n}\n\nfunction isAutomationAgent(agent: Agent | null | undefined): boolean {\n return Boolean(\n typeof agent?.meta?.automationId === 'string' ||\n typeof agent?.meta?.automationRunId === 'string' ||\n agent?.tags?.includes('automation')\n )\n}\n\nfunction isSystemAgent(agent: Agent | null | undefined): boolean {\n return isBuiltInAgent(agent) || isChannelAgent(agent) || isAutomationAgent(agent)\n}\n\nexport function agentType(agent: Agent | null | undefined): AgentType {\n if (agent?.id === HUMAN_AGENT_ID) return 'user'\n if (agent?.id === 'system') return 'system'\n if (isChannelAgent(agent)) return 'channel'\n\n const values = [\n ...(agent?.tags || []),\n agent?.meta?.provider as string,\n agent?.meta?.client as string,\n agent?.meta?.runtime as string,\n agent?.meta?.agentType as string,\n agent?.id,\n agent?.name,\n ]\n .filter((v): v is string => typeof v === 'string')\n .map((v) => v.toLowerCase())\n\n if (values.some((v) => v.includes('codex'))) return 'codex'\n if (values.some((v) => v.includes('claude'))) return 'claude'\n return 'agent'\n}\n\nfunction agentTypeIcon(agent: Agent | null | undefined): string {\n return AGENT_TYPE_ICONS[agentType(agent)] ?? AGENT_TYPE_ICONS.agent ?? 'ti-robot'\n}\n\nfunction agentTypeTitle(agent: Agent | null | undefined): string {\n return AGENT_TYPE_TITLES[agentType(agent)] ?? AGENT_TYPE_TITLES.agent ?? 'Agent'\n}\n\nexport function isAgentStale(now: number, agent: Agent | null | undefined): boolean {\n if (!agent?.lastSeen || agent.status === 'offline') return false\n if (agent.id === 'user' || agent.id === 'system') return false\n // A live bus connection is authoritative liveness: an idle-but-connected agent\n // is reachable, not stale, even if lastSeen has aged (e.g. heartbeat keepalive\n // gaps while idle). Genuine death flips transport.connected to false (or the\n // server reaps the agent offline), at which point the lastSeen check applies.\n const transport = agent.meta?.transport\n if (isRecord(transport) && transport.connected === true) return false\n const lastSeenMs = new Date(agent.lastSeen as string).getTime()\n if (!Number.isFinite(lastSeenMs)) return false\n return now - lastSeenMs > 60_000\n}\n\nexport type AgentControlAction = 'restart' | 'shutdown' | 'reconnect' | 'compact' | 'clearContext' | 'resume' | 'interrupt'\n\nexport function agentSupportsControlAction(agent: Agent | null | undefined, action: AgentControlAction): boolean {\n if (!agent || isBuiltInAgent(agent) || isChannelAgent(agent)) return false\n if (action === 'resume') return false\n // Interrupt is provider-independent (Claude ESC, Codex turn-interrupt) and only\n // meaningful while the agent is actually mid-turn.\n if (action === 'interrupt') return agent.providerCapabilities?.liveSession?.interrupt === true && agent.status === 'busy'\n const needsClaudeTmux = agentType(agent) === 'claude' && (action === 'restart' || action === 'shutdown' || action === 'compact' || action === 'clearContext')\n if (needsClaudeTmux && !agentHasTmuxSession(agent)) return false\n const lifecycle = agent.providerCapabilities?.lifecycle\n if (lifecycle) {\n if (action === 'restart') return lifecycle.restartHard === true\n if (action === 'shutdown') return lifecycle.shutdownHard === true\n if (action === 'reconnect') return lifecycle.reconnect === true\n }\n if (action === 'compact') return agent.providerCapabilities?.context?.compact === true\n if (action === 'clearContext') return agent.providerCapabilities?.context?.clear === true\n return agent.meta?.runnerManaged === true && (action === 'restart' || action === 'shutdown')\n}\n\nfunction agentHasTmuxSession(agent: Agent): boolean {\n return typeof agent.meta?.tmuxSession === 'string' && agent.meta.tmuxSession.trim().length > 0\n}\n\nfunction agentSupportsControlActions(agent: Agent | null | undefined): boolean {\n return agentSupportsControlAction(agent, 'restart') ||\n agentSupportsControlAction(agent, 'shutdown') ||\n agentSupportsControlAction(agent, 'reconnect') ||\n agentSupportsControlAction(agent, 'compact') ||\n agentSupportsControlAction(agent, 'clearContext')\n}\n\nexport function agentCanBeForgotten(agent: Agent | null | undefined): boolean {\n return Boolean(agent && !isBuiltInAgent(agent) && !isChannelAgent(agent) && !agentSupportsControlActions(agent) && (agent.status === 'offline' || agent.status === 'stale'))\n}\n\nfunction isRetiredManagedRuntimeAgent(agent: Agent, managedPolicies: ManagedPolicyHealth[]): boolean {\n if (agent.status !== 'offline') return false\n const policyName = typeof agent.meta?.policyName === 'string' ? agent.meta.policyName : ''\n if (!policyName) return false\n const policy = managedPolicies.find((item) => item.policy.name === policyName)\n return Boolean(policy?.state.agentId && policy.state.agentId !== agent.id)\n}\n\nexport function visibleAgents(agents: Agent[], showBuiltIns: boolean, managedPolicies: ManagedPolicyHealth[] = []): Agent[] {\n const activeRuntimeAgents = agents.filter((a) => !isRetiredManagedRuntimeAgent(a, managedPolicies))\n return showBuiltIns ? activeRuntimeAgents : activeRuntimeAgents.filter((a) => !isSystemAgent(a))\n}\n\nexport function displayName(agent: Agent | null | undefined): string {\n if (!agent) return '?'\n return agent.label || agent.name || agent.id.slice(-12)\n}\n\nexport function displayTarget(target: string, agentsById: Record<string, Agent>): string {\n if (!target) return '?'\n if (target === 'broadcast') return 'broadcast'\n if (target.startsWith('tag:')) return '#' + target.slice(4)\n if (target.startsWith('cap:')) return target.slice(4)\n if (target.startsWith('label:')) return target.slice(6)\n const agent = agentsById[target]\n return agent ? displayName(agent) : target.slice(-8)\n}\n\nexport function messageBody(msg: Message | null | undefined): string {\n if (!msg) return ''\n const payload = msg.payload || {}\n const channelMessage = payload.message as Record<string, unknown> | undefined\n if (channelMessage && typeof channelMessage === 'object' && typeof channelMessage.text === 'string' && (channelMessage.text as string).trim()) {\n return channelMessage.text as string\n }\n const interaction = payload.interaction as Record<string, unknown> | undefined\n if (interaction && typeof interaction === 'object') {\n const title = typeof interaction.title === 'string' ? interaction.title.trim() : ''\n const description = typeof interaction.description === 'string' ? interaction.description.trim() : ''\n if (title && description) return title + '\\n' + description\n if (title) return title\n if (description) return description\n }\n const reaction = payload.reaction as Record<string, unknown> | undefined\n if (reaction && typeof reaction === 'object') {\n const name = typeof reaction.name === 'string' ? reaction.name : ''\n const emoji = typeof reaction.emoji === 'string' ? reaction.emoji : ''\n const value = typeof reaction.value === 'string' ? reaction.value : ''\n const displayEmoji = normalizeReactionEmoji(emoji || name || value)\n return ['Reaction', displayEmoji].filter(Boolean).join(': ')\n }\n const activity = payload.activity as Record<string, unknown> | undefined\n if (activity && typeof activity === 'object') {\n const kind = typeof activity.kind === 'string' ? activity.kind : 'activity'\n const state = typeof activity.state === 'string' ? activity.state : ''\n return [kind, state].filter(Boolean).join(' ')\n }\n if (typeof payload.text === 'string' && (payload.text as string).trim()) return payload.text as string\n if (typeof payload.message === 'string' && (payload.message as string).trim()) return payload.message as string\n return extractTextFromJson(msg.body || '') ?? (msg.body || '')\n}\n\nexport function isReactionEventMessage(msg: Message | null | undefined): boolean {\n if (!msg?.payload) return false\n const event = msg.payload.event as Record<string, unknown> | undefined\n return msg.payload.reactionNotification === true || event?.type === 'message.reaction'\n}\n\ninterface ParsedJsonBody {\n text: string\n meta: Array<[string, string]>\n raw: Record<string, unknown>\n}\n\nexport function parseJsonBody(body: string): ParsedJsonBody | null {\n if (!body) return null\n const trimmed = body.trim()\n if (!trimmed.startsWith('{')) return null\n try {\n const obj = JSON.parse(trimmed)\n if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) return null\n const text = typeof obj.text === 'string' ? obj.text\n : typeof obj.message === 'string' ? obj.message\n : null\n if (!text?.trim()) return null\n const skipKeys = new Set(['text', 'message'])\n const meta = Object.entries(obj)\n .filter(([k, v]) => !skipKeys.has(k) && (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean'))\n .map(([k, v]) => [k, String(v)] as [string, string])\n return { text, meta, raw: obj }\n } catch {\n return null\n }\n}\n\nfunction extractTextFromJson(body: string): string | null {\n const parsed = parseJsonBody(body)\n return parsed?.text ?? null\n}\n\nexport function messagePreview(msg: Message | null | undefined): string {\n const text = msg?.subject || messageBody(msg) || ''\n return text.length > 90 ? text.slice(0, 90) + '...' : text\n}\n\nexport function inboxPeer(msg: Message): string {\n if (msg.from === HUMAN_AGENT_ID && msg.to) return msg.to\n if (msg.to === HUMAN_AGENT_ID && msg.from) return msg.from\n return ''\n}\n\nexport function isHumanInboundMessage(msg: Message): boolean {\n return msg.to === HUMAN_AGENT_ID && msg.from !== HUMAN_AGENT_ID\n}\n\n// Session \"reasoning\" and \"tool\" events stream into the chat timeline as\n// display-only activity steps — they are not user-facing messages and must not\n// inflate unread badges. Mirrors sessionActivityStep() in views/chat.tsx.\nexport function isSessionActivityStep(msg: Message): boolean {\n if (msg.kind !== 'session') return false\n const type = (msg.payload?.session as { type?: string } | undefined)?.type\n return type === 'reasoning' || type === 'tool'\n}\n\nfunction isUnreadHumanMessage(readCursors: Record<string, number>, peer: string, msg: Message): boolean {\n if (!isHumanInboundMessage(msg)) return false\n if ((msg.readBy || []).includes(HUMAN_AGENT_ID)) return false\n const cursor = Number(readCursors[peer] || 0)\n return msg.id > (Number.isFinite(cursor) ? cursor : 0)\n}\n\nfunction maxMessageId(messages: Message[], predicate: (msg: Message) => boolean): number {\n let max = 0\n for (const msg of messages) {\n if (predicate(msg) && msg.id > max) max = msg.id\n }\n return max\n}\n\nexport function isClaimableTaskWaiting(task: Task): boolean {\n return WAITING_TASK_STATUSES.has(task.status) && !task.claimedBy\n}\n\nexport function isClaimableMessageWaiting(msg: Message): boolean {\n return Boolean(msg.claimable && !msg.claimedBy && !(msg.kind === 'task' && Number.isSafeInteger((msg.payload as Record<string, unknown>)?.taskId)))\n}\n\nexport function dashboardTargetMatchesAgent(target: string, agent: Agent): boolean {\n if (!target || !agent) return false\n if (target === 'broadcast' || target === agent.id) return true\n if (target.startsWith('tag:')) return (agent.tags || []).includes(target.slice(4))\n if (target.startsWith('cap:')) return (agent.capabilities || []).includes(target.slice(4))\n if (target.startsWith('label:')) return agent.label === target.slice(6)\n return false\n}\n\nexport function messageMatchesChannel(message: Message, channel: ChannelSummary): boolean {\n if (!message || !channel) return false\n const channelKeys = [channel.id, channel.type, ...(channel.topicChannels || [])].filter(Boolean)\n if (message.channel && channelKeys.includes(message.channel)) return true\n const payloadChannel = message.payload?.channel as Record<string, unknown> | undefined\n if (payloadChannel && typeof payloadChannel === 'object') {\n const keys = [payloadChannel.agentId, payloadChannel.provider, payloadChannel.accountId].filter(Boolean) as string[]\n if (keys.includes(channel.id) || keys.includes(channel.type) || (channel.accountId && keys.includes(channel.accountId))) return true\n }\n if (channel.agentId && (message.from === channel.agentId || message.to === channel.agentId)) return true\n return false\n}\n\ninterface ActiveSubagentInfo {\n id: string\n label: string\n role?: string\n startedAt?: number\n}\n\ninterface ProviderRuntimeState {\n state: string\n reason?: string\n label: string\n recommendedAction?: string\n source?: string\n raw?: unknown\n updatedAt?: number\n pendingApproval?: ProviderPendingApproval\n}\n\ninterface ProviderPendingApprovalChoice {\n id: 'approve' | 'approve-session' | 'deny' | 'abort'\n label: string\n}\n\ninterface ProviderQuestionOption {\n label: string\n description?: string\n}\n\nexport interface ProviderQuestion {\n question: string\n header?: string\n options: ProviderQuestionOption[]\n multiSelect?: boolean\n}\n\nexport interface ProviderPendingApproval {\n id: string\n provider?: string\n kind?: string\n title: string\n body: string\n choices: ProviderPendingApprovalChoice[]\n // Present when kind === 'questions' (Claude AskUserQuestion). The dashboard\n // renders these as a form and submits an 'answer' decision.\n questions?: ProviderQuestion[]\n}\n\nexport function providerRuntimeState(agent: Agent | null | undefined): ProviderRuntimeState | null {\n const raw = agent?.meta?.providerState\n if (!isRecord(raw) || typeof raw.state !== 'string') return null\n return {\n state: raw.state,\n reason: typeof raw.reason === 'string' ? raw.reason : undefined,\n label: typeof raw.label === 'string' && raw.label ? raw.label : raw.state,\n recommendedAction: typeof raw.recommendedAction === 'string' ? raw.recommendedAction : undefined,\n source: typeof raw.source === 'string' ? raw.source : undefined,\n raw: raw.raw,\n updatedAt: typeof raw.updatedAt === 'number' ? raw.updatedAt : undefined,\n pendingApproval: providerPendingApproval(raw.pendingApproval),\n }\n}\n\nfunction providerPendingApproval(value: unknown): ProviderPendingApproval | undefined {\n if (!isRecord(value) || typeof value.id !== 'string') return undefined\n const choices = Array.isArray(value.choices)\n ? value.choices.filter(isRecord).map((choice) => ({\n id: choice.id,\n label: typeof choice.label === 'string' ? choice.label : String(choice.id || ''),\n })).filter((choice): choice is ProviderPendingApprovalChoice =>\n (choice.id === 'approve' || choice.id === 'approve-session' || choice.id === 'deny' || choice.id === 'abort') && Boolean(choice.label)\n )\n : []\n const questions = Array.isArray(value.questions)\n ? value.questions.filter(isRecord).map((q): ProviderQuestion => ({\n question: typeof q.question === 'string' ? q.question : '',\n header: typeof q.header === 'string' ? q.header : undefined,\n multiSelect: q.multiSelect === true,\n options: Array.isArray(q.options)\n ? q.options.filter(isRecord).map((o) => ({\n label: typeof o.label === 'string' ? o.label : String(o.label ?? ''),\n description: typeof o.description === 'string' ? o.description : undefined,\n })).filter((o) => Boolean(o.label))\n : [],\n })).filter((q) => Boolean(q.question) && q.options.length > 0)\n : undefined\n return {\n id: value.id,\n provider: typeof value.provider === 'string' ? value.provider : undefined,\n kind: typeof value.kind === 'string' ? value.kind : undefined,\n title: typeof value.title === 'string' && value.title ? value.title : 'Permission request',\n body: typeof value.body === 'string' ? value.body : '',\n choices,\n ...(questions && questions.length ? { questions } : {}),\n }\n}\n\nexport function providerBlockedState(agent: Agent | null | undefined): ProviderRuntimeState | null {\n const state = providerRuntimeState(agent)\n return state?.state === 'blocked' ? state : null\n}\n\nexport function activeSubagents(agent: Agent | null | undefined): ActiveSubagentInfo[] {\n const raw = agent?.meta?.activeSubagents\n if (!Array.isArray(raw)) {\n const count = typeof agent?.meta?.activeSubagentCount === 'number' ? agent.meta.activeSubagentCount : 0\n return count > 0 ? Array.from({ length: count }, (_, index) => ({ id: `subagent-${index + 1}`, label: `Subagent ${index + 1}` })) : []\n }\n return raw\n .filter(isRecord)\n .map((item, index) => {\n const id = typeof item.id === 'string' && item.id ? item.id : `subagent-${index + 1}`\n const role = typeof item.role === 'string' && item.role ? item.role : undefined\n const label = typeof item.label === 'string' && item.label ? item.label : role || id\n const startedAt = typeof item.startedAt === 'number' ? item.startedAt : undefined\n return { id, label, role, startedAt }\n })\n}\n\nexport function agentPresence(\n now: number, agent: Agent | null | undefined, attention: AttentionInfo, pair: PairSession | null\n): PresenceInfo {\n const lifecycleAction = typeof agent?.meta?.lifecycleAction === 'string' ? agent.meta.lifecycleAction : ''\n const stale = isAgentStale(now, agent)\n const reconnecting = agent?.status !== 'offline' && !agent?.ready && stale\n const starting = agent?.status !== 'offline' && !agent?.ready && !stale\n const unreadIdle = attention.unread > 0 && agent?.status !== 'busy'\n const blocked = providerBlockedState(agent)\n const badges = presenceBadges(agent, attention, pair)\n\n if (agent?.status === 'offline') return { label: 'offline', tone: 'secondary', icon: 'PlugZap', stale: false, reconnecting: false, badges }\n // Pre-session-destroy window (#183): the runner is running end-of-session work before a\n // compact/clear/restart/shutdown. Distinct from plain \"busy\" and non-addressable.\n if (lifecycleAction.startsWith('finalizing-')) {\n return { label: `wrapping up (${lifecycleAction.slice('finalizing-'.length)})`, tone: 'warning', icon: 'Hourglass', stale, reconnecting, badges }\n }\n if (lifecycleAction === 'shutting-down') return { label: 'shutting down', tone: 'warning', icon: 'Power', stale, reconnecting, badges }\n if (lifecycleAction === 'killing') return { label: 'killing', tone: 'danger', icon: 'Power', stale, reconnecting, badges }\n if (lifecycleAction === 'restarting') return { label: 'restarting', tone: 'warning', icon: 'RefreshCw', stale, reconnecting, badges }\n if (agent?.status === 'stale') return { label: 'stale', tone: 'danger', icon: 'RefreshCw', stale: true, reconnecting: true, badges }\n if (reconnecting) return { label: 'reconnecting', tone: 'danger', icon: 'RefreshCw', stale, reconnecting, badges }\n if (starting) return { label: 'online, not ready', tone: 'warning', icon: 'Loader', stale, reconnecting, badges }\n if (blocked) return { label: `blocked: ${blocked.label}`, tone: 'danger', icon: 'AlertCircle', stale, reconnecting, badges }\n if (agent?.status === 'busy') return { label: 'busy in turn', tone: 'warning', icon: 'Play', stale, reconnecting, badges }\n if (pair?.status === 'active') return { label: 'paired', tone: 'success', icon: 'Link', stale, reconnecting, badges }\n if (unreadIdle) return { label: 'idle, unread', tone: 'danger', icon: 'Bell', stale, reconnecting, badges }\n return { label: agent?.status === 'idle' ? 'idle' : 'ready', tone: 'success', icon: 'CircleCheck', stale, reconnecting, badges }\n}\n\nfunction presenceBadges(agent: Agent | null | undefined, attention: AttentionInfo, pair: PairSession | null): PresenceBadge[] {\n const badges: PresenceBadge[] = []\n const blocked = providerBlockedState(agent)\n const subagentCount = activeSubagents(agent).length\n if (blocked) badges.push({ label: blocked.reason ? `blocked: ${blocked.reason}` : 'blocked', className: 'bg-red-500/20 text-red-400' })\n if (subagentCount) badges.push({ label: subagentCount === 1 ? '1 subagent' : subagentCount + ' subagents', className: 'bg-cyan-500/20 text-cyan-300' })\n if (pair?.status === 'active') badges.push({ label: 'paired', className: 'bg-emerald-500/20 text-emerald-400' })\n if (pair?.status === 'pending') badges.push({ label: 'pair invite', className: 'bg-yellow-500/20 text-yellow-400' })\n if (attention.unread) badges.push({ label: attention.unread + ' unread', className: 'bg-red-500/20 text-red-400' })\n if (attention.claimableTasks) badges.push({ label: attention.claimableTasks + ' claimable', className: 'bg-orange-500/20 text-orange-400' })\n return badges\n}\n\nexport function emptyAttention(): AttentionInfo {\n return { unread: 0, needsHumanResponse: false, pendingPairInvite: false, claimableTasks: 0, total: 0, score: 0 }\n}\n\nexport function channelIsReady(channel: ChannelSummary | null): boolean {\n if (!channel || channel.status === 'offline') return false\n if (channel.targetHealth?.status === 'ok') return true\n if (channel.targetHealth?.status === 'error' || channel.targetHealth?.status === 'warning') return false\n return channel.ready\n}\n\nexport function channelPresence(channel: ChannelSummary | null): PresenceInfo {\n if (!channel) return { label: 'unknown', tone: 'secondary', icon: 'PlugZap', badges: [] }\n if (channel.targetHealth?.status === 'error') return { label: 'target broken', tone: 'danger', icon: 'AlertTriangle', badges: [] }\n if (channel.targetHealth?.status === 'warning') return { label: 'target warning', tone: 'warning', icon: 'AlertCircle', badges: [] }\n if (channel.status === 'offline') return { label: 'offline', tone: 'secondary', icon: 'PlugZap', badges: [] }\n if (channel.status === 'busy') return { label: 'busy', tone: 'warning', icon: 'Activity', badges: [] }\n if (!channelIsReady(channel)) return { label: 'not ready', tone: 'warning', icon: 'Loader', badges: [] }\n return { label: 'ready', tone: 'success', icon: 'CircleCheck', badges: [] }\n}\n\nexport function connectorPresence(connector: ConnectorSummary): PresenceInfo {\n const runtime = connector?.runtime || {}\n if (runtime.status === 'error') return { label: 'error', tone: 'danger', icon: 'AlertTriangle', badges: [] }\n if (runtime.status === 'warn') return { label: 'warning', tone: 'warning', icon: 'AlertCircle', badges: [] }\n if (runtime.running) return { label: 'running', tone: 'success', icon: 'CircleCheck', badges: [] }\n if (runtime.enabled === false) return { label: 'disabled', tone: 'secondary', icon: 'Pause', badges: [] }\n if (runtime.status === 'ok') return { label: 'ok', tone: 'success', icon: 'CircleCheck', badges: [] }\n return { label: 'unknown', tone: 'secondary', icon: 'HelpCircle', badges: [] }\n}\n\nexport function integrationPresence(integration: IntegrationSummary): PresenceInfo {\n const stats = integration?.taskStats || {}\n if ((stats.waitingTasks || 0) > 0) return { label: 'waiting', tone: 'warning', icon: 'AlertCircle', badges: [] }\n if ((stats.openTasks || 0) > 0) return { label: 'active', tone: 'info', icon: 'Activity', badges: [] }\n if (!integration?.configured) return { label: 'observed only', tone: 'secondary', icon: 'Eye', badges: [] }\n if (integration.observed) return { label: 'quiet', tone: 'success', icon: 'CircleCheck', badges: [] }\n return { label: 'configured', tone: 'primary', icon: 'PlugZap', badges: [] }\n}\n\nexport function orchestratorHealthLabel(orch: Orchestrator): string {\n const health = orch?.health\n if (!health || health.status === 'ok') return orch?.version || orch?.package?.version ? 'compatible' : 'unknown version'\n if (health.status === 'upgrade-required') return 'upgrade required'\n if (health.status === 'restart-required') return 'restart required'\n if (health.status === 'unknown') return 'unknown contract'\n if ((health.issues || []).some((i) => i.code === 'protocol-mismatch')) return 'protocol mismatch'\n if ((health.issues || []).some((i) => i.code === 'package-drift')) return 'package drift'\n if (health.restartRequired) return 'restart required'\n return health.status\n}\n\nexport function orchestratorHealthClass(orch: Orchestrator): string {\n const status = orch?.health?.status\n if (status === 'upgrade-required') return 'text-red-400 bg-red-500/10'\n if (status === 'warn' || status === 'restart-required') return 'text-yellow-400 bg-yellow-500/10'\n if (status === 'unknown') return 'text-zinc-400 bg-zinc-500/10'\n return 'text-emerald-400 bg-emerald-500/10'\n}\n\nexport function timeAgo(now: number, iso: string | number | null | undefined): string {\n if (!iso) return ''\n const ts = new Date(iso as string).getTime()\n if (!Number.isFinite(ts)) return ''\n const diff = Math.max(0, (now - ts) / 1000)\n if (diff < 60) return Math.floor(diff) + 's ago'\n if (diff < 3600) return Math.floor(diff / 60) + 'm ago'\n if (diff < 86400) return Math.floor(diff / 3600) + 'h ago'\n return Math.floor(diff / 86400) + 'd ago'\n}\n\nexport function fmtTime(iso: string | number | null | undefined): string {\n if (!iso) return ''\n return new Date(iso as string).toLocaleString()\n}\n\nexport function safeFilename(value: string): string {\n return String(value || 'export').replace(/[^a-z0-9._-]+/gi, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'export'\n}\n\nexport function healthDiagnostics(checks: HealthCheck[]): HealthDiagnostic[] {\n return checks.filter((c) => c.status !== 'ok').map((check) => {\n const base: HealthDiagnostic = {\n name: check.name,\n status: check.status,\n detail: check.detail || check.name,\n impact: healthImpact(check),\n subjects: check.subjects || [],\n actions: [\n { label: 'Inspect logs', icon: 'FileSearch', copy: 'agent-relay daemon logs' },\n { label: 'Restart daemon', icon: 'RefreshCw', copy: 'agent-relay daemon restart' },\n { label: 'Copy env', icon: 'Copy', copy: 'agent-relay doctor' },\n ],\n }\n if (check.name === 'stale-live-agents') {\n base.actions.unshift(\n { label: 'Run reaper', icon: 'Eraser', api: 'POST', path: '/system/reap' },\n { label: 'Show stale', icon: 'Filter', view: 'agents', preset: 'offline_stale' },\n )\n } else if (check.name === 'channel-delivery-targets') {\n base.actions.unshift(\n { label: 'Open channels', icon: 'MessagesSquare', view: 'channels' },\n { label: 'Run reaper', icon: 'Eraser', api: 'POST', path: '/system/reap' },\n )\n } else if (['expired-message-claims', 'expired-task-claims', 'offline-claimed-tasks'].includes(check.name)) {\n base.actions.unshift(\n { label: 'Run reaper', icon: 'Eraser', api: 'POST', path: '/system/reap' },\n { label: 'Open work', icon: 'ListChecks', view: 'work' },\n )\n }\n return base\n })\n}\n\nfunction healthImpact(check: HealthCheck): string {\n const impacts: Record<string, string> = {\n 'database': 'Relay persistence is unavailable; messages, state, and audit writes may fail.',\n 'stale-live-agents': 'Agents may look online even though their heartbeat has stopped.',\n 'expired-message-claims': 'Claimable messages may be stuck until the reaper releases expired claims.',\n 'expired-task-claims': 'Tasks can appear owned by agents that no longer hold a live lease.',\n 'offline-claimed-tasks': 'Offline agents are still shown as owners for active work.',\n 'channel-delivery-targets': 'Inbound channel messages may be accepted but routed to no live delivery agent.',\n }\n return impacts[check.name] || 'Relay health is degraded for this check.'\n}\n\nfunction toneToBadgeVariant(tone: string): 'default' | 'secondary' | 'destructive' | 'outline' {\n if (tone === 'danger' || tone === 'destructive') return 'destructive'\n if (tone === 'secondary') return 'secondary'\n return 'default'\n}\n\nexport function toneToColor(tone: string): string {\n const map: Record<string, string> = {\n success: 'text-emerald-400',\n warning: 'text-yellow-400',\n danger: 'text-red-400',\n info: 'text-blue-400',\n primary: 'text-blue-400',\n secondary: 'text-zinc-400',\n }\n return map[tone] || 'text-zinc-400'\n}\n\nfunction toneToBg(tone: string): string {\n const map: Record<string, string> = {\n success: 'bg-emerald-500',\n warning: 'bg-yellow-500',\n danger: 'bg-red-500',\n info: 'bg-blue-500',\n primary: 'bg-blue-500',\n secondary: 'bg-zinc-500',\n }\n return map[tone] || 'bg-zinc-500'\n}\n\nexport function statusDotColor(agent: Agent | null, now?: number): string {\n if (!agent) return 'bg-zinc-500 opacity-50'\n if (agent.status === 'offline') return 'bg-zinc-500 opacity-50'\n if (typeof agent.meta?.lifecycleAction === 'string' && agent.meta.lifecycleAction.startsWith('finalizing-')) return 'bg-amber-500 animate-pulse'\n if (agent.meta?.lifecycleAction === 'shutting-down' || agent.meta?.lifecycleAction === 'restarting') return 'bg-yellow-500 animate-pulse'\n if (agent.meta?.lifecycleAction === 'killing') return 'bg-red-500 animate-pulse'\n if (providerBlockedState(agent)) return 'bg-red-500 animate-pulse'\n if (agent.status === 'busy') return 'bg-yellow-500'\n if (now && isAgentStale(now, agent)) return 'bg-red-500 animate-pulse'\n if (!agent.ready) return 'bg-emerald-500 animate-pulse'\n return 'bg-emerald-500 shadow-[0_0_6px_theme(colors.emerald.500)]'\n}\n\nexport function downloadText(filename: string, text: string, type: string) {\n if (typeof document === 'undefined' || typeof URL === 'undefined' || typeof Blob === 'undefined') return\n const url = URL.createObjectURL(new Blob([text], { type }))\n const link = document.createElement('a')\n link.href = url\n link.download = filename\n link.click()\n URL.revokeObjectURL(url)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"display-JI19Vc7L.js","names":[],"sources":["../../dashboard/src/lib/constants.ts","../../sdk/src/types.ts","../../dashboard/src/lib/display.ts"],"sourcesContent":["import type { ComposeState, InboxComposeState, PairMessageState, PairInviteState, ViewName } from '@/types'\n\nconst PREF_PREFIX = 'ar-'\nexport const HUMAN_AGENT_ID = 'user'\nexport const INBOX_OPERATOR_ID = HUMAN_AGENT_ID\nexport const LIVE_REFRESH_MS = 5_000\n// Max live messages retained in memory. Must be >= the largest fetch limit\n// (chat view fetches 300) so SSE appends never trim already-loaded history.\nexport const MESSAGE_BUFFER_CAP = 500\n\nexport const CLOSED_TASK_STATUSES = new Set(['done', 'failed', 'canceled'])\nexport const WAITING_TASK_STATUSES = new Set(['open', 'blocked'])\n\nexport const STATUS_SORT_ORDER: Record<string, number> = { online: 0, idle: 0, busy: 2, offline: 3 }\nexport const CHAT_STATUS_SORT_ORDER: Record<string, number> = { idle: 0, online: 0, busy: 2, offline: 4 }\n\nexport const DEFAULT_COMPOSE: ComposeState = { from: '', to: '', body: '', channel: '', subject: '', claimable: false }\nexport const DEFAULT_INBOX_COMPOSE: InboxComposeState = { toMode: 'agent', to: '', body: '', channel: '', subject: '', claimable: false }\nexport const DEFAULT_PAIR_MESSAGE: PairMessageState = { pairId: '', from: '', body: '', subject: '' }\nexport const DEFAULT_PAIR_INVITE: PairInviteState = { requesterId: '', targetId: '', objective: '' }\n\n\nexport const AGENT_TYPE_ICONS: Record<string, string> = {\n codex: 'Terminal',\n claude: 'Sun',\n user: 'User',\n system: 'Server',\n channel: 'MessagesSquare',\n agent: 'Bot',\n}\n\nexport const AGENT_TYPE_TITLES: Record<string, string> = {\n codex: 'Codex agent',\n claude: 'Claude agent',\n user: 'Human operator',\n system: 'System',\n channel: 'Channel',\n agent: 'Agent',\n}\n\nexport const NAV_ITEMS: Array<{ key: ViewName; label: string; icon: string }> = [\n { key: 'overview', label: 'Overview', icon: 'LayoutDashboard' },\n { key: 'chat', label: 'Chat', icon: 'MessageCircle' },\n { key: 'agents', label: 'Agents', icon: 'Bot' },\n { key: 'managed', label: 'Managed Agents', icon: 'Workflow' },\n { key: 'profiles', label: 'Agent Profiles', icon: 'UserCog' },\n { key: 'orchestrators', label: 'Orchestrators', icon: 'Server' },\n { key: 'workspaces', label: 'Workspaces', icon: 'GitBranch' },\n { key: 'files', label: 'Files', icon: 'FolderTree' },\n { key: 'channels', label: 'Channels', icon: 'MessagesSquare' },\n { key: 'connectors', label: 'Connectors', icon: 'Plug' },\n { key: 'integrations', label: 'Integrations', icon: 'PlugZap' },\n { key: 'security', label: 'Security', icon: 'Shield' },\n { key: 'memory', label: 'Memory', icon: 'BrainCircuit' },\n { key: 'activity', label: 'Activity', icon: 'Activity' },\n { key: 'pairs', label: 'Pairs', icon: 'Link' },\n { key: 'messages', label: 'Messages', icon: 'Mail' },\n { key: 'work', label: 'Work Queue', icon: 'ListChecks' },\n { key: 'tasks', label: 'Tasks', icon: 'ClipboardList' },\n { key: 'automation', label: 'Automation', icon: 'CalendarClock' },\n { key: 'analytics', label: 'Analytics', icon: 'AreaChart' },\n { key: 'insights', label: 'Insights', icon: 'Lightbulb' },\n { key: 'maintenance', label: 'Maintenance', icon: 'Wrench' },\n { key: 'settings', label: 'Settings', icon: 'Settings' },\n]\n\nexport const SEVERITY_COLORS: Record<string, string> = {\n critical: 'text-red-400 bg-red-500/10',\n warning: 'text-yellow-400 bg-yellow-500/10',\n info: 'text-blue-400 bg-blue-500/10',\n}\n\nexport const PAIR_STATUS_COLORS: Record<string, string> = {\n active: 'bg-emerald-500/20 text-emerald-400',\n pending: 'bg-yellow-500/20 text-yellow-400',\n ended: 'bg-zinc-500/20 text-zinc-400',\n rejected: 'bg-red-500/20 text-red-400',\n expired: 'bg-red-500/20 text-red-400',\n}\n","export type AgentKind = \"provider\" | \"channel\" | \"orchestrator\" | \"system\" | \"user\";\n\nexport interface AgentCard {\n id: string;\n name: string;\n kind: AgentKind;\n label?: string; // human-friendly alias; acts as a fan-out target (\"label:foo\")\n tags: string[];\n machine?: string;\n rig?: string;\n capabilities: string[];\n ready: boolean;\n status: AgentStatus;\n instanceId?: string;\n epoch: number;\n providerCapabilities?: ProviderCapabilities;\n context?: ContextState;\n meta?: Record<string, unknown>;\n /** Agent id of the parent that spawned this one (set authoritatively from the child's\n * signed runner token at registration). Absent for top-level/user/system agents. Powers\n * spawn quotas, the `spawnedBy:` search filter, scoped shutdown, and the no-grandchild gate. */\n spawnedBy?: string;\n /** Branch-workspace lifecycle state, server-derived from the agent's owned isolated\n * worktree (#236). Absent for non-branch agents (no live isolated workspace). Lets the\n * dashboard show a \"where is this in the merge-back\" badge without recomputing it. */\n branchState?: BranchState;\n /** Id of the live isolated workspace this agent owns — populated whenever `branchState`\n * is, so the dashboard can deep-link the badge to that workspace row (#236). */\n branchWorkspaceId?: string;\n lastSeen: number;\n createdAt: number;\n}\n\nexport type AgentStatus = \"online\" | \"idle\" | \"busy\" | \"stale\" | \"offline\";\n\n/**\n * At-a-glance branch-workspace state for an isolated-workspace owner (#236). One\n * server-derived projection of `WorkspaceStatus` + claim + git ahead/dirty, so the\n * human can answer \"does this agent have unlanded work, and where is it in the\n * merge-back lifecycle\" from the agent card alone.\n * - `idle` ⚪ active worktree, nothing to land (0 ahead, clean tree)\n * - `changes` 🟡 active worktree with commits/dirty work — human can mark it ready\n * - `ready` 🔵 handed off; the relay auto-merge will land it (~2 min sweep)\n * - `steward` 🟠 under reconciliation (a steward holds it, or conflict/merge in flight)\n * - `blocked` 🔴 escalated/failed — needs human attention\n */\nexport type BranchState = \"idle\" | \"changes\" | \"ready\" | \"steward\" | \"blocked\";\n/** All BranchState values, for exhaustive UI maps and validation. */\nexport const BRANCH_STATE_VALUES = [\"idle\", \"changes\", \"ready\", \"steward\", \"blocked\"] as const satisfies readonly BranchState[];\n\ntype CapabilitySource = \"catalog\" | \"provider\" | \"runtime\" | \"override\" | \"estimate\";\ntype CapabilityConfidence = \"declared\" | \"reported\" | \"verified\" | \"estimated\" | \"unknown\";\n\nexport interface ProviderCapabilities {\n lifecycle: {\n managed: boolean;\n shutdownHard: boolean;\n restartHard: boolean;\n semanticStatus?: boolean;\n reconnect?: boolean;\n hibernate?: boolean;\n resume?: boolean;\n };\n model?: {\n provider?: SpawnProvider | string;\n id?: string;\n alias?: string;\n providerModel?: string;\n effort?: SpawnEffort | string;\n source: CapabilitySource;\n confidence: CapabilityConfidence;\n lastUpdatedAt?: number;\n };\n session?: {\n approvalMode?: SpawnApprovalMode | string;\n fileRead?: boolean;\n fileWrite?: boolean;\n shell?: boolean;\n source: CapabilitySource;\n confidence: CapabilityConfidence;\n lastUpdatedAt?: number;\n };\n context?: {\n stats?: {\n source: \"api\" | \"statusline\" | \"hook\" | \"estimate\";\n confidence: \"exact\" | \"reported\" | \"estimated\";\n };\n compact?: boolean;\n clear?: boolean;\n inject?: boolean;\n fork?: boolean;\n rollback?: boolean;\n archive?: boolean;\n };\n liveSession?: {\n capture?: boolean;\n inject?: boolean;\n /** Provider can be interrupted mid-turn from the dashboard (ESC / turn-interrupt). */\n interrupt?: boolean;\n /** Prompts typed directly into the provider (web terminal/TUI) are mirrored into chat. */\n promptEcho?: boolean;\n /** Reasoning/tool activity steps are surfaced to chat as discreet session events. */\n reasoning?: boolean;\n /** Provider exposes a slash-command catalog for the chat input palette. */\n slashCommands?: boolean;\n };\n terminal?: {\n live?: {\n read?: boolean;\n write?: boolean;\n };\n attach?: {\n create?: boolean;\n read?: boolean;\n write?: boolean;\n detach?: boolean;\n };\n };\n source: CapabilitySource;\n confidence: CapabilityConfidence;\n lastUpdatedAt: number;\n}\n\ntype ContextLifecycleState =\n | \"fresh\"\n | \"primed\"\n | \"working\"\n | \"cooling\"\n | \"compacting\"\n | \"hibernating\";\n\nexport interface ContextProbeMetrics {\n agentId: string;\n contextPercent: number;\n tokensUsed?: number;\n tokensMax?: number;\n quotaUsed?: number;\n quotaLimit?: number;\n quotaResetIn?: number;\n model?: string;\n effort?: string;\n source: \"statusline\" | \"hook\" | \"api\" | \"estimate\";\n confidence: \"exact\" | \"reported\" | \"estimated\";\n timestamp: number;\n}\n\nexport interface ContextState {\n utilization: number;\n tokensUsed?: number;\n tokensMax?: number;\n lifecycleState: ContextLifecycleState;\n warmTopics: string[];\n activeMemories: string[];\n tasksSinceCompact: number;\n lastCompactedAt?: number;\n lastUpdatedAt: number;\n source: \"statusline\" | \"hook\" | \"api\" | \"estimate\";\n confidence: \"exact\" | \"reported\" | \"estimated\";\n}\n\nexport interface ContextSnapshot {\n id: number;\n agentId: string;\n context: ContextState;\n utilization: number;\n lifecycleState: ContextLifecycleState;\n tokensUsed?: number;\n tokensMax?: number;\n source: ContextState[\"source\"];\n confidence: ContextState[\"confidence\"];\n capturedAt: number;\n}\n\nexport type MemoryType = \"organization\" | \"role\" | \"project\" | \"task\" | \"interaction\" | \"agent\";\nexport type MemoryVisibility = \"private\" | \"project\" | \"org\" | \"public\";\nexport type MemorySensitivity = \"public\" | \"normal\" | \"sensitive\" | \"secret\";\nexport type MemoryConfidence = \"reported\" | \"inferred\" | \"verified\";\nexport type MemoryRedactionState = \"raw\" | \"redacted\" | \"rejected\";\n\nexport interface Memory {\n id: string;\n type: MemoryType;\n scope: string;\n title: string;\n content: string;\n tags: string[];\n visibility: MemoryVisibility;\n sensitivity: MemorySensitivity;\n confidence: MemoryConfidence;\n redactionState: MemoryRedactionState;\n relevanceScore: number;\n sourceAgent?: string;\n sourceTask?: number;\n createdBy?: string;\n contentHash?: string;\n metadata: Record<string, unknown>;\n accessCount: number;\n lastAccessedAt?: number;\n createdAt: number;\n updatedAt: number;\n expiresAt?: number;\n}\n\nexport interface CreateMemoryInput {\n type: MemoryType;\n scope: string;\n title: string;\n content: string;\n tags?: string[];\n visibility?: MemoryVisibility;\n sensitivity?: MemorySensitivity;\n confidence?: MemoryConfidence;\n redactionState?: MemoryRedactionState;\n relevanceScore?: number;\n sourceAgent?: string;\n sourceTask?: number;\n createdBy?: string;\n metadata?: Record<string, unknown>;\n ttlMs?: number;\n}\n\nexport interface UpdateMemoryInput {\n title?: string;\n content?: string;\n tags?: string[];\n visibility?: MemoryVisibility;\n sensitivity?: MemorySensitivity;\n confidence?: MemoryConfidence;\n redactionState?: MemoryRedactionState;\n relevanceScore?: number;\n metadata?: Record<string, unknown>;\n expiresAt?: number | null;\n}\n\nexport interface MemoryQuery {\n type?: MemoryType;\n scope?: string;\n tags?: string[];\n minRelevance?: number;\n limit?: number;\n includeExpired?: boolean;\n visibility?: MemoryVisibility;\n includeSensitive?: boolean;\n}\n\nexport interface MemorySearchResult {\n memories: Memory[];\n total: number;\n}\n\nexport interface PackagedMemory {\n memory: Memory;\n reason: string;\n priority: 1 | 2 | 3;\n score?: number;\n}\n\nexport interface TaskRoutingHints {\n id?: number;\n title?: string;\n text?: string;\n scope?: string;\n tags?: string[];\n capabilities?: string[];\n target?: string;\n}\n\nexport interface ContextBudget {\n maxTokens: number;\n maxMemories: number;\n priorityCutoff: 1 | 2 | 3;\n}\n\ninterface TaskHistorySummary {\n taskId: number;\n agentId: string;\n status: string;\n summary?: string;\n completedAt?: number;\n}\n\nexport interface ContextPackage {\n memories: PackagedMemory[];\n rolePrompt?: string;\n recentContext?: string;\n taskHistory?: TaskHistorySummary[];\n estimatedTokens: number;\n}\n\nexport interface ContextPackageRequest {\n task: TaskRoutingHints;\n agent: AgentCard;\n budget: ContextBudget;\n memories?: Memory[];\n}\n\nexport interface MemoryStats {\n total: number;\n byType: Partial<Record<MemoryType, number>>;\n byScope: Record<string, number>;\n bySensitivity: Partial<Record<MemorySensitivity, number>>;\n}\n\nexport type ActiveMemoryClearReason = \"compact\" | \"clearContext\" | \"restart\" | \"shutdown\" | \"manual\";\n\nexport interface MemoryBrokerCapabilities {\n search: true;\n create: boolean;\n update: boolean;\n delete: boolean;\n stats: boolean;\n assemble: boolean;\n activeTracking: boolean;\n activeList: boolean;\n external: boolean;\n}\n\nexport interface MemoryBrokerContext {\n now: number;\n actor: string;\n scopes: string[];\n relayUrl?: string;\n requestId?: string;\n}\n\nexport type MemoryBrokerKind = \"sqlite\" | \"http\" | \"command\";\n\ninterface SqliteMemoryBrokerConfig {\n type: \"sqlite\";\n}\n\nexport interface HttpMemoryBrokerConfig {\n type: \"http\";\n url: string;\n tokenEnv?: string;\n timeoutMs?: number;\n}\n\nexport interface CommandMemoryBrokerConfig {\n type: \"command\";\n command: string;\n args?: string[];\n timeoutMs?: number;\n}\n\nexport type MemoryBrokerConfig = SqliteMemoryBrokerConfig | HttpMemoryBrokerConfig | CommandMemoryBrokerConfig;\n\nexport type MessageKind =\n | \"chat\"\n | \"channel.event\"\n | \"task\"\n | \"pair\"\n | \"control\"\n | \"system\"\n | \"session\";\n\n/**\n * Mechanical message kinds: the relay's own lifecycle/observability lane, not\n * agent-directed messaging. They bypass delivery resolution (never re-delivered\n * into a session) and — when targeting a reserved sink — the recipient-constraint\n * auth check (a managed token's `targets`/`policies`/`agents` constraints gate which\n * *agents* it may message, not a session-mirror capture to the reserved sink; #284).\n * Keep in sync with the `MessageKind` union.\n */\nexport const MECHANICAL_MESSAGE_KINDS: readonly MessageKind[] = [\"system\", \"control\", \"session\"];\n\nexport function isMechanicalMessageKind(kind: string | undefined): boolean {\n return MECHANICAL_MESSAGE_KINDS.includes((kind ?? \"\") as MessageKind);\n}\n\n/**\n * Reserved built-in identities that are not real agents: the `user` chat/mirror sink\n * and the `system` lifecycle sender. They are never registered, polled, or delivered to\n * like agents, so recipient constraints don't apply to mechanical posts addressed to them.\n */\nexport const RESERVED_AGENT_IDS: readonly string[] = [\"user\", \"system\"];\n\nexport function isReservedAgentId(id: string | undefined): boolean {\n return id === \"user\" || id === \"system\";\n}\n\n/**\n * Session-mirror event taxonomy. Every `kind: \"session\"` message carries a\n * `payload.session` of this shape so the dashboard can render the live provider\n * session faithfully regardless of which surface (chat box or web terminal)\n * started the turn. `prompt`/`response` render as chat bubbles; `narration` is\n * the agent's intermediate spoken text between tool calls (the terminal's `●`\n * lines) and renders inline in the turn's activity trace; `reasoning`, `tool`,\n * and `notice` render discreetly (collapsed/inline activity, never bubbles).\n * A legacy session message with no `payload.session` is treated as a `response`.\n */\ntype SessionEventType = \"prompt\" | \"response\" | \"narration\" | \"reasoning\" | \"tool\" | \"notice\";\n\ntype SessionEventOrigin = \"chat\" | \"terminal\" | \"provider\";\n\nexport interface MessageSessionMeta {\n type: SessionEventType;\n /** Where the event originated. `terminal` = typed into the provider directly. */\n origin?: SessionEventOrigin;\n /** Groups reasoning/tool steps and the final response under one provider turn. */\n turnId?: string;\n /** Short label: tool name (\"Bash\"), \"Thinking\", a slash command, etc. */\n label?: string;\n /** Tool/step lifecycle status when type is \"tool\": running | completed | failed. */\n status?: \"running\" | \"completed\" | \"failed\";\n /** True while the step body is still being streamed in (coalesced deltas). */\n streaming?: boolean;\n /** Provider that produced the event. */\n provider?: string;\n}\n\nexport type ArtifactKind = \"image\" | \"audio\" | \"video\" | \"document\" | \"archive\" | \"other\";\nexport type ArtifactSensitivity = \"public\" | \"normal\" | \"sensitive\" | \"secret\";\nexport type ArtifactVisibility = \"private\" | \"project\" | \"org\";\nexport type ArtifactRole = \"media\" | \"patch\" | \"report\" | \"log\" | \"output\" | \"input\";\n\nexport interface ArtifactBlob {\n digest: string;\n storageUri: string;\n mediaType: string;\n size: number;\n createdAt: number;\n}\n\nexport interface ArtifactLink {\n id: string;\n artifactId: string;\n entityType: \"message\" | \"task\" | \"recipeRun\" | \"recipeStep\" | \"channelEvent\";\n entityId: string;\n role?: ArtifactRole;\n title?: string;\n createdBy: string;\n createdAt: number;\n}\n\nexport interface Artifact {\n id: string;\n blobDigest: string;\n mediaType: string;\n kind: ArtifactKind;\n filename?: string;\n size: number;\n digest: string;\n visibility: ArtifactVisibility;\n sensitivity: ArtifactSensitivity;\n createdBy: string;\n createdAt: number;\n expiresAt?: number;\n metadata: Record<string, unknown>;\n links?: ArtifactLink[];\n url?: string;\n}\n\nexport interface AttachmentRef {\n artifactId: string;\n kind?: ArtifactKind;\n role?: ArtifactRole;\n title?: string;\n ref?: AttachmentSourceRef;\n metadata?: Record<string, unknown>;\n}\n\nexport interface ChannelAttachmentRef {\n artifactId?: string;\n kind?: ArtifactKind;\n role?: ArtifactRole;\n title?: string;\n ref?: AttachmentSourceRef;\n metadata?: Record<string, unknown>;\n}\n\nexport type AttachmentSourceRef =\n | { type: \"relay-blob\"; id: string; [key: string]: unknown }\n | { type: \"external-url\"; url: string; [key: string]: unknown }\n | { type: \"channel-file\"; id: string; provider?: string; uniqueId?: string; [key: string]: unknown };\n\nexport interface Message {\n id: number;\n from: string;\n to: string; // agent-id | \"tag:<name>\" | \"broadcast\" | \"cap:<name>\"\n kind: MessageKind;\n channel?: string;\n subject?: string;\n body: string;\n threadId?: number;\n replyTo?: number;\n // Server-owned reply obligation (#283). Absent/true = the message wants a response (default);\n // false = a notification (FYI, merge notice, lifecycle event) that must NOT be replied to.\n // The server keys footer rendering and the reply-obligation tracker off this, not off `from`.\n replyExpected?: boolean;\n claimable?: boolean;\n claimedBy?: string;\n claimedAt?: number;\n claimExpiresAt?: number;\n idempotencyKey?: string;\n deliveryStatus?: MessageDeliveryStatus;\n deliveryAttempts?: number;\n deliveryLastError?: string;\n deliveryNextRetryAt?: number;\n deliveryPoisonReason?: string;\n deliveryUpdatedAt?: number;\n queuedAt?: number;\n maxAgeSeconds?: number;\n resolvedToAgent?: string;\n payload: Record<string, unknown>;\n meta?: Record<string, unknown>;\n reactions?: MessageReaction[];\n readBy: string[];\n createdAt: number;\n // True event time (#196). Equals createdAt for messages posted live; differs only when a\n // Runner backfilled a message queued during an outage. Read `occurredAt ?? createdAt`.\n occurredAt?: number;\n}\n\nexport interface ReplyObligation {\n messageId: number;\n agentId: string;\n from: string;\n kind: MessageKind;\n subject?: string;\n channel?: string;\n bodyPreview: string;\n createdAt: number;\n replyCommand: string;\n}\n\nexport interface MessageReaction {\n messageId: number;\n actorId: string;\n emoji: string;\n createdAt: number;\n updatedAt: number;\n}\n\nexport type RelayNotificationKind =\n | \"message\"\n | \"reply\"\n | \"error\"\n | \"agent.blocked\";\n\nexport type RelayNotificationSeverity = \"info\" | \"warning\" | \"error\";\n\nexport interface RelayNotification {\n id: string;\n kind: RelayNotificationKind;\n severity: RelayNotificationSeverity;\n title: string;\n body: string;\n messageId?: number;\n agentId?: string;\n threadPeer?: string;\n view?: \"chat\" | \"messages\" | \"agents\" | \"work\" | \"tasks\" | \"channels\" | \"overview\";\n createdAt: number;\n}\n\nexport type MessageDeliveryStatus = \"pending\" | \"delivered\" | \"queued\" | \"failed\" | \"dead\";\n\nexport interface MessageDeliveryAttempt {\n id: number;\n messageId: number;\n agentId?: string;\n action: \"attempt\" | \"retry-now\" | \"mark-dead\" | \"clear\";\n status: MessageDeliveryStatus;\n error?: string;\n nextRetryAt?: number;\n poisonReason?: string;\n createdAt: number;\n}\n\nexport interface MessageDeliveryState extends Pick<Message,\n | \"id\"\n | \"to\"\n | \"deliveryStatus\"\n | \"deliveryAttempts\"\n | \"deliveryLastError\"\n | \"deliveryNextRetryAt\"\n | \"deliveryPoisonReason\"\n | \"deliveryUpdatedAt\"\n | \"queuedAt\"\n | \"maxAgeSeconds\"\n | \"resolvedToAgent\"\n> {\n attempts: MessageDeliveryAttempt[];\n}\n\nexport interface SendMessageInput {\n from: string;\n to: string;\n kind?: MessageKind;\n channel?: string;\n subject?: string;\n body: string;\n replyTo?: number;\n // Defaults to true server-side. Set false to mark a notification (no reply wanted) — the\n // server suppresses the reply-scaffold footer and appends a one-line no-reply nudge (#283).\n replyExpected?: boolean;\n claimable?: boolean;\n idempotencyKey?: string;\n maxAgeSeconds?: number;\n attachments?: AttachmentRef[];\n payload?: Record<string, unknown>;\n meta?: Record<string, unknown>;\n // Epoch-ms event time, stamped when the event actually occurred (#196). When a Runner\n // queues a message through its durable outbox during a server outage, this preserves the\n // true time rather than the (later) server receive time. Defaults server-side to created_at.\n occurredAt?: number;\n}\n\nexport type ConnectorKind = \"channel\" | \"event\" | \"provider\" | \"orchestrator\";\nexport type ConnectorAction = \"install\" | \"uninstall\" | \"enable\" | \"disable\" | \"start\" | \"stop\" | \"restart\" | \"status\" | \"doctor\";\n\nexport interface ConnectorManifest {\n schema: \"agent-relay.connector.v1\";\n id: string;\n kind: ConnectorKind;\n packageName?: string;\n binary: string;\n displayName: string;\n description?: string;\n version: string;\n capabilities: string[];\n commands: Partial<Record<ConnectorAction, string[]>>;\n configSchema?: Record<string, unknown>;\n}\n\ninterface ConnectorRuntime {\n installed: boolean;\n enabled?: boolean;\n running?: boolean;\n status?: \"ok\" | \"warn\" | \"error\" | \"unknown\";\n detail?: string;\n updatedAt?: string;\n raw?: unknown;\n}\n\nexport interface ConnectorSummary {\n id: string;\n kind: ConnectorKind;\n displayName: string;\n description?: string;\n version: string;\n packageName?: string;\n binary: string;\n capabilities: string[];\n registryPath: string;\n manifest: ConnectorManifest;\n config?: Record<string, unknown>;\n state?: Record<string, unknown>;\n runtime: ConnectorRuntime;\n}\n\nexport interface ConnectorActionResult {\n connectorId: string;\n action: ConnectorAction;\n command?: string[];\n ok: boolean;\n exitCode?: number | null;\n stdout?: string;\n stderr?: string;\n parsed?: unknown;\n}\n\nexport type PairStatus = \"pending\" | \"active\" | \"ended\" | \"rejected\" | \"expired\";\n\nexport interface PairSession {\n id: string;\n requesterId: string;\n targetId: string;\n status: PairStatus;\n objective?: string;\n createdAt: number;\n updatedAt: number;\n expiresAt: number;\n acceptedAt?: number;\n endedAt?: number;\n endedBy?: string;\n lastMessageAt?: number;\n meta: Record<string, unknown>;\n}\n\nexport interface CreatePairInput {\n from: string;\n target: string;\n objective?: string;\n ttlMs?: number;\n meta?: Record<string, unknown>;\n}\n\nexport interface PairActionInput {\n agentId: string;\n reason?: string;\n}\n\nexport interface PairMessageInput {\n from: string;\n body: string;\n subject?: string;\n}\n\nexport interface PollQuery {\n for: string; // agent-id\n since?: number; // unix ms (createdAt cursor)\n sinceId?: number; // monotonic message id cursor (preferred — avoids same-ms collisions)\n unread?: boolean;\n channel?: string;\n limit?: number;\n}\n\nexport interface RegisterAgentInput {\n id: string;\n name: string;\n kind?: AgentKind;\n label?: string | null;\n tags?: string[];\n machine?: string;\n rig?: string;\n capabilities?: string[];\n ready?: boolean;\n status?: AgentCard[\"status\"];\n instanceId?: string;\n providerCapabilities?: ProviderCapabilities;\n context?: ContextState;\n meta?: Record<string, unknown>;\n /** Parent agent id. Server sets this authoritatively from the registering token's\n * `spawnedBy`/`parentAgents` constraint; any client-supplied value is ignored. */\n spawnedBy?: string;\n}\n\nexport interface AgentSessionGuard {\n instanceId?: string;\n epoch?: number;\n}\n\nexport type TaskSeverity = \"info\" | \"warning\" | \"critical\";\nexport type TaskStatus =\n | \"open\"\n | \"claimed\"\n | \"in_progress\"\n | \"blocked\"\n | \"orphaned\"\n | \"done\"\n | \"failed\"\n | \"canceled\";\n\nexport type CommandStatus =\n | \"pending\"\n | \"accepted\"\n | \"running\"\n | \"succeeded\"\n | \"failed\"\n | \"timed_out\"\n | \"rejected\"\n | \"canceled\";\n\nexport interface Command {\n id: string;\n type: string;\n source: string;\n target: string;\n params: Record<string, unknown>;\n status: CommandStatus;\n result?: Record<string, unknown>;\n error?: string;\n correlationId?: string;\n createdAt: number;\n updatedAt: number;\n expiresAt?: number;\n}\n\nexport interface CreateCommandInput {\n type: string;\n source: string;\n target: string;\n params?: Record<string, unknown>;\n correlationId?: string;\n ttlMs?: number;\n}\n\nexport interface UpdateCommandInput {\n status?: CommandStatus;\n result?: Record<string, unknown>;\n error?: string;\n}\n\nexport interface Task {\n id: number;\n source: string;\n title: string;\n body: string;\n severity: TaskSeverity;\n status: TaskStatus;\n target: string;\n channel?: string;\n dedupeKey?: string;\n externalUrl?: string;\n occurrenceCount: number;\n claimedBy?: string;\n claimedAt?: number;\n claimExpiresAt?: number;\n messageId?: number;\n result?: string;\n metadata: Record<string, unknown>;\n createdAt: number;\n updatedAt: number;\n lastSeenAt: number;\n}\n\nexport interface TaskEvent {\n id: number;\n taskId: number;\n source: string;\n type: string;\n severity: TaskSeverity;\n title: string;\n body: string;\n metadata: Record<string, unknown>;\n createdAt: number;\n}\n\nexport interface IntegrationEventInput {\n source?: string;\n type?: string;\n severity?: TaskSeverity;\n status?: TaskStatus | \"resolved\";\n title: string;\n body: string;\n target: string;\n channel?: string;\n dedupeKey?: string;\n externalUrl?: string;\n attachments?: AttachmentRef[];\n metadata?: Record<string, unknown>;\n}\n\nexport interface IntegrationTaskStats {\n source: string;\n tasks: number;\n openTasks: number;\n waitingTasks: number;\n failedTasks: number;\n lastSeenAt?: number;\n lastUpdatedAt?: number;\n}\n\nexport interface IntegrationSummary {\n name: string;\n displayName?: string;\n description?: string;\n enabled: boolean;\n configured: boolean;\n observed: boolean;\n type?: string;\n icon?: string;\n accentColor?: string;\n tags: string[];\n homepageUrl?: string;\n repositoryUrl?: string;\n docsUrl?: string;\n manifest?: Record<string, unknown>;\n scopes: string[];\n targets: string[];\n channels: string[];\n callbackHost?: string;\n callbackConfigured: boolean;\n rateLimit: {\n limitPerMinute: number;\n currentWindowCount: number;\n windowStartedAt?: number;\n };\n taskStats: IntegrationTaskStats;\n}\n\nexport type RuntimeContractName =\n | \"relayApi\"\n | \"orchestratorProtocol\"\n | \"runnerProtocol\"\n | \"providerPluginProtocol\";\n\nexport type RuntimeContracts = Partial<Record<RuntimeContractName, number>>;\nexport type RuntimeCapabilities = Record<string, boolean>;\n\nexport interface RuntimePackageMetadata {\n name: string;\n version: string;\n}\n\nexport interface ContractCompatibilityIssue {\n contract: string;\n expected: string;\n actual?: number;\n}\n\nexport interface ContractCompatibility {\n status: \"compatible\" | \"incompatible\" | \"unknown\";\n compatible: boolean;\n issues: ContractCompatibilityIssue[];\n}\n\nexport type AutomationKind = \"scheduled_task\";\nexport type AutomationCatchUpPolicy = \"skip\" | \"run_once\" | \"run_all\";\nexport type AutomationConcurrencyPolicy = \"skip\" | \"queue\" | \"replace\";\nexport type AutomationRunStatus =\n | \"scheduled\"\n | \"dispatching\"\n | \"waiting_agent\"\n | \"running\"\n | \"succeeded\"\n | \"failed\"\n | \"canceled\"\n | \"timed_out\";\n\nexport interface AutomationExistingAgentPolicy {\n mode: \"existing_agent\";\n selector: {\n provider?: SpawnProvider;\n label?: string;\n tags?: string[];\n capabilities?: string[];\n };\n ifNoMatch?: \"fail\" | \"spawn\";\n}\n\nexport interface AutomationOnDemandAgentPolicy {\n mode: \"on_demand_agent\";\n provider: SpawnProvider;\n model?: string;\n effort?: SpawnEffort;\n cwd?: string;\n workspaceMode?: WorkspaceMode;\n profile?: string;\n approvalMode?: SpawnApprovalMode;\n keepAlive?: boolean;\n runtimeBudget?: AutomationRuntimeBudget;\n shutdownAfterMs?: number;\n}\n\nexport type AutomationTargetPolicy = AutomationExistingAgentPolicy | AutomationOnDemandAgentPolicy;\n\nexport interface AutomationRuntimeBudget {\n maxRuntimeMs: number;\n warnAtMs?: number;\n warningMessage?: string;\n}\n\nexport interface AutomationTaskTemplate {\n title: string;\n body: string;\n severity?: TaskSeverity;\n dedupeKey?: string;\n externalUrl?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface Automation {\n id: string;\n kind: AutomationKind;\n name: string;\n description?: string;\n enabled: boolean;\n schedule: string;\n timezone: string;\n nextRunAt?: number;\n catchUpPolicy: AutomationCatchUpPolicy;\n concurrencyPolicy: AutomationConcurrencyPolicy;\n orchestratorId: string;\n targetPolicy: AutomationTargetPolicy;\n taskTemplate: AutomationTaskTemplate;\n createdAt: number;\n updatedAt: number;\n}\n\nexport interface AutomationRun {\n id: string;\n automationId: string;\n status: AutomationRunStatus;\n scheduledFor: number;\n startedAt?: number;\n finishedAt?: number;\n orchestratorId: string;\n targetAgentId?: string;\n spawnedAgentId?: string;\n taskId?: number;\n messageId?: number;\n error?: string;\n result?: Record<string, unknown>;\n meta: Record<string, unknown>;\n createdAt: number;\n updatedAt: number;\n shutdownRequestedAt?: number;\n}\n\nexport interface CreateAutomationInput {\n kind?: AutomationKind;\n name: string;\n description?: string;\n enabled?: boolean;\n schedule: string;\n timezone?: string;\n catchUpPolicy?: AutomationCatchUpPolicy;\n concurrencyPolicy?: AutomationConcurrencyPolicy;\n orchestratorId: string;\n targetPolicy: AutomationTargetPolicy;\n taskTemplate: AutomationTaskTemplate;\n}\n\nexport interface UpdateAutomationInput {\n name?: string;\n description?: string | null;\n enabled?: boolean;\n schedule?: string;\n timezone?: string;\n catchUpPolicy?: AutomationCatchUpPolicy;\n concurrencyPolicy?: AutomationConcurrencyPolicy;\n orchestratorId?: string;\n targetPolicy?: AutomationTargetPolicy;\n taskTemplate?: AutomationTaskTemplate;\n}\n\nexport type ChannelDirection = \"inbound\" | \"outbound\" | \"bidirectional\";\n\nexport type ChannelRouteTarget =\n | { type: \"agent\"; id: string }\n | { type: \"label\"; id: string }\n | { type: \"tag\"; id: string }\n | { type: \"capability\"; id: string }\n | { type: \"broadcast\" }\n | { type: \"orchestrator\"; id: string }\n | { type: \"pool\"; id: string }\n | { type: \"policy\"; id: string };\n\nexport type ChannelBindingMode = \"exclusive\" | \"broadcast\";\n\nexport interface ChannelBinding {\n id: string;\n channelId: string;\n conversationId?: string;\n target: ChannelRouteTarget;\n mode: ChannelBindingMode;\n priority: number;\n createdAt: number;\n updatedAt: number;\n poolSelector?: string;\n poolAgentId?: string;\n poolAgentEpoch?: number;\n poolClaimExpiresAt?: number;\n}\n\nexport interface ChannelTargetHealth {\n status: \"ok\" | \"warning\" | \"error\";\n detail: string;\n target: ChannelRouteTarget;\n matches: Array<{\n id: string;\n name: string;\n status: AgentCard[\"status\"];\n ready: boolean;\n lastSeen: number;\n label?: string;\n tags: string[];\n capabilities: string[];\n }>;\n}\n\nexport interface ChannelSummary {\n id: string;\n name: string;\n type: string;\n transport: string;\n agentId: string;\n accountId: string;\n status: AgentCard[\"status\"];\n ready: boolean;\n direction: ChannelDirection;\n target?: string;\n binding?: ChannelBinding;\n targetHealth?: ChannelTargetHealth;\n topicChannels: string[];\n capabilities: string[];\n tags: string[];\n lastSeen: number;\n meta?: Record<string, unknown>;\n}\n\nexport interface ChannelEventInput {\n body?: string;\n payload: Record<string, unknown>;\n attachments?: ChannelAttachmentRef[];\n conversationId?: string;\n idempotencyKey?: string;\n instanceId?: string;\n epoch?: number;\n}\n\nexport interface ChannelEventResult {\n messages: Message[];\n bindings: ChannelBinding[];\n created: boolean;\n}\n\nexport interface TaskStatusInput {\n status: TaskStatus;\n agentId?: string;\n instanceId?: string;\n epoch?: number;\n result?: string;\n body?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport interface InboxThreadState {\n operatorId: string;\n peerId: string;\n readCursorMessageId?: number;\n archivedAtMessageId?: number;\n updatedAt: number;\n}\n\nexport interface InboxDraft {\n operatorId: string;\n peerId: string;\n body: string;\n subject?: string;\n channel?: string;\n updatedAt: number;\n}\n\nexport interface InboxState {\n operatorId: string;\n threads: InboxThreadState[];\n drafts: InboxDraft[];\n}\n\nexport interface ChatHistoryImportEntry {\n position: number;\n originalMessageId: number;\n originalFrom: string;\n originalTo: string;\n originalCreatedAt: number;\n message: Message;\n}\n\nexport interface ChatHistoryImport {\n id: string;\n targetAgentId?: string;\n targetSpawnRequestId?: string;\n sourcePeerId: string;\n sourceAgentId?: string;\n sourceThreadId?: string;\n sourceAgentLabel?: string;\n importedBy: string;\n importedAt: number;\n entries: ChatHistoryImportEntry[];\n}\n\nexport type ActivityKind = \"message\" | \"reply\" | \"question\" | \"operator\" | \"pair\" | \"task\" | \"state\";\n\nexport interface ActivityEvent {\n id: number;\n operatorId?: string;\n clientId?: string;\n kind: ActivityKind;\n title: string;\n body?: string;\n meta?: string;\n icon?: string;\n view?: string;\n peer?: string;\n messageId?: number;\n pairId?: string;\n taskId?: number;\n agentId?: string;\n metadata: Record<string, unknown>;\n createdAt: number;\n}\n\nexport interface ActivityEventInput {\n operatorId?: string;\n clientId?: string;\n kind: ActivityKind;\n title: string;\n body?: string;\n meta?: string;\n icon?: string;\n view?: string;\n peer?: string;\n messageId?: number;\n pairId?: string;\n taskId?: number;\n agentId?: string;\n metadata?: Record<string, unknown>;\n}\n\n// --- Orchestrators ---\n\nexport type OrchestratorStatus = \"online\" | \"offline\";\n\n/** Spawn providers — the runtime tuple is the single source of truth; the type derives from it. */\nexport const SPAWN_PROVIDERS = [\"claude\", \"codex\"] as const;\nexport type SpawnProvider = (typeof SPAWN_PROVIDERS)[number];\nfunction isSpawnProvider(value: unknown): value is SpawnProvider {\n return typeof value === \"string\" && (SPAWN_PROVIDERS as readonly string[]).includes(value);\n}\n\n/** Approval modes — runtime tuple + derived type. */\nexport const APPROVAL_MODES = [\"open\", \"guarded\", \"read-only\"] as const;\nexport type SpawnApprovalMode = (typeof APPROVAL_MODES)[number];\nfunction isApprovalMode(value: unknown): value is SpawnApprovalMode {\n return typeof value === \"string\" && (APPROVAL_MODES as readonly string[]).includes(value);\n}\n\nexport type SpawnEffort = \"low\" | \"medium\" | \"high\" | \"xhigh\" | \"max\";\n\n/** Workspace modes — runtime tuple + derived type + guard. */\nexport const VALID_WORKSPACE_MODES = [\"isolated\", \"shared\", \"inherit\"] as const;\nexport type WorkspaceMode = (typeof VALID_WORKSPACE_MODES)[number];\nexport function isWorkspaceMode(value: unknown): value is WorkspaceMode {\n return typeof value === \"string\" && (VALID_WORKSPACE_MODES as readonly string[]).includes(value);\n}\n/** Narrow an unknown to a WorkspaceMode, or undefined if it isn't one. */\nexport function normalizeWorkspaceMode(value: unknown): WorkspaceMode | undefined {\n return isWorkspaceMode(value) ? value : undefined;\n}\nexport type WorkspaceStatus = \"active\" | \"ready\" | \"conflict\" | \"review_requested\" | \"merge_planned\" | \"merged\" | \"abandoned\" | \"cleanup_requested\" | \"cleaned\";\n/** Terminal workspace statuses shared across server + dashboard filters. */\nexport const TERMINAL_WORKSPACE_STATUS_VALUES = [\"cleaned\", \"merged\", \"abandoned\"] as const satisfies readonly WorkspaceStatus[];\n\n/** How a workspace's work is integrated back into its base branch. */\nexport type WorkspaceMergeStrategy = \"pr\" | \"rebase-ff\" | \"auto\";\n\nexport interface WorkspaceProbeWorktree {\n path: string;\n branch?: string;\n headSha?: string;\n bare?: boolean;\n detached?: boolean;\n prunable?: boolean;\n locked?: boolean;\n reason?: string;\n}\n\nexport interface WorkspaceProbe {\n path: string;\n isGitRepo: boolean;\n repoRoot?: string;\n branch?: string;\n headSha?: string;\n dirty?: boolean;\n detached?: boolean;\n worktrees?: WorkspaceProbeWorktree[];\n error?: string;\n}\n\ninterface WorkspaceGitCommit {\n sha: string;\n message: string;\n at?: number;\n}\n\n/**\n * Live git state of a workspace worktree, computed on the host by the\n * orchestrator. Used to surface whether a worktree contains actual work\n * (commits ahead of base, dirty files) so the dashboard isn't a black box and\n * so exit-time reconcile can decide cleanup-vs-flag.\n */\nexport interface WorkspaceGitState {\n /** Commits on the agent branch not yet in base (raw `rev-list` count). */\n ahead?: number;\n /** Commits still genuinely unmerged after discounting work already present in\n * base via a squash/cherry-pick (git-cherry '+' count). Equals `ahead` for a\n * normal branch; 0 when the work landed but left `ahead` positive. */\n unmergedAhead?: number;\n /** True when `ahead` > 0 but the work is already in base (squash/cherry-pick\n * merge) — the worktree is effectively merged and safe to clean up. */\n landed?: boolean;\n /** Commits in base not in the agent branch (how far base moved on). */\n behind?: number;\n /** True if the worktree has uncommitted/untracked changes. */\n dirty?: boolean;\n /** Number of dirty (porcelain) entries. */\n dirtyCount?: number;\n /** Most recent commit in the worktree. */\n lastCommit?: WorkspaceGitCommit;\n /** Ref/sha the ahead/behind counts were computed against. */\n baseRef?: string;\n /** Branch currently checked out in the worktree (for recorded-vs-live mismatch). */\n branch?: string;\n /** Set when the worktree path no longer exists on disk. */\n missing?: boolean;\n /** Populated when git interrogation failed. */\n error?: string;\n}\n\n/**\n * Pre-flight check for integrating a workspace's work, computed on the host.\n * Reports whether the merge would be clean, conflict, or is a no-op, and which\n * strategy `auto` would pick — so the dashboard can warn before the user acts.\n */\nexport interface WorkspaceMergePreview {\n /** Strategy `auto` would resolve to given the repo's remote/gh state. */\n strategy: \"pr\" | \"rebase-ff\";\n /** Commits on the agent branch not yet in base (raw count). */\n ahead?: number;\n /** Genuinely-unmerged commits after discounting squash/cherry-pick landings. */\n unmergedAhead?: number;\n /** True when the work already landed in base via squash/cherry-pick. */\n landed?: boolean;\n /** Merged-PR state (ground truth when local git can't see a squash landing because\n * base moved on). Populated only with `checkPr` (a `gh` round-trip). */\n prState?: \"merged\" | \"open\";\n /** True when the branch's PR is merged on the remote. `prMergeSha` is its merge commit,\n * recorded as the landed SHA when the relay finalizes an out-of-band PR merge (#304). */\n prMerged?: boolean;\n prMergeSha?: string;\n /** URL of the branch's PR, when one was found via `gh`. */\n prUrl?: string;\n /** Commits in base not in the agent branch. */\n behind?: number;\n /** Uncommitted/untracked entries in the worktree. */\n dirtyCount?: number;\n /** True if the three-way merge of base and the branch would conflict. */\n conflict?: boolean;\n /** True if base can fast-forward to the branch with no rebase needed. */\n cleanFastForward?: boolean;\n /** Whether an `origin` remote is configured. */\n hasRemote?: boolean;\n /** Whether the `gh` CLI is available on the host. */\n ghAvailable?: boolean;\n /** Resolved base branch name the work would land on. */\n baseRef?: string;\n /** Human-readable reason a merge can't proceed (no work, dirty, missing). */\n reason?: string;\n /** True when there is nothing to land: ahead=0 with a clean worktree (branch tree\n * already in base — never diverged, or already landed via squash/cherry-pick/PR).\n * A no-op land resolves the workspace to a terminal state instead of parking it in\n * the steward queue (#230). Distinct from a dirty-worktree refusal, which also sets\n * `reason` but is not a no-op. */\n noop?: boolean;\n /** Set when the worktree path no longer exists. */\n missing?: boolean;\n /** Populated when git interrogation failed. */\n error?: string;\n}\n\n/** Outcome of a `workspace.merge` command executed on the host. */\nexport interface WorkspaceMergeResult {\n workspaceId?: string;\n /** Strategy actually used. */\n strategy: \"pr\" | \"rebase-ff\";\n /** True when work was landed locally (rebase-ff into base). */\n merged: boolean;\n /** True when the land was a no-op — nothing to merge (ahead=0, clean worktree),\n * resolved to a terminal `merged` status to clear the steward queue (#230). The\n * worktree/branch are reclaimed too when the owner is gone (`deleteBranch`). */\n noop?: boolean;\n /** True when a merge was prevented by conflicts. */\n conflict?: boolean;\n /** PR URL when the pr strategy opened one. */\n prUrl?: string;\n branch?: string;\n baseRef?: string;\n /** SHA of the landed commit — the agent branch's tip, preserved verbatim (#287).\n * This is the SHA reported in the `branch.landed` notice, so it exists on base\n * exactly as the agent produced it (no rebase rewrite). On a clean fast-forward\n * it equals {@link baseSha}; on a no-ff merge it's the merge's second parent. */\n mergedSha?: string;\n /** SHA base points at after the land (#287): equals {@link mergedSha} on a\n * fast-forward, or the merge commit on a no-ff merge when base had advanced. The\n * relay records this as the recycled workspace's new base_sha. Absent on older\n * orchestrators — consumers fall back to {@link mergedSha}. */\n baseSha?: string;\n /** Subject line of the landed commit (git log -1 --format=%s of mergedSha), for\n * the `branch.landed` notification body (#239). Best-effort; absent on older\n * orchestrators or when the subject can't be read. */\n subject?: string;\n branchDeleted?: boolean;\n worktreeRemoved?: boolean;\n /** Fresh branch the worktree was recycled onto after a land-and-continue merge\n * (#206) — the relay repoints the workspace row at it. Absent for terminal lands. */\n newBranch?: string;\n /** True when the landed base branch was pushed to its upstream (origin). */\n pushed?: boolean;\n /** Resulting workspace status the relay should record. */\n status: WorkspaceStatus;\n /** Deps re-provisioned after a land-and-continue recycle when the advanced base\n * brought new dependencies (issue #51). Absent when nothing was stale. */\n depsRefresh?: WorkspaceDepsRefreshResult;\n /** Populated when the merge could not complete. */\n error?: string;\n}\n\n/**\n * Joined steward briefing for one workspace (#208): the row, owner + orchestrator\n * liveness, live git state, recorded-vs-live branch mismatch, any active steward\n * claim, and a recommended next action — so a steward (or release agent) doesn't\n * reconstruct it from ad-hoc shell + API calls.\n */\nexport interface WorkspaceDiagnostics {\n workspaceId: string;\n status: WorkspaceStatus;\n mode: WorkspaceMode;\n repoRoot: string;\n worktreePath?: string;\n recordedBranch?: string;\n liveBranch?: string;\n baseRef?: string;\n branchMismatch?: boolean;\n owner?: { id?: string; status?: string; online: boolean };\n orchestrator?: { id?: string; online: boolean };\n /** Active claim that auto-merge yields to (#208), if held and unexpired. */\n claim?: { by?: string; purpose?: string; expiresAt?: number };\n /** Live worktree git state (proxied from the host); absent when unavailable. */\n gitState?: WorkspaceGitState;\n /** Why git state is absent (host offline, no worktree, …). */\n gitStateUnavailable?: string;\n recommendation: { action: \"merge\" | \"rebase\" | \"cleanup\" | \"review\" | \"wait\" | \"none\"; confidence: \"high\" | \"medium\" | \"low\"; reason: string };\n}\n\n/** One changed file in a workspace diff against its base. */\nexport interface WorkspaceDiffFile {\n path: string;\n /** Lines added (undefined for binary files). */\n additions?: number;\n /** Lines removed (undefined for binary files). */\n deletions?: number;\n binary?: boolean;\n}\n\n/** Diff of a workspace's committed work against its base, computed on the host. */\nexport interface WorkspaceDiff {\n baseRef?: string;\n ahead?: number;\n files: WorkspaceDiffFile[];\n /** Unified patch text (may be truncated). */\n patch?: string;\n /** True when the patch was capped at the size limit. */\n truncated?: boolean;\n /** Uncommitted entries in the worktree, not part of the committed diff. */\n dirtyCount?: number;\n missing?: boolean;\n error?: string;\n}\n\n/** A worktree found on disk with no matching active workspace row. */\nexport interface WorkspaceOrphan {\n worktreePath: string;\n repoRoot: string;\n branch?: string;\n headSha?: string;\n /** True if a DB row exists for this path but is already terminal (cleaned/merged). */\n hadTerminalRow?: boolean;\n /** Work already merged into base (squash/cherry/PR). Set from the host probe. */\n landed?: boolean;\n /** Raw commits ahead of base (a squash-landed branch still shows >0). */\n ahead?: number;\n /** Commits ahead whose patch isn't already in base — the squash-aware count. */\n unmergedAhead?: number;\n /** Uncommitted working-tree changes in the worktree. */\n dirty?: boolean;\n /**\n * Safe to remove with no loss: clean tree AND (nothing ahead OR already landed).\n * `false` means the worktree holds un-landed work and must be flagged, not reaped.\n * `undefined` means the host couldn't be probed — treat as not safe.\n */\n safeToReap?: boolean;\n}\n\n/**\n * Outcome of provisioning node_modules into a freshly created isolated worktree.\n * Git worktrees do not share gitignored/untracked files, so a new worktree starts\n * with no installed deps — typecheck/test/build would see thousands of phantom\n * missing-module errors. We either symlink the source checkout's node_modules\n * (fast, default) or run a package install (when the source has none to borrow).\n */\nexport interface WorkspaceDepsProvision {\n mode: \"symlink\" | \"install\" | \"none\";\n /** Relative dirs (from repo root) whose node_modules were symlinked. */\n linked?: string[];\n /** Relative dirs where a package install ran successfully. */\n installed?: string[];\n packageManager?: string;\n error?: string;\n}\n\n/** Result of symlinking configured untracked files/dirs from main into an isolated worktree. */\nexport interface WorkspaceSymlinkProvision {\n /** Relative paths (from repo root) that were symlinked from the main checkout. */\n linked: string[];\n /** Per-pattern failures, if any (best-effort: a failure never blocks the spawn). */\n errors?: string[];\n}\n\n/** One project dir's outcome in a workspace deps refresh (issue #51). */\nexport interface WorkspaceDepsRefreshDir {\n /** Relative dir from repo root (`.` for root, e.g. `dashboard`). */\n dir: string;\n /** ok — declared deps already present; stale — missing (check-only, not installed);\n * installed — re-installed in isolation; failed — install errored. */\n status: \"ok\" | \"stale\" | \"installed\" | \"failed\";\n /** Declared deps (dependencies + devDependencies) missing from node_modules, capped sample. */\n missing?: string[];\n /** True when the dir's node_modules was a shared symlink replaced with a real install. */\n wasSymlink?: boolean;\n packageManager?: string;\n error?: string;\n}\n\n/**\n * Outcome of refreshing an isolated worktree's deps (issue #51). The default symlink\n * provisioning shares the source checkout's node_modules, so a dep added to the base\n * AFTER worktree creation is missing in the worktree. Refresh promotes each stale dir\n * to a real, isolated install — never mutating the shared source node_modules.\n */\nexport interface WorkspaceDepsRefreshResult {\n /** True when at least one dir was actually (re)installed. */\n refreshed: boolean;\n /** True (check-only) when any dir is stale, or (refresh) when any install left deps missing. */\n stale?: boolean;\n dirs: WorkspaceDepsRefreshDir[];\n error?: string;\n}\n\nexport interface WorkspaceMetadata {\n id?: string;\n mode: WorkspaceMode;\n requestedMode?: WorkspaceMode;\n repoRoot?: string;\n sourceCwd?: string;\n worktreePath?: string;\n branch?: string;\n baseRef?: string;\n baseSha?: string;\n status?: WorkspaceStatus;\n stewardAgentId?: string;\n deps?: WorkspaceDepsProvision;\n /** Configured untracked files/dirs symlinked from main (see WorkspaceConfig.symlinkPaths). */\n symlinks?: WorkspaceSymlinkProvision;\n probe?: WorkspaceProbe;\n}\n\nexport interface WorkspaceRecord {\n id: string;\n repoRoot: string;\n sourceCwd: string;\n worktreePath: string;\n branch?: string;\n baseRef?: string;\n baseSha?: string;\n mode: WorkspaceMode;\n requestedMode?: WorkspaceMode;\n status: WorkspaceStatus;\n ownerAgentId?: string;\n /** Server-derived from ownerAgentId via the authoritative owner liveness helper. */\n ownerOnline?: boolean;\n ownerPolicyName?: string;\n ownerAutomationRunId?: string;\n stewardAgentId?: string;\n metadata: Record<string, unknown>;\n createdAt: number;\n updatedAt: number;\n readyAt?: number;\n cleanedAt?: number;\n}\n\nexport interface ConfigEntry<T = unknown> {\n namespace: string;\n key: string;\n value: T;\n version: number;\n updatedAt: string;\n updatedBy?: string;\n}\n\nexport interface ConfigHistoryEntry<T = unknown> {\n id: number;\n namespace: string;\n key: string;\n value: T;\n version: number;\n changedAt: string;\n changedBy?: string;\n}\n\nexport type ManagedAgentStatus = \"stopped\" | \"starting\" | \"running\" | \"stopping\" | \"backoff\";\n\nexport interface ManagedAgentState {\n policyName: string;\n status: ManagedAgentStatus;\n agentId?: string;\n orchestratorId: string;\n provider: SpawnProvider;\n tmuxSession?: string;\n spawnRequestId?: string;\n workspaceId?: string;\n workspacePath?: string;\n workspaceBranch?: string;\n lastSpawnAt?: number;\n lastStopAt?: number;\n healthySince?: number;\n restartCount: number;\n consecutiveFailures: number;\n backoffUntil?: number;\n lastError?: string;\n updatedAt: number;\n}\n\ntype SpawnPolicyMode = \"always-on\" | \"on-demand\";\n\nexport type AgentProfileProvider = SpawnProvider | \"any\";\nexport type AgentProfileBase = \"host\" | \"minimal\" | \"isolated\";\nexport type AgentProfileInstructionPolicy = \"allow\" | \"ignore\";\nexport type AgentProfileCategoryMode = \"host\" | \"profile\" | \"repo\" | \"none\";\nexport type AgentProfileFilesystemScope = \"repo\" | \"workspace\" | \"host\";\n\nexport interface AgentProfileAssetRef {\n source: \"relay\" | \"repo\" | \"inline\" | \"provider\";\n ref: string;\n enabled: boolean;\n provider?: AgentProfileProvider;\n meta?: Record<string, unknown>;\n}\n\nexport interface AgentProfileProviderOptions extends Record<string, unknown> {\n codex?: {\n toolOutputTokenLimit?: number | null;\n } & Record<string, unknown>;\n}\n\nexport interface AgentProfile {\n name: string;\n description?: string;\n provider?: AgentProfileProvider;\n base: AgentProfileBase;\n builtIn?: boolean;\n instructions: {\n system?: string;\n append: string[];\n repoInstructions: AgentProfileInstructionPolicy;\n globalInstructions: AgentProfileInstructionPolicy;\n };\n relay: {\n context: boolean;\n skills: boolean;\n plugins: boolean;\n statusLine: boolean;\n // Inject the relay HTTP MCP endpoint into the spawned agent. Default-on for\n // host/minimal; isolated clean-room profiles disable it like they disable\n // plugins. Optional so existing profile literals stay valid; the gate\n // (profileAllowsRelayFeature) treats absent as enabled and config-store\n // resolves a concrete default at write time.\n mcp?: boolean;\n };\n skills: AgentProfileAssetRef[];\n plugins: AgentProfileAssetRef[];\n mcp: {\n mode: AgentProfileCategoryMode;\n servers?: Record<string, unknown>;\n };\n hooks: {\n mode: AgentProfileCategoryMode;\n };\n permissions: {\n mode?: SpawnApprovalMode;\n filesystem: AgentProfileFilesystemScope;\n };\n env: Record<string, string>;\n providerOptions: AgentProfileProviderOptions;\n /** Spawn quota for agents granted this profile: max concurrent LIVE children they may\n * have at once. `0`/undefined = cannot spawn (the strict default). `N` = up to N live\n * children; a child exiting frees a slot. Drives both the minted token's `command:spawn`\n * scope (granted iff `> 0`) and the runtime quota count. Children never inherit it\n * (no grandchildren). */\n maxSpawnedAgents?: number;\n}\n\nexport type AgentProfileProjectionResult = \"applied\" | \"partial\" | \"unsupported\" | \"not-applicable\";\n\nexport interface AgentProfileProjectionEntry {\n capability: string;\n requested: string;\n result: AgentProfileProjectionResult;\n detail: string;\n}\n\nexport interface AgentProfileProjectionReport {\n profileName: string;\n provider: SpawnProvider;\n base: AgentProfileBase;\n generatedAt: number;\n entries: AgentProfileProjectionEntry[];\n warnings: string[];\n unsupported: string[];\n}\n\nexport interface SpawnPolicy {\n name: string;\n description?: string;\n // Desired on/off switch. `false` keeps the policy from (re)spawning even for\n // always-on mode, so a manual stop survives reconcile ticks. Defaults to true.\n enabled?: boolean;\n orchestratorId: string;\n cwd: string;\n provider: SpawnProvider;\n workspaceMode?: WorkspaceMode;\n rig?: string;\n model?: string;\n effort?: SpawnEffort;\n profile?: string;\n providerArgs: string[];\n prompt?: string;\n tags: string[];\n capabilities: string[];\n label?: string;\n mode: SpawnPolicyMode;\n permissionMode: SpawnApprovalMode;\n restartOnUpdate: boolean;\n scheduledDailyRestart: boolean;\n onDemand?: {\n keepaliveSeconds: number;\n idleDefinition: \"no-activity\";\n };\n backoff: {\n schedule: number[];\n resetAfterSeconds: number;\n };\n binding?: {\n type: \"channel\";\n channelId: string;\n };\n}\n\n/**\n * Global, provider-independent configuration for repo steward agents (issue #167).\n * Stewards are auto-provisioned per repo from these settings when one is needed.\n * Editable in the dashboard Settings → Stewards section. Disabled by default —\n * configuring a provider and enabling is the opt-in that also gates auto-spawn.\n */\nexport interface StewardConfig {\n /** When false, no steward is auto-provisioned or woken; the relay falls back to the legacy elected-steward ping. */\n enabled: boolean;\n /** Provider that runs the steward (e.g. claude, codex; future providers extend SpawnProvider). */\n provider: SpawnProvider;\n /** Optional model alias for the chosen provider (e.g. \"gpt-5.5\"). */\n model?: string;\n /** Optional reasoning effort. */\n effort?: SpawnEffort;\n /** Approval mode for the steward — needs merge/command authority, so defaults to \"open\". */\n permissionMode: SpawnApprovalMode;\n /** On-demand idle timeout before the steward is stopped when its queue drains. */\n keepaliveSeconds: number;\n}\n\n/**\n * Global workspace provisioning config for isolated worktrees (the dashboard \"Workspace\"\n * spawn option). A fresh git worktree only contains git-tracked files; node_modules are\n * symlinked from main automatically, and these additional untracked paths are too.\n */\nexport interface WorkspaceConfig {\n /**\n * Files or filename patterns to symlink from the main checkout into each isolated\n * worktree. Plain names (\"AGENTS.md\", \".claude-rig\") match files and directories;\n * entries containing glob metacharacters (*?[]{}) are expanded against main. A path\n * is only linked if it actually exists in main — missing entries are ignored.\n */\n symlinkPaths: string[];\n}\n\n/**\n * Continuous self-improvement (\"Insights\") module config — feature toggles for the\n * dogfooding flywheel (see docs/self-improvement.md, epic #183). Everything here is\n * opt-out: passive, read-only observation, so it defaults on. Each feature is\n * independently togglable so instrumentation can be disabled without losing the rest.\n */\nexport interface InsightsConfig {\n /** Master switch. When false, no signals are recorded regardless of per-feature flags. */\n enabled: boolean;\n /** #184 — server-side context-gathering ratio computed from the session trace at session end. */\n contextRatio: {\n enabled: boolean;\n };\n /** #185 — agent-authored end-of-session introspection artifact (3 fields), gated. */\n introspection: {\n enabled: boolean;\n /** Gate: skip sessions with fewer substantive turns than this (trivial work has nothing to learn). */\n minTurns: number;\n /** Gate: skip when remaining context fraction is below this (poor introspection = noise). 0–1. */\n minContextRemaining: number;\n };\n}\n\n/** Toggle for relay-driven lifecycle push notifications (#239 event bus). These wake\n * recipient agents, so they sit behind a switch the operator can flip off per-event. */\nexport interface NotificationsConfig {\n /** Master switch. When false, no lifecycle push notifications are sent. */\n enabled: boolean;\n /** #239 — push the author \"your branch landed\" + agents-on-main \"merged\" notices at land time. */\n branchLanded: boolean;\n}\n\n/** A single self-improvement datapoint. Generic by design: new instrumentation = new `signal`, not new schema. */\nexport interface InsightObservation {\n id: number;\n /** Session this datapoint belongs to (provider session id). */\n sessionId: string;\n /** Agent that produced the session, when known. */\n agentId?: string;\n /** Project/repo identity — baselines and deltas are scoped per-project. */\n project: string;\n /** Discriminator: which signal this row carries (e.g. \"context_ratio\", \"introspection\"). */\n signal: string;\n /** Signal-specific metric payload. */\n value: Record<string, unknown>;\n /** Paired outcome proxy (rework / operator-correction / success), kept alongside the metric — anti-Goodhart. */\n outcome?: Record<string, unknown>;\n /** Who computed it: \"server\" (relay, from trace) or \"agent\" (session-end hook). */\n source: \"server\" | \"agent\";\n createdAt: number;\n}\n\n/** Per-project + global rollups for the dashboard / API. */\nexport interface InsightsStats {\n signal: string;\n project: string | null; // null = global rollup across projects\n count: number;\n /** Mean of `value.ratio` where present (context_ratio); null for non-numeric signals. */\n avgRatio: number | null;\n lastAt: number | null;\n}\n\nexport interface Orchestrator {\n id: string;\n hostname: string;\n status: OrchestratorStatus;\n agentId: string; // relay agent id for messaging\n providers: SpawnProvider[];\n providerStatus?: ProviderStatusReport[];\n providerCatalog?: ProviderCatalogSummary[];\n baseDir: string;\n apiUrl?: string;\n envKeys: string[]; // names only, never values\n package?: RuntimePackageMetadata;\n contracts?: RuntimeContracts;\n capabilities?: RuntimeCapabilities;\n contractCompatibility?: ContractCompatibility;\n version?: string;\n protocolVersion?: number;\n gitSha?: string;\n health?: OrchestratorHealth;\n // Self-reported host supervision (how the orchestrator's own process is run),\n // used to target a remote self-upgrade at the right unit.\n supervisor?: \"process\" | \"systemd\" | \"launchd\" | \"unknown\";\n selfUnit?: string;\n runtimePrefix?: string;\n // In-flight / last remote upgrade state, reconciled by the relay against the\n // version the orchestrator reports after it restarts.\n upgrade?: OrchestratorUpgradeState;\n meta: Record<string, unknown>;\n managedAgents: ManagedAgent[];\n lastSeen: number;\n createdAt: number;\n}\n\nexport interface OrchestratorUpgradeState {\n desiredVersion: string;\n status: \"pending\" | \"succeeded\" | \"failed\";\n commandId?: string;\n providers?: string[];\n fromVersion?: string;\n requestedBy?: string;\n requestedAt: number;\n settledAt?: number;\n error?: string;\n}\n\nexport interface OrchestratorHealth {\n status: \"ok\" | \"warn\" | \"restart-required\" | \"upgrade-required\" | \"unknown\";\n restartRequired: boolean;\n upgradeRequired?: boolean;\n issues: Array<{\n code: \"missing-version\" | \"package-drift\" | \"missing-contract\" | \"protocol-mismatch\" | \"restart-required\" | \"upgrade-required\";\n detail: string;\n }>;\n}\n\nexport interface ManagedAgent {\n agentId: string;\n provider: SpawnProvider;\n model?: string;\n effort?: SpawnEffort;\n profile?: string;\n workspaceMode?: WorkspaceMode;\n workspace?: WorkspaceMetadata;\n sessionName?: string;\n supervisor?: \"process\" | \"systemd\" | \"launchd\" | \"unknown\";\n systemdUnit?: string;\n terminalSession?: string;\n terminalAvailable?: boolean;\n tmuxSession: string;\n cwd: string;\n label?: string;\n approvalMode: SpawnApprovalMode;\n policyName?: string;\n spawnRequestId?: string;\n automationRunId?: string;\n pid?: number;\n startedAt: number;\n}\n\nexport interface ManagedSessionExitDiagnostics {\n agentId: string;\n provider: SpawnProvider;\n workspaceMode?: WorkspaceMode;\n workspace?: WorkspaceMetadata;\n sessionName?: string;\n tmuxSession: string;\n cwd: string;\n label?: string;\n policyName?: string;\n spawnRequestId?: string;\n automationRunId?: string;\n supervisor: \"process\" | \"systemd\" | \"launchd\" | \"unknown\";\n systemdUnit?: string;\n terminalSession?: string;\n terminalAvailable?: boolean;\n pid?: number;\n currentPid?: number;\n startedAt: number;\n detectedAt: number;\n runtimeMs: number;\n logFile?: string;\n logBytes?: number;\n logEmpty?: boolean;\n logTail?: string[];\n runnerInfoFile?: string;\n runnerInfoPresent?: boolean;\n systemd?: {\n unit: string;\n activeState?: string;\n subState?: string;\n result?: string;\n execMainCode?: string;\n execMainStatus?: string;\n mainPid?: number;\n unavailable?: string;\n };\n unavailable?: string[];\n lastError: string;\n}\n\nexport interface RegisterOrchestratorInput {\n id: string;\n hostname: string;\n providers: SpawnProvider[];\n providerStatus?: ProviderStatusReport[];\n providerCatalog?: ProviderCatalogSummary[];\n baseDir: string;\n apiUrl?: string;\n envKeys?: string[];\n package?: RuntimePackageMetadata;\n contracts?: RuntimeContracts;\n capabilities?: RuntimeCapabilities;\n version?: string;\n protocolVersion?: number;\n gitSha?: string;\n meta?: Record<string, unknown>;\n}\n\nexport interface OrchestratorRuntimeInput {\n package?: RuntimePackageMetadata;\n contracts?: RuntimeContracts;\n capabilities?: RuntimeCapabilities;\n version?: string;\n protocolVersion?: number;\n gitSha?: string;\n providers?: SpawnProvider[];\n providerStatus?: ProviderStatusReport[];\n providerCatalog?: ProviderCatalogSummary[];\n}\n\ninterface OrchestratorSpawnInput {\n provider: SpawnProvider;\n model?: string;\n effort?: SpawnEffort;\n cwd?: string;\n workspaceMode?: WorkspaceMode;\n label?: string;\n approvalMode?: SpawnApprovalMode;\n prompt?: string;\n systemPromptAppend?: string;\n env?: Record<string, string>;\n}\n\nexport interface ProviderStatusReport {\n name: SpawnProvider;\n available: boolean;\n checkedAt: number;\n reason?: string;\n version?: string;\n features?: Record<string, boolean>;\n cli?: {\n command: string;\n path?: string;\n ok: boolean;\n version?: string;\n error?: string;\n };\n runner?: {\n command: string;\n path?: string;\n ok: boolean;\n version?: string;\n error?: string;\n };\n}\n\nexport interface ProviderCatalogSummary {\n provider: SpawnProvider;\n label: string;\n defaultModel?: string;\n models: Array<{\n alias: string;\n label: string;\n providerModel: string;\n efforts: SpawnEffort[];\n defaultEffort?: SpawnEffort;\n limits?: {\n contextWindowTokens?: {\n value: number;\n source: \"catalog\" | \"provider\" | \"runtime\" | \"override\";\n confidence: \"declared\" | \"verified\" | \"estimated\" | \"unknown\";\n lastUpdatedAt?: number;\n };\n maxOutputTokens?: {\n value: number;\n source: \"catalog\" | \"provider\" | \"runtime\" | \"override\";\n confidence: \"declared\" | \"verified\" | \"estimated\" | \"unknown\";\n lastUpdatedAt?: number;\n };\n };\n capabilities?: {\n modalities: {\n input: {\n text: boolean;\n image?: boolean;\n audio?: boolean;\n video?: boolean;\n pdf?: boolean;\n };\n output: {\n text: boolean;\n image?: boolean;\n audio?: boolean;\n video?: boolean;\n };\n };\n tools?: {\n code?: boolean;\n review?: boolean;\n debug?: boolean;\n refactor?: boolean;\n shell?: boolean;\n fileRead?: boolean;\n fileWrite?: boolean;\n webSearch?: boolean;\n imageGeneration?: boolean;\n imageEditing?: boolean;\n };\n source: \"catalog\" | \"provider\" | \"runtime\" | \"override\";\n confidence: \"declared\" | \"verified\" | \"estimated\" | \"unknown\";\n lastUpdatedAt?: number;\n };\n }>;\n}\n\ninterface OrchestratorSpawnResult {\n orchestratorId: string;\n provider: SpawnProvider;\n sessionName?: string;\n supervisor?: \"process\" | \"systemd\" | \"launchd\" | \"unknown\";\n systemdUnit?: string;\n terminalSession?: string;\n terminalAvailable?: boolean;\n tmuxSession: string;\n cwd: string;\n label?: string;\n approvalMode: SpawnApprovalMode;\n pid?: number;\n startedAt: number;\n}\n\nexport interface Recipe {\n name: string;\n description: string;\n version?: string;\n author?: string;\n agents: RecipeAgent[];\n workflow?: RecipeWorkflow;\n lifecycle?: RecipeLifecycle;\n}\n\nexport interface RecipeAgent {\n role: string;\n provider: \"claude\" | \"codex\";\n count?: number;\n capabilities: string[];\n label?: string;\n tags?: string[];\n memoryTags?: string[];\n approvalMode?: \"open\" | \"guarded\" | \"read-only\";\n prompt?: string;\n model?: string;\n effort?: SpawnEffort;\n env?: Record<string, string>;\n}\n\ninterface RecipeWorkflow {\n trigger?: string;\n fanOut?: \"all\" | \"first\";\n collect?: string;\n routing?: RecipeRoute[];\n}\n\ninterface RecipeRoute {\n pattern: string;\n pipeline: string[];\n}\n\ninterface RecipeLifecycle {\n mode?: \"persistent\" | \"ephemeral\";\n idleTimeoutMs?: number;\n memory?: RecipeMemoryPolicy;\n}\n\nexport interface RecipeMemoryPolicy {\n injectOnAssign?: boolean;\n autoCapture?: boolean;\n captureTypes?: MemoryType[];\n memoryTags?: string[];\n alwaysReload?: string[];\n scope?: string;\n maxTokens?: number;\n maxMemories?: number;\n priorityCutoff?: 1 | 2 | 3;\n ttlMs?: number;\n}\n\nexport interface RecipeInstance {\n id: string;\n recipeName: string;\n recipeSource: \"builtin\" | \"user\";\n cwd: string;\n orchestratorId: string;\n status: \"starting\" | \"running\" | \"stopping\" | \"stopped\" | \"failed\";\n agents: RecipeAgentInstance[];\n artifacts?: Artifact[];\n startedAt: number;\n stoppedAt?: number;\n startedBy: string;\n error?: string;\n}\n\nexport interface RecipeAgentInstance {\n role: string;\n agentId: string;\n provider: string;\n status: \"spawning\" | \"running\" | \"stopping\" | \"stopped\" | \"failed\";\n index?: number;\n}\n\nexport interface ComponentToken {\n sub: string;\n role: \"provider\" | \"channel\" | \"orchestrator\" | \"admin\" | \"dashboard\" | string;\n scope: string[];\n constraints?: TokenConstraints;\n iat: number;\n exp?: number;\n jti?: string;\n}\n\nexport interface TokenRecord {\n jti: string;\n sub: string;\n role: string;\n scope: string[];\n constraints?: TokenConstraints;\n profileId?: string;\n issuedAt: number;\n expiresAt?: number;\n revokedAt?: number;\n createdBy?: string;\n}\n\nexport interface TokenProfile {\n id: string;\n name: string;\n description?: string;\n role: string;\n scope: string[];\n constraints?: TokenConstraints;\n ttlSeconds?: number;\n builtIn: boolean;\n createdAt: number;\n updatedAt: number;\n createdBy?: string;\n}\n\nexport interface CreateTokenProfileInput {\n id?: string;\n name: string;\n description?: string;\n role: string;\n scope: string[];\n constraints?: TokenConstraints;\n ttlSeconds?: number;\n createdBy?: string;\n}\n\nexport type UpdateTokenProfileInput = Partial<Omit<CreateTokenProfileInput, \"id\">>;\n\nexport interface TokenConstraints {\n agents?: string[];\n policies?: string[];\n parentAgents?: string[];\n /** Parent agent id stamped on a child's runner token at spawn; read at registration to\n * set the child agent card's `spawnedBy` authoritatively (the child can't forge it). */\n spawnedBy?: string;\n /** Spawn quota (max concurrent live children) baked into a spawn-capable agent's token,\n * resolved from its profile's `maxSpawnedAgents`. The runtime quota check reads it here. */\n maxSpawnedAgents?: number;\n targets?: string[];\n channels?: string[];\n orchestrators?: string[];\n hosts?: string[];\n cwd?: string;\n cwdPrefixes?: string[];\n taskIds?: string[];\n memoryScopes?: string[];\n integrationNames?: string[];\n spawnRequestIds?: string[];\n terminalAttach?: boolean;\n logsRead?: boolean;\n canDelegate?: boolean;\n}\n\ntype TokenScope =\n | \"system:admin\"\n | \"token:read\"\n | \"token:write\"\n | \"agent:read\"\n | \"agent:write\"\n | \"message:read\"\n | \"message:send\"\n | \"task:read\"\n | \"task:write\"\n | \"command:read\"\n | \"command:write\"\n | \"command:*\"\n | \"artifact:read\"\n | \"artifact:write\"\n | \"artifact:admin\"\n | \"memory:read\"\n | \"memory:write\"\n | \"memory:admin\"\n | \"mcp:use\"\n | \"terminal:attach\"\n | \"logs:read\"\n | \"integration:read\"\n | \"integration:write\"\n | \"channel:read\"\n | \"channel:write\"\n | \"stats:read\"\n | \"health:read\"\n | \"events:read\"\n | \"command:spawn\"\n | \"command:shutdown\"\n | \"recipe:start\"\n | \"recipe:stop\"\n | \"admin:*\";\n\nexport interface MaintenanceJob {\n id: string;\n title: string;\n description?: string;\n intervalMs: number;\n timeoutMs: number;\n enabled: boolean;\n runOnStart: boolean;\n lastRunAt?: number;\n nextRunAt?: number;\n lastDurationMs?: number;\n lastStatus: \"idle\" | \"running\" | \"succeeded\" | \"failed\" | \"disabled\";\n lastError?: string;\n lastResult?: Record<string, unknown>;\n consecutiveFailures: number;\n running: boolean;\n leaseUntil?: number;\n updatedAt: number;\n}\n\nexport interface MaintenanceJobRun {\n id: string;\n status: \"succeeded\" | \"failed\" | \"skipped\";\n startedAt: number;\n finishedAt: number;\n durationMs: number;\n result?: Record<string, unknown>;\n error?: string;\n}\n\nexport interface HealthCheck {\n name: string;\n status: \"ok\" | \"warn\" | \"error\";\n detail?: string;\n count?: number;\n subjects?: Array<{ id: string; label?: string; status?: string; detail?: string }>;\n}\n\nexport interface HealthReport {\n status: \"ok\" | \"degraded\" | \"error\";\n version: string;\n generatedAt: number;\n checks: HealthCheck[];\n}\n\n// --- Shared micro-helpers (single source of truth; do not re-declare per file) ---\n\n/** True for a non-null, non-array object. The canonical type guard for the whole repo. */\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/**\n * Narrow `unknown` to a non-empty trimmed string, else `undefined`.\n * Settled semantics: whitespace-only is treated as empty (returns `undefined`).\n */\nexport function stringValue(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\n/** Extract a human-readable message from any thrown value. */\nexport function errMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\n// --- Relay connection defaults ---\n\n/** Default port the relay server listens on. */\nconst DEFAULT_RELAY_PORT = 4850;\n\n/** Default relay base URL. Loopback spelling settled on `127.0.0.1` (not `localhost`). */\nexport const DEFAULT_RELAY_URL = `http://127.0.0.1:${DEFAULT_RELAY_PORT}`;\n","import type {\n Agent, Message, PairSession, Task, ChannelSummary, ConnectorSummary,\n IntegrationSummary, Orchestrator, AgentType, PresenceInfo, PresenceBadge,\n AttentionInfo, InboxThread, HealthCheck, HealthDiagnostic, ManagedPolicyHealth,\n} from '@/types'\nimport { HUMAN_AGENT_ID, AGENT_TYPE_ICONS, AGENT_TYPE_TITLES, WAITING_TASK_STATUSES } from './constants'\nimport { isRecord } from 'agent-relay-sdk/types'\n\nexport function toTimestamp(value: unknown): number {\n const ts = typeof value === 'number' ? value : new Date((value as string) || 0).getTime()\n return Number.isFinite(ts) ? ts : 0\n}\n\nconst INTERNAL_CAP_PREFIXES = ['lifecycle.', 'commands.', 'capabilities.']\n\nconst REACTION_EMOJI_ALIASES: Record<string, string> = {\n '+1': '👍',\n ':+1:': '👍',\n thumbsup: '👍',\n ':thumbsup:': '👍',\n 'thumbs-up': '👍',\n ':thumbs-up:': '👍',\n thumbs_up: '👍',\n ':thumbs_up:': '👍',\n thumb_up: '👍',\n ':thumb_up:': '👍',\n like: '👍',\n ':like:': '👍',\n heart: '❤️',\n ':heart:': '❤️',\n redheart: '❤️',\n red_heart: '❤️',\n ':red_heart:': '❤️',\n check: '✅',\n ':check:': '✅',\n checkmark: '✅',\n ':checkmark:': '✅',\n white_check_mark: '✅',\n ':white_check_mark:': '✅',\n eyes: '👀',\n ':eyes:': '👀',\n}\n\nexport function normalizeReactionEmoji(value: string): string {\n const trimmed = value.trim()\n return REACTION_EMOJI_ALIASES[trimmed.toLowerCase()] ?? trimmed\n}\n\nexport function userFacingCapabilities(caps: string[]): string[] {\n return caps.filter((c) => !INTERNAL_CAP_PREFIXES.some((p) => c.startsWith(p)))\n}\n\nexport function shortPath(cwd: string | undefined | null, segments = 2): string {\n if (!cwd) return ''\n const parts = cwd.replace(/\\/+$/, '').split('/')\n return parts.length <= segments ? cwd : parts.slice(-segments).join('/')\n}\n\nexport function normalizePathForCompare(path: string): string {\n return path.trim().replace(/\\\\/g, '/').replace(/\\/+$/, '') || '/'\n}\n\nexport function pathWithinBase(path: string | undefined | null, baseDir: string | undefined | null): boolean {\n if (!path?.trim() || !baseDir?.trim()) return false\n const normalizedPath = normalizePathForCompare(path)\n const normalizedBase = normalizePathForCompare(baseDir)\n if (normalizedBase === '/') return normalizedPath.startsWith('/')\n if (normalizedPath === '/') return false\n return normalizedPath === normalizedBase || normalizedPath.startsWith(normalizedBase + '/')\n}\n\nfunction isBuiltInAgent(agent: Agent | null | undefined): boolean {\n return agent?.meta?.builtin === true || agent?.id === HUMAN_AGENT_ID || agent?.id === 'system'\n}\n\nfunction isChannelAgent(agent: Agent | null | undefined): boolean {\n const tags = agent?.tags || []\n const caps = agent?.capabilities || []\n const policyName = typeof agent?.meta?.policyName === 'string' ? agent.meta.policyName : ''\n return agent?.kind === 'channel' ||\n agent?.meta?.kind === 'channel' ||\n tags.includes('channel') ||\n tags.some((tag) => tag.startsWith('channel:')) ||\n caps.includes('channel') ||\n caps.some((cap) => cap.startsWith('channel:')) ||\n tags.includes('telegram') ||\n caps.includes('telegram') ||\n policyName === 'telegram-main'\n}\n\nfunction isAutomationAgent(agent: Agent | null | undefined): boolean {\n return Boolean(\n typeof agent?.meta?.automationId === 'string' ||\n typeof agent?.meta?.automationRunId === 'string' ||\n agent?.tags?.includes('automation')\n )\n}\n\nfunction isSystemAgent(agent: Agent | null | undefined): boolean {\n return isBuiltInAgent(agent) || isChannelAgent(agent) || isAutomationAgent(agent)\n}\n\nexport function agentType(agent: Agent | null | undefined): AgentType {\n if (agent?.id === HUMAN_AGENT_ID) return 'user'\n if (agent?.id === 'system') return 'system'\n if (isChannelAgent(agent)) return 'channel'\n\n const values = [\n ...(agent?.tags || []),\n agent?.meta?.provider as string,\n agent?.meta?.client as string,\n agent?.meta?.runtime as string,\n agent?.meta?.agentType as string,\n agent?.id,\n agent?.name,\n ]\n .filter((v): v is string => typeof v === 'string')\n .map((v) => v.toLowerCase())\n\n if (values.some((v) => v.includes('codex'))) return 'codex'\n if (values.some((v) => v.includes('claude'))) return 'claude'\n return 'agent'\n}\n\nfunction agentTypeIcon(agent: Agent | null | undefined): string {\n return AGENT_TYPE_ICONS[agentType(agent)] ?? AGENT_TYPE_ICONS.agent ?? 'ti-robot'\n}\n\nfunction agentTypeTitle(agent: Agent | null | undefined): string {\n return AGENT_TYPE_TITLES[agentType(agent)] ?? AGENT_TYPE_TITLES.agent ?? 'Agent'\n}\n\nexport function isAgentStale(now: number, agent: Agent | null | undefined): boolean {\n if (!agent?.lastSeen || agent.status === 'offline') return false\n if (agent.id === 'user' || agent.id === 'system') return false\n // A live bus connection is authoritative liveness: an idle-but-connected agent\n // is reachable, not stale, even if lastSeen has aged (e.g. heartbeat keepalive\n // gaps while idle). Genuine death flips transport.connected to false (or the\n // server reaps the agent offline), at which point the lastSeen check applies.\n const transport = agent.meta?.transport\n if (isRecord(transport) && transport.connected === true) return false\n const lastSeenMs = new Date(agent.lastSeen as string).getTime()\n if (!Number.isFinite(lastSeenMs)) return false\n return now - lastSeenMs > 60_000\n}\n\nexport type AgentControlAction = 'restart' | 'shutdown' | 'reconnect' | 'compact' | 'clearContext' | 'resume' | 'interrupt'\n\nexport function agentSupportsControlAction(agent: Agent | null | undefined, action: AgentControlAction): boolean {\n if (!agent || isBuiltInAgent(agent) || isChannelAgent(agent)) return false\n if (action === 'resume') return false\n // Interrupt is provider-independent (Claude ESC, Codex turn-interrupt) and only\n // meaningful while the agent is actually mid-turn.\n if (action === 'interrupt') return agent.providerCapabilities?.liveSession?.interrupt === true && agent.status === 'busy'\n const needsClaudeTmux = agentType(agent) === 'claude' && (action === 'restart' || action === 'shutdown' || action === 'compact' || action === 'clearContext')\n if (needsClaudeTmux && !agentHasTmuxSession(agent)) return false\n const lifecycle = agent.providerCapabilities?.lifecycle\n if (lifecycle) {\n if (action === 'restart') return lifecycle.restartHard === true\n if (action === 'shutdown') return lifecycle.shutdownHard === true\n if (action === 'reconnect') return lifecycle.reconnect === true\n }\n if (action === 'compact') return agent.providerCapabilities?.context?.compact === true\n if (action === 'clearContext') return agent.providerCapabilities?.context?.clear === true\n return agent.meta?.runnerManaged === true && (action === 'restart' || action === 'shutdown')\n}\n\nfunction agentHasTmuxSession(agent: Agent): boolean {\n return typeof agent.meta?.tmuxSession === 'string' && agent.meta.tmuxSession.trim().length > 0\n}\n\nfunction agentSupportsControlActions(agent: Agent | null | undefined): boolean {\n return agentSupportsControlAction(agent, 'restart') ||\n agentSupportsControlAction(agent, 'shutdown') ||\n agentSupportsControlAction(agent, 'reconnect') ||\n agentSupportsControlAction(agent, 'compact') ||\n agentSupportsControlAction(agent, 'clearContext')\n}\n\nexport function agentCanBeForgotten(agent: Agent | null | undefined): boolean {\n return Boolean(agent && !isBuiltInAgent(agent) && !isChannelAgent(agent) && !agentSupportsControlActions(agent) && (agent.status === 'offline' || agent.status === 'stale'))\n}\n\nfunction isRetiredManagedRuntimeAgent(agent: Agent, managedPolicies: ManagedPolicyHealth[]): boolean {\n if (agent.status !== 'offline') return false\n const policyName = typeof agent.meta?.policyName === 'string' ? agent.meta.policyName : ''\n if (!policyName) return false\n const policy = managedPolicies.find((item) => item.policy.name === policyName)\n return Boolean(policy?.state.agentId && policy.state.agentId !== agent.id)\n}\n\nexport function visibleAgents(agents: Agent[], showBuiltIns: boolean, managedPolicies: ManagedPolicyHealth[] = []): Agent[] {\n const activeRuntimeAgents = agents.filter((a) => !isRetiredManagedRuntimeAgent(a, managedPolicies))\n return showBuiltIns ? activeRuntimeAgents : activeRuntimeAgents.filter((a) => !isSystemAgent(a))\n}\n\nexport function displayName(agent: Agent | null | undefined): string {\n if (!agent) return '?'\n return agent.label || agent.name || agent.id.slice(-12)\n}\n\nexport function displayTarget(target: string, agentsById: Record<string, Agent>): string {\n if (!target) return '?'\n if (target === 'broadcast') return 'broadcast'\n if (target.startsWith('tag:')) return '#' + target.slice(4)\n if (target.startsWith('cap:')) return target.slice(4)\n if (target.startsWith('label:')) return target.slice(6)\n const agent = agentsById[target]\n return agent ? displayName(agent) : target.slice(-8)\n}\n\nexport function messageBody(msg: Message | null | undefined): string {\n if (!msg) return ''\n const payload = msg.payload || {}\n const channelMessage = payload.message as Record<string, unknown> | undefined\n if (channelMessage && typeof channelMessage === 'object' && typeof channelMessage.text === 'string' && (channelMessage.text as string).trim()) {\n return channelMessage.text as string\n }\n const interaction = payload.interaction as Record<string, unknown> | undefined\n if (interaction && typeof interaction === 'object') {\n const title = typeof interaction.title === 'string' ? interaction.title.trim() : ''\n const description = typeof interaction.description === 'string' ? interaction.description.trim() : ''\n if (title && description) return title + '\\n' + description\n if (title) return title\n if (description) return description\n }\n const reaction = payload.reaction as Record<string, unknown> | undefined\n if (reaction && typeof reaction === 'object') {\n const name = typeof reaction.name === 'string' ? reaction.name : ''\n const emoji = typeof reaction.emoji === 'string' ? reaction.emoji : ''\n const value = typeof reaction.value === 'string' ? reaction.value : ''\n const displayEmoji = normalizeReactionEmoji(emoji || name || value)\n return ['Reaction', displayEmoji].filter(Boolean).join(': ')\n }\n const activity = payload.activity as Record<string, unknown> | undefined\n if (activity && typeof activity === 'object') {\n const kind = typeof activity.kind === 'string' ? activity.kind : 'activity'\n const state = typeof activity.state === 'string' ? activity.state : ''\n return [kind, state].filter(Boolean).join(' ')\n }\n if (typeof payload.text === 'string' && (payload.text as string).trim()) return payload.text as string\n if (typeof payload.message === 'string' && (payload.message as string).trim()) return payload.message as string\n return extractTextFromJson(msg.body || '') ?? (msg.body || '')\n}\n\nexport function isReactionEventMessage(msg: Message | null | undefined): boolean {\n if (!msg?.payload) return false\n const event = msg.payload.event as Record<string, unknown> | undefined\n return msg.payload.reactionNotification === true || event?.type === 'message.reaction'\n}\n\ninterface ParsedJsonBody {\n text: string\n meta: Array<[string, string]>\n raw: Record<string, unknown>\n}\n\nexport function parseJsonBody(body: string): ParsedJsonBody | null {\n if (!body) return null\n const trimmed = body.trim()\n if (!trimmed.startsWith('{')) return null\n try {\n const obj = JSON.parse(trimmed)\n if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) return null\n const text = typeof obj.text === 'string' ? obj.text\n : typeof obj.message === 'string' ? obj.message\n : null\n if (!text?.trim()) return null\n const skipKeys = new Set(['text', 'message'])\n const meta = Object.entries(obj)\n .filter(([k, v]) => !skipKeys.has(k) && (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean'))\n .map(([k, v]) => [k, String(v)] as [string, string])\n return { text, meta, raw: obj }\n } catch {\n return null\n }\n}\n\nfunction extractTextFromJson(body: string): string | null {\n const parsed = parseJsonBody(body)\n return parsed?.text ?? null\n}\n\nexport function messagePreview(msg: Message | null | undefined): string {\n const text = msg?.subject || messageBody(msg) || ''\n return text.length > 90 ? text.slice(0, 90) + '...' : text\n}\n\nexport function inboxPeer(msg: Message): string {\n if (msg.from === HUMAN_AGENT_ID && msg.to) return msg.to\n if (msg.to === HUMAN_AGENT_ID && msg.from) return msg.from\n return ''\n}\n\nexport function isHumanInboundMessage(msg: Message): boolean {\n return msg.to === HUMAN_AGENT_ID && msg.from !== HUMAN_AGENT_ID\n}\n\n// Session \"reasoning\" and \"tool\" events stream into the chat timeline as\n// display-only activity steps — they are not user-facing messages and must not\n// inflate unread badges. Mirrors sessionActivityStep() in views/chat.tsx.\nexport function isSessionActivityStep(msg: Message): boolean {\n if (msg.kind !== 'session') return false\n const type = (msg.payload?.session as { type?: string } | undefined)?.type\n return type === 'reasoning' || type === 'tool'\n}\n\nfunction isUnreadHumanMessage(readCursors: Record<string, number>, peer: string, msg: Message): boolean {\n if (!isHumanInboundMessage(msg)) return false\n if ((msg.readBy || []).includes(HUMAN_AGENT_ID)) return false\n const cursor = Number(readCursors[peer] || 0)\n return msg.id > (Number.isFinite(cursor) ? cursor : 0)\n}\n\nfunction maxMessageId(messages: Message[], predicate: (msg: Message) => boolean): number {\n let max = 0\n for (const msg of messages) {\n if (predicate(msg) && msg.id > max) max = msg.id\n }\n return max\n}\n\nexport function isClaimableTaskWaiting(task: Task): boolean {\n return WAITING_TASK_STATUSES.has(task.status) && !task.claimedBy\n}\n\nexport function isClaimableMessageWaiting(msg: Message): boolean {\n return Boolean(msg.claimable && !msg.claimedBy && !(msg.kind === 'task' && Number.isSafeInteger((msg.payload as Record<string, unknown>)?.taskId)))\n}\n\nexport function dashboardTargetMatchesAgent(target: string, agent: Agent): boolean {\n if (!target || !agent) return false\n if (target === 'broadcast' || target === agent.id) return true\n if (target.startsWith('tag:')) return (agent.tags || []).includes(target.slice(4))\n if (target.startsWith('cap:')) return (agent.capabilities || []).includes(target.slice(4))\n if (target.startsWith('label:')) return agent.label === target.slice(6)\n return false\n}\n\nexport function messageMatchesChannel(message: Message, channel: ChannelSummary): boolean {\n if (!message || !channel) return false\n const channelKeys = [channel.id, channel.type, ...(channel.topicChannels || [])].filter(Boolean)\n if (message.channel && channelKeys.includes(message.channel)) return true\n const payloadChannel = message.payload?.channel as Record<string, unknown> | undefined\n if (payloadChannel && typeof payloadChannel === 'object') {\n const keys = [payloadChannel.agentId, payloadChannel.provider, payloadChannel.accountId].filter(Boolean) as string[]\n if (keys.includes(channel.id) || keys.includes(channel.type) || (channel.accountId && keys.includes(channel.accountId))) return true\n }\n if (channel.agentId && (message.from === channel.agentId || message.to === channel.agentId)) return true\n return false\n}\n\ninterface ActiveSubagentInfo {\n id: string\n label: string\n role?: string\n startedAt?: number\n}\n\ninterface ProviderRuntimeState {\n state: string\n reason?: string\n label: string\n recommendedAction?: string\n source?: string\n raw?: unknown\n updatedAt?: number\n pendingApproval?: ProviderPendingApproval\n}\n\ninterface ProviderPendingApprovalChoice {\n id: 'approve' | 'approve-session' | 'deny' | 'abort'\n label: string\n}\n\ninterface ProviderQuestionOption {\n label: string\n description?: string\n}\n\nexport interface ProviderQuestion {\n question: string\n header?: string\n options: ProviderQuestionOption[]\n multiSelect?: boolean\n}\n\nexport interface ProviderPendingApproval {\n id: string\n provider?: string\n kind?: string\n title: string\n body: string\n choices: ProviderPendingApprovalChoice[]\n // Present when kind === 'questions' (Claude AskUserQuestion). The dashboard\n // renders these as a form and submits an 'answer' decision.\n questions?: ProviderQuestion[]\n}\n\nexport function providerRuntimeState(agent: Agent | null | undefined): ProviderRuntimeState | null {\n const raw = agent?.meta?.providerState\n if (!isRecord(raw) || typeof raw.state !== 'string') return null\n return {\n state: raw.state,\n reason: typeof raw.reason === 'string' ? raw.reason : undefined,\n label: typeof raw.label === 'string' && raw.label ? raw.label : raw.state,\n recommendedAction: typeof raw.recommendedAction === 'string' ? raw.recommendedAction : undefined,\n source: typeof raw.source === 'string' ? raw.source : undefined,\n raw: raw.raw,\n updatedAt: typeof raw.updatedAt === 'number' ? raw.updatedAt : undefined,\n pendingApproval: providerPendingApproval(raw.pendingApproval),\n }\n}\n\nfunction providerPendingApproval(value: unknown): ProviderPendingApproval | undefined {\n if (!isRecord(value) || typeof value.id !== 'string') return undefined\n const choices = Array.isArray(value.choices)\n ? value.choices.filter(isRecord).map((choice) => ({\n id: choice.id,\n label: typeof choice.label === 'string' ? choice.label : String(choice.id || ''),\n })).filter((choice): choice is ProviderPendingApprovalChoice =>\n (choice.id === 'approve' || choice.id === 'approve-session' || choice.id === 'deny' || choice.id === 'abort') && Boolean(choice.label)\n )\n : []\n const questions = Array.isArray(value.questions)\n ? value.questions.filter(isRecord).map((q): ProviderQuestion => ({\n question: typeof q.question === 'string' ? q.question : '',\n header: typeof q.header === 'string' ? q.header : undefined,\n multiSelect: q.multiSelect === true,\n options: Array.isArray(q.options)\n ? q.options.filter(isRecord).map((o) => ({\n label: typeof o.label === 'string' ? o.label : String(o.label ?? ''),\n description: typeof o.description === 'string' ? o.description : undefined,\n })).filter((o) => Boolean(o.label))\n : [],\n })).filter((q) => Boolean(q.question) && q.options.length > 0)\n : undefined\n return {\n id: value.id,\n provider: typeof value.provider === 'string' ? value.provider : undefined,\n kind: typeof value.kind === 'string' ? value.kind : undefined,\n title: typeof value.title === 'string' && value.title ? value.title : 'Permission request',\n body: typeof value.body === 'string' ? value.body : '',\n choices,\n ...(questions && questions.length ? { questions } : {}),\n }\n}\n\nexport function providerBlockedState(agent: Agent | null | undefined): ProviderRuntimeState | null {\n const state = providerRuntimeState(agent)\n return state?.state === 'blocked' ? state : null\n}\n\nexport function activeSubagents(agent: Agent | null | undefined): ActiveSubagentInfo[] {\n const raw = agent?.meta?.activeSubagents\n if (!Array.isArray(raw)) {\n const count = typeof agent?.meta?.activeSubagentCount === 'number' ? agent.meta.activeSubagentCount : 0\n return count > 0 ? Array.from({ length: count }, (_, index) => ({ id: `subagent-${index + 1}`, label: `Subagent ${index + 1}` })) : []\n }\n return raw\n .filter(isRecord)\n .map((item, index) => {\n const id = typeof item.id === 'string' && item.id ? item.id : `subagent-${index + 1}`\n const role = typeof item.role === 'string' && item.role ? item.role : undefined\n const label = typeof item.label === 'string' && item.label ? item.label : role || id\n const startedAt = typeof item.startedAt === 'number' ? item.startedAt : undefined\n return { id, label, role, startedAt }\n })\n}\n\nexport function agentPresence(\n now: number, agent: Agent | null | undefined, attention: AttentionInfo, pair: PairSession | null\n): PresenceInfo {\n const lifecycleAction = typeof agent?.meta?.lifecycleAction === 'string' ? agent.meta.lifecycleAction : ''\n const stale = isAgentStale(now, agent)\n const reconnecting = agent?.status !== 'offline' && !agent?.ready && stale\n const starting = agent?.status !== 'offline' && !agent?.ready && !stale\n const unreadIdle = attention.unread > 0 && agent?.status !== 'busy'\n const blocked = providerBlockedState(agent)\n const badges = presenceBadges(agent, attention, pair)\n\n if (agent?.status === 'offline') return { label: 'offline', tone: 'secondary', icon: 'PlugZap', stale: false, reconnecting: false, badges }\n // Pre-session-destroy window (#183): the runner is running end-of-session work before a\n // compact/clear/restart/shutdown. Distinct from plain \"busy\" and non-addressable.\n if (lifecycleAction.startsWith('finalizing-')) {\n return { label: `wrapping up (${lifecycleAction.slice('finalizing-'.length)})`, tone: 'warning', icon: 'Hourglass', stale, reconnecting, badges }\n }\n if (lifecycleAction === 'shutting-down') return { label: 'shutting down', tone: 'warning', icon: 'Power', stale, reconnecting, badges }\n if (lifecycleAction === 'killing') return { label: 'killing', tone: 'danger', icon: 'Power', stale, reconnecting, badges }\n if (lifecycleAction === 'restarting') return { label: 'restarting', tone: 'warning', icon: 'RefreshCw', stale, reconnecting, badges }\n if (agent?.status === 'stale') return { label: 'stale', tone: 'danger', icon: 'RefreshCw', stale: true, reconnecting: true, badges }\n if (reconnecting) return { label: 'reconnecting', tone: 'danger', icon: 'RefreshCw', stale, reconnecting, badges }\n if (starting) return { label: 'online, not ready', tone: 'warning', icon: 'Loader', stale, reconnecting, badges }\n if (blocked) return { label: `blocked: ${blocked.label}`, tone: 'danger', icon: 'AlertCircle', stale, reconnecting, badges }\n if (agent?.status === 'busy') return { label: 'busy in turn', tone: 'warning', icon: 'Play', stale, reconnecting, badges }\n if (pair?.status === 'active') return { label: 'paired', tone: 'success', icon: 'Link', stale, reconnecting, badges }\n if (unreadIdle) return { label: 'idle, unread', tone: 'danger', icon: 'Bell', stale, reconnecting, badges }\n return { label: agent?.status === 'idle' ? 'idle' : 'ready', tone: 'success', icon: 'CircleCheck', stale, reconnecting, badges }\n}\n\nfunction presenceBadges(agent: Agent | null | undefined, attention: AttentionInfo, pair: PairSession | null): PresenceBadge[] {\n const badges: PresenceBadge[] = []\n const blocked = providerBlockedState(agent)\n const subagentCount = activeSubagents(agent).length\n if (blocked) badges.push({ label: blocked.reason ? `blocked: ${blocked.reason}` : 'blocked', className: 'bg-red-500/20 text-red-400' })\n if (subagentCount) badges.push({ label: subagentCount === 1 ? '1 subagent' : subagentCount + ' subagents', className: 'bg-cyan-500/20 text-cyan-300' })\n if (pair?.status === 'active') badges.push({ label: 'paired', className: 'bg-emerald-500/20 text-emerald-400' })\n if (pair?.status === 'pending') badges.push({ label: 'pair invite', className: 'bg-yellow-500/20 text-yellow-400' })\n if (attention.unread) badges.push({ label: attention.unread + ' unread', className: 'bg-red-500/20 text-red-400' })\n if (attention.claimableTasks) badges.push({ label: attention.claimableTasks + ' claimable', className: 'bg-orange-500/20 text-orange-400' })\n return badges\n}\n\nexport function emptyAttention(): AttentionInfo {\n return { unread: 0, needsHumanResponse: false, pendingPairInvite: false, claimableTasks: 0, total: 0, score: 0 }\n}\n\nexport function channelIsReady(channel: ChannelSummary | null): boolean {\n if (!channel || channel.status === 'offline') return false\n if (channel.targetHealth?.status === 'ok') return true\n if (channel.targetHealth?.status === 'error' || channel.targetHealth?.status === 'warning') return false\n return channel.ready\n}\n\nexport function channelPresence(channel: ChannelSummary | null): PresenceInfo {\n if (!channel) return { label: 'unknown', tone: 'secondary', icon: 'PlugZap', badges: [] }\n if (channel.targetHealth?.status === 'error') return { label: 'target broken', tone: 'danger', icon: 'AlertTriangle', badges: [] }\n if (channel.targetHealth?.status === 'warning') return { label: 'target warning', tone: 'warning', icon: 'AlertCircle', badges: [] }\n if (channel.status === 'offline') return { label: 'offline', tone: 'secondary', icon: 'PlugZap', badges: [] }\n if (channel.status === 'busy') return { label: 'busy', tone: 'warning', icon: 'Activity', badges: [] }\n if (!channelIsReady(channel)) return { label: 'not ready', tone: 'warning', icon: 'Loader', badges: [] }\n return { label: 'ready', tone: 'success', icon: 'CircleCheck', badges: [] }\n}\n\nexport function connectorPresence(connector: ConnectorSummary): PresenceInfo {\n const runtime = connector?.runtime || {}\n if (runtime.status === 'error') return { label: 'error', tone: 'danger', icon: 'AlertTriangle', badges: [] }\n if (runtime.status === 'warn') return { label: 'warning', tone: 'warning', icon: 'AlertCircle', badges: [] }\n if (runtime.running) return { label: 'running', tone: 'success', icon: 'CircleCheck', badges: [] }\n if (runtime.enabled === false) return { label: 'disabled', tone: 'secondary', icon: 'Pause', badges: [] }\n if (runtime.status === 'ok') return { label: 'ok', tone: 'success', icon: 'CircleCheck', badges: [] }\n return { label: 'unknown', tone: 'secondary', icon: 'HelpCircle', badges: [] }\n}\n\nexport function integrationPresence(integration: IntegrationSummary): PresenceInfo {\n const stats = integration?.taskStats || {}\n if ((stats.waitingTasks || 0) > 0) return { label: 'waiting', tone: 'warning', icon: 'AlertCircle', badges: [] }\n if ((stats.openTasks || 0) > 0) return { label: 'active', tone: 'info', icon: 'Activity', badges: [] }\n if (!integration?.configured) return { label: 'observed only', tone: 'secondary', icon: 'Eye', badges: [] }\n if (integration.observed) return { label: 'quiet', tone: 'success', icon: 'CircleCheck', badges: [] }\n return { label: 'configured', tone: 'primary', icon: 'PlugZap', badges: [] }\n}\n\nexport function orchestratorHealthLabel(orch: Orchestrator): string {\n const health = orch?.health\n if (!health || health.status === 'ok') return orch?.version || orch?.package?.version ? 'compatible' : 'unknown version'\n if (health.status === 'upgrade-required') return 'upgrade required'\n if (health.status === 'restart-required') return 'restart required'\n if (health.status === 'unknown') return 'unknown contract'\n if ((health.issues || []).some((i) => i.code === 'protocol-mismatch')) return 'protocol mismatch'\n if ((health.issues || []).some((i) => i.code === 'package-drift')) return 'package drift'\n if (health.restartRequired) return 'restart required'\n return health.status\n}\n\nexport function orchestratorHealthClass(orch: Orchestrator): string {\n const status = orch?.health?.status\n if (status === 'upgrade-required') return 'text-red-400 bg-red-500/10'\n if (status === 'warn' || status === 'restart-required') return 'text-yellow-400 bg-yellow-500/10'\n if (status === 'unknown') return 'text-zinc-400 bg-zinc-500/10'\n return 'text-emerald-400 bg-emerald-500/10'\n}\n\nexport function timeAgo(now: number, iso: string | number | null | undefined): string {\n if (!iso) return ''\n const ts = new Date(iso as string).getTime()\n if (!Number.isFinite(ts)) return ''\n const diff = Math.max(0, (now - ts) / 1000)\n if (diff < 60) return Math.floor(diff) + 's ago'\n if (diff < 3600) return Math.floor(diff / 60) + 'm ago'\n if (diff < 86400) return Math.floor(diff / 3600) + 'h ago'\n return Math.floor(diff / 86400) + 'd ago'\n}\n\nexport function fmtTime(iso: string | number | null | undefined): string {\n if (!iso) return ''\n return new Date(iso as string).toLocaleString()\n}\n\nexport function safeFilename(value: string): string {\n return String(value || 'export').replace(/[^a-z0-9._-]+/gi, '-').replace(/^-+|-+$/g, '').slice(0, 80) || 'export'\n}\n\nexport function healthDiagnostics(checks: HealthCheck[]): HealthDiagnostic[] {\n return checks.filter((c) => c.status !== 'ok').map((check) => {\n const base: HealthDiagnostic = {\n name: check.name,\n status: check.status,\n detail: check.detail || check.name,\n impact: healthImpact(check),\n subjects: check.subjects || [],\n actions: [\n { label: 'Inspect logs', icon: 'FileSearch', copy: 'agent-relay daemon logs' },\n { label: 'Restart daemon', icon: 'RefreshCw', copy: 'agent-relay daemon restart' },\n { label: 'Copy env', icon: 'Copy', copy: 'agent-relay doctor' },\n ],\n }\n if (check.name === 'stale-live-agents') {\n base.actions.unshift(\n { label: 'Run reaper', icon: 'Eraser', api: 'POST', path: '/system/reap' },\n { label: 'Show stale', icon: 'Filter', view: 'agents', preset: 'offline_stale' },\n )\n } else if (check.name === 'channel-delivery-targets') {\n base.actions.unshift(\n { label: 'Open channels', icon: 'MessagesSquare', view: 'channels' },\n { label: 'Run reaper', icon: 'Eraser', api: 'POST', path: '/system/reap' },\n )\n } else if (['expired-message-claims', 'expired-task-claims', 'offline-claimed-tasks'].includes(check.name)) {\n base.actions.unshift(\n { label: 'Run reaper', icon: 'Eraser', api: 'POST', path: '/system/reap' },\n { label: 'Open work', icon: 'ListChecks', view: 'work' },\n )\n }\n return base\n })\n}\n\nfunction healthImpact(check: HealthCheck): string {\n const impacts: Record<string, string> = {\n 'database': 'Relay persistence is unavailable; messages, state, and audit writes may fail.',\n 'stale-live-agents': 'Agents may look online even though their heartbeat has stopped.',\n 'expired-message-claims': 'Claimable messages may be stuck until the reaper releases expired claims.',\n 'expired-task-claims': 'Tasks can appear owned by agents that no longer hold a live lease.',\n 'offline-claimed-tasks': 'Offline agents are still shown as owners for active work.',\n 'channel-delivery-targets': 'Inbound channel messages may be accepted but routed to no live delivery agent.',\n }\n return impacts[check.name] || 'Relay health is degraded for this check.'\n}\n\nfunction toneToBadgeVariant(tone: string): 'default' | 'secondary' | 'destructive' | 'outline' {\n if (tone === 'danger' || tone === 'destructive') return 'destructive'\n if (tone === 'secondary') return 'secondary'\n return 'default'\n}\n\nexport function toneToColor(tone: string): string {\n const map: Record<string, string> = {\n success: 'text-emerald-400',\n warning: 'text-yellow-400',\n danger: 'text-red-400',\n info: 'text-blue-400',\n primary: 'text-blue-400',\n secondary: 'text-zinc-400',\n }\n return map[tone] || 'text-zinc-400'\n}\n\nfunction toneToBg(tone: string): string {\n const map: Record<string, string> = {\n success: 'bg-emerald-500',\n warning: 'bg-yellow-500',\n danger: 'bg-red-500',\n info: 'bg-blue-500',\n primary: 'bg-blue-500',\n secondary: 'bg-zinc-500',\n }\n return map[tone] || 'bg-zinc-500'\n}\n\nexport function statusDotColor(agent: Agent | null, now?: number): string {\n if (!agent) return 'bg-zinc-500 opacity-50'\n if (agent.status === 'offline') return 'bg-zinc-500 opacity-50'\n if (typeof agent.meta?.lifecycleAction === 'string' && agent.meta.lifecycleAction.startsWith('finalizing-')) return 'bg-amber-500 animate-pulse'\n if (agent.meta?.lifecycleAction === 'shutting-down' || agent.meta?.lifecycleAction === 'restarting') return 'bg-yellow-500 animate-pulse'\n if (agent.meta?.lifecycleAction === 'killing') return 'bg-red-500 animate-pulse'\n if (providerBlockedState(agent)) return 'bg-red-500 animate-pulse'\n if (agent.status === 'busy') return 'bg-yellow-500'\n if (now && isAgentStale(now, agent)) return 'bg-red-500 animate-pulse'\n if (!agent.ready) return 'bg-emerald-500 animate-pulse'\n return 'bg-emerald-500 shadow-[0_0_6px_theme(colors.emerald.500)]'\n}\n\nexport function downloadText(filename: string, text: string, type: string) {\n if (typeof document === 'undefined' || typeof URL === 'undefined' || typeof Blob === 'undefined') return\n const url = URL.createObjectURL(new Blob([text], { type }))\n const link = document.createElement('a')\n link.href = url\n link.download = filename\n link.click()\n URL.revokeObjectURL(url)\n}\n"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  import { emitRelayEvent } from "./events";
2
2
  import { getNotificationsConfig } from "./config-store";
3
3
  import { notifySystemMessage } from "./notify";
4
- import { listAgents } from "./db";
4
+ import { createActivityEvent, listAgents, updateWorkspaceStatus } from "./db";
5
5
  import { isAgentOnline } from "./agent-ref";
6
- import type { AgentCard, WorkspaceRecord } from "./types";
6
+ import type { AgentCard, WorkspaceMergePreview, WorkspaceRecord } from "./types";
7
7
 
8
8
  export interface BranchLandedInput {
9
9
  /**
@@ -113,6 +113,42 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
113
113
  }
114
114
  }
115
115
 
116
+ /**
117
+ * Finalize a workspace whose work landed in base out-of-band — a pr-strategy land whose
118
+ * PR merged (by anyone, incl. `gh pr merge`), or a squash/cherry-pick already in base
119
+ * (#304). Marks it terminal `merged`, records the merge SHA, and fires the same
120
+ * branch.landed push the local land path emits — a pr land never runs through the
121
+ * workspace.merge result handler, so without this the author never learns it shipped.
122
+ * Lives here (not maintenance.ts) so the giant doesn't grow and land-notify has one home.
123
+ */
124
+ export function reconcileLandedWorkspace(ws: WorkspaceRecord, preview: WorkspaceMergePreview): void {
125
+ const sha = typeof preview.prMergeSha === "string" ? preview.prMergeSha : undefined;
126
+ const via = preview.prMerged === true ? "pr" : "git";
127
+ updateWorkspaceStatus(ws.id, "merged", {
128
+ autoMerged: true,
129
+ mergedFromStatus: ws.status,
130
+ landedDetectedAt: Date.now(),
131
+ landedVia: via,
132
+ ...(sha ? { landedSha: sha } : {}),
133
+ autoConflict: false,
134
+ });
135
+ createActivityEvent({
136
+ clientId: "server-workspace-" + ws.id + "-merged-" + Date.now(),
137
+ kind: "state",
138
+ title: "Workspace work landed in base",
139
+ body: `${ws.branch ?? ws.id} is ${via === "pr" ? "merged on the remote (PR)" : "already merged into base"} ${preview.baseRef ? `(${preview.baseRef})` : ""} — marking merged`,
140
+ meta: ws.branch ?? ws.id,
141
+ icon: "ti-git-merge",
142
+ view: "orchestrators",
143
+ metadata: { source: "server", maintenanceJobId: "workspace-conflict-scan", workspaceId: ws.id, fromStatus: ws.status, ...(sha ? { landedSha: sha } : {}) },
144
+ });
145
+ try {
146
+ notifyBranchLanded({ workspace: ws, mergedSha: sha, pushed: true });
147
+ } catch {
148
+ // Notification is best-effort; the merged status + activity event still stand.
149
+ }
150
+ }
151
+
116
152
  // An agent is "on `main`" when its registered cwd equals the repo's main checkout — i.e. it
117
153
  // works in the base, not an isolated worktree. Excludes the author, pseudo agents (system/
118
154
  // user), channels, and offline sessions.
package/src/cli.ts CHANGED
@@ -1617,7 +1617,7 @@ async function handleWorkspaceCommand(args: string[]): Promise<void> {
1617
1617
  throw new Error(WORKSPACE_USAGE);
1618
1618
  }
1619
1619
 
1620
- let id = currentWorkspaceId();
1620
+ let id = currentWorkspaceId(), idExplicit = false; // idExplicit: --id was passed, not the ambient default (#307)
1621
1621
  let strategy: string | undefined;
1622
1622
  let purpose: string | undefined;
1623
1623
  let repo: string | undefined;
@@ -1628,7 +1628,7 @@ async function handleWorkspaceCommand(args: string[]): Promise<void> {
1628
1628
  let timeoutSeconds: number | undefined;
1629
1629
  for (let i = 1; i < args.length; i++) {
1630
1630
  const arg = args[i];
1631
- if (arg === "--id" && i + 1 < args.length) id = args[++i];
1631
+ if (arg === "--id" && i + 1 < args.length) { id = args[++i]; idExplicit = true; }
1632
1632
  else if (arg === "--strategy" && i + 1 < args.length) strategy = args[++i];
1633
1633
  else if (arg === "--purpose" && i + 1 < args.length) purpose = args[++i];
1634
1634
  else if (arg === "--repo" && i + 1 < args.length) repo = args[++i];
@@ -1651,7 +1651,7 @@ async function handleWorkspaceCommand(args: string[]): Promise<void> {
1651
1651
  }
1652
1652
 
1653
1653
  if (action === "cleanup-stale") {
1654
- const result = await apiRequest("POST", "/api/workspaces/actions/cleanup-stale", { repoRoot: repo, dryRun: !execute });
1654
+ const result = await apiRequest("POST", "/api/workspaces/actions/cleanup-stale", { repoRoot: repo, dryRun: !execute, ...(idExplicit && id ? { workspaceId: id } : {}) });
1655
1655
  console.log(JSON.stringify(result, null, 2));
1656
1656
  return;
1657
1657
  }
@@ -33,6 +33,7 @@ import {
33
33
  } from "./db";
34
34
  import type { WorkspaceMergePreview, WorkspaceRecord, WorkspaceStatus } from "./types";
35
35
  import { requestWorkspaceMerge } from "./workspace-merge";
36
+ import { reconcileLandedWorkspace } from "./branch-landed";
36
37
  import { workspaceActiveClaim } from "./workspace-claim";
37
38
  import { reapOrphanedWorktrees } from "./workspace-orphans";
38
39
  import { deriveBranchState, READY_TO_LAND_STATUSES, TERMINAL_WORKSPACE_STATUSES } from "./workspace-phase";
@@ -434,8 +435,11 @@ function workspacePathWithinBase(path: string | undefined, baseDir: string | und
434
435
  return rel === "" || (!!rel && !rel.startsWith("..") && !isAbsolute(rel));
435
436
  }
436
437
 
437
- async function fetchHostMergePreview(apiUrl: string, workspace: WorkspaceRecord): Promise<WorkspaceMergePreview | { available: false } | null> {
438
- const query = new URLSearchParams({ path: workspace.worktreePath, checkPr: "1" });
438
+ async function fetchHostMergePreview(apiUrl: string, workspace: WorkspaceRecord, opts: { checkPr?: boolean } = {}): Promise<WorkspaceMergePreview | { available: false } | null> {
439
+ // `checkPr` costs a `gh` round-trip, so the caller opts in only where PR-merge ground
440
+ // truth is actionable (reconcile/land candidates), not for every badge-probe (#304).
441
+ const checkPr = opts.checkPr !== false;
442
+ const query = new URLSearchParams({ path: workspace.worktreePath, ...(checkPr ? { checkPr: "1" } : {}) });
439
443
  if (workspace.baseRef) query.set("baseRef", workspace.baseRef);
440
444
  if (workspace.baseSha) query.set("baseSha", workspace.baseSha);
441
445
  const headers: Record<string, string> = {};
@@ -577,10 +581,12 @@ async function scanWorkspaceConflicts(): Promise<Record<string, unknown>> {
577
581
  for (const ws of candidates) {
578
582
  const orch = orchestrators.find((candidate) => workspacePathWithinBase(ws.sourceCwd, candidate.baseDir));
579
583
  if (!orch?.apiUrl) continue;
580
- const preview = await fetchHostMergePreview(orch.apiUrl, ws);
584
+ // Spend the `gh` PR-state round-trip only on reconcile-eligible rows; active/ready
585
+ // are excluded from reconcile, so their scan stays git-only (#304).
586
+ const preview = await fetchHostMergePreview(orch.apiUrl, ws, { checkPr: LANDED_RECONCILE_STATUSES.has(ws.status) });
581
587
  if (!preview || (preview as { available?: false }).available === false) continue;
582
588
  const p = preview as WorkspaceMergePreview;
583
- if (p.error || p.missing || p.conflict === undefined) continue;
589
+ if (p.error || p.missing) continue;
584
590
 
585
591
  const meta = ws.metadata as Record<string, unknown>;
586
592
 
@@ -605,29 +611,23 @@ async function scanWorkspaceConflicts(): Promise<Record<string, unknown>> {
605
611
  // merge_planned forever otherwise, and the conflict scan can even pin a
606
612
  // landed branch to `conflict`). Reconcile to the terminal `merged` status so
607
613
  // the dashboard stops showing it as unmerged and GC prunes it on schedule.
614
+ // This runs BEFORE the conflict-undefined skip below: a PR merged via a regular
615
+ // merge commit makes the branch an ancestor (ahead=0 → no-op → conflict comes
616
+ // back undefined), which the skip would otherwise drop, stranding the row at
617
+ // merge_planned — the exact #304 stall.
618
+ // Reconcile + finalize (record SHA, fire branch.landed) in branch-landed.ts so the
619
+ // giant doesn't grow (#291) and land-notify stays single-homed (#304).
608
620
  const landed = p.landed === true || p.prMerged === true;
609
621
  if (landed && LANDED_RECONCILE_STATUSES.has(ws.status)) {
610
- updateWorkspaceStatus(ws.id, "merged", {
611
- autoMerged: true,
612
- mergedFromStatus: ws.status,
613
- landedDetectedAt: Date.now(),
614
- landedVia: p.prMerged === true ? "pr" : "git",
615
- autoConflict: false,
616
- });
622
+ reconcileLandedWorkspace(ws, p);
617
623
  merged.push(ws.id);
618
- createActivityEvent({
619
- clientId: "server-workspace-" + ws.id + "-merged-" + Date.now(),
620
- kind: "state",
621
- title: "Workspace work landed in base",
622
- body: `${ws.branch ?? ws.id} is ${p.prMerged === true ? "merged on the remote (PR)" : "already merged into base"} ${p.baseRef ? `(${p.baseRef})` : ""} — marking merged`,
623
- meta: ws.branch ?? ws.id,
624
- icon: "ti-git-merge",
625
- view: "orchestrators",
626
- metadata: { source: "server", maintenanceJobId: "workspace-conflict-scan", workspaceId: ws.id, fromStatus: ws.status },
627
- });
628
624
  continue;
629
625
  }
630
626
 
627
+ // Past here we act on the conflict signal — skip when the host couldn't assess it
628
+ // (undefined): never flag/clear a conflict on incomplete data.
629
+ if (p.conflict === undefined) continue;
630
+
631
631
  if (p.conflict === true && ws.status !== "conflict") {
632
632
  updateWorkspaceStatus(ws.id, "conflict", {
633
633
  autoConflict: true,
package/src/mcp.ts CHANGED
@@ -45,7 +45,7 @@ import {
45
45
  isIntegrationAllowed,
46
46
  } from "./security";
47
47
  import type { ActivityKind, AgentCard, ArtifactKind, ArtifactSensitivity, AttachmentRef, Command, SendMessageInput, Message, SpawnApprovalMode, SpawnProvider, WorkspaceMergeStrategy, WorkspaceRecord } from "./types";
48
- import { applyWorkspaceAction, waitForWorkspaceStatus, type WorkspaceAction } from "./workspace-actions";
48
+ import { LAND_STRATEGIES, applyWorkspaceAction, waitForWorkspaceStatus, type WorkspaceAction } from "./workspace-actions";
49
49
  import { describeWorkspacePhase, landReceipt, readyContract, worktreeMcpInstructions } from "./workspace-phase";
50
50
  import { type ProviderEffort } from "agent-relay-sdk/provider-catalog";
51
51
  import { errMessage, isRecord, SPAWN_PROVIDERS, APPROVAL_MODES, VALID_EFFORTS } from "agent-relay-sdk";
@@ -1011,7 +1011,7 @@ function relayWorkspaceMutation(auth: McpAuthContext, action: WorkspaceAction, a
1011
1011
  action,
1012
1012
  agentId: callerAgentId(auth) ?? auth.actor,
1013
1013
  detail: optionalString(args.detail, "detail", 4000),
1014
- strategy: action === "merge" ? (optionalEnum(args.strategy, "strategy", ["pr", "rebase-ff", "auto"] as const) as WorkspaceMergeStrategy | undefined) : undefined,
1014
+ strategy: action === "merge" ? (optionalEnum(args.strategy, "strategy", LAND_STRATEGIES) as WorkspaceMergeStrategy | undefined) : undefined,
1015
1015
  deleteBranch: action === "merge" ? optionalBoolean(args.deleteBranch, "deleteBranch") : undefined,
1016
1016
  prTitle: optionalString(args.prTitle, "prTitle", 240),
1017
1017
  prBody: optionalString(args.prBody, "prBody", 8000),
@@ -7,7 +7,7 @@ import { WORKSPACE_ACTIONS, applyWorkspaceAction, buildWorkspaceCleanupCommand,
7
7
  import { auditEvent, authAuditMetadata, authorizeRoute, emitCommand, error, json, parseBody, type Handler } from "./_shared";
8
8
  import { collectWorkspaceOrphans } from "../workspace-orphans";
9
9
  import { createCommand } from "../commands-db";
10
- import { isOwnerAlive, withOwnerOnline } from "../workspace-merge";
10
+ import { LAND_STRATEGIES, isOwnerAlive, withOwnerOnline } from "../workspace-merge";
11
11
  import { isPathWithinBase } from "../utils";
12
12
  import { resolve } from "node:path";
13
13
  import { type WorkspaceDiagnostics, type WorkspaceGitState, type WorkspaceMergeStrategy, type WorkspaceRecord, type WorkspaceStatus } from "../types";
@@ -220,12 +220,18 @@ export const postWorkspaceCleanupStale: Handler = async (req) => {
220
220
  if (!parsed.ok) return error(parsed.error, parsed.status);
221
221
  const body = isRecord(parsed.body) ? parsed.body : {};
222
222
  const repoRoot = cleanString(body.repoRoot, "repoRoot", { max: 1000 });
223
+ // Scope the sweep to a single workspace when the caller named one (#307). Without
224
+ // this, `--id` was silently ignored and a sweep scoped to one id went repo-wide —
225
+ // a destructive-action footgun. Now an explicit id narrows the candidate set.
226
+ const workspaceId = cleanString(body.workspaceId, "workspaceId", { max: 160 });
223
227
  const dryRun = body.dryRun !== false; // safe by default
224
228
  const landedOnly = body.landedOnly !== false;
225
229
  const offlineOwnerOnly = body.offlineOwnerOnly !== false;
226
230
 
227
231
  const candidates = listWorkspaces().filter((ws) =>
228
- ws.mode === "isolated" && Boolean(ws.worktreePath) && !TERMINAL_WORKSPACE_STATUSES.has(ws.status) && (!repoRoot || ws.repoRoot === repoRoot),
232
+ ws.mode === "isolated" && Boolean(ws.worktreePath) && !TERMINAL_WORKSPACE_STATUSES.has(ws.status)
233
+ && (!repoRoot || ws.repoRoot === repoRoot)
234
+ && (!workspaceId || ws.id === workspaceId),
229
235
  );
230
236
 
231
237
  const rows: Array<Record<string, unknown>> = [];
@@ -259,7 +265,7 @@ export const postWorkspaceCleanupStale: Handler = async (req) => {
259
265
  }
260
266
  rows.push(row);
261
267
  }
262
- return json({ dryRun, landedOnly, offlineOwnerOnly, repoRoot, scanned: candidates.length, eligible: rows.filter((r) => r.safe).length, cleaned, candidates: rows }, dryRun ? 200 : 202);
268
+ return json({ dryRun, landedOnly, offlineOwnerOnly, repoRoot, ...(workspaceId ? { workspaceId } : {}), scanned: candidates.length, eligible: rows.filter((r) => r.safe).length, cleaned, candidates: rows }, dryRun ? 200 : 202);
263
269
  };
264
270
 
265
271
  export const postWorkspaceAction: Handler = async (req, params) => {
@@ -313,7 +319,10 @@ export const postWorkspaceAction: Handler = async (req, params) => {
313
319
  agentId,
314
320
  detail: cleanString(parsed.body.detail, "detail", { max: 4000 }),
315
321
  metadata: cleanMeta(parsed.body.metadata) ?? {},
316
- strategy: optionalEnum(parsed.body.strategy, "strategy", ["pr", "rebase-ff", "auto"] as const, "auto") as WorkspaceMergeStrategy,
322
+ // No inline fallback: an absent strategy stays undefined and the shared core
323
+ // (requestWorkspaceMerge) applies DEFAULT_MERGE_STRATEGY, identically to the
324
+ // MCP land tool — one resolution point, no per-surface drift (#304).
325
+ strategy: optionalEnum(parsed.body.strategy, "strategy", LAND_STRATEGIES) as WorkspaceMergeStrategy | undefined,
317
326
  deleteBranch: typeof parsed.body.deleteBranch === "boolean" ? parsed.body.deleteBranch : undefined,
318
327
  force: parsed.body.force === true,
319
328
  prTitle: cleanString(parsed.body.prTitle, "prTitle", { max: 240 }),
@@ -20,6 +20,9 @@ import {
20
20
  } from "./db";
21
21
  import { emitActivityEvent } from "./sse";
22
22
  import { isOwnerAlive, requestWorkspaceMerge } from "./workspace-merge";
23
+ // Re-export the land-strategy contract so the MCP land tool validates against the same
24
+ // tuple as the HTTP route without a second import hop (one source: workspace-merge, #304).
25
+ export { LAND_STRATEGIES, DEFAULT_MERGE_STRATEGY } from "./workspace-merge";
23
26
  import { claimMetadataPatch, workspaceActiveClaim } from "./workspace-claim";
24
27
  import { TERMINAL_WORKSPACE_STATUSES } from "./workspace-phase";
25
28
  import type { Command, WorkspaceMergeStrategy, WorkspaceRecord, WorkspaceStatus } from "./types";
@@ -161,7 +164,10 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
161
164
  if (action === "merge") {
162
165
  const result = requestWorkspaceMerge(workspace, {
163
166
  requestedBy: agentId ?? "dashboard",
164
- strategy: input.strategy ?? "auto",
167
+ // Pass the strategy through verbatim (undefined when unset) — the single
168
+ // default lives in requestWorkspaceMerge, so both land surfaces resolve it
169
+ // identically instead of each applying its own fallback (#304).
170
+ strategy: input.strategy,
165
171
  deleteBranch: input.deleteBranch !== false,
166
172
  prTitle: input.prTitle,
167
173
  prBody: input.prBody,
@@ -10,6 +10,17 @@ import {
10
10
  import type { Command, WorkspaceMergeStrategy, WorkspaceRecord } from "./types";
11
11
  import { isPathWithinBase } from "./utils";
12
12
 
13
+ // One home for the land-strategy contract, shared by BOTH land surfaces — the HTTP
14
+ // route (`POST /api/workspaces/:id/actions`, driven by `agent-relay workspace land`)
15
+ // and the `relay_workspace_land` MCP tool. They validate against the same tuple and
16
+ // fall back to the same default, so the effective strategy can't drift by surface
17
+ // (#304: the CLI used to omit the strategy and let the server default while MCP
18
+ // passed its own — two encodings of one rule). The default is applied ONCE, in
19
+ // `requestWorkspaceMerge` below; callers pass `strategy` through verbatim (undefined
20
+ // when unset) so there is a single resolution point, not one per entrypoint.
21
+ export const LAND_STRATEGIES = ["pr", "rebase-ff", "auto"] as const;
22
+ export const DEFAULT_MERGE_STRATEGY: WorkspaceMergeStrategy = "auto";
23
+
13
24
  interface RequestWorkspaceMergeOptions {
14
25
  /** Who asked for the merge (lease holder + audit). e.g. an agent id, "dashboard", "auto-merge". */
15
26
  requestedBy: string;
@@ -103,7 +114,7 @@ export function requestWorkspaceMerge(workspace: WorkspaceRecord, opts: RequestW
103
114
  branch: workspace.branch,
104
115
  baseRef: workspace.baseRef,
105
116
  baseSha: workspace.baseSha,
106
- strategy: opts.strategy ?? "auto",
117
+ strategy: opts.strategy ?? DEFAULT_MERGE_STRATEGY,
107
118
  deleteBranch,
108
119
  push: opts.push !== false,
109
120
  prTitle: opts.prTitle,