agent-relay-server 0.62.2 → 0.63.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 (161) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +2 -2
  3. package/public/assets/{MessageBubble-DRrxfjyT.js → MessageBubble-Cl6ufPHw.js} +2 -2
  4. package/public/assets/MessageBubble-Cl6ufPHw.js.map +1 -0
  5. package/public/assets/{PlanGraphPanel-Dp2eYazz.js → PlanGraphPanel-BnfjmkQ8.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-Dp2eYazz.js.map → PlanGraphPanel-BnfjmkQ8.js.map} +1 -1
  7. package/public/assets/{activity--t0U2yzq.js → activity-aFxvGLtd.js} +2 -2
  8. package/public/assets/{activity--t0U2yzq.js.map → activity-aFxvGLtd.js.map} +1 -1
  9. package/public/assets/{agent-profiles-BMtJYzxn.js → agent-profiles-B4mleUDz.js} +2 -2
  10. package/public/assets/{agent-profiles-BMtJYzxn.js.map → agent-profiles-B4mleUDz.js.map} +1 -1
  11. package/public/assets/{agents-Ck9AePlc.js → agents-DW5djToQ.js} +2 -2
  12. package/public/assets/{agents-Ck9AePlc.js.map → agents-DW5djToQ.js.map} +1 -1
  13. package/public/assets/{analytics-CzfK387L.js → analytics-CRi6thtg.js} +2 -2
  14. package/public/assets/{analytics-CzfK387L.js.map → analytics-CRi6thtg.js.map} +1 -1
  15. package/public/assets/{automation-Cgw8vtE8.js → automation-Do4OpauI.js} +2 -2
  16. package/public/assets/{automation-Cgw8vtE8.js.map → automation-Do4OpauI.js.map} +1 -1
  17. package/public/assets/{branch-state-badge-DnEEPtOP.js → branch-state-badge-D3W3w7zz.js} +2 -2
  18. package/public/assets/{branch-state-badge-DnEEPtOP.js.map → branch-state-badge-D3W3w7zz.js.map} +1 -1
  19. package/public/assets/{channels-Dc3L2MQs.js → channels-BytdR9NS.js} +2 -2
  20. package/public/assets/{channels-Dc3L2MQs.js.map → channels-BytdR9NS.js.map} +1 -1
  21. package/public/assets/{chat-Fc6iJ7sa.js → chat-CvaQ2czL.js} +2 -2
  22. package/public/assets/{chat-Fc6iJ7sa.js.map → chat-CvaQ2czL.js.map} +1 -1
  23. package/public/assets/{connectors-Dc2ttGWs.js → connectors-BLQDp8Bc.js} +2 -2
  24. package/public/assets/{connectors-Dc2ttGWs.js.map → connectors-BLQDp8Bc.js.map} +1 -1
  25. package/public/assets/{display-DzNvOu1j.js → display-B06I5sqa.js} +3 -3
  26. package/public/assets/display-B06I5sqa.js.map +1 -0
  27. package/public/assets/{formatted-body-impl-Bg-gx7rG.js → formatted-body-impl-0Lf8e1Ic.js} +2 -2
  28. package/public/assets/{formatted-body-impl-Bg-gx7rG.js.map → formatted-body-impl-0Lf8e1Ic.js.map} +1 -1
  29. package/public/assets/{index-BeDncjN4.js → index-C3su8ezq.js} +6 -6
  30. package/public/assets/{index-BeDncjN4.js.map → index-C3su8ezq.js.map} +1 -1
  31. package/public/assets/{insights-c08aW-7n.js → insights-64uExH1E.js} +2 -2
  32. package/public/assets/{insights-c08aW-7n.js.map → insights-64uExH1E.js.map} +1 -1
  33. package/public/assets/{integrations-BwpgcqnF.js → integrations-HyC1EaIp.js} +2 -2
  34. package/public/assets/{integrations-BwpgcqnF.js.map → integrations-HyC1EaIp.js.map} +1 -1
  35. package/public/assets/{maintenance-CTKP0kWB.js → maintenance-EVBpKH07.js} +2 -2
  36. package/public/assets/{maintenance-CTKP0kWB.js.map → maintenance-EVBpKH07.js.map} +1 -1
  37. package/public/assets/{managed-agents-BofCxo8d.js → managed-agents-BLZPxGuw.js} +2 -2
  38. package/public/assets/{managed-agents-BofCxo8d.js.map → managed-agents-BLZPxGuw.js.map} +1 -1
  39. package/public/assets/{markdown-preview-impl-CQ-6BIb8.js → markdown-preview-impl-Bg6rCWYN.js} +2 -2
  40. package/public/assets/{markdown-preview-impl-CQ-6BIb8.js.map → markdown-preview-impl-Bg6rCWYN.js.map} +1 -1
  41. package/public/assets/{memory-DvfXjMoi.js → memory-Bjr8bgjj.js} +2 -2
  42. package/public/assets/{memory-DvfXjMoi.js.map → memory-Bjr8bgjj.js.map} +1 -1
  43. package/public/assets/{messages-C9VsPzZ_.js → messages-CRYw1F4z.js} +2 -2
  44. package/public/assets/{messages-C9VsPzZ_.js.map → messages-CRYw1F4z.js.map} +1 -1
  45. package/public/assets/{orchestrators-MffVueeE.js → orchestrators-D-co1vdp.js} +2 -2
  46. package/public/assets/{orchestrators-MffVueeE.js.map → orchestrators-D-co1vdp.js.map} +1 -1
  47. package/public/assets/{overview-BSVajefM.js → overview-Ddy5czri.js} +2 -2
  48. package/public/assets/{overview-BSVajefM.js.map → overview-Ddy5czri.js.map} +1 -1
  49. package/public/assets/{pairs-B5711-Cs.js → pairs-CFVM8uHL.js} +2 -2
  50. package/public/assets/{pairs-B5711-Cs.js.map → pairs-CFVM8uHL.js.map} +1 -1
  51. package/public/assets/{security-DkUwK9Vj.js → security-Df4_76bh.js} +2 -2
  52. package/public/assets/{security-DkUwK9Vj.js.map → security-Df4_76bh.js.map} +1 -1
  53. package/public/assets/{select-DrM2RRiT.js → select-Cq3BBnL9.js} +2 -2
  54. package/public/assets/{select-DrM2RRiT.js.map → select-Cq3BBnL9.js.map} +1 -1
  55. package/public/assets/{settings-DdKCb-vy.js → settings-DOLEYXZL.js} +2 -2
  56. package/public/assets/{settings-DdKCb-vy.js.map → settings-DOLEYXZL.js.map} +1 -1
  57. package/public/assets/{store-iTiheuTD.js → store-C1E8jmNE.js} +4 -4
  58. package/public/assets/store-C1E8jmNE.js.map +1 -0
  59. package/public/assets/{tasks-e1y1PFfO.js → tasks-BcsmPi9_.js} +2 -2
  60. package/public/assets/{tasks-e1y1PFfO.js.map → tasks-BcsmPi9_.js.map} +1 -1
  61. package/public/assets/{teams-BUcxoGC_.js → teams-BQrV3eKI.js} +2 -2
  62. package/public/assets/{teams-BUcxoGC_.js.map → teams-BQrV3eKI.js.map} +1 -1
  63. package/public/assets/{terminal-viewer-impl-CmZjQOB2.js → terminal-viewer-impl-Dle04qpH.js} +2 -2
  64. package/public/assets/{terminal-viewer-impl-CmZjQOB2.js.map → terminal-viewer-impl-Dle04qpH.js.map} +1 -1
  65. package/public/assets/{work-queue-CXBQx43n.js → work-queue-BLhTLg4o.js} +2 -2
  66. package/public/assets/{work-queue-CXBQx43n.js.map → work-queue-BLhTLg4o.js.map} +1 -1
  67. package/public/assets/{workspaces-D2EfZh9a.js → workspaces-4NCNTmRQ.js} +3 -3
  68. package/public/assets/{workspaces-D2EfZh9a.js.map → workspaces-4NCNTmRQ.js.map} +1 -1
  69. package/public/index.html +3 -3
  70. package/scripts/orchestrator-spawn-smoke.ts +87 -67
  71. package/src/agent-ref.ts +8 -6
  72. package/src/automations/constants.ts +12 -0
  73. package/src/automations/cron.ts +129 -0
  74. package/src/automations/crud.ts +84 -0
  75. package/src/automations/dispatch.ts +275 -0
  76. package/src/automations/index.ts +5 -0
  77. package/src/automations/reconcile.ts +245 -0
  78. package/src/automations/storage.ts +181 -0
  79. package/src/automations/types.ts +16 -0
  80. package/src/automations/validation.ts +140 -0
  81. package/src/automations.ts +1 -1045
  82. package/src/db/activity.ts +1 -1
  83. package/src/db/agent-predicates.ts +9 -0
  84. package/src/db/agent-search.ts +2 -2
  85. package/src/db/agents.ts +13 -41
  86. package/src/db/artifacts.ts +8 -8
  87. package/src/db/channel-identity.ts +38 -0
  88. package/src/db/channels.ts +37 -61
  89. package/src/db/continuation-archives.ts +4 -4
  90. package/src/db/delivery.ts +75 -30
  91. package/src/db/inbox.ts +1 -1
  92. package/src/db/integrations.ts +4 -4
  93. package/src/db/issue-lifecycle.ts +6 -6
  94. package/src/db/mappers.ts +1 -2
  95. package/src/db/merge-lease.ts +3 -3
  96. package/src/db/message-claims.ts +16 -0
  97. package/src/db/message-queries.ts +8 -0
  98. package/src/db/message-reads.ts +157 -18
  99. package/src/db/messages.ts +23 -48
  100. package/src/db/notification-subscriptions.ts +2 -2
  101. package/src/db/orchestrators.ts +4 -4
  102. package/src/db/pair-maintenance.ts +9 -0
  103. package/src/db/pairs.ts +10 -18
  104. package/src/db/plans.ts +1 -1
  105. package/src/db/policy-runtime.ts +14 -0
  106. package/src/db/project-entries.ts +1 -1
  107. package/src/db/projects.ts +2 -2
  108. package/src/db/shared.ts +7 -0
  109. package/src/db/stats.ts +2 -2
  110. package/src/db/task-core.ts +18 -0
  111. package/src/db/task-maintenance.ts +24 -0
  112. package/src/db/tasks.ts +5 -17
  113. package/src/db/teams.ts +3 -3
  114. package/src/db/workspaces.ts +6 -6
  115. package/src/issue-tracker/github.ts +3 -3
  116. package/src/maintenance/constants.ts +35 -0
  117. package/src/maintenance/events.ts +32 -0
  118. package/src/maintenance/index.ts +7 -0
  119. package/src/maintenance/jobs.ts +340 -0
  120. package/src/maintenance/orphaned-sessions.ts +86 -0
  121. package/src/maintenance/scheduler.ts +231 -0
  122. package/src/maintenance/teams.ts +65 -0
  123. package/src/maintenance/types.ts +31 -0
  124. package/src/maintenance/workspaces.ts +251 -0
  125. package/src/maintenance.ts +1 -1099
  126. package/src/mcp/identity.ts +131 -0
  127. package/src/mcp/index.ts +208 -0
  128. package/src/mcp/tools-messaging.ts +459 -0
  129. package/src/mcp/tools-spawn.ts +145 -0
  130. package/src/mcp/tools-workspace.ts +227 -0
  131. package/src/mcp/types.ts +24 -0
  132. package/src/mcp/validation.ts +104 -0
  133. package/src/mcp.ts +1 -1319
  134. package/src/notification-router.ts +2 -2
  135. package/src/notification-rules.ts +2 -2
  136. package/src/project-ingest.ts +3 -3
  137. package/src/project-instructions.ts +1 -1
  138. package/src/provider-config-store.ts +3 -3
  139. package/src/provider-state.ts +1 -1
  140. package/src/reviewer-pipeline.ts +1 -1
  141. package/src/self-resume.ts +3 -7
  142. package/src/services/agent-action.ts +2 -2
  143. package/src/services/issue-activity.ts +0 -4
  144. package/src/services/issue-associations.ts +2 -2
  145. package/src/services/issue-lifecycle.ts +7 -7
  146. package/src/services/managed-running.ts +1 -1
  147. package/src/services/parity-harness.ts +3 -3
  148. package/src/services/plan-graph.ts +3 -3
  149. package/src/services/send-message.ts +16 -12
  150. package/src/services/shutdown-agent.ts +2 -2
  151. package/src/services/team-templates.ts +2 -2
  152. package/src/services/team.ts +2 -2
  153. package/src/settings/modules.ts +1 -1
  154. package/src/settings/registry.ts +3 -3
  155. package/src/spawn-command.ts +1 -1
  156. package/src/sse.ts +0 -4
  157. package/src/workspace-actions.ts +1 -1
  158. package/src/workspace-probe.ts +3 -7
  159. package/public/assets/MessageBubble-DRrxfjyT.js.map +0 -1
  160. package/public/assets/display-DzNvOu1j.js.map +0 -1
  161. package/public/assets/store-iTiheuTD.js.map +0 -1
