agent-relay-server 0.62.3 → 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.
- package/docs/openapi.json +1 -1
- package/package.json +2 -2
- package/public/assets/{MessageBubble-DRrxfjyT.js → MessageBubble-Cl6ufPHw.js} +2 -2
- package/public/assets/MessageBubble-Cl6ufPHw.js.map +1 -0
- package/public/assets/{PlanGraphPanel-Dp2eYazz.js → PlanGraphPanel-BnfjmkQ8.js} +2 -2
- package/public/assets/{PlanGraphPanel-Dp2eYazz.js.map → PlanGraphPanel-BnfjmkQ8.js.map} +1 -1
- package/public/assets/{activity--t0U2yzq.js → activity-aFxvGLtd.js} +2 -2
- package/public/assets/{activity--t0U2yzq.js.map → activity-aFxvGLtd.js.map} +1 -1
- package/public/assets/{agent-profiles-BMtJYzxn.js → agent-profiles-B4mleUDz.js} +2 -2
- package/public/assets/{agent-profiles-BMtJYzxn.js.map → agent-profiles-B4mleUDz.js.map} +1 -1
- package/public/assets/{agents-Ck9AePlc.js → agents-DW5djToQ.js} +2 -2
- package/public/assets/{agents-Ck9AePlc.js.map → agents-DW5djToQ.js.map} +1 -1
- package/public/assets/{analytics-CzfK387L.js → analytics-CRi6thtg.js} +2 -2
- package/public/assets/{analytics-CzfK387L.js.map → analytics-CRi6thtg.js.map} +1 -1
- package/public/assets/{automation-Cgw8vtE8.js → automation-Do4OpauI.js} +2 -2
- package/public/assets/{automation-Cgw8vtE8.js.map → automation-Do4OpauI.js.map} +1 -1
- package/public/assets/{branch-state-badge-DnEEPtOP.js → branch-state-badge-D3W3w7zz.js} +2 -2
- package/public/assets/{branch-state-badge-DnEEPtOP.js.map → branch-state-badge-D3W3w7zz.js.map} +1 -1
- package/public/assets/{channels-Dc3L2MQs.js → channels-BytdR9NS.js} +2 -2
- package/public/assets/{channels-Dc3L2MQs.js.map → channels-BytdR9NS.js.map} +1 -1
- package/public/assets/{chat-Fc6iJ7sa.js → chat-CvaQ2czL.js} +2 -2
- package/public/assets/{chat-Fc6iJ7sa.js.map → chat-CvaQ2czL.js.map} +1 -1
- package/public/assets/{connectors-Dc2ttGWs.js → connectors-BLQDp8Bc.js} +2 -2
- package/public/assets/{connectors-Dc2ttGWs.js.map → connectors-BLQDp8Bc.js.map} +1 -1
- package/public/assets/{display-DzNvOu1j.js → display-B06I5sqa.js} +3 -3
- package/public/assets/display-B06I5sqa.js.map +1 -0
- package/public/assets/{formatted-body-impl-Bg-gx7rG.js → formatted-body-impl-0Lf8e1Ic.js} +2 -2
- package/public/assets/{formatted-body-impl-Bg-gx7rG.js.map → formatted-body-impl-0Lf8e1Ic.js.map} +1 -1
- package/public/assets/{index-BeDncjN4.js → index-C3su8ezq.js} +6 -6
- package/public/assets/{index-BeDncjN4.js.map → index-C3su8ezq.js.map} +1 -1
- package/public/assets/{insights-c08aW-7n.js → insights-64uExH1E.js} +2 -2
- package/public/assets/{insights-c08aW-7n.js.map → insights-64uExH1E.js.map} +1 -1
- package/public/assets/{integrations-BwpgcqnF.js → integrations-HyC1EaIp.js} +2 -2
- package/public/assets/{integrations-BwpgcqnF.js.map → integrations-HyC1EaIp.js.map} +1 -1
- package/public/assets/{maintenance-CTKP0kWB.js → maintenance-EVBpKH07.js} +2 -2
- package/public/assets/{maintenance-CTKP0kWB.js.map → maintenance-EVBpKH07.js.map} +1 -1
- package/public/assets/{managed-agents-BofCxo8d.js → managed-agents-BLZPxGuw.js} +2 -2
- package/public/assets/{managed-agents-BofCxo8d.js.map → managed-agents-BLZPxGuw.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CQ-6BIb8.js → markdown-preview-impl-Bg6rCWYN.js} +2 -2
- package/public/assets/{markdown-preview-impl-CQ-6BIb8.js.map → markdown-preview-impl-Bg6rCWYN.js.map} +1 -1
- package/public/assets/{memory-DvfXjMoi.js → memory-Bjr8bgjj.js} +2 -2
- package/public/assets/{memory-DvfXjMoi.js.map → memory-Bjr8bgjj.js.map} +1 -1
- package/public/assets/{messages-C9VsPzZ_.js → messages-CRYw1F4z.js} +2 -2
- package/public/assets/{messages-C9VsPzZ_.js.map → messages-CRYw1F4z.js.map} +1 -1
- package/public/assets/{orchestrators-MffVueeE.js → orchestrators-D-co1vdp.js} +2 -2
- package/public/assets/{orchestrators-MffVueeE.js.map → orchestrators-D-co1vdp.js.map} +1 -1
- package/public/assets/{overview-BSVajefM.js → overview-Ddy5czri.js} +2 -2
- package/public/assets/{overview-BSVajefM.js.map → overview-Ddy5czri.js.map} +1 -1
- package/public/assets/{pairs-B5711-Cs.js → pairs-CFVM8uHL.js} +2 -2
- package/public/assets/{pairs-B5711-Cs.js.map → pairs-CFVM8uHL.js.map} +1 -1
- package/public/assets/{security-DkUwK9Vj.js → security-Df4_76bh.js} +2 -2
- package/public/assets/{security-DkUwK9Vj.js.map → security-Df4_76bh.js.map} +1 -1
- package/public/assets/{select-DrM2RRiT.js → select-Cq3BBnL9.js} +2 -2
- package/public/assets/{select-DrM2RRiT.js.map → select-Cq3BBnL9.js.map} +1 -1
- package/public/assets/{settings-DdKCb-vy.js → settings-DOLEYXZL.js} +2 -2
- package/public/assets/{settings-DdKCb-vy.js.map → settings-DOLEYXZL.js.map} +1 -1
- package/public/assets/{store-iTiheuTD.js → store-C1E8jmNE.js} +4 -4
- package/public/assets/store-C1E8jmNE.js.map +1 -0
- package/public/assets/{tasks-e1y1PFfO.js → tasks-BcsmPi9_.js} +2 -2
- package/public/assets/{tasks-e1y1PFfO.js.map → tasks-BcsmPi9_.js.map} +1 -1
- package/public/assets/{teams-BUcxoGC_.js → teams-BQrV3eKI.js} +2 -2
- package/public/assets/{teams-BUcxoGC_.js.map → teams-BQrV3eKI.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-CmZjQOB2.js → terminal-viewer-impl-Dle04qpH.js} +2 -2
- package/public/assets/{terminal-viewer-impl-CmZjQOB2.js.map → terminal-viewer-impl-Dle04qpH.js.map} +1 -1
- package/public/assets/{work-queue-CXBQx43n.js → work-queue-BLhTLg4o.js} +2 -2
- package/public/assets/{work-queue-CXBQx43n.js.map → work-queue-BLhTLg4o.js.map} +1 -1
- package/public/assets/{workspaces-D2EfZh9a.js → workspaces-4NCNTmRQ.js} +3 -3
- package/public/assets/{workspaces-D2EfZh9a.js.map → workspaces-4NCNTmRQ.js.map} +1 -1
- package/public/index.html +3 -3
- package/src/agent-ref.ts +8 -6
- package/src/automations/constants.ts +12 -0
- package/src/automations/cron.ts +129 -0
- package/src/automations/crud.ts +84 -0
- package/src/automations/dispatch.ts +275 -0
- package/src/automations/index.ts +5 -0
- package/src/automations/reconcile.ts +245 -0
- package/src/automations/storage.ts +181 -0
- package/src/automations/types.ts +16 -0
- package/src/automations/validation.ts +140 -0
- package/src/automations.ts +1 -1045
- package/src/db/activity.ts +1 -1
- package/src/db/agent-predicates.ts +9 -0
- package/src/db/agent-search.ts +2 -2
- package/src/db/agents.ts +13 -41
- package/src/db/artifacts.ts +8 -8
- package/src/db/channel-identity.ts +38 -0
- package/src/db/channels.ts +37 -61
- package/src/db/continuation-archives.ts +4 -4
- package/src/db/delivery.ts +7 -12
- package/src/db/inbox.ts +1 -1
- package/src/db/integrations.ts +4 -4
- package/src/db/issue-lifecycle.ts +6 -6
- package/src/db/mappers.ts +1 -2
- package/src/db/merge-lease.ts +3 -3
- package/src/db/message-claims.ts +16 -0
- package/src/db/message-queries.ts +8 -0
- package/src/db/message-reads.ts +26 -9
- package/src/db/messages.ts +17 -41
- package/src/db/notification-subscriptions.ts +2 -2
- package/src/db/orchestrators.ts +4 -4
- package/src/db/pair-maintenance.ts +9 -0
- package/src/db/pairs.ts +10 -18
- package/src/db/plans.ts +1 -1
- package/src/db/policy-runtime.ts +14 -0
- package/src/db/project-entries.ts +1 -1
- package/src/db/projects.ts +2 -2
- package/src/db/shared.ts +7 -0
- package/src/db/stats.ts +2 -2
- package/src/db/task-core.ts +18 -0
- package/src/db/task-maintenance.ts +24 -0
- package/src/db/tasks.ts +5 -17
- package/src/db/teams.ts +3 -3
- package/src/db/workspaces.ts +6 -6
- package/src/issue-tracker/github.ts +3 -3
- package/src/maintenance/constants.ts +35 -0
- package/src/maintenance/events.ts +32 -0
- package/src/maintenance/index.ts +7 -0
- package/src/maintenance/jobs.ts +340 -0
- package/src/maintenance/orphaned-sessions.ts +86 -0
- package/src/maintenance/scheduler.ts +231 -0
- package/src/maintenance/teams.ts +65 -0
- package/src/maintenance/types.ts +31 -0
- package/src/maintenance/workspaces.ts +251 -0
- package/src/maintenance.ts +1 -1099
- package/src/mcp/identity.ts +131 -0
- package/src/mcp/index.ts +208 -0
- package/src/mcp/tools-messaging.ts +459 -0
- package/src/mcp/tools-spawn.ts +145 -0
- package/src/mcp/tools-workspace.ts +227 -0
- package/src/mcp/types.ts +24 -0
- package/src/mcp/validation.ts +104 -0
- package/src/mcp.ts +1 -1319
- package/src/notification-router.ts +2 -2
- package/src/notification-rules.ts +2 -2
- package/src/project-ingest.ts +3 -3
- package/src/project-instructions.ts +1 -1
- package/src/provider-config-store.ts +3 -3
- package/src/provider-state.ts +1 -1
- package/src/reviewer-pipeline.ts +1 -1
- package/src/self-resume.ts +3 -7
- package/src/services/agent-action.ts +2 -2
- package/src/services/issue-activity.ts +0 -4
- package/src/services/issue-associations.ts +2 -2
- package/src/services/issue-lifecycle.ts +7 -7
- package/src/services/managed-running.ts +1 -1
- package/src/services/parity-harness.ts +3 -3
- package/src/services/plan-graph.ts +3 -3
- package/src/services/send-message.ts +16 -12
- package/src/services/shutdown-agent.ts +2 -2
- package/src/services/team-templates.ts +2 -2
- package/src/services/team.ts +2 -2
- package/src/settings/modules.ts +1 -1
- package/src/settings/registry.ts +3 -3
- package/src/spawn-command.ts +1 -1
- package/src/sse.ts +0 -4
- package/src/workspace-actions.ts +1 -1
- package/src/workspace-probe.ts +3 -7
- package/public/assets/MessageBubble-DRrxfjyT.js.map +0 -1
- package/public/assets/display-DzNvOu1j.js.map +0 -1
- package/public/assets/store-iTiheuTD.js.map +0 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { parseJson } from "../utils";
|
|
3
|
+
import { getDb } from "../db";
|
|
4
|
+
import { BLOCKING_RUN_STATUSES, OPEN_RUN_STATUSES } from "./constants";
|
|
5
|
+
import type { Automation, AutomationRun, AutomationRunStatus, AutomationTargetPolicy, AutomationTaskTemplate } from "../types";
|
|
6
|
+
|
|
7
|
+
let initializedDb: unknown = null;
|
|
8
|
+
|
|
9
|
+
export function db() {
|
|
10
|
+
return getDb();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ensureAutomationTables(): void {
|
|
14
|
+
const current = db();
|
|
15
|
+
if (initializedDb === current) return;
|
|
16
|
+
initializedDb = current;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function rowToAutomation(row: any): Automation {
|
|
20
|
+
return {
|
|
21
|
+
id: row.id,
|
|
22
|
+
kind: row.kind,
|
|
23
|
+
name: row.name,
|
|
24
|
+
description: row.description ?? undefined,
|
|
25
|
+
enabled: row.enabled === 1,
|
|
26
|
+
schedule: row.schedule,
|
|
27
|
+
timezone: row.timezone,
|
|
28
|
+
nextRunAt: row.next_run_at ?? undefined,
|
|
29
|
+
catchUpPolicy: row.catch_up_policy,
|
|
30
|
+
concurrencyPolicy: row.concurrency_policy,
|
|
31
|
+
orchestratorId: row.orchestrator_id,
|
|
32
|
+
targetPolicy: parseJson<AutomationTargetPolicy>(row.target_policy, { mode: "existing_agent", selector: {}, ifNoMatch: "fail" }),
|
|
33
|
+
taskTemplate: parseJson<AutomationTaskTemplate>(row.task_template, { title: row.name, body: "" }),
|
|
34
|
+
createdAt: row.created_at,
|
|
35
|
+
updatedAt: row.updated_at,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function rowToAutomationRun(row: any): AutomationRun {
|
|
40
|
+
return {
|
|
41
|
+
id: row.id,
|
|
42
|
+
automationId: row.automation_id,
|
|
43
|
+
status: row.status,
|
|
44
|
+
scheduledFor: row.scheduled_for,
|
|
45
|
+
startedAt: row.started_at ?? undefined,
|
|
46
|
+
finishedAt: row.finished_at ?? undefined,
|
|
47
|
+
orchestratorId: row.orchestrator_id,
|
|
48
|
+
targetAgentId: row.target_agent_id ?? undefined,
|
|
49
|
+
spawnedAgentId: row.spawned_agent_id ?? undefined,
|
|
50
|
+
taskId: row.task_id ?? undefined,
|
|
51
|
+
messageId: row.message_id ?? undefined,
|
|
52
|
+
error: row.error ?? undefined,
|
|
53
|
+
result: parseJson<Record<string, unknown> | undefined>(row.result, undefined),
|
|
54
|
+
meta: parseJson<Record<string, unknown>>(row.meta, {}),
|
|
55
|
+
createdAt: row.created_at,
|
|
56
|
+
updatedAt: row.updated_at,
|
|
57
|
+
shutdownRequestedAt: row.shutdown_requested_at ?? undefined,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getAutomation(id: string): Automation | null {
|
|
62
|
+
ensureAutomationTables();
|
|
63
|
+
const row = db().query("SELECT * FROM automations WHERE id = ?").get(id) as any;
|
|
64
|
+
return row ? rowToAutomation(row) : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function listAutomations(): Automation[] {
|
|
68
|
+
ensureAutomationTables();
|
|
69
|
+
return (db().query("SELECT * FROM automations ORDER BY enabled DESC, next_run_at IS NULL, next_run_at ASC, name COLLATE NOCASE").all() as any[])
|
|
70
|
+
.map(rowToAutomation);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function listAutomationRuns(filter: { automationId?: string; status?: string; limit?: number } = {}): AutomationRun[] {
|
|
74
|
+
ensureAutomationTables();
|
|
75
|
+
const clauses: string[] = [];
|
|
76
|
+
const params: (string | number)[] = [];
|
|
77
|
+
if (filter.automationId) {
|
|
78
|
+
clauses.push("automation_id = ?");
|
|
79
|
+
params.push(filter.automationId);
|
|
80
|
+
}
|
|
81
|
+
if (filter.status) {
|
|
82
|
+
clauses.push("status = ?");
|
|
83
|
+
params.push(filter.status);
|
|
84
|
+
}
|
|
85
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
86
|
+
params.push(filter.limit ?? 100);
|
|
87
|
+
return (db().query(`SELECT * FROM automation_runs ${where} ORDER BY created_at DESC LIMIT ?`).all(...params) as any[])
|
|
88
|
+
.map(rowToAutomationRun);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getAutomationRun(id: string): AutomationRun | null {
|
|
92
|
+
ensureAutomationTables();
|
|
93
|
+
const row = db().query("SELECT * FROM automation_runs WHERE id = ?").get(id) as any;
|
|
94
|
+
return row ? rowToAutomationRun(row) : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function insertRun(automation: Automation, scheduledFor: number, now: number): AutomationRun {
|
|
98
|
+
const id = randomUUID();
|
|
99
|
+
db().query(`
|
|
100
|
+
INSERT INTO automation_runs (id, automation_id, status, scheduled_for, orchestrator_id, meta, created_at, updated_at)
|
|
101
|
+
VALUES (?, ?, 'dispatching', ?, ?, '{}', ?, ?)
|
|
102
|
+
`).run(id, automation.id, scheduledFor, automation.orchestratorId, now, now);
|
|
103
|
+
return getAutomationRun(id)!;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function enqueueAutomationRun(automation: Automation, scheduledFor: number, now: number): AutomationRun {
|
|
107
|
+
const existing = db().query("SELECT * FROM automation_runs WHERE automation_id = ? AND status = 'scheduled' AND scheduled_for = ?")
|
|
108
|
+
.get(automation.id, scheduledFor) as any;
|
|
109
|
+
if (existing) return rowToAutomationRun(existing);
|
|
110
|
+
const id = randomUUID();
|
|
111
|
+
db().query(`
|
|
112
|
+
INSERT INTO automation_runs (id, automation_id, status, scheduled_for, orchestrator_id, meta, created_at, updated_at)
|
|
113
|
+
VALUES (?, ?, 'scheduled', ?, ?, '{}', ?, ?)
|
|
114
|
+
`).run(id, automation.id, scheduledFor, automation.orchestratorId, now, now);
|
|
115
|
+
return getAutomationRun(id)!;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function updateRun(id: string, input: {
|
|
119
|
+
status?: AutomationRunStatus;
|
|
120
|
+
startedAt?: number;
|
|
121
|
+
finishedAt?: number;
|
|
122
|
+
targetAgentId?: string;
|
|
123
|
+
spawnedAgentId?: string;
|
|
124
|
+
taskId?: number;
|
|
125
|
+
messageId?: number;
|
|
126
|
+
error?: string;
|
|
127
|
+
result?: Record<string, unknown>;
|
|
128
|
+
meta?: Record<string, unknown>;
|
|
129
|
+
shutdownRequestedAt?: number;
|
|
130
|
+
}, now: number): void {
|
|
131
|
+
const current = getAutomationRun(id);
|
|
132
|
+
const meta = input.meta !== undefined ? input.meta : current?.meta;
|
|
133
|
+
db().query(`
|
|
134
|
+
UPDATE automation_runs
|
|
135
|
+
SET status = COALESCE(?, status),
|
|
136
|
+
started_at = COALESCE(?, started_at),
|
|
137
|
+
finished_at = COALESCE(?, finished_at),
|
|
138
|
+
target_agent_id = COALESCE(?, target_agent_id),
|
|
139
|
+
spawned_agent_id = COALESCE(?, spawned_agent_id),
|
|
140
|
+
task_id = COALESCE(?, task_id),
|
|
141
|
+
message_id = COALESCE(?, message_id),
|
|
142
|
+
control_message_id = control_message_id,
|
|
143
|
+
error = COALESCE(?, error),
|
|
144
|
+
result = COALESCE(?, result),
|
|
145
|
+
meta = COALESCE(?, meta),
|
|
146
|
+
shutdown_requested_at = COALESCE(?, shutdown_requested_at),
|
|
147
|
+
updated_at = ?
|
|
148
|
+
WHERE id = ?
|
|
149
|
+
`).run(
|
|
150
|
+
input.status ?? null,
|
|
151
|
+
input.startedAt ?? null,
|
|
152
|
+
input.finishedAt ?? null,
|
|
153
|
+
input.targetAgentId ?? null,
|
|
154
|
+
input.spawnedAgentId ?? null,
|
|
155
|
+
input.taskId ?? null,
|
|
156
|
+
input.messageId ?? null,
|
|
157
|
+
input.error ?? null,
|
|
158
|
+
input.result ? JSON.stringify(input.result) : null,
|
|
159
|
+
meta ? JSON.stringify(meta) : null,
|
|
160
|
+
input.shutdownRequestedAt ?? null,
|
|
161
|
+
now,
|
|
162
|
+
id,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function hasActiveRun(automationId: string): boolean {
|
|
167
|
+
const placeholders = [...BLOCKING_RUN_STATUSES].map(() => "?").join(",");
|
|
168
|
+
const row = db().query(`SELECT id FROM automation_runs WHERE automation_id = ? AND status IN (${placeholders}) LIMIT 1`)
|
|
169
|
+
.get(automationId, ...BLOCKING_RUN_STATUSES) as any;
|
|
170
|
+
return Boolean(row);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function cancelActiveRuns(automationId: string, now: number, reason: string): void {
|
|
174
|
+
const placeholders = [...OPEN_RUN_STATUSES].map(() => "?").join(",");
|
|
175
|
+
db().query(`UPDATE automation_runs SET status = 'canceled', finished_at = ?, error = ?, updated_at = ? WHERE automation_id = ? AND status IN (${placeholders})`)
|
|
176
|
+
.run(now, reason, now, automationId, ...OPEN_RUN_STATUSES);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function rescheduleAutomation(automation: Automation, nextRunAt: number | undefined, now: number): void {
|
|
180
|
+
db().query("UPDATE automations SET next_run_at = ?, updated_at = ? WHERE id = ?").run(nextRunAt ?? null, now, automation.id);
|
|
181
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Automation, AutomationRun, Command, Message, Task } from "../types";
|
|
2
|
+
|
|
3
|
+
export interface AutomationDispatchResult {
|
|
4
|
+
automation: Automation;
|
|
5
|
+
run: AutomationRun;
|
|
6
|
+
task?: Task;
|
|
7
|
+
message?: Message;
|
|
8
|
+
command?: Command;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AutomationReconcileResult {
|
|
12
|
+
run: AutomationRun;
|
|
13
|
+
task?: Task;
|
|
14
|
+
message?: Message;
|
|
15
|
+
command?: Command;
|
|
16
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { errMessage, VALID_WORKSPACE_MODES } from "agent-relay-sdk";
|
|
2
|
+
import { resolveProviderSelection, type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
3
|
+
import { getAgentProfile } from "../config-store";
|
|
4
|
+
import { ValidationError } from "../db";
|
|
5
|
+
import { effectiveProviderCatalogList } from "../provider-catalog-store";
|
|
6
|
+
import { cleanEnum, cleanString, cleanStringArray, optionalEnum } from "../validation";
|
|
7
|
+
import { DEFAULT_CATCH_UP, DEFAULT_CONCURRENCY, DEFAULT_TIMEZONE, MAX_RUNTIME_BUDGET_MS, MIN_RUNTIME_BUDGET_MS, AUTOMATION_KIND } from "./constants";
|
|
8
|
+
import { validateCron } from "./cron";
|
|
9
|
+
import type {
|
|
10
|
+
AutomationRuntimeBudget,
|
|
11
|
+
AutomationTargetPolicy,
|
|
12
|
+
AutomationTaskTemplate,
|
|
13
|
+
CreateAutomationInput,
|
|
14
|
+
TaskSeverity,
|
|
15
|
+
} from "../types";
|
|
16
|
+
|
|
17
|
+
function cleanBool(value: unknown, field: string): boolean | undefined {
|
|
18
|
+
if (value === undefined || value === null) return undefined;
|
|
19
|
+
if (typeof value !== "boolean") throw new ValidationError(`${field} must be a boolean`);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function cleanMeta(value: unknown, field = "metadata"): Record<string, unknown> | undefined {
|
|
24
|
+
if (value === undefined || value === null) return undefined;
|
|
25
|
+
if (typeof value !== "object" || Array.isArray(value)) throw new ValidationError(`${field} must be an object`);
|
|
26
|
+
if (JSON.stringify(value).length > 65_536) throw new ValidationError(`${field} is too large`);
|
|
27
|
+
return value as Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeTaskTemplate(value: unknown): AutomationTaskTemplate {
|
|
31
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new ValidationError("taskTemplate must be an object");
|
|
32
|
+
const input = value as Record<string, unknown>;
|
|
33
|
+
return {
|
|
34
|
+
title: cleanString(input.title, "taskTemplate.title", { required: true, max: 240 })!,
|
|
35
|
+
body: cleanString(input.body, "taskTemplate.body", { required: true, max: 200_000 })!,
|
|
36
|
+
severity: optionalEnum(input.severity, "taskTemplate.severity", ["info", "warning", "critical"] as const, "info") as TaskSeverity,
|
|
37
|
+
dedupeKey: cleanString(input.dedupeKey, "taskTemplate.dedupeKey", { max: 240 }),
|
|
38
|
+
externalUrl: cleanString(input.externalUrl, "taskTemplate.externalUrl", { max: 1000 }),
|
|
39
|
+
metadata: cleanMeta(input.metadata, "taskTemplate.metadata"),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeRuntimeBudget(input: Record<string, unknown>): AutomationRuntimeBudget | undefined {
|
|
44
|
+
const rawBudget = input.runtimeBudget;
|
|
45
|
+
let budgetInput: Record<string, unknown> | undefined;
|
|
46
|
+
if (rawBudget !== undefined && rawBudget !== null) {
|
|
47
|
+
if (typeof rawBudget !== "object" || Array.isArray(rawBudget)) throw new ValidationError("targetPolicy.runtimeBudget must be an object");
|
|
48
|
+
budgetInput = rawBudget as Record<string, unknown>;
|
|
49
|
+
} else if (typeof input.shutdownAfterMs === "number" && input.shutdownAfterMs > 0) {
|
|
50
|
+
budgetInput = { maxRuntimeMs: input.shutdownAfterMs };
|
|
51
|
+
}
|
|
52
|
+
if (!budgetInput) return undefined;
|
|
53
|
+
|
|
54
|
+
const maxRuntimeMsRaw = budgetInput.maxRuntimeMs;
|
|
55
|
+
if (!Number.isSafeInteger(maxRuntimeMsRaw)) {
|
|
56
|
+
throw new ValidationError("targetPolicy.runtimeBudget.maxRuntimeMs must be between 60000 and 86400000");
|
|
57
|
+
}
|
|
58
|
+
const maxRuntimeMs = maxRuntimeMsRaw as number;
|
|
59
|
+
if (maxRuntimeMs < MIN_RUNTIME_BUDGET_MS || maxRuntimeMs > MAX_RUNTIME_BUDGET_MS) {
|
|
60
|
+
throw new ValidationError("targetPolicy.runtimeBudget.maxRuntimeMs must be between 60000 and 86400000");
|
|
61
|
+
}
|
|
62
|
+
const warnAtMsRaw = budgetInput.warnAtMs;
|
|
63
|
+
if (warnAtMsRaw !== undefined && !Number.isSafeInteger(warnAtMsRaw)) {
|
|
64
|
+
throw new ValidationError("targetPolicy.runtimeBudget.warnAtMs must be less than maxRuntimeMs");
|
|
65
|
+
}
|
|
66
|
+
const warnAtMs = warnAtMsRaw as number | undefined;
|
|
67
|
+
if (warnAtMs !== undefined && (warnAtMs < 0 || warnAtMs >= maxRuntimeMs)) {
|
|
68
|
+
throw new ValidationError("targetPolicy.runtimeBudget.warnAtMs must be less than maxRuntimeMs");
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
maxRuntimeMs,
|
|
72
|
+
warnAtMs,
|
|
73
|
+
warningMessage: cleanString(budgetInput.warningMessage, "targetPolicy.runtimeBudget.warningMessage", { max: 4000 }),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeTargetPolicy(value: unknown): AutomationTargetPolicy {
|
|
78
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new ValidationError("targetPolicy must be an object");
|
|
79
|
+
const input = value as Record<string, unknown>;
|
|
80
|
+
const mode = cleanEnum(input.mode, "targetPolicy.mode", ["existing_agent", "on_demand_agent"] as const);
|
|
81
|
+
if (mode === "existing_agent") {
|
|
82
|
+
const selectorInput = (typeof input.selector === "object" && input.selector !== null && !Array.isArray(input.selector))
|
|
83
|
+
? input.selector as Record<string, unknown>
|
|
84
|
+
: {};
|
|
85
|
+
return {
|
|
86
|
+
mode,
|
|
87
|
+
selector: {
|
|
88
|
+
provider: cleanString(selectorInput.provider, "targetPolicy.selector.provider", { max: 40 }) as "claude" | "codex" | undefined,
|
|
89
|
+
label: cleanString(selectorInput.label, "targetPolicy.selector.label", { max: 120 }),
|
|
90
|
+
tags: cleanStringArray(selectorInput.tags, "targetPolicy.selector.tags", { itemMax: 100 }),
|
|
91
|
+
capabilities: cleanStringArray(selectorInput.capabilities, "targetPolicy.selector.capabilities", { itemMax: 100 }),
|
|
92
|
+
},
|
|
93
|
+
ifNoMatch: optionalEnum(input.ifNoMatch, "targetPolicy.ifNoMatch", ["fail", "spawn"] as const, "fail"),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const provider = optionalEnum(input.provider, "targetPolicy.provider", ["claude", "codex"] as const, "codex");
|
|
97
|
+
const model = cleanString(input.model, "targetPolicy.model", { max: 120 });
|
|
98
|
+
const effort = cleanString(input.effort, "targetPolicy.effort", { max: 120 }) as ProviderEffort | undefined;
|
|
99
|
+
const profile = cleanString(input.profile, "targetPolicy.profile", { max: 120 });
|
|
100
|
+
if (profile && !getAgentProfile(profile)) throw new ValidationError("agent profile not found");
|
|
101
|
+
try {
|
|
102
|
+
resolveProviderSelection({ provider, model, effort }, effectiveProviderCatalogList());
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw new ValidationError(errMessage(error));
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
mode,
|
|
108
|
+
provider,
|
|
109
|
+
model,
|
|
110
|
+
effort,
|
|
111
|
+
cwd: cleanString(input.cwd, "targetPolicy.cwd", { max: 500 }),
|
|
112
|
+
workspaceMode: optionalEnum(input.workspaceMode, "targetPolicy.workspaceMode", VALID_WORKSPACE_MODES, "inherit"),
|
|
113
|
+
profile,
|
|
114
|
+
approvalMode: optionalEnum(input.approvalMode, "targetPolicy.approvalMode", ["open", "guarded", "read-only"] as const, "guarded"),
|
|
115
|
+
keepAlive: cleanBool(input.keepAlive, "targetPolicy.keepAlive") ?? false,
|
|
116
|
+
runtimeBudget: normalizeRuntimeBudget(input),
|
|
117
|
+
shutdownAfterMs: typeof input.shutdownAfterMs === "number" && Number.isSafeInteger(input.shutdownAfterMs) && input.shutdownAfterMs >= 0
|
|
118
|
+
? input.shutdownAfterMs
|
|
119
|
+
: undefined,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function normalizeCreateInput(input: CreateAutomationInput): Required<Omit<CreateAutomationInput, "description">> & { description?: string } {
|
|
124
|
+
const schedule = cleanString(input.schedule, "schedule", { required: true, max: 120 })!;
|
|
125
|
+
const timezone = cleanString(input.timezone, "timezone", { max: 80 }) ?? DEFAULT_TIMEZONE;
|
|
126
|
+
validateCron(schedule, timezone);
|
|
127
|
+
return {
|
|
128
|
+
kind: AUTOMATION_KIND,
|
|
129
|
+
name: cleanString(input.name, "name", { required: true, max: 160 })!,
|
|
130
|
+
description: cleanString(input.description, "description", { max: 2000 }),
|
|
131
|
+
enabled: cleanBool(input.enabled, "enabled") ?? true,
|
|
132
|
+
schedule,
|
|
133
|
+
timezone,
|
|
134
|
+
catchUpPolicy: optionalEnum(input.catchUpPolicy, "catchUpPolicy", ["skip", "run_once", "run_all"] as const, DEFAULT_CATCH_UP),
|
|
135
|
+
concurrencyPolicy: optionalEnum(input.concurrencyPolicy, "concurrencyPolicy", ["skip", "queue", "replace"] as const, DEFAULT_CONCURRENCY),
|
|
136
|
+
orchestratorId: cleanString(input.orchestratorId, "orchestratorId", { required: true, max: 160 })!,
|
|
137
|
+
targetPolicy: normalizeTargetPolicy(input.targetPolicy),
|
|
138
|
+
taskTemplate: normalizeTaskTemplate(input.taskTemplate),
|
|
139
|
+
};
|
|
140
|
+
}
|