@kafca/agentdock 0.1.61 → 0.1.62
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-BgJgAdVO.js +16 -0
- package/dist/renderer/assets/{Badge-BLI4LHse.js → Badge-Bq2hPVcR.js} +1 -1
- package/dist/renderer/assets/{Card-DJeLDdL3.js → Card-BPJNSZce.js} +1 -1
- package/dist/renderer/assets/{Config-D1awc81A.js → Config-aSFqlI88.js} +2 -2
- package/dist/renderer/assets/Dashboard-D3ezFmb7.js +36 -0
- package/dist/renderer/assets/{EmptyState-CkI1RwPX.js → EmptyState-DK496gBq.js} +1 -1
- package/dist/renderer/assets/{HighlightedMarkdown-BUhDlhgW.js → HighlightedMarkdown-Ckre5jNv.js} +1 -1
- package/dist/renderer/assets/{Input-DHYrn-Ne.js → Input-C5C1d0ba.js} +1 -1
- package/dist/renderer/assets/{KnowledgeDetail-Bij-Xo90.js → KnowledgeDetail-N8Ey1ikc.js} +2 -2
- package/dist/renderer/assets/{KnowledgeHome-DmPZ2IlI.js → KnowledgeHome-9aF8nJEf.js} +2 -2
- package/dist/renderer/assets/{Logs-ISY99heA.js → Logs-sgAdwEeS.js} +2 -2
- package/dist/renderer/assets/{Modal-DAQfz0ot.js → Modal-BB10hTRc.js} +3 -3
- package/dist/renderer/assets/{Page-Dw43UYDT.js → Page-DfC319Ck.js} +1 -1
- package/dist/renderer/assets/{Select-DzmN5OGQ.js → Select-RHJBmNER.js} +1 -1
- package/dist/renderer/assets/{ThreadChat-B5CWqqMK.js → ThreadChat-BU1zCdr4.js} +3 -3
- package/dist/renderer/assets/{Workspace-C5g9LBj9.js → Workspace-BoZCz7S8.js} +1 -1
- package/dist/renderer/assets/{arrow-left-BLXh56Ip.js → arrow-left-D2V1eZSD.js} +1 -1
- package/dist/renderer/assets/{book-open-DhPGbc-7.js → book-open-DUcBn7Bb.js} +1 -1
- package/dist/renderer/assets/{channels-DfQrmuYO.js → channels-DMhDSlf2.js} +2 -2
- package/dist/renderer/assets/{chevron-down-CEFtpPDc.js → chevron-down-Ca2FQDB7.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-CYhDUAR_.js → index-BlnvlekJ.js} +1 -1
- package/dist/renderer/assets/index-DRJ5wgw_.js +162 -0
- package/dist/renderer/assets/ja-CDWPWqEW.js +1 -0
- package/dist/renderer/assets/{knowledge-uHRQ80sR.js → knowledge-D1-k56eY.js} +1 -1
- package/dist/renderer/assets/{pencil-BD2Y31YC.js → pencil-BhWdv_86.js} +1 -1
- package/dist/renderer/assets/{plus-DDcldp_c.js → plus-BacLMlK3.js} +1 -1
- package/dist/renderer/assets/{save-CMW_cZI9.js → save-Bpoxbzau.js} +1 -1
- package/dist/renderer/assets/{search-BM-0CvJr.js → search-BVJcpdFX.js} +1 -1
- package/dist/renderer/assets/{shield-check-DaMB0FKG.js → shield-check-C2cw_Ip6.js} +1 -1
- package/dist/renderer/assets/{threads-DZgt9tWj.js → threads-DOsBQBg_.js} +1 -1
- package/dist/renderer/assets/{trash-2-DTXVbC5y.js → trash-2-Bs3rEm3g.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 +333 -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,1019 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AutomationService = exports.NATIVE_AUTOMATION_OWNERSHIP = exports.PROVIDER_LIFECYCLE_BLOCK_PREFIX = exports.AUTOMATION_ERROR_MAX_LENGTH = void 0;
|
|
4
|
+
exports.normalizeAutomationError = normalizeAutomationError;
|
|
5
|
+
exports.providerLifecycleBlockReason = providerLifecycleBlockReason;
|
|
6
|
+
exports.normalizeProviderEventSnapshot = normalizeProviderEventSnapshot;
|
|
7
|
+
const local_core_acp_store_js_1 = require("../acp/local-core-acp-store.js");
|
|
8
|
+
const automation_condition_engine_js_1 = require("./automation-condition-engine.js");
|
|
9
|
+
const script_protocol_runner_js_1 = require("./scripts/script-protocol-runner.js");
|
|
10
|
+
const anthropic_sandbox_runner_js_1 = require("./scripts/anthropic-sandbox-runner.js");
|
|
11
|
+
const condition_evaluator_js_1 = require("./condition-evaluator.js");
|
|
12
|
+
const automation_trigger_engine_js_1 = require("./automation-trigger-engine.js");
|
|
13
|
+
const legacy_automation_mappers_js_1 = require("./legacy-automation-mappers.js");
|
|
14
|
+
const DUE_LOOP_INTERVAL_MS = 30_000;
|
|
15
|
+
const DEFAULT_MAX_CONCURRENCY = 4;
|
|
16
|
+
exports.AUTOMATION_ERROR_MAX_LENGTH = 2_000;
|
|
17
|
+
exports.PROVIDER_LIFECYCLE_BLOCK_PREFIX = 'Automation monitor provider lifecycle blocked: ';
|
|
18
|
+
const PROVIDER_JSON_MAX_DEPTH = 64;
|
|
19
|
+
const PROVIDER_JSON_MAX_SIZE = 100_000;
|
|
20
|
+
const PROVIDER_EVENT_STRING_MAX_LENGTH = 16_384;
|
|
21
|
+
const FAILURE_ALERT_COUNTS = new Set([1, 3, 7, 15, 31]);
|
|
22
|
+
const RESTART_INTERRUPTION_REASON = 'Automation action interrupted by Local AI Core restart.';
|
|
23
|
+
exports.NATIVE_AUTOMATION_OWNERSHIP = {
|
|
24
|
+
executes: () => true,
|
|
25
|
+
};
|
|
26
|
+
class AutomationService {
|
|
27
|
+
options;
|
|
28
|
+
timer;
|
|
29
|
+
tickPromise;
|
|
30
|
+
startPromise;
|
|
31
|
+
inFlight = new Map();
|
|
32
|
+
runtimeStatus = { status: 'stopped' };
|
|
33
|
+
stopping = false;
|
|
34
|
+
lifecycleGeneration = 0;
|
|
35
|
+
scriptProtocolRunner;
|
|
36
|
+
constructor(options) {
|
|
37
|
+
this.options = options;
|
|
38
|
+
const concurrency = options.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY;
|
|
39
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
|
|
40
|
+
throw new Error('Automation maxConcurrency must be a positive safe integer.');
|
|
41
|
+
}
|
|
42
|
+
this.scriptProtocolRunner = options.scriptProtocolRunner;
|
|
43
|
+
}
|
|
44
|
+
list(workspaceId) {
|
|
45
|
+
return this.options.store.listAutomations(workspaceId);
|
|
46
|
+
}
|
|
47
|
+
get(automationId) {
|
|
48
|
+
return this.options.store.getAutomation(automationId);
|
|
49
|
+
}
|
|
50
|
+
create(input) {
|
|
51
|
+
const updated = this.options.store.createAutomationAtomically(input, (automation) => ({
|
|
52
|
+
nextCheckAt: this.initialNextCheckAt(automation),
|
|
53
|
+
}));
|
|
54
|
+
this.emitDefinition(updated);
|
|
55
|
+
return updated;
|
|
56
|
+
}
|
|
57
|
+
createFromLegacy(input) {
|
|
58
|
+
this.assertLegacyFacadesAvailable();
|
|
59
|
+
const updated = this.options.store.createTrustedAutomationAtomically(input, (automation) => ({
|
|
60
|
+
nextCheckAt: this.initialNextCheckAt(automation),
|
|
61
|
+
}));
|
|
62
|
+
this.emitDefinition(updated);
|
|
63
|
+
return updated;
|
|
64
|
+
}
|
|
65
|
+
assertLegacyFacadesAvailable() {
|
|
66
|
+
if (this.runtimeStatus.status === 'degraded') {
|
|
67
|
+
throw new Error(`Unified automation migration is unavailable: ${this.runtimeStatus.reason}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
update(automationId, input) {
|
|
71
|
+
return this.updateInternal(automationId, input);
|
|
72
|
+
}
|
|
73
|
+
updateFromLegacy(automationId, input) {
|
|
74
|
+
this.assertLegacyFacadesAvailable();
|
|
75
|
+
return this.updateInternal(automationId, input, true);
|
|
76
|
+
}
|
|
77
|
+
updateInternal(automationId, input, trustedLegacy = false) {
|
|
78
|
+
const existing = this.requireAutomation(automationId);
|
|
79
|
+
const initialize = (automation) => {
|
|
80
|
+
if (input.activation !== undefined) {
|
|
81
|
+
return { nextCheckAt: this.initialNextCheckAt(automation, true) };
|
|
82
|
+
}
|
|
83
|
+
if (input.enabled === true
|
|
84
|
+
&& existing.enabled === false
|
|
85
|
+
&& this.options.store.getAutomationNextCheckAt(automation.id) === null
|
|
86
|
+
&& !this.isConsumedOnce(automation))
|
|
87
|
+
return { nextCheckAt: this.initialNextCheckAt(automation) };
|
|
88
|
+
return undefined;
|
|
89
|
+
};
|
|
90
|
+
const updated = trustedLegacy
|
|
91
|
+
? this.options.store.updateTrustedAutomationAtomically(automationId, input, initialize)
|
|
92
|
+
: this.options.store.updateAutomationAtomically(automationId, input, initialize);
|
|
93
|
+
this.emitDefinition(updated);
|
|
94
|
+
return updated;
|
|
95
|
+
}
|
|
96
|
+
delete(automationId) {
|
|
97
|
+
return this.options.store.deleteAutomation(automationId);
|
|
98
|
+
}
|
|
99
|
+
listEvaluations(automationId) {
|
|
100
|
+
return this.options.store.listAutomationEvaluations(automationId);
|
|
101
|
+
}
|
|
102
|
+
getLatestEvaluationWithState(automationId) {
|
|
103
|
+
return this.options.store.getLatestAutomationEvaluationWithState(automationId);
|
|
104
|
+
}
|
|
105
|
+
listRuns(automationId) {
|
|
106
|
+
return this.options.store.listAutomationRuns(automationId);
|
|
107
|
+
}
|
|
108
|
+
getRuntimeStatus() {
|
|
109
|
+
return this.runtimeStatus;
|
|
110
|
+
}
|
|
111
|
+
start() {
|
|
112
|
+
if (this.startPromise)
|
|
113
|
+
return this.startPromise;
|
|
114
|
+
if (this.runtimeStatus.status === 'running')
|
|
115
|
+
return Promise.resolve();
|
|
116
|
+
const work = this.startInternal();
|
|
117
|
+
let shared;
|
|
118
|
+
shared = work.finally(() => {
|
|
119
|
+
if (this.startPromise === shared)
|
|
120
|
+
this.startPromise = undefined;
|
|
121
|
+
});
|
|
122
|
+
this.startPromise = shared;
|
|
123
|
+
return shared;
|
|
124
|
+
}
|
|
125
|
+
async startInternal() {
|
|
126
|
+
this.stopping = false;
|
|
127
|
+
const generation = ++this.lifecycleGeneration;
|
|
128
|
+
await this.settleActiveWork();
|
|
129
|
+
if (generation !== this.lifecycleGeneration || this.stopping)
|
|
130
|
+
return;
|
|
131
|
+
try {
|
|
132
|
+
this.options.store.importLegacyAutomations();
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
const reason = normalizeAutomationError(error, 'Legacy automation import failed: ');
|
|
136
|
+
this.runtimeStatus = { status: 'degraded', reason };
|
|
137
|
+
this.reportDiagnostic('legacy-import', reason);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const recovered = this.options.store.reconcileInterruptedAutomationRuns(normalizeAutomationError(RESTART_INTERRUPTION_REASON), this.now().toISOString());
|
|
142
|
+
for (const run of recovered)
|
|
143
|
+
this.emitRun(run);
|
|
144
|
+
for (const automation of this.list()) {
|
|
145
|
+
if (this.shouldPoll(automation)
|
|
146
|
+
&& this.options.store.getAutomationNextCheckAt(automation.id) === null
|
|
147
|
+
&& !this.isConsumedOnce(automation)) {
|
|
148
|
+
this.persistInitialNextCheck(automation);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
this.runtimeStatus = { status: 'running' };
|
|
152
|
+
if (this.timer === undefined) {
|
|
153
|
+
this.timer = (this.options.setInterval || setInterval)(() => {
|
|
154
|
+
this.runTimerTick(generation);
|
|
155
|
+
}, DUE_LOOP_INTERVAL_MS);
|
|
156
|
+
}
|
|
157
|
+
await this.tick();
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
const isCurrentGeneration = this.lifecycleGeneration === generation && !this.stopping;
|
|
161
|
+
if (isCurrentGeneration) {
|
|
162
|
+
this.clearTimer();
|
|
163
|
+
this.runtimeStatus = { status: 'stopped' };
|
|
164
|
+
}
|
|
165
|
+
const message = normalizeAutomationError(error, 'Automation startup failed: ');
|
|
166
|
+
this.reportDiagnostic('startup', message);
|
|
167
|
+
throw new Error(message);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async stop() {
|
|
171
|
+
this.stopping = true;
|
|
172
|
+
const stopGeneration = ++this.lifecycleGeneration;
|
|
173
|
+
this.clearTimer();
|
|
174
|
+
this.runtimeStatus = { status: 'stopped' };
|
|
175
|
+
await this.settleActiveWork();
|
|
176
|
+
if (this.lifecycleGeneration === stopGeneration) {
|
|
177
|
+
this.runtimeStatus = { status: 'stopped' };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async tick() {
|
|
181
|
+
if (this.tickPromise)
|
|
182
|
+
return this.tickPromise;
|
|
183
|
+
const work = this.runTick(this.lifecycleGeneration);
|
|
184
|
+
this.tickPromise = work;
|
|
185
|
+
try {
|
|
186
|
+
await work;
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
if (this.tickPromise === work)
|
|
190
|
+
this.tickPromise = undefined;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async checkNow(automationId) {
|
|
194
|
+
const automation = this.requireAutomation(automationId);
|
|
195
|
+
if (automation.activation.kind === 'provider-event') {
|
|
196
|
+
throw new Error(`Provider-event automation requires an event snapshot: ${automationId}`);
|
|
197
|
+
}
|
|
198
|
+
return this.checkAutomation(automation, undefined, true);
|
|
199
|
+
}
|
|
200
|
+
/** The HTTP layer calls this only after the approval service grants one test run. */
|
|
201
|
+
async executeAuthorizedScriptTest(versionId) {
|
|
202
|
+
const version = this.options.store.getAutomationScriptVersion(versionId);
|
|
203
|
+
if (!version || version.status !== 'testing') {
|
|
204
|
+
throw new Error('Automation script test requires a claimed test authorization.');
|
|
205
|
+
}
|
|
206
|
+
const finishedAt = this.now().toISOString();
|
|
207
|
+
try {
|
|
208
|
+
const result = await this.getScriptTestRunner().runTest({
|
|
209
|
+
scriptId: version.scriptId,
|
|
210
|
+
approvedVersionId: version.id,
|
|
211
|
+
evaluationId: `script-test:${version.id}`,
|
|
212
|
+
triggeredAt: finishedAt,
|
|
213
|
+
previousState: {},
|
|
214
|
+
});
|
|
215
|
+
return {
|
|
216
|
+
status: 'passed',
|
|
217
|
+
finishedAt: this.now().toISOString(),
|
|
218
|
+
...(result.summary === undefined ? {} : { summary: result.summary }),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
return {
|
|
223
|
+
status: 'failed',
|
|
224
|
+
finishedAt: this.now().toISOString(),
|
|
225
|
+
summary: normalizeAutomationError(error),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async evaluateProviderEvent(automationId, event) {
|
|
230
|
+
this.assertLegacyFacadesAvailable();
|
|
231
|
+
const automation = this.requireAutomation(automationId);
|
|
232
|
+
if (automation.activation.kind !== 'provider-event') {
|
|
233
|
+
throw new Error(`Automation is not provider-event activated: ${automationId}`);
|
|
234
|
+
}
|
|
235
|
+
const snapshot = normalizeProviderEventSnapshot(event);
|
|
236
|
+
if (snapshot.sourceType !== automation.activation.sourceType) {
|
|
237
|
+
throw new Error(normalizeAutomationError(`Provider event source "${snapshot.sourceType}" does not match automation source "${automation.activation.sourceType}".`));
|
|
238
|
+
}
|
|
239
|
+
return this.checkAutomation(automation, this.providerEventContext(automation, snapshot));
|
|
240
|
+
}
|
|
241
|
+
recordUnavailableProviderEvent(automationId, reason) {
|
|
242
|
+
this.assertLegacyFacadesAvailable();
|
|
243
|
+
const automation = this.requireAutomation(automationId);
|
|
244
|
+
if (automation.activation.kind !== 'provider-event') {
|
|
245
|
+
throw new Error(`Automation is not provider-event activated: ${automationId}`);
|
|
246
|
+
}
|
|
247
|
+
const startedAt = this.now();
|
|
248
|
+
const running = this.createEvaluation(automation, startedAt);
|
|
249
|
+
const finishedAt = this.now();
|
|
250
|
+
const finished = this.finishEvaluation(running.id, {
|
|
251
|
+
conditionOutcome: 'skipped',
|
|
252
|
+
triggerDecision: 'not_evaluated',
|
|
253
|
+
finishedAt: finishedAt.toISOString(),
|
|
254
|
+
durationMs: Math.max(0, finishedAt.getTime() - startedAt.getTime()),
|
|
255
|
+
resultSummary: normalizeAutomationError(reason),
|
|
256
|
+
});
|
|
257
|
+
this.updateDefinitionAfterEvaluation(automation, finishedAt, {
|
|
258
|
+
nextMatch: automation.lastSuccessfulMatch,
|
|
259
|
+
failureCount: automation.consecutiveEvaluationFailures,
|
|
260
|
+
});
|
|
261
|
+
this.emitCompatibilityEvaluationRun(automation, finished);
|
|
262
|
+
return finished;
|
|
263
|
+
}
|
|
264
|
+
failClosedLegacyAutomation(automationId, reason) {
|
|
265
|
+
const automation = this.requireAutomation(automationId);
|
|
266
|
+
if (automation.originKind === 'native') {
|
|
267
|
+
throw new Error(`Automation is not owned by a legacy facade: ${automationId}`);
|
|
268
|
+
}
|
|
269
|
+
const blocked = this.options.store.updateAutomationAtomically(automationId, { enabled: false }, () => ({
|
|
270
|
+
health: 'blocked',
|
|
271
|
+
blockedReason: providerLifecycleBlockReason(reason),
|
|
272
|
+
}));
|
|
273
|
+
this.emitDefinition(blocked);
|
|
274
|
+
return blocked;
|
|
275
|
+
}
|
|
276
|
+
markLegacyProviderLifecycleBlocked(automationId, reason) {
|
|
277
|
+
const automation = this.requireAutomation(automationId);
|
|
278
|
+
if (automation.originKind !== 'automation-monitor') {
|
|
279
|
+
throw new Error(`Automation is not owned by the legacy monitor facade: ${automationId}`);
|
|
280
|
+
}
|
|
281
|
+
const blocked = this.options.store.updateAutomationState(automationId, {
|
|
282
|
+
health: 'blocked',
|
|
283
|
+
blockedReason: providerLifecycleBlockReason(reason),
|
|
284
|
+
});
|
|
285
|
+
this.emitDefinition(blocked);
|
|
286
|
+
return blocked;
|
|
287
|
+
}
|
|
288
|
+
clearLegacyProviderLifecycleBlocked(automationId, expectedReason) {
|
|
289
|
+
const automation = this.requireAutomation(automationId);
|
|
290
|
+
if (automation.originKind !== 'automation-monitor') {
|
|
291
|
+
throw new Error(`Automation is not owned by the legacy monitor facade: ${automationId}`);
|
|
292
|
+
}
|
|
293
|
+
if (automation.health !== 'blocked' || automation.blockedReason !== normalizeAutomationError(expectedReason)) {
|
|
294
|
+
return automation;
|
|
295
|
+
}
|
|
296
|
+
const healthy = this.options.store.updateAutomationState(automationId, { health: 'healthy' });
|
|
297
|
+
this.emitDefinition(healthy);
|
|
298
|
+
return healthy;
|
|
299
|
+
}
|
|
300
|
+
async runTick(generation) {
|
|
301
|
+
const now = this.now();
|
|
302
|
+
const due = [];
|
|
303
|
+
for (const automation of this.list()) {
|
|
304
|
+
if (!this.shouldPoll(automation))
|
|
305
|
+
continue;
|
|
306
|
+
const nextCheckAt = this.options.store.getAutomationNextCheckAt(automation.id);
|
|
307
|
+
if (nextCheckAt !== null && Date.parse(nextCheckAt) <= now.getTime()) {
|
|
308
|
+
due.push(automation);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
let index = 0;
|
|
312
|
+
const worker = async () => {
|
|
313
|
+
while (!this.stopping && generation === this.lifecycleGeneration) {
|
|
314
|
+
const automation = due[index];
|
|
315
|
+
index += 1;
|
|
316
|
+
if (!automation)
|
|
317
|
+
return;
|
|
318
|
+
await this.checkAutomation(automation);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
const workerCount = Math.min(due.length, this.options.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY);
|
|
322
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
323
|
+
if (!this.stopping && generation === this.lifecycleGeneration) {
|
|
324
|
+
this.options.store.pruneAutomationEvaluations(now);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async checkAutomation(automation, context, manual = false) {
|
|
328
|
+
const existing = this.inFlight.get(automation.id);
|
|
329
|
+
if (existing)
|
|
330
|
+
return this.recordConcurrentSkip(automation, context);
|
|
331
|
+
const work = this.evaluateAndMaybeRun(automation, context, manual);
|
|
332
|
+
this.inFlight.set(automation.id, work);
|
|
333
|
+
try {
|
|
334
|
+
return await work;
|
|
335
|
+
}
|
|
336
|
+
finally {
|
|
337
|
+
if (this.inFlight.get(automation.id) === work)
|
|
338
|
+
this.inFlight.delete(automation.id);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async recordConcurrentSkip(automation, context) {
|
|
342
|
+
const now = context ? new Date(context.occurredAt) : this.now();
|
|
343
|
+
const running = this.createEvaluation(automation, now);
|
|
344
|
+
const finished = this.finishEvaluation(running.id, {
|
|
345
|
+
conditionOutcome: 'skipped',
|
|
346
|
+
triggerDecision: 'skipped_concurrent',
|
|
347
|
+
finishedAt: now.toISOString(),
|
|
348
|
+
durationMs: 0,
|
|
349
|
+
resultSummary: 'Skipped because another evaluation is still running.',
|
|
350
|
+
...(context ? { payload: context.payload, nextState: context.nextState } : {}),
|
|
351
|
+
});
|
|
352
|
+
this.updateDefinitionAfterEvaluation(automation, now, {
|
|
353
|
+
nextMatch: automation.lastSuccessfulMatch,
|
|
354
|
+
failureCount: automation.consecutiveEvaluationFailures,
|
|
355
|
+
});
|
|
356
|
+
this.emitCompatibilityEvaluationRun(automation, finished);
|
|
357
|
+
return finished;
|
|
358
|
+
}
|
|
359
|
+
async evaluateAndMaybeRun(automation, context, manual = false) {
|
|
360
|
+
const startedAt = context ? new Date(context.occurredAt) : this.now();
|
|
361
|
+
const running = this.createEvaluation(automation, startedAt);
|
|
362
|
+
const actionRunning = this.listRuns(automation.id)
|
|
363
|
+
.some((run) => run.status === 'queued' || run.status === 'running');
|
|
364
|
+
const coolingDown = automation.lastTriggeredAt !== undefined
|
|
365
|
+
&& startedAt.getTime() < Date.parse(automation.lastTriggeredAt) + automation.policies.cooldownMs;
|
|
366
|
+
const payload = context?.payload || {};
|
|
367
|
+
let decision;
|
|
368
|
+
try {
|
|
369
|
+
const legacySnapshot = automation.originKind === 'automation-monitor'
|
|
370
|
+
&& automation.condition.kind === 'expression'
|
|
371
|
+
&& isPlainRecord(payload.eventSnapshot)
|
|
372
|
+
? payload.eventSnapshot
|
|
373
|
+
: undefined;
|
|
374
|
+
const evaluator = this.options.conditionEvaluator || (legacySnapshot
|
|
375
|
+
? ((condition) => condition.kind === 'expression'
|
|
376
|
+
? { kind: 'evaluated', matched: (0, condition_evaluator_js_1.evaluateExpression)(condition.expression, legacySnapshot) }
|
|
377
|
+
: (0, automation_condition_engine_js_1.evaluateCondition)(condition, payload))
|
|
378
|
+
: automation_condition_engine_js_1.evaluateCondition);
|
|
379
|
+
decision = (0, automation_condition_engine_js_1.decideCondition)({
|
|
380
|
+
condition: automation.condition,
|
|
381
|
+
payload,
|
|
382
|
+
previous: automation.condition.kind === 'always' ? undefined : automation.lastSuccessfulMatch,
|
|
383
|
+
coolingDown,
|
|
384
|
+
actionRunning,
|
|
385
|
+
}, evaluator);
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
const finishedAt = this.now();
|
|
389
|
+
return this.finishError(automation, running, startedAt, finishedAt, normalizeAutomationError(error), context);
|
|
390
|
+
}
|
|
391
|
+
const finishedAt = this.now();
|
|
392
|
+
if (decision.kind === 'script-delegation') {
|
|
393
|
+
try {
|
|
394
|
+
const previousEvaluation = this.getLatestEvaluationWithState(automation.id);
|
|
395
|
+
const previousState = previousEvaluation?.status === 'finished' && previousEvaluation.nextState
|
|
396
|
+
? previousEvaluation.nextState
|
|
397
|
+
: {};
|
|
398
|
+
const scriptResult = await this.getScriptProtocolRunner().run({
|
|
399
|
+
scriptId: decision.request.scriptId,
|
|
400
|
+
approvedVersionId: decision.request.approvedVersionId,
|
|
401
|
+
evaluationId: running.id,
|
|
402
|
+
triggeredAt: startedAt.toISOString(),
|
|
403
|
+
previousState,
|
|
404
|
+
});
|
|
405
|
+
const scriptDecision = (0, automation_condition_engine_js_1.decideTrigger)({
|
|
406
|
+
previous: automation.lastSuccessfulMatch,
|
|
407
|
+
matched: scriptResult.matched,
|
|
408
|
+
coolingDown,
|
|
409
|
+
actionRunning,
|
|
410
|
+
});
|
|
411
|
+
const scriptFinishedAt = this.now();
|
|
412
|
+
const scriptPayload = scriptResult.payload || {};
|
|
413
|
+
const scriptFinishDetails = {
|
|
414
|
+
finishedAt: scriptFinishedAt.toISOString(),
|
|
415
|
+
durationMs: Math.max(0, scriptFinishedAt.getTime() - startedAt.getTime()),
|
|
416
|
+
...(scriptDecision.triggerDecision === 'triggered' ? { triggeredAt: startedAt.toISOString() } : {}),
|
|
417
|
+
payload: scriptPayload,
|
|
418
|
+
...(scriptResult.nextState === undefined ? {} : { nextState: scriptResult.nextState }),
|
|
419
|
+
stdout: scriptResult.stdout,
|
|
420
|
+
stderr: scriptResult.stderr,
|
|
421
|
+
exitCode: scriptResult.exitCode === null ? undefined : scriptResult.exitCode,
|
|
422
|
+
outputTruncated: scriptResult.outputTruncated,
|
|
423
|
+
...(scriptResult.networkAudit === undefined ? {} : { networkAudit: scriptResult.networkAudit }),
|
|
424
|
+
...(scriptResult.summary === undefined ? {} : { resultSummary: scriptResult.summary }),
|
|
425
|
+
};
|
|
426
|
+
const scriptFinished = this.finishEvaluation(running.id, scriptDecision.conditionOutcome === 'matched'
|
|
427
|
+
? { ...scriptFinishDetails, conditionOutcome: 'matched', triggerDecision: scriptDecision.triggerDecision }
|
|
428
|
+
: { ...scriptFinishDetails, conditionOutcome: 'not_matched', triggerDecision: 'not_rising' });
|
|
429
|
+
const updated = this.updateDefinitionAfterEvaluation(automation, startedAt, {
|
|
430
|
+
nextMatch: scriptDecision.nextMatch,
|
|
431
|
+
failureCount: 0,
|
|
432
|
+
triggered: scriptDecision.triggerDecision === 'triggered',
|
|
433
|
+
});
|
|
434
|
+
if (scriptDecision.triggerDecision === 'triggered') {
|
|
435
|
+
await this.executeAction(updated, scriptFinished, scriptPayload);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
this.emitCompatibilityEvaluationRun(updated, scriptFinished);
|
|
439
|
+
}
|
|
440
|
+
return scriptFinished;
|
|
441
|
+
}
|
|
442
|
+
catch (error) {
|
|
443
|
+
const message = normalizeAutomationError(error);
|
|
444
|
+
if (error instanceof script_protocol_runner_js_1.ScriptProtocolError && error.blockAutomation)
|
|
445
|
+
this.blockAutomation(automation, message);
|
|
446
|
+
return this.finishError(automation, running, startedAt, this.now(), message, undefined, error instanceof script_protocol_runner_js_1.ScriptProtocolError ? { networkAudit: error.networkAudit } : undefined);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (decision.conditionOutcome === 'error') {
|
|
450
|
+
return this.finishError(automation, running, startedAt, finishedAt, normalizeAutomationError(decision.error || 'Condition evaluation failed.'), context);
|
|
451
|
+
}
|
|
452
|
+
const finishDetails = {
|
|
453
|
+
finishedAt: finishedAt.toISOString(),
|
|
454
|
+
durationMs: Math.max(0, finishedAt.getTime() - startedAt.getTime()),
|
|
455
|
+
...(decision.triggerDecision === 'triggered' ? { triggeredAt: startedAt.toISOString() } : {}),
|
|
456
|
+
payload,
|
|
457
|
+
...(context ? { nextState: context.nextState } : {}),
|
|
458
|
+
};
|
|
459
|
+
let finishInput;
|
|
460
|
+
if (decision.conditionOutcome === 'matched') {
|
|
461
|
+
finishInput = { ...finishDetails, conditionOutcome: 'matched', triggerDecision: decision.triggerDecision };
|
|
462
|
+
}
|
|
463
|
+
else if (decision.conditionOutcome === 'not_matched') {
|
|
464
|
+
finishInput = { ...finishDetails, conditionOutcome: 'not_matched', triggerDecision: 'not_rising' };
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
finishInput = { ...finishDetails, conditionOutcome: 'skipped', triggerDecision: decision.triggerDecision };
|
|
468
|
+
}
|
|
469
|
+
const finished = this.finishEvaluation(running.id, finishInput);
|
|
470
|
+
const updated = this.updateDefinitionAfterEvaluation(automation, startedAt, {
|
|
471
|
+
nextMatch: decision.nextMatch,
|
|
472
|
+
failureCount: 0,
|
|
473
|
+
triggered: decision.triggerDecision === 'triggered',
|
|
474
|
+
});
|
|
475
|
+
if (decision.triggerDecision === 'triggered') {
|
|
476
|
+
const run = await this.executeAction(updated, finished, payload);
|
|
477
|
+
if (!manual && updated.originKind === 'scheduled-job' && updated.activation.kind === 'once' && run.status === 'succeeded') {
|
|
478
|
+
const disabled = this.options.store.updateAutomation(updated.id, { enabled: false });
|
|
479
|
+
this.emitDefinition(disabled);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
this.emitCompatibilityEvaluationRun(updated, finished);
|
|
484
|
+
}
|
|
485
|
+
return finished;
|
|
486
|
+
}
|
|
487
|
+
finishError(automation, running, startedAt, finishedAt, error, context, details) {
|
|
488
|
+
const finished = this.finishEvaluation(running.id, {
|
|
489
|
+
conditionOutcome: 'error',
|
|
490
|
+
triggerDecision: 'not_evaluated',
|
|
491
|
+
finishedAt: finishedAt.toISOString(),
|
|
492
|
+
durationMs: Math.max(0, finishedAt.getTime() - startedAt.getTime()),
|
|
493
|
+
errorCategory: 'condition_evaluation',
|
|
494
|
+
resultSummary: error,
|
|
495
|
+
...(context ? { payload: context.payload, nextState: context.nextState } : {}),
|
|
496
|
+
...(details?.networkAudit === undefined ? {} : { networkAudit: details.networkAudit }),
|
|
497
|
+
});
|
|
498
|
+
const count = automation.consecutiveEvaluationFailures + 1;
|
|
499
|
+
const updated = this.updateDefinitionAfterEvaluation(automation, context ? startedAt : finishedAt, {
|
|
500
|
+
nextMatch: automation.lastSuccessfulMatch,
|
|
501
|
+
failureCount: count,
|
|
502
|
+
});
|
|
503
|
+
this.options.log?.(normalizeAutomationError(error, `automation evaluation failed ${automation.id} (${count}): `));
|
|
504
|
+
if (FAILURE_ALERT_COUNTS.has(count))
|
|
505
|
+
this.options.alert?.({ automation: updated, count, error });
|
|
506
|
+
this.emitCompatibilityEvaluationRun(updated, finished);
|
|
507
|
+
return finished;
|
|
508
|
+
}
|
|
509
|
+
getScriptProtocolRunner() {
|
|
510
|
+
if (!this.scriptProtocolRunner) {
|
|
511
|
+
this.scriptProtocolRunner = new script_protocol_runner_js_1.ScriptProtocolRunner({
|
|
512
|
+
sandbox: (0, anthropic_sandbox_runner_js_1.createAnthropicSandboxRunner)(),
|
|
513
|
+
getVersion: (versionId) => this.options.store.getAutomationScriptVersion(versionId),
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
return this.scriptProtocolRunner;
|
|
517
|
+
}
|
|
518
|
+
getScriptTestRunner() {
|
|
519
|
+
if (!this.scriptProtocolRunner?.runTest) {
|
|
520
|
+
this.scriptProtocolRunner = new script_protocol_runner_js_1.ScriptProtocolRunner({
|
|
521
|
+
sandbox: (0, anthropic_sandbox_runner_js_1.createAnthropicSandboxRunner)(),
|
|
522
|
+
getVersion: (versionId) => this.options.store.getAutomationScriptVersion(versionId),
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
return this.scriptProtocolRunner;
|
|
526
|
+
}
|
|
527
|
+
blockAutomation(automation, reason) {
|
|
528
|
+
const blocked = this.options.store.updateAutomationState(automation.id, {
|
|
529
|
+
health: 'blocked',
|
|
530
|
+
blockedReason: normalizeAutomationError(reason),
|
|
531
|
+
});
|
|
532
|
+
this.emitDefinition(blocked);
|
|
533
|
+
}
|
|
534
|
+
async executeAction(automation, evaluation, payload) {
|
|
535
|
+
let run = this.options.store.createAutomationRun(automation.id, evaluation.id);
|
|
536
|
+
this.emitRun(run);
|
|
537
|
+
const startedAt = this.now().toISOString();
|
|
538
|
+
run = this.options.store.updateAutomationRun(run.id, { status: 'running', startedAt });
|
|
539
|
+
this.emitRun(run);
|
|
540
|
+
const promptVariables = {
|
|
541
|
+
title: automation.title,
|
|
542
|
+
timestamp: evaluation.status === 'finished' ? evaluation.finishedAt : startedAt,
|
|
543
|
+
evaluationId: evaluation.id,
|
|
544
|
+
...payload,
|
|
545
|
+
};
|
|
546
|
+
try {
|
|
547
|
+
const result = await this.options.actionExecutor.execute({ automation, evaluation, promptVariables });
|
|
548
|
+
run = this.options.store.updateAutomationRun(run.id, this.successfulRunUpdate(result));
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
const message = normalizeAutomationError(error);
|
|
552
|
+
run = this.options.store.updateAutomationRun(run.id, {
|
|
553
|
+
status: 'failed',
|
|
554
|
+
finishedAt: this.now().toISOString(),
|
|
555
|
+
deliveryStatus: 'failed',
|
|
556
|
+
error: message,
|
|
557
|
+
});
|
|
558
|
+
this.options.log?.(normalizeAutomationError(run.error, `automation action failed ${automation.id}: `));
|
|
559
|
+
}
|
|
560
|
+
this.emitRun(run);
|
|
561
|
+
return run;
|
|
562
|
+
}
|
|
563
|
+
successfulRunUpdate(result) {
|
|
564
|
+
const bridgeActivity = {
|
|
565
|
+
...(result.deliveryMode ? { deliveryMode: result.deliveryMode } : {}),
|
|
566
|
+
...(result.lastBridgeEventAt ? { lastBridgeEventAt: result.lastBridgeEventAt } : {}),
|
|
567
|
+
};
|
|
568
|
+
return {
|
|
569
|
+
status: 'succeeded',
|
|
570
|
+
threadId: result.threadId,
|
|
571
|
+
acpRunId: result.acpRunId,
|
|
572
|
+
finishedAt: this.now().toISOString(),
|
|
573
|
+
deliveryStatus: result.deliveryStatus === 'failed' ? 'failed' : 'delivered',
|
|
574
|
+
...(result.deliveryError ? { error: normalizeAutomationError(result.deliveryError) } : {}),
|
|
575
|
+
...(Object.keys(bridgeActivity).length > 0 ? { bridgeActivity } : {}),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
createEvaluation(automation, now) {
|
|
579
|
+
const evaluation = this.options.store.createAutomationEvaluation(automation.id, {
|
|
580
|
+
activationKind: automation.activation.kind,
|
|
581
|
+
...(automation.condition.kind === 'approved-script'
|
|
582
|
+
? { scriptVersionId: automation.condition.approvedVersionId }
|
|
583
|
+
: {}),
|
|
584
|
+
startedAt: now.toISOString(),
|
|
585
|
+
});
|
|
586
|
+
this.emitEvaluation(evaluation);
|
|
587
|
+
return evaluation;
|
|
588
|
+
}
|
|
589
|
+
finishEvaluation(evaluationId, input) {
|
|
590
|
+
const evaluation = this.options.store.finishAutomationEvaluation(evaluationId, input);
|
|
591
|
+
this.emitEvaluation(evaluation);
|
|
592
|
+
return evaluation;
|
|
593
|
+
}
|
|
594
|
+
updateDefinitionAfterEvaluation(automation, now, input) {
|
|
595
|
+
const next = (0, automation_trigger_engine_js_1.nextActivationAt)(automation.activation, now);
|
|
596
|
+
const updated = this.options.store.updateAutomationState(automation.id, {
|
|
597
|
+
...(input.nextMatch === undefined ? {} : { lastSuccessfulMatch: input.nextMatch }),
|
|
598
|
+
lastEvaluationAt: now.toISOString(),
|
|
599
|
+
...(input.triggered ? { lastTriggeredAt: now.toISOString() } : {}),
|
|
600
|
+
consecutiveEvaluationFailures: input.failureCount,
|
|
601
|
+
nextCheckAt: next?.toISOString() || null,
|
|
602
|
+
});
|
|
603
|
+
this.emitDefinition(updated);
|
|
604
|
+
return updated;
|
|
605
|
+
}
|
|
606
|
+
persistInitialNextCheck(automation, replace = false, activationReplaced = false) {
|
|
607
|
+
if (!replace && this.options.store.getAutomationNextCheckAt(automation.id) !== null)
|
|
608
|
+
return automation;
|
|
609
|
+
return this.options.store.updateAutomationState(automation.id, {
|
|
610
|
+
nextCheckAt: this.initialNextCheckAt(automation, activationReplaced),
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
initialNextCheckAt(automation, activationReplaced = false) {
|
|
614
|
+
const now = this.now();
|
|
615
|
+
let next;
|
|
616
|
+
if ((!automation.enabled && !activationReplaced) || automation.activation.kind === 'provider-event') {
|
|
617
|
+
next = null;
|
|
618
|
+
}
|
|
619
|
+
else if (automation.activation.kind === 'once') {
|
|
620
|
+
next = new Date(automation.activation.runAt);
|
|
621
|
+
}
|
|
622
|
+
else if (automation.lastEvaluationAt) {
|
|
623
|
+
next = (0, automation_trigger_engine_js_1.missedActivationAt)(automation.activation, automation.lastEvaluationAt, now)
|
|
624
|
+
|| (0, automation_trigger_engine_js_1.nextActivationAt)(automation.activation, now);
|
|
625
|
+
}
|
|
626
|
+
else if (automation.activation.kind === 'interval') {
|
|
627
|
+
next = new Date(Math.floor(now.getTime() / automation.activation.intervalMs) * automation.activation.intervalMs);
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
const baseline = new Date(Math.min(Date.parse(automation.createdAt), now.getTime() - 24 * 60 * 60 * 1_000));
|
|
631
|
+
next = (0, automation_trigger_engine_js_1.missedActivationAt)(automation.activation, baseline.toISOString(), now)
|
|
632
|
+
|| (0, automation_trigger_engine_js_1.nextActivationAt)(automation.activation, now);
|
|
633
|
+
}
|
|
634
|
+
return next?.toISOString() || null;
|
|
635
|
+
}
|
|
636
|
+
shouldPoll(automation) {
|
|
637
|
+
return automation.enabled
|
|
638
|
+
&& automation.health === 'healthy'
|
|
639
|
+
&& automation.activation.kind !== 'provider-event'
|
|
640
|
+
&& (this.options.ownershipPolicy || exports.NATIVE_AUTOMATION_OWNERSHIP).executes(automation);
|
|
641
|
+
}
|
|
642
|
+
isConsumedOnce(automation) {
|
|
643
|
+
if (automation.activation.kind !== 'once')
|
|
644
|
+
return false;
|
|
645
|
+
if (automation.lastEvaluationAt !== undefined)
|
|
646
|
+
return true;
|
|
647
|
+
return this.listEvaluations(automation.id).some((evaluation) => evaluation.status === 'finished');
|
|
648
|
+
}
|
|
649
|
+
requireAutomation(automationId) {
|
|
650
|
+
const automation = this.get(automationId);
|
|
651
|
+
if (!automation)
|
|
652
|
+
throw new Error(`Automation not found: ${automationId}`);
|
|
653
|
+
return automation;
|
|
654
|
+
}
|
|
655
|
+
now() {
|
|
656
|
+
const now = (this.options.clock || (() => new Date()))();
|
|
657
|
+
if (!Number.isFinite(now.getTime()))
|
|
658
|
+
throw new Error('Automation clock returned an invalid date.');
|
|
659
|
+
return new Date(now);
|
|
660
|
+
}
|
|
661
|
+
emitDefinition(automation) {
|
|
662
|
+
this.emitEvent({ type: 'automation.definition.updated', payload: automation });
|
|
663
|
+
try {
|
|
664
|
+
if (automation.originKind === 'scheduled-job') {
|
|
665
|
+
this.emitEvent({
|
|
666
|
+
type: 'scheduler.job.updated',
|
|
667
|
+
payload: (0, legacy_automation_mappers_js_1.automationToScheduledJob)(automation, (0, legacy_automation_mappers_js_1.latestAutomationRun)(this.listRuns(automation.id))),
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
else if (automation.originKind === 'automation-monitor') {
|
|
671
|
+
const latest = (0, legacy_automation_mappers_js_1.latestFinishedEvaluation)(this.listEvaluations(automation.id));
|
|
672
|
+
this.emitEvent({
|
|
673
|
+
type: 'automation.monitor.updated',
|
|
674
|
+
payload: (0, legacy_automation_mappers_js_1.automationToMonitor)(automation, latest, (0, legacy_automation_mappers_js_1.latestAutomationRun)(this.listRuns(automation.id)), this.getLatestEvaluationWithState(automation.id)),
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
this.reportDiagnostic('event-projection', normalizeAutomationError(error, 'Automation event projection failed: '));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
emitEvaluation(evaluation) {
|
|
683
|
+
this.emitEvent({ type: 'automation.evaluation.updated', payload: evaluation });
|
|
684
|
+
}
|
|
685
|
+
emitRun(run) {
|
|
686
|
+
this.emitEvent({ type: 'automation.run.updated', payload: run });
|
|
687
|
+
try {
|
|
688
|
+
const automation = this.get(run.automationId);
|
|
689
|
+
const evaluation = this.listEvaluations(run.automationId).find((candidate) => candidate.id === run.evaluationId);
|
|
690
|
+
if (!automation || !evaluation)
|
|
691
|
+
return;
|
|
692
|
+
if (automation.originKind === 'scheduled-job') {
|
|
693
|
+
this.emitEvent({ type: 'scheduler.run.updated', payload: (0, legacy_automation_mappers_js_1.automationToScheduledJobRun)(evaluation, run) });
|
|
694
|
+
}
|
|
695
|
+
else if (automation.originKind === 'automation-monitor') {
|
|
696
|
+
this.emitEvent({ type: 'automation.monitor.run.updated', payload: (0, legacy_automation_mappers_js_1.automationToMonitorRun)(evaluation, run) });
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
this.reportDiagnostic('event-projection', normalizeAutomationError(error, 'Automation event projection failed: '));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
emitCompatibilityEvaluationRun(automation, evaluation) {
|
|
704
|
+
try {
|
|
705
|
+
if (automation.originKind === 'scheduled-job') {
|
|
706
|
+
this.emitEvent({
|
|
707
|
+
type: 'scheduler.run.updated',
|
|
708
|
+
payload: (0, legacy_automation_mappers_js_1.automationToScheduledJobRun)(evaluation),
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
else if (automation.originKind === 'automation-monitor') {
|
|
712
|
+
this.emitEvent({
|
|
713
|
+
type: 'automation.monitor.run.updated',
|
|
714
|
+
payload: (0, legacy_automation_mappers_js_1.automationToMonitorRun)(evaluation),
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
catch (error) {
|
|
719
|
+
this.reportDiagnostic('event-projection', normalizeAutomationError(error, 'Automation event projection failed: '));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
providerEventContext(automation, event) {
|
|
723
|
+
const occurredAt = normalizeProviderEventTimestamp(event.occurredAt);
|
|
724
|
+
const previousEvaluation = this.getLatestEvaluationWithState(automation.id);
|
|
725
|
+
const previous = previousEvaluation?.status === 'finished' ? previousEvaluation.nextState : undefined;
|
|
726
|
+
const nextState = {
|
|
727
|
+
...(previous || {}),
|
|
728
|
+
...event.payload,
|
|
729
|
+
payload: event.payload,
|
|
730
|
+
lastEventId: event.id,
|
|
731
|
+
lastEventAt: occurredAt,
|
|
732
|
+
};
|
|
733
|
+
return {
|
|
734
|
+
payload: {
|
|
735
|
+
...event.payload,
|
|
736
|
+
...(!Object.prototype.hasOwnProperty.call(event.payload, 'subject') ? { subject: event.subject } : {}),
|
|
737
|
+
...(!Object.prototype.hasOwnProperty.call(event.payload, 'sourceType') ? { sourceType: event.sourceType } : {}),
|
|
738
|
+
eventSubject: event.subject,
|
|
739
|
+
eventSourceType: event.sourceType,
|
|
740
|
+
occurredAt,
|
|
741
|
+
timestamp: occurredAt,
|
|
742
|
+
summary: event.summary || '',
|
|
743
|
+
eventSnapshot: event,
|
|
744
|
+
previous: previous || {},
|
|
745
|
+
},
|
|
746
|
+
nextState,
|
|
747
|
+
occurredAt,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
emitEvent(event) {
|
|
751
|
+
try {
|
|
752
|
+
this.options.eventBus.emit(event);
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
this.reportDiagnostic('event-projection', normalizeAutomationError(error, 'Automation event projection failed: '));
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
clearTimer() {
|
|
759
|
+
if (this.timer === undefined)
|
|
760
|
+
return;
|
|
761
|
+
(this.options.clearInterval || clearInterval)(this.timer);
|
|
762
|
+
this.timer = undefined;
|
|
763
|
+
}
|
|
764
|
+
runTimerTick(generation) {
|
|
765
|
+
if (!this.isActiveGeneration(generation))
|
|
766
|
+
return;
|
|
767
|
+
void this.tick().catch((error) => this.handleTimerFailure(error, generation));
|
|
768
|
+
}
|
|
769
|
+
handleTimerFailure(error, generation) {
|
|
770
|
+
const reason = normalizeAutomationError(error, 'Automation timer tick failed: ');
|
|
771
|
+
if (this.isActiveGeneration(generation)) {
|
|
772
|
+
try {
|
|
773
|
+
this.clearTimer();
|
|
774
|
+
}
|
|
775
|
+
catch {
|
|
776
|
+
this.timer = undefined;
|
|
777
|
+
}
|
|
778
|
+
this.runtimeStatus = { status: 'degraded', reason };
|
|
779
|
+
}
|
|
780
|
+
this.reportDiagnostic('timer-tick', reason);
|
|
781
|
+
}
|
|
782
|
+
isActiveGeneration(generation) {
|
|
783
|
+
return generation === this.lifecycleGeneration
|
|
784
|
+
&& !this.stopping
|
|
785
|
+
&& this.runtimeStatus.status === 'running';
|
|
786
|
+
}
|
|
787
|
+
async settleActiveWork() {
|
|
788
|
+
const tick = this.tickPromise;
|
|
789
|
+
if (tick)
|
|
790
|
+
await Promise.allSettled([tick]);
|
|
791
|
+
await Promise.allSettled([...this.inFlight.values()]);
|
|
792
|
+
}
|
|
793
|
+
reportDiagnostic(phase, error) {
|
|
794
|
+
try {
|
|
795
|
+
this.options.log?.(error);
|
|
796
|
+
}
|
|
797
|
+
catch {
|
|
798
|
+
// Diagnostics must not destabilize automation lifecycle handling.
|
|
799
|
+
}
|
|
800
|
+
try {
|
|
801
|
+
this.options.eventBus.emit({
|
|
802
|
+
type: 'localcore.error',
|
|
803
|
+
payload: { scope: 'automation-service', error, context: { phase } },
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
catch {
|
|
807
|
+
// Diagnostics must not destabilize automation lifecycle handling.
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
exports.AutomationService = AutomationService;
|
|
812
|
+
function normalizeAutomationError(error, prefix = '') {
|
|
813
|
+
let raw;
|
|
814
|
+
try {
|
|
815
|
+
raw = error instanceof Error ? error.message : String(error);
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
raw = 'Unprintable error';
|
|
819
|
+
}
|
|
820
|
+
const withoutControls = `${prefix}${raw}`.replace(/[\u0000-\u001f\u007f-\u009f]/g, ' ');
|
|
821
|
+
return (0, local_core_acp_store_js_1.redactSecrets)(withoutControls)
|
|
822
|
+
.replace(/\b(password|api[-_]?key)\s*[:=]\s*[^\s]+/gi, '$1=[REDACTED_SECRET]')
|
|
823
|
+
.slice(0, exports.AUTOMATION_ERROR_MAX_LENGTH);
|
|
824
|
+
}
|
|
825
|
+
function providerLifecycleBlockReason(error) {
|
|
826
|
+
const normalized = normalizeAutomationError(error);
|
|
827
|
+
return normalized.startsWith(exports.PROVIDER_LIFECYCLE_BLOCK_PREFIX)
|
|
828
|
+
? normalized
|
|
829
|
+
: normalizeAutomationError(normalized, exports.PROVIDER_LIFECYCLE_BLOCK_PREFIX);
|
|
830
|
+
}
|
|
831
|
+
function normalizeProviderEventTimestamp(value) {
|
|
832
|
+
if (typeof value !== 'string'
|
|
833
|
+
|| !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(value.trim())) {
|
|
834
|
+
throw new Error('Provider event occurredAt must be a valid ISO timestamp.');
|
|
835
|
+
}
|
|
836
|
+
const timestamp = new Date(value);
|
|
837
|
+
if (!Number.isFinite(timestamp.getTime())) {
|
|
838
|
+
throw new Error('Provider event occurredAt must be a valid ISO timestamp.');
|
|
839
|
+
}
|
|
840
|
+
return timestamp.toISOString();
|
|
841
|
+
}
|
|
842
|
+
function normalizeProviderEventSnapshot(value) {
|
|
843
|
+
if (!isPlainRecord(value))
|
|
844
|
+
throw new Error('Provider event must be a plain object.');
|
|
845
|
+
assertOwnDataProperties(value, 'Provider event');
|
|
846
|
+
for (const field of ['id', 'sourceType', 'subject']) {
|
|
847
|
+
const fieldValue = ownDataProperty(value, field);
|
|
848
|
+
if (typeof fieldValue !== 'string' || !fieldValue.trim()) {
|
|
849
|
+
throw new Error(`Provider event ${field} must be a non-empty string.`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
const summary = ownDataProperty(value, 'summary');
|
|
853
|
+
if (summary !== undefined && typeof summary !== 'string') {
|
|
854
|
+
throw new Error('Provider event summary must be a string when provided.');
|
|
855
|
+
}
|
|
856
|
+
const occurredAt = ownDataProperty(value, 'occurredAt');
|
|
857
|
+
const normalizedOccurredAt = normalizeProviderEventTimestampStrict(occurredAt);
|
|
858
|
+
const topLevelStrings = [
|
|
859
|
+
ownDataProperty(value, 'id'),
|
|
860
|
+
ownDataProperty(value, 'sourceType'),
|
|
861
|
+
ownDataProperty(value, 'subject'),
|
|
862
|
+
occurredAt,
|
|
863
|
+
...(summary === undefined ? [] : [summary]),
|
|
864
|
+
];
|
|
865
|
+
if (topLevelStrings.some((field) => field.length > PROVIDER_EVENT_STRING_MAX_LENGTH)) {
|
|
866
|
+
throw new Error('Provider event string field exceeds the maximum length.');
|
|
867
|
+
}
|
|
868
|
+
const topLevelSize = topLevelStrings.reduce((total, field) => total + field.length, 0);
|
|
869
|
+
if (topLevelSize > PROVIDER_JSON_MAX_SIZE)
|
|
870
|
+
throw new Error('Provider event exceeds the maximum total size.');
|
|
871
|
+
const payloadValue = ownDataProperty(value, 'payload');
|
|
872
|
+
if (!isPlainRecord(payloadValue))
|
|
873
|
+
throw new Error('Provider event payload must be a plain object.');
|
|
874
|
+
let payload;
|
|
875
|
+
try {
|
|
876
|
+
payload = cloneProviderJsonValue(payloadValue, '$', {
|
|
877
|
+
ancestors: new WeakSet(),
|
|
878
|
+
size: topLevelSize,
|
|
879
|
+
}, 0);
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
throw new Error(normalizeAutomationError(error, 'Invalid provider event payload: '));
|
|
883
|
+
}
|
|
884
|
+
return {
|
|
885
|
+
id: ownDataProperty(value, 'id'),
|
|
886
|
+
sourceType: ownDataProperty(value, 'sourceType'),
|
|
887
|
+
subject: ownDataProperty(value, 'subject'),
|
|
888
|
+
occurredAt: normalizedOccurredAt,
|
|
889
|
+
...(summary === undefined ? {} : { summary }),
|
|
890
|
+
payload,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
function cloneProviderJsonValue(value, path, state, depth) {
|
|
894
|
+
if (depth > PROVIDER_JSON_MAX_DEPTH)
|
|
895
|
+
throw new Error(`${path} exceeds the maximum nesting depth.`);
|
|
896
|
+
state.size += typeof value === 'string' ? value.length + 1 : 1;
|
|
897
|
+
if (state.size > PROVIDER_JSON_MAX_SIZE)
|
|
898
|
+
throw new Error(`${path} exceeds the maximum payload size.`);
|
|
899
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
900
|
+
return value;
|
|
901
|
+
if (typeof value === 'number') {
|
|
902
|
+
if (!Number.isFinite(value))
|
|
903
|
+
throw new Error(`${path} must contain only finite numbers.`);
|
|
904
|
+
return value;
|
|
905
|
+
}
|
|
906
|
+
if (typeof value !== 'object')
|
|
907
|
+
throw new Error(`${path} contains a non-JSON value.`);
|
|
908
|
+
if (state.ancestors.has(value))
|
|
909
|
+
throw new Error(`${path} contains a cycle.`);
|
|
910
|
+
state.ancestors.add(value);
|
|
911
|
+
try {
|
|
912
|
+
if (Array.isArray(value)) {
|
|
913
|
+
if (Object.getPrototypeOf(value) !== Array.prototype)
|
|
914
|
+
throw new Error(`${path} must be a plain array.`);
|
|
915
|
+
if (Object.getOwnPropertySymbols(value).length > 0)
|
|
916
|
+
throw new Error(`${path} contains a symbol property.`);
|
|
917
|
+
const keys = Object.getOwnPropertyNames(value);
|
|
918
|
+
for (const key of keys) {
|
|
919
|
+
if (key !== 'length' && !isArrayIndex(key, value.length)) {
|
|
920
|
+
throw new Error(`${path} contains a non-index array property.`);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
const clone = [];
|
|
924
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
925
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
926
|
+
if (!descriptor)
|
|
927
|
+
throw new Error(`${path}[${index}] must not be sparse.`);
|
|
928
|
+
if (!('value' in descriptor))
|
|
929
|
+
throw new Error(`${path}[${index}] must be a data property.`);
|
|
930
|
+
clone.push(cloneProviderJsonValue(descriptor.value, `${path}[${index}]`, state, depth + 1));
|
|
931
|
+
}
|
|
932
|
+
return clone;
|
|
933
|
+
}
|
|
934
|
+
if (!isPlainRecord(value))
|
|
935
|
+
throw new Error(`${path} must contain only plain objects and arrays.`);
|
|
936
|
+
if (Object.getOwnPropertySymbols(value).length > 0)
|
|
937
|
+
throw new Error(`${path} contains a symbol property.`);
|
|
938
|
+
const clone = {};
|
|
939
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
940
|
+
state.size += key.length;
|
|
941
|
+
if (state.size > PROVIDER_JSON_MAX_SIZE)
|
|
942
|
+
throw new Error(`${path} exceeds the maximum payload size.`);
|
|
943
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
944
|
+
const propertyPath = providerJsonPropertyPath(path, key);
|
|
945
|
+
if (!('value' in descriptor))
|
|
946
|
+
throw new Error(`${propertyPath} must be a data property.`);
|
|
947
|
+
Object.defineProperty(clone, key, {
|
|
948
|
+
value: cloneProviderJsonValue(descriptor.value, propertyPath, state, depth + 1),
|
|
949
|
+
enumerable: true,
|
|
950
|
+
configurable: true,
|
|
951
|
+
writable: true,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
return clone;
|
|
955
|
+
}
|
|
956
|
+
finally {
|
|
957
|
+
state.ancestors.delete(value);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function assertOwnDataProperties(value, context) {
|
|
961
|
+
if (Object.getOwnPropertySymbols(value).length > 0)
|
|
962
|
+
throw new Error(`${context} must not contain symbol properties.`);
|
|
963
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
964
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
965
|
+
if (!('value' in descriptor))
|
|
966
|
+
throw new Error(`${context} ${key} must be a data property.`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
function ownDataProperty(value, key) {
|
|
970
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
971
|
+
return descriptor && 'value' in descriptor ? descriptor.value : undefined;
|
|
972
|
+
}
|
|
973
|
+
function isArrayIndex(key, length) {
|
|
974
|
+
if (!/^(0|[1-9]\d*)$/.test(key))
|
|
975
|
+
return false;
|
|
976
|
+
const index = Number(key);
|
|
977
|
+
return Number.isSafeInteger(index) && index >= 0 && index < length;
|
|
978
|
+
}
|
|
979
|
+
function providerJsonPropertyPath(path, key) {
|
|
980
|
+
return /^[A-Za-z_$][\w$]*$/.test(key)
|
|
981
|
+
? `${path}.${key}`
|
|
982
|
+
: `${path}[${JSON.stringify(key.slice(0, 80))}${key.length > 80 ? '…' : ''}]`;
|
|
983
|
+
}
|
|
984
|
+
function normalizeProviderEventTimestampStrict(value) {
|
|
985
|
+
const raw = typeof value === 'string' ? value.trim() : '';
|
|
986
|
+
const match = raw.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})$/);
|
|
987
|
+
if (!match)
|
|
988
|
+
throw new Error('Provider event occurredAt must be a valid ISO timestamp.');
|
|
989
|
+
const year = Number(match[1]);
|
|
990
|
+
const month = Number(match[2]);
|
|
991
|
+
const day = Number(match[3]);
|
|
992
|
+
const hour = Number(match[4]);
|
|
993
|
+
const minute = Number(match[5]);
|
|
994
|
+
const second = Number(match[6]);
|
|
995
|
+
const fraction = String(match[7] || '').padEnd(3, '0').slice(0, 3);
|
|
996
|
+
const local = new Date(0);
|
|
997
|
+
local.setUTCFullYear(year, month - 1, day);
|
|
998
|
+
local.setUTCHours(hour, minute, second, Number(fraction));
|
|
999
|
+
if (local.getUTCFullYear() !== year || local.getUTCMonth() !== month - 1 || local.getUTCDate() !== day
|
|
1000
|
+
|| local.getUTCHours() !== hour || local.getUTCMinutes() !== minute || local.getUTCSeconds() !== second)
|
|
1001
|
+
throw new Error('Provider event occurredAt must be a valid ISO timestamp.');
|
|
1002
|
+
const zone = String(match[8]);
|
|
1003
|
+
let offsetMinutes = 0;
|
|
1004
|
+
if (zone !== 'Z') {
|
|
1005
|
+
const zoneMatch = zone.match(/^([+-])(\d{2}):(\d{2})$/);
|
|
1006
|
+
const offsetHour = Number(zoneMatch[2]);
|
|
1007
|
+
const offsetMinute = Number(zoneMatch[3]);
|
|
1008
|
+
if (offsetHour > 23 || offsetMinute > 59)
|
|
1009
|
+
throw new Error('Provider event occurredAt must be a valid ISO timestamp.');
|
|
1010
|
+
offsetMinutes = (offsetHour * 60 + offsetMinute) * (zoneMatch[1] === '+' ? 1 : -1);
|
|
1011
|
+
}
|
|
1012
|
+
return new Date(local.getTime() - offsetMinutes * 60_000).toISOString();
|
|
1013
|
+
}
|
|
1014
|
+
function isPlainRecord(value) {
|
|
1015
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
1016
|
+
return false;
|
|
1017
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1018
|
+
return prototype === Object.prototype || prototype === null;
|
|
1019
|
+
}
|