agent-relay-server 0.119.3 → 0.119.7

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.
Files changed (98) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +5 -4
  3. package/public/assets/{MessageBubble-DbaaemH3.js → MessageBubble-Azda2GIf.js} +2 -2
  4. package/public/assets/{MessageBubble-DbaaemH3.js.map → MessageBubble-Azda2GIf.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-C_hnK9yH.js → PlanGraphPanel-B9TN_KQr.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-C_hnK9yH.js.map → PlanGraphPanel-B9TN_KQr.js.map} +1 -1
  7. package/public/assets/{activity-Cbw43afW.js → activity-B0hz4XPy.js} +2 -2
  8. package/public/assets/{activity-Cbw43afW.js.map → activity-B0hz4XPy.js.map} +1 -1
  9. package/public/assets/{agent-profiles-BZZpTAZn.js → agent-profiles-BF0oSOTq.js} +2 -2
  10. package/public/assets/{agent-profiles-BZZpTAZn.js.map → agent-profiles-BF0oSOTq.js.map} +1 -1
  11. package/public/assets/{agent-type-icon-2YWBWhYC.js → agent-type-icon-BEz393ra.js} +2 -2
  12. package/public/assets/{agent-type-icon-2YWBWhYC.js.map → agent-type-icon-BEz393ra.js.map} +1 -1
  13. package/public/assets/{agents-nvHlibqo.js → agents-CW4--j3Q.js} +2 -2
  14. package/public/assets/{agents-nvHlibqo.js.map → agents-CW4--j3Q.js.map} +1 -1
  15. package/public/assets/{analytics-B9bbDReP.js → analytics-CmSHiDAI.js} +2 -2
  16. package/public/assets/{analytics-B9bbDReP.js.map → analytics-CmSHiDAI.js.map} +1 -1
  17. package/public/assets/{automation-CjtLgS3h.js → automation-S6_bQ2t-.js} +2 -2
  18. package/public/assets/{automation-CjtLgS3h.js.map → automation-S6_bQ2t-.js.map} +1 -1
  19. package/public/assets/{branch-state-badge-DRFIs9q0.js → branch-state-badge-Db6uU7yT.js} +2 -2
  20. package/public/assets/{branch-state-badge-DRFIs9q0.js.map → branch-state-badge-Db6uU7yT.js.map} +1 -1
  21. package/public/assets/{channels-CL00bLDh.js → channels-trEI29j0.js} +2 -2
  22. package/public/assets/{channels-CL00bLDh.js.map → channels-trEI29j0.js.map} +1 -1
  23. package/public/assets/{chat-DKf_9fgB.js → chat-hoFhhvgy.js} +2 -2
  24. package/public/assets/{chat-DKf_9fgB.js.map → chat-hoFhhvgy.js.map} +1 -1
  25. package/public/assets/{connectors-QYtScfNz.js → connectors-DNV138gK.js} +2 -2
  26. package/public/assets/{connectors-QYtScfNz.js.map → connectors-DNV138gK.js.map} +1 -1
  27. package/public/assets/{display-CP3jEw1H.js → display-exQDWum3.js} +2 -2
  28. package/public/assets/{display-CP3jEw1H.js.map → display-exQDWum3.js.map} +1 -1
  29. package/public/assets/{formatted-body-BnSiFCc3.js → formatted-body-BHm145gP.js} +3 -3
  30. package/public/assets/{formatted-body-BnSiFCc3.js.map → formatted-body-BHm145gP.js.map} +1 -1
  31. package/public/assets/{formatted-body-impl-Cnchy-Iv.js → formatted-body-impl-CvE31j9F.js} +2 -2
  32. package/public/assets/{formatted-body-impl-Cnchy-Iv.js.map → formatted-body-impl-CvE31j9F.js.map} +1 -1
  33. package/public/assets/{index-kelZvgcV.js → index-TamtghSr.js} +6 -6
  34. package/public/assets/{index-kelZvgcV.js.map → index-TamtghSr.js.map} +1 -1
  35. package/public/assets/{insights-Ccz5zFIQ.js → insights-Ci9ftVfl.js} +2 -2
  36. package/public/assets/{insights-Ccz5zFIQ.js.map → insights-Ci9ftVfl.js.map} +1 -1
  37. package/public/assets/{integrations-BICEazmP.js → integrations-COnkRfWW.js} +2 -2
  38. package/public/assets/{integrations-BICEazmP.js.map → integrations-COnkRfWW.js.map} +1 -1
  39. package/public/assets/{maintenance-DpYnmrVo.js → maintenance-bVhNbr33.js} +2 -2
  40. package/public/assets/{maintenance-DpYnmrVo.js.map → maintenance-bVhNbr33.js.map} +1 -1
  41. package/public/assets/{managed-agents-qQtr531p.js → managed-agents-D5HO4dhI.js} +2 -2
  42. package/public/assets/{managed-agents-qQtr531p.js.map → managed-agents-D5HO4dhI.js.map} +1 -1
  43. package/public/assets/{markdown-preview-impl-DhkWrgsW.js → markdown-preview-impl-DO-p83dz.js} +2 -2
  44. package/public/assets/{markdown-preview-impl-DhkWrgsW.js.map → markdown-preview-impl-DO-p83dz.js.map} +1 -1
  45. package/public/assets/{memory-AsWF2gfa.js → memory-D72o8umQ.js} +2 -2
  46. package/public/assets/{memory-AsWF2gfa.js.map → memory-D72o8umQ.js.map} +1 -1
  47. package/public/assets/{messages-D0vT-ovM.js → messages-5L_Bc_9w.js} +2 -2
  48. package/public/assets/{messages-D0vT-ovM.js.map → messages-5L_Bc_9w.js.map} +1 -1
  49. package/public/assets/{orchestrators-Oo8U_GW2.js → orchestrators-CnfgkQpl.js} +2 -2
  50. package/public/assets/{orchestrators-Oo8U_GW2.js.map → orchestrators-CnfgkQpl.js.map} +1 -1
  51. package/public/assets/{overview-UTAK7pAB.js → overview-Bu39NIyR.js} +2 -2
  52. package/public/assets/{overview-UTAK7pAB.js.map → overview-Bu39NIyR.js.map} +1 -1
  53. package/public/assets/{pairs-TZYezwEi.js → pairs-WuamP5W5.js} +2 -2
  54. package/public/assets/{pairs-TZYezwEi.js.map → pairs-WuamP5W5.js.map} +1 -1
  55. package/public/assets/{projects-ClgQ10Al.js → projects-Dd8gYzcn.js} +2 -2
  56. package/public/assets/{projects-ClgQ10Al.js.map → projects-Dd8gYzcn.js.map} +1 -1
  57. package/public/assets/{prompt-settings-Tia685AV.js → prompt-settings-BCrQreU6.js} +2 -2
  58. package/public/assets/{prompt-settings-Tia685AV.js.map → prompt-settings-BCrQreU6.js.map} +1 -1
  59. package/public/assets/{registry-CAXZWvzr.js → registry-kHKn0xwg.js} +2 -2
  60. package/public/assets/{registry-CAXZWvzr.js.map → registry-kHKn0xwg.js.map} +1 -1
  61. package/public/assets/{security-BDAMCUzN.js → security-Dk5t3r_K.js} +2 -2
  62. package/public/assets/{security-BDAMCUzN.js.map → security-Dk5t3r_K.js.map} +1 -1
  63. package/public/assets/{select-ytVYIxZP.js → select-BdsZQfU2.js} +2 -2
  64. package/public/assets/{select-ytVYIxZP.js.map → select-BdsZQfU2.js.map} +1 -1
  65. package/public/assets/{settings-CnalH4Zd.js → settings-EpjzQHnZ.js} +2 -2
  66. package/public/assets/{settings-CnalH4Zd.js.map → settings-EpjzQHnZ.js.map} +1 -1
  67. package/public/assets/{store-BDCUnpt5.js → store-OLe8gFh0.js} +2 -2
  68. package/public/assets/{store-BDCUnpt5.js.map → store-OLe8gFh0.js.map} +1 -1
  69. package/public/assets/{tasks-CHGO51En.js → tasks-CedMQHbg.js} +2 -2
  70. package/public/assets/{tasks-CHGO51En.js.map → tasks-CedMQHbg.js.map} +1 -1
  71. package/public/assets/{teams-DDL-I-pP.js → teams-BlHBo5Qt.js} +2 -2
  72. package/public/assets/{teams-DDL-I-pP.js.map → teams-BlHBo5Qt.js.map} +1 -1
  73. package/public/assets/{terminal-viewer-impl-CnXpHy1R.js → terminal-viewer-impl-gGqYpsjq.js} +2 -2
  74. package/public/assets/{terminal-viewer-impl-CnXpHy1R.js.map → terminal-viewer-impl-gGqYpsjq.js.map} +1 -1
  75. package/public/assets/types-uVOXgvMT.js +2 -0
  76. package/public/assets/types-uVOXgvMT.js.map +1 -0
  77. package/public/assets/{user-avatar-Cw7jcQY-.js → user-avatar-DuwCFrER.js} +2 -2
  78. package/public/assets/{user-avatar-Cw7jcQY-.js.map → user-avatar-DuwCFrER.js.map} +1 -1
  79. package/public/assets/{work-queue-BwX92bVS.js → work-queue-D7opMqtU.js} +2 -2
  80. package/public/assets/{work-queue-BwX92bVS.js.map → work-queue-D7opMqtU.js.map} +1 -1
  81. package/public/assets/{workspaces-DSsvsOFa.js → workspaces-M3xEMNXu.js} +2 -2
  82. package/public/assets/{workspaces-DSsvsOFa.js.map → workspaces-M3xEMNXu.js.map} +1 -1
  83. package/public/index.html +6 -6
  84. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  85. package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +2 -2
  86. package/runner/src/adapter.ts +1 -0
  87. package/runner/src/config.ts +6 -0
  88. package/src/commands-db.ts +72 -20
  89. package/src/db/workspaces.ts +41 -0
  90. package/src/provider-config-store.ts +2 -0
  91. package/src/routes/workspaces.ts +2 -4
  92. package/src/services/shutdown-agent.ts +6 -1
  93. package/src/services/spawn-agent.ts +50 -7
  94. package/src/services/transient-agent-reaper.ts +33 -19
  95. package/src/settings/registry.ts +2 -0
  96. package/src/workspace-actions.ts +24 -13
  97. package/public/assets/types-8ThujPP2.js +0 -2
  98. package/public/assets/types-8ThujPP2.js.map +0 -1
