agent-relay-server 0.107.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.
Files changed (143) hide show
  1. package/docs/openapi.json +475 -1
  2. package/package.json +4 -4
  3. package/public/assets/{MessageBubble-PEdvVh_m.js → MessageBubble-DYJezSjU.js} +2 -2
  4. package/public/assets/{MessageBubble-PEdvVh_m.js.map → MessageBubble-DYJezSjU.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-DSts7Rtq.js → PlanGraphPanel-MWcVw38W.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-DSts7Rtq.js.map → PlanGraphPanel-MWcVw38W.js.map} +1 -1
  7. package/public/assets/{activity-BcYf8YNO.js → activity-Db3X3dg8.js} +2 -2
  8. package/public/assets/{activity-BcYf8YNO.js.map → activity-Db3X3dg8.js.map} +1 -1
  9. package/public/assets/{agent-profiles-CuTQtmV6.js → agent-profiles-Di2w5U1o.js} +2 -2
  10. package/public/assets/{agent-profiles-CuTQtmV6.js.map → agent-profiles-Di2w5U1o.js.map} +1 -1
  11. package/public/assets/{agents-CgRFJyks.js → agents-GSJXvmU1.js} +2 -2
  12. package/public/assets/{agents-CgRFJyks.js.map → agents-GSJXvmU1.js.map} +1 -1
  13. package/public/assets/{analytics-C3OTFIXo.js → analytics-B9USQLl5.js} +2 -2
  14. package/public/assets/{analytics-C3OTFIXo.js.map → analytics-B9USQLl5.js.map} +1 -1
  15. package/public/assets/{automation-CH9YGYoE.js → automation-XRePs1dT.js} +2 -2
  16. package/public/assets/{automation-CH9YGYoE.js.map → automation-XRePs1dT.js.map} +1 -1
  17. package/public/assets/{branch-state-badge-CcLZ_u86.js → branch-state-badge-C_VNyER3.js} +2 -2
  18. package/public/assets/{branch-state-badge-CcLZ_u86.js.map → branch-state-badge-C_VNyER3.js.map} +1 -1
  19. package/public/assets/{channels-CcSnLzCG.js → channels-DACvNoC2.js} +2 -2
  20. package/public/assets/{channels-CcSnLzCG.js.map → channels-DACvNoC2.js.map} +1 -1
  21. package/public/assets/{chat-BLcm2Aht.js → chat-D_0uHgB6.js} +2 -2
  22. package/public/assets/{chat-BLcm2Aht.js.map → chat-D_0uHgB6.js.map} +1 -1
  23. package/public/assets/{connectors-DbPOlQhL.js → connectors-C0B38LUK.js} +2 -2
  24. package/public/assets/{connectors-DbPOlQhL.js.map → connectors-C0B38LUK.js.map} +1 -1
  25. package/public/assets/display-1J__Ti1K.js +3 -0
  26. package/public/assets/display-1J__Ti1K.js.map +1 -0
  27. package/public/assets/{formatted-body-impl-BJTthVHg.js → formatted-body-impl-BKWCAjBd.js} +2 -2
  28. package/public/assets/{formatted-body-impl-BJTthVHg.js.map → formatted-body-impl-BKWCAjBd.js.map} +1 -1
  29. package/public/assets/index-DlBSuzd2.js +23 -0
  30. package/public/assets/index-DlBSuzd2.js.map +1 -0
  31. package/public/assets/{insights-DklrIhb6.js → insights-BzNoYvDe.js} +2 -2
  32. package/public/assets/{insights-DklrIhb6.js.map → insights-BzNoYvDe.js.map} +1 -1
  33. package/public/assets/{integrations-hkSW8CzM.js → integrations-DLzJpt9G.js} +2 -2
  34. package/public/assets/{integrations-hkSW8CzM.js.map → integrations-DLzJpt9G.js.map} +1 -1
  35. package/public/assets/{maintenance-FAAMGpuZ.js → maintenance-B6_0-ECm.js} +2 -2
  36. package/public/assets/{maintenance-FAAMGpuZ.js.map → maintenance-B6_0-ECm.js.map} +1 -1
  37. package/public/assets/{managed-agents-CVca-s3t.js → managed-agents-CpzKXDtV.js} +2 -2
  38. package/public/assets/{managed-agents-CVca-s3t.js.map → managed-agents-CpzKXDtV.js.map} +1 -1
  39. package/public/assets/{markdown-preview-impl-BPW4OQo9.js → markdown-preview-impl-DxBnlJ2k.js} +2 -2
  40. package/public/assets/{markdown-preview-impl-BPW4OQo9.js.map → markdown-preview-impl-DxBnlJ2k.js.map} +1 -1
  41. package/public/assets/{memory-BzuDTHC5.js → memory-Bnm7ILWL.js} +2 -2
  42. package/public/assets/{memory-BzuDTHC5.js.map → memory-Bnm7ILWL.js.map} +1 -1
  43. package/public/assets/{messages-_W24PNvT.js → messages-D9LkocTc.js} +2 -2
  44. package/public/assets/{messages-_W24PNvT.js.map → messages-D9LkocTc.js.map} +1 -1
  45. package/public/assets/{orchestrators-Du6I_n8i.js → orchestrators-C__JfrNr.js} +2 -2
  46. package/public/assets/{orchestrators-Du6I_n8i.js.map → orchestrators-C__JfrNr.js.map} +1 -1
  47. package/public/assets/{overview-Dyvm1qpT.js → overview-BQ0b0V7Q.js} +2 -2
  48. package/public/assets/{overview-Dyvm1qpT.js.map → overview-BQ0b0V7Q.js.map} +1 -1
  49. package/public/assets/{pairs-B8x1RCNd.js → pairs-ChyeBs0Z.js} +2 -2
  50. package/public/assets/{pairs-B8x1RCNd.js.map → pairs-ChyeBs0Z.js.map} +1 -1
  51. package/public/assets/{projects-vt63bwHN.js → projects-v5Kp1Oaq.js} +2 -2
  52. package/public/assets/{projects-vt63bwHN.js.map → projects-v5Kp1Oaq.js.map} +1 -1
  53. package/public/assets/{prompt-settings-xVXmvxB5.js → prompt-settings-YhAOlYc6.js} +2 -2
  54. package/public/assets/{prompt-settings-xVXmvxB5.js.map → prompt-settings-YhAOlYc6.js.map} +1 -1
  55. package/public/assets/{registry-ypiICauZ.js → registry-DZoAfrzx.js} +2 -2
  56. package/public/assets/{registry-ypiICauZ.js.map → registry-DZoAfrzx.js.map} +1 -1
  57. package/public/assets/{security-BfOQ6T5r.js → security-DRCj00eA.js} +2 -2
  58. package/public/assets/{security-BfOQ6T5r.js.map → security-DRCj00eA.js.map} +1 -1
  59. package/public/assets/{select-Dx8Ro-A3.js → select-ZOpLw2Fd.js} +2 -2
  60. package/public/assets/{select-Dx8Ro-A3.js.map → select-ZOpLw2Fd.js.map} +1 -1
  61. package/public/assets/{settings-87clTx4N.js → settings-DGgwr9WH.js} +2 -2
  62. package/public/assets/{settings-87clTx4N.js.map → settings-DGgwr9WH.js.map} +1 -1
  63. package/public/assets/{store-DUfrGvOE.js → store-Bnag1kY-.js} +2 -2
  64. package/public/assets/{store-DUfrGvOE.js.map → store-Bnag1kY-.js.map} +1 -1
  65. package/public/assets/{tasks-1Kvp53wh.js → tasks-DUTu8ZhA.js} +2 -2
  66. package/public/assets/{tasks-1Kvp53wh.js.map → tasks-DUTu8ZhA.js.map} +1 -1
  67. package/public/assets/{teams-DJWp4AKi.js → teams-BJpaoG2m.js} +2 -2
  68. package/public/assets/{teams-DJWp4AKi.js.map → teams-BJpaoG2m.js.map} +1 -1
  69. package/public/assets/{terminal-viewer-impl-DgMuRAe_.js → terminal-viewer-impl-CQhh6oXa.js} +2 -2
  70. package/public/assets/{terminal-viewer-impl-DgMuRAe_.js.map → terminal-viewer-impl-CQhh6oXa.js.map} +1 -1
  71. package/public/assets/types-BiJFuTiL.js +2 -0
  72. package/public/assets/types-BiJFuTiL.js.map +1 -0
  73. package/public/assets/{user-avatar-CfAUS3PZ.js → user-avatar-BYGrUx1U.js} +2 -2
  74. package/public/assets/{user-avatar-CfAUS3PZ.js.map → user-avatar-BYGrUx1U.js.map} +1 -1
  75. package/public/assets/{work-queue-D-Otxd9u.js → work-queue-BH2-mARc.js} +2 -2
  76. package/public/assets/{work-queue-D-Otxd9u.js.map → work-queue-BH2-mARc.js.map} +1 -1
  77. package/public/assets/{workspaces-CIsQqQby.js → workspaces-DldYyXZR.js} +2 -2
  78. package/public/assets/{workspaces-CIsQqQby.js.map → workspaces-DldYyXZR.js.map} +1 -1
  79. package/public/index.html +4 -4
  80. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  81. package/runner/plugins/claude/monitors/relay-monitor.provisioned.mjs +6 -0
  82. package/src/branch-landed.ts +17 -3
  83. package/src/config-store.ts +12 -1
  84. package/src/db/agents.ts +3 -2
  85. package/src/db/continuations.ts +13 -3
  86. package/src/db/index.ts +3 -0
  87. package/src/db/issue-lifecycle.ts +12 -0
  88. package/src/db/mappers.ts +17 -0
  89. package/src/db/messages.ts +9 -4
  90. package/src/db/migrations.ts +91 -1
  91. package/src/db/notification-subscriptions.ts +1 -1
  92. package/src/db/prompt-templates.ts +19 -0
  93. package/src/db/schema.ts +67 -1
  94. package/src/db/stats.ts +3 -2
  95. package/src/db/task-comment-through.ts +112 -0
  96. package/src/db/task-core.ts +14 -1
  97. package/src/db/task-entity.ts +629 -0
  98. package/src/db/task-maintenance.ts +4 -3
  99. package/src/db/task-memory.ts +180 -0
  100. package/src/db/tasks.ts +24 -10
  101. package/src/issue-tracker/adapter.ts +15 -0
  102. package/src/issue-tracker/github.ts +40 -2
  103. package/src/issue-tracker/index.ts +1 -0
  104. package/src/mcp/index.ts +12 -1
  105. package/src/mcp/tools-spawn.ts +4 -0
  106. package/src/mcp-dispatch-tools.ts +164 -0
  107. package/src/mcp-notification-subscription-tools.ts +62 -4
  108. package/src/mcp-plan-tools.ts +30 -3
  109. package/src/mcp-release-tools.ts +33 -0
  110. package/src/mcp-task-memory-tools.ts +132 -0
  111. package/src/mcp-task-tools.ts +232 -0
  112. package/src/mcp-task-weave-tools.ts +66 -0
  113. package/src/notification-router.ts +24 -2
  114. package/src/notification-rules.ts +4 -0
  115. package/src/notification-types.ts +23 -0
  116. package/src/prompt-resolver.ts +66 -3
  117. package/src/routes/agents-spawn.ts +10 -0
  118. package/src/routes/index.ts +14 -1
  119. package/src/routes/plans.ts +10 -2
  120. package/src/routes/tasks.ts +276 -2
  121. package/src/routing-policy-store.ts +147 -2
  122. package/src/self-resume.ts +13 -2
  123. package/src/services/issue-lifecycle.ts +9 -1
  124. package/src/services/plan-graph.ts +107 -9
  125. package/src/services/release-readiness.ts +142 -0
  126. package/src/services/send-message.ts +31 -0
  127. package/src/services/spawn-agent.ts +81 -4
  128. package/src/services/task-context-hydration.ts +361 -0
  129. package/src/services/task-entity.ts +149 -0
  130. package/src/services/task-issue-weave.ts +558 -0
  131. package/src/services/task-lifecycle.ts +465 -0
  132. package/src/services/task-plan-view.ts +178 -0
  133. package/src/services/task-semantic-events.ts +137 -0
  134. package/src/services/transient-agent-reaper.ts +55 -22
  135. package/src/spawn-targets.ts +27 -2
  136. package/src/task-dispatcher.ts +461 -0
  137. package/src/upgrade-install-verify.ts +20 -1
  138. package/public/assets/display-BWi8DQCp.js +0 -3
  139. package/public/assets/display-BWi8DQCp.js.map +0 -1
  140. package/public/assets/index-3ydeNO0c.js +0 -23
  141. package/public/assets/index-3ydeNO0c.js.map +0 -1
  142. package/public/assets/types-DorDDvEb.js +0 -2
  143. 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]);
