agent-relay-server 0.121.4 → 0.122.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 (132) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +5 -4
  3. package/public/assets/{MessageBubble-CLqxCG9b.js → MessageBubble-DRdUD-kW.js} +2 -2
  4. package/public/assets/{MessageBubble-CLqxCG9b.js.map → MessageBubble-DRdUD-kW.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-BkkToFEb.js → PlanGraphPanel-siKMPCln.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-BkkToFEb.js.map → PlanGraphPanel-siKMPCln.js.map} +1 -1
  7. package/public/assets/{activity-D-ocGQGs.js → activity-D3MNcSaV.js} +2 -2
  8. package/public/assets/{activity-D-ocGQGs.js.map → activity-D3MNcSaV.js.map} +1 -1
  9. package/public/assets/{agent-profiles-CRPgYTyb.js → agent-profiles-CKzttq6G.js} +2 -2
  10. package/public/assets/{agent-profiles-CRPgYTyb.js.map → agent-profiles-CKzttq6G.js.map} +1 -1
  11. package/public/assets/{agent-type-icon-CeEoMouY.js → agent-type-icon-0ZJRP1Vy.js} +2 -2
  12. package/public/assets/{agent-type-icon-CeEoMouY.js.map → agent-type-icon-0ZJRP1Vy.js.map} +1 -1
  13. package/public/assets/{agents-Chw5E_2q.js → agents-CuVj_rfG.js} +2 -2
  14. package/public/assets/{agents-Chw5E_2q.js.map → agents-CuVj_rfG.js.map} +1 -1
  15. package/public/assets/{analytics-BmNDG0hR.js → analytics-iob6C27d.js} +2 -2
  16. package/public/assets/{analytics-BmNDG0hR.js.map → analytics-iob6C27d.js.map} +1 -1
  17. package/public/assets/{automation-D7g90kTv.js → automation-B2ixxs4q.js} +2 -2
  18. package/public/assets/{automation-D7g90kTv.js.map → automation-B2ixxs4q.js.map} +1 -1
  19. package/public/assets/{branch-state-badge-Bv7DhLBZ.js → branch-state-badge-CafBJWKo.js} +2 -2
  20. package/public/assets/{branch-state-badge-Bv7DhLBZ.js.map → branch-state-badge-CafBJWKo.js.map} +1 -1
  21. package/public/assets/{channels-CLwPeEPi.js → channels-C-WsbL2H.js} +2 -2
  22. package/public/assets/{channels-CLwPeEPi.js.map → channels-C-WsbL2H.js.map} +1 -1
  23. package/public/assets/{chat-Cgj7VKzc.js → chat-DeoVTVob.js} +2 -2
  24. package/public/assets/{chat-Cgj7VKzc.js.map → chat-DeoVTVob.js.map} +1 -1
  25. package/public/assets/{connectors-Br6fiTny.js → connectors-C2Ac03LJ.js} +2 -2
  26. package/public/assets/{connectors-Br6fiTny.js.map → connectors-C2Ac03LJ.js.map} +1 -1
  27. package/public/assets/display-CJzSoTMq.js +3 -0
  28. package/public/assets/display-CJzSoTMq.js.map +1 -0
  29. package/public/assets/{formatted-body-CmkuD23M.js → formatted-body-35XBOY5L.js} +3 -3
  30. package/public/assets/{formatted-body-CmkuD23M.js.map → formatted-body-35XBOY5L.js.map} +1 -1
  31. package/public/assets/{formatted-body-impl-DbdFmbwW.js → formatted-body-impl-RmDxI5Ux.js} +2 -2
  32. package/public/assets/{formatted-body-impl-DbdFmbwW.js.map → formatted-body-impl-RmDxI5Ux.js.map} +1 -1
  33. package/public/assets/index-D0xMQQb_.js +25 -0
  34. package/public/assets/{index-D9OogaB5.js.map → index-D0xMQQb_.js.map} +1 -1
  35. package/public/assets/{insights-qvaPqWCV.js → insights-B643SFvm.js} +2 -2
  36. package/public/assets/{insights-qvaPqWCV.js.map → insights-B643SFvm.js.map} +1 -1
  37. package/public/assets/{integrations-DYEGSqq_.js → integrations-hFIHeCF4.js} +2 -2
  38. package/public/assets/{integrations-DYEGSqq_.js.map → integrations-hFIHeCF4.js.map} +1 -1
  39. package/public/assets/{maintenance-C0HIEB-J.js → maintenance-CURvdp_Z.js} +2 -2
  40. package/public/assets/{maintenance-C0HIEB-J.js.map → maintenance-CURvdp_Z.js.map} +1 -1
  41. package/public/assets/{managed-agents-DdS7aI38.js → managed-agents-DrZtdlSn.js} +2 -2
  42. package/public/assets/{managed-agents-DdS7aI38.js.map → managed-agents-DrZtdlSn.js.map} +1 -1
  43. package/public/assets/{markdown-preview-impl-BnXMH1z1.js → markdown-preview-impl-CVDO--ek.js} +2 -2
  44. package/public/assets/{markdown-preview-impl-BnXMH1z1.js.map → markdown-preview-impl-CVDO--ek.js.map} +1 -1
  45. package/public/assets/{memory-CSwx7bz8.js → memory-Bm7dt_Qf.js} +2 -2
  46. package/public/assets/{memory-CSwx7bz8.js.map → memory-Bm7dt_Qf.js.map} +1 -1
  47. package/public/assets/{messages-Be9CmvDv.js → messages-DTq4i2KZ.js} +2 -2
  48. package/public/assets/{messages-Be9CmvDv.js.map → messages-DTq4i2KZ.js.map} +1 -1
  49. package/public/assets/{orchestrators-BgtvvHvo.js → orchestrators-Dra318Ap.js} +2 -2
  50. package/public/assets/{orchestrators-BgtvvHvo.js.map → orchestrators-Dra318Ap.js.map} +1 -1
  51. package/public/assets/{overview-BvhbPWtF.js → overview-yr1Vmk64.js} +2 -2
  52. package/public/assets/{overview-BvhbPWtF.js.map → overview-yr1Vmk64.js.map} +1 -1
  53. package/public/assets/{pairs-CpXSOMx0.js → pairs-mnlKDBGp.js} +2 -2
  54. package/public/assets/{pairs-CpXSOMx0.js.map → pairs-mnlKDBGp.js.map} +1 -1
  55. package/public/assets/projects-Dzdf-gkK.js +2 -0
  56. package/public/assets/{projects-DdPNPFJI.js.map → projects-Dzdf-gkK.js.map} +1 -1
  57. package/public/assets/prompt-settings-ByL8SNCi.js +2 -0
  58. package/public/assets/{prompt-settings-BpZse7-X.js.map → prompt-settings-ByL8SNCi.js.map} +1 -1
  59. package/public/assets/{registry-pb1gTZEg.js → registry-CXWyOtpr.js} +2 -2
  60. package/public/assets/{registry-pb1gTZEg.js.map → registry-CXWyOtpr.js.map} +1 -1
  61. package/public/assets/{security-BgcX1n9D.js → security-0SBsc_4W.js} +2 -2
  62. package/public/assets/{security-BgcX1n9D.js.map → security-0SBsc_4W.js.map} +1 -1
  63. package/public/assets/{select-DZPVpKPv.js → select-Bw5z2KJ_.js} +2 -2
  64. package/public/assets/{select-DZPVpKPv.js.map → select-Bw5z2KJ_.js.map} +1 -1
  65. package/public/assets/{settings-hd5kzdRB.js → settings-78Cyq_tA.js} +4 -4
  66. package/public/assets/{settings-hd5kzdRB.js.map → settings-78Cyq_tA.js.map} +1 -1
  67. package/public/assets/{store-DKTLubBT.js → store-BPrVuKiv.js} +4 -4
  68. package/public/assets/store-BPrVuKiv.js.map +1 -0
  69. package/public/assets/{tasks-C5d2LU5E.js → tasks-Cg2MMeri.js} +2 -2
  70. package/public/assets/{tasks-C5d2LU5E.js.map → tasks-Cg2MMeri.js.map} +1 -1
  71. package/public/assets/teams-DWv6UwFV.js +2 -0
  72. package/public/assets/{teams-B8f2pGJe.js.map → teams-DWv6UwFV.js.map} +1 -1
  73. package/public/assets/{terminal-viewer-impl-CxAscH0U.js → terminal-viewer-impl-DBLDjbIg.js} +2 -2
  74. package/public/assets/{terminal-viewer-impl-CxAscH0U.js.map → terminal-viewer-impl-DBLDjbIg.js.map} +1 -1
  75. package/public/assets/{types-uVOXgvMT.js → types-BeJUSfDj.js} +2 -2
  76. package/public/assets/types-BeJUSfDj.js.map +1 -0
  77. package/public/assets/{user-avatar-Dw6mzWhv.js → user-avatar-BsOLyVJj.js} +2 -2
  78. package/public/assets/{user-avatar-Dw6mzWhv.js.map → user-avatar-BsOLyVJj.js.map} +1 -1
  79. package/public/assets/{work-queue-9imc7aNK.js → work-queue-DmwvnF6F.js} +2 -2
  80. package/public/assets/{work-queue-9imc7aNK.js.map → work-queue-DmwvnF6F.js.map} +1 -1
  81. package/public/assets/{workspaces-CLYc3yF3.js → workspaces-dIBivJN8.js} +2 -2
  82. package/public/assets/{workspaces-CLYc3yF3.js.map → workspaces-dIBivJN8.js.map} +1 -1
  83. package/public/index.html +6 -6
  84. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  85. package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +44 -938
  86. package/runner/src/adapter.ts +53 -18
  87. package/runner/src/config.ts +1 -4
  88. package/src/agent-card-projection.ts +5 -1
  89. package/src/agent-command-activity.ts +39 -0
  90. package/src/automations/reconcile.ts +2 -2
  91. package/src/bus.ts +6 -4
  92. package/src/cli/workspace.ts +3 -1
  93. package/src/config.ts +10 -1
  94. package/src/db/agents.ts +138 -32
  95. package/src/db/message-reads.ts +50 -0
  96. package/src/db/messages.ts +48 -9
  97. package/src/db/migrations.ts +54 -0
  98. package/src/db/provider-quota-burn.ts +96 -67
  99. package/src/db/provider-quotas.ts +29 -7
  100. package/src/db/scheduled-timers.ts +21 -0
  101. package/src/db/schema.ts +1 -0
  102. package/src/db/tasks.ts +33 -3
  103. package/src/db/work-evidence.ts +10 -7
  104. package/src/event-loop-lag.ts +15 -0
  105. package/src/gate-resolver.ts +24 -1
  106. package/src/maintenance/jobs.ts +23 -6
  107. package/src/managed-policy.ts +35 -5
  108. package/src/mcp-plan-tools.ts +13 -1
  109. package/src/project-instructions.ts +36 -2
  110. package/src/quota-advisory.ts +6 -4
  111. package/src/quota-band-transition.ts +23 -14
  112. package/src/routes/agent-sessions.ts +39 -7
  113. package/src/routes/agents.ts +3 -2
  114. package/src/routes/tasks.ts +16 -4
  115. package/src/routes/workspaces.ts +35 -10
  116. package/src/services/plan-graph.ts +27 -9
  117. package/src/services/register-agent.ts +12 -1
  118. package/src/services/send-message.ts +43 -15
  119. package/src/services/spawn-agent.ts +3 -0
  120. package/src/spawn-command.ts +8 -0
  121. package/src/spawn-targets.ts +19 -34
  122. package/src/sse.ts +25 -3
  123. package/src/workspace-merge.ts +25 -5
  124. package/src/workspace-orphans.ts +13 -6
  125. package/public/assets/display-exQDWum3.js +0 -3
  126. package/public/assets/display-exQDWum3.js.map +0 -1
  127. package/public/assets/index-D9OogaB5.js +0 -25
  128. package/public/assets/projects-DdPNPFJI.js +0 -2
  129. package/public/assets/prompt-settings-BpZse7-X.js +0 -2
  130. package/public/assets/store-DKTLubBT.js.map +0 -1
  131. package/public/assets/teams-B8f2pGJe.js +0 -2
  132. package/public/assets/types-uVOXgvMT.js.map +0 -1