@@ -26,19 +26,25 @@ interface CommandFilters {
26
26
  limit?: number;
27
27
  }
28
28
 
29
- const ACTIVE_STATUSES: CommandStatus[] = ["pending", "accepted", "running"];
29
+ const CLAIMED_ACTIVE_STATUSES: CommandStatus[] = ["accepted", "running"];
30
+ const SPAWN_IN_FLIGHT_STATUSES: CommandStatus[] = ["pending", "accepted", "running"];
30
31
  const TERMINAL_STATUSES: CommandStatus[] = ["succeeded", "failed", "canceled", "timed_out"];
31
32
 
32
- // #930 — an `agent.spawn` command carries a hard TTL from CREATION (see
33
- // defaultExpiresAt). The #880 concurrency cap introduces REAL queuing, so a
34
- // legitimately-queued spawn can sit `accepted`/`running` in the orchestrator past
35
- // that create-time TTL and be swept to `timed_out` while the orchestrator still
36
- // intends to run it. Refresh the TTL from dispatch-intent (claim → accepted, or
37
- // slot-acquire running) so the window measures from when the host committed to
38
- // the spawn, not from enqueue. Scoped to `agent.spawn`; every other command type
39
- // keeps create-time TTL semantics.
33
+ // #930/#953command TTLs are execution leases, not delivery deadlines. The #880
34
+ // spawn cap and long inline orchestrator commands both introduce legitimate
35
+ // queuing before dispatch, so pending commands must not be swept before the host
36
+ // can claim them. Refresh expiring command types at dispatch-intent (claim
37
+ // accepted, or slot-acquire running) so the window measures from when the host
38
+ // committed to doing the work, not from enqueue.
40
39
  const SPAWN_TTL_MS = 180_000;
41
- const SPAWN_DISPATCH_STATUSES: CommandStatus[] = ["accepted", "running"];
40
+ const LIFECYCLE_TTL_MS = 30_000;
41
+ const DISPATCH_STATUSES: CommandStatus[] = ["accepted", "running"];
42
+
43
+ function commandTtlMs(type: string): number | undefined {
44
+ if (type === "agent.spawn") return SPAWN_TTL_MS;
45
+ if (type === "agent.shutdown" || type === "agent.restart" || type === "agent.reconnect" || type === "agent.kill" || type === "agent.compact" || type === "agent.clearContext" || type === "agent.injectContext") return LIFECYCLE_TTL_MS;
46
+ return undefined;
47
+ }
42
48
 
