agent-relay-server 0.106.0 → 0.108.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 +503 -1
- package/package.json +6 -5
- package/public/assets/{MessageBubble-CPYQQLDI.js → MessageBubble-DYJezSjU.js} +2 -2
- package/public/assets/{MessageBubble-CPYQQLDI.js.map → MessageBubble-DYJezSjU.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-B7xZt5DI.js → PlanGraphPanel-MWcVw38W.js} +2 -2
- package/public/assets/{PlanGraphPanel-B7xZt5DI.js.map → PlanGraphPanel-MWcVw38W.js.map} +1 -1
- package/public/assets/{activity-UKQrQ8_0.js → activity-Db3X3dg8.js} +2 -2
- package/public/assets/{activity-UKQrQ8_0.js.map → activity-Db3X3dg8.js.map} +1 -1
- package/public/assets/{agent-profiles-Zr_5nNEJ.js → agent-profiles-Di2w5U1o.js} +2 -2
- package/public/assets/{agent-profiles-Zr_5nNEJ.js.map → agent-profiles-Di2w5U1o.js.map} +1 -1
- package/public/assets/{agents-FdrfmJsj.js → agents-GSJXvmU1.js} +2 -2
- package/public/assets/{agents-FdrfmJsj.js.map → agents-GSJXvmU1.js.map} +1 -1
- package/public/assets/{analytics-VximqXQ9.js → analytics-B9USQLl5.js} +18 -18
- package/public/assets/{analytics-VximqXQ9.js.map → analytics-B9USQLl5.js.map} +1 -1
- package/public/assets/{automation-DlEcJvgu.js → automation-XRePs1dT.js} +2 -2
- package/public/assets/{automation-DlEcJvgu.js.map → automation-XRePs1dT.js.map} +1 -1
- package/public/assets/{branch-state-badge-CcLZ_u86.js → branch-state-badge-C_VNyER3.js} +2 -2
- package/public/assets/{branch-state-badge-CcLZ_u86.js.map → branch-state-badge-C_VNyER3.js.map} +1 -1
- package/public/assets/{channels-CcSnLzCG.js → channels-DACvNoC2.js} +2 -2
- package/public/assets/{channels-CcSnLzCG.js.map → channels-DACvNoC2.js.map} +1 -1
- package/public/assets/{chat-D1bunuI-.js → chat-D_0uHgB6.js} +2 -2
- package/public/assets/{chat-D1bunuI-.js.map → chat-D_0uHgB6.js.map} +1 -1
- package/public/assets/{connectors-DbPOlQhL.js → connectors-C0B38LUK.js} +2 -2
- package/public/assets/{connectors-DbPOlQhL.js.map → connectors-C0B38LUK.js.map} +1 -1
- package/public/assets/display-1J__Ti1K.js +3 -0
- package/public/assets/display-1J__Ti1K.js.map +1 -0
- package/public/assets/{formatted-body-impl-4v9SkUD1.js → formatted-body-impl-BKWCAjBd.js} +2 -2
- package/public/assets/{formatted-body-impl-4v9SkUD1.js.map → formatted-body-impl-BKWCAjBd.js.map} +1 -1
- package/public/assets/index-DlBSuzd2.js +23 -0
- package/public/assets/index-DlBSuzd2.js.map +1 -0
- package/public/assets/{insights-CntgHcTp.js → insights-BzNoYvDe.js} +2 -2
- package/public/assets/{insights-CntgHcTp.js.map → insights-BzNoYvDe.js.map} +1 -1
- package/public/assets/{integrations-hkSW8CzM.js → integrations-DLzJpt9G.js} +2 -2
- package/public/assets/{integrations-hkSW8CzM.js.map → integrations-DLzJpt9G.js.map} +1 -1
- package/public/assets/{maintenance-Dw7frQtu.js → maintenance-B6_0-ECm.js} +2 -2
- package/public/assets/{maintenance-Dw7frQtu.js.map → maintenance-B6_0-ECm.js.map} +1 -1
- package/public/assets/{managed-agents-Bj--bFmZ.js → managed-agents-CpzKXDtV.js} +2 -2
- package/public/assets/{managed-agents-Bj--bFmZ.js.map → managed-agents-CpzKXDtV.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-DGLCocU8.js → markdown-preview-impl-DxBnlJ2k.js} +2 -2
- package/public/assets/{markdown-preview-impl-DGLCocU8.js.map → markdown-preview-impl-DxBnlJ2k.js.map} +1 -1
- package/public/assets/{memory-yj2MIxUA.js → memory-Bnm7ILWL.js} +2 -2
- package/public/assets/{memory-yj2MIxUA.js.map → memory-Bnm7ILWL.js.map} +1 -1
- package/public/assets/{messages-BT5mL7iz.js → messages-D9LkocTc.js} +2 -2
- package/public/assets/{messages-BT5mL7iz.js.map → messages-D9LkocTc.js.map} +1 -1
- package/public/assets/{orchestrators-BVDAhrPk.js → orchestrators-C__JfrNr.js} +2 -2
- package/public/assets/{orchestrators-BVDAhrPk.js.map → orchestrators-C__JfrNr.js.map} +1 -1
- package/public/assets/{overview-DlqJIqYK.js → overview-BQ0b0V7Q.js} +2 -2
- package/public/assets/{overview-DlqJIqYK.js.map → overview-BQ0b0V7Q.js.map} +1 -1
- package/public/assets/{pairs-DQ4tNCo3.js → pairs-ChyeBs0Z.js} +2 -2
- package/public/assets/{pairs-DQ4tNCo3.js.map → pairs-ChyeBs0Z.js.map} +1 -1
- package/public/assets/{projects-vt63bwHN.js → projects-v5Kp1Oaq.js} +2 -2
- package/public/assets/{projects-vt63bwHN.js.map → projects-v5Kp1Oaq.js.map} +1 -1
- package/public/assets/{prompt-settings-DlsqOP0C.js → prompt-settings-YhAOlYc6.js} +2 -2
- package/public/assets/{prompt-settings-DlsqOP0C.js.map → prompt-settings-YhAOlYc6.js.map} +1 -1
- package/public/assets/{registry-Cw2fdWrn.js → registry-DZoAfrzx.js} +2 -2
- package/public/assets/{registry-Cw2fdWrn.js.map → registry-DZoAfrzx.js.map} +1 -1
- package/public/assets/{security-ucr7IT1V.js → security-DRCj00eA.js} +2 -2
- package/public/assets/{security-ucr7IT1V.js.map → security-DRCj00eA.js.map} +1 -1
- package/public/assets/{select-D8UWCtYZ.js → select-ZOpLw2Fd.js} +2 -2
- package/public/assets/{select-D8UWCtYZ.js.map → select-ZOpLw2Fd.js.map} +1 -1
- package/public/assets/settings-DGgwr9WH.js +6 -0
- package/public/assets/settings-DGgwr9WH.js.map +1 -0
- package/public/assets/{store-DUfrGvOE.js → store-Bnag1kY-.js} +2 -2
- package/public/assets/{store-DUfrGvOE.js.map → store-Bnag1kY-.js.map} +1 -1
- package/public/assets/{tasks-jRUvxUd5.js → tasks-DUTu8ZhA.js} +2 -2
- package/public/assets/{tasks-jRUvxUd5.js.map → tasks-DUTu8ZhA.js.map} +1 -1
- package/public/assets/{teams-Dn0yggD-.js → teams-BJpaoG2m.js} +2 -2
- package/public/assets/{teams-Dn0yggD-.js.map → teams-BJpaoG2m.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-fLly20IP.js → terminal-viewer-impl-CQhh6oXa.js} +2 -2
- package/public/assets/{terminal-viewer-impl-fLly20IP.js.map → terminal-viewer-impl-CQhh6oXa.js.map} +1 -1
- package/public/assets/types-BiJFuTiL.js +2 -0
- package/public/assets/types-BiJFuTiL.js.map +1 -0
- package/public/assets/{user-avatar-BWLUHt2e.js → user-avatar-BYGrUx1U.js} +2 -2
- package/public/assets/{user-avatar-BWLUHt2e.js.map → user-avatar-BYGrUx1U.js.map} +1 -1
- package/public/assets/{work-queue-BAnNY0xE.js → work-queue-BH2-mARc.js} +2 -2
- package/public/assets/{work-queue-BAnNY0xE.js.map → work-queue-BH2-mARc.js.map} +1 -1
- package/public/assets/{workspaces-CIsQqQby.js → workspaces-DldYyXZR.js} +2 -2
- package/public/assets/{workspaces-CIsQqQby.js.map → workspaces-DldYyXZR.js.map} +1 -1
- package/public/index.html +4 -4
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +6 -0
- package/src/branch-landed.ts +93 -4
- package/src/config-store.ts +12 -1
- package/src/db/agents.ts +3 -2
- package/src/db/continuations.ts +13 -3
- package/src/db/index.ts +3 -0
- package/src/db/issue-lifecycle.ts +12 -0
- package/src/db/mappers.ts +17 -0
- package/src/db/messages.ts +9 -4
- package/src/db/migrations.ts +91 -1
- package/src/db/notification-subscriptions.ts +1 -1
- package/src/db/prompt-templates.ts +19 -0
- package/src/db/provider-quotas.ts +6 -0
- package/src/db/schema.ts +67 -1
- package/src/db/stats.ts +3 -2
- package/src/db/task-comment-through.ts +112 -0
- package/src/db/task-core.ts +14 -1
- package/src/db/task-entity.ts +629 -0
- package/src/db/task-maintenance.ts +4 -3
- package/src/db/task-memory.ts +180 -0
- package/src/db/tasks.ts +24 -10
- package/src/issue-tracker/adapter.ts +15 -0
- package/src/issue-tracker/github.ts +40 -2
- package/src/issue-tracker/index.ts +1 -0
- package/src/mcp/index.ts +12 -1
- package/src/mcp/tools-spawn.ts +4 -0
- package/src/mcp-dispatch-tools.ts +164 -0
- package/src/mcp-notification-subscription-tools.ts +62 -4
- package/src/mcp-plan-tools.ts +30 -3
- package/src/mcp-release-tools.ts +33 -0
- package/src/mcp-task-memory-tools.ts +132 -0
- package/src/mcp-task-tools.ts +232 -0
- package/src/mcp-task-weave-tools.ts +66 -0
- package/src/notification-router.ts +24 -2
- package/src/notification-rules.ts +4 -0
- package/src/notification-types.ts +25 -0
- package/src/prompt-resolver.ts +66 -3
- package/src/routes/agents-spawn.ts +10 -0
- package/src/routes/commands.ts +17 -1
- package/src/routes/index.ts +16 -2
- package/src/routes/plans.ts +10 -2
- package/src/routes/provider-quotas.ts +13 -1
- package/src/routes/tasks.ts +276 -2
- package/src/routing-policy-store.ts +147 -2
- package/src/security.ts +2 -0
- package/src/self-resume.ts +13 -2
- package/src/services/issue-lifecycle.ts +9 -1
- package/src/services/plan-graph.ts +107 -9
- package/src/services/release-readiness.ts +142 -0
- package/src/services/send-message.ts +31 -0
- package/src/services/spawn-agent.ts +81 -4
- package/src/services/task-context-hydration.ts +361 -0
- package/src/services/task-entity.ts +149 -0
- package/src/services/task-issue-weave.ts +558 -0
- package/src/services/task-lifecycle.ts +465 -0
- package/src/services/task-plan-view.ts +178 -0
- package/src/services/task-semantic-events.ts +137 -0
- package/src/services/transient-agent-reaper.ts +71 -23
- package/src/spawn-targets.ts +27 -2
- package/src/task-dispatcher.ts +461 -0
- package/src/upgrade-install-verify.ts +20 -1
- package/public/assets/display-BWi8DQCp.js +0 -3
- package/public/assets/display-BWi8DQCp.js.map +0 -1
- package/public/assets/index-Vmhw442d.js +0 -23
- package/public/assets/index-Vmhw442d.js.map +0 -1
- package/public/assets/settings-Dvf_ps1B.js +0 -6
- package/public/assets/settings-Dvf_ps1B.js.map +0 -1
- package/public/assets/types-DorDDvEb.js +0 -2
- package/public/assets/types-DorDDvEb.js.map +0 -1
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { getAgent } from "../db";
|
|
2
|
+
import { emitRelayEvent } from "../events";
|
|
3
|
+
import type { NotificationScope, NotificationType } from "../notification-types";
|
|
4
|
+
import { routeNotification } from "../notification-router";
|
|
5
|
+
import { resolveAudience } from "../notification-rules";
|
|
6
|
+
import type { Task, TaskBlocker, TaskStatus } from "../types";
|
|
7
|
+
|
|
8
|
+
export type TaskSemanticTransition = "created" | "started" | "blocked" | "needs-decision" | "landed" | "closed";
|
|
9
|
+
|
|
10
|
+
const TASK_NOTIFICATION_TYPE: Record<TaskSemanticTransition, NotificationType> = {
|
|
11
|
+
created: "task.created",
|
|
12
|
+
started: "task.started",
|
|
13
|
+
blocked: "task.blocked",
|
|
14
|
+
"needs-decision": "task.needs_decision",
|
|
15
|
+
landed: "task.landed",
|
|
16
|
+
closed: "task.closed",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function taskTransitionForStatus(status: TaskStatus, blockers: TaskBlocker[] | undefined): TaskSemanticTransition | null {
|
|
20
|
+
if (status === "claimed" || status === "in_progress") return "started";
|
|
21
|
+
if (status === "blocked") {
|
|
22
|
+
return blockers?.some((blocker) => blocker.type === "decision") ? "needs-decision" : "blocked";
|
|
23
|
+
}
|
|
24
|
+
if (status === "done" || status === "failed" || status === "canceled") return "closed";
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TaskSemanticNotificationInput {
|
|
29
|
+
task: Task;
|
|
30
|
+
transition: TaskSemanticTransition;
|
|
31
|
+
actor?: string;
|
|
32
|
+
recipients?: string[];
|
|
33
|
+
workspaceId?: string;
|
|
34
|
+
branch?: string;
|
|
35
|
+
sha?: string;
|
|
36
|
+
landKey?: string;
|
|
37
|
+
occurrenceKey?: string;
|
|
38
|
+
outcome?: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function emitTaskSemanticNotification(input: TaskSemanticNotificationInput): void {
|
|
42
|
+
const type = TASK_NOTIFICATION_TYPE[input.transition];
|
|
43
|
+
const scope = taskNotificationScope(input);
|
|
44
|
+
const semanticEvent = `task.${input.transition}`;
|
|
45
|
+
const eventData = {
|
|
46
|
+
taskId: input.task.id,
|
|
47
|
+
transition: input.transition,
|
|
48
|
+
semanticEvent,
|
|
49
|
+
title: input.task.title,
|
|
50
|
+
status: input.task.status,
|
|
51
|
+
deliverableStage: input.task.deliverableStage,
|
|
52
|
+
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
|
53
|
+
...(input.branch ? { branch: input.branch } : {}),
|
|
54
|
+
...(input.sha ? { sha: input.sha } : {}),
|
|
55
|
+
...(input.landKey ? { landKey: input.landKey } : {}),
|
|
56
|
+
...(input.outcome ? { outcome: input.outcome } : {}),
|
|
57
|
+
};
|
|
58
|
+
emitRelayEvent({
|
|
59
|
+
type,
|
|
60
|
+
source: input.actor ?? "server",
|
|
61
|
+
subject: String(input.task.id),
|
|
62
|
+
data: eventData as Record<string, unknown>,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const subscriptionAudience = resolveAudience(type, scope).map((member) => ({
|
|
66
|
+
agentId: member.agentId,
|
|
67
|
+
matchedRule: member.matchedRule,
|
|
68
|
+
}));
|
|
69
|
+
const explicit = (input.recipients ?? [])
|
|
70
|
+
.map((agentId) => agentId.trim())
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.map((agentId) => ({ agentId, matchedRule: "task-stakeholder" }));
|
|
73
|
+
|
|
74
|
+
const recipients = [...subscriptionAudience, ...explicit];
|
|
75
|
+
if (recipients.length === 0) return;
|
|
76
|
+
|
|
77
|
+
routeNotification({
|
|
78
|
+
type,
|
|
79
|
+
scope,
|
|
80
|
+
recipients,
|
|
81
|
+
ttlMs: null,
|
|
82
|
+
message: {
|
|
83
|
+
subject: taskNotificationSubject(input),
|
|
84
|
+
body: taskNotificationBody(input),
|
|
85
|
+
payload: {
|
|
86
|
+
kind: "task.transition",
|
|
87
|
+
notification: "task-transition",
|
|
88
|
+
taskId: input.task.id,
|
|
89
|
+
transition: input.transition,
|
|
90
|
+
semanticEvent,
|
|
91
|
+
...(input.outcome ? { outcome: input.outcome } : {}),
|
|
92
|
+
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
|
93
|
+
...(input.branch ? { branch: input.branch } : {}),
|
|
94
|
+
...(input.sha ? { sha: input.sha } : {}),
|
|
95
|
+
},
|
|
96
|
+
idempotencyKey: taskNotificationIdempotencyKey(input),
|
|
97
|
+
replyExpected: false,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function taskNotificationIdempotencyKey(input: TaskSemanticNotificationInput): string | undefined {
|
|
103
|
+
if (input.landKey) return `task:${input.task.id}:transition:${input.transition}:${input.landKey}`;
|
|
104
|
+
if (input.occurrenceKey) return `task:${input.task.id}:transition:${input.transition}:occurrence:${input.occurrenceKey}`;
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function taskNotificationScope(input: TaskSemanticNotificationInput): NotificationScope {
|
|
109
|
+
const owner = input.task.ownerAgentId;
|
|
110
|
+
const ownerAgent = owner ? getAgent(owner) : undefined;
|
|
111
|
+
const teamId = input.task.teamId ?? ownerAgent?.teamId;
|
|
112
|
+
return {
|
|
113
|
+
taskId: String(input.task.id),
|
|
114
|
+
transition: input.transition,
|
|
115
|
+
...(input.landKey ? { landKey: input.landKey } : {}),
|
|
116
|
+
...(owner ? { author: owner, agentId: owner } : {}),
|
|
117
|
+
...(input.task.createdBy ? { parent: input.task.createdBy } : {}),
|
|
118
|
+
...(teamId ? { teamId } : {}),
|
|
119
|
+
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function taskNotificationSubject(input: TaskSemanticNotificationInput): string {
|
|
124
|
+
return `Task #${input.task.id} ${input.transition}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function taskNotificationBody(input: TaskSemanticNotificationInput): string {
|
|
128
|
+
if (input.transition === "landed") {
|
|
129
|
+
const bits = [`Task #${input.task.id} landed: ${input.task.title}`];
|
|
130
|
+
if (input.sha) bits.push(`sha ${input.sha.slice(0, 12)}`);
|
|
131
|
+
const gatesPassed = input.outcome?.gatesPassed;
|
|
132
|
+
if (typeof gatesPassed === "boolean") bits.push(gatesPassed ? "gates passed" : "gates not verified");
|
|
133
|
+
if (input.outcome?.statusAdvanced === true) bits.push("closed");
|
|
134
|
+
return bits.join("; ");
|
|
135
|
+
}
|
|
136
|
+
return `Task #${input.task.id} ${input.transition}: ${input.task.title}`;
|
|
137
|
+
}
|
|
@@ -4,7 +4,52 @@ import { createActivityEvent, getAgent, listAgents, listWorkspaces, recordTermin
|
|
|
4
4
|
import { routeNotification } from "../notification-router";
|
|
5
5
|
import { isProviderBlocked } from "../provider-state";
|
|
6
6
|
import { TERMINAL_WORKSPACE_STATUSES } from "../workspace-phase";
|
|
7
|
-
import type { WorkspaceRecord } from "../types";
|
|
7
|
+
import type { AgentCard, Command, WorkspaceRecord } from "../types";
|
|
8
|
+
|
|
9
|
+
// The single home for "dispose a transient/one-shot agent" — the canonical `agent.shutdown`
|
|
10
|
+
// command + its events/terminal-marker/activity. Extracted so the idle-grace scan below AND
|
|
11
|
+
// the #831 on-land reap signal emit the byte-identical disposal (one shutdown shape, not two).
|
|
12
|
+
// Pure emission: the CALLER owns the decision (idle+grace vs landed+gates) and any dedup.
|
|
13
|
+
export interface TransientShutdownOptions {
|
|
14
|
+
/** Disposal cause stamped on the command, agent.exited event, and terminal marker. */
|
|
15
|
+
reason: string;
|
|
16
|
+
/** Audit attribution (the command's requestedBy + the activity row's maintenanceJobId). */
|
|
17
|
+
requestedBy: string;
|
|
18
|
+
workspaceId?: string;
|
|
19
|
+
workspaceStatus?: string;
|
|
20
|
+
activityTitle: string;
|
|
21
|
+
activityBody: string;
|
|
22
|
+
/** Why the land was considered complete (idle-scan reason or "deliverable-landed"). */
|
|
23
|
+
landReason?: string;
|
|
24
|
+
now?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function requestTransientAgentShutdown(agent: AgentCard, opts: TransientShutdownOptions): Command {
|
|
28
|
+
const now = opts.now ?? Date.now();
|
|
29
|
+
const command = createCommand({
|
|
30
|
+
type: "agent.shutdown",
|
|
31
|
+
source: "system",
|
|
32
|
+
target: agent.id,
|
|
33
|
+
params: { action: "shutdown", agentId: agent.id, graceful: true, timeoutMs: 10_000, reason: opts.reason, requestedBy: opts.requestedBy, requestedAt: now },
|
|
34
|
+
});
|
|
35
|
+
emitRelayEvent({ type: "command.requested", source: command.source, subject: command.id, data: { command } });
|
|
36
|
+
emitRelayEvent({ type: "agent.exited", source: "server", subject: agent.id, data: { agentId: agent.id, parent: agent.spawnedBy, reason: opts.reason, workspaceId: opts.workspaceId, workspaceStatus: opts.workspaceStatus } });
|
|
37
|
+
// #645 — persist a clean terminal marker so the #634 heartbeat-gap watchdog reads this benign
|
|
38
|
+
// completion as a terminus (not a silent death) once the reaped worker stops heartbeating.
|
|
39
|
+
recordTerminalEvent({ agentId: agent.id, reason: opts.reason, kind: "agent.exited", epoch: agent.epoch, createdAt: now });
|
|
40
|
+
createActivityEvent({
|
|
41
|
+
clientId: `transient-agent-reaped-${agent.id}-${now}`,
|
|
42
|
+
kind: "state",
|
|
43
|
+
title: opts.activityTitle,
|
|
44
|
+
body: opts.activityBody,
|
|
45
|
+
meta: agent.id,
|
|
46
|
+
icon: "ti-check",
|
|
47
|
+
view: "agents",
|
|
48
|
+
agentId: agent.id,
|
|
49
|
+
metadata: { source: "server", maintenanceJobId: opts.requestedBy, commandId: command.id, reason: opts.reason, workspaceId: opts.workspaceId, landReason: opts.landReason },
|
|
50
|
+
});
|
|
51
|
+
return command;
|
|
52
|
+
}
|
|
8
53
|
|
|
9
54
|
const envMsOrDefault = (name: string, fallback: number): number => {
|
|
10
55
|
const v = Number(process.env[name]);
|
|
@@ -37,6 +82,19 @@ function defaultCommitsAhead(ws: WorkspaceRecord): number | null {
|
|
|
37
82
|
}
|
|
38
83
|
}
|
|
39
84
|
|
|
85
|
+
// #800 — A transient agent is protected from reaping ONLY by one of three evidence-based states:
|
|
86
|
+
// it is not idle (still working), it is not ready (mid-startup/teardown), or it carries a REAL
|
|
87
|
+
// provider block. `isProviderBlocked` reads the agent's own `meta.providerState`, which is set
|
|
88
|
+
// exclusively from hard evidence — an HTTP 429 / StopFailure hook, the scraped CLI usage-limit
|
|
89
|
+
// modal, an approval gate, or a model-unavailable hold (see runner rate-limit.ts / control-server).
|
|
90
|
+
// It is NEVER derived from a quota-utilization number. The reaper deliberately does not read quota
|
|
91
|
+
// utilization at all, so a provider merely sitting at high 7d/5h utilization can neither cause nor
|
|
92
|
+
// prevent a reap. This keeps the false "high-util ⇒ blocked ⇒ reaped" chain (#800) impossible by
|
|
93
|
+
// construction: utilization is not an input to this decision.
|
|
94
|
+
function reapProtected(agent: ReturnType<typeof listAgents>[number]): boolean {
|
|
95
|
+
return agent.status !== "idle" || !agent.ready || isProviderBlocked(agent);
|
|
96
|
+
}
|
|
97
|
+
|
|
40
98
|
let _commitsAheadFn: (ws: WorkspaceRecord) => number | null = defaultCommitsAhead;
|
|
41
99
|
|
|
42
100
|
export function overrideCommitsAheadForTests(fn: (ws: WorkspaceRecord) => number | null): void {
|
|
@@ -109,7 +167,9 @@ export function reapTransientAgents(): Record<string, unknown> {
|
|
|
109
167
|
// A held agent (rate-limit hold #286, or any provider-blocked state) reads as
|
|
110
168
|
// idle+ready but is NOT done — reaping it would kill work that's just waiting
|
|
111
169
|
// for the limit to reset. Treat it as not-idle; the clock restarts on resume.
|
|
112
|
-
|
|
170
|
+
// #800 — `reapProtected` is the single home for that decision: it honors only
|
|
171
|
+
// evidence-based block states, never a high quota-utilization number.
|
|
172
|
+
if (reapProtected(agent)) { transientTracker.delete(agent.id); continue; }
|
|
113
173
|
const entry = transientTracker.get(agent.id) ?? { firstIdleAt: now };
|
|
114
174
|
transientTracker.set(agent.id, entry);
|
|
115
175
|
const idleForMs = now - entry.firstIdleAt;
|
|
@@ -127,27 +187,15 @@ export function reapTransientAgents(): Record<string, unknown> {
|
|
|
127
187
|
}
|
|
128
188
|
if (entry.lastReapAt) continue;
|
|
129
189
|
entry.lastReapAt = now;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// completion as a terminus (not a silent death) once the reaped worker stops heartbeating.
|
|
140
|
-
recordTerminalEvent({ agentId: agent.id, reason: "transient-complete", kind: "agent.exited", epoch: agent.epoch, createdAt: now });
|
|
141
|
-
createActivityEvent({
|
|
142
|
-
clientId: `transient-agent-reaped-${agent.id}-${now}`,
|
|
143
|
-
kind: "state",
|
|
144
|
-
title: "Transient agent complete",
|
|
145
|
-
body: "Agent idle grace elapsed and branch work is land-complete; shutdown requested",
|
|
146
|
-
meta: agent.id,
|
|
147
|
-
icon: "ti-check",
|
|
148
|
-
view: "agents",
|
|
149
|
-
agentId: agent.id,
|
|
150
|
-
metadata: { source: "server", maintenanceJobId: "transient-agent-reaper", commandId: command.id, reason: "transient-complete", workspaceId: land.workspaceId, landReason: land.reason },
|
|
190
|
+
requestTransientAgentShutdown(agent, {
|
|
191
|
+
reason: "transient-complete",
|
|
192
|
+
requestedBy: "transient-agent-reaper",
|
|
193
|
+
workspaceId: land.workspaceId,
|
|
194
|
+
workspaceStatus: land.status,
|
|
195
|
+
activityTitle: "Transient agent complete",
|
|
196
|
+
activityBody: "Agent idle grace elapsed and branch work is land-complete; shutdown requested",
|
|
197
|
+
landReason: land.reason,
|
|
198
|
+
now,
|
|
151
199
|
});
|
|
152
200
|
reaped.push(agent.id);
|
|
153
201
|
}
|
package/src/spawn-targets.ts
CHANGED
|
@@ -206,6 +206,29 @@ function quotaAdvisoryFor(
|
|
|
206
206
|
};
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
// #839 — compact per-provider quota band view for the task dispatcher. Projects the same
|
|
210
|
+
// advisory the spawn matrix surfaces (band / prefer / blockedNow) down to the fields the
|
|
211
|
+
// dispatcher needs to protect scarce pools and load-shift onto abundant ones. Returns one
|
|
212
|
+
// entry per provider that has quota records; providers without records are simply absent
|
|
213
|
+
// (the dispatcher then treats them as band-unknown = "normal").
|
|
214
|
+
export interface ProviderQuotaBand {
|
|
215
|
+
provider: SpawnProvider;
|
|
216
|
+
band: QuotaBand;
|
|
217
|
+
prefer: boolean;
|
|
218
|
+
blockedNow: boolean;
|
|
219
|
+
recommendedAction: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function providerQuotaBands(now = Date.now()): ProviderQuotaBand[] {
|
|
223
|
+
return [...providerQuotaAdvisories(now).entries()].map(([provider, advisory]) => ({
|
|
224
|
+
provider,
|
|
225
|
+
band: advisory.quotaPenaltyBand,
|
|
226
|
+
prefer: advisory.prefer,
|
|
227
|
+
blockedNow: advisory.blockedNow,
|
|
228
|
+
recommendedAction: advisory.recommendedAction,
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
|
|
209
232
|
function quotaWindowsFor(record: ProviderQuotaRecord, now: number, providerCfg?: ProviderQuotaRoutingConfig): SpawnQuotaBurnWindow[] {
|
|
210
233
|
const quotaByName = new Map((record.quota?.windows ?? []).map((window) => [window.name, window]));
|
|
211
234
|
return (record.burn?.windows ?? [])
|
|
@@ -381,9 +404,11 @@ export function quotaBandFromSignal(
|
|
|
381
404
|
function recommendedAction(input: { quotaPenaltyBand: SpawnQuotaBurnBand; blockedNow: boolean; prefer: boolean; slack: number | null; windows: SpawnQuotaBurnWindow[] }): string {
|
|
382
405
|
if (input.blockedNow) {
|
|
383
406
|
const hasSevenDayBlock = input.windows.some((window) => window.role === "7d" && window.blockedNow);
|
|
407
|
+
// #827 — "at capacity" (util ≥ hardBackstop) is a routing-avoid advisory, not an evidence-based
|
|
408
|
+
// rate-limit hold. Use distinct wording so operators don't confuse it with an HTTP 429 block.
|
|
384
409
|
return hasSevenDayBlock
|
|
385
|
-
? "Quota
|
|
386
|
-
: "5h quota
|
|
410
|
+
? "Quota at capacity (routing-avoid); switch provider or wait for reset unless this provider was explicitly requested."
|
|
411
|
+
: "5h quota at capacity (routing-avoid); wait for reset or use another provider unless the job is urgent.";
|
|
387
412
|
}
|
|
388
413
|
return windowRecommendedAction(input.quotaPenaltyBand, input.slack);
|
|
389
414
|
}
|