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.
- package/docs/openapi.json +1 -1
- package/package.json +5 -4
- package/public/assets/{MessageBubble-CLqxCG9b.js → MessageBubble-DRdUD-kW.js} +2 -2
- package/public/assets/{MessageBubble-CLqxCG9b.js.map → MessageBubble-DRdUD-kW.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-BkkToFEb.js → PlanGraphPanel-siKMPCln.js} +2 -2
- package/public/assets/{PlanGraphPanel-BkkToFEb.js.map → PlanGraphPanel-siKMPCln.js.map} +1 -1
- package/public/assets/{activity-D-ocGQGs.js → activity-D3MNcSaV.js} +2 -2
- package/public/assets/{activity-D-ocGQGs.js.map → activity-D3MNcSaV.js.map} +1 -1
- package/public/assets/{agent-profiles-CRPgYTyb.js → agent-profiles-CKzttq6G.js} +2 -2
- package/public/assets/{agent-profiles-CRPgYTyb.js.map → agent-profiles-CKzttq6G.js.map} +1 -1
- package/public/assets/{agent-type-icon-CeEoMouY.js → agent-type-icon-0ZJRP1Vy.js} +2 -2
- package/public/assets/{agent-type-icon-CeEoMouY.js.map → agent-type-icon-0ZJRP1Vy.js.map} +1 -1
- package/public/assets/{agents-Chw5E_2q.js → agents-CuVj_rfG.js} +2 -2
- package/public/assets/{agents-Chw5E_2q.js.map → agents-CuVj_rfG.js.map} +1 -1
- package/public/assets/{analytics-BmNDG0hR.js → analytics-iob6C27d.js} +2 -2
- package/public/assets/{analytics-BmNDG0hR.js.map → analytics-iob6C27d.js.map} +1 -1
- package/public/assets/{automation-D7g90kTv.js → automation-B2ixxs4q.js} +2 -2
- package/public/assets/{automation-D7g90kTv.js.map → automation-B2ixxs4q.js.map} +1 -1
- package/public/assets/{branch-state-badge-Bv7DhLBZ.js → branch-state-badge-CafBJWKo.js} +2 -2
- package/public/assets/{branch-state-badge-Bv7DhLBZ.js.map → branch-state-badge-CafBJWKo.js.map} +1 -1
- package/public/assets/{channels-CLwPeEPi.js → channels-C-WsbL2H.js} +2 -2
- package/public/assets/{channels-CLwPeEPi.js.map → channels-C-WsbL2H.js.map} +1 -1
- package/public/assets/{chat-Cgj7VKzc.js → chat-DeoVTVob.js} +2 -2
- package/public/assets/{chat-Cgj7VKzc.js.map → chat-DeoVTVob.js.map} +1 -1
- package/public/assets/{connectors-Br6fiTny.js → connectors-C2Ac03LJ.js} +2 -2
- package/public/assets/{connectors-Br6fiTny.js.map → connectors-C2Ac03LJ.js.map} +1 -1
- package/public/assets/display-CJzSoTMq.js +3 -0
- package/public/assets/display-CJzSoTMq.js.map +1 -0
- package/public/assets/{formatted-body-CmkuD23M.js → formatted-body-35XBOY5L.js} +3 -3
- package/public/assets/{formatted-body-CmkuD23M.js.map → formatted-body-35XBOY5L.js.map} +1 -1
- package/public/assets/{formatted-body-impl-DbdFmbwW.js → formatted-body-impl-RmDxI5Ux.js} +2 -2
- package/public/assets/{formatted-body-impl-DbdFmbwW.js.map → formatted-body-impl-RmDxI5Ux.js.map} +1 -1
- package/public/assets/index-D0xMQQb_.js +25 -0
- package/public/assets/{index-D9OogaB5.js.map → index-D0xMQQb_.js.map} +1 -1
- package/public/assets/{insights-qvaPqWCV.js → insights-B643SFvm.js} +2 -2
- package/public/assets/{insights-qvaPqWCV.js.map → insights-B643SFvm.js.map} +1 -1
- package/public/assets/{integrations-DYEGSqq_.js → integrations-hFIHeCF4.js} +2 -2
- package/public/assets/{integrations-DYEGSqq_.js.map → integrations-hFIHeCF4.js.map} +1 -1
- package/public/assets/{maintenance-C0HIEB-J.js → maintenance-CURvdp_Z.js} +2 -2
- package/public/assets/{maintenance-C0HIEB-J.js.map → maintenance-CURvdp_Z.js.map} +1 -1
- package/public/assets/{managed-agents-DdS7aI38.js → managed-agents-DrZtdlSn.js} +2 -2
- package/public/assets/{managed-agents-DdS7aI38.js.map → managed-agents-DrZtdlSn.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-BnXMH1z1.js → markdown-preview-impl-CVDO--ek.js} +2 -2
- package/public/assets/{markdown-preview-impl-BnXMH1z1.js.map → markdown-preview-impl-CVDO--ek.js.map} +1 -1
- package/public/assets/{memory-CSwx7bz8.js → memory-Bm7dt_Qf.js} +2 -2
- package/public/assets/{memory-CSwx7bz8.js.map → memory-Bm7dt_Qf.js.map} +1 -1
- package/public/assets/{messages-Be9CmvDv.js → messages-DTq4i2KZ.js} +2 -2
- package/public/assets/{messages-Be9CmvDv.js.map → messages-DTq4i2KZ.js.map} +1 -1
- package/public/assets/{orchestrators-BgtvvHvo.js → orchestrators-Dra318Ap.js} +2 -2
- package/public/assets/{orchestrators-BgtvvHvo.js.map → orchestrators-Dra318Ap.js.map} +1 -1
- package/public/assets/{overview-BvhbPWtF.js → overview-yr1Vmk64.js} +2 -2
- package/public/assets/{overview-BvhbPWtF.js.map → overview-yr1Vmk64.js.map} +1 -1
- package/public/assets/{pairs-CpXSOMx0.js → pairs-mnlKDBGp.js} +2 -2
- package/public/assets/{pairs-CpXSOMx0.js.map → pairs-mnlKDBGp.js.map} +1 -1
- package/public/assets/projects-Dzdf-gkK.js +2 -0
- package/public/assets/{projects-DdPNPFJI.js.map → projects-Dzdf-gkK.js.map} +1 -1
- package/public/assets/prompt-settings-ByL8SNCi.js +2 -0
- package/public/assets/{prompt-settings-BpZse7-X.js.map → prompt-settings-ByL8SNCi.js.map} +1 -1
- package/public/assets/{registry-pb1gTZEg.js → registry-CXWyOtpr.js} +2 -2
- package/public/assets/{registry-pb1gTZEg.js.map → registry-CXWyOtpr.js.map} +1 -1
- package/public/assets/{security-BgcX1n9D.js → security-0SBsc_4W.js} +2 -2
- package/public/assets/{security-BgcX1n9D.js.map → security-0SBsc_4W.js.map} +1 -1
- package/public/assets/{select-DZPVpKPv.js → select-Bw5z2KJ_.js} +2 -2
- package/public/assets/{select-DZPVpKPv.js.map → select-Bw5z2KJ_.js.map} +1 -1
- package/public/assets/{settings-hd5kzdRB.js → settings-78Cyq_tA.js} +4 -4
- package/public/assets/{settings-hd5kzdRB.js.map → settings-78Cyq_tA.js.map} +1 -1
- package/public/assets/{store-DKTLubBT.js → store-BPrVuKiv.js} +4 -4
- package/public/assets/store-BPrVuKiv.js.map +1 -0
- package/public/assets/{tasks-C5d2LU5E.js → tasks-Cg2MMeri.js} +2 -2
- package/public/assets/{tasks-C5d2LU5E.js.map → tasks-Cg2MMeri.js.map} +1 -1
- package/public/assets/teams-DWv6UwFV.js +2 -0
- package/public/assets/{teams-B8f2pGJe.js.map → teams-DWv6UwFV.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-CxAscH0U.js → terminal-viewer-impl-DBLDjbIg.js} +2 -2
- package/public/assets/{terminal-viewer-impl-CxAscH0U.js.map → terminal-viewer-impl-DBLDjbIg.js.map} +1 -1
- package/public/assets/{types-uVOXgvMT.js → types-BeJUSfDj.js} +2 -2
- package/public/assets/types-BeJUSfDj.js.map +1 -0
- package/public/assets/{user-avatar-Dw6mzWhv.js → user-avatar-BsOLyVJj.js} +2 -2
- package/public/assets/{user-avatar-Dw6mzWhv.js.map → user-avatar-BsOLyVJj.js.map} +1 -1
- package/public/assets/{work-queue-9imc7aNK.js → work-queue-DmwvnF6F.js} +2 -2
- package/public/assets/{work-queue-9imc7aNK.js.map → work-queue-DmwvnF6F.js.map} +1 -1
- package/public/assets/{workspaces-CLYc3yF3.js → workspaces-dIBivJN8.js} +2 -2
- package/public/assets/{workspaces-CLYc3yF3.js.map → workspaces-dIBivJN8.js.map} +1 -1
- package/public/index.html +6 -6
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +44 -938
- package/runner/src/adapter.ts +53 -18
- package/runner/src/config.ts +1 -4
- package/src/agent-card-projection.ts +5 -1
- package/src/agent-command-activity.ts +39 -0
- package/src/automations/reconcile.ts +2 -2
- package/src/bus.ts +6 -4
- package/src/cli/workspace.ts +3 -1
- package/src/config.ts +10 -1
- package/src/db/agents.ts +138 -32
- package/src/db/message-reads.ts +50 -0
- package/src/db/messages.ts +48 -9
- package/src/db/migrations.ts +54 -0
- package/src/db/provider-quota-burn.ts +96 -67
- package/src/db/provider-quotas.ts +29 -7
- package/src/db/scheduled-timers.ts +21 -0
- package/src/db/schema.ts +1 -0
- package/src/db/tasks.ts +33 -3
- package/src/db/work-evidence.ts +10 -7
- package/src/event-loop-lag.ts +15 -0
- package/src/gate-resolver.ts +24 -1
- package/src/maintenance/jobs.ts +23 -6
- package/src/managed-policy.ts +35 -5
- package/src/mcp-plan-tools.ts +13 -1
- package/src/project-instructions.ts +36 -2
- package/src/quota-advisory.ts +6 -4
- package/src/quota-band-transition.ts +23 -14
- package/src/routes/agent-sessions.ts +39 -7
- package/src/routes/agents.ts +3 -2
- package/src/routes/tasks.ts +16 -4
- package/src/routes/workspaces.ts +35 -10
- package/src/services/plan-graph.ts +27 -9
- package/src/services/register-agent.ts +12 -1
- package/src/services/send-message.ts +43 -15
- package/src/services/spawn-agent.ts +3 -0
- package/src/spawn-command.ts +8 -0
- package/src/spawn-targets.ts +19 -34
- package/src/sse.ts +25 -3
- package/src/workspace-merge.ts +25 -5
- package/src/workspace-orphans.ts +13 -6
- package/public/assets/display-exQDWum3.js +0 -3
- package/public/assets/display-exQDWum3.js.map +0 -1
- package/public/assets/index-D9OogaB5.js +0 -25
- package/public/assets/projects-DdPNPFJI.js +0 -2
- package/public/assets/prompt-settings-BpZse7-X.js +0 -2
- package/public/assets/store-DKTLubBT.js.map +0 -1
- package/public/assets/teams-B8f2pGJe.js +0 -2
- package/public/assets/types-uVOXgvMT.js.map +0 -1
package/src/routes/workspaces.ts
CHANGED
|
@@ -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 (
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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
|
|
300
|
-
objective
|
|
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
|
-
|
|
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
|
|
300
|
-
|
|
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(/[
|
|
305
|
-
return
|
|
306
|
-
|
|
307
|
-
|
|
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,
|
package/src/spawn-command.ts
CHANGED
|
@@ -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"),
|
package/src/spawn-targets.ts
CHANGED
|
@@ -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
|
|
282
|
-
//
|
|
283
|
-
//
|
|
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
|
-
// #
|
|
288
|
-
//
|
|
289
|
-
//
|
|
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
|
-
|
|
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
|
-
// #
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const rawPenalty = Math.max(pacePenalty, positionPenalty);
|
|
291
|
+
// #1072 — ONE 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
|
-
|
|
485
|
-
return session?.final === true;
|
|
507
|
+
return isWorkFinalMessagePayload(msg.payload);
|
|
486
508
|
}
|
|
487
509
|
|
|
488
510
|
function notificationForMessage(msg: Message): RelayNotification | undefined {
|
package/src/workspace-merge.ts
CHANGED
|
@@ -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.
|
|
153
|
-
//
|
|
154
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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> {
|
package/src/workspace-orphans.ts
CHANGED
|
@@ -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
|
-
|
|
610
|
-
|
|
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
|
|
615
|
-
// reconnecting agent isn't raced; the in-memory tracker re-arms on
|
|
616
|
-
// grace of 0 reaps on the first
|
|
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
|