43
49
  function isTerminalStatus(status: CommandStatus): boolean {
44
50
  return TERMINAL_STATUSES.includes(status);
@@ -123,6 +129,51 @@ export function listCommands(filters: CommandFilters = {}): Command[] {
123
129
  return rows.map(rowToCommand);
124
130
  }
125
131
 
132
+ export function findSpawnCommandByRequestId(spawnRequestId: string, statuses?: CommandStatus[]): Command | null {
133
+ const cleaned = spawnRequestId.trim();
134
+ if (!cleaned) return null;
135
+ const statusClause = statuses?.length
136
+ ? `AND status IN (${statuses.map(() => "?").join(",")})`
137
+ : "";
138
+ const rows = getDb()
139
+ .query(`
140
+ SELECT * FROM commands
141
+ WHERE type = 'agent.spawn'
142
+ ${statusClause}
143
+ AND (correlation_id = ? OR json_extract(params, '$.spawnRequestId') = ?)
144
+ ORDER BY created_at DESC
145
+ LIMIT 1
146
+ `)
147
+ .all(...(statuses ?? []), cleaned, cleaned) as CommandRow[];
148
+ return rows[0] ? rowToCommand(rows[0]) : null;
149
+ }
150
+
151
+ export function countInFlightSpawnCommands(parentId: string): number {
152
+ const row = getDb()
153
+ .query(
154
+ `SELECT count(*) AS n
155
+ FROM commands
156
+ WHERE type = 'agent.spawn'
157
+ AND status IN (${SPAWN_IN_FLIGHT_STATUSES.map(() => "?").join(",")})
158
+ AND json_extract(params, '$.spawnedBy') = ?`,
159
+ )
160
+ .get(...SPAWN_IN_FLIGHT_STATUSES, parentId) as { n: number };
161
+ return row.n;
162
+ }
163
+
164
+ export function createSpawnCommandWithinQuota(
165
+ input: CreateCommandInput,
166
+ quota: { parentId: string; maxSpawnedAgents: number; liveChildren: () => number },
167
+ ): { command: Command | null; occupiedSlots: number } {
168
+ return getDb().transaction(() => {
169
+ const live = quota.liveChildren();
170
+ const inFlight = countInFlightSpawnCommands(quota.parentId);
171
+ const occupied = live + inFlight;
172
+ if (occupied >= quota.maxSpawnedAgents) return { command: null, occupiedSlots: occupied };
173
+ return { command: createCommand(input), occupiedSlots: occupied + 1 };
174
+ })();
175
+ }
176
+
126
177
  export function updateCommand(id: string, input: UpdateCommandInput): Command | null {
127
178
  const existing = getCommand(id);
128
179
  if (!existing) return null;
@@ -145,12 +196,14 @@ export function updateCommand(id: string, input: UpdateCommandInput): Command |
145
196
  return existing;
146
197
  }
147
198
  const now = Date.now();
148
- // #930 — refresh the spawn TTL from dispatch-intent when the command is claimed or
149
- // dispatched (see SPAWN_DISPATCH_STATUSES). Other command types keep their existing
150
- // expiry untouched.
199
+ // #930/#953 — refresh the lease from dispatch-intent when an expiring command is
200
+ // claimed or starts running. Pending commands are not swept before delivery, so a
201
+ // busy orchestrator must not inherit an already-expired lease when it finally gets
202
+ // back to the command loop.
203
+ const ttlMs = input.status && DISPATCH_STATUSES.includes(input.status) ? commandTtlMs(existing.type) : undefined;
151
204
  const nextExpiresAt =
152
- existing.type === "agent.spawn" && input.status && SPAWN_DISPATCH_STATUSES.includes(input.status)
153
- ? now + SPAWN_TTL_MS
205
+ ttlMs !== undefined
206
+ ? now + ttlMs
154
207
  : existing.expiresAt ?? null;
155
208
  getDb().query(`
156
209
  UPDATE commands
@@ -179,16 +232,15 @@ export function deleteCommand(id: string): boolean {
179
232
 
180
233
  export function expireCommands(now: number = Date.now()): Command[] {
181
234
  const rows = getDb()
182
- .query(`SELECT * FROM commands WHERE expires_at IS NOT NULL AND expires_at <= ? AND status IN (${ACTIVE_STATUSES.map(() => "?").join(",")})`)
183
- .all(now, ...ACTIVE_STATUSES) as CommandRow[];
235
+ .query(`SELECT * FROM commands WHERE expires_at IS NOT NULL AND expires_at <= ? AND status IN (${CLAIMED_ACTIVE_STATUSES.map(() => "?").join(",")})`)
236
+ .all(now, ...CLAIMED_ACTIVE_STATUSES) as CommandRow[];
184
237
  for (const row of rows) updateCommand(row.id, { status: "timed_out", error: "command timed out" });
185
238
  return rows.map((row) => getCommand(row.id)).filter((command): command is Command => Boolean(command));
186
239
  }
187
240
 
188
241
  function defaultExpiresAt(type: string, now: number): number | undefined {
189
- if (type === "agent.spawn") return now + SPAWN_TTL_MS;
190
- if (type === "agent.shutdown" || type === "agent.restart" || type === "agent.reconnect" || type === "agent.kill" || type === "agent.compact" || type === "agent.clearContext" || type === "agent.injectContext") return now + 30_000;
191
- return undefined;
242
+ const ttlMs = commandTtlMs(type);
243
+ return ttlMs === undefined ? undefined : now + ttlMs;
192
244
  }
193
245
 
194
246
  function recordShutdownTerminalEventOnSuccess(previousStatus: CommandStatus, command: Command): void {
@@ -16,6 +16,7 @@ import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_ME
16
16
  import { matchAgents } from "../agent-ref";
17
17
  import { getDb } from "./connection.ts";
18
18
  import { resolveProjectsForCwds } from "./projects.ts";
19
+ import { workspaceActiveClaim } from "../workspace-claim";
19
20
  import type {
20
21
  AgentCard,
21
22
  ActivityEvent,
@@ -511,6 +512,46 @@ export function patchWorkspaceMetadata(id: string, patch: Record<string, unknown
511
512
  return getWorkspace(id);
512
513
  }
513
514
 
515
+ type WorkspaceClaimResult =
516
+ | { ok: true; workspace: WorkspaceRecord }
517
+ | { ok: false; reason: "not-found" | "claimed-by-other"; workspace?: WorkspaceRecord };
518
+
519
+ export function claimWorkspaceMetadata(
520
+ id: string,
521
+ claimant: string,
522
+ patch: Record<string, unknown>,
523
+ now: number = Date.now(),
524
+ ): WorkspaceClaimResult {
525
+ const run = getDb().transaction((): WorkspaceClaimResult => {
526
+ const existing = getWorkspace(id);
527
+ if (!existing) return { ok: false, reason: "not-found" as const };
528
+ const active = workspaceActiveClaim(existing, now);
529
+ if (active?.by && active.by !== claimant) return { ok: false, reason: "claimed-by-other" as const, workspace: existing };
530
+ const next = { ...existing.metadata, ...patch };
531
+ getDb().query("UPDATE workspaces SET metadata = ? WHERE id = ?").run(JSON.stringify(next), id);
532
+ return { ok: true as const, workspace: getWorkspace(id)! };
533
+ });
534
+ return run();
535
+ }
536
+
537
+ export function releaseWorkspaceClaimMetadata(
538
+ id: string,
539
+ claimant: string,
540
+ patch: Record<string, unknown>,
541
+ now: number = Date.now(),
542
+ ): WorkspaceClaimResult {
543
+ const run = getDb().transaction((): WorkspaceClaimResult => {
544
+ const existing = getWorkspace(id);
545
+ if (!existing) return { ok: false, reason: "not-found" as const };
546
+ const active = workspaceActiveClaim(existing, now);
547
+ if (active?.by && active.by !== claimant) return { ok: false, reason: "claimed-by-other" as const, workspace: existing };
548
+ const next = { ...existing.metadata, ...patch };
549
+ getDb().query("UPDATE workspaces SET metadata = ? WHERE id = ?").run(JSON.stringify(next), id);
550
+ return { ok: true as const, workspace: getWorkspace(id)! };
551
+ });
552
+ return run();
553
+ }
554
+
514
555
  // Re-elect stewards for every repo where an agent owns a live workspace. Called
515
556
  // when an agent (re)registers so a dormant repo regains a steward on rejoin
516
557
  // without a full unscoped sweep.
@@ -115,6 +115,7 @@ export function providerConfigPublic(entry: ProviderConfigEntry, opts: { maskEnv
115
115
  defaultArgs: entry.value.defaultArgs,
116
116
  env: opts.maskEnv === false ? entry.value.env : maskEnv(entry.value.env),
117
117
  pluginDirs: entry.value.pluginDirs,
118
+ globalInstructionFiles: entry.value.globalInstructionFiles ?? [],
118
119
  defaultCapabilities: entry.value.defaultCapabilities,
119
120
  defaultApprovalMode: entry.value.defaultApprovalMode,
120
121
  defaultTags: entry.value.defaultTags,
@@ -184,6 +185,7 @@ function cleanProviderConfig(provider: string, value: unknown): ProviderConfig {
184
185
  defaultArgs: cleanStringArray(value.defaultArgs, "defaultArgs", { itemMax: 80, maxItems: 50 }) ?? defaults.defaultArgs,
185
186
  env: cleanEnvRecord(value.env),
186
187
  pluginDirs: cleanStringArray(value.pluginDirs, "pluginDirs", { itemMax: 80, maxItems: 50 }) ?? defaults.pluginDirs,
188
+ globalInstructionFiles: cleanStringArray(value.globalInstructionFiles, "globalInstructionFiles", { itemMax: 80, maxItems: 10 }) ?? defaults.globalInstructionFiles,
187
189
  defaultCapabilities: cleanStringArray(value.defaultCapabilities, "defaultCapabilities", { itemMax: 80, maxItems: 50 }) ?? defaults.defaultCapabilities,
188
190
  defaultApprovalMode: cleanString(value.defaultApprovalMode, "defaultApprovalMode", { max: 80 }) ?? defaults.defaultApprovalMode,
189
191
  defaultTags: cleanStringArray(value.defaultTags, "defaultTags", { itemMax: 80, maxItems: 50 }) ?? defaults.defaultTags,
@@ -380,8 +380,7 @@ export const postWorkspaceCleanupStale: Handler = async (req) => {
380
380
  // a destructive-action footgun. Now an explicit id narrows the candidate set.
381
381
  const workspaceId = cleanString(body.workspaceId, "workspaceId", { max: 160 });
382
382
  const dryRun = body.dryRun !== false; // safe by default
383
- const landedOnly = body.landedOnly !== false;
384
- const offlineOwnerOnly = body.offlineOwnerOnly !== false;
383
+ const landedOnly = true;
385
384
 
386
385
  const candidates = listWorkspaces().filter((ws) =>
387
386
  ws.mode === "isolated" && Boolean(ws.worktreePath) && !TERMINAL_WORKSPACE_STATUSES.has(ws.status)
@@ -395,7 +394,6 @@ export const postWorkspaceCleanupStale: Handler = async (req) => {
395
394
  const owner = ws.ownerAgentId ? getAgent(ws.ownerAgentId) : null;
396
395
  const ownerOnline = isOwnerAlive(ws.ownerAgentId);
397
396
  if (ownerOnline) continue; // never clean a live owner's worktree
398
- if (offlineOwnerOnly && !ws.ownerAgentId) { /* no owner recorded — still eligible */ }
399
397
  if (workspaceActiveClaim(ws)) continue; // respect steward claims
400
398
  const fetched = await fetchWorkspaceGitState(ws);
401
399
  const gitState = "state" in fetched ? fetched.state : undefined;
@@ -420,7 +418,7 @@ export const postWorkspaceCleanupStale: Handler = async (req) => {
420
418
  }
421
419
  rows.push(row);
422
420
  }
423
- return json({ dryRun, landedOnly, offlineOwnerOnly, repoRoot, ...(workspaceId ? { workspaceId } : {}), scanned: candidates.length, eligible: rows.filter((r) => r.safe).length, cleaned, candidates: rows }, dryRun ? 200 : 202);
421
+ return json({ dryRun, landedOnly, repoRoot, ...(workspaceId ? { workspaceId } : {}), scanned: candidates.length, eligible: rows.filter((r) => r.safe).length, cleaned, candidates: rows }, dryRun ? 200 : 202);
424
422
  };
425
423
 
426
424
  export const postWorkspaceAction: Handler = async (req, params) => {
@@ -79,6 +79,11 @@ function str(v: unknown): string | undefined {
79
79
  return typeof v === "string" && v.length > 0 ? v : undefined;
80
80
  }
81
81
 
82
+ export function callerOwnsSpawnedAgent(ctx: AuthContext, target: AgentCard | null | undefined): boolean {
83
+ const callerId = ctx.callerAgentId;
84
+ return Boolean(callerId && target?.spawnedBy === callerId);
85
+ }
86
+
82
87
  /** Build a ShutdownAgentInput from a WS-bus `agent.shutdown` command frame's target +
83
88
  * params. Lives here (not in bus.ts) so the bus transport stays a thin dispatcher and the
84
89
  * shutdown-input shape has one home. */
@@ -123,7 +128,7 @@ export function authorizeShutdown(ctx: AuthContext, input: ShutdownAgentInput):
123
128
  const target = input.agentId
124
129
  ? getAgent(input.agentId)
125
130
  : listAgents().find((a) => a.meta?.spawnRequestId === input.spawnRequestId);
126
- if (!target || target.spawnedBy !== callerId) {
131
+ if (!target || !callerOwnsSpawnedAgent(ctx, target)) {
127
132
  throw new ShutdownAuthError(
128
133
  input.agentId
129
134
  ? `agent ${input.agentId} is not one of your spawned children`
@@ -28,7 +28,7 @@ import { DEFAULT_USER_ID, errMessage, isRecord, stringValue } from "agent-relay-
28
28
  import { statSync } from "node:fs";
29
29
  import { dirname, resolve } from "node:path";
30
30
  import { countLiveSpawnedAgents, createActivityEvent, ensureTeam, getAgent, getTaskDetail, listAgents, listProjects, listWorkspaces, recordTaskSpawnRequested, resolveAgentOwnership, resolveProjectForCwd, resolveScopedAnchor, rootsForProject, setAgentTeam, ValidationError, withResolvedProvisioning } from "../db";
31
- import { createCommand } from "../commands-db";
31
+ import { countInFlightSpawnCommands, createCommand, createSpawnCommandWithinQuota, findSpawnCommandByRequestId } from "../commands-db";
32
32
  import { emitCommandEvent } from "../command-events";
33
33
  import { buildSpawnCommand, generateSpawnRequestId, resolveSpawnModelParams } from "../spawn-command";
34
34
  import { runnerRuntimeTokenEnv } from "../runtime-tokens";
@@ -48,12 +48,14 @@ import { hasComponentScope, isComponentAuthorizedFor } from "../security";
48
48
  import { isPathWithinBase } from "../utils";
49
49
  import { McpAuthError, McpNotFoundError } from "../mcp-errors";
50
50
  import type { AuthContext } from "./auth-context";
51
- import type { AgentCard, AgentLifecycle, Command, SpawnApprovalMode, WorkspaceAutoMergePolicy, WorkspaceLandingPolicy, WorkspaceMode } from "../types";
51
+ import { callerOwnsSpawnedAgent } from "./shutdown-agent";
52
+ import type { AgentCard, AgentLifecycle, Command, CommandStatus, CreateCommandInput, SpawnApprovalMode, WorkspaceAutoMergePolicy, WorkspaceLandingPolicy, WorkspaceMode } from "../types";
52
53
  import { isOwnerAlive, liveAgentCwds, worktreeInUseByLiveAgent } from "../workspace-merge";
53
54
  import { TERMINAL_WORKSPACE_STATUSES } from "../workspace-phase";
54
55
 
55
56
  /** The spawn scope every transport must hold (admin `*` / `admin:*` pass; `system` bypasses). */
56
57
  const SPAWN_SCOPE = "command:spawn";
58
+ const ACTIVE_SPAWN_COMMAND_STATUSES: CommandStatus[] = ["pending", "accepted", "running"];
57
59
 
58
60
  function hasGitMarker(dir: string): boolean {
59
61
  try {
@@ -362,11 +364,14 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
362
364
  throw new McpAuthError("spawned agents cannot spawn further agents (no grandchildren)");
363
365
  }
364
366
  const quota = ctx.constraints?.maxSpawnedAgents ?? 0;
365
- const live = countLiveSpawnedAgents(callerId);
366
- if (live >= quota) {
367
- throw new ValidationError(`spawn quota reached (${live}/${quota} live children) — shut one down or wait for one to exit`);
367
+ const occupied = countLiveSpawnedAgents(callerId) + countInFlightSpawnCommands(callerId);
368
+ if (occupied >= quota) {
369
+ throw new ValidationError(`spawn quota reached (${occupied}/${quota} live or in-flight children) — shut one down or wait for one to exit`);
368
370
  }
369
371
  }
372
+ const spawnQuota = callerId
373
+ ? { parentId: callerId, maxSpawnedAgents: ctx.constraints?.maxSpawnedAgents ?? 0 }
374
+ : undefined;
370
375
  // Gate the child spawn on the token's legit bounds: the target orchestrator AND the spawn cwd
371
376
  // (`cwdPrefixes`/`cwd` constraint). Excludes the #323 footguns (spawnRequestIds/policies) — those
372
377
  // are self-scoping per-spawn ids the CHILD gets fresh, so gating on them makes the quota
@@ -379,6 +384,31 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
379
384
  // the raw token actor. Recorded in the command payload, the token mint, and the audit row.
380
385
  const requestedBy = callerId ?? taskCreatorAgentId ?? input.requestedBy ?? ctx.actor.id;
381
386
 
387
+ const requestedSpawnRequestId = input.spawnRequestId?.trim();
388
+ if (requestedSpawnRequestId) {
389
+ const registeredAgent = listAgents().find((agent) => agent.meta?.spawnRequestId === requestedSpawnRequestId);
390
+ const activeCommand = findSpawnCommandByRequestId(requestedSpawnRequestId, ACTIVE_SPAWN_COMMAND_STATUSES);
391
+ const existingCommand = activeCommand ?? (registeredAgent ? findSpawnCommandByRequestId(requestedSpawnRequestId) : null);
392
+ if (existingCommand || registeredAgent) {
393
+ const existingParent = existingCommand ? stringValue(existingCommand.params.spawnedBy) : undefined;
394
+ if (callerId && existingParent !== callerId && !callerOwnsSpawnedAgent(ctx, registeredAgent)) {
395
+ throw new ValidationError(`spawnRequestId ${requestedSpawnRequestId} is already in use`);
396
+ }
397
+ if (!existingCommand) {
398
+ throw new ValidationError(`spawnRequestId ${requestedSpawnRequestId} is already registered to agent ${registeredAgent!.id}`);
399
+ }
400
+ return {
401
+ ok: true,
402
+ spawnRequestId: requestedSpawnRequestId,
403
+ orchestratorId: stringValue(existingCommand.params.orchestratorId) ?? orchestrator.id,
404
+ provider: (stringValue(existingCommand.params.provider) as SpawnProvider | undefined) ?? input.provider,
405
+ agentId: registeredAgent?.id ?? null,
406
+ registered: Boolean(registeredAgent),
407
+ command: existingCommand,
408
+ };
409
+ }
410
+ }
411
+
382
412
  // #405 — durable project binding: the project whose registered root contains the spawn cwd.
383
413
  // Stamped on both the child token and an auto-scaffolded Task row so issueless spawned work
384
414
  // stays filterable by project.
@@ -579,7 +609,7 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
579
609
  // command payload.
580
610
  const expectReply = input.expectReply === true;
581
611
 
582
- const command = createCommand({
612
+ const commandInput: CreateCommandInput = {
583
613
  type: "agent.spawn",
584
614
  source: "system",
585
615
  target: orchestrator.agentId,
@@ -619,7 +649,20 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
619
649
  // so there's no agent row to resolve `spawnedBy` from) can be routed back to it.
620
650
  ...(lineageAgentId ? { extra: { spawnedBy: lineageAgentId } } : {}),
621
651
  }),
622
- });
652
+ };
653
+ let command: Command;
654
+ if (spawnQuota) {
655
+ const quotaResult = createSpawnCommandWithinQuota(commandInput, {
656
+ ...spawnQuota,
657
+ liveChildren: () => countLiveSpawnedAgents(spawnQuota.parentId),
658
+ });
659
+ if (!quotaResult.command) {
660
+ throw new ValidationError(`spawn quota reached (${quotaResult.occupiedSlots}/${spawnQuota.maxSpawnedAgents} live or in-flight children) — shut one down or wait for one to exit`);
661
+ }
662
+ command = quotaResult.command;
663
+ } else {
664
+ command = createCommand(commandInput);
665
+ }
623
666
  emitCommandEvent(command, "command.requested");
624
667
 
625
668
  // ONE spawn-request audit row, identical across transports (the MCP path previously emitted
@@ -1,11 +1,14 @@
1
+ import { RELAY_TOKEN_HEADER } from "agent-relay-sdk";
1
2
  import { createCommand } from "../commands-db";
2
3
  import { emitRelayEvent } from "../events";
3
- import { createActivityEvent, getAgent, listAgents, listWorkspaces, recordTerminalEvent } from "../db";
4
+ import { createActivityEvent, getAgent, listAgents, listOrchestrators, listWorkspaces, recordTerminalEvent } from "../db";
4
5
  import { routeNotification } from "../notification-router";
5
6
  import { isProviderBlocked } from "../provider-state";
7
+ import { relayToken } from "../config";
8
+ import { isPathWithinBase } from "../utils";
6
9
  import { TERMINAL_WORKSPACE_STATUSES } from "../workspace-phase";
7
10
  import { finalizeEphemeralTasksOnReap } from "./task-entity";
8
- import type { AgentCard, Command, WorkspaceRecord } from "../types";
11
+ import type { AgentCard, Command, WorkspaceGitState, WorkspaceRecord } from "../types";
9
12
 
10
13
  // The single home for "dispose a transient/one-shot agent" — the canonical `agent.shutdown`
11
14
  // command + its events/terminal-marker/activity. Extracted so the idle-grace scan below AND
@@ -65,19 +68,30 @@ export function resetTransientTrackerForTests(): void {
65
68
  }
66
69
 
67
70
  // How many commits the agent's branch has ahead of its base. Returns null when
68
- // the check can't run (missing fields, git failure) — callers treat null as
71
+ // the check can't run (missing fields, host/probe failure) — callers treat null as
69
72
  // "unknown" and preserve the safe #614 hold-pending bias: only 0 is fast-reaped.
70
- function defaultCommitsAhead(ws: WorkspaceRecord): number | null {
71
- const base = ws.baseSha ?? ws.baseRef;
72
- if (!base || !ws.branch || !ws.worktreePath) return null;
73
+ async function defaultCommitsAhead(ws: WorkspaceRecord): Promise<number | null> {
74
+ if (!ws.branch || !ws.worktreePath) return null;
75
+ const orch = listOrchestrators().find(
76
+ (candidate) => candidate.status === "online" && candidate.apiUrl && isPathWithinBase(ws.sourceCwd, candidate.baseDir),
77
+ );
78
+ if (!orch?.apiUrl) return null;
79
+ const query = new URLSearchParams({ path: ws.worktreePath });
80
+ if (ws.baseRef) query.set("baseRef", ws.baseRef);
81
+ if (ws.baseSha) query.set("baseSha", ws.baseSha);
82
+ const headers: Record<string, string> = {};
83
+ const token = relayToken();
84
+ if (token) headers[RELAY_TOKEN_HEADER] = token;
73
85
  try {
74
- const proc = Bun.spawnSync(
75
- ["git", "-C", ws.worktreePath, "rev-list", "--count", `${base}..${ws.branch}`],
76
- { stdin: "ignore", stdout: "pipe", stderr: "ignore" },
77
- );
78
- if (proc.exitCode !== 0) return null;
79
- const n = parseInt(Buffer.from(proc.stdout).toString("utf8").trim(), 10);
80
- return Number.isFinite(n) ? n : null;
86
+ const res = await fetch(`${orch.apiUrl}/api/workspace/state?${query.toString()}`, {
87
+ headers,
88
+ signal: AbortSignal.timeout(8_000),
89
+ });
90
+ if (!res.ok) return null;
91
+ const state = await res.json() as WorkspaceGitState;
92
+ if (state.missing || state.error || (state.dirtyCount ?? 0) > 0) return null;
93
+ const n = state.landed === true ? 0 : (state.unmergedAhead ?? state.ahead);
94
+ return typeof n === "number" && Number.isFinite(n) ? n : null;
81
95
  } catch {
82
96
  return null;
83
97
  }
@@ -96,9 +110,9 @@ function reapProtected(agent: ReturnType<typeof listAgents>[number]): boolean {
96
110
  return agent.status !== "idle" || !agent.ready || isProviderBlocked(agent);
97
111
  }
98
112
 
99
- let _commitsAheadFn: (ws: WorkspaceRecord) => number | null = defaultCommitsAhead;
113
+ let _commitsAheadFn: (ws: WorkspaceRecord) => number | null | Promise<number | null> = defaultCommitsAhead;
100
114
 
101
- export function overrideCommitsAheadForTests(fn: (ws: WorkspaceRecord) => number | null): void {
115
+ export function overrideCommitsAheadForTests(fn: (ws: WorkspaceRecord) => number | null | Promise<number | null>): void {
102
116
  _commitsAheadFn = fn;
103
117
  }
104
118
  export function resetCommitsAheadForTests(): void {
@@ -116,7 +130,7 @@ function workspaceProducedWork(ws: WorkspaceRecord | undefined): boolean {
116
130
  return typeof ws.metadata.mergedAt === "number";
117
131
  }
118
132
 
119
- function transientLandComplete(agent: AgentCard): { complete: boolean; producedWork: boolean; workspaceId?: string; status?: string; reason?: string } {
133
+ async function transientLandComplete(agent: AgentCard): Promise<{ complete: boolean; producedWork: boolean; workspaceId?: string; status?: string; reason?: string }> {
120
134
  const agentId = agent.id;
121
135
  const isolated = listWorkspaces({ ownerAgentId: agentId }).filter((ws) => ws.mode === "isolated");
122
136
  if (!isolated.length) return { complete: true, producedWork: completedProviderTurns(agent) > 0, reason: "no-isolated-workspace" };
@@ -134,7 +148,7 @@ function transientLandComplete(agent: AgentCard): { complete: boolean; producedW
134
148
  // instead of holding it pending a land that will never happen. Only fires when the
135
149
  // branch is provably empty (count === 0); null (unknown/git error) falls through to
136
150
  // the safe #614 hold-pending bias below.
137
- if (live.status === "active" && _commitsAheadFn(live) === 0) {
151
+ if (live.status === "active" && await _commitsAheadFn(live) === 0) {
138
152
  return { complete: true, producedWork: false, workspaceId: live.id, status: live.status, reason: "empty-branch" };
139
153
  }
140
154
  return { complete: false, producedWork: false, workspaceId: live.id, status: live.status, reason: "land-pending" };
@@ -171,7 +185,7 @@ function notifyTransientHeld(agentId: string, workspaceId: string | undefined, s
171
185
  }
172
186
  }
173
187
 
174
- export function reapTransientAgents(): Record<string, unknown> {
188
+ export async function reapTransientAgents(): Promise<Record<string, unknown>> {
175
189
  const now = Date.now();
176
190
  const grace = transientGraceMs();
177
191
  const maxLandWait = transientMaxLandWaitMs();
@@ -193,7 +207,7 @@ export function reapTransientAgents(): Record<string, unknown> {
193
207
  transientTracker.set(agent.id, entry);
194
208
  const idleForMs = now - entry.firstIdleAt;
195
209
  if (idleForMs < grace) { waiting.push(agent.id); continue; }
196
- const land = transientLandComplete(agent);
210
+ const land = await transientLandComplete(agent);
197
211
  if (!land.complete) {
198
212
  if (idleForMs >= grace + maxLandWait) {
199
213
  held.push(agent.id);
@@ -300,6 +300,7 @@ const PROVIDER_CONFIG_SCHEMA: JSONSchema = {
300
300
  "defaultArgs",
301
301
  "env",
302
302
  "pluginDirs",
303
+ "globalInstructionFiles",
303
304
  "defaultCapabilities",
304
305
  "defaultApprovalMode",
305
306
  "defaultTags",
@@ -312,6 +313,7 @@ const PROVIDER_CONFIG_SCHEMA: JSONSchema = {
312
313
  defaultArgs: { type: "array", maxItems: 50, items: { type: "string", maxLength: 80 } },
313
314
  env: { type: "object", additionalProperties: { type: "string" } },
314
315
  pluginDirs: { type: "array", maxItems: 50, items: { type: "string", maxLength: 80 } },
316
+ globalInstructionFiles: { type: "array", maxItems: 10, items: { type: "string", maxLength: 80 } },
315
317
  defaultCapabilities: { type: "array", maxItems: 50, items: { type: "string", maxLength: 80 } },
316
318
  defaultApprovalMode: { type: "string", maxLength: 80 },
317
319
  defaultTags: { type: "array", maxItems: 50, items: { type: "string", maxLength: 80 } },
@@ -14,8 +14,10 @@ import { isPathWithinBase } from "./utils";
14
14
  import {
15
15
  createActivityEvent,
16
16
  getWorkspace,
17
+ claimWorkspaceMetadata,
17
18
  listOrchestrators,
18
19
  patchWorkspaceMetadata,
20
+ releaseWorkspaceClaimMetadata,
19
21
  updateWorkspaceStatus,
20
22
  } from "./db";
21
23
  import { emitActivityEvent } from "./sse";
@@ -148,8 +150,17 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
148
150
  // so deterministic landing can't race a steward mid-validation. No status change.
149
151
  if (action === "claim" || action === "release-claim") {
150
152
  const release = action === "release-claim";
151
- const updated = patchWorkspaceMetadata(workspace.id, claimMetadataPatch(release, agentId ?? "steward", input.purpose));
152
- if (!updated) return { ok: false, httpStatus: 404, error: "workspace not found" };
153
+ const claimant = agentId ?? "steward";
154
+ const result = release
155
+ ? releaseWorkspaceClaimMetadata(workspace.id, claimant, claimMetadataPatch(true), Date.now())
156
+ : claimWorkspaceMetadata(workspace.id, claimant, claimMetadataPatch(false, claimant, input.purpose), Date.now());
157
+ if (!result.ok) {
158
+ if (result.reason === "not-found") return { ok: false, httpStatus: 404, error: "workspace not found" };
159
+ const active = result.workspace ? workspaceActiveClaim(result.workspace) : null;
160
+ const owner = active?.by ? ` by ${active.by}` : "";
161
+ return { ok: false, httpStatus: 409, error: `workspace ${workspace.id} is already claimed${owner}` };
162
+ }
163
+ const updated = result.workspace;
153
164
  recordWorkspaceAudit({
154
165
  action,
155
166
  workspace: updated,
@@ -231,6 +242,14 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
231
242
  };
232
243
  }
233
244
 
245
+ let command: Command | undefined;
246
+ if (requiresCommand) {
247
+ // Only `cleanup` reaches here — `merge` returned early via the shared helper.
248
+ const built = buildWorkspaceCleanupCommand(workspace, agentId ?? "dashboard", { force: input.force === true });
249
+ if (!built.ok) return { ok: false, httpStatus: built.status, error: built.error };
250
+ command = built.command;
251
+ }
252
+
234
253
  const updated = updateWorkspaceStatus(workspace.id, nextStatus, {
235
254
  ...metadata,
236
255
  ...(detail ? { detail } : {}),
@@ -250,13 +269,6 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
250
269
  // + lease-serialized; a non-landable row is a harmless no-op (autoMergeForRepo re-checks).
251
270
  if (READY_TO_LAND_STATUSES.has(nextStatus)) scheduleRepoMerge(updated.repoRoot);
252
271
 
253
- let command: Command | undefined;
254
- if (requiresCommand) {
255
- // Only `cleanup` reaches here — `merge` returned early via the shared helper.
256
- const built = buildWorkspaceCleanupCommand(workspace, agentId ?? "dashboard", { force: input.force === true });
257
- if (!built.ok) return { ok: false, httpStatus: built.status, error: built.error };
258
- command = built.command;
259
- }
260
272
  recordWorkspaceAudit({
261
273
  action,
262
274
  workspace: updated,
@@ -289,9 +301,8 @@ export function buildWorkspaceCleanupCommand(
289
301
  error: `workspace ${workspace.id} owner is still online; pass force:true to clean it up intentionally`,
290
302
  };
291
303
  }
292
- const owners = listOrchestrators().filter((candidate) => isPathWithinBase(workspace.sourceCwd, candidate.baseDir));
293
- const owner = owners.find((candidate) => candidate.status === "online") ?? owners[0];
294
- if (!owner) return { ok: false, status: 409, error: "no orchestrator owns this workspace path; use DELETE /api/workspaces/:id to purge the record" };
304
+ const owner = listOrchestrators().find((candidate) => candidate.status === "online" && isPathWithinBase(workspace.sourceCwd, candidate.baseDir));
305
+ if (!owner) return { ok: false, status: 409, error: "no online orchestrator owns this workspace path; retry when the host is online or use DELETE /api/workspaces/:id to purge the record" };
295
306
  const command = createCommand({
296
307
  type: "workspace.cleanup",
297
308
  source: "system",
@@ -308,7 +319,7 @@ export function buildWorkspaceCleanupCommand(
308
319
  requestedBy,
309
320
  requestedAt: Date.now(),
310
321
  deleteBranch: true,
311
- queued: owner.status !== "online",
322
+ queued: false,
312
323
  },
313
324
  });
314
325
  return { ok: true, command };
@@ -1,2 +0,0 @@
1
- var e=/^[a-z][a-z0-9-]*$/,t=new Map;function n(n){if(!e.test(n.id))throw Error(`invalid provider id: ${n.id}`);if(!n.label.trim())throw Error(`provider ${n.id} is missing a label`);if(!n.icon.trim())throw Error(`provider ${n.id} is missing an icon`);if(t.has(n.id))throw Error(`provider already registered: ${n.id}`);t.set(n.id,n)}function r(e){return t.get(e)}function i(){return[...t.values()]}var a=[`low`,`medium`,`high`,`max`],o=[`low`,`medium`,`high`,`xhigh`,`max`],s={value:2e5,source:`catalog`,confidence:`declared`},c={value:1e6,source:`catalog`,confidence:`declared`},l={modalities:{input:{text:!0,image:!0},output:{text:!0}},tools:{code:!0,review:!0,debug:!0,refactor:!0},source:`catalog`,confidence:`declared`},u=[{name:`five_hour`,unit:`%`,reset:!0},{name:`seven_day`,unit:`%`,reset:!0}];function d(e){return{...e?{limits:e}:{},capabilities:l}}n({id:`claude`,label:`Claude`,icon:`🟠`,launch:{managedPolicyPrompt:{alwaysOn:`append-system-prompt`},taskHydration:{channel:`append-system-prompt`}},catalog:{defaultModel:`sonnet-5`,models:[{alias:`fable-5`,label:`Fable 5`,providerModel:`claude-fable-5`,efforts:o,defaultEffort:`medium`,unavailable:`suspended by export-control directive, 2026-06-12`,...d({contextWindowTokens:c})},{alias:`opus-4.8`,label:`Opus 4.8`,providerModel:`claude-opus-4-8`,efforts:o,defaultEffort:`medium`,...d({contextWindowTokens:s})},{alias:`opus-4.8[1m]`,label:`Opus 4.8 1M`,providerModel:`claude-opus-4-8[1m]`,efforts:o,defaultEffort:`medium`,...d({contextWindowTokens:c})},{alias:`opus-4.7`,label:`Opus 4.7`,providerModel:`claude-opus-4-7`,efforts:o,defaultEffort:`medium`,...d({contextWindowTokens:s})},{alias:`opus-4.7[1m]`,label:`Opus 4.7 1M`,providerModel:`claude-opus-4-7[1m]`,efforts:o,defaultEffort:`medium`,...d({contextWindowTokens:c})},{alias:`opus-4.6`,label:`Opus 4.6`,providerModel:`claude-opus-4-6`,efforts:a,defaultEffort:`medium`,...d({contextWindowTokens:s})},{alias:`opus-4.6[1m]`,label:`Opus 4.6 1M`,providerModel:`claude-opus-4-6[1m]`,efforts:a,defaultEffort:`medium`,...d({contextWindowTokens:c})},{alias:`sonnet-5`,label:`Sonnet 5`,providerModel:`claude-sonnet-5`,efforts:o,defaultEffort:`medium`,...d({contextWindowTokens:s})},{alias:`sonnet-5[1m]`,label:`Sonnet 5 1M`,providerModel:`claude-sonnet-5[1m]`,efforts:o,defaultEffort:`medium`,...d({contextWindowTokens:c})},{alias:`haiku`,label:`Haiku`,providerModel:`haiku`,efforts:[],...d()}]},quotaWindows:[...u],capabilityDefaults:{lifecycle:{managed:!0,shutdownHard:{when:`headless`,value:!0,otherwise:!1},restartHard:{when:`headless`,value:!0,otherwise:!1},semanticStatus:!0,reconnect:!0},session:{fileRead:!0,fileWrite:{when:`approval:not-read-only`,value:!0,otherwise:!1},shell:!0,shellMode:{open:`unrestricted`,guarded:`guarded`,readOnly:`read-only-guarded`}},context:{managed:`headless`,compact:{when:`headless`,value:!0,otherwise:!1},clear:{when:`headless`,value:!0,otherwise:!1},inject:!0,resumePreference:[`native`,`clear-inject`,`none`]},quota:{supported:!0,source:`probe`,unit:`%`,windows:[...u]},terminal:{mode:`live`,condition:`headless`,live:{read:!0,write:!0}},liveSession:{capture:!0,inject:!0,interrupt:!0,promptEcho:!0,reasoning:!0,slashCommands:!0}},slashCommands:[`/clear`,`/compact`,`/model`,`/help`],resume:{supported:!0,idFormat:`uuid-v4`,idPattern:`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`},home:{configDir:`.claude`,envPrefix:`CLAUDE`,configDirEnvVar:`CLAUDE_CONFIG_DIR`,quotaCredentialFile:`.credentials.json`,authItems:[`.credentials.json`,`statsig`]},provisioning:{pluginManifestPath:`.claude-plugin/plugin.json`,bundledPlugin:{ref:`agent-relay-claude-plugin`,dir:`runner/plugins/claude`,title:`Agent Relay Claude plugin`,description:`Bundled Agent Relay Claude plugin, materialized whole through the provisioning registry.`,omitFiles:[`monitors/relay-monitor.provisioned.mjs`],fileOverrides:[{path:`monitors/relay-monitor.ts`,sourcePath:`monitors/relay-monitor.provisioned.mjs`}]}},upgrade:{packages:[`agent-relay-runner`,`agent-relay-plugin`],detectPackages:[`agent-relay-runner`],detectCommands:[`claude`]},probe:{command:`claude`,args:[`--version`],healthCheck:`claude --version`,featureChecks:[{name:`rig`,command:`claude-rig`,homeDir:`.claude-rig`}]},quotaPoll:{strategy:`none`},shim:{kind:`claude-relay`}});var f=[`low`,`medium`,`high`,`xhigh`],p={value:2e5,source:`catalog`,confidence:`declared`},m={modalities:{input:{text:!0,image:!0},output:{text:!0}},tools:{code:!0,review:!0,debug:!0,refactor:!0},source:`catalog`,confidence:`declared`},h=[{name:`primary`,unit:`%`,reset:!0},{name:`secondary`,unit:`%`,reset:!0}];function g(e){return{...e?{limits:e}:{},capabilities:m}}n({id:`codex`,label:`Codex`,icon:`⌨️`,launch:{managedPolicyPrompt:{alwaysOn:`prompt`},taskHydration:{channel:`prompt`}},catalog:{defaultModel:`gpt-5.5`,models:[{alias:`gpt-5.5`,label:`GPT-5.5`,providerModel:`gpt-5.5`,efforts:f,defaultEffort:`medium`,...g({contextWindowTokens:p})},{alias:`gpt-5.4`,label:`GPT-5.4`,providerModel:`gpt-5.4`,efforts:f,defaultEffort:`medium`,...g({contextWindowTokens:p})},{alias:`gpt-5.4-mini`,label:`GPT-5.4 Mini`,providerModel:`gpt-5.4-mini`,efforts:f,defaultEffort:`medium`,...g({contextWindowTokens:p})},{alias:`gpt-5.3-codex`,label:`GPT-5.3 Codex`,providerModel:`gpt-5.3-codex`,efforts:f,defaultEffort:`medium`,...g({contextWindowTokens:p})},{alias:`gpt-5.3-codex-spark`,label:`GPT-5.3 Codex Spark`,providerModel:`gpt-5.3-codex-spark`,efforts:f,defaultEffort:`high`,...g({contextWindowTokens:p})},{alias:`gpt-5.2`,label:`GPT-5.2`,providerModel:`gpt-5.2`,efforts:f,defaultEffort:`medium`,...g({contextWindowTokens:p})}]},quotaWindows:[...h],capabilityDefaults:{lifecycle:{managed:!0,shutdownHard:!0,restartHard:!0,semanticStatus:!0,reconnect:!0},session:{fileRead:!0,fileWrite:{when:`approval:not-read-only`,value:!0,otherwise:!1},shell:{when:`approval:not-read-only`,value:!0,otherwise:!1},shellMode:{open:`unrestricted`,guarded:`guarded`,readOnly:`none`}},context:{managed:`always`,compact:!0,clear:!0,inject:!0,resumePreference:[`native`,`clear-inject`,`none`]},quota:{supported:!0,source:`probe`,unit:`%`,windows:[...h]},terminal:{mode:`attach`,attach:{create:!0,read:!0,write:!0,detach:!0}},liveSession:{capture:!0,inject:!0,interrupt:!0,promptEcho:!0,reasoning:!0,slashCommands:!0}},slashCommands:[`/compact`,`/new`],resume:{supported:!0,idFormat:`codex-thread-id`,idPattern:`[\\w-]+`},home:{configDir:`.codex`,envPrefix:`CODEX`,configDirEnvVar:`CODEX_HOME`,quotaCredentialFile:`auth.json`,authItems:[`auth.json`,`installation_id`]},provisioning:{pluginManifestPath:`.codex-plugin/plugin.json`,bundledSkills:{refPrefix:`agent-relay-codex-`,dir:`runner/plugins/codex/skills`,titlePrefix:`Agent Relay Codex skill: `,description:`Bundled Agent Relay Codex skill, materialized through the provisioning registry.`,entryFile:`SKILL.md`}},upgrade:{packages:[`agent-relay-runner`,`agent-relay-codex`],detectPackages:[`agent-relay-runner`,`agent-relay-codex`],detectCommands:[`codex-relay`,`agent-relay-runner`]},probe:{command:`codex`,args:[`--version`],healthCheck:`codex --version`},quotaPoll:{strategy:`codex-app-server`},shim:{kind:`codex-relay`,extraBinPaths:[`codex/bin/codex`],cleanLegacyHooks:!0}});var _=[`bg-amber-500/10 text-amber-400 border-amber-500/30`,`bg-sky-500/10 text-sky-400 border-sky-500/30`,`bg-violet-500/10 text-violet-400 border-violet-500/30`,`bg-emerald-500/10 text-emerald-400 border-emerald-500/30`,`bg-rose-500/10 text-rose-400 border-rose-500/30`];function v(e){let t=0;for(let n=0;n<e.length;n++)t=t*31+e.charCodeAt(n)>>>0;return t}var y=new Map(i().map((e,t)=>[e.id,_[t%_.length]]));function b(e){return r(e)?.label??e.charAt(0).toUpperCase()+e.slice(1)}function x(e){let t=y.get(e);if(t)return t;let n=_[v(e)%_.length];return y.set(e,n),n}function S(){return i().map(e=>e.id)}function C(e){return r(e)}function w(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function T(e){if(typeof e!=`string`)return;let t=e.trim();return t.length>0?t:void 0}function E(e){return e instanceof Error?e.message:String(e)}var D=[`cleaned`,`merged`,`abandoned`],O={"admin:*":`system:admin`,"system:write":`system:admin`,"agents:read":`agent:read`,"agents:write":`agent:write`,"messages:read":`message:read`,"messages:write":`message:send`,"tasks:read":`task:read`,"tasks:write":`task:write`,"commands:read":`command:read`,"commands:write":`command:write`,"channels:read":`channel:read`,"channels:write":`channel:write`,"integrations:read":`integration:read`,"integrations:write":`integration:write`,"connectors:read":`integration:read`,"connectors:write":`integration:write`,"events:create":`integration:write`,"tasks:create":`task:write`};function k(e){return O[e]??e}function A(e,t){let n=k(t),r=j(n);if(!r)return!1;if(e?.includes(`*`))return!0;for(let t of e??[]){let e=k(t),i=j(e);if(i&&(e===`system:admin`||e===n||e===`${r.category}:*`||r.category===`settings`&&r.action&&r.namespace&&i.category===`settings`&&i.action===r.action&&i.namespace===void 0))return!0}return!1}function j(e){if(e===`*`)return{category:`*`};let t=e.split(`:`);if(t.some(e=>!e))return null;if(t.length===2){let[e,n]=t;return{category:e,action:n}}if(t.length===3){let[e,n,r]=t;return e!==`settings`||n!==`read`&&n!==`write`||r===`*`?null:{category:e,action:n,namespace:r}}return null}var M=`default`;function N(e){return e===`user`||typeof e==`string`&&e.startsWith(`user:`)}function P(e){if(e===`user`)return M;if(typeof e==`string`&&e.startsWith(`user:`))return e.slice(5)||`default`}i().map(e=>e.id);var F=[`none`,`uncommitted`,`committed-on-branch`,`landed-on-main`,`deploying`,`deployed`],I=[`trivial`,`moderate`,`complex`],L=[`build`,`review`,`research`,`ops`,`ui`,`refactor`,`debug`],R=[`small`,`medium`,`large`],z=[`low`,`normal`,`high`];export{i as _,L as a,A as c,w as d,T as f,C as g,b as h,R as i,D as l,x as m,I as n,N as o,S as p,z as r,P as s,F as t,E as u};
2
- //# sourceMappingURL=types-8ThujPP2.js.map