@@ -142,27 +187,15 @@ export function reapTransientAgents(): Record<string, unknown> {
142
187
  }
143
188
  if (entry.lastReapAt) continue;
144
189
  entry.lastReapAt = now;
145
- const command = createCommand({
146
- type: "agent.shutdown",
147
- source: "system",
148
- target: agent.id,
149
- params: { action: "shutdown", agentId: agent.id, graceful: true, timeoutMs: 10_000, reason: "transient-complete", requestedBy: "transient-agent-reaper", requestedAt: now },
150
- });
151
- emitRelayEvent({ type: "command.requested", source: command.source, subject: command.id, data: { command } });
152
- emitRelayEvent({ type: "agent.exited", source: "server", subject: agent.id, data: { agentId: agent.id, parent: agent.spawnedBy, reason: "transient-complete", workspaceId: land.workspaceId, workspaceStatus: land.status } });
153
- // #645 — persist a clean terminal marker so the #634 heartbeat-gap watchdog reads this benign
154
- // completion as a terminus (not a silent death) once the reaped worker stops heartbeating.
155
- recordTerminalEvent({ agentId: agent.id, reason: "transient-complete", kind: "agent.exited", epoch: agent.epoch, createdAt: now });
156
- createActivityEvent({
157
- clientId: `transient-agent-reaped-${agent.id}-${now}`,
158
- kind: "state",
159
- title: "Transient agent complete",
160
- body: "Agent idle grace elapsed and branch work is land-complete; shutdown requested",
161
- meta: agent.id,
162
- icon: "ti-check",
163
- view: "agents",
164
- agentId: agent.id,
165
- 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,
166
199
  });
167
200
  reaped.push(agent.id);
168
201
  }
@@ -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 is blocked now; switch provider or escalate unless this provider was explicitly requested."
386
- : "5h quota is blocked now; wait for reset or use another provider unless the job is urgent.";
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
  }