agent-relay-server 0.32.4 → 0.33.0

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 (101) hide show
  1. package/package.json +2 -2
  2. package/public/assets/{activity-DT1JGHnp.js → activity-B0_uE6Yh.js} +2 -2
  3. package/public/assets/{activity-DT1JGHnp.js.map → activity-B0_uE6Yh.js.map} +1 -1
  4. package/public/assets/{agent-profiles-CrMemMkZ.js → agent-profiles-Rwxrcf9F.js} +2 -2
  5. package/public/assets/{agent-profiles-CrMemMkZ.js.map → agent-profiles-Rwxrcf9F.js.map} +1 -1
  6. package/public/assets/{agents-Bl-rrgOy.js → agents-Dp1EXJc8.js} +2 -2
  7. package/public/assets/{agents-Bl-rrgOy.js.map → agents-Dp1EXJc8.js.map} +1 -1
  8. package/public/assets/{analytics-a663ak56.js → analytics-D5OT5ajj.js} +2 -2
  9. package/public/assets/{analytics-a663ak56.js.map → analytics-D5OT5ajj.js.map} +1 -1
  10. package/public/assets/automation-Dm6rXNxK.js +2 -0
  11. package/public/assets/{automation-CiaLThdO.js.map → automation-Dm6rXNxK.js.map} +1 -1
  12. package/public/assets/{branch-state-badge-D4ur3m3_.js → branch-state-badge-FX5Yww2s.js} +2 -2
  13. package/public/assets/{branch-state-badge-D4ur3m3_.js.map → branch-state-badge-FX5Yww2s.js.map} +1 -1
  14. package/public/assets/{channels-o9KLTHoK.js → channels--rdAiX17.js} +2 -2
  15. package/public/assets/{channels-o9KLTHoK.js.map → channels--rdAiX17.js.map} +1 -1
  16. package/public/assets/chat-JZAEDGfX.js +2 -0
  17. package/public/assets/chat-JZAEDGfX.js.map +1 -0
  18. package/public/assets/{connectors-CdC806mA.js → connectors-Bx4gzvNf.js} +2 -2
  19. package/public/assets/{connectors-CdC806mA.js.map → connectors-Bx4gzvNf.js.map} +1 -1
  20. package/public/assets/display-Bebqs1qu.js +3 -0
  21. package/public/assets/display-Bebqs1qu.js.map +1 -0
  22. package/public/assets/{formatted-body-impl-Ca74OAEH.js → formatted-body-impl-CVq4qHix.js} +2 -2
  23. package/public/assets/{formatted-body-impl-Ca74OAEH.js.map → formatted-body-impl-CVq4qHix.js.map} +1 -1
  24. package/public/assets/{index-C_33ymaw.js → index-BHRtR4q7.js} +8 -8
  25. package/public/assets/{index-C_33ymaw.js.map → index-BHRtR4q7.js.map} +1 -1
  26. package/public/assets/{insights-ClI68s39.js → insights-yJFgCa3o.js} +2 -2
  27. package/public/assets/{insights-ClI68s39.js.map → insights-yJFgCa3o.js.map} +1 -1
  28. package/public/assets/{integrations-1nxMizDY.js → integrations-k1HIONjo.js} +2 -2
  29. package/public/assets/{integrations-1nxMizDY.js.map → integrations-k1HIONjo.js.map} +1 -1
  30. package/public/assets/maintenance-CsoOFBXx.js +2 -0
  31. package/public/assets/{maintenance-DiFNzNPN.js.map → maintenance-CsoOFBXx.js.map} +1 -1
  32. package/public/assets/{managed-agents-Do3dKvfj.js → managed-agents-Q3HuVjGg.js} +2 -2
  33. package/public/assets/{managed-agents-Do3dKvfj.js.map → managed-agents-Q3HuVjGg.js.map} +1 -1
  34. package/public/assets/{markdown-preview-impl-CLA0J255.js → markdown-preview-impl-CnsMjrnu.js} +2 -2
  35. package/public/assets/{markdown-preview-impl-CLA0J255.js.map → markdown-preview-impl-CnsMjrnu.js.map} +1 -1
  36. package/public/assets/{memory-IjwqFzBd.js → memory-D3-K5eJS.js} +2 -2
  37. package/public/assets/{memory-IjwqFzBd.js.map → memory-D3-K5eJS.js.map} +1 -1
  38. package/public/assets/{messages-DjvWqHyn.js → messages-B4lCP5rS.js} +2 -2
  39. package/public/assets/{messages-DjvWqHyn.js.map → messages-B4lCP5rS.js.map} +1 -1
  40. package/public/assets/{orchestrators-D2IqDxDT.js → orchestrators-CRoZtLeQ.js} +2 -2
  41. package/public/assets/{orchestrators-D2IqDxDT.js.map → orchestrators-CRoZtLeQ.js.map} +1 -1
  42. package/public/assets/{overview-DKC3TbAh.js → overview-CxCU2fOF.js} +2 -2
  43. package/public/assets/{overview-DKC3TbAh.js.map → overview-CxCU2fOF.js.map} +1 -1
  44. package/public/assets/pairs-unqjPlmq.js +2 -0
  45. package/public/assets/{pairs-WpKCPE1n.js.map → pairs-unqjPlmq.js.map} +1 -1
  46. package/public/assets/{security-BF7ZtPQe.js → security-B7HhSYNy.js} +2 -2
  47. package/public/assets/{security-BF7ZtPQe.js.map → security-B7HhSYNy.js.map} +1 -1
  48. package/public/assets/{settings-CQnjrTa-.js → settings-B9NDhsAb.js} +2 -2
  49. package/public/assets/{settings-CQnjrTa-.js.map → settings-B9NDhsAb.js.map} +1 -1
  50. package/public/assets/store-DiSzYHj9.js +9 -0
  51. package/public/assets/{store-C9VcSo05.js.map → store-DiSzYHj9.js.map} +1 -1
  52. package/public/assets/{tasks-CbN_GSSb.js → tasks-CIQolvNm.js} +2 -2
  53. package/public/assets/{tasks-CbN_GSSb.js.map → tasks-CIQolvNm.js.map} +1 -1
  54. package/public/assets/{terminal-viewer-impl-BJRohThT.js → terminal-viewer-impl-DCifVqFR.js} +2 -2
  55. package/public/assets/{terminal-viewer-impl-BJRohThT.js.map → terminal-viewer-impl-DCifVqFR.js.map} +1 -1
  56. package/public/assets/{work-queue-C5xLBLmm.js → work-queue-Dr3c1V6O.js} +2 -2
  57. package/public/assets/{work-queue-C5xLBLmm.js.map → work-queue-Dr3c1V6O.js.map} +1 -1
  58. package/public/assets/{workspaces-D91H3wDX.js → workspaces-B1Jxop7h.js} +3 -3
  59. package/public/assets/{workspaces-D91H3wDX.js.map → workspaces-B1Jxop7h.js.map} +1 -1
  60. package/public/index.html +3 -3
  61. package/runner/src/adapter.ts +1 -1
  62. package/src/agent-lifecycle-events.ts +137 -0
  63. package/src/artifact-storage.ts +3 -5
  64. package/src/cli/_shared.ts +80 -0
  65. package/src/cli/agent-detect.ts +188 -0
  66. package/src/cli/agent-meta.ts +95 -0
  67. package/src/cli/context-probe.ts +88 -0
  68. package/src/cli/daemon.ts +111 -0
  69. package/src/cli/dev.ts +173 -0
  70. package/src/cli/index.ts +361 -0
  71. package/src/cli/introspect.ts +73 -0
  72. package/src/cli/memory.ts +37 -0
  73. package/src/cli/message.ts +201 -0
  74. package/src/cli/orchestrator.ts +227 -0
  75. package/src/cli/pair.ts +125 -0
  76. package/src/cli/provider.ts +209 -0
  77. package/src/cli/recipe.ts +110 -0
  78. package/src/cli/reply.ts +141 -0
  79. package/src/cli/setup.ts +57 -0
  80. package/src/cli/steward.ts +59 -0
  81. package/src/cli/token.ts +81 -0
  82. package/src/cli/upgrade.ts +193 -0
  83. package/src/cli/workspace.ts +215 -0
  84. package/src/cli.ts +4 -2718
  85. package/src/config-store.ts +10 -6
  86. package/src/maintenance.ts +4 -0
  87. package/src/mcp-errors.ts +7 -0
  88. package/src/mcp.ts +32 -34
  89. package/src/routes/agents-spawn.ts +9 -1
  90. package/src/routes/agents.ts +5 -0
  91. package/src/routes/commands.ts +15 -0
  92. package/src/spawn-targets.ts +159 -0
  93. package/src/utils.ts +16 -1
  94. package/public/assets/automation-CiaLThdO.js +0 -2
  95. package/public/assets/chat-5hvHZcAe.js +0 -2
  96. package/public/assets/chat-5hvHZcAe.js.map +0 -1
  97. package/public/assets/display-JI19Vc7L.js +0 -3
  98. package/public/assets/display-JI19Vc7L.js.map +0 -1
  99. package/public/assets/maintenance-DiFNzNPN.js +0 -2
  100. package/public/assets/pairs-WpKCPE1n.js +0 -2
  101. package/public/assets/store-C9VcSo05.js +0 -9