@@ -0,0 +1,275 @@
1
+ import { errMessage } from "agent-relay-sdk";
2
+ import { createCommand } from "../commands-db";
3
+ import { getAgentProfile, getSpawnPolicy } from "../config-store";
4
+ import { getOrchestrator, ingestIntegrationEvent, listAgents, ValidationError } from "../db";
5
+ import { runnerRuntimeTokenEnv } from "../runtime-tokens";
6
+ import { buildSpawnCommand, resolveSpawnModelParams } from "../spawn-command";
7
+ import { nextScheduledAt, formatRunDate } from "./cron";
8
+ import {
9
+ cancelActiveRuns,
10
+ db,
11
+ enqueueAutomationRun,
12
+ ensureAutomationTables,
13
+ getAutomation,
14
+ getAutomationRun,
15
+ hasActiveRun,
16
+ insertRun,
17
+ listAutomations,
18
+ rowToAutomation,
19
+ rowToAutomationRun,
20
+ updateRun,
21
+ rescheduleAutomation as persistNextRun,
22
+ } from "./storage";
23
+ import type { AutomationDispatchResult } from "./types";
24
+ import type { AgentCard, Automation, AutomationTargetPolicy, ManagedAgent, Orchestrator } from "../types";
25
+
26
+ export function runDueAutomations(now = Date.now()): AutomationDispatchResult[] {
27
+ ensureAutomationTables();
28
+ const results = dispatchQueuedAutomationRuns(now);
29
+ const due = (db().query("SELECT * FROM automations WHERE enabled = 1 AND next_run_at IS NOT NULL AND next_run_at <= ? ORDER BY next_run_at ASC LIMIT 25").all(now) as any[])
30
+ .map(rowToAutomation);
31
+ for (const automation of due) {
32
+ if (hasActiveRun(automation.id)) {
33
+ if (automation.concurrencyPolicy === "replace") cancelActiveRuns(automation.id, now, "Replaced by newer scheduled run");
34
+ else {
35
+ if (automation.concurrencyPolicy === "queue") enqueueAutomationRun(automation, automation.nextRunAt ?? now, now);
36
+ rescheduleAutomation(automation, now);
37
+ continue;
38
+ }
39
+ }
40
+ results.push(dispatchAutomation(automation, automation.nextRunAt ?? now, now));
41
+ rescheduleAutomation(automation, now);
42
+ }
43
+ return results;
44
+ }
45
+
46
+ export function runAutomationNow(id: string, now = Date.now()): AutomationDispatchResult | null {
47
+ ensureAutomationTables();
48
+ const automation = getAutomation(id);
49
+ if (!automation) return null;
50
+ if (hasActiveRun(automation.id)) throw new ValidationError("automation already has an active run");
51
+ return dispatchAutomation(automation, now, now);
52
+ }
53
+
54
+ function dispatchAutomation(automation: Automation, scheduledFor: number, now: number): AutomationDispatchResult {
55
+ const run = insertRun(automation, scheduledFor, now);
56
+ return dispatchAutomationRun(automation, run, now);
57
+ }
58
+
59
+ function dispatchAutomationRun(automation: Automation, run: ReturnType<typeof getAutomationRun> & NonNullable<unknown>, now: number): AutomationDispatchResult {
60
+ updateRun(run.id, { status: "dispatching" }, now);
61
+ try {
62
+ const orchestrator = requireOnlineOrchestrator(automation.orchestratorId);
63
+ if (automation.targetPolicy.mode === "existing_agent") {
64
+ const agent = resolveExistingAgent(automation.targetPolicy, orchestrator);
65
+ if (!agent && automation.targetPolicy.ifNoMatch !== "spawn") throw new ValidationError("no matching managed agent for automation");
66
+ if (agent) {
67
+ const durableTarget = durableExistingAgentTarget(automation.targetPolicy);
68
+ return createRunTask(automation, run, durableTarget, now, { targetMode: "existing_agent", resolvedAgentId: agent.id });
69
+ }
70
+ const fallbackPolicy: AutomationTargetPolicy = { mode: "on_demand_agent", provider: orchestrator.providers[0] ?? "codex", keepAlive: false };
71
+ return dispatchOnDemandAutomation(automation, run, orchestrator, fallbackPolicy, now);
72
+ }
73
+ return dispatchOnDemandAutomation(automation, run, orchestrator, automation.targetPolicy, now);
74
+ } catch (e) {
75
+ updateRun(run.id, {
76
+ status: "failed",
77
+ finishedAt: now,
78
+ error: errMessage(e),
79
+ }, now);
80
+ return { automation, run: getAutomationRun(run.id)! };
81
+ }
82
+ }
83
+
84
+ function dispatchOnDemandAutomation(
85
+ automation: Automation,
86
+ run: ReturnType<typeof getAutomationRun> & NonNullable<unknown>,
87
+ orchestrator: Orchestrator,
88
+ policy: Extract<AutomationTargetPolicy, { mode: "on_demand_agent" }>,
89
+ now: number,
90
+ ): AutomationDispatchResult {
91
+ if (!orchestrator.providers.includes(policy.provider)) throw new ValidationError(`orchestrator ${orchestrator.id} does not have provider available: ${policy.provider}`);
92
+ const agentProfile = policy.profile ? getAgentProfile(policy.profile)?.value : undefined;
93
+ const label = automationRunLabel(automation.id, run.id);
94
+ const command = createCommand({
95
+ type: "agent.spawn",
96
+ source: "automation",
97
+ target: orchestrator.agentId,
98
+ params: buildSpawnCommand({
99
+ provider: policy.provider,
100
+ modelParams: resolveSpawnModelParams(policy.provider, policy.model, policy.effort),
101
+ profile: policy.profile,
102
+ agentProfile,
103
+ cwd: policy.cwd || orchestrator.baseDir,
104
+ workspaceMode: policy.workspaceMode ?? "inherit",
105
+ label,
106
+ approvalMode: policy.approvalMode ?? "guarded",
107
+ requestedBy: "automation",
108
+ requestedAt: now,
109
+ automationId: automation.id,
110
+ automationRunId: run.id,
111
+ env: runnerRuntimeTokenEnv({
112
+ orchestratorId: orchestrator.id,
113
+ cwd: policy.cwd || orchestrator.baseDir,
114
+ provider: policy.provider,
115
+ label,
116
+ createdBy: "automation",
117
+ profile: policy.profile,
118
+ }),
119
+ }),
120
+ });
121
+ const result = createRunTask(automation, run, `label:${label}`, now, {
122
+ targetMode: "on_demand_agent",
123
+ provider: policy.provider,
124
+ label,
125
+ profile: policy.profile,
126
+ workspaceMode: policy.workspaceMode ?? "inherit",
127
+ });
128
+ updateRun(run.id, {
129
+ status: "waiting_agent",
130
+ meta: { ...result.run.meta, onDemandLabel: label, spawnCommandId: command.id, workspaceMode: policy.workspaceMode ?? "inherit" },
131
+ }, now);
132
+ return { ...result, run: getAutomationRun(run.id)!, command };
133
+ }
134
+
135
+ function createRunTask(
136
+ automation: Automation,
137
+ run: ReturnType<typeof getAutomationRun> & NonNullable<unknown>,
138
+ target: string,
139
+ now: number,
140
+ meta: Record<string, unknown>,
141
+ ): AutomationDispatchResult {
142
+ const template = automation.taskTemplate;
143
+ // Inject the authoritative dispatch-time run date (#480). A long-lived managed agent's
144
+ // session-start day awareness goes stale across midnight, so it can't be trusted to derive
145
+ // "today" - the briefing emitted "Monday Jun 16" when it ran on a Tuesday. The scheduled
146
+ // instant + timezone are authoritative here; surface them so the task never depends on the
147
+ // model's recollection. Templates may place it via {{runDate}}/{{runWeekday}}/{{runDateLine}};
148
+ // otherwise we prepend a Run date line so every scheduled task carries it.
149
+ const runDate = formatRunDate(run.scheduledFor, automation.timezone);
150
+ const tokens: Record<string, string> = { runDate: runDate.iso, runWeekday: runDate.weekday, runDateLine: runDate.line };
151
+ const fill = (text: string) => text.replace(/\{\{\s*(runDate|runWeekday|runDateLine)\s*\}\}/g, (_match, key) => tokens[key] ?? "");
152
+ const usesRunDateToken = /\{\{\s*(runDate|runWeekday|runDateLine)\s*\}\}/.test(`${template.title}\n${template.body}`);
153
+ const title = fill(template.title);
154
+ const body = usesRunDateToken ? fill(template.body) : `Run date: ${runDate.line}.\n\n${template.body}`;
155
+ const result = ingestIntegrationEvent({
156
+ source: "automation",
157
+ type: "scheduled_task",
158
+ severity: template.severity ?? "info",
159
+ title,
160
+ body,
161
+ target,
162
+ channel: "automation",
163
+ // #479 - automation tasks complete via the task-status lifecycle (claim -> done), never a
164
+ // `/reply`. A reply obligation here is unsatisfiable and re-injects the task into every
165
+ // successor session when a restart-prone managed agent (e.g. the daily telegram briefing)
166
+ // mints a new agent id, re-firing the scheduled work off-schedule. Dispatch without one.
167
+ replyExpected: false,
168
+ dedupeKey: template.dedupeKey ? `${template.dedupeKey}:${run.id}` : `automation:${automation.id}:run:${run.id}`,
169
+ externalUrl: template.externalUrl,
170
+ metadata: {
171
+ ...(template.metadata ?? {}),
172
+ automationId: automation.id,
173
+ automationRunId: run.id,
174
+ orchestratorId: automation.orchestratorId,
175
+ runDate: runDate.iso,
176
+ runWeekday: runDate.weekday,
177
+ runTimezone: automation.timezone,
178
+ ...meta,
179
+ },
180
+ }, "automation");
181
+ updateRun(run.id, {
182
+ status: meta.targetMode === "on_demand_agent" ? "waiting_agent" : "running",
183
+ startedAt: now,
184
+ targetAgentId: typeof meta.resolvedAgentId === "string"
185
+ ? meta.resolvedAgentId
186
+ : target.startsWith("label:") || target.startsWith("cap:")
187
+ ? undefined
188
+ : target,
189
+ taskId: result.task.id,
190
+ messageId: result.message?.id,
191
+ meta,
192
+ }, now);
193
+ return { automation, run: getAutomationRun(run.id)!, task: result.task, message: result.message };
194
+ }
195
+
196
+ function durableExistingAgentTarget(policy: Extract<AutomationTargetPolicy, { mode: "existing_agent" }>): string {
197
+ const label = policy.selector.label?.trim();
198
+ if (label) return `label:${label}`;
199
+ const cap = policy.selector.capabilities?.find((item) => item.trim().length > 0)?.trim();
200
+ if (cap) return `cap:${cap}`;
201
+ throw new ValidationError("existing-agent automation target selector must include a label or capability");
202
+ }
203
+
204
+ function dispatchQueuedAutomationRuns(now: number): AutomationDispatchResult[] {
205
+ const queued = (db().query(`
206
+ SELECT r.* FROM automation_runs r
207
+ JOIN automations a ON a.id = r.automation_id
208
+ WHERE r.status = 'scheduled' AND a.enabled = 1
209
+ ORDER BY r.scheduled_for ASC
210
+ LIMIT 25
211
+ `).all() as any[]).map(rowToAutomationRun);
212
+ const results: AutomationDispatchResult[] = [];
213
+ for (const run of queued) {
214
+ const automation = getAutomation(run.automationId);
215
+ if (!automation || hasActiveRun(automation.id)) continue;
216
+ results.push(dispatchAutomationRun(automation, run, now));
217
+ }
218
+ return results;
219
+ }
220
+
221
+ function rescheduleAutomation(automation: Automation, now: number): void {
222
+ const next = automation.enabled ? nextScheduledAt(automation.schedule, automation.timezone, now) : undefined;
223
+ persistNextRun(automation, next, now);
224
+ }
225
+
226
+ function requireOnlineOrchestrator(orchestratorId: string): Orchestrator {
227
+ const orchestrator = getOrchestrator(orchestratorId);
228
+ if (!orchestrator) throw new ValidationError(`orchestrator ${orchestratorId} not found`);
229
+ if (orchestrator.status !== "online") throw new ValidationError(`orchestrator ${orchestratorId} is ${orchestrator.status}`);
230
+ return orchestrator;
231
+ }
232
+
233
+ function resolveExistingAgent(policy: Extract<AutomationTargetPolicy, { mode: "existing_agent" }>, orchestrator: Orchestrator): AgentCard | null {
234
+ const managedAgentsById = new Map(orchestrator.managedAgents.map((agent) => [agent.agentId, agent]));
235
+ const managedIds = new Set(managedAgentsById.keys());
236
+ const candidates = listAgents()
237
+ .filter((agent) => agent.id !== "user" && agent.id !== "system" && agent.kind !== "channel" && agent.kind !== "orchestrator")
238
+ .filter((agent) => agent.ready && agent.status !== "offline")
239
+ .filter((agent) => managedIds.size === 0 || managedIds.has(agent.id))
240
+ .filter((agent) => automationAgentMatchesSelector(agent, policy.selector, managedAgentsById.get(agent.id)));
241
+ candidates.sort((a, b) => b.lastSeen - a.lastSeen);
242
+ return candidates[0] ?? null;
243
+ }
244
+
245
+ function automationAgentMatchesSelector(
246
+ agent: AgentCard,
247
+ selector: Extract<AutomationTargetPolicy, { mode: "existing_agent" }>["selector"],
248
+ managedAgent?: ManagedAgent,
249
+ ): boolean {
250
+ const policyName = managedAgent?.policyName ?? (typeof agent.meta?.policyName === "string" ? agent.meta.policyName : undefined);
251
+ const spawnPolicy = policyName ? getSpawnPolicy(policyName)?.value : undefined;
252
+ const tags = new Set([...agent.tags, ...stringArray((managedAgent as { tags?: unknown } | undefined)?.tags), ...(spawnPolicy?.tags ?? [])]);
253
+ const capabilities = new Set([
254
+ ...agent.capabilities,
255
+ ...stringArray((managedAgent as { capabilities?: unknown } | undefined)?.capabilities),
256
+ ...(spawnPolicy?.capabilities ?? []),
257
+ ]);
258
+ const label = agent.label ?? managedAgent?.label ?? spawnPolicy?.label;
259
+ const provider = typeof agent.meta?.provider === "string" ? agent.meta.provider : managedAgent?.provider ?? spawnPolicy?.provider;
260
+ if (selector.provider) {
261
+ if (provider !== selector.provider && !tags.has(selector.provider)) return false;
262
+ }
263
+ if (selector.label && label !== selector.label) return false;
264
+ if (selector.tags?.length && !selector.tags.every((tag) => tags.has(tag))) return false;
265
+ if (selector.capabilities?.length && !selector.capabilities.every((cap) => capabilities.has(cap))) return false;
266
+ return true;
267
+ }
268
+
269
+ function stringArray(value: unknown): string[] {
270
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.length > 0) : [];
271
+ }
272
+
273
+ function automationRunLabel(automationId: string, runId: string): string {
274
+ return `automation-${automationId.slice(0, 8)}-${runId.slice(0, 8)}`;
275
+ }
@@ -0,0 +1,5 @@
1
+ export type { AutomationDispatchResult, AutomationReconcileResult } from "./types";
2
+ export { createAutomation, deleteAutomation, updateAutomation } from "./crud";
3
+ export { getAutomation, listAutomationRuns, listAutomations } from "./storage";
4
+ export { runAutomationNow, runDueAutomations } from "./dispatch";
5
+ export { reconcileAutomationRuns } from "./reconcile";
@@ -0,0 +1,245 @@
1
+ import { createCommand } from "../commands-db";
2
+ import { getAgent, getTask, listOrchestrators, sendMessage, updateTaskStatus } from "../db";
3
+ import { CLOSED_TASK_STATUS } from "./constants";
4
+ import {
5
+ ensureAutomationTables,
6
+ getAutomation,
7
+ getAutomationRun,
8
+ rowToAutomationRun,
9
+ updateRun,
10
+ db,
11
+ } from "./storage";
12
+ import type { AutomationReconcileResult } from "./types";
13
+ import type { AutomationRun, AutomationRunStatus, AutomationRuntimeBudget, Command, Message, Task } from "../types";
14
+
15
+ export function reconcileAutomationRuns(now = Date.now()): AutomationReconcileResult[] {
16
+ ensureAutomationTables();
17
+ const rows = db().query(`
18
+ SELECT * FROM automation_runs
19
+ WHERE status IN ('scheduled', 'dispatching', 'waiting_agent', 'running') AND task_id IS NOT NULL
20
+ ORDER BY created_at ASC
21
+ `).all() as any[];
22
+ const results: AutomationReconcileResult[] = [];
23
+ for (const row of rows) {
24
+ const run = rowToAutomationRun(row);
25
+ const task = run.taskId ? getTask(run.taskId) : null;
26
+ if (!task) continue;
27
+
28
+ const runtimeMeta = automationRunRuntimeMeta(run, task);
29
+ const hasRuntimeMetaChange = hasMetaChanges(run.meta, runtimeMeta);
30
+ const nextOpenStatus: AutomationRunStatus = task.status === "open" ? "waiting_agent" : "running";
31
+ if ((task.claimedBy && task.claimedBy !== run.targetAgentId) || run.status !== nextOpenStatus || hasRuntimeMetaChange) {
32
+ updateRun(run.id, {
33
+ status: nextOpenStatus,
34
+ targetAgentId: task.claimedBy ?? run.targetAgentId,
35
+ spawnedAgentId: run.spawnedAgentId ?? task.claimedBy,
36
+ meta: hasRuntimeMetaChange ? { ...run.meta, ...runtimeMeta } : undefined,
37
+ }, now);
38
+ }
39
+
40
+ if (!CLOSED_TASK_STATUS.has(task.status)) {
41
+ const updated = getAutomationRun(run.id);
42
+ if (!updated) continue;
43
+ const timedOut = maybeTimeoutRuntimeBudget(updated, task, now);
44
+ if (timedOut) {
45
+ results.push(timedOut);
46
+ continue;
47
+ }
48
+ const warning = maybeWarnRuntimeBudget(updated, task, now);
49
+ results.push({ run: getAutomationRun(run.id) ?? updated, task, message: warning });
50
+ continue;
51
+ }
52
+
53
+ const status: AutomationRunStatus = task.status === "done" ? "succeeded" : task.status === "canceled" ? "canceled" : "failed";
54
+ updateRun(run.id, {
55
+ status,
56
+ finishedAt: now,
57
+ result: { taskStatus: task.status, result: task.result ?? null },
58
+ targetAgentId: task.claimedBy ?? run.targetAgentId,
59
+ spawnedAgentId: run.spawnedAgentId ?? task.claimedBy,
60
+ meta: hasMetaChanges(run.meta, runtimeMeta) ? { ...run.meta, ...runtimeMeta } : undefined,
61
+ }, now);
62
+ const updated = getAutomationRun(run.id)!;
63
+ const command = maybeShutdownOnDemandAgent(updated, task, now);
64
+ results.push({ run: getAutomationRun(run.id)!, task, command });
65
+ }
66
+ return results;
67
+ }
68
+
69
+ function automationRunRuntimeMeta(run: AutomationRun, task: Task): Record<string, unknown> {
70
+ const agentId = task.claimedBy ?? run.targetAgentId ?? run.spawnedAgentId;
71
+ if (!agentId) return {};
72
+ const agent = getAgent(agentId);
73
+ const managed = listOrchestrators()
74
+ .flatMap((orchestrator) => orchestrator.managedAgents.map((managedAgent) => ({ orchestrator, managedAgent })))
75
+ .find(({ managedAgent }) => managedAgent.agentId === agentId);
76
+ const agentMeta = agent?.meta;
77
+ return compactMeta({
78
+ runtimeAgentId: agentId,
79
+ runtimeAgentStatus: agent?.status,
80
+ runtimeAgentReady: agent?.ready,
81
+ provider: stringMeta(agentMeta, "provider") ?? managed?.managedAgent.provider,
82
+ profile: stringMeta(agentMeta, "profile") ?? managed?.managedAgent.profile,
83
+ cwd: stringMeta(agentMeta, "cwd") ?? managed?.managedAgent.cwd,
84
+ label: agent?.label ?? stringMeta(agentMeta, "label") ?? managed?.managedAgent.label,
85
+ sessionName: stringMeta(agentMeta, "sessionName") ?? managed?.managedAgent.sessionName,
86
+ tmuxSession: stringMeta(agentMeta, "tmuxSession") ?? managed?.managedAgent.tmuxSession,
87
+ terminalSession: stringMeta(agentMeta, "terminalSession") ?? managed?.managedAgent.terminalSession,
88
+ orchestratorId: managed?.orchestrator.id ?? run.orchestratorId,
89
+ });
90
+ }
91
+
92
+ function compactMeta(input: Record<string, unknown>): Record<string, unknown> {
93
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== null && value !== ""));
94
+ }
95
+
96
+ function stringMeta(meta: Record<string, unknown> | undefined, key: string): string | undefined {
97
+ const value = meta?.[key];
98
+ return typeof value === "string" && value.length > 0 ? value : undefined;
99
+ }
100
+
101
+ function hasMetaChanges(current: Record<string, unknown>, patch: Record<string, unknown>): boolean {
102
+ return Object.entries(patch).some(([key, value]) => current[key] !== value);
103
+ }
104
+
105
+ function runtimeBudgetForRun(run: AutomationRun): AutomationRuntimeBudget | undefined {
106
+ const automation = getAutomation(run.automationId);
107
+ if (!automation || automation.targetPolicy.mode !== "on_demand_agent") return undefined;
108
+ return automation.targetPolicy.runtimeBudget;
109
+ }
110
+
111
+ function runtimeDeadline(run: AutomationRun, budget: AutomationRuntimeBudget): number | undefined {
112
+ return run.startedAt ? run.startedAt + budget.maxRuntimeMs : undefined;
113
+ }
114
+
115
+ function runtimeWarningAt(run: AutomationRun, budget: AutomationRuntimeBudget): number | undefined {
116
+ if (budget.warnAtMs === undefined || !run.startedAt) return undefined;
117
+ return run.startedAt + budget.warnAtMs;
118
+ }
119
+
120
+ function maybeWarnRuntimeBudget(run: AutomationRun, task: Task, now: number): Message | undefined {
121
+ if (run.meta.runtimeBudgetWarnedAt) return undefined;
122
+ const budget = runtimeBudgetForRun(run);
123
+ if (!budget) return undefined;
124
+ const warnAt = runtimeWarningAt(run, budget);
125
+ const deadline = runtimeDeadline(run, budget);
126
+ if (warnAt === undefined || deadline === undefined || now < warnAt || now >= deadline) return undefined;
127
+ const target = runtimeBudgetMessageTarget(run, task);
128
+ if (!target) return undefined;
129
+ const remainingMinutes = Math.max(1, Math.ceil((deadline - now) / 60_000));
130
+ const message = sendMessage({
131
+ from: "system",
132
+ to: target,
133
+ kind: "control",
134
+ subject: "Automation runtime budget warning",
135
+ body: budget.warningMessage ?? `Runtime budget warning: this automation run will be shut down in about ${remainingMinutes} minute${remainingMinutes === 1 ? "" : "s"}. Finish current work, commit and push anything worth keeping, then stop cleanly before the deadline.`,
136
+ payload: {
137
+ automationId: run.automationId,
138
+ automationRunId: run.id,
139
+ taskId: task.id,
140
+ runtimeBudgetWarning: true,
141
+ maxRuntimeMs: budget.maxRuntimeMs,
142
+ warnAtMs: budget.warnAtMs,
143
+ deadlineAt: deadline,
144
+ },
145
+ meta: {
146
+ source: "automation",
147
+ automationId: run.automationId,
148
+ automationRunId: run.id,
149
+ runtimeBudgetWarning: true,
150
+ },
151
+ });
152
+ updateRun(run.id, {
153
+ meta: {
154
+ ...run.meta,
155
+ runtimeBudgetWarnedAt: now,
156
+ runtimeBudgetWarningMessageId: message.id,
157
+ runtimeBudgetDeadlineAt: deadline,
158
+ },
159
+ }, now);
160
+ return message;
161
+ }
162
+
163
+ function maybeTimeoutRuntimeBudget(run: AutomationRun, task: Task, now: number): AutomationReconcileResult | undefined {
164
+ const budget = runtimeBudgetForRun(run);
165
+ if (!budget) return undefined;
166
+ const deadline = runtimeDeadline(run, budget);
167
+ if (deadline === undefined || now < deadline) return undefined;
168
+
169
+ const agentId = task.claimedBy ?? run.targetAgentId ?? run.spawnedAgentId;
170
+ const taskResult = updateTaskStatus(task.id, {
171
+ status: "canceled",
172
+ agentId,
173
+ result: "Automation runtime budget elapsed",
174
+ body: "Automation runtime budget elapsed",
175
+ metadata: {
176
+ automationId: run.automationId,
177
+ automationRunId: run.id,
178
+ runtimeBudgetElapsed: true,
179
+ runtimeBudgetTimedOut: true,
180
+ maxRuntimeMs: budget.maxRuntimeMs,
181
+ deadlineAt: deadline,
182
+ },
183
+ });
184
+ const updatedTask = taskResult.task ?? getTask(task.id) ?? task;
185
+ const command = requestOnDemandAgentShutdown(run, updatedTask, now, "runtime-budget-timeout");
186
+ updateRun(run.id, {
187
+ status: "timed_out",
188
+ finishedAt: now,
189
+ error: "Automation runtime budget elapsed",
190
+ result: {
191
+ taskStatus: updatedTask.status,
192
+ runtimeBudgetElapsed: true,
193
+ runtimeBudgetTimedOut: true,
194
+ maxRuntimeMs: budget.maxRuntimeMs,
195
+ deadlineAt: deadline,
196
+ },
197
+ targetAgentId: agentId,
198
+ spawnedAgentId: run.spawnedAgentId ?? agentId,
199
+ shutdownRequestedAt: command ? now : undefined,
200
+ meta: {
201
+ ...run.meta,
202
+ runtimeBudgetElapsedAt: now,
203
+ runtimeBudgetTimedOutAt: now,
204
+ runtimeBudgetDeadlineAt: deadline,
205
+ ...(command ? { shutdownCommandId: command.id } : {}),
206
+ },
207
+ }, now);
208
+ return { run: getAutomationRun(run.id)!, task: getTask(task.id) ?? updatedTask, command };
209
+ }
210
+
211
+ function runtimeBudgetMessageTarget(run: AutomationRun, task: Task): string | undefined {
212
+ if (task.claimedBy) return task.claimedBy;
213
+ if (run.targetAgentId) return run.targetAgentId;
214
+ if (run.spawnedAgentId) return run.spawnedAgentId;
215
+ if (typeof run.meta.onDemandLabel === "string" && run.meta.onDemandLabel) return `label:${run.meta.onDemandLabel}`;
216
+ return undefined;
217
+ }
218
+
219
+ function maybeShutdownOnDemandAgent(run: AutomationRun, task: Task, now: number): Command | undefined {
220
+ if (run.shutdownRequestedAt) return undefined;
221
+ const automation = getAutomation(run.automationId);
222
+ if (!automation || automation.targetPolicy.mode !== "on_demand_agent" || automation.targetPolicy.keepAlive) return undefined;
223
+ const command = requestOnDemandAgentShutdown(run, task, now, "task-complete");
224
+ if (command) updateRun(run.id, { shutdownRequestedAt: now, meta: { ...run.meta, shutdownCommandId: command.id } }, now);
225
+ return command;
226
+ }
227
+
228
+ function requestOnDemandAgentShutdown(run: AutomationRun, task: Task, now: number, reason: string): Command | undefined {
229
+ const agentId = task.claimedBy ?? run.targetAgentId ?? run.spawnedAgentId;
230
+ if (!agentId) return undefined;
231
+ return createCommand({
232
+ type: "agent.shutdown",
233
+ source: "automation",
234
+ target: agentId,
235
+ params: {
236
+ action: "shutdown",
237
+ agentId,
238
+ requestedBy: "automation",
239
+ requestedAt: now,
240
+ automationId: run.automationId,
241
+ automationRunId: run.id,
242
+ reason,
243
+ },
244
+ });
245
+ }