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.
- package/package.json +2 -2
- package/public/assets/{activity-DT1JGHnp.js → activity-B0_uE6Yh.js} +2 -2
- package/public/assets/{activity-DT1JGHnp.js.map → activity-B0_uE6Yh.js.map} +1 -1
- package/public/assets/{agent-profiles-CrMemMkZ.js → agent-profiles-Rwxrcf9F.js} +2 -2
- package/public/assets/{agent-profiles-CrMemMkZ.js.map → agent-profiles-Rwxrcf9F.js.map} +1 -1
- package/public/assets/{agents-Bl-rrgOy.js → agents-Dp1EXJc8.js} +2 -2
- package/public/assets/{agents-Bl-rrgOy.js.map → agents-Dp1EXJc8.js.map} +1 -1
- package/public/assets/{analytics-a663ak56.js → analytics-D5OT5ajj.js} +2 -2
- package/public/assets/{analytics-a663ak56.js.map → analytics-D5OT5ajj.js.map} +1 -1
- package/public/assets/automation-Dm6rXNxK.js +2 -0
- package/public/assets/{automation-CiaLThdO.js.map → automation-Dm6rXNxK.js.map} +1 -1
- package/public/assets/{branch-state-badge-D4ur3m3_.js → branch-state-badge-FX5Yww2s.js} +2 -2
- package/public/assets/{branch-state-badge-D4ur3m3_.js.map → branch-state-badge-FX5Yww2s.js.map} +1 -1
- package/public/assets/{channels-o9KLTHoK.js → channels--rdAiX17.js} +2 -2
- package/public/assets/{channels-o9KLTHoK.js.map → channels--rdAiX17.js.map} +1 -1
- package/public/assets/chat-JZAEDGfX.js +2 -0
- package/public/assets/chat-JZAEDGfX.js.map +1 -0
- package/public/assets/{connectors-CdC806mA.js → connectors-Bx4gzvNf.js} +2 -2
- package/public/assets/{connectors-CdC806mA.js.map → connectors-Bx4gzvNf.js.map} +1 -1
- package/public/assets/display-Bebqs1qu.js +3 -0
- package/public/assets/display-Bebqs1qu.js.map +1 -0
- package/public/assets/{formatted-body-impl-Ca74OAEH.js → formatted-body-impl-CVq4qHix.js} +2 -2
- package/public/assets/{formatted-body-impl-Ca74OAEH.js.map → formatted-body-impl-CVq4qHix.js.map} +1 -1
- package/public/assets/{index-C_33ymaw.js → index-BHRtR4q7.js} +8 -8
- package/public/assets/{index-C_33ymaw.js.map → index-BHRtR4q7.js.map} +1 -1
- package/public/assets/{insights-ClI68s39.js → insights-yJFgCa3o.js} +2 -2
- package/public/assets/{insights-ClI68s39.js.map → insights-yJFgCa3o.js.map} +1 -1
- package/public/assets/{integrations-1nxMizDY.js → integrations-k1HIONjo.js} +2 -2
- package/public/assets/{integrations-1nxMizDY.js.map → integrations-k1HIONjo.js.map} +1 -1
- package/public/assets/maintenance-CsoOFBXx.js +2 -0
- package/public/assets/{maintenance-DiFNzNPN.js.map → maintenance-CsoOFBXx.js.map} +1 -1
- package/public/assets/{managed-agents-Do3dKvfj.js → managed-agents-Q3HuVjGg.js} +2 -2
- package/public/assets/{managed-agents-Do3dKvfj.js.map → managed-agents-Q3HuVjGg.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CLA0J255.js → markdown-preview-impl-CnsMjrnu.js} +2 -2
- package/public/assets/{markdown-preview-impl-CLA0J255.js.map → markdown-preview-impl-CnsMjrnu.js.map} +1 -1
- package/public/assets/{memory-IjwqFzBd.js → memory-D3-K5eJS.js} +2 -2
- package/public/assets/{memory-IjwqFzBd.js.map → memory-D3-K5eJS.js.map} +1 -1
- package/public/assets/{messages-DjvWqHyn.js → messages-B4lCP5rS.js} +2 -2
- package/public/assets/{messages-DjvWqHyn.js.map → messages-B4lCP5rS.js.map} +1 -1
- package/public/assets/{orchestrators-D2IqDxDT.js → orchestrators-CRoZtLeQ.js} +2 -2
- package/public/assets/{orchestrators-D2IqDxDT.js.map → orchestrators-CRoZtLeQ.js.map} +1 -1
- package/public/assets/{overview-DKC3TbAh.js → overview-CxCU2fOF.js} +2 -2
- package/public/assets/{overview-DKC3TbAh.js.map → overview-CxCU2fOF.js.map} +1 -1
- package/public/assets/pairs-unqjPlmq.js +2 -0
- package/public/assets/{pairs-WpKCPE1n.js.map → pairs-unqjPlmq.js.map} +1 -1
- package/public/assets/{security-BF7ZtPQe.js → security-B7HhSYNy.js} +2 -2
- package/public/assets/{security-BF7ZtPQe.js.map → security-B7HhSYNy.js.map} +1 -1
- package/public/assets/{settings-CQnjrTa-.js → settings-B9NDhsAb.js} +2 -2
- package/public/assets/{settings-CQnjrTa-.js.map → settings-B9NDhsAb.js.map} +1 -1
- package/public/assets/store-DiSzYHj9.js +9 -0
- package/public/assets/{store-C9VcSo05.js.map → store-DiSzYHj9.js.map} +1 -1
- package/public/assets/{tasks-CbN_GSSb.js → tasks-CIQolvNm.js} +2 -2
- package/public/assets/{tasks-CbN_GSSb.js.map → tasks-CIQolvNm.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-BJRohThT.js → terminal-viewer-impl-DCifVqFR.js} +2 -2
- package/public/assets/{terminal-viewer-impl-BJRohThT.js.map → terminal-viewer-impl-DCifVqFR.js.map} +1 -1
- package/public/assets/{work-queue-C5xLBLmm.js → work-queue-Dr3c1V6O.js} +2 -2
- package/public/assets/{work-queue-C5xLBLmm.js.map → work-queue-Dr3c1V6O.js.map} +1 -1
- package/public/assets/{workspaces-D91H3wDX.js → workspaces-B1Jxop7h.js} +3 -3
- package/public/assets/{workspaces-D91H3wDX.js.map → workspaces-B1Jxop7h.js.map} +1 -1
- package/public/index.html +3 -3
- package/runner/src/adapter.ts +1 -1
- package/src/agent-lifecycle-events.ts +137 -0
- package/src/artifact-storage.ts +3 -5
- package/src/cli/_shared.ts +80 -0
- package/src/cli/agent-detect.ts +188 -0
- package/src/cli/agent-meta.ts +95 -0
- package/src/cli/context-probe.ts +88 -0
- package/src/cli/daemon.ts +111 -0
- package/src/cli/dev.ts +173 -0
- package/src/cli/index.ts +361 -0
- package/src/cli/introspect.ts +73 -0
- package/src/cli/memory.ts +37 -0
- package/src/cli/message.ts +201 -0
- package/src/cli/orchestrator.ts +227 -0
- package/src/cli/pair.ts +125 -0
- package/src/cli/provider.ts +209 -0
- package/src/cli/recipe.ts +110 -0
- package/src/cli/reply.ts +141 -0
- package/src/cli/setup.ts +57 -0
- package/src/cli/steward.ts +59 -0
- package/src/cli/token.ts +81 -0
- package/src/cli/upgrade.ts +193 -0
- package/src/cli/workspace.ts +215 -0
- package/src/cli.ts +4 -2718
- package/src/config-store.ts +10 -6
- package/src/maintenance.ts +4 -0
- package/src/mcp-errors.ts +7 -0
- package/src/mcp.ts +32 -34
- package/src/routes/agents-spawn.ts +9 -1
- package/src/routes/agents.ts +5 -0
- package/src/routes/commands.ts +15 -0
- package/src/spawn-targets.ts +159 -0
- package/src/utils.ts +16 -1
- package/public/assets/automation-CiaLThdO.js +0 -2
- package/public/assets/chat-5hvHZcAe.js +0 -2
- package/public/assets/chat-5hvHZcAe.js.map +0 -1
- package/public/assets/display-JI19Vc7L.js +0 -3
- package/public/assets/display-JI19Vc7L.js.map +0 -1
- package/public/assets/maintenance-DiFNzNPN.js +0 -2
- package/public/assets/pairs-WpKCPE1n.js +0 -2
- package/public/assets/store-C9VcSo05.js +0 -9
package/src/config-store.ts
CHANGED
|
@@ -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:
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
|
package/src/maintenance.ts
CHANGED
|
@@ -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
|
-
...(
|
|
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
|
|
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
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
)
|
|
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
|
|
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
|
package/src/routes/agents.ts
CHANGED
|
@@ -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;
|
package/src/routes/commands.ts
CHANGED
|
@@ -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
|