@@ -469,17 +469,21 @@ function validateInsightsConfig(value: unknown): InsightsConfig {
469
469
  const NOTIFICATIONS_CONFIG_DEFAULTS: NotificationsConfig = {
470
470
  enabled: true,
471
471
  branchLanded: true,
472
+ agentReady: true,
473
+ agentExited: true,
474
+ agentSpawnFailed: true,
472
475
  };
473
476
 
474
477
  function validateNotificationsConfig(value: unknown): NotificationsConfig {
475
478
  if (!isRecord(value)) throw new ValidationError("notifications config value must be an object");
479
+ const bool = (key: keyof NotificationsConfig): boolean =>
480
+ value[key] === undefined ? NOTIFICATIONS_CONFIG_DEFAULTS[key] : cleanBoolean(value[key], key);
476
481
  return {
477
- enabled: value.enabled === undefined
478
- ? NOTIFICATIONS_CONFIG_DEFAULTS.enabled
479
- : cleanBoolean(value.enabled, "enabled"),
480
- branchLanded: value.branchLanded === undefined
481
- ? NOTIFICATIONS_CONFIG_DEFAULTS.branchLanded
482
- : cleanBoolean(value.branchLanded, "branchLanded"),
482
+ enabled: bool("enabled"),
483
+ branchLanded: bool("branchLanded"),
484
+ agentReady: bool("agentReady"),
485
+ agentExited: bool("agentExited"),
486
+ agentSpawnFailed: bool("agentSpawnFailed"),
483
487
  };
484
488
  }
485
489
 
@@ -34,6 +34,7 @@ import {
34
34
  import type { WorkspaceMergePreview, WorkspaceRecord, WorkspaceStatus } from "./types";
35
35
  import { requestWorkspaceMerge } from "./workspace-merge";
36
36
  import { reconcileLandedWorkspace } from "./branch-landed";
37
+ import { notifyAgentOffline } from "./agent-lifecycle-events";
37
38
  import { workspaceActiveClaim } from "./workspace-claim";
38
39
  import { reapOrphanedWorktrees } from "./workspace-orphans";
39
40
  import { deriveBranchState, READY_TO_LAND_STATUSES, TERMINAL_WORKSPACE_STATUSES } from "./workspace-phase";
@@ -252,6 +253,9 @@ const definitions: MaintenanceJobDefinition[] = [
252
253
  for (const id of reapedAgentIds) {
253
254
  emitAgentStatus(id);
254
255
  getLifecycleManager().onAgentDisappeared(id);
256
+ // #308 — if a reaped agent was a spawned child, wake its parent: exited (it had become
257
+ // ready) vs spawn_failed (it never did — crash-loop / onboarding gate). No-op otherwise.
258
+ notifyAgentOffline(id, "heartbeat lost");
255
259
  createActivityEvent({
256
260
  clientId: "server-agent-" + id + "-heartbeat-lost-" + Date.now(),
257
261
  kind: "state",
@@ -0,0 +1,7 @@
1
+ // Typed MCP errors shared between the MCP endpoint (src/mcp.ts) and the spawn-target
2
+ // selection it delegates (src/spawn-targets.ts). callTool maps these to JSON-RPC error
3
+ // codes + HTTP statuses (auth → 403/-32001, not-found → 404/-32004, else 400/-32602).
4
+ // Kept in their own module so spawn-targets.ts can throw them without importing mcp.ts
5
+ // (which would be a cycle: mcp.ts → spawn-targets.ts → mcp.ts).
6
+ export class McpAuthError extends Error {}
7
+ export class McpNotFoundError extends Error {}
package/src/mcp.ts CHANGED
@@ -8,6 +8,8 @@ import { listManagedOrchestratorsForAgent } from "./orchestrator-lookup";
8
8
  import { bytesToStream, readBodyBytes } from "./http-body";
9
9
  import { MAX_BODY_BYTES, VERSION } from "./config";
10
10
  import { getManagedAgentState, getSpawnPolicy, listSpawnPolicies } from "./config-store";
11
+ import { buildSpawnTargets, selectSpawnOrchestrator, spawnCapablePrimer } from "./spawn-targets";
12
+ import { McpAuthError, McpNotFoundError } from "./mcp-errors";
11
13
  import {
12
14
  countLiveSpawnedAgents,
13
15
  createArtifact,
@@ -274,6 +276,16 @@ const TOOLS: ToolDefinition[] = [
274
276
  additionalProperties: false,
275
277
  },
276
278
  },
279
+ {
280
+ name: "relay_spawn_targets",
281
+ description: "Return the LIVE spawn capability matrix before you spawn: which orchestrators/hosts are online, which providers actually run on each (orchestrator-probed, not a static guess), each provider's models with per-model effort levels + defaults, your own host (isSelf) and remaining spawn quota, and named profiles you can spawn by role. Call this first whenever you decide to spawn — it removes the guesswork that otherwise only surfaces as a runtime rejection. Requires the command:spawn scope.",
282
+ requiredScopes: ["command:spawn"],
283
+ inputSchema: {
284
+ type: "object",
285
+ properties: {},
286
+ additionalProperties: false,
287
+ },
288
+ },
277
289
  {
278
290
  name: "relay_shutdown_agent",
279
291
  description: "Shut down an agent through Relay's orchestrator. Gated: requires the command:shutdown scope; an agent caller may only target its OWN live spawned children. Admin tokens keep full reach.",
@@ -413,12 +425,18 @@ export async function postMcp(req: Request): Promise<Response> {
413
425
  if (method === "initialize") {
414
426
  // Mode-tailored primer (#214 §3): surface the worktree merge-back map only
415
427
  // to callers that own an isolated worktree; shared-workspace agents omit it.
428
+ // #308: append the spawn primer only when the token actually grants command:spawn —
429
+ // authoritative scope-gating, eager + thin, so non-spawn agents get nothing.
416
430
  const worktree = callerIsolatedWorkspace(auth);
431
+ const instructions = [
432
+ worktree ? worktreeMcpInstructions(worktree) : "",
433
+ spawnCapablePrimer({ canSpawn: hasAnyScope(auth, ["command:spawn"]), quota: auth.component?.constraints?.maxSpawnedAgents }),
434
+ ].filter(Boolean).join("\n\n");
417
435
  return Response.json(jsonRpcResult(id, {
418
436
  protocolVersion: MCP_PROTOCOL_VERSION,
419
437
  capabilities: { tools: {} },
420
438
  serverInfo: { name: "agent-relay", title: "Agent Relay", version: VERSION },
421
- ...(worktree ? { instructions: worktreeMcpInstructions(worktree) } : {}),
439
+ ...(instructions ? { instructions } : {}),
422
440
  }));
423
441
  }
424
442
  if (method === "notifications/initialized") {
@@ -488,6 +506,7 @@ async function callTool(auth: McpAuthContext, params: unknown): Promise<Record<s
488
506
  else if (name === "relay_find_agents") result = relayFindAgents(auth, args);
489
507
  else if (name === "relay_whoami") result = relayWhoami(auth);
490
508
  else if (name === "relay_spawn_agent") result = await relaySpawnAgent(auth, args);
509
+ else if (name === "relay_spawn_targets") result = relaySpawnTargets(auth);
491
510
  else if (name === "relay_shutdown_agent") result = relayShutdownAgent(auth, args);
492
511
  else if (name === "relay_workspace_status") result = await relayWorkspaceStatus(auth, args);
493
512
  else if (name === "relay_workspace_list") result = relayWorkspaceList(auth, args);
@@ -774,8 +793,10 @@ async function relaySpawnAgent(auth: McpAuthContext, args: Record<string, unknow
774
793
  const preferHost = callerId ? getAgent(callerId)?.machine : undefined;
775
794
  const orchestrator = selectSpawnOrchestrator(provider, optionalString(args.orchestratorId, "orchestratorId", 200), cwd, preferHost);
776
795
  const resolvedCwd = cwd || orchestrator.baseDir;
796
+ // #308 §3 — cwd must resolve within the TARGET host's base dir. A path valid on your own host
797
+ // may not exist on a different orchestrator, so validate against the chosen host and say which.
777
798
  if (cwd && !isPathWithinBase(cwd, orchestrator.baseDir)) {
778
- throw new ValidationError(`cwd must be within orchestrator base directory: ${orchestrator.baseDir}`);
799
+ throw new ValidationError(`cwd '${cwd}' is not within ${orchestrator.id} (host ${orchestrator.hostname})'s base dir '${orchestrator.baseDir}' — a path valid on your host may not exist on the target. Pass a cwd under that base dir, or omit cwd to default to it.`);
779
800
  }
780
801
  const selection = providerSelection(provider, args);
781
802
  const approvalMode = optionalEnum(args.approvalMode, "approvalMode", APPROVAL_MODES) as SpawnApprovalMode | undefined ?? "guarded";
@@ -843,6 +864,9 @@ async function relaySpawnAgent(auth: McpAuthContext, args: Record<string, unknow
843
864
  requestedVia: "mcp",
844
865
  requestedAt: Date.now(),
845
866
  orchestratorId: orchestrator.id,
867
+ // #308 — stamp the spawning parent so a failed agent.spawn command can be routed back to
868
+ // it (the child never registers, so there's no agent record to resolve `spawnedBy` from).
869
+ ...(callerId ? { extra: { spawnedBy: callerId } } : {}),
846
870
  }),
847
871
  });
848
872
  emitCommand(command);
@@ -1087,35 +1111,12 @@ function policyStatusPayload(policy: NonNullable<ReturnType<typeof getSpawnPolic
1087
1111
  };
1088
1112
  }
1089
1113
 
1090
- export function selectSpawnOrchestrator(
1091
- provider: SpawnProvider,
1092
- orchestratorId?: string,
1093
- cwd?: string,
1094
- preferHost?: string,
1095
- ): NonNullable<ReturnType<typeof getOrchestrator>> {
1096
- if (orchestratorId) {
1097
- const orchestrator = getOrchestrator(orchestratorId);
1098
- if (!orchestrator) throw new McpNotFoundError(`orchestrator ${orchestratorId} not found`);
1099
- if (orchestrator.status !== "online") throw new ValidationError("orchestrator is offline");
1100
- if (!orchestrator.providers.includes(provider)) throw new ValidationError(`orchestrator does not have provider available: ${provider}`);
1101
- return orchestrator;
1102
- }
1103
- const candidates = listOrchestrators().filter((item) => item.status === "online" && item.providers.includes(provider));
1104
- if (cwd) {
1105
- const match = candidates.find((item) => isPathWithinBase(cwd, item.baseDir));
1106
- if (match) return match;
1107
- }
1108
- // #255: with neither an explicit id nor a cwd to pin the host, default to the CALLER's own
1109
- // host instead of silently grabbing candidates[0] (a foreign host whose baseDir would then
1110
- // reject the caller's cwd — the footgun the spawn recipe warned about). An agent's `machine`
1111
- // is its OS hostname; match it against the orchestrator hostname (or id, defensively).
1112
- if (preferHost) {
1113
- const own = candidates.find((item) => item.hostname === preferHost || item.id === preferHost);
1114
- if (own) return own;
1115
- }
1116
- const orchestrator = candidates[0];
1117
- if (!orchestrator) throw new McpNotFoundError(`no orchestrator available for provider: ${provider}`);
1118
- return orchestrator;
1114
+ // #308 — thin auth wrapper over the spawn capability matrix (src/spawn-targets.ts owns the build).
1115
+ function relaySpawnTargets(auth: McpAuthContext): Record<string, unknown> {
1116
+ return buildSpawnTargets({
1117
+ callerId: callerAgentId(auth),
1118
+ quota: auth.component?.constraints?.maxSpawnedAgents ?? 0,
1119
+ });
1119
1120
  }
1120
1121
 
1121
1122
  function selectControlOrchestrator(input: {
@@ -1396,6 +1397,3 @@ function optionalAttachments(value: unknown): AttachmentRef[] | undefined {
1396
1397
  function encodedLength(value: string): number {
1397
1398
  return new TextEncoder().encode(value).byteLength;
1398
1399
  }
1399
-
1400
- class McpAuthError extends Error {}
1401
- export class McpNotFoundError extends Error {}
@@ -1,6 +1,7 @@
1
1
  // Auto-split from routes.ts (#299). Domain: agents-spawn.
2
2
  import { APPROVAL_MODES, SPAWN_PROVIDERS, VALID_EFFORTS, VALID_WORKSPACE_MODES, isRecord } from "agent-relay-sdk";
3
- import { McpNotFoundError, selectSpawnOrchestrator } from "../mcp";
3
+ import { McpNotFoundError } from "../mcp-errors";
4
+ import { selectSpawnOrchestrator } from "../spawn-targets";
4
5
  import { VALID_AGENT_STATUSES, auditEvent, authAuditMetadata, authorizeRoute, emitCommand, error, json, metaString, parseBody, spawnRequestId, type Handler } from "./_shared";
5
6
  import { ValidationError, getAgent, getOrchestrator, listAgents, resolveQueuedPolicyMessages, upsertAgent } from "../db";
6
7
  import { buildSpawnCommand, resolveSpawnModelParams, type SpawnModelParams } from "../spawn-command";
@@ -8,6 +9,7 @@ import { cleanMeta, cleanNullableString, cleanString, cleanStringArray, optional
8
9
  import { createCommand } from "../commands-db";
9
10
  import { emitAgentStatus, emitManagedAgentStateChanged, emitMessageAvailable, emitMessageDeliveryUpdated, emitNewMessage } from "../sse";
10
11
  import { getAgentProfile, getManagedAgentState, updateManagedAgentState } from "../config-store";
12
+ import { notifyAgentReady } from "../agent-lifecycle-events";
11
13
  import { getCompactionWatch } from "../compaction-watch";
12
14
  import { getComponentAuth } from "../security";
13
15
  import { isPathWithinBase } from "../utils";
@@ -98,6 +100,12 @@ export const postAgent: Handler = async (req) => {
98
100
  }
99
101
  }
100
102
  emitAgentStatus(agent.id);
103
+ // #308 — a spawned child registering already-ready (the common isolated-worktree case: it
104
+ // comes up ready+idle) is genuinely ready; wake its parent so it can stop block-polling.
105
+ // Fire on the first-ever ready (no prior record, or prior record wasn't ready yet) — the
106
+ // ready/false→true flip path is handled in patchAgentReady. notifyAgentReady no-ops for
107
+ // non-spawned agents and dedups repeats.
108
+ if (agent.ready && !existing?.ready) notifyAgentReady(agent.id);
101
109
  // A real PreCompact / SessionStart(clear) hook reports progress via the
102
110
  // agent's timelineEvent — clears any pending stall watch for this agent.
103
111
  // timelineEvent is latched, so pass its timestamp: only a fresh event
@@ -5,6 +5,7 @@ import { attachBranchState, attachBranchStates } from "../agent-branch-state";
5
5
  import { cleanStringArray } from "../validation";
6
6
  import { clearActiveMemories, memoryBroker } from "../memory-service";
7
7
  import { emitAgentRemoved, emitAgentStatus } from "../sse";
8
+ import { notifyAgentReady } from "../agent-lifecycle-events";
8
9
  import { getCompactionWatch } from "../compaction-watch";
9
10
  import { isRecord } from "agent-relay-sdk";
10
11
  import { type AgentCard } from "../types";
@@ -231,6 +232,10 @@ export const patchAgentReady: Handler = async (req, params) => {
231
232
  const before = getAgent(params.id!);
232
233
  if (!markReady(params.id!, body.ready, guard)) return error("agent not found", 404);
233
234
  auditAgentStateTransition(params.id!, before, getAgent(params.id!));
235
+ // #308 — a spawned child flipping false→true ready is "genuinely ready" (past onboarding):
236
+ // wake its parent so it can stop block-polling. notifyAgentReady no-ops for non-spawned
237
+ // agents and dedups repeat readies.
238
+ if (body.ready && !before?.ready) notifyAgentReady(params.id!);
234
239
  } catch (e) {
235
240
  if (e instanceof ValidationError) return error(e.message, 400);
236
241
  throw e;
@@ -11,6 +11,7 @@ import { getCompactionWatch } from "../compaction-watch";
11
11
  import { isRecord } from "agent-relay-sdk";
12
12
  import { isRequestAuthorizedFor } from "../security";
13
13
  import { notifyBranchLanded } from "../branch-landed";
14
+ import { notifyAgentSpawnFailed } from "../agent-lifecycle-events";
14
15
  import { type Command, type CommandStatus, type CreateCommandInput, type WorkspaceStatus } from "../types";
15
16
 
16
17
  const VALID_COMMAND_STATUSES = ["pending", "accepted", "running", "succeeded", "failed", "timed_out", "rejected", "canceled"] as const;
@@ -298,6 +299,20 @@ export const patchCommand: Handler = async (req, params) => {
298
299
  patchWorkspaceMetadata(workspaceId, { lastDepsRefresh: command.result, lastDepsRefreshCommandId: command.id, lastDepsRefreshAt: Date.now() });
299
300
  }
300
301
  }
302
+ // #308 — a failed agent.spawn command never produces a child agent, so route the failure
303
+ // straight to the spawning parent stamped on the command (provider unavailable, bad cwd,
304
+ // launch error). The never-became-ready crash-loop case is covered separately by the reaper.
305
+ if (command.type === "agent.spawn" && command.status === "failed") {
306
+ const parent = isRecord(command.params) ? cleanString(command.params.spawnedBy, "params.spawnedBy", { max: 240 }) : undefined;
307
+ if (parent) {
308
+ notifyAgentSpawnFailed({
309
+ parent,
310
+ spawnRequestId: isRecord(command.params) ? cleanString(command.params.spawnRequestId, "params.spawnRequestId", { max: 160 }) : undefined,
311
+ provider: isRecord(command.params) ? cleanString(command.params.provider, "params.provider", { max: 40 }) : undefined,
312
+ reason: command.error ?? "spawn command failed",
313
+ });
314
+ }
315
+ }
301
316
  settleFailedOrchestratorUpgrade(command);
302
317
  emitCommand(command);
303
318
  auditCommandOutcome(command);
@@ -0,0 +1,159 @@
1
+ import { countLiveSpawnedAgents, getAgent, getOrchestrator, listOrchestrators, ValidationError } from "./db";
2
+ import { listAgentProfiles } from "./config-store";
3
+ import { effectiveProviderCatalogList } from "./provider-catalog-store";
4
+ import { isPathWithinBase } from "./utils";
5
+ import { McpNotFoundError } from "./mcp-errors";
6
+ import type { Orchestrator, ProviderCatalogSummary, SpawnProvider } from "./types";
7
+
8
+ // #308 — spawn discovery + target selection. Home for the live capability matrix that
9
+ // relay_spawn_targets returns, the scope-gated startup primer, and the orchestrator-selection
10
+ // (with structured "name the nearest valid alternative" errors). Lives in its own module so the
11
+ // already-large src/mcp.ts only carries the thin auth wrappers that call in here.
12
+
13
+ // === relay_spawn_targets: the live spawn capability matrix ===
14
+
15
+ // Reuses the per-host catalog each orchestrator already reports on heartbeat (orchestrator-probed
16
+ // availability, not a static config guess), so the agent learns what it can spawn (and where)
17
+ // upfront instead of from a runtime rejection. Caller identity + quota come from the MCP auth.
18
+ export function buildSpawnTargets(input: { callerId?: string; quota: number }): Record<string, unknown> {
19
+ const me = input.callerId ? getAgent(input.callerId) : undefined;
20
+ const preferHost = me?.machine;
21
+ const orchestrators = listOrchestrators();
22
+ const self = preferHost ? orchestrators.find((o) => o.hostname === preferHost || o.id === preferHost) : undefined;
23
+ const liveChildren = input.callerId ? countLiveSpawnedAgents(input.callerId) : 0;
24
+
25
+ return {
26
+ self: {
27
+ orchestratorId: self?.id,
28
+ host: self?.hostname ?? preferHost,
29
+ spawnQuota: { max: input.quota, liveChildren, remaining: Math.max(0, input.quota - liveChildren) },
30
+ },
31
+ orchestrators: orchestrators
32
+ .filter((o) => o.status === "online")
33
+ .map((o) => ({
34
+ orchestratorId: o.id,
35
+ host: o.hostname,
36
+ online: true,
37
+ isSelf: self ? o.id === self.id : false,
38
+ providers: providerMatrixFor(o),
39
+ })),
40
+ profiles: spawnProfilesSummary(),
41
+ guidance:
42
+ "Default to your own host (isSelf:true); only set orchestratorId to spawn onto another host that runs a provider/model yours doesn't. Omitting model/effort uses the provider default (marked below). Prefer a named profile over assembling raw knobs; set spawnRequestId for idempotency. After spawning you'll be pushed agent.ready / agent.exited / agent.spawn_failed — don't block-poll.",
43
+ };
44
+ }
45
+
46
+ // The subset of the (orchestrator-reported OR global) catalog the matrix projects. Both
47
+ // ProviderCatalogSummary and ProviderCatalogEntry are structurally compatible with this shape.
48
+ interface SpawnCatalogish {
49
+ provider: SpawnProvider;
50
+ defaultModel?: string;
51
+ models: Array<{ alias: string; providerModel: string; efforts: readonly string[]; defaultEffort?: string }>;
52
+ }
53
+
54
+ function providerMatrixFor(o: Orchestrator): Array<Record<string, unknown>> {
55
+ const reported: ProviderCatalogSummary[] = o.providerCatalog ?? [];
56
+ const global = effectiveProviderCatalogList();
57
+ return o.providers.map((provider) => {
58
+ const cat: SpawnCatalogish | undefined =
59
+ reported.find((s) => s.provider === provider) ?? global.find((e) => e.provider === provider);
60
+ return {
61
+ provider,
62
+ available: true,
63
+ ...(cat
64
+ ? {
65
+ defaultModel: cat.defaultModel,
66
+ models: cat.models.map((m) => ({
67
+ id: m.alias,
68
+ providerModel: m.providerModel,
69
+ default: cat.defaultModel === m.alias,
70
+ effortLevels: m.efforts,
71
+ ...(m.defaultEffort ? { defaultEffort: m.defaultEffort } : {}),
72
+ })),
73
+ }
74
+ : { models: [] }),
75
+ };
76
+ });
77
+ }
78
+
79
+ function spawnProfilesSummary(): Array<Record<string, unknown>> {
80
+ return listAgentProfiles().map(({ key, value }) => ({
81
+ name: value.name ?? key,
82
+ description: value.description,
83
+ provider: value.provider,
84
+ approvalMode: value.permissions?.mode,
85
+ ...(value.maxSpawnedAgents !== undefined ? { maxSpawnedAgents: value.maxSpawnedAgents } : {}),
86
+ builtIn: value.builtIn ?? false,
87
+ }));
88
+ }
89
+
90
+ // A human-readable list of (provider@host) an agent can spawn onto right now, so a structured
91
+ // rejection names the nearest valid alternative instead of a bare "offline".
92
+ function spawnAlternatives(provider?: SpawnProvider): string {
93
+ const live = listOrchestrators()
94
+ .filter((o) => o.status === "online")
95
+ .flatMap((o) => o.providers.filter((p) => !provider || p === provider).map((p) => `${p}@${o.hostname}`));
96
+ return live.length ? live.join(", ") : "none currently online";
97
+ }
98
+
99
+ // === Startup primer (eager, scope-gated) ===
100
+
101
+ // Thin, eager startup primer for spawn-capable agents only (canSpawn = token carries
102
+ // command:spawn). Names the exact discovery tool (relay_spawn_targets) so there's zero discovery
103
+ // cost under lazy tool-loading, plus the same-host-default + fire-and-forget rules. The full matrix
104
+ // stays lazy — fetched only when the agent actually decides to spawn. Returns "" for non-spawn agents.
105
+ export function spawnCapablePrimer(input: { canSpawn: boolean; quota?: number }): string {
106
+ if (!input.canSpawn) return "";
107
+ const quotaLabel = typeof input.quota === "number" && input.quota > 0 ? ` (live-children quota: ${input.quota})` : "";
108
+ return [
109
+ `You can spawn long-living child agents${quotaLabel}. Shut your own down with relay_shutdown_agent.`,
110
+ "- BEFORE spawning, call relay_spawn_targets: it returns the LIVE matrix of hosts × providers × models × per-model effort, your remaining quota, and named profiles. Don't guess — that tool is the discovery path.",
111
+ "- Default to your OWN host. Only set orchestratorId to spawn onto another host when it runs a provider/model yours doesn't, or the work must run there.",
112
+ "- Prefer a named profile (profile: \"<name>\") over hand-assembling provider+model+effort+approvalMode. Omitting model/effort uses the provider default. Lean to the cheapest provider/model/effort that fits; escalate only for complex work.",
113
+ "- Set spawnRequestId for idempotency so a retried spawn doesn't double-create.",
114
+ "- Spawn is fire-and-forget: pass waitForRegistrationMs:0 and keep working — you'll be PUSHED agent.ready when the child is genuinely ready (past onboarding), or agent.spawn_failed / agent.exited otherwise. Don't block-poll.",
115
+ "- Spawned children cannot themselves spawn (no grandchildren).",
116
+ ].join("\n");
117
+ }
118
+
119
+ // === Orchestrator selection ===
120
+
121
+ export function selectSpawnOrchestrator(
122
+ provider: SpawnProvider,
123
+ orchestratorId?: string,
124
+ cwd?: string,
125
+ preferHost?: string,
126
+ ): Orchestrator {
127
+ if (orchestratorId) {
128
+ const orchestrator = getOrchestrator(orchestratorId);
129
+ if (!orchestrator) throw new McpNotFoundError(`orchestrator ${orchestratorId} not found`);
130
+ // #308 — name the nearest valid alternative on a miss instead of a bare "offline"/"unavailable",
131
+ // so the agent can re-target without a second relay_spawn_targets round trip.
132
+ if (orchestrator.status !== "online") {
133
+ throw new ValidationError(`orchestrator ${orchestratorId} is offline. Available: ${spawnAlternatives(provider)} (call relay_spawn_targets for the live matrix).`);
134
+ }
135
+ if (!orchestrator.providers.includes(provider)) {
136
+ const has = orchestrator.providers.length ? orchestrator.providers.join(", ") : "no providers";
137
+ throw new ValidationError(`orchestrator ${orchestratorId} does not run provider '${provider}' (it has: ${has}). Available for '${provider}': ${spawnAlternatives(provider)}.`);
138
+ }
139
+ return orchestrator;
140
+ }
141
+ const candidates = listOrchestrators().filter((item) => item.status === "online" && item.providers.includes(provider));
142
+ if (cwd) {
143
+ const match = candidates.find((item) => isPathWithinBase(cwd, item.baseDir));
144
+ if (match) return match;
145
+ }
146
+ // #255: with neither an explicit id nor a cwd to pin the host, default to the CALLER's own
147
+ // host instead of silently grabbing candidates[0] (a foreign host whose baseDir would then
148
+ // reject the caller's cwd — the footgun the spawn recipe warned about). An agent's `machine`
149
+ // is its OS hostname; match it against the orchestrator hostname (or id, defensively).
150
+ if (preferHost) {
151
+ const own = candidates.find((item) => item.hostname === preferHost || item.id === preferHost);
152
+ if (own) return own;
153
+ }
154
+ const orchestrator = candidates[0];
155
+ if (!orchestrator) {
156
+ throw new McpNotFoundError(`no online orchestrator runs provider '${provider}'. Available: ${spawnAlternatives()} (call relay_spawn_targets for the live matrix).`);
157
+ }
158
+ return orchestrator;
159
+ }
package/src/utils.ts CHANGED
@@ -1,4 +1,19 @@
1
- import { isAbsolute, relative, resolve } from "node:path";
1
+ import { isAbsolute, join, relative, resolve } from "node:path";
2
+ import { homedir } from "node:os";
3
+
4
+ /**
5
+ * Expand a leading `~` / `~/` to the current user's home directory. Leaves any
6
+ * other value (absolute paths, `./rel`, embedded `~` mid-string) untouched.
7
+ *
8
+ * Folds the identical hand-rolled copies in `cli.ts` (`expandHomePath`, used for
9
+ * orchestrator `--base-dir`/`--runtime-prefix`/`--path-prefix`) and
10
+ * `artifact-storage.ts` (artifact root). Import this; never re-declare it.
11
+ */
12
+ export function expandTilde(value: string): string {
13
+ if (value === "~") return homedir();
14
+ if (value.startsWith("~/")) return join(homedir(), value.slice(2));
15
+ return value;
16
+ }
2
17
 
3
18
  /**
4
19
  * Path containment check — is `path` inside (or equal to) `baseDir`?
@@ -1,2 +0,0 @@
1
- import{r as e}from"./chunk-CilyBKbf.js";import{I as t,O as n,R as r,Vn as i,k as a,kn as o,kt as s,ln as c,m as l,z as u}from"./lucide-react-CD8Xl2U3.js";import{i as d,n as f,t as p}from"./store-C9VcSo05.js";import{R as m,m as h}from"./display-JI19Vc7L.js";import{t as g}from"./switch-22dlDUXs.js";import{t as _}from"./badge-t8zAwHW9.js";import{t as v}from"./button-DDA5P2YQ.js";import{t as y}from"./input-BW9UD3FM.js";import{c as b,n as x,r as S}from"./index-C_33ymaw.js";import{i as C,n as w,r as T,t as E}from"./card-CggxP1h9.js";var D=e(i(),1),O=o(),k={scheduled:`bg-blue-500/10 text-blue-400 border-blue-500/20`,dispatching:`bg-sky-500/10 text-sky-400 border-sky-500/20`,waiting_agent:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,running:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,succeeded:`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`,failed:`bg-red-500/10 text-red-400 border-red-500/20`,canceled:`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`,timed_out:`bg-red-500/10 text-red-400 border-red-500/20`},A=new Set([`scheduled`,`dispatching`,`waiting_agent`,`running`]);function j(e){return e.status===`timed_out`&&e.result?.runtimeBudgetElapsed===!0?`budget elapsed`:e.status}function M(e){return e.status===`timed_out`&&e.result?.runtimeBudgetElapsed===!0?`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`:k[e.status]||``}function N(e){return e.status===`timed_out`&&e.result?.runtimeBudgetElapsed===!0?void 0:e.error}function P(e){return A.has(e.status)}function F(e){return typeof e==`number`&&Number.isFinite(e)?e:void 0}function I(e){return typeof e==`string`&&e.length>0?e:void 0}function L(e){let t=Math.max(0,Math.round(e/1e3)),n=Math.floor(t/3600),r=Math.floor(t%3600/60),i=t%60;return n?`${n}h ${r}m`:r?`${r}m ${i}s`:`${i}s`}function R(e){return F(e.meta?.runtimeBudgetDeadlineAt)??F(e.result?.deadlineAt)}function z(e,t){if(!e.startedAt)return`-`;let n=e.finishedAt||(P(e)?t:e.updatedAt);return L(Number(n)-Number(e.startedAt))}function B(e,t){let n=R(e);if(!(!n||!P(e)))return L(n-t)}function V(e){return e.status===`timed_out`&&e.result?.runtimeBudgetElapsed===!0?`runtime budget elapsed`:e.error}function H(e,t,n){let r=e.targetAgentId||e.spawnedAgentId||I(e.meta?.runtimeAgentId),i=r?t.find(e=>e.id===r):void 0,a=r?n.flatMap(e=>e.managedAgents.map(t=>({orchestrator:e,managedAgent:t}))).find(({managedAgent:e})=>e.agentId===r):void 0,o=I(e.meta?.terminalSession)||I(e.meta?.tmuxSession)||I(e.meta?.sessionName)||I(i?.meta?.terminalSession)||I(i?.meta?.tmuxSession)||I(i?.meta?.sessionName)||a?.managedAgent.terminalSession||a?.managedAgent.tmuxSession||a?.managedAgent.sessionName,s=I(e.meta?.orchestratorId)||a?.orchestrator.id||e.orchestratorId;return o?{orchestratorId:s,session:o}:void 0}function U(e=``){return{name:``,description:``,enabled:!0,schedule:`0 8 * * *`,timezone:Intl.DateTimeFormat().resolvedOptions().timeZone||`UTC`,orchestratorId:e,targetMode:`existing_agent`,provider:`codex`,model:f.codex.defaultModel||``,effort:``,profile:`default-relay`,label:``,tags:``,capabilities:``,ifNoMatch:`fail`,cwd:``,approvalMode:`guarded`,workspaceMode:`inherit`,keepAlive:!1,budgetMinutes:``,warningMinutes:``,warningMessage:``,concurrencyPolicy:`skip`,title:``,body:``,severity:`info`,dedupeKey:``,externalUrl:``}}function W(e){let t=e.split(`,`).map(e=>e.trim()).filter(Boolean);return t.length?t:void 0}function G(e){let t=e.targetPolicy;return{...U(e.orchestratorId),id:e.id,name:e.name,description:e.description||``,enabled:e.enabled,schedule:e.schedule,timezone:e.timezone,orchestratorId:e.orchestratorId,targetMode:t.mode,provider:t.mode===`on_demand_agent`?t.provider:t.selector.provider||`codex`,model:t.mode===`on_demand_agent`&&(t.model||f[t.provider].defaultModel)||``,effort:t.mode===`on_demand_agent`&&t.effort||``,profile:t.mode===`on_demand_agent`&&t.profile||`default-relay`,label:t.mode===`existing_agent`&&t.selector.label||``,tags:t.mode===`existing_agent`?(t.selector.tags||[]).join(`, `):``,capabilities:t.mode===`existing_agent`?(t.selector.capabilities||[]).join(`, `):``,ifNoMatch:t.mode===`existing_agent`&&t.ifNoMatch||`fail`,cwd:t.mode===`on_demand_agent`&&t.cwd||``,approvalMode:t.mode===`on_demand_agent`&&t.approvalMode||`guarded`,workspaceMode:t.mode===`on_demand_agent`&&t.workspaceMode||`inherit`,keepAlive:t.mode===`on_demand_agent`?!!t.keepAlive:!1,budgetMinutes:t.mode===`on_demand_agent`&&t.runtimeBudget?String(Math.round(t.runtimeBudget.maxRuntimeMs/6e4)):``,warningMinutes:t.mode===`on_demand_agent`&&t.runtimeBudget?.warnAtMs!==void 0?String(Math.round(t.runtimeBudget.warnAtMs/6e4)):``,warningMessage:t.mode===`on_demand_agent`&&t.runtimeBudget?.warningMessage||``,concurrencyPolicy:e.concurrencyPolicy,title:e.taskTemplate.title,body:e.taskTemplate.body,severity:e.taskTemplate.severity||`info`,dedupeKey:e.taskTemplate.dedupeKey||``,externalUrl:e.taskTemplate.externalUrl||``}}function K(e){let t=Number(e.budgetMinutes),n=Number(e.warningMinutes),r=e.targetMode===`on_demand_agent`&&e.budgetMinutes.trim()&&Number.isFinite(t)?{maxRuntimeMs:Math.round(t*6e4),warnAtMs:e.warningMinutes.trim()&&Number.isFinite(n)?Math.round(n*6e4):void 0,warningMessage:e.warningMessage.trim()||void 0}:void 0,i=e.targetMode===`existing_agent`?{mode:`existing_agent`,selector:{provider:e.provider||void 0,label:e.label.trim()||void 0,tags:W(e.tags),capabilities:W(e.capabilities)},ifNoMatch:e.ifNoMatch}:{mode:`on_demand_agent`,provider:e.provider,model:e.model||void 0,effort:e.effort?e.effort:void 0,profile:e.profile||void 0,cwd:e.cwd.trim()||void 0,approvalMode:e.approvalMode,workspaceMode:e.workspaceMode,keepAlive:e.keepAlive,runtimeBudget:r};return{kind:`scheduled_task`,name:e.name.trim(),description:e.description.trim()||void 0,enabled:e.enabled,schedule:e.schedule.trim(),timezone:e.timezone.trim()||`UTC`,catchUpPolicy:`skip`,concurrencyPolicy:e.concurrencyPolicy,orchestratorId:e.orchestratorId,targetPolicy:i,taskTemplate:{title:e.title.trim(),body:e.body.trim(),severity:e.severity,dedupeKey:e.dedupeKey.trim()||void 0,externalUrl:e.externalUrl.trim()||void 0}}}function q(){let e=d(),n=p(e=>e.automations),r=p(e=>e.automationRuns),i=p(e=>e.orchestrators),a=p(e=>e.agents),o=p(e=>e.apiCall),s=p(e=>e.showError),l=p(e=>e.openConfirm),u=p(e=>e.fetchAutomations),f=p(e=>e.fetchAutomationRuns),m=p(e=>e.fetchTasks),h=p(e=>e.agentProfiles),[g,y]=(0,D.useState)(``),[b,x]=(0,D.useState)(()=>U(i[0]?.id||``)),[S,C]=(0,D.useState)(!1),[w,T]=(0,D.useState)(null),E=n.find(e=>e.id===g),k=i.filter(e=>e.status===`online`),A=r.filter(P);(0,D.useEffect)(()=>{Promise.all([u(),f()])},[]),(0,D.useEffect)(()=>{!b.orchestratorId&&i[0]&&x(e=>({...e,orchestratorId:i[0].id}))},[i]);let j=(0,D.useMemo)(()=>{let e=new Map;for(let t of r){let n=e.get(t.automationId)||[];n.push(t),e.set(t.automationId,n)}return e},[r]);function M(){y(``),x(U(i[0]?.id||``))}function N(e){y(e.id),x(G(e))}async function F(){C(!0);try{let e=K(b);if(b.id)await o(`PATCH`,`/automations/`+encodeURIComponent(b.id),e);else{let t=await o(`POST`,`/automations`,e);y(t.id),x(G(t))}await Promise.all([u(),f()])}catch(e){s(`Save Failed`,e.message)}finally{C(!1)}}async function I(e,t){try{await o(`PATCH`,`/automations/`+encodeURIComponent(e.id),t),await u()}catch(e){s(`Update Failed`,e.message)}}async function L(e){try{await o(`POST`,`/automations/`+encodeURIComponent(e.id)+`/run`),await Promise.all([f(),m()])}catch(e){s(`Run Failed`,e.message)}}function R(e){l(`Delete Automation`,`Delete automation "${e.name}"?`,async()=>{try{await o(`DELETE`,`/automations/`+encodeURIComponent(e.id)),g===e.id&&M(),await Promise.all([u(),f()])}catch(e){s(`Delete Failed`,e.message)}})}return(0,O.jsxs)(`div`,{className:`space-y-4`,children:[(0,O.jsxs)(`div`,{className:`flex flex-wrap items-center gap-3`,children:[(0,O.jsx)(c,{className:`w-5 h-5 text-muted-foreground`}),(0,O.jsxs)(`div`,{children:[(0,O.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Automation`}),(0,O.jsx)(`p`,{className:`text-xs text-muted-foreground`,children:`Scheduled tasks`})]}),(0,O.jsxs)(`div`,{className:`ml-auto flex flex-wrap items-center gap-2`,children:[(0,O.jsxs)(_,{variant:`outline`,className:`text-xs`,children:[n.length,` schedules`]}),(0,O.jsxs)(_,{variant:`outline`,className:`text-xs`,children:[A.length,` running`]}),(0,O.jsxs)(v,{size:`sm`,className:`gap-1`,onClick:M,children:[(0,O.jsx)(t,{className:`w-3.5 h-3.5`}),`New`]})]})]}),A.length>0&&(0,O.jsx)(J,{runs:A,automations:n,agents:a,orchestrators:i,now:e,logRunId:w,onToggleLogs:e=>T(t=>t===e?null:e)}),(0,O.jsxs)(`div`,{className:`grid gap-4 xl:grid-cols-[minmax(0,1fr)_420px]`,children:[(0,O.jsx)(`div`,{className:`space-y-3`,children:n.length===0?(0,O.jsx)(`div`,{className:`py-16 text-center text-sm text-muted-foreground`,children:`No automations`}):n.map(t=>(0,O.jsx)(Y,{automation:t,runs:j.get(t.id)||[],now:e,selected:g===t.id,onEdit:()=>N(t),onRun:()=>L(t),onToggle:()=>I(t,{enabled:!t.enabled}),onDelete:()=>R(t)},t.id))}),(0,O.jsx)(X,{form:b,agentProfiles:h,selected:E,saving:S,orchestrators:k.length?k:i,onChange:e=>x(t=>({...t,...e})),onSave:F})]}),(0,O.jsx)(Q,{runs:r,automations:n,agents:a,orchestrators:i,now:e,logRunId:w,onToggleLogs:e=>T(t=>t===e?null:e)})]})}function J({runs:e,automations:t,agents:n,orchestrators:r,now:i,logRunId:a,onToggleLogs:o}){let c=new Map(t.map(e=>[e.id,e.name]));return(0,O.jsxs)(`div`,{className:`rounded-lg border border-border bg-card/40`,children:[(0,O.jsx)(`div`,{className:`border-b border-border px-3 py-2 text-sm font-medium`,children:`Active runs`}),(0,O.jsx)(`div`,{className:`divide-y divide-border`,children:e.map(e=>{let t=H(e,n,r),l=B(e,i);return(0,O.jsxs)(`div`,{className:`space-y-3 px-3 py-3`,children:[(0,O.jsxs)(`div`,{className:`grid gap-3 text-xs md:grid-cols-[minmax(12rem,1fr)_7rem_7rem_7rem_8rem_auto] md:items-center`,children:[(0,O.jsxs)(`div`,{className:`min-w-0`,children:[(0,O.jsx)(`div`,{className:`truncate text-sm font-medium`,children:c.get(e.automationId)||e.automationId.slice(0,12)}),(0,O.jsx)(`div`,{className:`truncate font-mono text-muted-foreground`,children:e.targetAgentId||e.spawnedAgentId||I(e.meta?.onDemandLabel)||`-`})]}),(0,O.jsx)(_,{variant:`outline`,className:`border text-[10px] w-fit ${M(e)}`,children:j(e)}),(0,O.jsxs)(`div`,{children:[(0,O.jsx)(`span`,{className:`text-muted-foreground`,children:`elapsed `}),z(e,i)]}),(0,O.jsxs)(`div`,{children:[(0,O.jsx)(`span`,{className:`text-muted-foreground`,children:`left `}),l||`-`]}),(0,O.jsx)(`div`,{className:`font-mono text-muted-foreground`,children:e.taskId?`task #${e.taskId}`:`-`}),t&&(0,O.jsxs)(v,{type:`button`,size:`sm`,variant:`outline`,className:`h-7 gap-1 text-xs`,onClick:()=>o(e.id),children:[(0,O.jsx)(s,{className:`w-3 h-3`}),`Logs`]})]}),a===e.id&&t&&(0,O.jsx)(b,{orchestratorId:t.orchestratorId,session:t.session})]},e.id)})})]})}function Y({automation:e,runs:t,now:n,selected:i,onEdit:a,onRun:o,onToggle:s,onDelete:c}){let d=t[0],f=e.targetPolicy,p=f.mode===`existing_agent`?[f.selector.provider,f.selector.label?`label:${f.selector.label}`:``,...(f.selector.tags||[]).map(e=>`tag:${e}`),...(f.selector.capabilities||[]).map(e=>`cap:${e}`)].filter(Boolean).join(` `):`${f.provider} on demand`;return(0,O.jsxs)(E,{className:`${i?`ring-1 ring-primary/50`:``} ${e.enabled?``:`opacity-60`}`,children:[(0,O.jsx)(T,{className:`px-4 py-3`,children:(0,O.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,O.jsx)(g,{checked:e.enabled,onCheckedChange:s,className:`mt-0.5`}),(0,O.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,O.jsx)(C,{className:`text-sm truncate`,children:e.name}),(0,O.jsxs)(`div`,{className:`mt-1 flex flex-wrap gap-1.5 text-xs text-muted-foreground`,children:[(0,O.jsx)(`span`,{className:`font-mono`,children:e.schedule}),(0,O.jsx)(`span`,{children:e.timezone}),(0,O.jsx)(`span`,{children:e.orchestratorId}),(0,O.jsx)(`span`,{className:`truncate max-w-[28rem]`,children:p})]})]}),(0,O.jsxs)(`div`,{className:`flex gap-1`,children:[(0,O.jsx)(v,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Run now`,onClick:o,children:(0,O.jsx)(r,{className:`w-3.5 h-3.5`})}),(0,O.jsx)(v,{size:`icon`,variant:`ghost`,className:`h-7 w-7`,title:`Edit`,onClick:a,children:(0,O.jsx)(u,{className:`w-3.5 h-3.5`})}),(0,O.jsx)(v,{size:`icon`,variant:`ghost`,className:`h-7 w-7 text-red-400 hover:text-red-300`,title:`Delete`,onClick:c,children:(0,O.jsx)(l,{className:`w-3.5 h-3.5`})})]})]})}),(0,O.jsx)(w,{className:`px-4 pb-3 pt-0`,children:(0,O.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2 text-xs text-muted-foreground`,children:[(0,O.jsxs)(`span`,{title:e.nextRunAt?h(e.nextRunAt):void 0,children:[`next `,e.nextRunAt?m(n,e.nextRunAt):`disabled`]}),d&&(0,O.jsx)(_,{variant:`outline`,className:`border text-[10px] ${M(d)}`,children:j(d)}),d?.taskId&&(0,O.jsxs)(`span`,{children:[`task #`,d.taskId]}),d&&N(d)&&(0,O.jsx)(`span`,{className:`text-red-400 truncate`,children:N(d)})]})})]})}function X({form:e,selected:t,saving:r,orchestrators:i,agentProfiles:o,onChange:s,onSave:c}){let l=i.find(t=>t.id===e.orchestratorId)?.providers||[],u=f[e.provider]?.models||[],d=u.find(t=>t.alias===e.model)?.efforts||[];function p(t){let n=i.find(e=>e.id===t)?.providers[0]||e.provider;s({orchestratorId:t,provider:n,model:f[n]?.defaultModel||``,effort:``})}function m(e){s({provider:e,model:f[e]?.defaultModel||``,effort:``})}return(0,O.jsxs)(`div`,{className:`rounded-lg border border-border bg-card p-4 space-y-4 h-fit xl:sticky xl:top-4`,children:[(0,O.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,O.jsx)(`h3`,{className:`text-sm font-semibold`,children:t?`Edit schedule`:`New schedule`}),(0,O.jsx)(_,{variant:e.enabled?`default`:`secondary`,className:`ml-auto text-[10px]`,children:e.enabled?`enabled`:`disabled`})]}),(0,O.jsxs)(`div`,{className:`grid gap-3`,children:[(0,O.jsx)(Z,{label:`Name`,children:(0,O.jsx)(y,{value:e.name,onChange:e=>s({name:e.target.value})})}),(0,O.jsx)(Z,{label:`Description`,children:(0,O.jsx)(y,{value:e.description,onChange:e=>s({description:e.target.value})})}),(0,O.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,O.jsx)(Z,{label:`Cron`,children:(0,O.jsx)(y,{value:e.schedule,onChange:e=>s({schedule:e.target.value}),className:`font-mono`})}),(0,O.jsx)(Z,{label:`Timezone`,children:(0,O.jsx)(y,{value:e.timezone,onChange:e=>s({timezone:e.target.value})})})]}),(0,O.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,O.jsx)(Z,{label:`Orchestrator`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.orchestratorId,onChange:e=>p(e.target.value),children:[(0,O.jsx)(`option`,{value:``,children:`Select`}),i.map(e=>(0,O.jsxs)(`option`,{value:e.id,children:[e.hostname,` (`,e.id,`)`]},e.id))]})}),(0,O.jsx)(Z,{label:`Concurrency`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.concurrencyPolicy,onChange:e=>s({concurrencyPolicy:e.target.value}),children:[(0,O.jsx)(`option`,{value:`skip`,children:`skip`}),(0,O.jsx)(`option`,{value:`replace`,children:`replace`}),(0,O.jsx)(`option`,{value:`queue`,children:`queue`})]})})]}),(0,O.jsxs)(`label`,{className:`flex items-center justify-between rounded-md border border-border px-3 py-2`,children:[(0,O.jsx)(`span`,{className:`text-sm`,children:`Enabled`}),(0,O.jsx)(g,{checked:e.enabled,onCheckedChange:e=>s({enabled:e})})]})]}),(0,O.jsxs)(`div`,{className:`space-y-3`,children:[(0,O.jsx)(x,{children:`Target`}),(0,O.jsxs)(`div`,{className:`grid grid-cols-2 gap-2`,children:[(0,O.jsx)(v,{type:`button`,variant:e.targetMode===`existing_agent`?`default`:`outline`,onClick:()=>s({targetMode:`existing_agent`}),children:`Existing`}),(0,O.jsx)(v,{type:`button`,variant:e.targetMode===`on_demand_agent`?`default`:`outline`,onClick:()=>s({targetMode:`on_demand_agent`}),children:`On demand`})]}),(0,O.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,O.jsx)(Z,{label:`Provider`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.provider,onChange:e=>m(e.target.value),disabled:!l.length,children:[!l.length&&(0,O.jsx)(`option`,{value:e.provider,children:`No providers available`}),l.map(e=>(0,O.jsx)(`option`,{value:e,children:e},e))]})}),e.targetMode===`existing_agent`?(0,O.jsx)(Z,{label:`If no match`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.ifNoMatch,onChange:e=>s({ifNoMatch:e.target.value}),children:[(0,O.jsx)(`option`,{value:`fail`,children:`fail`}),(0,O.jsx)(`option`,{value:`spawn`,children:`spawn`})]})}):(0,O.jsx)(Z,{label:`Approval`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.approvalMode,onChange:e=>s({approvalMode:e.target.value}),children:[(0,O.jsx)(`option`,{value:`guarded`,children:`guarded`}),(0,O.jsx)(`option`,{value:`read-only`,children:`read-only`}),(0,O.jsx)(`option`,{value:`open`,children:`open`})]})})]}),e.targetMode===`on_demand_agent`&&(0,O.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,O.jsx)(Z,{label:`Model`,children:(0,O.jsx)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.model,onChange:e=>s({model:e.target.value,effort:``}),children:u.map(e=>(0,O.jsx)(`option`,{value:e.alias,children:e.label},e.alias))})}),(0,O.jsx)(Z,{label:`Effort`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.effort,onChange:e=>s({effort:e.target.value}),disabled:!d.length,children:[(0,O.jsx)(`option`,{value:``,children:`Default`}),d.map(e=>(0,O.jsx)(`option`,{value:e,children:e},e))]})}),(0,O.jsx)(Z,{label:`Agent profile`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.profile||`default-relay`,onChange:e=>s({profile:e.target.value}),children:[!o.length&&(0,O.jsx)(`option`,{value:`default-relay`,children:`default-relay`}),o.map(e=>(0,O.jsx)(`option`,{value:e.name,children:e.name},e.name))]})}),(0,O.jsx)(Z,{label:`Workspace`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.workspaceMode,onChange:e=>s({workspaceMode:e.target.value}),children:[(0,O.jsx)(`option`,{value:`inherit`,children:`inherit (default → isolated worktree)`}),(0,O.jsx)(`option`,{value:`shared`,children:`shared (run in the repo, commit to its branch)`}),(0,O.jsx)(`option`,{value:`isolated`,children:`isolated (fresh git worktree per run)`})]})})]}),e.targetMode===`existing_agent`?(0,O.jsxs)(`div`,{className:`grid gap-3`,children:[(0,O.jsx)(Z,{label:`Label`,children:(0,O.jsx)(y,{value:e.label,onChange:e=>s({label:e.target.value})})}),(0,O.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,O.jsx)(Z,{label:`Tags`,children:(0,O.jsx)(y,{value:e.tags,onChange:e=>s({tags:e.target.value})})}),(0,O.jsx)(Z,{label:`Capabilities`,children:(0,O.jsx)(y,{value:e.capabilities,onChange:e=>s({capabilities:e.target.value})})})]})]}):(0,O.jsxs)(`div`,{className:`grid gap-3`,children:[(0,O.jsx)(Z,{label:`Cwd`,children:(0,O.jsx)(y,{value:e.cwd,onChange:e=>s({cwd:e.target.value})})}),(0,O.jsxs)(`label`,{className:`flex items-center justify-between rounded-md border border-border px-3 py-2`,children:[(0,O.jsx)(`span`,{className:`text-sm`,children:`Keep alive`}),(0,O.jsx)(g,{checked:e.keepAlive,onCheckedChange:e=>s({keepAlive:e})})]}),(0,O.jsxs)(`div`,{className:`grid grid-cols-2 gap-3`,children:[(0,O.jsx)(Z,{label:`Budget minutes`,children:(0,O.jsx)(y,{type:`number`,min:1,step:1,value:e.budgetMinutes,onChange:e=>s({budgetMinutes:e.target.value})})}),(0,O.jsx)(Z,{label:`Warning minute`,children:(0,O.jsx)(y,{type:`number`,min:0,step:1,value:e.warningMinutes,onChange:e=>s({warningMinutes:e.target.value})})})]}),(0,O.jsx)(Z,{label:`Warning message`,children:(0,O.jsx)(S,{value:e.warningMessage,onChange:e=>s({warningMessage:e.target.value}),className:`min-h-20`})})]})]}),(0,O.jsxs)(`div`,{className:`space-y-3`,children:[(0,O.jsx)(x,{children:`Task`}),(0,O.jsx)(Z,{label:`Title`,children:(0,O.jsx)(y,{value:e.title,onChange:e=>s({title:e.target.value})})}),(0,O.jsx)(Z,{label:`Body`,children:(0,O.jsx)(S,{value:e.body,onChange:e=>s({body:e.target.value}),className:`min-h-28`})}),(0,O.jsxs)(`div`,{className:`grid grid-cols-3 gap-3`,children:[(0,O.jsx)(Z,{label:`Severity`,children:(0,O.jsxs)(`select`,{className:`h-9 w-full rounded-md border border-input bg-background px-3 text-sm`,value:e.severity,onChange:e=>s({severity:e.target.value}),children:[(0,O.jsx)(`option`,{value:`info`,children:`info`}),(0,O.jsx)(`option`,{value:`warning`,children:`warning`}),(0,O.jsx)(`option`,{value:`critical`,children:`critical`})]})}),(0,O.jsx)(Z,{label:`Dedupe key`,children:(0,O.jsx)(y,{value:e.dedupeKey,onChange:e=>s({dedupeKey:e.target.value})})}),(0,O.jsx)(Z,{label:`External URL`,children:(0,O.jsx)(y,{value:e.externalUrl,onChange:e=>s({externalUrl:e.target.value})})})]})]}),(0,O.jsxs)(v,{className:`w-full gap-2`,onClick:c,disabled:r||!e.name||!e.schedule||!e.orchestratorId||!e.title||!e.body,children:[r?(0,O.jsx)(a,{className:`w-4 h-4 animate-spin`}):(0,O.jsx)(n,{className:`w-4 h-4`}),`Save`]})]})}function Z({label:e,children:t}){return(0,O.jsxs)(`div`,{className:`space-y-1.5`,children:[(0,O.jsx)(x,{className:`text-xs text-muted-foreground`,children:e}),t]})}function Q({runs:e,automations:t,agents:n,orchestrators:r,now:i,logRunId:a,onToggleLogs:o}){let c=new Map(t.map(e=>[e.id,e.name]));return(0,O.jsxs)(`div`,{className:`space-y-2`,children:[(0,O.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,O.jsx)(`h3`,{className:`text-sm font-semibold`,children:`Runs`}),(0,O.jsx)(_,{variant:`secondary`,className:`text-[10px]`,children:e.length})]}),(0,O.jsxs)(`div`,{className:`rounded-lg border border-border overflow-x-auto`,children:[(0,O.jsxs)(`div`,{className:`grid grid-cols-[minmax(10rem,1fr)_8rem_8rem_7rem_7rem_5rem] gap-3 bg-muted/30 px-3 py-2 text-xs text-muted-foreground`,children:[(0,O.jsx)(`span`,{children:`Automation`}),(0,O.jsx)(`span`,{children:`Status`}),(0,O.jsx)(`span`,{children:`Agent`}),(0,O.jsx)(`span`,{children:`Runtime`}),(0,O.jsx)(`span`,{children:`Updated`}),(0,O.jsx)(`span`,{children:`Logs`})]}),e.length===0?(0,O.jsx)(`div`,{className:`px-3 py-10 text-center text-sm text-muted-foreground`,children:`No runs`}):e.slice(0,80).map(e=>{let t=H(e,n,r),l=B(e,i),u=V(e);return(0,O.jsxs)(`div`,{className:`border-t border-border`,children:[(0,O.jsxs)(`div`,{className:`grid grid-cols-[minmax(10rem,1fr)_8rem_8rem_7rem_7rem_5rem] gap-3 px-3 py-2 text-xs items-center`,children:[(0,O.jsxs)(`div`,{className:`min-w-0`,children:[(0,O.jsx)(`div`,{className:`truncate`,children:c.get(e.automationId)||e.automationId.slice(0,12)}),(0,O.jsxs)(`div`,{className:`truncate text-muted-foreground`,children:[e.taskId?`task #${e.taskId}`:`no task`,l?` · ${l} left`:``,u?` · ${u}`:``]})]}),(0,O.jsx)(_,{variant:`outline`,className:`border text-[10px] w-fit ${M(e)}`,children:j(e)}),(0,O.jsx)(`span`,{className:`truncate font-mono text-muted-foreground`,children:e.targetAgentId||e.spawnedAgentId||`-`}),(0,O.jsx)(`span`,{className:`text-muted-foreground`,children:z(e,i)}),(0,O.jsx)(`span`,{className:`text-muted-foreground`,title:h(e.updatedAt),children:m(i,e.updatedAt)}),t?(0,O.jsx)(v,{type:`button`,size:`sm`,variant:`ghost`,className:`h-7 w-7 p-0`,title:`Open logs`,onClick:()=>o(e.id),children:(0,O.jsx)(s,{className:`w-3.5 h-3.5`})}):(0,O.jsx)(`span`,{className:`text-muted-foreground`,children:`-`})]}),a===e.id&&t&&(0,O.jsx)(`div`,{className:`border-t border-border px-3 py-3`,children:(0,O.jsx)(b,{orchestratorId:t.orchestratorId,session:t.session})})]},e.id)})]})]})}export{q as AutomationView};
2
- //# sourceMappingURL=automation-CiaLThdO.js.map