@kafca/agentdock 0.1.61 → 0.1.63
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/README.md +18 -0
- package/dist/renderer/assets/AutomationList-DR-micIT.js +16 -0
- package/dist/renderer/assets/{Badge-BLI4LHse.js → Badge-COE1Zm2i.js} +1 -1
- package/dist/renderer/assets/{Card-DJeLDdL3.js → Card-F6UNFxnQ.js} +1 -1
- package/dist/renderer/assets/{Config-D1awc81A.js → Config-BKqxfhZi.js} +2 -2
- package/dist/renderer/assets/Dashboard-Bvp7-_2z.js +36 -0
- package/dist/renderer/assets/{EmptyState-CkI1RwPX.js → EmptyState-BwV_J0Og.js} +1 -1
- package/dist/renderer/assets/{HighlightedMarkdown-BUhDlhgW.js → HighlightedMarkdown-CBIBaWyM.js} +1 -1
- package/dist/renderer/assets/{Input-DHYrn-Ne.js → Input-C8LZd4Bi.js} +1 -1
- package/dist/renderer/assets/{KnowledgeDetail-Bij-Xo90.js → KnowledgeDetail-DjXYNMIs.js} +2 -2
- package/dist/renderer/assets/{KnowledgeHome-DmPZ2IlI.js → KnowledgeHome-Cqk5_nJh.js} +2 -2
- package/dist/renderer/assets/{Logs-ISY99heA.js → Logs-B6cXFvW4.js} +2 -2
- package/dist/renderer/assets/{Modal-DAQfz0ot.js → Modal-CAj7soll.js} +3 -3
- package/dist/renderer/assets/{Page-Dw43UYDT.js → Page-DlnMnu4F.js} +1 -1
- package/dist/renderer/assets/{Select-DzmN5OGQ.js → Select-CVay_LUX.js} +1 -1
- package/dist/renderer/assets/{ThreadChat-B5CWqqMK.js → ThreadChat-BxXRpYa1.js} +3 -3
- package/dist/renderer/assets/{Workspace-C5g9LBj9.js → Workspace-YC0pC7cM.js} +1 -1
- package/dist/renderer/assets/{arrow-left-BLXh56Ip.js → arrow-left-Cwm09dxx.js} +1 -1
- package/dist/renderer/assets/{book-open-DhPGbc-7.js → book-open-BwcGC_im.js} +1 -1
- package/dist/renderer/assets/{channels-DfQrmuYO.js → channels-BPytmVCd.js} +2 -2
- package/dist/renderer/assets/{chevron-down-CEFtpPDc.js → chevron-down-2LP9oGHG.js} +1 -1
- package/dist/renderer/assets/en-Bnr4nMzg.js +1 -0
- package/dist/renderer/assets/es-B1GIFh3i.js +1 -0
- package/dist/renderer/assets/{index-BIKL3fTP.css → index-B6WcpkJF.css} +1 -1
- package/dist/renderer/assets/index-eoVERL5K.js +162 -0
- package/dist/renderer/assets/{index-CYhDUAR_.js → index-llCPCiYk.js} +1 -1
- package/dist/renderer/assets/ja-CDWPWqEW.js +1 -0
- package/dist/renderer/assets/{knowledge-uHRQ80sR.js → knowledge-QujAv-Zh.js} +1 -1
- package/dist/renderer/assets/{pencil-BD2Y31YC.js → pencil-0P2INETc.js} +1 -1
- package/dist/renderer/assets/{plus-DDcldp_c.js → plus-tiGRgE2v.js} +1 -1
- package/dist/renderer/assets/{save-CMW_cZI9.js → save-D8U93DNa.js} +1 -1
- package/dist/renderer/assets/{search-BM-0CvJr.js → search-C-9J9J3f.js} +1 -1
- package/dist/renderer/assets/{shield-check-DaMB0FKG.js → shield-check-BCKkPPTJ.js} +1 -1
- package/dist/renderer/assets/{threads-DZgt9tWj.js → threads-Bsrwwnpm.js} +1 -1
- package/dist/renderer/assets/{trash-2-DTXVbC5y.js → trash-2-CP4sn15n.js} +1 -1
- package/dist/renderer/assets/zh-MSddxMDX.js +1 -0
- package/dist/renderer/assets/zh-TW-C_LNWX0o.js +1 -0
- package/dist/renderer/index.html +2 -2
- package/dist-electron/electron/managed-skills/agent-browser/SKILL.md +41 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/authentication.md +83 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/commands.md +50 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/profiling.md +20 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/proxy-support.md +18 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/session-management.md +17 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/snapshot-refs.md +17 -0
- package/dist-electron/electron/managed-skills/agent-browser/references/video-recording.md +21 -0
- package/dist-electron/electron/managed-skills/agent-browser/templates/authenticated-session.sh +11 -0
- package/dist-electron/electron/managed-skills/agent-browser/templates/capture-workflow.sh +12 -0
- package/dist-electron/electron/managed-skills/agent-browser/templates/form-automation.sh +10 -0
- package/dist-electron/electron/managed-skills/condition-trigger/SKILL.md +21 -0
- package/dist-electron/electron/managed-skills/condition-trigger/scripts/register-condition-trigger.sh +106 -0
- package/dist-electron/electron/managed-skills/knowledge-base/SKILL.md +13 -0
- package/dist-electron/electron/managed-skills/knowledge-base/scripts/search-knowledge.sh +80 -0
- package/dist-electron/packages/contracts/src/automations.js +211 -0
- package/dist-electron/packages/contracts/src/index.js +1 -0
- package/dist-electron/packages/core-sdk/src/automations.js +87 -0
- package/dist-electron/packages/core-sdk/src/client.js +101 -0
- package/dist-electron/packages/core-sdk/src/index.js +2 -1
- package/dist-electron/services/local-ai-core/src/acp/store/automation-script-store.js +415 -0
- package/dist-electron/services/local-ai-core/src/acp/store/automation-store.js +829 -0
- package/dist-electron/services/local-ai-core/src/acp/store/local-core-acp-store.js +158 -0
- package/dist-electron/services/local-ai-core/src/acp/store/schema.js +81 -0
- package/dist-electron/services/local-ai-core/src/automation/automation-action-executor.js +144 -0
- package/dist-electron/services/local-ai-core/src/automation/automation-condition-engine.js +79 -0
- package/dist-electron/services/local-ai-core/src/automation/automation-conversation-executor.js +39 -73
- package/dist-electron/services/local-ai-core/src/automation/automation-monitor-service.js +695 -260
- package/dist-electron/services/local-ai-core/src/automation/automation-script-service.js +507 -0
- package/dist-electron/services/local-ai-core/src/automation/automation-service.js +1019 -0
- package/dist-electron/services/local-ai-core/src/automation/automation-trigger-engine.js +141 -0
- package/dist-electron/services/local-ai-core/src/automation/condition-evaluator.js +60 -17
- package/dist-electron/services/local-ai-core/src/automation/legacy-automation-mappers.js +286 -0
- package/dist-electron/services/local-ai-core/src/automation/scripts/anthropic-sandbox-runner.js +792 -0
- package/dist-electron/services/local-ai-core/src/automation/scripts/sandbox-runner.js +2 -0
- package/dist-electron/services/local-ai-core/src/automation/scripts/script-package.js +475 -0
- package/dist-electron/services/local-ai-core/src/automation/scripts/script-protocol-runner.js +266 -0
- package/dist-electron/services/local-ai-core/src/automation/scripts/secret-resolver.js +37 -0
- package/dist-electron/services/local-ai-core/src/channel/weixin/inbound-poller.js +1 -2
- package/dist-electron/services/local-ai-core/src/channel/weixin/transport.js +9 -8
- package/dist-electron/services/local-ai-core/src/cli/lac.js +229 -0
- package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +21 -0
- package/dist-electron/services/local-ai-core/src/runtime/deployment-diagnostics.js +76 -0
- package/dist-electron/services/local-ai-core/src/runtime/handlers/automations-handler.js +322 -0
- package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +20 -0
- package/dist-electron/services/local-ai-core/src/runtime/managed-skill-catalog.js +37 -0
- package/dist-electron/services/local-ai-core/src/runtime/server-helpers.js +14 -4
- package/dist-electron/services/local-ai-core/src/runtime/server-routes.js +75 -0
- package/dist-electron/services/local-ai-core/src/runtime/server.js +21 -0
- package/dist-electron/services/local-ai-core/src/runtime/standalone.js +1 -0
- package/dist-electron/services/local-ai-core/src/scheduler/cron.js +151 -39
- package/dist-electron/services/local-ai-core/src/scheduler/scheduled-conversation-executor.js +2 -4
- package/dist-electron/services/local-ai-core/src/scheduler/scheduled-job-application-service.js +64 -12
- package/dist-electron/services/local-ai-core/src/scheduler/scheduler-service.js +71 -4
- package/dist-electron/services/local-ai-core/src/scheduler/thread-resolution.js +10 -0
- package/dist-electron/services/local-ai-core/src/thread/agent-message-policy.js +16 -1
- package/dist-electron/src/pages/Automation/automation-page-model.js +76 -0
- package/dist-electron/tests/contracts/agent-message-policy.test.js +32 -0
- package/dist-electron/tests/contracts/architecture-docs.test.js +30 -0
- package/dist-electron/tests/contracts/automation-contracts.test.js +154 -0
- package/dist-electron/tests/contracts/automation-renderer-model.test.js +47 -0
- package/dist-electron/tests/electron/automation-action-executor.test.js +75 -0
- package/dist-electron/tests/electron/automation-compatibility.test.js +463 -0
- package/dist-electron/tests/electron/automation-deployment.test.js +66 -0
- package/dist-electron/tests/electron/automation-engine.test.js +264 -0
- package/dist-electron/tests/electron/automation-monitor.test.js +1333 -0
- package/dist-electron/tests/electron/automation-sandbox-runner.test.js +344 -0
- package/dist-electron/tests/electron/automation-script-approval.test.js +553 -0
- package/dist-electron/tests/electron/automation-script-package.test.js +499 -0
- package/dist-electron/tests/electron/automation-script-runner.test.js +120 -0
- package/dist-electron/tests/electron/automation-service.test.js +1078 -0
- package/dist-electron/tests/electron/automation-store.test.js +593 -0
- package/dist-electron/tests/electron/condition-trigger-skill.test.js +93 -0
- package/dist-electron/tests/electron/core-client.test.js +99 -0
- package/dist-electron/tests/electron/lac-cli.test.js +110 -0
- package/dist-electron/tests/electron/workspace-task-store.test.js +25 -0
- package/dist-electron/tests/integration/automation-routes.test.js +186 -0
- package/dist-electron/tests/integration/automation-sandbox-runtime.test.js +92 -0
- package/package.json +4 -3
- package/dist/renderer/assets/CronList-CupZoNwW.js +0 -1
- package/dist/renderer/assets/Dashboard-nLZnOtcs.js +0 -41
- package/dist/renderer/assets/MonitorList-BlX82rXF.js +0 -1
- package/dist/renderer/assets/en-CmAr_0e6.js +0 -1
- package/dist/renderer/assets/es-Cs9x52ca.js +0 -1
- package/dist/renderer/assets/index-Dd00oqgY.js +0 -167
- package/dist/renderer/assets/ja-DtvtBRTW.js +0 -1
- package/dist/renderer/assets/play-DBJ8wsSy.js +0 -6
- package/dist/renderer/assets/zh-CcJ9vBZj.js +0 -1
- package/dist/renderer/assets/zh-TW-elI72AC1.js +0 -1
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocalAutomationStore = void 0;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const superai_contracts_1 = require("../../../../../packages/contracts/src/index.js");
|
|
6
|
+
const AUTOMATION_COLUMNS = `
|
|
7
|
+
id, workspace_id, title, enabled, health, blocked_reason, activation_json, condition_json, action_json,
|
|
8
|
+
delivery_json, policies_json, last_successful_match, last_evaluation_at, last_triggered_at,
|
|
9
|
+
consecutive_evaluation_failures, next_check_at, origin_kind, created_at, updated_at, legacy_metadata_json
|
|
10
|
+
`;
|
|
11
|
+
const EVALUATION_COLUMNS = `
|
|
12
|
+
id, automation_id, status, activation_kind, script_version_id, started_at, finished_at, evaluation_json
|
|
13
|
+
`;
|
|
14
|
+
const RUN_COLUMNS = 'id, automation_id, evaluation_id, status, created_at, run_json';
|
|
15
|
+
class LocalAutomationStore {
|
|
16
|
+
db;
|
|
17
|
+
constructor(db) {
|
|
18
|
+
this.db = db;
|
|
19
|
+
}
|
|
20
|
+
list(workspaceId) {
|
|
21
|
+
const rows = this.db.prepare(`
|
|
22
|
+
SELECT ${AUTOMATION_COLUMNS}
|
|
23
|
+
FROM automations
|
|
24
|
+
${workspaceId ? 'WHERE workspace_id = ?' : ''}
|
|
25
|
+
ORDER BY updated_at DESC
|
|
26
|
+
`).all(...(workspaceId ? [workspaceId] : []));
|
|
27
|
+
return rows.map((row) => this.toDefinition(row));
|
|
28
|
+
}
|
|
29
|
+
get(id) {
|
|
30
|
+
const row = this.db.prepare(`SELECT ${AUTOMATION_COLUMNS} FROM automations WHERE id = ?`)
|
|
31
|
+
.get(id);
|
|
32
|
+
return row ? this.toDefinition(row) : undefined;
|
|
33
|
+
}
|
|
34
|
+
create(input) {
|
|
35
|
+
return this.createWithOrigin(input, 'native');
|
|
36
|
+
}
|
|
37
|
+
createTrusted(input) {
|
|
38
|
+
return this.createWithOrigin(input, input.originKind, input.legacyMetadata);
|
|
39
|
+
}
|
|
40
|
+
createWithOrigin(input, originKind, legacyMetadata) {
|
|
41
|
+
const now = new Date().toISOString();
|
|
42
|
+
const uuid = (0, node_crypto_1.randomUUID)();
|
|
43
|
+
const id = originKind === 'scheduled-job'
|
|
44
|
+
? uuid.split('-')[0] || uuid
|
|
45
|
+
: originKind === 'automation-monitor'
|
|
46
|
+
? `monitor:${uuid}`
|
|
47
|
+
: `automation:${uuid}`;
|
|
48
|
+
const definition = normalizeDefinition({
|
|
49
|
+
id,
|
|
50
|
+
workspaceId: input.workspaceId,
|
|
51
|
+
title: input.title,
|
|
52
|
+
enabled: input.enabled,
|
|
53
|
+
health: 'healthy',
|
|
54
|
+
activation: input.activation,
|
|
55
|
+
condition: input.condition,
|
|
56
|
+
action: input.action,
|
|
57
|
+
delivery: input.delivery,
|
|
58
|
+
policies: input.policies,
|
|
59
|
+
consecutiveEvaluationFailures: 0,
|
|
60
|
+
originKind,
|
|
61
|
+
...(legacyMetadata ? { legacyMetadata } : {}),
|
|
62
|
+
createdAt: now,
|
|
63
|
+
updatedAt: now,
|
|
64
|
+
});
|
|
65
|
+
this.insertDefinition(definition);
|
|
66
|
+
return this.get(definition.id);
|
|
67
|
+
}
|
|
68
|
+
update(id, input) {
|
|
69
|
+
return this.updateWithMetadata(id, input);
|
|
70
|
+
}
|
|
71
|
+
updateTrusted(id, input) {
|
|
72
|
+
return this.updateWithMetadata(id, input, input.legacyMetadata);
|
|
73
|
+
}
|
|
74
|
+
updateWithMetadata(id, input, legacyMetadata) {
|
|
75
|
+
const existing = this.requireDefinition(id);
|
|
76
|
+
const candidate = { ...existing };
|
|
77
|
+
if (input.title !== undefined)
|
|
78
|
+
candidate.title = input.title;
|
|
79
|
+
if (input.enabled !== undefined)
|
|
80
|
+
candidate.enabled = input.enabled;
|
|
81
|
+
if (input.activation !== undefined)
|
|
82
|
+
candidate.activation = input.activation;
|
|
83
|
+
if (input.condition !== undefined)
|
|
84
|
+
candidate.condition = input.condition;
|
|
85
|
+
if (input.action !== undefined)
|
|
86
|
+
candidate.action = input.action;
|
|
87
|
+
if (input.delivery !== undefined)
|
|
88
|
+
candidate.delivery = input.delivery;
|
|
89
|
+
if (input.policies !== undefined)
|
|
90
|
+
candidate.policies = input.policies;
|
|
91
|
+
if (legacyMetadata !== undefined)
|
|
92
|
+
candidate.legacyMetadata = legacyMetadata;
|
|
93
|
+
candidate.updatedAt = new Date().toISOString();
|
|
94
|
+
const definition = normalizeDefinition(candidate);
|
|
95
|
+
this.writeDefinition(definition);
|
|
96
|
+
return this.get(id);
|
|
97
|
+
}
|
|
98
|
+
updateState(id, input) {
|
|
99
|
+
const existing = this.requireDefinition(id);
|
|
100
|
+
const candidate = { ...existing, updatedAt: new Date().toISOString() };
|
|
101
|
+
this.applyNullable(candidate, input, 'blockedReason');
|
|
102
|
+
this.applyNullable(candidate, input, 'lastSuccessfulMatch');
|
|
103
|
+
this.applyNullable(candidate, input, 'lastEvaluationAt');
|
|
104
|
+
this.applyNullable(candidate, input, 'lastTriggeredAt');
|
|
105
|
+
if (input.health !== undefined)
|
|
106
|
+
candidate.health = input.health;
|
|
107
|
+
if (input.consecutiveEvaluationFailures !== undefined) {
|
|
108
|
+
candidate.consecutiveEvaluationFailures = input.consecutiveEvaluationFailures;
|
|
109
|
+
}
|
|
110
|
+
if (candidate.health === 'healthy') {
|
|
111
|
+
delete candidate.blockedReason;
|
|
112
|
+
}
|
|
113
|
+
else if (candidate.health === 'blocked' &&
|
|
114
|
+
(typeof candidate.blockedReason !== 'string' || !candidate.blockedReason.trim())) {
|
|
115
|
+
throw new Error('Blocked automation health requires a non-empty blocked reason.');
|
|
116
|
+
}
|
|
117
|
+
const definition = normalizeDefinition(candidate);
|
|
118
|
+
const nextCheckAt = Object.prototype.hasOwnProperty.call(input, 'nextCheckAt')
|
|
119
|
+
? input.nextCheckAt ?? null
|
|
120
|
+
: this.getNextCheckAt(id);
|
|
121
|
+
const canonicalNextCheckAt = nextCheckAt === null
|
|
122
|
+
? null
|
|
123
|
+
: assertIsoTimestamp(nextCheckAt, 'Automation nextCheckAt');
|
|
124
|
+
this.writeDefinition(definition, canonicalNextCheckAt);
|
|
125
|
+
return this.get(id);
|
|
126
|
+
}
|
|
127
|
+
delete(id) {
|
|
128
|
+
const result = this.db.prepare('DELETE FROM automations WHERE id = ?').run(id);
|
|
129
|
+
if (result.changes === 0)
|
|
130
|
+
throw new Error(`Automation not found: ${id}`);
|
|
131
|
+
return { deleted: true };
|
|
132
|
+
}
|
|
133
|
+
createEvaluation(automationId, input) {
|
|
134
|
+
this.requireDefinition(automationId);
|
|
135
|
+
const evaluation = validateEvaluation({
|
|
136
|
+
id: `automation-evaluation:${(0, node_crypto_1.randomUUID)()}`,
|
|
137
|
+
automationId,
|
|
138
|
+
status: 'running',
|
|
139
|
+
activationKind: input.activationKind,
|
|
140
|
+
...(input.scriptVersionId === undefined ? {} : { scriptVersionId: input.scriptVersionId }),
|
|
141
|
+
startedAt: input.startedAt,
|
|
142
|
+
});
|
|
143
|
+
this.insertEvaluation(evaluation);
|
|
144
|
+
return this.getEvaluation(evaluation.id);
|
|
145
|
+
}
|
|
146
|
+
finishEvaluation(evaluationId, input) {
|
|
147
|
+
const existing = this.requireEvaluation(evaluationId);
|
|
148
|
+
if (existing.status !== 'running')
|
|
149
|
+
throw new Error(`Automation evaluation already finished: ${evaluationId}`);
|
|
150
|
+
const evaluation = validateEvaluation({
|
|
151
|
+
id: existing.id,
|
|
152
|
+
automationId: existing.automationId,
|
|
153
|
+
activationKind: existing.activationKind,
|
|
154
|
+
...(existing.scriptVersionId === undefined ? {} : { scriptVersionId: existing.scriptVersionId }),
|
|
155
|
+
startedAt: existing.startedAt,
|
|
156
|
+
status: 'finished',
|
|
157
|
+
...pickEvaluationFinishFields(input),
|
|
158
|
+
});
|
|
159
|
+
if (evaluation.status !== 'finished')
|
|
160
|
+
throw new Error('Finished automation evaluation validation failed.');
|
|
161
|
+
this.db.prepare(`
|
|
162
|
+
UPDATE automation_evaluations
|
|
163
|
+
SET status = ?, finished_at = ?, evaluation_json = ?
|
|
164
|
+
WHERE id = ?
|
|
165
|
+
`).run(evaluation.status, evaluation.finishedAt, JSON.stringify(evaluation), evaluationId);
|
|
166
|
+
return this.getEvaluation(evaluationId);
|
|
167
|
+
}
|
|
168
|
+
listEvaluations(automationId) {
|
|
169
|
+
const rows = this.db.prepare(`
|
|
170
|
+
SELECT ${EVALUATION_COLUMNS}
|
|
171
|
+
FROM automation_evaluations
|
|
172
|
+
WHERE automation_id = ?
|
|
173
|
+
ORDER BY started_at DESC, id DESC
|
|
174
|
+
`).all(automationId);
|
|
175
|
+
return rows.map((row) => this.toEvaluation(row));
|
|
176
|
+
}
|
|
177
|
+
getLatestEvaluationWithState(automationId) {
|
|
178
|
+
const row = this.db.prepare(`
|
|
179
|
+
SELECT ${EVALUATION_COLUMNS}
|
|
180
|
+
FROM automation_evaluations
|
|
181
|
+
WHERE automation_id = ?
|
|
182
|
+
AND status = 'finished'
|
|
183
|
+
AND json_type(evaluation_json, '$.nextState') = 'object'
|
|
184
|
+
ORDER BY started_at DESC, id DESC
|
|
185
|
+
LIMIT 1
|
|
186
|
+
`).get(automationId);
|
|
187
|
+
return row ? this.toEvaluation(row) : undefined;
|
|
188
|
+
}
|
|
189
|
+
createRun(automationId, evaluationId, input = {}) {
|
|
190
|
+
const automation = this.requireDefinition(automationId);
|
|
191
|
+
const evaluation = this.requireEvaluation(evaluationId);
|
|
192
|
+
if (evaluation.automationId !== automationId) {
|
|
193
|
+
throw new Error(`Automation evaluation ${evaluationId} does not belong to automation ${automationId}.`);
|
|
194
|
+
}
|
|
195
|
+
if (evaluation.status !== 'finished' ||
|
|
196
|
+
evaluation.conditionOutcome !== 'matched' ||
|
|
197
|
+
evaluation.triggerDecision !== 'triggered') {
|
|
198
|
+
throw new Error('Automation action runs require a finished evaluation with matched outcome and triggered decision.');
|
|
199
|
+
}
|
|
200
|
+
if (this.getRunByEvaluation(evaluationId)) {
|
|
201
|
+
throw new Error(`Automation evaluation already has an action run: ${evaluationId}`);
|
|
202
|
+
}
|
|
203
|
+
const mutableFields = pickRunMutableFields(input);
|
|
204
|
+
const run = validateRun({
|
|
205
|
+
id: `automation-run:${(0, node_crypto_1.randomUUID)()}`,
|
|
206
|
+
automationId,
|
|
207
|
+
evaluationId,
|
|
208
|
+
executionMode: automation.action.executionMode,
|
|
209
|
+
createdAt: new Date().toISOString(),
|
|
210
|
+
status: 'queued',
|
|
211
|
+
...mutableFields,
|
|
212
|
+
});
|
|
213
|
+
this.db.prepare(`
|
|
214
|
+
INSERT INTO automation_runs (id, automation_id, evaluation_id, status, created_at, run_json)
|
|
215
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
216
|
+
`).run(run.id, run.automationId, run.evaluationId, run.status, run.createdAt, JSON.stringify(run));
|
|
217
|
+
return this.getRun(run.id);
|
|
218
|
+
}
|
|
219
|
+
updateRun(runId, input) {
|
|
220
|
+
const existing = this.requireRun(runId);
|
|
221
|
+
const run = validateRun({ ...existing, ...pickRunMutableFields(input) });
|
|
222
|
+
this.db.prepare('UPDATE automation_runs SET status = ?, run_json = ? WHERE id = ?')
|
|
223
|
+
.run(run.status, JSON.stringify(run), runId);
|
|
224
|
+
return this.getRun(runId);
|
|
225
|
+
}
|
|
226
|
+
listRuns(automationId) {
|
|
227
|
+
const rows = this.db.prepare(`
|
|
228
|
+
SELECT ${RUN_COLUMNS}
|
|
229
|
+
FROM automation_runs
|
|
230
|
+
WHERE automation_id = ?
|
|
231
|
+
ORDER BY created_at DESC, id DESC
|
|
232
|
+
`).all(automationId);
|
|
233
|
+
return rows.map((row) => this.toRun(row));
|
|
234
|
+
}
|
|
235
|
+
reconcileInterruptedRuns(reason, finishedAt) {
|
|
236
|
+
const interruptedAt = assertIsoTimestamp(finishedAt, 'Automation interrupted run finishedAt');
|
|
237
|
+
const rows = this.db.prepare(`
|
|
238
|
+
SELECT ${RUN_COLUMNS}
|
|
239
|
+
FROM automation_runs
|
|
240
|
+
WHERE status IN ('queued', 'running')
|
|
241
|
+
ORDER BY created_at, id
|
|
242
|
+
`).all();
|
|
243
|
+
return rows.map((row) => this.updateRun(row.id, {
|
|
244
|
+
status: 'failed',
|
|
245
|
+
deliveryStatus: 'failed',
|
|
246
|
+
error: reason,
|
|
247
|
+
finishedAt: interruptedAt,
|
|
248
|
+
}));
|
|
249
|
+
}
|
|
250
|
+
importLegacyRecords() {
|
|
251
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
252
|
+
try {
|
|
253
|
+
let scheduled = 0;
|
|
254
|
+
let monitors = 0;
|
|
255
|
+
const jobs = this.db.prepare(`
|
|
256
|
+
SELECT id, workspace_id, platform, route_type, route_config, execution_mode, trigger_type, cron_expr, run_at,
|
|
257
|
+
prompt_template, description, enabled, concurrency_policy, created_at, updated_at, last_run_at,
|
|
258
|
+
last_status, last_error
|
|
259
|
+
FROM scheduled_jobs
|
|
260
|
+
`).all();
|
|
261
|
+
for (const row of jobs) {
|
|
262
|
+
try {
|
|
263
|
+
scheduled += this.importScheduledJob(row);
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
throw withContext(`Scheduled job ${row.id} failed legacy import`, error);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const legacyMonitors = this.db.prepare(`
|
|
270
|
+
SELECT id, workspace_id, title, source_type, source_config_json, condition_json, prompt_template, platform,
|
|
271
|
+
route_type, route_config, execution_mode, enabled, cooldown_ms, concurrency_policy, last_state_json,
|
|
272
|
+
created_at, updated_at, last_triggered_at, last_status, last_error
|
|
273
|
+
FROM automation_monitors
|
|
274
|
+
`).all();
|
|
275
|
+
for (const row of legacyMonitors) {
|
|
276
|
+
try {
|
|
277
|
+
monitors += this.importMonitor(row);
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
throw withContext(`Automation monitor ${row.id} failed legacy import`, error);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
this.db.exec('COMMIT');
|
|
284
|
+
return { scheduled, monitors };
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
// Legacy migration is intentionally atomic and fail-fast; Task 4 reports this as degraded startup.
|
|
288
|
+
this.db.exec('ROLLBACK');
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
pruneEvaluations(now) {
|
|
293
|
+
if (Number.isNaN(now.getTime()))
|
|
294
|
+
throw new Error('Evaluation retention time must be a valid date.');
|
|
295
|
+
const cutoff = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1_000).toISOString();
|
|
296
|
+
const result = this.db.prepare(`
|
|
297
|
+
WITH ranked AS (
|
|
298
|
+
SELECT id, automation_id, started_at,
|
|
299
|
+
ROW_NUMBER() OVER (PARTITION BY automation_id ORDER BY started_at DESC, id DESC) AS position
|
|
300
|
+
FROM automation_evaluations
|
|
301
|
+
), state_ranked AS (
|
|
302
|
+
SELECT id,
|
|
303
|
+
ROW_NUMBER() OVER (PARTITION BY automation_id ORDER BY started_at DESC, id DESC) AS position
|
|
304
|
+
FROM automation_evaluations
|
|
305
|
+
WHERE json_type(evaluation_json, '$.nextState') IS NOT NULL
|
|
306
|
+
), latest_state AS (
|
|
307
|
+
SELECT id FROM state_ranked WHERE position = 1
|
|
308
|
+
)
|
|
309
|
+
DELETE FROM automation_evaluations
|
|
310
|
+
WHERE id IN (SELECT id FROM ranked WHERE started_at < ? OR position > 1000)
|
|
311
|
+
AND id NOT IN (SELECT id FROM latest_state)
|
|
312
|
+
AND NOT EXISTS (
|
|
313
|
+
SELECT 1 FROM automation_runs WHERE automation_runs.evaluation_id = automation_evaluations.id
|
|
314
|
+
)
|
|
315
|
+
`).run(cutoff);
|
|
316
|
+
return Number(result.changes);
|
|
317
|
+
}
|
|
318
|
+
getEvaluation(id) {
|
|
319
|
+
const row = this.db.prepare(`SELECT ${EVALUATION_COLUMNS} FROM automation_evaluations WHERE id = ?`)
|
|
320
|
+
.get(id);
|
|
321
|
+
return row ? this.toEvaluation(row) : undefined;
|
|
322
|
+
}
|
|
323
|
+
getRun(id) {
|
|
324
|
+
const row = this.db.prepare(`SELECT ${RUN_COLUMNS} FROM automation_runs WHERE id = ?`)
|
|
325
|
+
.get(id);
|
|
326
|
+
return row ? this.toRun(row) : undefined;
|
|
327
|
+
}
|
|
328
|
+
getRunByEvaluation(evaluationId) {
|
|
329
|
+
const row = this.db.prepare(`SELECT ${RUN_COLUMNS} FROM automation_runs WHERE evaluation_id = ?`)
|
|
330
|
+
.get(evaluationId);
|
|
331
|
+
return row ? this.toRun(row) : undefined;
|
|
332
|
+
}
|
|
333
|
+
requireDefinition(id) {
|
|
334
|
+
const definition = this.get(id);
|
|
335
|
+
if (!definition)
|
|
336
|
+
throw new Error(`Automation not found: ${id}`);
|
|
337
|
+
return definition;
|
|
338
|
+
}
|
|
339
|
+
requireEvaluation(id) {
|
|
340
|
+
const evaluation = this.getEvaluation(id);
|
|
341
|
+
if (!evaluation)
|
|
342
|
+
throw new Error(`Automation evaluation not found: ${id}`);
|
|
343
|
+
return evaluation;
|
|
344
|
+
}
|
|
345
|
+
requireRun(id) {
|
|
346
|
+
const run = this.getRun(id);
|
|
347
|
+
if (!run)
|
|
348
|
+
throw new Error(`Automation run not found: ${id}`);
|
|
349
|
+
return run;
|
|
350
|
+
}
|
|
351
|
+
insertDefinition(definition, nextCheckAt = null, ignoreConflict = false) {
|
|
352
|
+
const canonicalDefinition = normalizeDefinition(definition);
|
|
353
|
+
const canonicalNextCheckAt = nextCheckAt === null
|
|
354
|
+
? null
|
|
355
|
+
: assertIsoTimestamp(nextCheckAt, 'Automation nextCheckAt');
|
|
356
|
+
const result = this.db.prepare(`
|
|
357
|
+
INSERT INTO automations (
|
|
358
|
+
id, workspace_id, title, enabled, health, blocked_reason, activation_json, condition_json, action_json,
|
|
359
|
+
delivery_json, policies_json, last_successful_match, last_evaluation_at, last_triggered_at,
|
|
360
|
+
consecutive_evaluation_failures, next_check_at, origin_kind, created_at, updated_at, legacy_metadata_json
|
|
361
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
362
|
+
${ignoreConflict ? 'ON CONFLICT(id) DO NOTHING' : ''}
|
|
363
|
+
`).run(...this.definitionValues(canonicalDefinition, canonicalNextCheckAt));
|
|
364
|
+
return Number(result.changes);
|
|
365
|
+
}
|
|
366
|
+
insertEvaluation(evaluation, ignoreConflict = false) {
|
|
367
|
+
const finishedAt = evaluation.status === 'finished' ? evaluation.finishedAt : null;
|
|
368
|
+
const result = this.db.prepare(`
|
|
369
|
+
INSERT INTO automation_evaluations (
|
|
370
|
+
id, automation_id, status, activation_kind, script_version_id, started_at, finished_at, evaluation_json
|
|
371
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
372
|
+
${ignoreConflict ? 'ON CONFLICT(id) DO NOTHING' : ''}
|
|
373
|
+
`).run(evaluation.id, evaluation.automationId, evaluation.status, evaluation.activationKind, evaluation.scriptVersionId ?? null, evaluation.startedAt, finishedAt, JSON.stringify(evaluation));
|
|
374
|
+
return Number(result.changes);
|
|
375
|
+
}
|
|
376
|
+
writeDefinition(definition, nextCheckAt = this.getNextCheckAt(definition.id)) {
|
|
377
|
+
const canonicalDefinition = normalizeDefinition(definition);
|
|
378
|
+
const canonicalNextCheckAt = nextCheckAt === null
|
|
379
|
+
? null
|
|
380
|
+
: assertIsoTimestamp(nextCheckAt, 'Automation nextCheckAt');
|
|
381
|
+
this.db.prepare(`
|
|
382
|
+
UPDATE automations SET
|
|
383
|
+
workspace_id = ?, title = ?, enabled = ?, health = ?, blocked_reason = ?, activation_json = ?,
|
|
384
|
+
condition_json = ?, action_json = ?, delivery_json = ?, policies_json = ?, last_successful_match = ?,
|
|
385
|
+
last_evaluation_at = ?, last_triggered_at = ?, consecutive_evaluation_failures = ?, next_check_at = ?,
|
|
386
|
+
origin_kind = ?, created_at = ?, updated_at = ?, legacy_metadata_json = ?
|
|
387
|
+
WHERE id = ?
|
|
388
|
+
`).run(...this.definitionValues(canonicalDefinition, canonicalNextCheckAt).slice(1), canonicalDefinition.id);
|
|
389
|
+
}
|
|
390
|
+
definitionValues(definition, nextCheckAt) {
|
|
391
|
+
return [
|
|
392
|
+
definition.id,
|
|
393
|
+
definition.workspaceId,
|
|
394
|
+
definition.title,
|
|
395
|
+
definition.enabled ? 1 : 0,
|
|
396
|
+
definition.health,
|
|
397
|
+
definition.blockedReason ?? null,
|
|
398
|
+
JSON.stringify(definition.activation),
|
|
399
|
+
JSON.stringify(definition.condition),
|
|
400
|
+
JSON.stringify(definition.action),
|
|
401
|
+
JSON.stringify(definition.delivery),
|
|
402
|
+
JSON.stringify(definition.policies),
|
|
403
|
+
definition.lastSuccessfulMatch === undefined ? null : definition.lastSuccessfulMatch ? 1 : 0,
|
|
404
|
+
definition.lastEvaluationAt ?? null,
|
|
405
|
+
definition.lastTriggeredAt ?? null,
|
|
406
|
+
definition.consecutiveEvaluationFailures,
|
|
407
|
+
nextCheckAt,
|
|
408
|
+
definition.originKind ?? 'native',
|
|
409
|
+
definition.createdAt,
|
|
410
|
+
definition.updatedAt,
|
|
411
|
+
definition.legacyMetadata ? JSON.stringify(definition.legacyMetadata) : null,
|
|
412
|
+
];
|
|
413
|
+
}
|
|
414
|
+
getNextCheckAt(id) {
|
|
415
|
+
const row = this.db.prepare('SELECT next_check_at FROM automations WHERE id = ?').get(id);
|
|
416
|
+
return row?.next_check_at ?? null;
|
|
417
|
+
}
|
|
418
|
+
toDefinition(row) {
|
|
419
|
+
try {
|
|
420
|
+
return normalizeDefinition({
|
|
421
|
+
id: row.id,
|
|
422
|
+
workspaceId: row.workspace_id,
|
|
423
|
+
title: row.title,
|
|
424
|
+
enabled: row.enabled === 1,
|
|
425
|
+
health: row.health,
|
|
426
|
+
...(row.blocked_reason ? { blockedReason: row.blocked_reason } : {}),
|
|
427
|
+
activation: parseStoredJson(row.activation_json, `Automation ${row.id} activation`),
|
|
428
|
+
condition: parseStoredJson(row.condition_json, `Automation ${row.id} condition`),
|
|
429
|
+
action: parseStoredJson(row.action_json, `Automation ${row.id} action`),
|
|
430
|
+
delivery: parseStoredJson(row.delivery_json, `Automation ${row.id} delivery`),
|
|
431
|
+
policies: parseStoredJson(row.policies_json, `Automation ${row.id} policies`),
|
|
432
|
+
...(row.last_successful_match === null ? {} : { lastSuccessfulMatch: row.last_successful_match === 1 }),
|
|
433
|
+
...(row.last_evaluation_at ? { lastEvaluationAt: row.last_evaluation_at } : {}),
|
|
434
|
+
...(row.last_triggered_at ? { lastTriggeredAt: row.last_triggered_at } : {}),
|
|
435
|
+
consecutiveEvaluationFailures: row.consecutive_evaluation_failures,
|
|
436
|
+
originKind: row.origin_kind,
|
|
437
|
+
...(row.legacy_metadata_json
|
|
438
|
+
? { legacyMetadata: parseStoredJson(row.legacy_metadata_json, `Automation ${row.id} legacy metadata`) }
|
|
439
|
+
: {}),
|
|
440
|
+
createdAt: row.created_at,
|
|
441
|
+
updatedAt: row.updated_at,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
throw withContext(`Automation ${row.id} contains invalid persisted data`, error);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
toEvaluation(row) {
|
|
449
|
+
try {
|
|
450
|
+
const evaluation = validateEvaluation(parseStoredJson(row.evaluation_json, `Automation evaluation ${row.id}`));
|
|
451
|
+
const finishedAt = evaluation.status === 'finished' ? evaluation.finishedAt : null;
|
|
452
|
+
assertDuplicatedField('id', row.id, evaluation.id);
|
|
453
|
+
assertDuplicatedField('automationId', row.automation_id, evaluation.automationId);
|
|
454
|
+
assertDuplicatedField('status', row.status, evaluation.status);
|
|
455
|
+
assertDuplicatedField('activationKind', row.activation_kind, evaluation.activationKind);
|
|
456
|
+
assertDuplicatedField('scriptVersionId', row.script_version_id, evaluation.scriptVersionId ?? null);
|
|
457
|
+
assertDuplicatedField('startedAt', row.started_at, evaluation.startedAt);
|
|
458
|
+
assertDuplicatedField('finishedAt', row.finished_at, finishedAt);
|
|
459
|
+
return evaluation;
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
throw withContext(`Automation evaluation ${row.id} contains invalid persisted data`, error);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
toRun(row) {
|
|
466
|
+
try {
|
|
467
|
+
const run = validateRun(parseStoredJson(row.run_json, `Automation run ${row.id}`));
|
|
468
|
+
assertDuplicatedField('id', row.id, run.id);
|
|
469
|
+
assertDuplicatedField('automationId', row.automation_id, run.automationId);
|
|
470
|
+
assertDuplicatedField('evaluationId', row.evaluation_id, run.evaluationId);
|
|
471
|
+
assertDuplicatedField('status', row.status, run.status);
|
|
472
|
+
assertDuplicatedField('createdAt', row.created_at, run.createdAt);
|
|
473
|
+
return run;
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
throw withContext(`Automation run ${row.id} contains invalid persisted data`, error);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
importScheduledJob(row) {
|
|
480
|
+
const consumedOnce = row.trigger_type === 'once' && Boolean(row.last_run_at || row.last_status);
|
|
481
|
+
const activation = row.trigger_type === 'once'
|
|
482
|
+
? { kind: 'once', runAt: row.run_at }
|
|
483
|
+
: { kind: 'cron', expression: row.cron_expr, timezone: 'UTC' };
|
|
484
|
+
const definition = normalizeDefinition({
|
|
485
|
+
id: row.id,
|
|
486
|
+
workspaceId: row.workspace_id,
|
|
487
|
+
title: row.description.trim() || row.prompt_template.trim(),
|
|
488
|
+
enabled: consumedOnce ? false : row.enabled === 1,
|
|
489
|
+
health: 'healthy',
|
|
490
|
+
activation,
|
|
491
|
+
condition: { kind: 'always' },
|
|
492
|
+
action: { kind: 'agent-prompt', promptTemplate: row.prompt_template, executionMode: row.execution_mode },
|
|
493
|
+
delivery: {
|
|
494
|
+
platform: row.platform,
|
|
495
|
+
route: parseStoredJson(row.route_config, `Scheduled job ${row.id} route`),
|
|
496
|
+
},
|
|
497
|
+
policies: { concurrency: 'skip-if-running', cooldownMs: 0 },
|
|
498
|
+
...(row.last_status ? { lastSuccessfulMatch: row.last_status === 'succeeded' } : {}),
|
|
499
|
+
...(row.last_run_at || row.last_status
|
|
500
|
+
? { lastEvaluationAt: row.last_run_at || row.updated_at }
|
|
501
|
+
: {}),
|
|
502
|
+
...(row.last_run_at ? { lastTriggeredAt: row.last_run_at } : {}),
|
|
503
|
+
consecutiveEvaluationFailures: row.last_status === 'failed' ? 1 : 0,
|
|
504
|
+
originKind: 'scheduled-job',
|
|
505
|
+
legacyMetadata: { scheduledDescription: row.description },
|
|
506
|
+
createdAt: row.created_at,
|
|
507
|
+
updatedAt: row.updated_at,
|
|
508
|
+
});
|
|
509
|
+
return this.insertDefinition(definition, row.trigger_type === 'once' && !consumedOnce ? row.run_at : null, true);
|
|
510
|
+
}
|
|
511
|
+
importMonitor(row) {
|
|
512
|
+
const sourceConfig = parseStoredJson(row.source_config_json, `Automation monitor ${row.id} source config`);
|
|
513
|
+
const legacyCondition = asRecord(parseStoredJson(row.condition_json, `Automation monitor ${row.id} condition`), `Automation monitor ${row.id} condition`);
|
|
514
|
+
const lastState = row.last_state_json
|
|
515
|
+
? asRecord(parseStoredJson(row.last_state_json, `Automation monitor ${row.id} state`), `Automation monitor ${row.id} state`)
|
|
516
|
+
: undefined;
|
|
517
|
+
const expression = typeof legacyCondition.expression === 'string' && legacyCondition.expression.trim()
|
|
518
|
+
? legacyCondition.expression.trim()
|
|
519
|
+
: `${String(legacyCondition.metric || '').trim()} ${String(legacyCondition.operator || '').trim()} ${JSON.stringify(legacyCondition.value)}`;
|
|
520
|
+
const definition = normalizeDefinition({
|
|
521
|
+
id: row.id,
|
|
522
|
+
workspaceId: row.workspace_id,
|
|
523
|
+
title: row.title,
|
|
524
|
+
enabled: row.enabled === 1,
|
|
525
|
+
health: 'healthy',
|
|
526
|
+
activation: { kind: 'provider-event', sourceType: row.source_type, sourceConfig },
|
|
527
|
+
condition: { kind: 'expression', expression },
|
|
528
|
+
action: { kind: 'agent-prompt', promptTemplate: row.prompt_template, executionMode: row.execution_mode },
|
|
529
|
+
delivery: {
|
|
530
|
+
platform: row.platform,
|
|
531
|
+
route: parseStoredJson(row.route_config, `Automation monitor ${row.id} route`),
|
|
532
|
+
},
|
|
533
|
+
policies: { concurrency: 'skip-if-running', cooldownMs: row.cooldown_ms },
|
|
534
|
+
...(typeof lastState?.lastSuccessfulMatch === 'boolean'
|
|
535
|
+
? { lastSuccessfulMatch: lastState.lastSuccessfulMatch }
|
|
536
|
+
: {}),
|
|
537
|
+
...(row.last_triggered_at ? { lastEvaluationAt: row.last_triggered_at, lastTriggeredAt: row.last_triggered_at } : {}),
|
|
538
|
+
consecutiveEvaluationFailures: row.last_status === 'failed' ? 1 : 0,
|
|
539
|
+
originKind: 'automation-monitor',
|
|
540
|
+
createdAt: row.created_at,
|
|
541
|
+
updatedAt: row.updated_at,
|
|
542
|
+
});
|
|
543
|
+
const imported = this.insertDefinition(definition, null, true);
|
|
544
|
+
if (lastState && this.get(row.id)?.originKind === 'automation-monitor') {
|
|
545
|
+
const stateTimestamp = row.last_triggered_at ?? row.updated_at;
|
|
546
|
+
const stateEvaluation = validateEvaluation({
|
|
547
|
+
id: `automation-evaluation:legacy-monitor-state:${row.id}`,
|
|
548
|
+
automationId: row.id,
|
|
549
|
+
status: 'finished',
|
|
550
|
+
activationKind: 'provider-event',
|
|
551
|
+
startedAt: stateTimestamp,
|
|
552
|
+
conditionOutcome: 'skipped',
|
|
553
|
+
triggerDecision: 'not_evaluated',
|
|
554
|
+
finishedAt: stateTimestamp,
|
|
555
|
+
resultSummary: 'Imported legacy monitor state.',
|
|
556
|
+
nextState: lastState,
|
|
557
|
+
});
|
|
558
|
+
this.insertEvaluation(stateEvaluation, true);
|
|
559
|
+
}
|
|
560
|
+
return imported;
|
|
561
|
+
}
|
|
562
|
+
applyNullable(candidate, input, key) {
|
|
563
|
+
if (!Object.prototype.hasOwnProperty.call(input, key))
|
|
564
|
+
return;
|
|
565
|
+
const value = input[key];
|
|
566
|
+
if (value === null || value === undefined)
|
|
567
|
+
delete candidate[key];
|
|
568
|
+
else
|
|
569
|
+
candidate[key] = value;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
exports.LocalAutomationStore = LocalAutomationStore;
|
|
573
|
+
function pickEvaluationFinishFields(input) {
|
|
574
|
+
const fields = {
|
|
575
|
+
conditionOutcome: input.conditionOutcome,
|
|
576
|
+
triggerDecision: input.triggerDecision,
|
|
577
|
+
finishedAt: input.finishedAt,
|
|
578
|
+
};
|
|
579
|
+
for (const key of [
|
|
580
|
+
'triggeredAt',
|
|
581
|
+
'durationMs',
|
|
582
|
+
'exitCode',
|
|
583
|
+
'errorCategory',
|
|
584
|
+
'stdout',
|
|
585
|
+
'stderr',
|
|
586
|
+
'outputTruncated',
|
|
587
|
+
'resultSummary',
|
|
588
|
+
'payload',
|
|
589
|
+
'nextState',
|
|
590
|
+
'sandboxViolations',
|
|
591
|
+
'networkAudit',
|
|
592
|
+
]) {
|
|
593
|
+
if (input[key] !== undefined)
|
|
594
|
+
fields[key] = input[key];
|
|
595
|
+
}
|
|
596
|
+
return fields;
|
|
597
|
+
}
|
|
598
|
+
function pickRunMutableFields(input) {
|
|
599
|
+
const fields = {};
|
|
600
|
+
for (const key of [
|
|
601
|
+
'status',
|
|
602
|
+
'threadId',
|
|
603
|
+
'acpRunId',
|
|
604
|
+
'deliveryStatus',
|
|
605
|
+
'bridgeActivity',
|
|
606
|
+
'error',
|
|
607
|
+
'startedAt',
|
|
608
|
+
'finishedAt',
|
|
609
|
+
]) {
|
|
610
|
+
if (input[key] !== undefined)
|
|
611
|
+
fields[key] = input[key];
|
|
612
|
+
}
|
|
613
|
+
return fields;
|
|
614
|
+
}
|
|
615
|
+
function normalizeDefinition(value) {
|
|
616
|
+
const definition = { ...(0, superai_contracts_1.normalizeAutomationDefinition)(value) };
|
|
617
|
+
definition.createdAt = assertIsoTimestamp(definition.createdAt, 'Automation createdAt');
|
|
618
|
+
definition.updatedAt = assertIsoTimestamp(definition.updatedAt, 'Automation updatedAt');
|
|
619
|
+
if (definition.lastEvaluationAt !== undefined) {
|
|
620
|
+
definition.lastEvaluationAt = assertIsoTimestamp(definition.lastEvaluationAt, 'Automation lastEvaluationAt');
|
|
621
|
+
}
|
|
622
|
+
if (definition.lastTriggeredAt !== undefined) {
|
|
623
|
+
definition.lastTriggeredAt = assertIsoTimestamp(definition.lastTriggeredAt, 'Automation lastTriggeredAt');
|
|
624
|
+
}
|
|
625
|
+
if (definition.activation.kind === 'once') {
|
|
626
|
+
definition.activation = {
|
|
627
|
+
...definition.activation,
|
|
628
|
+
runAt: assertIsoTimestamp(definition.activation.runAt, 'Automation runAt'),
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
return definition;
|
|
632
|
+
}
|
|
633
|
+
function assertDuplicatedField(label, indexedValue, jsonValue) {
|
|
634
|
+
if (!Object.is(indexedValue, jsonValue)) {
|
|
635
|
+
throw new Error(`stored ${label} does not match its indexed column.`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function validateEvaluation(value) {
|
|
639
|
+
const input = asRecord(value, 'Automation evaluation');
|
|
640
|
+
const id = requiredString(input.id, 'Automation evaluation id');
|
|
641
|
+
const automationId = requiredString(input.automationId, 'Automation evaluation automationId');
|
|
642
|
+
const activationKind = input.activationKind;
|
|
643
|
+
if (activationKind !== 'cron' && activationKind !== 'once' && activationKind !== 'interval' && activationKind !== 'provider-event') {
|
|
644
|
+
throw new Error('Automation evaluation activationKind is invalid.');
|
|
645
|
+
}
|
|
646
|
+
const startedAt = assertIsoTimestamp(input.startedAt, 'Automation evaluation startedAt');
|
|
647
|
+
const scriptVersionId = optionalString(input.scriptVersionId, 'Automation evaluation scriptVersionId');
|
|
648
|
+
if (input.status === 'running') {
|
|
649
|
+
if (input.conditionOutcome !== undefined || input.triggerDecision !== undefined || input.finishedAt !== undefined) {
|
|
650
|
+
throw new Error('Running automation evaluations cannot contain finish fields.');
|
|
651
|
+
}
|
|
652
|
+
return { id, automationId, status: 'running', activationKind, ...(scriptVersionId ? { scriptVersionId } : {}), startedAt };
|
|
653
|
+
}
|
|
654
|
+
if (input.status !== 'finished')
|
|
655
|
+
throw new Error('Automation evaluation status must be running or finished.');
|
|
656
|
+
const conditionOutcome = input.conditionOutcome;
|
|
657
|
+
const triggerDecision = input.triggerDecision;
|
|
658
|
+
const allowed = (conditionOutcome === 'matched' && ['triggered', 'not_rising', 'skipped_cooldown', 'skipped_action_running'].includes(String(triggerDecision))) ||
|
|
659
|
+
(conditionOutcome === 'not_matched' && triggerDecision === 'not_rising') ||
|
|
660
|
+
(conditionOutcome === 'error' && triggerDecision === 'not_evaluated') ||
|
|
661
|
+
(conditionOutcome === 'skipped' && ['not_evaluated', 'skipped_concurrent', 'skipped_cooldown', 'skipped_action_running'].includes(String(triggerDecision)));
|
|
662
|
+
if (!allowed)
|
|
663
|
+
throw new Error('Automation evaluation condition outcome and trigger decision are incompatible.');
|
|
664
|
+
const result = {
|
|
665
|
+
id,
|
|
666
|
+
automationId,
|
|
667
|
+
status: 'finished',
|
|
668
|
+
activationKind,
|
|
669
|
+
...(scriptVersionId ? { scriptVersionId } : {}),
|
|
670
|
+
startedAt,
|
|
671
|
+
conditionOutcome,
|
|
672
|
+
triggerDecision,
|
|
673
|
+
finishedAt: assertIsoTimestamp(input.finishedAt, 'Automation evaluation finishedAt'),
|
|
674
|
+
};
|
|
675
|
+
for (const key of ['triggeredAt']) {
|
|
676
|
+
if (input[key] !== undefined)
|
|
677
|
+
result[key] = assertIsoTimestamp(input[key], `Automation evaluation ${key}`);
|
|
678
|
+
}
|
|
679
|
+
for (const key of ['durationMs', 'exitCode']) {
|
|
680
|
+
if (input[key] !== undefined)
|
|
681
|
+
result[key] = assertInteger(input[key], `Automation evaluation ${key}`, key === 'durationMs');
|
|
682
|
+
}
|
|
683
|
+
for (const key of ['errorCategory', 'stdout', 'stderr', 'resultSummary']) {
|
|
684
|
+
if (input[key] !== undefined)
|
|
685
|
+
result[key] = assertString(input[key], `Automation evaluation ${key}`);
|
|
686
|
+
}
|
|
687
|
+
if (input.outputTruncated !== undefined) {
|
|
688
|
+
if (typeof input.outputTruncated !== 'boolean')
|
|
689
|
+
throw new Error('Automation evaluation outputTruncated must be a boolean.');
|
|
690
|
+
result.outputTruncated = input.outputTruncated;
|
|
691
|
+
}
|
|
692
|
+
for (const key of ['payload', 'nextState']) {
|
|
693
|
+
if (input[key] !== undefined)
|
|
694
|
+
result[key] = asRecord(input[key], `Automation evaluation ${key}`);
|
|
695
|
+
}
|
|
696
|
+
for (const key of ['sandboxViolations']) {
|
|
697
|
+
if (input[key] !== undefined)
|
|
698
|
+
result[key] = stringArray(input[key], `Automation evaluation ${key}`);
|
|
699
|
+
}
|
|
700
|
+
if (input.networkAudit !== undefined) {
|
|
701
|
+
if (!Array.isArray(input.networkAudit))
|
|
702
|
+
throw new Error('Automation evaluation networkAudit must be an array.');
|
|
703
|
+
result.networkAudit = input.networkAudit.map((entry, index) => {
|
|
704
|
+
const record = asRecord(entry, `Automation evaluation networkAudit[${index}]`);
|
|
705
|
+
if (typeof record.allowed !== 'boolean')
|
|
706
|
+
throw new Error('Automation evaluation networkAudit allowed must be a boolean.');
|
|
707
|
+
if (record.host === undefined) {
|
|
708
|
+
const legacy = requiredString(record.target, 'Automation evaluation networkAudit target');
|
|
709
|
+
const parsed = safeLegacyNetworkTarget(legacy);
|
|
710
|
+
return { host: parsed.host, ...(parsed.port === undefined ? {} : { port: parsed.port }), allowed: record.allowed };
|
|
711
|
+
}
|
|
712
|
+
const port = record.port === undefined ? undefined : assertInteger(record.port, 'Automation evaluation networkAudit port', true);
|
|
713
|
+
return {
|
|
714
|
+
host: requiredString(record.host, 'Automation evaluation networkAudit host').toLowerCase(),
|
|
715
|
+
...(port === undefined ? {} : { port }), allowed: record.allowed,
|
|
716
|
+
...(record.timestamp === undefined ? {} : { timestamp: requiredString(record.timestamp, 'Automation evaluation networkAudit timestamp') }),
|
|
717
|
+
};
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
return result;
|
|
721
|
+
}
|
|
722
|
+
function safeLegacyNetworkTarget(target) {
|
|
723
|
+
try {
|
|
724
|
+
const url = new URL(target.includes('://') ? target : `https://${target}`);
|
|
725
|
+
const port = url.port ? Number(url.port) : undefined;
|
|
726
|
+
return { host: url.hostname.toLowerCase(), ...(Number.isSafeInteger(port) && port >= 0 ? { port } : {}) };
|
|
727
|
+
}
|
|
728
|
+
catch {
|
|
729
|
+
return { host: 'legacy-invalid-target' };
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
function validateRun(value) {
|
|
733
|
+
const input = asRecord(value, 'Automation run');
|
|
734
|
+
const status = input.status;
|
|
735
|
+
if (status !== 'queued' && status !== 'running' && status !== 'succeeded' && status !== 'failed' && status !== 'skipped') {
|
|
736
|
+
throw new Error('Automation run status is invalid.');
|
|
737
|
+
}
|
|
738
|
+
const executionMode = input.executionMode;
|
|
739
|
+
if (executionMode !== 'same-thread' && executionMode !== 'side-thread') {
|
|
740
|
+
throw new Error('Automation run executionMode is invalid.');
|
|
741
|
+
}
|
|
742
|
+
const run = {
|
|
743
|
+
id: requiredString(input.id, 'Automation run id'),
|
|
744
|
+
automationId: requiredString(input.automationId, 'Automation run automationId'),
|
|
745
|
+
evaluationId: requiredString(input.evaluationId, 'Automation run evaluationId'),
|
|
746
|
+
status,
|
|
747
|
+
executionMode,
|
|
748
|
+
createdAt: assertIsoTimestamp(input.createdAt, 'Automation run createdAt'),
|
|
749
|
+
};
|
|
750
|
+
for (const key of ['threadId', 'acpRunId', 'error']) {
|
|
751
|
+
if (input[key] !== undefined)
|
|
752
|
+
run[key] = assertString(input[key], `Automation run ${key}`);
|
|
753
|
+
}
|
|
754
|
+
for (const key of ['startedAt', 'finishedAt']) {
|
|
755
|
+
if (input[key] !== undefined)
|
|
756
|
+
run[key] = assertIsoTimestamp(input[key], `Automation run ${key}`);
|
|
757
|
+
}
|
|
758
|
+
if (input.deliveryStatus !== undefined) {
|
|
759
|
+
if (!['pending', 'delivering', 'delivered', 'failed'].includes(String(input.deliveryStatus))) {
|
|
760
|
+
throw new Error('Automation run deliveryStatus is invalid.');
|
|
761
|
+
}
|
|
762
|
+
run.deliveryStatus = input.deliveryStatus;
|
|
763
|
+
}
|
|
764
|
+
if (input.bridgeActivity !== undefined)
|
|
765
|
+
run.bridgeActivity = asRecord(input.bridgeActivity, 'Automation run bridgeActivity');
|
|
766
|
+
return run;
|
|
767
|
+
}
|
|
768
|
+
function parseStoredJson(value, label) {
|
|
769
|
+
try {
|
|
770
|
+
return JSON.parse(value);
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
throw withContext(`${label} is malformed JSON`, error);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
function asRecord(value, label) {
|
|
777
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
778
|
+
throw new Error(`${label} must be an object.`);
|
|
779
|
+
return value;
|
|
780
|
+
}
|
|
781
|
+
function requiredString(value, label) {
|
|
782
|
+
if (typeof value !== 'string' || !value.trim())
|
|
783
|
+
throw new Error(`${label} is required.`);
|
|
784
|
+
return value.trim();
|
|
785
|
+
}
|
|
786
|
+
function optionalString(value, label) {
|
|
787
|
+
return value === undefined ? undefined : requiredString(value, label);
|
|
788
|
+
}
|
|
789
|
+
function assertString(value, label) {
|
|
790
|
+
if (typeof value !== 'string')
|
|
791
|
+
throw new Error(`${label} must be a string.`);
|
|
792
|
+
return value;
|
|
793
|
+
}
|
|
794
|
+
function assertIsoTimestamp(value, label) {
|
|
795
|
+
const timestamp = requiredString(value, label);
|
|
796
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/.exec(timestamp);
|
|
797
|
+
if (!match || Number.isNaN(Date.parse(timestamp))) {
|
|
798
|
+
throw new Error(`${label} must be a valid ISO timestamp.`);
|
|
799
|
+
}
|
|
800
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText] = match;
|
|
801
|
+
const year = Number(yearText);
|
|
802
|
+
const month = Number(monthText);
|
|
803
|
+
const day = Number(dayText);
|
|
804
|
+
const calendarDate = new Date(Date.UTC(year, month - 1, day));
|
|
805
|
+
if (calendarDate.getUTCFullYear() !== year ||
|
|
806
|
+
calendarDate.getUTCMonth() !== month - 1 ||
|
|
807
|
+
calendarDate.getUTCDate() !== day ||
|
|
808
|
+
Number(hourText) > 23 ||
|
|
809
|
+
Number(minuteText) > 59 ||
|
|
810
|
+
Number(secondText) > 59) {
|
|
811
|
+
throw new Error(`${label} must be a valid ISO timestamp.`);
|
|
812
|
+
}
|
|
813
|
+
return new Date(timestamp).toISOString();
|
|
814
|
+
}
|
|
815
|
+
function assertInteger(value, label, nonNegative = false) {
|
|
816
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || (nonNegative && value < 0)) {
|
|
817
|
+
throw new Error(`${label} must be ${nonNegative ? 'a non-negative ' : 'an '}integer.`);
|
|
818
|
+
}
|
|
819
|
+
return value;
|
|
820
|
+
}
|
|
821
|
+
function stringArray(value, label) {
|
|
822
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {
|
|
823
|
+
throw new Error(`${label} must be an array of strings.`);
|
|
824
|
+
}
|
|
825
|
+
return [...value];
|
|
826
|
+
}
|
|
827
|
+
function withContext(label, error) {
|
|
828
|
+
return new Error(`${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
829
|
+
}
|