@clinebot/core 0.0.36 → 0.0.37
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/dist/ClineCore.d.ts +312 -3
- package/dist/ClineCore.d.ts.map +1 -1
- package/dist/account/cline-account-service.d.ts.map +1 -1
- package/dist/cron/cron-event-ingress.d.ts +38 -0
- package/dist/cron/cron-event-ingress.d.ts.map +1 -0
- package/dist/cron/cron-materializer.d.ts +36 -0
- package/dist/cron/cron-materializer.d.ts.map +1 -0
- package/dist/cron/cron-reconciler.d.ts +62 -0
- package/dist/cron/cron-reconciler.d.ts.map +1 -0
- package/dist/cron/cron-report-writer.d.ts +41 -0
- package/dist/cron/cron-report-writer.d.ts.map +1 -0
- package/dist/cron/cron-runner.d.ts +43 -0
- package/dist/cron/cron-runner.d.ts.map +1 -0
- package/dist/cron/cron-schema.d.ts +3 -0
- package/dist/cron/cron-schema.d.ts.map +1 -0
- package/dist/cron/cron-service.d.ts +57 -0
- package/dist/cron/cron-service.d.ts.map +1 -0
- package/dist/cron/cron-spec-parser.d.ts +27 -0
- package/dist/cron/cron-spec-parser.d.ts.map +1 -0
- package/dist/cron/cron-watcher.d.ts +23 -0
- package/dist/cron/cron-watcher.d.ts.map +1 -0
- package/dist/cron/scheduler.d.ts +3 -1
- package/dist/cron/scheduler.d.ts.map +1 -1
- package/dist/cron/sqlite-cron-store.d.ts +230 -0
- package/dist/cron/sqlite-cron-store.d.ts.map +1 -0
- package/dist/extensions/plugin/plugin-config-loader.d.ts +7 -1
- package/dist/extensions/plugin/plugin-config-loader.d.ts.map +1 -1
- package/dist/extensions/plugin/plugin-loader.d.ts +10 -6
- package/dist/extensions/plugin/plugin-loader.d.ts.map +1 -1
- package/dist/extensions/plugin/plugin-sandbox.d.ts +7 -1
- package/dist/extensions/plugin/plugin-sandbox.d.ts.map +1 -1
- package/dist/extensions/plugin-sandbox-bootstrap.js +236 -275
- package/dist/extensions/tools/constants.d.ts +1 -0
- package/dist/extensions/tools/constants.d.ts.map +1 -1
- package/dist/extensions/tools/definitions.d.ts +2 -3
- package/dist/extensions/tools/definitions.d.ts.map +1 -1
- package/dist/extensions/tools/executors/editor.d.ts.map +1 -1
- package/dist/extensions/tools/helpers.d.ts +1 -0
- package/dist/extensions/tools/helpers.d.ts.map +1 -1
- package/dist/extensions/tools/index.d.ts +1 -2
- package/dist/extensions/tools/index.d.ts.map +1 -1
- package/dist/extensions/tools/presets.d.ts +1 -1
- package/dist/extensions/tools/schemas.d.ts +25 -3
- package/dist/extensions/tools/schemas.d.ts.map +1 -1
- package/dist/extensions/tools/team/delegated-agent.d.ts +2 -2
- package/dist/extensions/tools/team/delegated-agent.d.ts.map +1 -1
- package/dist/extensions/tools/team/multi-agent.d.ts +7 -3
- package/dist/extensions/tools/team/multi-agent.d.ts.map +1 -1
- package/dist/extensions/tools/team/team-tools.d.ts.map +1 -1
- package/dist/extensions/tools/types.d.ts +0 -5
- package/dist/extensions/tools/types.d.ts.map +1 -1
- package/dist/hooks/hook-bridge.d.ts +118 -0
- package/dist/hooks/hook-bridge.d.ts.map +1 -0
- package/dist/hooks/hook-file-hooks.d.ts +2 -1
- package/dist/hooks/hook-file-hooks.d.ts.map +1 -1
- package/dist/hooks/hook-registry.d.ts +16 -0
- package/dist/hooks/hook-registry.d.ts.map +1 -0
- package/dist/hub/browser-websocket.d.ts.map +1 -1
- package/dist/hub/client.d.ts +7 -1
- package/dist/hub/client.d.ts.map +1 -1
- package/dist/hub/daemon-entry.js +721 -461
- package/dist/hub/daemon.d.ts.map +1 -1
- package/dist/hub/defaults.d.ts +8 -4
- package/dist/hub/defaults.d.ts.map +1 -1
- package/dist/hub/index.js +665 -415
- package/dist/hub/runtime-handlers.d.ts.map +1 -1
- package/dist/hub/server.d.ts +18 -0
- package/dist/hub/server.d.ts.map +1 -1
- package/dist/hub/session-client.d.ts +3 -0
- package/dist/hub/session-client.d.ts.map +1 -1
- package/dist/hub/start-shared-server.d.ts.map +1 -1
- package/dist/hub/ui-client.d.ts +1 -0
- package/dist/hub/ui-client.d.ts.map +1 -1
- package/dist/index.d.ts +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +756 -467
- package/dist/llms/cline-recommended-models.d.ts +20 -0
- package/dist/llms/cline-recommended-models.d.ts.map +1 -0
- package/dist/llms/handler-factory.d.ts +16 -0
- package/dist/llms/handler-factory.d.ts.map +1 -0
- package/dist/llms/provider-defaults.d.ts.map +1 -1
- package/dist/llms/provider-settings.d.ts +45 -2
- package/dist/llms/provider-settings.d.ts.map +1 -1
- package/dist/llms/runtime-registry.d.ts.map +1 -1
- package/dist/runtime/agent-config-adapter.d.ts +148 -0
- package/dist/runtime/agent-config-adapter.d.ts.map +1 -0
- package/dist/runtime/agent-runtime-config-builder.d.ts +96 -0
- package/dist/runtime/agent-runtime-config-builder.d.ts.map +1 -0
- package/dist/runtime/history.d.ts +6 -0
- package/dist/runtime/history.d.ts.map +1 -1
- package/dist/runtime/host.d.ts.map +1 -1
- package/dist/runtime/loop-detection.d.ts +59 -0
- package/dist/runtime/loop-detection.d.ts.map +1 -0
- package/dist/runtime/mistake-tracker.d.ts +69 -0
- package/dist/runtime/mistake-tracker.d.ts.map +1 -0
- package/dist/runtime/runtime-builder.d.ts.map +1 -1
- package/dist/runtime/runtime-event-adapter.d.ts +102 -0
- package/dist/runtime/runtime-event-adapter.d.ts.map +1 -0
- package/dist/runtime/runtime-host.d.ts +28 -3
- package/dist/runtime/runtime-host.d.ts.map +1 -1
- package/dist/runtime/session-runtime-orchestrator.d.ts +261 -0
- package/dist/runtime/session-runtime-orchestrator.d.ts.map +1 -0
- package/dist/runtime/session-runtime.d.ts +16 -3
- package/dist/runtime/session-runtime.d.ts.map +1 -1
- package/dist/runtime/user-input-builder.d.ts +24 -0
- package/dist/runtime/user-input-builder.d.ts.map +1 -0
- package/dist/services/index.js +28 -0
- package/dist/services/local-runtime-bootstrap.d.ts.map +1 -1
- package/dist/services/plugin-tools.d.ts.map +1 -1
- package/dist/services/providers/local-provider-registry.d.ts +197 -21
- package/dist/services/providers/local-provider-registry.d.ts.map +1 -1
- package/dist/services/providers/local-provider-service.d.ts +3 -1
- package/dist/services/providers/local-provider-service.d.ts.map +1 -1
- package/dist/services/session-data.d.ts.map +1 -1
- package/dist/services/session-telemetry.d.ts +7 -2
- package/dist/services/session-telemetry.d.ts.map +1 -1
- package/dist/services/storage/file-team-store.d.ts.map +1 -1
- package/dist/services/storage/provider-settings-legacy-migration.d.ts.map +1 -1
- package/dist/services/storage/provider-settings-manager.d.ts +1 -0
- package/dist/services/storage/provider-settings-manager.d.ts.map +1 -1
- package/dist/services/storage/sqlite-team-store.d.ts.map +1 -1
- package/dist/session/conversation-store.d.ts +30 -0
- package/dist/session/conversation-store.d.ts.map +1 -0
- package/dist/session/message-builder.d.ts +65 -0
- package/dist/session/message-builder.d.ts.map +1 -0
- package/dist/session/session-manifest.d.ts +1 -1
- package/dist/transports/hub.d.ts +14 -3
- package/dist/transports/hub.d.ts.map +1 -1
- package/dist/transports/local.d.ts +14 -4
- package/dist/transports/local.d.ts.map +1 -1
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/types/chat-schema.d.ts +5 -5
- package/dist/types/config.d.ts +9 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/events.d.ts +7 -6
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/provider-settings.d.ts +2 -2
- package/dist/types/provider-settings.d.ts.map +1 -1
- package/dist/types/session.d.ts +5 -2
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types.d.ts +4 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ClineCore.ts +691 -6
- package/src/account/cline-account-service.ts +44 -6
- package/src/cron/cron-event-ingress.ts +357 -0
- package/src/cron/cron-materializer.ts +97 -0
- package/src/cron/cron-reconciler.ts +241 -0
- package/src/cron/cron-report-writer.ts +153 -0
- package/src/cron/cron-runner.ts +495 -0
- package/src/cron/cron-schema.ts +127 -0
- package/src/cron/cron-service.ts +163 -0
- package/src/cron/cron-spec-parser.ts +489 -0
- package/src/cron/cron-watcher.ts +102 -0
- package/src/cron/index.ts +10 -0
- package/src/cron/scheduler.ts +141 -6
- package/src/cron/sqlite-cron-store.ts +1286 -0
- package/src/extensions/plugin/plugin-config-loader.ts +21 -1
- package/src/extensions/plugin/plugin-loader.ts +25 -9
- package/src/extensions/plugin/plugin-sandbox-bootstrap.ts +151 -1
- package/src/extensions/plugin/plugin-sandbox.ts +131 -7
- package/src/extensions/tools/constants.ts +2 -0
- package/src/extensions/tools/definitions.ts +31 -22
- package/src/extensions/tools/executors/editor.ts +4 -3
- package/src/extensions/tools/helpers.ts +24 -0
- package/src/extensions/tools/index.ts +1 -2
- package/src/extensions/tools/presets.ts +1 -1
- package/src/extensions/tools/schemas.ts +13 -18
- package/src/extensions/tools/team/delegated-agent.ts +8 -3
- package/src/extensions/tools/team/multi-agent.ts +135 -19
- package/src/extensions/tools/team/team-tools.ts +151 -91
- package/src/extensions/tools/types.ts +0 -6
- package/src/hooks/hook-bridge.ts +489 -0
- package/src/hooks/hook-file-hooks.ts +58 -3
- package/src/hooks/hook-registry.ts +257 -0
- package/src/hub/browser-websocket.ts +26 -4
- package/src/hub/client.ts +72 -13
- package/src/hub/daemon-entry.ts +35 -0
- package/src/hub/daemon.ts +117 -14
- package/src/hub/defaults.ts +39 -12
- package/src/hub/runtime-handlers.ts +4 -3
- package/src/hub/server.ts +506 -77
- package/src/hub/session-client.ts +43 -1
- package/src/hub/start-shared-server.ts +3 -0
- package/src/hub/ui-client.ts +4 -0
- package/src/index.ts +46 -1
- package/src/llms/cline-recommended-models.ts +167 -0
- package/src/llms/handler-factory.ts +56 -0
- package/src/llms/provider-defaults.ts +17 -1
- package/src/llms/provider-settings.ts +48 -1
- package/src/llms/runtime-registry.ts +1 -0
- package/src/runtime/agent-config-adapter.ts +636 -0
- package/src/runtime/agent-runtime-config-builder.ts +205 -0
- package/src/runtime/error-feedback.ts +142 -0
- package/src/runtime/history.ts +137 -0
- package/src/runtime/host.ts +22 -0
- package/src/runtime/loop-detection.ts +162 -0
- package/src/runtime/mistake-tracker.ts +221 -0
- package/src/runtime/runtime-builder.ts +61 -5
- package/src/runtime/runtime-event-adapter.ts +412 -0
- package/src/runtime/runtime-host.ts +45 -1
- package/src/runtime/session-runtime-orchestrator.ts +1253 -0
- package/src/runtime/session-runtime.ts +16 -2
- package/src/runtime/user-input-builder.ts +167 -0
- package/src/services/local-runtime-bootstrap.ts +128 -22
- package/src/services/plugin-tools.ts +1 -0
- package/src/services/providers/local-provider-registry.ts +273 -57
- package/src/services/providers/local-provider-service.ts +67 -7
- package/src/services/session-data.ts +16 -14
- package/src/services/session-telemetry.ts +6 -15
- package/src/services/storage/file-team-store.ts +1 -5
- package/src/services/storage/provider-settings-legacy-migration.ts +8 -47
- package/src/services/storage/provider-settings-manager.ts +16 -1
- package/src/services/storage/sqlite-team-store.ts +1 -5
- package/src/session/conversation-store.ts +77 -0
- package/src/session/message-builder.ts +941 -0
- package/src/transports/hub.ts +458 -33
- package/src/transports/local.ts +296 -65
- package/src/transports/remote.ts +1 -0
- package/src/types/config.ts +9 -0
- package/src/types/events.ts +8 -6
- package/src/types/index.ts +3 -0
- package/src/types/provider-settings.ts +8 -1
- package/src/types/session.ts +5 -2
- package/src/types.ts +15 -1
- package/dist/cron/index.d.ts +0 -6
- package/dist/cron/index.d.ts.map +0 -1
- package/dist/services/telemetry/index.js +0 -28
|
@@ -19,6 +19,33 @@ interface ClineApiEnvelope<T> {
|
|
|
19
19
|
data?: T;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function getClineApiEnvelopeError(parsed: unknown): string | undefined {
|
|
23
|
+
if (typeof parsed !== "object" || parsed === null || !("error" in parsed)) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const error = parsed.error;
|
|
27
|
+
return typeof error === "string" && error.trim() ? error : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function formatClineAccountRequestFailure(
|
|
31
|
+
status: number,
|
|
32
|
+
bodyText: string,
|
|
33
|
+
parsed: unknown,
|
|
34
|
+
): string {
|
|
35
|
+
const envelopeError = getClineApiEnvelopeError(parsed);
|
|
36
|
+
if (envelopeError) {
|
|
37
|
+
return envelopeError;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const body = bodyText.trim();
|
|
41
|
+
if (body) {
|
|
42
|
+
const preview = body.length > 200 ? `${body.slice(0, 200)}...` : body;
|
|
43
|
+
return `Cline account request failed with status ${status}: ${preview}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return `Cline account request failed with status ${status}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
22
49
|
export interface ClineAccountServiceOptions {
|
|
23
50
|
apiBaseUrl: string;
|
|
24
51
|
getAuthToken: () => Promise<string | undefined | null>;
|
|
@@ -266,15 +293,26 @@ export class ClineAccountService {
|
|
|
266
293
|
const text = await response.text();
|
|
267
294
|
let parsed: unknown;
|
|
268
295
|
if (text.trim()) {
|
|
269
|
-
|
|
296
|
+
try {
|
|
297
|
+
parsed = JSON.parse(text);
|
|
298
|
+
} catch {
|
|
299
|
+
if (!response.ok) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
formatClineAccountRequestFailure(
|
|
302
|
+
response.status,
|
|
303
|
+
text,
|
|
304
|
+
undefined,
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
throw new Error("Cline account response was not valid JSON");
|
|
309
|
+
}
|
|
270
310
|
}
|
|
271
311
|
|
|
272
312
|
if (!response.ok) {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
: `Cline account request failed with status ${response.status}`;
|
|
277
|
-
throw new Error(message);
|
|
313
|
+
throw new Error(
|
|
314
|
+
formatClineAccountRequestFailure(response.status, text, parsed),
|
|
315
|
+
);
|
|
278
316
|
}
|
|
279
317
|
|
|
280
318
|
if (typeof parsed === "object" && parsed !== null) {
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import type { AutomationEventEnvelope, BasicLogger } from "@clinebot/shared";
|
|
2
|
+
import type {
|
|
3
|
+
CronEventLogRecord,
|
|
4
|
+
CronRunRecord,
|
|
5
|
+
CronSpecRecord,
|
|
6
|
+
SqliteCronStore,
|
|
7
|
+
} from "./sqlite-cron-store";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Durable ingress for normalized automation events.
|
|
11
|
+
*
|
|
12
|
+
* This layer persists the incoming event before matching, then materializes
|
|
13
|
+
* queued `cron_runs` for matching event specs. It deliberately does not
|
|
14
|
+
* execute agents; the normal runner claim loop owns execution.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface CronEventIngressOptions {
|
|
18
|
+
store: SqliteCronStore;
|
|
19
|
+
now?: () => number;
|
|
20
|
+
logger?: BasicLogger;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type CronEventSuppressionReason =
|
|
24
|
+
| "duplicate_event"
|
|
25
|
+
| "filter_mismatch"
|
|
26
|
+
| "dedupe_window"
|
|
27
|
+
| "cooldown";
|
|
28
|
+
|
|
29
|
+
export interface CronEventSuppression {
|
|
30
|
+
specId?: string;
|
|
31
|
+
externalId?: string;
|
|
32
|
+
reason: CronEventSuppressionReason;
|
|
33
|
+
dedupeKey?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CronEventIngressResult {
|
|
37
|
+
event: CronEventLogRecord;
|
|
38
|
+
duplicate: boolean;
|
|
39
|
+
matchedSpecs: CronSpecRecord[];
|
|
40
|
+
queuedRuns: CronRunRecord[];
|
|
41
|
+
suppressions: CronEventSuppression[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
45
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function trimOrUndefined(value: string | undefined): string | undefined {
|
|
49
|
+
const trimmed = value?.trim();
|
|
50
|
+
return trimmed ? trimmed : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeIso(value: string | undefined, fallback: string): string {
|
|
54
|
+
const candidate = value?.trim();
|
|
55
|
+
if (!candidate) return fallback;
|
|
56
|
+
const ms = Date.parse(candidate);
|
|
57
|
+
if (!Number.isFinite(ms)) return fallback;
|
|
58
|
+
return new Date(ms).toISOString();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function addSeconds(iso: string, seconds: number): string {
|
|
62
|
+
return new Date(
|
|
63
|
+
new Date(iso).getTime() + Math.max(0, Math.floor(seconds)) * 1000,
|
|
64
|
+
).toISOString();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function subtractSeconds(iso: string, seconds: number): string {
|
|
68
|
+
return new Date(
|
|
69
|
+
new Date(iso).getTime() - Math.max(0, Math.floor(seconds)) * 1000,
|
|
70
|
+
).toISOString();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function maxIso(a: string | undefined, b: string): string {
|
|
74
|
+
if (!a) return b;
|
|
75
|
+
return new Date(a).getTime() >= new Date(b).getTime() ? a : b;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function normalizeEvent(
|
|
79
|
+
event: AutomationEventEnvelope,
|
|
80
|
+
receivedAt: string,
|
|
81
|
+
): AutomationEventEnvelope {
|
|
82
|
+
const eventId = event.eventId.trim();
|
|
83
|
+
const eventType = event.eventType.trim();
|
|
84
|
+
const source = event.source.trim();
|
|
85
|
+
const subject = trimOrUndefined(event.subject);
|
|
86
|
+
const dedupeKey =
|
|
87
|
+
trimOrUndefined(event.dedupeKey) ??
|
|
88
|
+
`${eventType}:${source}:${subject ?? eventId}`;
|
|
89
|
+
return {
|
|
90
|
+
eventId,
|
|
91
|
+
eventType,
|
|
92
|
+
source,
|
|
93
|
+
subject,
|
|
94
|
+
occurredAt: normalizeIso(event.occurredAt, receivedAt),
|
|
95
|
+
workspaceRoot: trimOrUndefined(event.workspaceRoot),
|
|
96
|
+
payload: isRecord(event.payload) ? event.payload : undefined,
|
|
97
|
+
attributes: isRecord(event.attributes) ? event.attributes : undefined,
|
|
98
|
+
dedupeKey,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getPath(value: unknown, path: string): unknown {
|
|
103
|
+
if (!path) return undefined;
|
|
104
|
+
const parts = path.split(".");
|
|
105
|
+
let current: unknown = value;
|
|
106
|
+
for (const part of parts) {
|
|
107
|
+
if (!isRecord(current)) return undefined;
|
|
108
|
+
current = current[part];
|
|
109
|
+
}
|
|
110
|
+
return current;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveFilterValue(
|
|
114
|
+
event: AutomationEventEnvelope,
|
|
115
|
+
filterKey: string,
|
|
116
|
+
): unknown {
|
|
117
|
+
if (event.attributes && Object.hasOwn(event.attributes, filterKey)) {
|
|
118
|
+
return event.attributes[filterKey];
|
|
119
|
+
}
|
|
120
|
+
if (event.payload && Object.hasOwn(event.payload, filterKey)) {
|
|
121
|
+
return event.payload[filterKey];
|
|
122
|
+
}
|
|
123
|
+
const candidate = {
|
|
124
|
+
eventId: event.eventId,
|
|
125
|
+
eventType: event.eventType,
|
|
126
|
+
source: event.source,
|
|
127
|
+
subject: event.subject,
|
|
128
|
+
occurredAt: event.occurredAt,
|
|
129
|
+
workspaceRoot: event.workspaceRoot,
|
|
130
|
+
dedupeKey: event.dedupeKey,
|
|
131
|
+
attributes: event.attributes,
|
|
132
|
+
payload: event.payload,
|
|
133
|
+
};
|
|
134
|
+
const direct = getPath(candidate, filterKey);
|
|
135
|
+
if (direct !== undefined) return direct;
|
|
136
|
+
if (event.attributes) {
|
|
137
|
+
const fromAttributes = getPath(event.attributes, filterKey);
|
|
138
|
+
if (fromAttributes !== undefined) return fromAttributes;
|
|
139
|
+
}
|
|
140
|
+
if (event.payload) {
|
|
141
|
+
return getPath(event.payload, filterKey);
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function matchesExpected(actual: unknown, expected: unknown): boolean {
|
|
147
|
+
if (Array.isArray(expected)) {
|
|
148
|
+
return expected.some((item) => matchesExpected(actual, item));
|
|
149
|
+
}
|
|
150
|
+
if (Array.isArray(actual)) {
|
|
151
|
+
return actual.some((item) => matchesExpected(item, expected));
|
|
152
|
+
}
|
|
153
|
+
if (isRecord(expected)) {
|
|
154
|
+
if (!isRecord(actual)) return false;
|
|
155
|
+
return Object.entries(expected).every(([key, value]) =>
|
|
156
|
+
matchesExpected(actual[key], value),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return Object.is(actual, expected);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function automationEventMatchesFilters(
|
|
163
|
+
event: AutomationEventEnvelope,
|
|
164
|
+
filters: Record<string, unknown> | undefined,
|
|
165
|
+
): boolean {
|
|
166
|
+
if (!filters || Object.keys(filters).length === 0) return true;
|
|
167
|
+
return Object.entries(filters).every(([key, expected]) =>
|
|
168
|
+
matchesExpected(resolveFilterValue(event, key), expected),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export class CronEventIngress {
|
|
173
|
+
private readonly store: SqliteCronStore;
|
|
174
|
+
private readonly nowFn: () => number;
|
|
175
|
+
private readonly logger?: BasicLogger;
|
|
176
|
+
|
|
177
|
+
constructor(options: CronEventIngressOptions) {
|
|
178
|
+
this.store = options.store;
|
|
179
|
+
this.nowFn = options.now ?? (() => Date.now());
|
|
180
|
+
this.logger = options.logger;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public ingestEvent(event: AutomationEventEnvelope): CronEventIngressResult {
|
|
184
|
+
const receivedAt = new Date(this.nowFn()).toISOString();
|
|
185
|
+
const normalized = normalizeEvent(event, receivedAt);
|
|
186
|
+
const inserted = this.store.insertEventLog(normalized, {
|
|
187
|
+
receivedAtIso: receivedAt,
|
|
188
|
+
});
|
|
189
|
+
if (!inserted.created) {
|
|
190
|
+
this.logger?.debug("cron.event.duplicate", {
|
|
191
|
+
eventId: inserted.record.eventId,
|
|
192
|
+
eventType: inserted.record.eventType,
|
|
193
|
+
source: inserted.record.source,
|
|
194
|
+
});
|
|
195
|
+
return {
|
|
196
|
+
event: inserted.record,
|
|
197
|
+
duplicate: true,
|
|
198
|
+
matchedSpecs: [],
|
|
199
|
+
queuedRuns: [],
|
|
200
|
+
suppressions: [
|
|
201
|
+
{
|
|
202
|
+
reason: "duplicate_event",
|
|
203
|
+
dedupeKey: inserted.record.dedupeKey,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const allCandidateSpecs = this.store.listEventSpecsForType(
|
|
211
|
+
normalized.eventType,
|
|
212
|
+
);
|
|
213
|
+
const suppressions: CronEventSuppression[] = [];
|
|
214
|
+
const matchedSpecs: CronSpecRecord[] = [];
|
|
215
|
+
const queuedRuns: CronRunRecord[] = [];
|
|
216
|
+
|
|
217
|
+
for (const spec of allCandidateSpecs) {
|
|
218
|
+
if (!automationEventMatchesFilters(normalized, spec.filters)) {
|
|
219
|
+
suppressions.push({
|
|
220
|
+
specId: spec.specId,
|
|
221
|
+
externalId: spec.externalId,
|
|
222
|
+
reason: "filter_mismatch",
|
|
223
|
+
dedupeKey: normalized.dedupeKey,
|
|
224
|
+
});
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
matchedSpecs.push(spec);
|
|
228
|
+
const run = this.materializeForSpec(
|
|
229
|
+
spec,
|
|
230
|
+
normalized,
|
|
231
|
+
inserted.record.receivedAt,
|
|
232
|
+
);
|
|
233
|
+
if (run.run) {
|
|
234
|
+
queuedRuns.push(run.run);
|
|
235
|
+
} else {
|
|
236
|
+
suppressions.push({
|
|
237
|
+
specId: spec.specId,
|
|
238
|
+
externalId: spec.externalId,
|
|
239
|
+
reason: run.reason,
|
|
240
|
+
dedupeKey: normalized.dedupeKey,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const status =
|
|
246
|
+
matchedSpecs.length === 0
|
|
247
|
+
? "unmatched"
|
|
248
|
+
: queuedRuns.length > 0
|
|
249
|
+
? "queued"
|
|
250
|
+
: "suppressed";
|
|
251
|
+
this.store.updateEventLogProcessing(inserted.record.eventId, {
|
|
252
|
+
status,
|
|
253
|
+
matchedSpecCount: matchedSpecs.length,
|
|
254
|
+
queuedRunCount: queuedRuns.length,
|
|
255
|
+
suppressedCount: suppressions.filter(
|
|
256
|
+
(s) => s.reason !== "filter_mismatch",
|
|
257
|
+
).length,
|
|
258
|
+
});
|
|
259
|
+
const updated = this.store.getEventLog(inserted.record.eventId);
|
|
260
|
+
this.logger?.debug("cron.event.processed", {
|
|
261
|
+
eventId: inserted.record.eventId,
|
|
262
|
+
eventType: inserted.record.eventType,
|
|
263
|
+
status,
|
|
264
|
+
matchedSpecCount: matchedSpecs.length,
|
|
265
|
+
queuedRunCount: queuedRuns.length,
|
|
266
|
+
});
|
|
267
|
+
return {
|
|
268
|
+
event: updated ?? inserted.record,
|
|
269
|
+
duplicate: false,
|
|
270
|
+
matchedSpecs,
|
|
271
|
+
queuedRuns,
|
|
272
|
+
suppressions,
|
|
273
|
+
};
|
|
274
|
+
} catch (err) {
|
|
275
|
+
this.store.updateEventLogProcessing(inserted.record.eventId, {
|
|
276
|
+
status: "failed",
|
|
277
|
+
error: err instanceof Error ? err.message : String(err),
|
|
278
|
+
});
|
|
279
|
+
if (this.logger?.error) {
|
|
280
|
+
this.logger.error("cron.event.failed", {
|
|
281
|
+
eventId: inserted.record.eventId,
|
|
282
|
+
eventType: inserted.record.eventType,
|
|
283
|
+
error: err,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
throw err;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private materializeForSpec(
|
|
291
|
+
spec: CronSpecRecord,
|
|
292
|
+
event: AutomationEventEnvelope,
|
|
293
|
+
receivedAt: string,
|
|
294
|
+
): {
|
|
295
|
+
run?: CronRunRecord;
|
|
296
|
+
reason: Exclude<
|
|
297
|
+
CronEventSuppressionReason,
|
|
298
|
+
"duplicate_event" | "filter_mismatch"
|
|
299
|
+
>;
|
|
300
|
+
} {
|
|
301
|
+
const dedupeKey = event.dedupeKey ?? event.eventId;
|
|
302
|
+
const debounceSeconds = spec.debounceSeconds ?? 0;
|
|
303
|
+
if (debounceSeconds > 0) {
|
|
304
|
+
const existing = this.store.findQueuedEventRunForDedupe({
|
|
305
|
+
specId: spec.specId,
|
|
306
|
+
dedupeKey,
|
|
307
|
+
});
|
|
308
|
+
if (existing) {
|
|
309
|
+
const scheduledFor = maxIso(
|
|
310
|
+
existing.scheduledFor,
|
|
311
|
+
addSeconds(receivedAt, debounceSeconds),
|
|
312
|
+
);
|
|
313
|
+
const updated = this.store.updateQueuedEventRunForDebounce({
|
|
314
|
+
runId: existing.runId,
|
|
315
|
+
triggerEventId: event.eventId,
|
|
316
|
+
scheduledFor,
|
|
317
|
+
});
|
|
318
|
+
if (updated) return { run: updated, reason: "dedupe_window" };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const dedupeWindowSeconds = spec.dedupeWindowSeconds ?? 0;
|
|
323
|
+
if (
|
|
324
|
+
dedupeWindowSeconds > 0 &&
|
|
325
|
+
this.store.hasRecentEventRunForDedupe({
|
|
326
|
+
specId: spec.specId,
|
|
327
|
+
dedupeKey,
|
|
328
|
+
sinceIso: subtractSeconds(receivedAt, dedupeWindowSeconds),
|
|
329
|
+
})
|
|
330
|
+
) {
|
|
331
|
+
return { reason: "dedupe_window" };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const cooldownSeconds = spec.cooldownSeconds ?? 0;
|
|
335
|
+
if (
|
|
336
|
+
cooldownSeconds > 0 &&
|
|
337
|
+
this.store.hasRecentEventRunForSpec({
|
|
338
|
+
specId: spec.specId,
|
|
339
|
+
sinceIso: subtractSeconds(receivedAt, cooldownSeconds),
|
|
340
|
+
})
|
|
341
|
+
) {
|
|
342
|
+
return { reason: "cooldown" };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const run = this.store.enqueueRun({
|
|
346
|
+
specId: spec.specId,
|
|
347
|
+
specRevision: spec.revision,
|
|
348
|
+
triggerKind: "event",
|
|
349
|
+
triggerEventId: event.eventId,
|
|
350
|
+
scheduledFor:
|
|
351
|
+
debounceSeconds > 0
|
|
352
|
+
? addSeconds(receivedAt, debounceSeconds)
|
|
353
|
+
: receivedAt,
|
|
354
|
+
});
|
|
355
|
+
return { run, reason: "dedupe_window" };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { CronSpecRecord, SqliteCronStore } from "./sqlite-cron-store";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Queue materialization rules shared between the reconciler, watcher, and
|
|
5
|
+
* periodic runner tick. Execution stays trigger-agnostic once a row is
|
|
6
|
+
* queued; this module is the only place that decides when to create a row.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface CronMaterializerOptions {
|
|
10
|
+
store: SqliteCronStore;
|
|
11
|
+
now?: () => number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface MaterializeSummary {
|
|
15
|
+
oneOffQueued: number;
|
|
16
|
+
scheduleQueued: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class CronMaterializer {
|
|
20
|
+
private readonly store: SqliteCronStore;
|
|
21
|
+
private readonly nowFn: () => number;
|
|
22
|
+
|
|
23
|
+
constructor(options: CronMaterializerOptions) {
|
|
24
|
+
this.store = options.store;
|
|
25
|
+
this.nowFn = options.now ?? (() => Date.now());
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Run materialization for every valid/enabled spec. Typically called at
|
|
30
|
+
* startup (after reconciliation) and on each runner tick.
|
|
31
|
+
*/
|
|
32
|
+
public materializeAll(): MaterializeSummary {
|
|
33
|
+
const summary: MaterializeSummary = {
|
|
34
|
+
oneOffQueued: 0,
|
|
35
|
+
scheduleQueued: 0,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const oneOffs = this.store.listSpecs({
|
|
39
|
+
triggerKind: "one_off",
|
|
40
|
+
enabled: true,
|
|
41
|
+
parseStatus: "valid",
|
|
42
|
+
});
|
|
43
|
+
for (const spec of oneOffs) {
|
|
44
|
+
if (this.materializeOneOff(spec)) summary.oneOffQueued += 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const schedules = this.store.listSpecs({
|
|
48
|
+
triggerKind: "schedule",
|
|
49
|
+
enabled: true,
|
|
50
|
+
parseStatus: "valid",
|
|
51
|
+
});
|
|
52
|
+
for (const spec of schedules) {
|
|
53
|
+
try {
|
|
54
|
+
if (this.materializeSchedule(spec)) summary.scheduleQueued += 1;
|
|
55
|
+
} catch {
|
|
56
|
+
// Keep one stale/invalid persisted schedule from blocking other specs.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return summary;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Ensure a single one-off spec has exactly one run record for its current
|
|
65
|
+
* revision unless an explicit rerun path creates a different trigger kind.
|
|
66
|
+
* Returns true if a new queued run was created.
|
|
67
|
+
*/
|
|
68
|
+
public materializeOneOff(spec: CronSpecRecord): boolean {
|
|
69
|
+
if (spec.triggerKind !== "one_off") return false;
|
|
70
|
+
if (!spec.enabled || spec.removed) return false;
|
|
71
|
+
if (this.store.hasOneOffRunForRevision(spec.specId, spec.revision)) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
this.store.enqueueRun({
|
|
75
|
+
specId: spec.specId,
|
|
76
|
+
specRevision: spec.revision,
|
|
77
|
+
triggerKind: "one_off",
|
|
78
|
+
scheduledFor: new Date(this.nowFn()).toISOString(),
|
|
79
|
+
});
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Materialize schedule runs. Implements the "one overdue catch-up on
|
|
85
|
+
* startup, then advance" policy described in PLAN.md.
|
|
86
|
+
*/
|
|
87
|
+
public materializeSchedule(spec: CronSpecRecord): boolean {
|
|
88
|
+
if (spec.triggerKind !== "schedule") return false;
|
|
89
|
+
if (!spec.enabled || spec.removed) return false;
|
|
90
|
+
if (!spec.scheduleExpr) return false;
|
|
91
|
+
|
|
92
|
+
return this.store.materializeDueScheduleRun({
|
|
93
|
+
specId: spec.specId,
|
|
94
|
+
nowMs: this.nowFn(),
|
|
95
|
+
}).queued;
|
|
96
|
+
}
|
|
97
|
+
}
|