@@ -433,6 +433,12 @@ export const postWorkspaceCleanupStale: Handler = async (req) => {
433
433
  const workspaceId = cleanString(body.workspaceId, "workspaceId", { max: 160 });
434
434
  const dryRun = body.dryRun !== false; // safe by default
435
435
  const landedOnly = true;
436
+ // #965 — an explicit operator force-discard for a genuinely-abandoned branch: clear even when
437
+ // git state is a conflict / unlanded / unprobeable. The land-safety gate is correct for LIVE
438
+ // work but wrong for a confirmed-abandoned branch, which otherwise left a raw DELETE as the only
439
+ // path. Gated hard on the owner NOT being online (force can never override liveness) and, like
440
+ // every mutation here, on `--execute` (dryRun:false) — the two together are the confirmation.
441
+ const force = body.force === true;
436
442
 
437
443
  const candidates = listWorkspaces().filter((ws) =>
438
444
  ws.mode === "isolated" && Boolean(ws.worktreePath) && !TERMINAL_WORKSPACE_STATUSES.has(ws.status)
@@ -455,22 +461,41 @@ export const postWorkspaceCleanupStale: Handler = async (req) => {
455
461
  const landed = gitState?.landed === true;
456
462
  // Safe = gone from disk, OR clean tree with no unmerged work (landed/empty).
457
463
  const safe = missing || (gitState !== undefined && (dirtyCount ?? 1) === 0 && (!landedOnly || landed || ahead === 0));
464
+ // #965 — a force-discard clears an unsafe (conflict/unlanded/unprobeable) row too, but only
465
+ // for an offline owner. #1066: the `ownerOnline` snapshot above predates the
466
+ // `fetchWorkspaceGitState` await, so it is stale by dispatch time; liveness is re-verified
467
+ // immediately before dispatch below. Force overrides the land-safety gate, NEVER liveness.
468
+ const forced = force && !safe;
469
+ const discardable = safe || force;
458
470
  const proof = { ownerStatus: owner?.status ?? "missing", ownerOnline, ahead, behind: gitState?.behind, landed, dirtyCount, missing, gitStateUnavailable: gitState ? undefined : ("unavailable" in fetched ? fetched.unavailable : undefined) };
459
- const row: Record<string, unknown> = { workspaceId: ws.id, branch: ws.branch, worktreePath: ws.worktreePath, repoRoot: ws.repoRoot, safe, proof };
460
- if (safe && !dryRun) {
461
- const built = buildWorkspaceCleanupCommand(ws, "cleanup-stale");
462
- if (built.ok) {
463
- updateWorkspaceStatus(ws.id, "cleanup_requested", { lastWorkspaceAction: "cleanup-stale", lastWorkspaceActionAt: Date.now(), cleanupProof: proof });
464
- emitCommand(built.command);
465
- row.commandId = built.command.id;
466
- cleaned.push(ws.id);
471
+ const row: Record<string, unknown> = { workspaceId: ws.id, branch: ws.branch, worktreePath: ws.worktreePath, repoRoot: ws.repoRoot, safe, discardable, ...(forced ? { forced: true } : {}), proof };
472
+ if (discardable && !dryRun) {
473
+ // #1066 re-snapshot liveness AFTER the git-state probe, immediately before dispatch. An
474
+ // owner offline at the initial snapshot can reconnect during the probe window (relay lag —
475
+ // #1054), and `force` bypasses buildWorkspaceCleanupCommand's dispatch-time live-owner guard,
476
+ // so this route is the only place that can refuse a just-reconnected owner under force.
477
+ // Force may clear an unsafe/conflict/unlanded row, but it must never nuke a live owner's tree.
478
+ if (isOwnerAlive(ws.ownerAgentId)) {
479
+ row.discardable = false;
480
+ row.cleanupError = `workspace ${ws.id} owner reconnected during git-state probe; force cannot override liveness`;
481
+ row.proof = { ...proof, ownerOnline: true };
467
482
  } else {
468
- row.cleanupError = built.error;
483
+ // Owner re-confirmed offline just above; force past buildWorkspaceCleanupCommand's
484
+ // live-owner guard and carry a reason so the host records an intentional force-discard.
485
+ const built = buildWorkspaceCleanupCommand(ws, "cleanup-stale", forced ? { force: true, reason: "operator force-discard: abandoned branch, owner offline" } : {});
486
+ if (built.ok) {
487
+ updateWorkspaceStatus(ws.id, "cleanup_requested", { lastWorkspaceAction: "cleanup-stale", lastWorkspaceActionAt: Date.now(), cleanupProof: { ...proof, forced } });
488
+ emitCommand(built.command);
489
+ row.commandId = built.command.id;
490
+ cleaned.push(ws.id);
491
+ } else {
492
+ row.cleanupError = built.error;
493
+ }
469
494
  }
470
495
  }
471
496
  rows.push(row);
472
497
  }
473
- return json({ dryRun, landedOnly, repoRoot, ...(workspaceId ? { workspaceId } : {}), scanned: candidates.length, eligible: rows.filter((r) => r.safe).length, cleaned, candidates: rows }, dryRun ? 200 : 202);
498
+ return json({ dryRun, landedOnly, force, repoRoot, ...(workspaceId ? { workspaceId } : {}), scanned: candidates.length, eligible: rows.filter((r) => r.discardable).length, cleaned, candidates: rows }, dryRun ? 200 : 202);
474
499
  };
475
500
 
476
501
  export const postWorkspaceAction: Handler = async (req, params) => {
@@ -1,4 +1,4 @@
1
- import { isRecord } from "agent-relay-sdk";
1
+ import { hasScope, isRecord } from "agent-relay-sdk";
2
2
  import { addTaskDependency, createPlanRow, deletePlan, getAgent, getEntry, getPlan, getPlanByTeam, getPlanByThread, getTaskDetail, listPlansWithBoundOwner, planThreadExists, removeTaskDependency, replacePlan, sendMessage, setPlanArchived, ValidationError } from "../db";
3
3
  import { associateIssue, planNodeIssueEntityId, removeAssociation } from "../db/issue-associations";
4
4
  import { resolveIssueRefFromString } from "../db/issue-refs";
@@ -9,6 +9,7 @@ import { issueRepoForAgent } from "../agent-issue-repo";
9
9
  import { buildTaskGraphView, parseTaskId, planStatusToTaskStatus, projectPlanOverTasks } from "./task-plan-view";
10
10
  import { createTaskEntity, setTaskEntityStatus } from "./task-entity";
11
11
  import type { AuthContext } from "./auth-context";
12
+ import { ServiceAuthError } from "./errors";
12
13
  import type { AgentCard, Plan, PlanEdge, PlanNode, PlanOwnerRef, PlanStatus } from "../types";
13
14
 
14
15
  export const PLAN_STATUSES = ["pending", "in-progress", "landed", "blocked"] as const;
@@ -37,7 +38,7 @@ interface PlanNodePatch {
37
38
  }
38
39
 
39
40
  interface PlanCreateInput {
40
- threadId: unknown;
41
+ threadId?: unknown;
41
42
  teamId?: unknown;
42
43
  channel?: unknown;
43
44
  title?: unknown;
@@ -269,7 +270,9 @@ function emitPlan(type: "plan.created" | "plan.changed" | "plan.deleted", plan:
269
270
  // thread server-side by self-threading a lightweight system message (the same sendMessage path the
270
271
  // caller would have used). Sent AS the actor when that agent is registered, else the reserved
271
272
  // `system` sink, so the mint always passes the sender-registration gate.
272
- function mintPlanThread(actor: string, channel: string | undefined): number {
273
+ function mintPlanThread(ctx: AuthContext, channel: string | undefined): number {
274
+ ensureCanMintThread(ctx);
275
+ const actor = actorId(ctx);
273
276
  const sender = getAgent(actor) ? actor : "system";
274
277
  const root = sendMessage({
275
278
  from: sender,
@@ -280,24 +283,39 @@ function mintPlanThread(actor: string, channel: string | undefined): number {
280
283
  return root.threadId ?? root.id;
281
284
  }
282
285
 
286
+ // #1043: the mint inserts a real message row through the db `sendMessage` path, which normally
287
+ // lives behind the `message:send` scope. `plans:write` alone must not be a side-door around it, so
288
+ // gate the mint on `message:send`. Internal `system` principals (empty-scope trusted callers) and
289
+ // full-reach `*` tokens pass through untouched — `hasScope` short-circuits `*`.
290
+ function ensureCanMintThread(ctx: AuthContext): void {
291
+ if (ctx.actor.kind === "system") return;
292
+ if (!hasScope(ctx.scopes, "message:send")) {
293
+ throw new ServiceAuthError("minting a plan thread requires the message:send scope");
294
+ }
295
+ }
296
+
283
297
  export function createPlan(input: PlanCreateInput, ctx: AuthContext): Plan {
284
298
  const channel = cleanString(input.channel, "channel", { max: 120 });
285
299
  const actor = actorId(ctx);
286
- const threadId = input.threadId === undefined || input.threadId === null
287
- ? mintPlanThread(actor, channel)
288
- : cleanThreadId(input.threadId);
289
- if (!planThreadExists(threadId)) throw new ValidationError(`thread ${threadId} not found`);
290
300
  const explicitTeamId = cleanString(input.teamId, "teamId", { max: 120 });
291
301
  const teamId = explicitTeamId ?? (input.teamId === undefined || input.teamId === null ? getAgent(actor)?.teamId : undefined);
292
302
  const nodes = cleanNodes(input.nodes, actor, teamId);
293
303
  const edges = cleanEdges(input.edges);
294
304
  validateEdges(nodes, edges);
305
+ const title = cleanString(input.title, "title", { max: 240 });
306
+ const objective = cleanString(input.objective, "objective", { max: 4000 });
307
+ // Resolve the thread only AFTER the payload validates (#1043): auto-minting first left an orphan
308
+ // 'Plan thread root' row behind on every rejected create.
309
+ const threadId = input.threadId === undefined || input.threadId === null
310
+ ? mintPlanThread(ctx, channel)
311
+ : cleanThreadId(input.threadId);
312
+ if (!planThreadExists(threadId)) throw new ValidationError(`thread ${threadId} not found`);
295
313
  const plan = createPlanRow({
296
314
  threadId,
297
315
  teamId,
298
316
  channel,
299
- title: cleanString(input.title, "title", { max: 240 }),
300
- objective: cleanString(input.objective, "objective", { max: 4000 }),
317
+ title,
318
+ objective,
301
319
  nodes,
302
320
  edges,
303
321
  createdBy: actor,
@@ -91,7 +91,18 @@ export function registerAgent(input: RegisterAgentInput, ctx: AuthContext): Agen
91
91
  // Registration labels are spawn defaults. Runtime /label changes remain authoritative.
92
92
  const registrationInput = existing ? { ...input } : input;
93
93
  if (existing) delete registrationInput.label;
94
- const agent = upsertAgent(registrationInput);
94
+ // #1051/#484 instance takeover is where the restart-strand invariant fires now that a
95
+ // heartbeat lapse no longer revokes anything. When a FRESH runner process re-registers under
96
+ // this agent id, the epoch bump inside upsertAgent atomically revokes the PRIOR instance's
97
+ // runtime tokens "stale": refused for auth yet re-mintable via the orchestrator (#484). A
98
+ // same-instance reconnect keeps its instanceId, so the epoch is unchanged and the live token
99
+ // is left valid — the whole point of #1051 (in-band recovery after lag without a re-mint).
100
+ // #1057(3) — the ONLY jti spared from that revoke is server-verified: the token that
101
+ // authenticated THIS registration, and only when it resolves to this very agent (a
102
+ // same-credential instance rotation, e.g. a respawn reusing its unexpired env token).
103
+ // Client-sent meta.auth can no longer nominate jtis to keep.
104
+ const verifiedKeepJti = ctx.callerAgentId === input.id ? ctx.component?.jti : undefined;
105
+ const agent = upsertAgent(registrationInput, verifiedKeepJti ? { verifiedKeepJtis: [verifiedKeepJti] } : undefined);
95
106
 
96
107
  // Managed came-up-running reconcile — no-op for non-managed agents (the guard lives
97
108
  // in markManagedAgentRunning). Clears backoff, flushes the policy queue, transitions
@@ -21,12 +21,13 @@
21
21
  //
22
22
  // OUT OF SCOPE (transport-edge, excluded from the parity facet): automatic memory injection
23
23
  // (needs request-derived context) and per-transport audit (HTTP domain row vs MCP tool-call row).
24
- import { canonicalHumanAgentId, isHumanAgentId, isMechanicalMessageKind, isRecord, isReservedAgentId } from "agent-relay-sdk";
24
+ import { canonicalHumanAgentId, isHumanAgentId, isMechanicalMessageKind, isRecord, isReservedAgentId, isTurnFinalSession } from "agent-relay-sdk";
25
25
  import {
26
26
  ValidationError,
27
27
  flushDeferredLineageReport,
28
28
  getAgent,
29
29
  getMessage,
30
+ latestInboundMessage,
30
31
  listAgents,
31
32
  listRecentFinalResponseCaptures,
32
33
  promoteLineageCapturedResponse,
@@ -287,6 +288,14 @@ function emitSendResults(results: DbSendMessageResult[]): void {
287
288
  emitNewMessages(liveMessages);
288
289
  }
289
290
 
291
+ // #976 — a spawned child (results-only profile) whose turn-final response Relay already promotes to
292
+ // its spawn-parent does not also need to fire a bare ack/report via relay_send_message. Codex workers
293
+ // do this routinely ("Noted.", "Cleared.", "Going forward I'll…", or a duplicate of the final report),
294
+ // and each redundant message wakes the coordinator and burns the context coordinators exist to
295
+ // protect. This ONE home suppresses such acks on BOTH transports (HTTP + MCP) and BOTH providers
296
+ // (codex + claude): a reply-path ack collapses onto its parent; a standalone self→parent ack (no
297
+ // replyTo — the codex relay_send_message leak) collapses onto the latest inbound it answers. A
298
+ // genuine result (final capture, a session-final step, or non-ack body) always passes through.
290
299
  function suppressResultsOnlyAck(input: SendMessageInput): Message | null {
291
300
  if (input.kind && input.kind !== "chat" && input.kind !== "session") return null;
292
301
  const sender = getAgent(input.from);
@@ -296,21 +305,40 @@ function suppressResultsOnlyAck(input: SendMessageInput): Message | null {
296
305
  if (!isPassiveAck(input.body)) return null;
297
306
  if (isRecord(input.payload?.responseCapture)) return null;
298
307
  const session = isRecord(input.payload?.session) ? input.payload.session : undefined;
299
- if (session?.final === true) return null;
300
- return input.replyTo ? getMessage(input.replyTo) : null;
308
+ if (isTurnFinalSession(session)) return null;
309
+ // Reply-path: collapse onto the parent message being acked.
310
+ if (input.replyTo) return getMessage(input.replyTo);
311
+ // Standalone self→parent path (#976): only a spawned child acking its OWN spawn-parent. Anchor to
312
+ // the latest inbound from that parent so the drop mirrors the reply-path collapse; if there is no
313
+ // prior inbound to anchor to, deliver (never drop a message we can't attribute to a conversation).
314
+ if (sender?.spawnedBy && sender.spawnedBy === input.to) {
315
+ return latestInboundMessage(input.from, sender.spawnedBy);
316
+ }
317
+ return null;
301
318
  }
302
319
 
320
+ // Exact-match ceremonial acks (after normalization). Kept an explicit set — high precision, so a
321
+ // real short status ("Cleared the queue", "PASS") never matches, only the bare acknowledgement.
322
+ const PASSIVE_ACK_PHRASES = new Set([
323
+ "noted", "noted standing by",
324
+ "acknowledged", "acknowledged standing by",
325
+ "standing by", "ok standing by", "okay standing by",
326
+ "understood", "understood standing by",
327
+ "cleared", "will do", "will do standing by", "will comply",
328
+ "on it", "got it", "roger", "roger that", "copy", "copy that",
329
+ "sounds good", "ok", "okay", "okay will do", "thanks", "thank you",
330
+ "no problem", "no worries",
331
+ ]);
332
+
333
+ // A short SELF-REFERENTIAL acknowledgement of an instruction about the agent's OWN reporting
334
+ // behavior ("Going forward, I'll only use Relay mid-task…"). Bounded length + it must name the
335
+ // messaging/reporting channel, so a genuine forward-looking REPORT that happens to open with
336
+ // "going forward" ("going forward the build fails unless we bump node") still passes through.
337
+ const REPORTING_META_ACK = /^(going forward|from now on)\b.{0,160}\b(relay|messages?|acks?|reports?|reporting|pings?|notif\w*)\b/;
338
+
303
339
  function isPassiveAck(body: string): boolean {
304
- const text = body.toLowerCase().replace(/[.!]+/g, "").replace(/\s+/g, " ").trim();
305
- return [
306
- "noted",
307
- "noted standing by",
308
- "acknowledged",
309
- "acknowledged standing by",
310
- "standing by",
311
- "ok standing by",
312
- "okay standing by",
313
- "understood",
314
- "understood standing by",
315
- ].includes(text);
340
+ const text = body.toLowerCase().replace(/[.!,…]+/g, " ").replace(/\s+/g, " ").trim();
341
+ if (!text) return false;
342
+ if (PASSIVE_ACK_PHRASES.has(text)) return true;
343
+ return text.length <= 160 && REPORTING_META_ACK.test(text);
316
344
  }
@@ -647,6 +647,9 @@ export async function spawnAgent(input: SpawnAgentInput, ctx: AuthContext): Prom
647
647
  providerArgs: input.providerArgs ?? [],
648
648
  prompt: resolvedPrompt,
649
649
  systemPromptAppend,
650
+ // #1062 — stash the RAW caller brief (pre-composition) so a dashboard (non-policy) restart can
651
+ // re-thread it as callerSystemPromptAppend and stay fresh-spawn-identical instead of dropping it.
652
+ ...(resolvedSystemPromptAppend ? { callerSystemPromptAppend: resolvedSystemPromptAppend } : {}),
650
653
  relayInjectionEvents: composedInstructions.relayInjectionEvents,
651
654
  landStrategy: input.landStrategy,
652
655
  autoMerge: input.autoMerge,
@@ -96,6 +96,13 @@ interface BuildSpawnCommandOptions {
96
96
  providerArgs?: string[];
97
97
  prompt?: string;
98
98
  systemPromptAppend?: string;
99
+ /**
100
+ * #1062 — the RAW caller/parent brief (before project/profile/memory composition), persisted
101
+ * alongside the composed `systemPromptAppend` so a dashboard (non-policy) restart can re-thread it
102
+ * as `callerSystemPromptAppend` and re-project the rest fresh — otherwise the restart re-composes
103
+ * project/profile/memory but silently drops the parent's original task brief.
104
+ */
105
+ callerSystemPromptAppend?: string;
99
106
  relayInjectionEvents?: unknown[];
100
107
  landStrategy?: WorkspaceLandingPolicy;
101
108
  autoMerge?: WorkspaceAutoMergePolicy;
@@ -170,6 +177,7 @@ export function buildSpawnCommand(opts: BuildSpawnCommandOptions): Record<string
170
177
  ...def(opts.providerArgs, "providerArgs"),
171
178
  ...def(opts.prompt, "prompt"),
172
179
  ...def(opts.systemPromptAppend, "systemPromptAppend"),
180
+ ...def(opts.callerSystemPromptAppend, "callerSystemPromptAppend"),
173
181
  ...def(opts.relayInjectionEvents, "relayInjectionEvents"),
174
182
  ...def(opts.landStrategy, "landStrategy"),
175
183
  ...def(opts.autoMerge, "autoMerge"),
@@ -6,7 +6,7 @@ import { renderTemplate } from "./prompt-resolver";
6
6
  import { getRoutingPolicy } from "./routing-policy-store";
7
7
  import { isPathWithinBase } from "./utils";
8
8
  import { McpNotFoundError } from "./mcp-errors";
9
- import { DEFAULT_QUOTA_PREFER_SLACK_AT, DEFAULT_QUOTA_TARGET_UTILIZATION, quotaPaceMetrics } from "agent-relay-sdk/provider-quota";
9
+ import { DEFAULT_QUOTA_PREFER_SLACK_AT, DEFAULT_QUOTA_TARGET_UTILIZATION, quotaOvershootPenalty, quotaPaceMetrics } from "agent-relay-sdk/provider-quota";
10
10
  import { isSpawnProvider, type ProviderQuotaRoutingConfig } from "agent-relay-sdk";
11
11
  import type { Orchestrator, ProviderCatalogSummary, ProviderQuotaBurnRole, ProviderQuotaBurnWindow, ProviderQuotaRecord, QuotaState, SpawnProvider } from "./types";
12
12
 
@@ -82,13 +82,11 @@ interface SpawnQuotaBurnWindow {
82
82
  timeToWallMs: number | null;
83
83
  projectedWallAt: number | null;
84
84
  earlyWallMs: number | null;
85
- earlyFraction: number | null;
86
85
  // #760 — symmetric underburn signal (see ProviderQuotaBurnWindow.slack), re-derived against
87
86
  // the per-provider target, plus the "prefer" flag for this window.
88
87
  projectedUtilAtReset: number | null;
89
88
  slack: number | null;
90
89
  prefer: boolean;
91
- weight: 1 | 3;
92
90
  penalty: number;
93
91
  blockedNow: boolean;
94
92
  }
@@ -272,45 +270,34 @@ function spawnQuotaWindow(
272
270
  ): SpawnQuotaBurnWindow | undefined {
273
271
  if (!quotaWindow) return undefined;
274
272
  const timeToResetMs = typeof quotaWindow.resetsAt === "number" ? Math.max(0, quotaWindow.resetsAt - now) : null;
275
- // earlyWallMs may be negative (#760 symmetric); the over-pace penalty only sees the positive half.
276
- const earlyFraction = burnWindow.earlyWallMs !== null && timeToResetMs && timeToResetMs > 0
277
- ? Math.max(0, burnWindow.earlyWallMs) / timeToResetMs
278
- : null;
279
- const weight = burnWindow.role === "7d" ? 3 : 1;
280
273
  const hardBackstop = providerCfg?.hardBackstopUtilization;
281
- // #760 — floor default is now 0 (off). The projection/pace gate (earlyFraction≈0 when the
282
- // recency-weighted rate projects no early wall) replaces the blunt utilization floor, which
283
- // used to hide a legitimate low-util-high-burst alarm. Operators may still opt back in.
274
+ // #760 — floor default is now 0 (off). The evidence-capped projection replaces the blunt
275
+ // utilization floor (which hid a legitimate low-util-high-burst alarm); operators may still opt
276
+ // back in to raw-utilization suppression by setting a positive floor.
284
277
  const utilizationFloor = providerCfg?.burnPenaltyUtilizationFloor ?? 0;
285
- const pacePenalty = (earlyFraction ?? 0) * weight;
286
278
 
287
- // #760re-derive the pace projection against the PER-PROVIDER target (the stored burn used
288
- // the default). Slack is the symmetric underburn signal; prefer flags a window with no
289
- // over-pace penalty and preferSlackAt projected headroom at reset.
279
+ // #1072 — the band projection is evidence-capped at INGEST off the slow rate and is
280
+ // target-independent, so keep it as-is and re-derive only the per-provider-target slack + penalty
281
+ // from it. burnRatio (display) re-paces the same slow rate against the per-provider target.
290
282
  const target = providerCfg?.targetUtilization ?? DEFAULT_QUOTA_TARGET_UTILIZATION;
291
283
  const preferSlackAt = providerCfg?.preferSlackAt ?? DEFAULT_QUOTA_PREFER_SLACK_AT;
292
284
  const blockedNow = hardBackstop === 0 ? false : quotaWindow.utilization >= (hardBackstop ?? 0.99);
285
+ const projectedUtilAtReset = burnWindow.projectedUtilAtReset;
293
286
  let burnRatio = burnWindow.burnRatio;
294
- let projectedUtilAtReset = burnWindow.projectedUtilAtReset;
295
- let slack = burnWindow.slack;
296
287
  if (burnWindow.observedRatePerMs !== null && timeToResetMs !== null && timeToResetMs > 0) {
297
- const pace = quotaPaceMetrics({ utilization: quotaWindow.utilization, observedRatePerMs: burnWindow.observedRatePerMs, timeToResetMs, target });
298
- burnRatio = pace.burnRatio;
299
- projectedUtilAtReset = pace.projectedUtilAtReset;
300
- slack = pace.slack;
288
+ burnRatio = quotaPaceMetrics({ utilization: quotaWindow.utilization, observedRatePerMs: burnWindow.observedRatePerMs, timeToResetMs, target }).burnRatio;
301
289
  }
302
290
 
303
- // #798position penalty: an idle provider whose (de-biased) projection lands AT/OVER target is
304
- // already over-extended, but its forward-wall pace penalty is 0 (no burn) so without this the
305
- // band would flip to "normal" and route big jobs onto a near-exhausted provider. Derive a STABLE
306
- // penalty from the overshoot magnitude (slack < 0 projectedUtilAtReset target), decoupled
307
- // from the jittery forward-burn term. Gated to idle (observedRatePerMs collapsed to 0): an ACTIVE
308
- // provider is still governed entirely by the pace penalty (effective = max of the two), so this
309
- // adds a floor for the parked-high case WITHOUT changing active routing. Under-target idle ⇒ 0.
310
- const idle = burnWindow.observedRatePerMs !== null && burnWindow.observedRatePerMs <= 0;
311
- const overshoot = slack !== null ? Math.max(0, -slack) : 0;
312
- const positionPenalty = idle ? Math.min(1, overshoot / Math.max(1e-9, 1 - target)) * weight : 0;
313
- const rawPenalty = Math.max(pacePenalty, positionPenalty);
291
+ // #1072ONE unified overshoot penalty (replaces #760 earlyFraction, the #1071 7d ×3 weight,
292
+ // and the #798 idle position penalty). The measured current overshoot counts at full weight; the
293
+ // projected trend overshoot is scaled by the continuous confidence, so thin evidence cannot
294
+ // hard-escalate on an extrapolated slope. Idle and active share this single formula — an idle
295
+ // over-target provider is held by the current-overshoot term (confidence-independent, so the
296
+ // band is flat tick-to-tick), an active climb escalates as the confidence-weighted trend crosses.
297
+ const confidence = burnWindow.confidenceScore ?? 0;
298
+ const { slack, penalty: rawPenalty } = projectedUtilAtReset !== null
299
+ ? quotaOvershootPenalty(projectedUtilAtReset, quotaWindow.utilization, target, confidence)
300
+ : { slack: burnWindow.slack, penalty: 0 };
314
301
  const penalty = quotaWindow.utilization < utilizationFloor ? 0 : rawPenalty;
315
302
  const prefer = !blockedNow && penalty === 0
316
303
  && quotaPenaltyBandFor(penalty) === "normal"
@@ -327,11 +314,9 @@ function spawnQuotaWindow(
327
314
  timeToWallMs: burnWindow.timeToWallMs,
328
315
  projectedWallAt: burnWindow.projectedWallAt,
329
316
  earlyWallMs: burnWindow.earlyWallMs,
330
- earlyFraction,
331
317
  projectedUtilAtReset,
332
318
  slack,
333
319
  prefer,
334
- weight,
335
320
  penalty,
336
321
  blockedNow,
337
322
  };
package/src/sse.ts CHANGED
@@ -4,7 +4,7 @@ import { projectAgentStatusData } from "./agent-card-projection";
4
4
  import { emitRelayEvent, subscribeRelayEvents, type RelayEvent } from "./events";
5
5
  import { messageMatchesAgent, targetMatchesAgent } from "./agent-ref";
6
6
  import type { ActivityEvent, AgentCard, Message, RelayInjectionCategory, RelayNotification, Task } from "./types";
7
- import { isRecord } from "agent-relay-sdk";
7
+ import { isRecord, isWorkFinalMessagePayload } from "agent-relay-sdk";
8
8
  import { agentProviderState } from "./provider-state";
9
9
 
10
10
  interface Connection {
@@ -165,6 +165,10 @@ function fanout(event: RelayEvent): void {
165
165
  sendNewMessages(batchMessages(event.data), event.seq);
166
166
  return;
167
167
  }
168
+ if (event.type === "message.finalized") {
169
+ sendMessageUpdate("message.finalized", event.data as unknown as Message, event.seq);
170
+ return;
171
+ }
168
172
  if (event.type === "task.updated" || event.type === "task.created" || event.type === "task.claimed" || event.type === "task.status") {
169
173
  sendTaskChanged(event.data as unknown as Task, event.type, event.seq);
170
174
  return;
@@ -203,6 +207,15 @@ function sendNewMessages(messages: Message[], seq?: number): void {
203
207
  }
204
208
  }
205
209
 
210
+ function sendMessageUpdate(eventName: string, msg: Message, seq?: number): void {
211
+ const chunk = sseFrame(eventName, msg, seq);
212
+ const agentCache = new Map<string, AgentCard | null>();
213
+ for (const conn of connections.values()) {
214
+ if (conn.agentId && !messageMatchesAgent(msg, conn.agentId, fanoutAgent(conn.agentId, agentCache))) continue;
215
+ sendEncoded(conn, chunk);
216
+ }
217
+ }
218
+
206
219
  function batchMessages(data: Record<string, unknown>): Message[] {
207
220
  const messages = (data as { messages?: unknown }).messages;
208
221
  return Array.isArray(messages) ? messages as Message[] : [];
@@ -329,6 +342,10 @@ export function emitMessageReactionUpdated(msg: Message) {
329
342
  emitRelayEvent({ type: "message.reaction_updated", source: "server", subject: String(msg.id), data: msg as unknown as Record<string, unknown> });
330
343
  }
331
344
 
345
+ export function emitMessageFinalized(msg: Message) {
346
+ emitRelayEvent({ type: "message.finalized", source: "server", subject: String(msg.id), data: msg as unknown as Record<string, unknown> });
347
+ }
348
+
332
349
  function sendTaskChanged(task: Task, eventType = "task.updated", seq?: number): void {
333
350
  const chunk = sseFrame(eventType, task, seq);
334
351
  const agentCache = new Map<string, AgentCard | null>();
@@ -387,6 +404,12 @@ function sendReplayEvent(conn: Connection, event: RelayEvent, agentCache: Map<st
387
404
  send(conn, "message.new", msg, event.seq);
388
405
  return;
389
406
  }
407
+ if (event.type === "message.finalized" && conn.agentId) {
408
+ const msg = event.data as unknown as Message;
409
+ if (!messageMatchesAgent(msg, conn.agentId, fanoutAgent(conn.agentId, agentCache))) return;
410
+ send(conn, "message.finalized", msg, event.seq);
411
+ return;
412
+ }
390
413
  if (event.type.startsWith("task.") && conn.agentId) {
391
414
  const task = event.data as unknown as Task;
392
415
  if (!targetMatchesAgent(task.target, conn.agentId, fanoutAgent(conn.agentId, agentCache))) return;
@@ -481,8 +504,7 @@ function isInboundChannelMessage(msg: Message): boolean {
481
504
 
482
505
  function isBadgeWorthyMessage(msg: Message): boolean {
483
506
  if (msg.kind !== "session") return false;
484
- const session = isRecord(msg.payload?.session) ? msg.payload.session : undefined;
485
- return session?.final === true;
507
+ return isWorkFinalMessagePayload(msg.payload);
486
508
  }
487
509
 
488
510
  function notificationForMessage(msg: Message): RelayNotification | undefined {
@@ -3,6 +3,7 @@ import { isRecord, stringValue } from "agent-relay-sdk";
3
3
  import { createCommand } from "./commands-db";
4
4
  import {
5
5
  acquireMergeLease,
6
+ exitCertificateMatchesInstance,
6
7
  getActiveMergePause,
7
8
  getAgent,
8
9
  listAgents,
@@ -11,6 +12,7 @@ import {
11
12
  setMergeLeaseCommand,
12
13
  updateWorkspaceStatus,
13
14
  } from "./db";
15
+ import { OFFLINE_PRUNE_MS } from "./config";
14
16
  import { getLandingConfig } from "./config-store";
15
17
  import type {
16
18
  Command,
@@ -149,15 +151,33 @@ export function withOwnerOnline<T extends { ownerAgentId?: string }>(workspace:
149
151
  // the 2026-07-04 cascade). A genuine process exit is only known when the owning
150
152
  // orchestrator REPORTS it: it stamps `meta.lastExit` (routes/orchestrator.ts). So
151
153
  // require both offline AND a reported exit before treating the owner as gone — never
152
- // nuke/land a worktree on a heartbeat gap alone. Conservative by design: an unmanaged
153
- // agent with no orchestrator never gets `lastExit`, so it reads as maybe-alive and the
154
- // orphan reaper's land-safety + grace window handle its eventual cleanup instead.
154
+ // nuke/land a worktree on a heartbeat gap alone.
155
+ //
156
+ // The confirmed-gone signal is the reconciling primitive between two opposing failures:
157
+ // • false POSITIVE (#1054/#1059) — reaping a live-but-lagging owner. Guarded by requiring
158
+ // a reported exit AND that the exit certificate belongs to the CURRENT instance: a stale
159
+ // report for a superseded session carries a different `spawnRequestId` and is rejected.
160
+ // • false NEGATIVE (#1060) — leaking a genuinely-gone owner's worktree. A MISSING row (pruned
161
+ // after OFFLINE_PRUNE_MS offline, or an explicit deleteAgent) can't be a live process under
162
+ // relay management, so it is gone; likewise an owner offline past the prune horizon is about
163
+ // to be pruned anyway and can't be a mere heartbeat-lagging owner (that window is bounded by
164
+ // OFFLINE_PRUNE_MS). Both restore the pre-#1054 isOwnerAlive reapability the #1054 gate lost.
155
165
  export function isOwnerProcessConfirmedGone(ownerAgentId: string | undefined): boolean {
156
166
  if (!ownerAgentId) return false;
157
167
  const agent = getAgent(ownerAgentId);
158
- if (!agent || agent.status !== "offline") return false;
168
+ // #1060 the owner row is gone (24h-offline prune or deleteAgent): genuinely gone, reapable.
169
+ if (!agent) return true;
170
+ if (agent.status !== "offline") return false;
171
+ // #1060(3) — offline past the prune horizon: can't be a live-but-lagging owner and is about to
172
+ // become a missing row anyway. Covers an unmanaged owner that never gets meta.lastExit.
173
+ if (Date.now() - agent.lastSeen >= OFFLINE_PRUNE_MS) return true;
159
174
  const meta = isRecord(agent.meta) ? agent.meta : undefined;
160
- return Boolean(meta && meta.lastExit);
175
+ if (!meta) return false;
176
+ const lastExit = isRecord(meta.lastExit) ? meta.lastExit : undefined;
177
+ if (!lastExit) return false;
178
+ // #1059 — the reported exit must be for THIS instance, not a stale certificate stamped by a
179
+ // late report for a superseded predecessor (see exitCertificateMatchesInstance).
180
+ return exitCertificateMatchesInstance(meta, lastExit);
161
181
  }
162
182
 
163
183
  function workspaceOwnerHostKeys(workspace: WorkspaceRecord): Set<string> {
@@ -26,7 +26,7 @@ import { createCommand } from "./commands-db";
26
26
  import { emitRelayEvent } from "./events";
27
27
  import { isPathWithinBase } from "./utils";
28
28
  import { TERMINAL_WORKSPACE_STATUSES, worktreeReapable, type WorktreeReapState } from "./workspace-phase";
29
- import { isOwnerAlive, liveAgentCwds, worktreeInUseByLiveAgent } from "./workspace-merge";
29
+ import { isOwnerAlive, isOwnerProcessConfirmedGone, liveAgentCwds, worktreeInUseByLiveAgent } from "./workspace-merge";
30
30
  import { applyWorkspaceAction } from "./workspace-actions";
31
31
  import { workspaceOwnerOrchestrator } from "./workspace-host";
32
32
  import { UNLANDED_FLAG_COOLDOWN_MS, orphanGraceMs, orphanWorktreeReapEnabled, relayToken } from "./config";
@@ -606,14 +606,21 @@ export async function reapOrphanedWorktrees(): Promise<Record<string, unknown>>
606
606
  const key = `dead-owner:${resolve(workspace.worktreePath)}`;
607
607
  liveDeadOwnerKeys.add(key);
608
608
 
609
- if (isOwnerAlive(workspace.ownerAgentId)) {
610
- deadOwnerTracker.delete(key); // owner alive reset the grace window (regression vs #278)
609
+ // #1054 — gate on PROCESS-confirmed death, not heartbeat-only liveness. A live
610
+ // owner that is falsely marked `offline` (sustained relay lag / network partition
611
+ // >30min) has no orchestrator-reported `meta.lastExit`; isOwnerAlive alone would
612
+ // treat it as reap-eligible and destroy its landed+clean worktree — killing the
613
+ // live session's cwd. worktreeInUseByLiveAgent can't protect it (offline owners are
614
+ // excluded from liveCwds). Require offline AND a reported exit before the grace clock
615
+ // even starts, so only a genuinely-gone owner's worktree is ever reaped.
616
+ if (!isOwnerProcessConfirmedGone(workspace.ownerAgentId)) {
617
+ deadOwnerTracker.delete(key); // alive, or falsely-offline live — reset the grace window (#1054/#278)
611
618
  continue;
612
619
  }
613
620
 
614
- // Owner observed dead. Require continuous dead-ness for the grace window so a
615
- // reconnecting agent isn't raced; the in-memory tracker re-arms on restart. A
616
- // grace of 0 reaps on the first dead observation (the env's escape hatch).
621
+ // Owner process confirmed gone. Require continuous confirmed-gone-ness for the grace
622
+ // window so a reconnecting agent isn't raced; the in-memory tracker re-arms on
623
+ // restart. A grace of 0 reaps on the first confirmed-gone observation (env escape hatch).
617
624
  const tracked = deadOwnerTracker.get(key);
618
625
  const firstSeenDeadAt = tracked?.firstSeenDeadAt ?? now;
619
626
  if (!tracked) deadOwnerTracker.set(key, { firstSeenDeadAt });
@@ -1,3 +0,0 @@
1
- import{d as e,g as t,h as n,o as r,p as i,s as a}from"./types-uVOXgvMT.js";var o=`user`,s=o,c=5e3,l=new Set([`done`,`failed`,`canceled`]),u=new Set([`open`,`blocked`]),d={online:0,idle:0,busy:2,offline:3},f={idle:0,online:0,busy:2,offline:4},p={from:``,to:``,body:``,channel:``,subject:``,claimable:!1},m={toMode:`agent`,to:``,body:``,channel:``,subject:``,claimable:!1},h={pairId:``,from:``,body:``,subject:``},ee={requesterId:``,targetId:``,objective:``},te={user:`User`,system:`Server`,channel:`MessagesSquare`,agent:`Bot`},ne={user:`Human operator`,system:`System`,channel:`Channel`,agent:`Agent`};({...te,...Object.fromEntries(i().map(e=>[e,`Bot`]))}),{...ne,...Object.fromEntries(i().map(e=>[e,`${n(e)} agent`]))};var re=[{key:`overview`,label:`Overview`,icon:`LayoutDashboard`},{key:`chat`,label:`Chat`,icon:`MessageCircle`},{key:`agents`,label:`Agents`,icon:`Bot`},{key:`managed`,label:`Managed Agents`,icon:`Workflow`},{key:`teams`,label:`Teams`,icon:`Users`},{key:`profiles`,label:`Agent Profiles`,icon:`UserCog`},{key:`orchestrators`,label:`Orchestrators`,icon:`Server`},{key:`workspaces`,label:`Workspaces`,icon:`GitBranch`},{key:`files`,label:`Files`,icon:`FolderTree`},{key:`channels`,label:`Channels`,icon:`MessagesSquare`},{key:`connectors`,label:`Connectors`,icon:`Plug`},{key:`integrations`,label:`Integrations`,icon:`PlugZap`},{key:`security`,label:`Security`,icon:`Shield`},{key:`memory`,label:`Memory`,icon:`BrainCircuit`},{key:`activity`,label:`Activity`,icon:`Activity`},{key:`pairs`,label:`Pairs`,icon:`Link`},{key:`messages`,label:`Messages`,icon:`Mail`},{key:`work`,label:`Work Queue`,icon:`ListChecks`},{key:`tasks`,label:`Tasks`,icon:`ClipboardList`},{key:`automation`,label:`Automation`,icon:`CalendarClock`},{key:`analytics`,label:`Analytics`,icon:`AreaChart`},{key:`insights`,label:`Insights`,icon:`Lightbulb`},{key:`projects`,label:`Projects`,icon:`Boxes`},{key:`prompts`,label:`Prompts`,icon:`FileText`},{key:`registry`,label:`Registry`,icon:`PackagePlus`},{key:`maintenance`,label:`Maintenance`,icon:`Wrench`},{key:`settings`,label:`Settings`,icon:`Settings`}],ie={critical:`text-red-400 bg-red-500/10`,warning:`text-yellow-400 bg-yellow-500/10`,info:`text-blue-400 bg-blue-500/10`},ae={active:`bg-emerald-500/20 text-emerald-400`,pending:`bg-yellow-500/20 text-yellow-400`,ended:`bg-zinc-500/20 text-zinc-400`,rejected:`bg-red-500/20 text-red-400`,expired:`bg-red-500/20 text-red-400`};function g(e){let t=typeof e==`number`?e:new Date(e||0).getTime();return Number.isFinite(t)?t:0}var _=[`lifecycle.`,`commands.`,`capabilities.`],v={"+1":`👍`,":+1:":`👍`,thumbsup:`👍`,":thumbsup:":`👍`,"thumbs-up":`👍`,":thumbs-up:":`👍`,thumbs_up:`👍`,":thumbs_up:":`👍`,thumb_up:`👍`,":thumb_up:":`👍`,like:`👍`,":like:":`👍`,heart:`❤️`,":heart:":`❤️`,redheart:`❤️`,red_heart:`❤️`,":red_heart:":`❤️`,check:`✅`,":check:":`✅`,checkmark:`✅`,":checkmark:":`✅`,white_check_mark:`✅`,":white_check_mark:":`✅`,eyes:`👀`,":eyes:":`👀`};function y(e){let t=e.trim();return v[t.toLowerCase()]??t}function oe(e){return e.filter(e=>!_.some(t=>e.startsWith(t)))}function se(e,t=2){if(!e)return``;let n=e.replace(/\/+$/,``).split(`/`);return n.length<=t?e:n.slice(-t).join(`/`)}function b(e){return e.trim().replace(/\\/g,`/`).replace(/\/+$/,``)||`/`}function ce(e,t){if(!e?.trim()||!t?.trim())return!1;let n=b(e),r=b(t);return r===`/`?n.startsWith(`/`):n===`/`?!1:n===r||n.startsWith(r+`/`)}function x(e){return e?.meta?.builtin===!0||e?.id===`user`||e?.id===`system`}function S(e){let t=e?.tags||[],n=e?.capabilities||[],r=typeof e?.meta?.policyName==`string`?e.meta.policyName:``;return e?.kind===`channel`||e?.meta?.kind===`channel`||t.includes(`channel`)||t.some(e=>e.startsWith(`channel:`))||n.includes(`channel`)||n.some(e=>e.startsWith(`channel:`))||t.includes(`telegram`)||n.includes(`telegram`)||r===`telegram-main`}function C(e){return!!(typeof e?.meta?.automationId==`string`||typeof e?.meta?.automationRunId==`string`||e?.tags?.includes(`automation`))}function w(e){return x(e)||S(e)||C(e)}function T(e){if(e?.id===`user`)return`user`;if(e?.id===`system`)return`system`;if(S(e))return`channel`;let t=i(),n=typeof e?.meta?.provider==`string`?e.meta.provider.toLowerCase():void 0;if(n&&t.includes(n))return n;let r=[...e?.tags||[],e?.meta?.client,e?.meta?.runtime,e?.meta?.agentType].filter(e=>typeof e==`string`).map(e=>e.toLowerCase());for(let e of t)if(r.some(t=>t.includes(e)))return e;return`agent`}function E(t,n){if(!n?.lastSeen||n.status===`offline`||n.id===`user`||n.id===`system`)return!1;let r=n.meta?.transport;if(e(r)&&r.connected===!0)return!1;let i=new Date(n.lastSeen).getTime();return Number.isFinite(i)?t-i>6e4:!1}function D(e,t){if(!e||x(e)||S(e)||t===`resume`)return!1;if(t===`interrupt`)return e.providerCapabilities?.liveSession?.interrupt===!0&&e.status===`busy`;if(k(e)&&(t===`restart`||t===`shutdown`||t===`compact`||t===`clearContext`)&&!O(e))return!1;let n=e.providerCapabilities?.lifecycle;if(n){if(t===`restart`)return n.restartHard===!0;if(t===`shutdown`)return n.shutdownHard===!0;if(t===`reconnect`)return n.reconnect===!0}return t===`compact`?e.providerCapabilities?.context?.compact===!0:t===`clearContext`?e.providerCapabilities?.context?.clear===!0:e.meta?.runnerManaged===!0&&(t===`restart`||t===`shutdown`)}function O(e){return typeof e.meta?.tmuxSession==`string`&&e.meta.tmuxSession.trim().length>0}function k(e){return(typeof e?.meta?.provider==`string`?t(e.meta.provider):void 0)?.capabilityDefaults?.terminal?.mode===`live`}function A(e){return D(e,`restart`)||D(e,`shutdown`)||D(e,`reconnect`)||D(e,`compact`)||D(e,`clearContext`)}function j(e){return!!(e&&!x(e)&&!S(e)&&!A(e)&&(e.status===`offline`||e.status===`stale`))}function M(e,t){if(e.status!==`offline`)return!1;let n=typeof e.meta?.policyName==`string`?e.meta.policyName:``;if(!n)return!1;let r=t.find(e=>e.policy.name===n);return!!(r?.state.agentId&&r.state.agentId!==e.id)}function N(e,t,n=[]){let r=e.filter(e=>!M(e,n));return t?r:r.filter(e=>!w(e))}function P(e){return e?e.label||e.name||e.id.slice(-12):`?`}function F(e,t){let n=a(e);return n?t[n]:void 0}function I(e,t){let n=F(e,t);return n?.displayName?.trim()||n?.handle?.trim()||`User`}var L=[`text-sky-300`,`text-amber-300`,`text-emerald-300`,`text-violet-300`,`text-rose-300`,`text-cyan-300`];function R(e){let t=a(e)||``,n=0;for(let e=0;e<t.length;e++)n=n*31+t.charCodeAt(e)>>>0;return L[n%L.length]}function z(e,t){if(!e)return`?`;if(e===`broadcast`)return`broadcast`;if(e.startsWith(`tag:`))return`#`+e.slice(4);if(e.startsWith(`cap:`))return e.slice(4);if(e.startsWith(`label:`))return e.slice(6);if(e.startsWith(`team:`)){let n=e.slice(5),r=Object.values(t).find(e=>e.teamId===n);return r?`team:${P(r)}`:`team:${n.slice(-8)}`}let n=t[e];return n?P(n):e.slice(-8)}function B(e){if(!e)return``;let t=e.payload||{},n=t.message;if(n&&typeof n==`object`&&typeof n.text==`string`&&n.text.trim())return n.text;let r=t.interaction;if(r&&typeof r==`object`){let e=typeof r.title==`string`?r.title.trim():``,t=typeof r.description==`string`?r.description.trim():``;if(e&&t)return e+`
2
- `+t;if(e)return e;if(t)return t}let i=t.reaction;if(i&&typeof i==`object`){let e=typeof i.name==`string`?i.name:``,t=typeof i.emoji==`string`?i.emoji:``,n=typeof i.value==`string`?i.value:``;return[`Reaction`,y(t||e||n)].filter(Boolean).join(`: `)}let a=t.activity;return a&&typeof a==`object`?[typeof a.kind==`string`?a.kind:`activity`,typeof a.state==`string`?a.state:``].filter(Boolean).join(` `):typeof t.text==`string`&&t.text.trim()?t.text:typeof t.message==`string`&&t.message.trim()?t.message:U(e.body||``)??(e.body||``)}function V(e){if(!e?.payload)return!1;let t=e.payload.event;return e.payload.reactionNotification===!0||t?.type===`message.reaction`}function H(e){if(!e)return null;let t=e.trim();if(!t.startsWith(`{`))return null;try{let e=JSON.parse(t);if(typeof e!=`object`||!e||Array.isArray(e))return null;let n=typeof e.text==`string`?e.text:typeof e.message==`string`?e.message:null;if(!n?.trim())return null;let r=new Set([`text`,`message`]);return{text:n,meta:Object.entries(e).filter(([e,t])=>!r.has(e)&&(typeof t==`string`||typeof t==`number`||typeof t==`boolean`)).map(([e,t])=>[e,String(t)]),raw:e}}catch{return null}}function U(e){return H(e)?.text??null}function W(e){let t=e?.subject||B(e)||``;return t.length>90?t.slice(0,90)+`...`:t}function G(e){return r(e.from)&&e.to?e.to:r(e.to)&&e.from?e.from:``}function K(e){return r(e.to)&&!r(e.from)}function le(e){if(e.kind!==`session`)return!1;let t=e.payload?.session?.type;return t===`reasoning`||t===`tool`}function q(e){return e.kind===`session`&&e.payload?.session?.final===!0}function ue(e,t){let n=e.trim(),r=t.trim();return r?n===r?!0:n.startsWith(`${r}\n\n`)||n.endsWith(`\n\n${r}`)||n.includes(`\n\n${r}\n\n`):!1}function de(e,t,n){if(!K(n)||!q(n)||(n.readBy||[]).includes(`user`))return!1;let r=Number(e[t]||0);return n.id>(Number.isFinite(r)?r:0)}function J(e){return(e.kind??`claim`)===`claim`&&u.has(e.status)&&!e.claimedBy}function fe(e){return!!(e.claimable&&!e.claimedBy&&!(e.kind===`task`&&Number.isSafeInteger(e.payload?.taskId)))}function pe(e,t){return!e||!t?!1:e===`broadcast`||e===t.id?!0:e.startsWith(`tag:`)?(t.tags||[]).includes(e.slice(4)):e.startsWith(`cap:`)?(t.capabilities||[]).includes(e.slice(4)):e.startsWith(`label:`)?t.label===e.slice(6):e.startsWith(`team:`)?t.teamId===e.slice(5):!1}function me(e,t){return e?t.from===e||t.to===e||t.resolvedToAgent===e:!1}function he(e,t){if(!e||!t)return!1;let n=[t.id,t.type,...t.topicChannels||[]].filter(Boolean);if(e.channel&&n.includes(e.channel))return!0;let r=e.payload?.channel;if(r&&typeof r==`object`){let e=[r.agentId,r.provider,r.accountId].filter(Boolean);if(e.includes(t.id)||e.includes(t.type)||t.accountId&&e.includes(t.accountId))return!0}return!!(t.agentId&&(e.from===t.agentId||e.to===t.agentId))}function Y(t){let n=t?.meta?.providerState;return!e(n)||typeof n.state!=`string`?null:{state:n.state,reason:typeof n.reason==`string`?n.reason:void 0,label:typeof n.label==`string`&&n.label?n.label:n.state,recommendedAction:typeof n.recommendedAction==`string`?n.recommendedAction:void 0,source:typeof n.source==`string`?n.source:void 0,raw:n.raw,updatedAt:typeof n.updatedAt==`number`?n.updatedAt:void 0,pendingApproval:ge(n.pendingApproval)}}function ge(t){if(!e(t)||typeof t.id!=`string`)return;let n=Array.isArray(t.choices)?t.choices.filter(e).map(e=>({id:e.id,label:typeof e.label==`string`?e.label:String(e.id||``)})).filter(e=>(e.id===`approve`||e.id===`approve-session`||e.id===`deny`||e.id===`abort`)&&!!e.label):[],r=Array.isArray(t.questions)?t.questions.filter(e).map(t=>({question:typeof t.question==`string`?t.question:``,header:typeof t.header==`string`?t.header:void 0,multiSelect:t.multiSelect===!0,options:Array.isArray(t.options)?t.options.filter(e).map(e=>({label:typeof e.label==`string`?e.label:String(e.label??``),description:typeof e.description==`string`?e.description:void 0})).filter(e=>!!e.label):[]})).filter(e=>!!e.question&&e.options.length>0):void 0;return{id:t.id,provider:typeof t.provider==`string`?t.provider:void 0,kind:typeof t.kind==`string`?t.kind:void 0,title:typeof t.title==`string`&&t.title?t.title:`Permission request`,body:typeof t.body==`string`?t.body:``,choices:n,...r&&r.length?{questions:r}:{}}}function X(e){let t=Y(e);return t?.state===`blocked`?t:null}function _e(e){return!!e?.id}function ve(e){return _e(X(e)?.pendingApproval)}function Z(t){let n=t?.meta?.activeSubagents;if(!Array.isArray(n)){let e=typeof t?.meta?.activeSubagentCount==`number`?t.meta.activeSubagentCount:0;return e>0?Array.from({length:e},(e,t)=>({id:`subagent-${t+1}`,label:`Subagent ${t+1}`})):[]}return n.filter(e).map((e,t)=>{let n=typeof e.id==`string`&&e.id?e.id:`subagent-${t+1}`,r=typeof e.role==`string`&&e.role?e.role:void 0;return{id:n,label:typeof e.label==`string`&&e.label?e.label:r||n,role:r,startedAt:typeof e.startedAt==`number`?e.startedAt:void 0}})}function Q(t){let n=t?.meta?.activeWork;if(!Array.isArray(n))return null;let r=n.find(t=>e(t)&&t.kind===`background-script`);return!r||!e(r)?null:typeof r.label==`string`&&r.label?r.label:`background script`}function ye(e,t,n,r){let i=typeof t?.meta?.lifecycleAction==`string`?t.meta.lifecycleAction:``,a=E(e,t),o=t?.status!==`offline`&&!t?.ready&&a,s=t?.status!==`offline`&&!t?.ready&&!a,c=n.unread>0&&t?.status!==`busy`,l=X(t),u=be(t,n,r);if(t?.status===`offline`)return{label:`offline`,tone:`secondary`,icon:`PlugZap`,stale:!1,reconnecting:!1,badges:u};if(i.startsWith(`finalizing-`))return{label:`wrapping up (${i.slice(11)})`,tone:`warning`,icon:`Hourglass`,stale:a,reconnecting:o,badges:u};if(i===`shutting-down`)return{label:`shutting down`,tone:`warning`,icon:`Power`,stale:a,reconnecting:o,badges:u};if(i===`killing`)return{label:`killing`,tone:`danger`,icon:`Power`,stale:a,reconnecting:o,badges:u};if(i===`restarting`)return{label:`restarting`,tone:`warning`,icon:`RefreshCw`,stale:a,reconnecting:o,badges:u};if(t?.status===`stale`)return{label:`stale`,tone:`danger`,icon:`RefreshCw`,stale:!0,reconnecting:!0,badges:u};if(o)return{label:`reconnecting`,tone:`danger`,icon:`RefreshCw`,stale:a,reconnecting:o,badges:u};if(s)return{label:`online, not ready`,tone:`warning`,icon:`Loader`,stale:a,reconnecting:o,badges:u};if(l)return{label:`blocked: ${l.label}`,tone:`danger`,icon:`AlertCircle`,stale:a,reconnecting:o,badges:u};if(t?.status===`busy`){let e=Array.isArray(t?.meta?.busyReasons)?t.meta.busyReasons:[];return Q(t)&&!e.includes(`provider-turn`)&&!e.includes(`subagent`)?{label:`running background work`,tone:`warning`,icon:`Play`,stale:a,reconnecting:o,badges:u}:{label:`busy in turn`,tone:`warning`,icon:`Play`,stale:a,reconnecting:o,badges:u}}return r?.status===`active`?{label:`paired`,tone:`success`,icon:`Link`,stale:a,reconnecting:o,badges:u}:c?{label:`idle, unread`,tone:`danger`,icon:`Bell`,stale:a,reconnecting:o,badges:u}:{label:t?.status===`idle`?`idle`:`ready`,tone:`success`,icon:`CircleCheck`,stale:a,reconnecting:o,badges:u}}function be(e,t,n){let r=[],i=X(e),a=Z(e).length;i&&r.push({label:i.reason?`blocked: ${i.reason}`:`blocked`,className:`bg-red-500/20 text-red-400`}),a&&r.push({label:a===1?`1 subagent`:a+` subagents`,className:`bg-cyan-500/20 text-cyan-300`});let o=Q(e);return o&&r.push({label:o,className:`bg-indigo-500/20 text-indigo-300`}),n?.status===`active`&&r.push({label:`paired`,className:`bg-emerald-500/20 text-emerald-400`}),n?.status===`pending`&&r.push({label:`pair invite`,className:`bg-yellow-500/20 text-yellow-400`}),t.needsHumanResponse&&r.push({label:`needs response`,className:`bg-amber-500/20 text-amber-300`}),t.unread&&r.push({label:t.unread+` unread`,className:`bg-red-500/20 text-red-400`}),t.claimableTasks&&r.push({label:t.claimableTasks+` claimable`,className:`bg-orange-500/20 text-orange-400`}),r}function xe(){return{unread:0,needsHumanResponse:!1,pendingPairInvite:!1,claimableTasks:0,total:0,score:0}}function $(e){return!e||e.status===`offline`?!1:e.targetHealth?.status===`ok`?!0:e.targetHealth?.status===`error`||e.targetHealth?.status===`warning`?!1:e.ready}function Se(e){return e?e.targetHealth?.status===`error`?{label:`target broken`,tone:`danger`,icon:`AlertTriangle`,badges:[]}:e.targetHealth?.status===`warning`?{label:`target warning`,tone:`warning`,icon:`AlertCircle`,badges:[]}:e.status===`offline`?{label:`offline`,tone:`secondary`,icon:`PlugZap`,badges:[]}:e.status===`busy`?{label:`busy`,tone:`warning`,icon:`Activity`,badges:[]}:$(e)?{label:`ready`,tone:`success`,icon:`CircleCheck`,badges:[]}:{label:`not ready`,tone:`warning`,icon:`Loader`,badges:[]}:{label:`unknown`,tone:`secondary`,icon:`PlugZap`,badges:[]}}function Ce(e){let t=e?.runtime||{};return t.status===`error`?{label:`error`,tone:`danger`,icon:`AlertTriangle`,badges:[]}:t.status===`warn`?{label:`warning`,tone:`warning`,icon:`AlertCircle`,badges:[]}:t.running?{label:`running`,tone:`success`,icon:`CircleCheck`,badges:[]}:t.enabled===!1?{label:`disabled`,tone:`secondary`,icon:`Pause`,badges:[]}:t.status===`ok`?{label:`ok`,tone:`success`,icon:`CircleCheck`,badges:[]}:{label:`unknown`,tone:`secondary`,icon:`HelpCircle`,badges:[]}}function we(e){let t=e?.taskStats||{};return(t.waitingTasks||0)>0?{label:`waiting`,tone:`warning`,icon:`AlertCircle`,badges:[]}:(t.openTasks||0)>0?{label:`active`,tone:`info`,icon:`Activity`,badges:[]}:e?.configured?e.observed?{label:`quiet`,tone:`success`,icon:`CircleCheck`,badges:[]}:{label:`configured`,tone:`primary`,icon:`PlugZap`,badges:[]}:{label:`observed only`,tone:`secondary`,icon:`Eye`,badges:[]}}function Te(e){let t=e?.health;return!t||t.status===`ok`?e?.version||e?.package?.version?`compatible`:`unknown version`:t.status===`upgrade-required`?`upgrade required`:t.status===`restart-required`?`restart required`:t.status===`unknown`?`unknown contract`:(t.issues||[]).some(e=>e.code===`protocol-mismatch`)?`protocol mismatch`:(t.issues||[]).some(e=>e.code===`package-drift`)?`package drift`:t.restartRequired?`restart required`:t.status}function Ee(e){let t=e?.health?.status;return t===`upgrade-required`?`text-red-400 bg-red-500/10`:t===`warn`||t===`restart-required`?`text-yellow-400 bg-yellow-500/10`:t===`unknown`?`text-zinc-400 bg-zinc-500/10`:`text-emerald-400 bg-emerald-500/10`}function De(e,t){if(!t)return``;let n=new Date(t).getTime();if(!Number.isFinite(n))return``;let r=Math.max(0,(e-n)/1e3);return r<60?Math.floor(r)+`s ago`:r<3600?Math.floor(r/60)+`m ago`:r<86400?Math.floor(r/3600)+`h ago`:Math.floor(r/86400)+`d ago`}function Oe(e){return e?new Date(e).toLocaleString():``}function ke(e){return String(e||`export`).replace(/[^a-z0-9._-]+/gi,`-`).replace(/^-+|-+$/g,``).slice(0,80)||`export`}function Ae(e){return e.filter(e=>e.status!==`ok`).map(e=>{let t={name:e.name,status:e.status,detail:e.detail||e.name,impact:je(e),subjects:e.subjects||[],actions:[{label:`Inspect logs`,icon:`FileSearch`,copy:`agent-relay daemon logs`},{label:`Restart daemon`,icon:`RefreshCw`,copy:`agent-relay daemon restart`},{label:`Copy env`,icon:`Copy`,copy:`agent-relay doctor`}]};return e.name===`stale-live-agents`?t.actions.unshift({label:`Run reaper`,icon:`Eraser`,api:`POST`,path:`/system/reap`},{label:`Show stale`,icon:`Filter`,view:`agents`,preset:`offline_stale`}):e.name===`channel-delivery-targets`?t.actions.unshift({label:`Open channels`,icon:`MessagesSquare`,view:`channels`},{label:`Run reaper`,icon:`Eraser`,api:`POST`,path:`/system/reap`}):[`expired-message-claims`,`expired-task-claims`,`offline-claimed-tasks`].includes(e.name)&&t.actions.unshift({label:`Run reaper`,icon:`Eraser`,api:`POST`,path:`/system/reap`},{label:`Open work`,icon:`ListChecks`,view:`work`}),t})}function je(e){return{database:`Relay persistence is unavailable; messages, state, and audit writes may fail.`,"stale-live-agents":`Agents may look online even though their heartbeat has stopped.`,"expired-message-claims":`Claimable messages may be stuck until the reaper releases expired claims.`,"expired-task-claims":`Tasks can appear owned by agents that no longer hold a live lease.`,"offline-claimed-tasks":`Offline agents are still shown as owners for active work.`,"channel-delivery-targets":`Inbound channel messages may be accepted but routed to no live delivery agent.`}[e.name]||`Relay health is degraded for this check.`}function Me(e){return{success:`text-emerald-400`,warning:`text-yellow-400`,danger:`text-red-400`,info:`text-blue-400`,primary:`text-blue-400`,secondary:`text-zinc-400`}[e]||`text-zinc-400`}function Ne(e,t){return!e||e.status===`offline`?`bg-zinc-500 opacity-50`:typeof e.meta?.lifecycleAction==`string`&&e.meta.lifecycleAction.startsWith(`finalizing-`)?`bg-amber-500 animate-pulse`:e.meta?.lifecycleAction===`shutting-down`||e.meta?.lifecycleAction===`restarting`?`bg-yellow-500 animate-pulse`:e.meta?.lifecycleAction===`killing`||X(e)?`bg-red-500 animate-pulse`:e.status===`busy`?`bg-yellow-500`:t&&E(t,e)?`bg-red-500 animate-pulse`:e.ready?`bg-emerald-500 shadow-[0_0_6px_theme(colors.emerald.500)]`:`bg-emerald-500 animate-pulse`}function Pe(e,t,n){if(typeof document>`u`||typeof URL>`u`||typeof Blob>`u`)return;let r=URL.createObjectURL(new Blob([t],{type:n})),i=document.createElement(`a`);i.href=r,i.download=e,i.click(),URL.revokeObjectURL(r)}export{p as $,me as A,Y as B,fe as C,le as D,V as E,Ee as F,Ne as G,ue as H,Te as I,Me as J,De as K,H as L,W as M,b as N,de as O,y as P,l as Q,ce as R,q as S,K as T,ke as U,F as V,se as W,N as X,oe as Y,f as Z,R as _,D as a,c as at,we as b,Se as c,ie as ct,P as d,m as et,z as f,Ae as g,Oe as h,ye as i,s as it,he as j,B as k,Ce as l,d as lt,xe as m,j as n,h as nt,T as o,re as ot,Pe as p,g as q,ve as r,o as rt,$ as s,ae as st,Z as t,ee as tt,pe as u,I as v,J as w,E as x,G as y,X as z};
3
- //# sourceMappingURL=display-exQDWum3.js.map