@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
|
@@ -0,0 +1,1286 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
AutomationEventEnvelope,
|
|
4
|
+
CronSpec,
|
|
5
|
+
CronSpecExtensionKind,
|
|
6
|
+
CronTriggerKind,
|
|
7
|
+
} from "@clinebot/shared";
|
|
8
|
+
import {
|
|
9
|
+
asOptionalString,
|
|
10
|
+
asString,
|
|
11
|
+
loadSqliteDb,
|
|
12
|
+
nowIso,
|
|
13
|
+
type SqliteDb,
|
|
14
|
+
} from "@clinebot/shared/db";
|
|
15
|
+
import { resolveCronDbPath } from "@clinebot/shared/storage";
|
|
16
|
+
import { ensureCronSchema } from "./cron-schema";
|
|
17
|
+
import { getNextCronTime } from "./scheduler";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generalized cron/automation store backed by `cron.db`. Sessions stay in
|
|
21
|
+
* their own database (see @clinebot/shared `ensureSessionSchema`). cron_runs
|
|
22
|
+
* here absorb one-off, recurring, and event-driven work under one queue.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export type CronRunStatus =
|
|
26
|
+
| "queued"
|
|
27
|
+
| "running"
|
|
28
|
+
| "done"
|
|
29
|
+
| "failed"
|
|
30
|
+
| "cancelled";
|
|
31
|
+
|
|
32
|
+
export type CronRunTriggerKind =
|
|
33
|
+
| "one_off"
|
|
34
|
+
| "schedule"
|
|
35
|
+
| "event"
|
|
36
|
+
| "manual"
|
|
37
|
+
| "retry";
|
|
38
|
+
|
|
39
|
+
export type CronParseStatus = "valid" | "invalid";
|
|
40
|
+
|
|
41
|
+
export type CronEventProcessingStatus =
|
|
42
|
+
| "received"
|
|
43
|
+
| "unmatched"
|
|
44
|
+
| "queued"
|
|
45
|
+
| "suppressed"
|
|
46
|
+
| "failed";
|
|
47
|
+
|
|
48
|
+
export interface CronSpecRecord {
|
|
49
|
+
specId: string;
|
|
50
|
+
externalId: string;
|
|
51
|
+
sourcePath: string;
|
|
52
|
+
triggerKind: CronTriggerKind;
|
|
53
|
+
sourceMtimeMs?: number;
|
|
54
|
+
sourceHash?: string;
|
|
55
|
+
parseStatus: CronParseStatus;
|
|
56
|
+
parseError?: string;
|
|
57
|
+
enabled: boolean;
|
|
58
|
+
removed: boolean;
|
|
59
|
+
title: string;
|
|
60
|
+
prompt?: string;
|
|
61
|
+
workspaceRoot?: string;
|
|
62
|
+
scheduleExpr?: string;
|
|
63
|
+
timezone?: string;
|
|
64
|
+
eventType?: string;
|
|
65
|
+
filters?: Record<string, unknown>;
|
|
66
|
+
debounceSeconds?: number;
|
|
67
|
+
dedupeWindowSeconds?: number;
|
|
68
|
+
cooldownSeconds?: number;
|
|
69
|
+
mode?: string;
|
|
70
|
+
systemPrompt?: string;
|
|
71
|
+
providerId?: string;
|
|
72
|
+
modelId?: string;
|
|
73
|
+
maxIterations?: number;
|
|
74
|
+
timeoutSeconds?: number;
|
|
75
|
+
maxParallel?: number;
|
|
76
|
+
tools?: string[];
|
|
77
|
+
notesDirectory?: string;
|
|
78
|
+
extensions?: CronSpecExtensionKind[];
|
|
79
|
+
source?: string;
|
|
80
|
+
tags?: string[];
|
|
81
|
+
metadata?: Record<string, unknown>;
|
|
82
|
+
revision: number;
|
|
83
|
+
lastMaterializedRunId?: string;
|
|
84
|
+
lastRunAt?: string;
|
|
85
|
+
nextRunAt?: string;
|
|
86
|
+
createdAt: string;
|
|
87
|
+
updatedAt: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface CronRunRecord {
|
|
91
|
+
runId: string;
|
|
92
|
+
specId: string;
|
|
93
|
+
specRevision: number;
|
|
94
|
+
triggerKind: CronRunTriggerKind;
|
|
95
|
+
status: CronRunStatus;
|
|
96
|
+
claimToken?: string;
|
|
97
|
+
claimStartedAt?: string;
|
|
98
|
+
claimUntilAt?: string;
|
|
99
|
+
scheduledFor?: string;
|
|
100
|
+
triggerEventId?: string;
|
|
101
|
+
startedAt?: string;
|
|
102
|
+
completedAt?: string;
|
|
103
|
+
sessionId?: string;
|
|
104
|
+
reportPath?: string;
|
|
105
|
+
error?: string;
|
|
106
|
+
attemptCount: number;
|
|
107
|
+
createdAt: string;
|
|
108
|
+
updatedAt: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface CronEventLogRecord {
|
|
112
|
+
eventId: string;
|
|
113
|
+
eventType: string;
|
|
114
|
+
source: string;
|
|
115
|
+
subject?: string;
|
|
116
|
+
occurredAt: string;
|
|
117
|
+
receivedAt: string;
|
|
118
|
+
workspaceRoot?: string;
|
|
119
|
+
dedupeKey?: string;
|
|
120
|
+
payload?: Record<string, unknown>;
|
|
121
|
+
attributes?: Record<string, unknown>;
|
|
122
|
+
processingStatus: CronEventProcessingStatus;
|
|
123
|
+
matchedSpecCount: number;
|
|
124
|
+
queuedRunCount: number;
|
|
125
|
+
suppressedCount: number;
|
|
126
|
+
error?: string;
|
|
127
|
+
createdAt: string;
|
|
128
|
+
updatedAt: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface UpsertSpecInput {
|
|
132
|
+
externalId: string;
|
|
133
|
+
sourcePath: string;
|
|
134
|
+
triggerKind: CronTriggerKind;
|
|
135
|
+
sourceMtimeMs?: number;
|
|
136
|
+
sourceHash: string;
|
|
137
|
+
parseStatus: CronParseStatus;
|
|
138
|
+
parseError?: string;
|
|
139
|
+
spec?: CronSpec;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface UpsertSpecResult {
|
|
143
|
+
record: CronSpecRecord;
|
|
144
|
+
created: boolean;
|
|
145
|
+
revisionChanged: boolean;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface SqliteCronStoreOptions {
|
|
149
|
+
dbPath?: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseJsonRecord(
|
|
153
|
+
value: string | undefined,
|
|
154
|
+
): Record<string, unknown> | undefined {
|
|
155
|
+
if (!value) return undefined;
|
|
156
|
+
try {
|
|
157
|
+
const parsed = JSON.parse(value) as unknown;
|
|
158
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
159
|
+
return parsed as Record<string, unknown>;
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// ignore
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseJsonArray(
|
|
168
|
+
value: string | undefined,
|
|
169
|
+
options: { preserveEmpty?: boolean } = {},
|
|
170
|
+
): string[] | undefined {
|
|
171
|
+
if (!value) return undefined;
|
|
172
|
+
try {
|
|
173
|
+
const parsed = JSON.parse(value) as unknown;
|
|
174
|
+
if (!Array.isArray(parsed)) return undefined;
|
|
175
|
+
const tags = parsed
|
|
176
|
+
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
177
|
+
.filter((item) => item.length > 0);
|
|
178
|
+
if (options.preserveEmpty) {
|
|
179
|
+
return tags;
|
|
180
|
+
}
|
|
181
|
+
return tags.length > 0 ? tags : undefined;
|
|
182
|
+
} catch {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function toInt(value: unknown): number | undefined {
|
|
188
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
189
|
+
if (typeof value === "bigint") return Number(value);
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function specToRecord(row: Record<string, unknown>): CronSpecRecord {
|
|
194
|
+
return {
|
|
195
|
+
specId: asString(row.spec_id),
|
|
196
|
+
externalId: asString(row.external_id),
|
|
197
|
+
sourcePath: asString(row.source_path),
|
|
198
|
+
triggerKind: asString(row.trigger_kind) as CronTriggerKind,
|
|
199
|
+
sourceMtimeMs: toInt(row.source_mtime_ms),
|
|
200
|
+
sourceHash: asOptionalString(row.source_hash),
|
|
201
|
+
parseStatus: asString(row.parse_status) === "invalid" ? "invalid" : "valid",
|
|
202
|
+
parseError: asOptionalString(row.parse_error),
|
|
203
|
+
enabled: Number(row.enabled ?? 0) === 1,
|
|
204
|
+
removed: Number(row.removed ?? 0) === 1,
|
|
205
|
+
title: asString(row.title),
|
|
206
|
+
prompt: asOptionalString(row.prompt),
|
|
207
|
+
workspaceRoot: asOptionalString(row.workspace_root),
|
|
208
|
+
scheduleExpr: asOptionalString(row.schedule_expr),
|
|
209
|
+
timezone: asOptionalString(row.timezone),
|
|
210
|
+
eventType: asOptionalString(row.event_type),
|
|
211
|
+
filters: parseJsonRecord(asOptionalString(row.filters_json)),
|
|
212
|
+
debounceSeconds: toInt(row.debounce_seconds),
|
|
213
|
+
dedupeWindowSeconds: toInt(row.dedupe_window_seconds),
|
|
214
|
+
cooldownSeconds: toInt(row.cooldown_seconds),
|
|
215
|
+
mode: asOptionalString(row.mode),
|
|
216
|
+
systemPrompt: asOptionalString(row.system_prompt),
|
|
217
|
+
providerId: asOptionalString(row.provider_id),
|
|
218
|
+
modelId: asOptionalString(row.model_id),
|
|
219
|
+
maxIterations: toInt(row.max_iterations),
|
|
220
|
+
timeoutSeconds: toInt(row.timeout_seconds),
|
|
221
|
+
maxParallel: toInt(row.max_parallel),
|
|
222
|
+
tools: parseJsonArray(asOptionalString(row.tools_json), {
|
|
223
|
+
preserveEmpty: true,
|
|
224
|
+
}),
|
|
225
|
+
notesDirectory: asOptionalString(row.notes_directory),
|
|
226
|
+
extensions: parseJsonArray(asOptionalString(row.extensions_json), {
|
|
227
|
+
preserveEmpty: true,
|
|
228
|
+
}) as CronSpecExtensionKind[] | undefined,
|
|
229
|
+
source: asOptionalString(row.source),
|
|
230
|
+
tags: parseJsonArray(asOptionalString(row.tags_json)),
|
|
231
|
+
metadata: parseJsonRecord(asOptionalString(row.metadata_json)),
|
|
232
|
+
revision: Number(row.revision ?? 1),
|
|
233
|
+
lastMaterializedRunId: asOptionalString(row.last_materialized_run_id),
|
|
234
|
+
lastRunAt: asOptionalString(row.last_run_at),
|
|
235
|
+
nextRunAt: asOptionalString(row.next_run_at),
|
|
236
|
+
createdAt: asString(row.created_at),
|
|
237
|
+
updatedAt: asString(row.updated_at),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function runToRecord(row: Record<string, unknown>): CronRunRecord {
|
|
242
|
+
return {
|
|
243
|
+
runId: asString(row.run_id),
|
|
244
|
+
specId: asString(row.spec_id),
|
|
245
|
+
specRevision: Number(row.spec_revision ?? 1),
|
|
246
|
+
triggerKind: asString(row.trigger_kind) as CronRunTriggerKind,
|
|
247
|
+
status: asString(row.status) as CronRunStatus,
|
|
248
|
+
claimToken: asOptionalString(row.claim_token),
|
|
249
|
+
claimStartedAt: asOptionalString(row.claim_started_at),
|
|
250
|
+
claimUntilAt: asOptionalString(row.claim_until_at),
|
|
251
|
+
scheduledFor: asOptionalString(row.scheduled_for),
|
|
252
|
+
triggerEventId: asOptionalString(row.trigger_event_id),
|
|
253
|
+
startedAt: asOptionalString(row.started_at),
|
|
254
|
+
completedAt: asOptionalString(row.completed_at),
|
|
255
|
+
sessionId: asOptionalString(row.session_id),
|
|
256
|
+
reportPath: asOptionalString(row.report_path),
|
|
257
|
+
error: asOptionalString(row.error),
|
|
258
|
+
attemptCount: Number(row.attempt_count ?? 0),
|
|
259
|
+
createdAt: asString(row.created_at),
|
|
260
|
+
updatedAt: asString(row.updated_at),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function eventLogToRecord(row: Record<string, unknown>): CronEventLogRecord {
|
|
265
|
+
return {
|
|
266
|
+
eventId: asString(row.event_id),
|
|
267
|
+
eventType: asString(row.event_type),
|
|
268
|
+
source: asString(row.source),
|
|
269
|
+
subject: asOptionalString(row.subject),
|
|
270
|
+
occurredAt: asString(row.occurred_at),
|
|
271
|
+
receivedAt: asString(row.received_at),
|
|
272
|
+
workspaceRoot: asOptionalString(row.workspace_root),
|
|
273
|
+
dedupeKey: asOptionalString(row.dedupe_key),
|
|
274
|
+
payload: parseJsonRecord(asOptionalString(row.payload_json)),
|
|
275
|
+
attributes: parseJsonRecord(asOptionalString(row.attributes_json)),
|
|
276
|
+
processingStatus: asString(
|
|
277
|
+
row.processing_status,
|
|
278
|
+
) as CronEventProcessingStatus,
|
|
279
|
+
matchedSpecCount: Number(row.matched_spec_count ?? 0),
|
|
280
|
+
queuedRunCount: Number(row.queued_run_count ?? 0),
|
|
281
|
+
suppressedCount: Number(row.suppressed_count ?? 0),
|
|
282
|
+
error: asOptionalString(row.error),
|
|
283
|
+
createdAt: asString(row.created_at),
|
|
284
|
+
updatedAt: asString(row.updated_at),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function jsonOrNull(value: Record<string, unknown> | undefined): string | null {
|
|
289
|
+
return value ? JSON.stringify(value) : null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const MEANINGFUL_FIELD_KEYS = [
|
|
293
|
+
"prompt",
|
|
294
|
+
"workspaceRoot",
|
|
295
|
+
"mode",
|
|
296
|
+
"systemPrompt",
|
|
297
|
+
"providerId",
|
|
298
|
+
"modelId",
|
|
299
|
+
"maxIterations",
|
|
300
|
+
"timeoutSeconds",
|
|
301
|
+
"maxParallel",
|
|
302
|
+
"tools",
|
|
303
|
+
"notesDirectory",
|
|
304
|
+
"extensions",
|
|
305
|
+
"source",
|
|
306
|
+
"scheduleExpr",
|
|
307
|
+
"timezone",
|
|
308
|
+
"eventType",
|
|
309
|
+
"filters",
|
|
310
|
+
"debounceSeconds",
|
|
311
|
+
"dedupeWindowSeconds",
|
|
312
|
+
"cooldownSeconds",
|
|
313
|
+
] as const;
|
|
314
|
+
|
|
315
|
+
function normalizeForCompare(value: unknown): unknown {
|
|
316
|
+
if (value === undefined) return null;
|
|
317
|
+
if (value && typeof value === "object") {
|
|
318
|
+
return JSON.stringify(value);
|
|
319
|
+
}
|
|
320
|
+
return value;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function hasMeaningfulChange(
|
|
324
|
+
prev: CronSpecRecord,
|
|
325
|
+
nextValues: Record<string, unknown>,
|
|
326
|
+
prevEnabled: boolean,
|
|
327
|
+
nextEnabled: boolean,
|
|
328
|
+
): boolean {
|
|
329
|
+
for (const key of MEANINGFUL_FIELD_KEYS) {
|
|
330
|
+
const prevVal = (prev as unknown as Record<string, unknown>)[key];
|
|
331
|
+
const nextVal = nextValues[key];
|
|
332
|
+
if (normalizeForCompare(prevVal) !== normalizeForCompare(nextVal)) {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (prevEnabled === false && nextEnabled === true) return true;
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function filenameStemFromPath(sourcePath: string): string {
|
|
341
|
+
const base = sourcePath.split("/").pop() ?? sourcePath;
|
|
342
|
+
return base
|
|
343
|
+
.replace(/\.event\.md$/, "")
|
|
344
|
+
.replace(/\.cron\.md$/, "")
|
|
345
|
+
.replace(/\.md$/, "");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export interface ListSpecsOptions {
|
|
349
|
+
triggerKind?: CronTriggerKind;
|
|
350
|
+
enabled?: boolean;
|
|
351
|
+
parseStatus?: CronParseStatus;
|
|
352
|
+
includeRemoved?: boolean;
|
|
353
|
+
limit?: number;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export interface ListRunsOptions {
|
|
357
|
+
specId?: string;
|
|
358
|
+
status?: CronRunStatus | CronRunStatus[];
|
|
359
|
+
limit?: number;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export interface ClaimRunOptions {
|
|
363
|
+
nowIso: string;
|
|
364
|
+
leaseMs: number;
|
|
365
|
+
limit?: number;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export interface ClaimedCronRun {
|
|
369
|
+
run: CronRunRecord;
|
|
370
|
+
claimToken: string;
|
|
371
|
+
claimUntilAt: string;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
interface ClaimBoundUpdate {
|
|
375
|
+
runId: string;
|
|
376
|
+
claimToken: string;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export interface MaterializeScheduleRunResult {
|
|
380
|
+
queued: boolean;
|
|
381
|
+
run?: CronRunRecord;
|
|
382
|
+
nextRunAt?: string;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export interface EnqueueRunInput {
|
|
386
|
+
specId: string;
|
|
387
|
+
specRevision: number;
|
|
388
|
+
triggerKind: CronRunTriggerKind;
|
|
389
|
+
scheduledFor?: string;
|
|
390
|
+
triggerEventId?: string;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export interface InsertEventLogResult {
|
|
394
|
+
record: CronEventLogRecord;
|
|
395
|
+
created: boolean;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export interface ListEventLogsOptions {
|
|
399
|
+
eventType?: string;
|
|
400
|
+
source?: string;
|
|
401
|
+
processingStatus?: CronEventProcessingStatus;
|
|
402
|
+
limit?: number;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export class SqliteCronStore {
|
|
406
|
+
private readonly db: SqliteDb;
|
|
407
|
+
|
|
408
|
+
constructor(options: SqliteCronStoreOptions = {}) {
|
|
409
|
+
const path = options.dbPath ?? resolveCronDbPath();
|
|
410
|
+
this.db = loadSqliteDb(path);
|
|
411
|
+
ensureCronSchema(this.db);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
public close(): void {
|
|
415
|
+
this.db.close?.();
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
public getSpecBySourcePath(sourcePath: string): CronSpecRecord | undefined {
|
|
419
|
+
const row = this.db
|
|
420
|
+
.prepare("SELECT * FROM cron_specs WHERE source_path = ?")
|
|
421
|
+
.get(sourcePath);
|
|
422
|
+
return row ? specToRecord(row) : undefined;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
public getSpec(specId: string): CronSpecRecord | undefined {
|
|
426
|
+
const row = this.db
|
|
427
|
+
.prepare("SELECT * FROM cron_specs WHERE spec_id = ?")
|
|
428
|
+
.get(specId);
|
|
429
|
+
return row ? specToRecord(row) : undefined;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
public getSpecByExternalId(externalId: string): CronSpecRecord | undefined {
|
|
433
|
+
const row = this.db
|
|
434
|
+
.prepare(
|
|
435
|
+
"SELECT * FROM cron_specs WHERE external_id = ? ORDER BY created_at ASC LIMIT 1",
|
|
436
|
+
)
|
|
437
|
+
.get(externalId);
|
|
438
|
+
return row ? specToRecord(row) : undefined;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
public listSpecs(options: ListSpecsOptions = {}): CronSpecRecord[] {
|
|
442
|
+
const where: string[] = [];
|
|
443
|
+
const params: unknown[] = [];
|
|
444
|
+
if (options.triggerKind) {
|
|
445
|
+
where.push("trigger_kind = ?");
|
|
446
|
+
params.push(options.triggerKind);
|
|
447
|
+
}
|
|
448
|
+
if (typeof options.enabled === "boolean") {
|
|
449
|
+
where.push("enabled = ?");
|
|
450
|
+
params.push(options.enabled ? 1 : 0);
|
|
451
|
+
}
|
|
452
|
+
if (options.parseStatus) {
|
|
453
|
+
where.push("parse_status = ?");
|
|
454
|
+
params.push(options.parseStatus);
|
|
455
|
+
}
|
|
456
|
+
if (!options.includeRemoved) {
|
|
457
|
+
where.push("removed = 0");
|
|
458
|
+
}
|
|
459
|
+
const whereClause = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
|
|
460
|
+
const limit = Math.max(1, Math.floor(options.limit ?? 500));
|
|
461
|
+
const rows = this.db
|
|
462
|
+
.prepare(
|
|
463
|
+
`SELECT * FROM cron_specs ${whereClause} ORDER BY created_at DESC LIMIT ?`,
|
|
464
|
+
)
|
|
465
|
+
.all(...params, limit);
|
|
466
|
+
return rows.map((row) => specToRecord(row));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
public listEventSpecsForType(eventType: string): CronSpecRecord[] {
|
|
470
|
+
const rows = this.db
|
|
471
|
+
.prepare(
|
|
472
|
+
`SELECT * FROM cron_specs
|
|
473
|
+
WHERE trigger_kind = 'event'
|
|
474
|
+
AND event_type = ?
|
|
475
|
+
AND enabled = 1
|
|
476
|
+
AND removed = 0
|
|
477
|
+
AND parse_status = 'valid'
|
|
478
|
+
ORDER BY created_at ASC`,
|
|
479
|
+
)
|
|
480
|
+
.all(eventType);
|
|
481
|
+
return rows.map((row) => specToRecord(row));
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
public upsertSpec(input: UpsertSpecInput): UpsertSpecResult {
|
|
485
|
+
const now = nowIso();
|
|
486
|
+
const existing = this.getSpecBySourcePath(input.sourcePath);
|
|
487
|
+
|
|
488
|
+
const spec = input.spec;
|
|
489
|
+
const nextValues: Record<string, unknown> = {
|
|
490
|
+
title:
|
|
491
|
+
spec?.title ??
|
|
492
|
+
existing?.title ??
|
|
493
|
+
filenameStemFromPath(input.sourcePath),
|
|
494
|
+
prompt: spec?.prompt,
|
|
495
|
+
workspaceRoot: spec?.workspaceRoot,
|
|
496
|
+
scheduleExpr:
|
|
497
|
+
spec?.triggerKind === "schedule" ? spec.schedule : undefined,
|
|
498
|
+
timezone: spec?.triggerKind === "schedule" ? spec.timezone : undefined,
|
|
499
|
+
eventType: spec?.triggerKind === "event" ? spec.event : undefined,
|
|
500
|
+
filters: spec?.triggerKind === "event" ? spec.filters : undefined,
|
|
501
|
+
debounceSeconds:
|
|
502
|
+
spec?.triggerKind === "event" ? spec.debounceSeconds : undefined,
|
|
503
|
+
dedupeWindowSeconds:
|
|
504
|
+
spec?.triggerKind === "event" ? spec.dedupeWindowSeconds : undefined,
|
|
505
|
+
cooldownSeconds:
|
|
506
|
+
spec?.triggerKind === "event" ? spec.cooldownSeconds : undefined,
|
|
507
|
+
mode: spec?.mode,
|
|
508
|
+
systemPrompt: spec?.systemPrompt,
|
|
509
|
+
providerId: spec?.modelSelection?.providerId,
|
|
510
|
+
modelId: spec?.modelSelection?.modelId,
|
|
511
|
+
maxIterations: spec?.maxIterations,
|
|
512
|
+
timeoutSeconds: spec?.timeoutSeconds,
|
|
513
|
+
maxParallel: spec?.triggerKind === "event" ? spec.maxParallel : undefined,
|
|
514
|
+
tools: spec?.tools,
|
|
515
|
+
notesDirectory: spec?.notesDirectory,
|
|
516
|
+
extensions: spec?.extensions,
|
|
517
|
+
source: spec?.source,
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const enabled = input.parseStatus === "valid" && (spec?.enabled ?? true);
|
|
521
|
+
|
|
522
|
+
if (!existing) {
|
|
523
|
+
const specId = `cspec_${randomUUID()}`;
|
|
524
|
+
this.insertSpecRow(specId, input, nextValues, enabled, now);
|
|
525
|
+
const record = this.getSpec(specId);
|
|
526
|
+
if (!record) throw new Error("failed to insert cron_spec row");
|
|
527
|
+
return { record, created: true, revisionChanged: true };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const hashChanged = existing.sourceHash !== input.sourceHash;
|
|
531
|
+
const revisionChanged =
|
|
532
|
+
hashChanged &&
|
|
533
|
+
hasMeaningfulChange(existing, nextValues, existing.enabled, enabled);
|
|
534
|
+
const revision = revisionChanged
|
|
535
|
+
? existing.revision + 1
|
|
536
|
+
: existing.revision;
|
|
537
|
+
this.updateSpecRow(
|
|
538
|
+
existing.specId,
|
|
539
|
+
input,
|
|
540
|
+
nextValues,
|
|
541
|
+
enabled,
|
|
542
|
+
revision,
|
|
543
|
+
now,
|
|
544
|
+
);
|
|
545
|
+
const record = this.getSpec(existing.specId);
|
|
546
|
+
if (!record) throw new Error("failed to reload cron_spec after update");
|
|
547
|
+
return { record, created: false, revisionChanged };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private insertSpecRow(
|
|
551
|
+
specId: string,
|
|
552
|
+
input: UpsertSpecInput,
|
|
553
|
+
v: Record<string, unknown>,
|
|
554
|
+
enabled: boolean,
|
|
555
|
+
now: string,
|
|
556
|
+
): void {
|
|
557
|
+
const spec = input.spec;
|
|
558
|
+
this.db
|
|
559
|
+
.prepare(
|
|
560
|
+
`INSERT INTO cron_specs (
|
|
561
|
+
spec_id, external_id, source_path, trigger_kind,
|
|
562
|
+
source_mtime_ms, source_hash, parse_status, parse_error,
|
|
563
|
+
enabled, removed, title, prompt, workspace_root,
|
|
564
|
+
schedule_expr, timezone, event_type, filters_json,
|
|
565
|
+
debounce_seconds, dedupe_window_seconds, cooldown_seconds,
|
|
566
|
+
mode, system_prompt, provider_id, model_id,
|
|
567
|
+
max_iterations, timeout_seconds, max_parallel,
|
|
568
|
+
tools_json, notes_directory, extensions_json, source,
|
|
569
|
+
tags_json, metadata_json, revision,
|
|
570
|
+
created_at, updated_at
|
|
571
|
+
) VALUES (${Array.from({ length: 36 }, () => "?").join(",")})`,
|
|
572
|
+
)
|
|
573
|
+
.run(
|
|
574
|
+
specId,
|
|
575
|
+
input.externalId,
|
|
576
|
+
input.sourcePath,
|
|
577
|
+
input.triggerKind,
|
|
578
|
+
input.sourceMtimeMs ?? null,
|
|
579
|
+
input.sourceHash,
|
|
580
|
+
input.parseStatus,
|
|
581
|
+
input.parseError ?? null,
|
|
582
|
+
enabled ? 1 : 0,
|
|
583
|
+
0,
|
|
584
|
+
(v.title as string) ?? "",
|
|
585
|
+
(v.prompt as string | undefined) ?? null,
|
|
586
|
+
(v.workspaceRoot as string | undefined) ?? null,
|
|
587
|
+
(v.scheduleExpr as string | undefined) ?? null,
|
|
588
|
+
(v.timezone as string | undefined) ?? null,
|
|
589
|
+
(v.eventType as string | undefined) ?? null,
|
|
590
|
+
v.filters ? JSON.stringify(v.filters) : null,
|
|
591
|
+
(v.debounceSeconds as number | undefined) ?? null,
|
|
592
|
+
(v.dedupeWindowSeconds as number | undefined) ?? null,
|
|
593
|
+
(v.cooldownSeconds as number | undefined) ?? null,
|
|
594
|
+
(v.mode as string | undefined) ?? null,
|
|
595
|
+
(v.systemPrompt as string | undefined) ?? null,
|
|
596
|
+
(v.providerId as string | undefined) ?? null,
|
|
597
|
+
(v.modelId as string | undefined) ?? null,
|
|
598
|
+
(v.maxIterations as number | undefined) ?? null,
|
|
599
|
+
(v.timeoutSeconds as number | undefined) ?? null,
|
|
600
|
+
(v.maxParallel as number | undefined) ?? null,
|
|
601
|
+
v.tools ? JSON.stringify(v.tools) : null,
|
|
602
|
+
(v.notesDirectory as string | undefined) ?? null,
|
|
603
|
+
v.extensions ? JSON.stringify(v.extensions) : null,
|
|
604
|
+
(v.source as string | undefined) ?? null,
|
|
605
|
+
spec?.tags ? JSON.stringify(spec.tags) : null,
|
|
606
|
+
spec?.metadata ? JSON.stringify(spec.metadata) : null,
|
|
607
|
+
1,
|
|
608
|
+
now,
|
|
609
|
+
now,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private updateSpecRow(
|
|
614
|
+
specId: string,
|
|
615
|
+
input: UpsertSpecInput,
|
|
616
|
+
v: Record<string, unknown>,
|
|
617
|
+
enabled: boolean,
|
|
618
|
+
revision: number,
|
|
619
|
+
now: string,
|
|
620
|
+
): void {
|
|
621
|
+
const spec = input.spec;
|
|
622
|
+
this.db
|
|
623
|
+
.prepare(
|
|
624
|
+
`UPDATE cron_specs SET
|
|
625
|
+
external_id = ?, trigger_kind = ?,
|
|
626
|
+
source_mtime_ms = ?, source_hash = ?, parse_status = ?, parse_error = ?,
|
|
627
|
+
enabled = ?, removed = 0, title = ?, prompt = ?,
|
|
628
|
+
workspace_root = ?, schedule_expr = ?, timezone = ?,
|
|
629
|
+
event_type = ?, filters_json = ?,
|
|
630
|
+
debounce_seconds = ?, dedupe_window_seconds = ?, cooldown_seconds = ?,
|
|
631
|
+
mode = ?, system_prompt = ?, provider_id = ?, model_id = ?,
|
|
632
|
+
max_iterations = ?, timeout_seconds = ?, max_parallel = ?,
|
|
633
|
+
tools_json = ?, notes_directory = ?, extensions_json = ?, source = ?,
|
|
634
|
+
tags_json = ?, metadata_json = ?,
|
|
635
|
+
revision = ?, updated_at = ?
|
|
636
|
+
WHERE spec_id = ?`,
|
|
637
|
+
)
|
|
638
|
+
.run(
|
|
639
|
+
input.externalId,
|
|
640
|
+
input.triggerKind,
|
|
641
|
+
input.sourceMtimeMs ?? null,
|
|
642
|
+
input.sourceHash,
|
|
643
|
+
input.parseStatus,
|
|
644
|
+
input.parseError ?? null,
|
|
645
|
+
enabled ? 1 : 0,
|
|
646
|
+
(v.title as string) ?? "",
|
|
647
|
+
(v.prompt as string | undefined) ?? null,
|
|
648
|
+
(v.workspaceRoot as string | undefined) ?? null,
|
|
649
|
+
(v.scheduleExpr as string | undefined) ?? null,
|
|
650
|
+
(v.timezone as string | undefined) ?? null,
|
|
651
|
+
(v.eventType as string | undefined) ?? null,
|
|
652
|
+
v.filters ? JSON.stringify(v.filters) : null,
|
|
653
|
+
(v.debounceSeconds as number | undefined) ?? null,
|
|
654
|
+
(v.dedupeWindowSeconds as number | undefined) ?? null,
|
|
655
|
+
(v.cooldownSeconds as number | undefined) ?? null,
|
|
656
|
+
(v.mode as string | undefined) ?? null,
|
|
657
|
+
(v.systemPrompt as string | undefined) ?? null,
|
|
658
|
+
(v.providerId as string | undefined) ?? null,
|
|
659
|
+
(v.modelId as string | undefined) ?? null,
|
|
660
|
+
(v.maxIterations as number | undefined) ?? null,
|
|
661
|
+
(v.timeoutSeconds as number | undefined) ?? null,
|
|
662
|
+
(v.maxParallel as number | undefined) ?? null,
|
|
663
|
+
v.tools ? JSON.stringify(v.tools) : null,
|
|
664
|
+
(v.notesDirectory as string | undefined) ?? null,
|
|
665
|
+
v.extensions ? JSON.stringify(v.extensions) : null,
|
|
666
|
+
(v.source as string | undefined) ?? null,
|
|
667
|
+
spec?.tags ? JSON.stringify(spec.tags) : null,
|
|
668
|
+
spec?.metadata ? JSON.stringify(spec.metadata) : null,
|
|
669
|
+
revision,
|
|
670
|
+
now,
|
|
671
|
+
specId,
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
public markSpecRemoved(specId: string): void {
|
|
676
|
+
this.db
|
|
677
|
+
.prepare(
|
|
678
|
+
`UPDATE cron_specs SET removed = 1, enabled = 0, updated_at = ? WHERE spec_id = ?`,
|
|
679
|
+
)
|
|
680
|
+
.run(nowIso(), specId);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
public updateSpecNextRunAt(
|
|
684
|
+
specId: string,
|
|
685
|
+
nextRunAt: string | undefined,
|
|
686
|
+
): void {
|
|
687
|
+
this.db
|
|
688
|
+
.prepare(
|
|
689
|
+
`UPDATE cron_specs SET next_run_at = ?, updated_at = ? WHERE spec_id = ?`,
|
|
690
|
+
)
|
|
691
|
+
.run(nextRunAt ?? null, nowIso(), specId);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
public updateSpecLastRunAt(specId: string, lastRunAt: string): void {
|
|
695
|
+
this.db
|
|
696
|
+
.prepare(
|
|
697
|
+
`UPDATE cron_specs SET last_run_at = ?, updated_at = ? WHERE spec_id = ?`,
|
|
698
|
+
)
|
|
699
|
+
.run(lastRunAt, nowIso(), specId);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
public updateLastMaterializedRunId(specId: string, runId: string): void {
|
|
703
|
+
this.db
|
|
704
|
+
.prepare(
|
|
705
|
+
`UPDATE cron_specs SET last_materialized_run_id = ?, updated_at = ? WHERE spec_id = ?`,
|
|
706
|
+
)
|
|
707
|
+
.run(runId, nowIso(), specId);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
public materializeDueScheduleRun(options: {
|
|
711
|
+
specId: string;
|
|
712
|
+
nowMs: number;
|
|
713
|
+
}): MaterializeScheduleRunResult {
|
|
714
|
+
const nowMs = options.nowMs;
|
|
715
|
+
const now = new Date(nowMs).toISOString();
|
|
716
|
+
this.db.exec("BEGIN IMMEDIATE;");
|
|
717
|
+
try {
|
|
718
|
+
const row = this.db
|
|
719
|
+
.prepare("SELECT * FROM cron_specs WHERE spec_id = ?")
|
|
720
|
+
.get(options.specId);
|
|
721
|
+
if (!row) {
|
|
722
|
+
this.db.exec("COMMIT;");
|
|
723
|
+
return { queued: false };
|
|
724
|
+
}
|
|
725
|
+
const spec = specToRecord(row);
|
|
726
|
+
if (
|
|
727
|
+
spec.triggerKind !== "schedule" ||
|
|
728
|
+
!spec.enabled ||
|
|
729
|
+
spec.removed ||
|
|
730
|
+
spec.parseStatus !== "valid" ||
|
|
731
|
+
!spec.scheduleExpr
|
|
732
|
+
) {
|
|
733
|
+
this.db.exec("COMMIT;");
|
|
734
|
+
return { queued: false };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const dueAt = spec.nextRunAt;
|
|
738
|
+
if (!dueAt) {
|
|
739
|
+
const initializedNext = new Date(
|
|
740
|
+
getNextCronTime(spec.scheduleExpr, nowMs, spec.timezone),
|
|
741
|
+
).toISOString();
|
|
742
|
+
this.db
|
|
743
|
+
.prepare(
|
|
744
|
+
`UPDATE cron_specs SET next_run_at = ?, updated_at = ? WHERE spec_id = ?`,
|
|
745
|
+
)
|
|
746
|
+
.run(initializedNext, now, spec.specId);
|
|
747
|
+
this.db.exec("COMMIT;");
|
|
748
|
+
return { queued: false, nextRunAt: initializedNext };
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (new Date(dueAt).getTime() > nowMs) {
|
|
752
|
+
this.db.exec("COMMIT;");
|
|
753
|
+
return { queued: false, nextRunAt: dueAt };
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const runId = `crun_${randomUUID()}`;
|
|
757
|
+
let nextRunAt: string | undefined;
|
|
758
|
+
try {
|
|
759
|
+
nextRunAt = new Date(
|
|
760
|
+
getNextCronTime(spec.scheduleExpr, nowMs, spec.timezone),
|
|
761
|
+
).toISOString();
|
|
762
|
+
} catch {
|
|
763
|
+
nextRunAt = undefined;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
this.db
|
|
767
|
+
.prepare(
|
|
768
|
+
`INSERT INTO cron_runs (
|
|
769
|
+
run_id, spec_id, spec_revision, trigger_kind, status,
|
|
770
|
+
scheduled_for, trigger_event_id, attempt_count,
|
|
771
|
+
created_at, updated_at
|
|
772
|
+
) VALUES (?,?,?,?,?, ?,?,?, ?,?)`,
|
|
773
|
+
)
|
|
774
|
+
.run(
|
|
775
|
+
runId,
|
|
776
|
+
spec.specId,
|
|
777
|
+
spec.revision,
|
|
778
|
+
"schedule",
|
|
779
|
+
"queued",
|
|
780
|
+
dueAt,
|
|
781
|
+
null,
|
|
782
|
+
0,
|
|
783
|
+
now,
|
|
784
|
+
now,
|
|
785
|
+
);
|
|
786
|
+
this.db
|
|
787
|
+
.prepare(
|
|
788
|
+
`UPDATE cron_specs SET
|
|
789
|
+
last_materialized_run_id = ?,
|
|
790
|
+
last_run_at = ?,
|
|
791
|
+
next_run_at = ?,
|
|
792
|
+
updated_at = ?
|
|
793
|
+
WHERE spec_id = ?`,
|
|
794
|
+
)
|
|
795
|
+
.run(runId, now, nextRunAt ?? null, now, spec.specId);
|
|
796
|
+
this.db.exec("COMMIT;");
|
|
797
|
+
return {
|
|
798
|
+
queued: true,
|
|
799
|
+
run: this.getRun(runId),
|
|
800
|
+
nextRunAt,
|
|
801
|
+
};
|
|
802
|
+
} catch (err) {
|
|
803
|
+
this.db.exec("ROLLBACK;");
|
|
804
|
+
throw err;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
public getRun(runId: string): CronRunRecord | undefined {
|
|
809
|
+
const row = this.db
|
|
810
|
+
.prepare("SELECT * FROM cron_runs WHERE run_id = ?")
|
|
811
|
+
.get(runId);
|
|
812
|
+
return row ? runToRecord(row) : undefined;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
public insertEventLog(
|
|
816
|
+
event: AutomationEventEnvelope,
|
|
817
|
+
options: { receivedAtIso?: string } = {},
|
|
818
|
+
): InsertEventLogResult {
|
|
819
|
+
const now = nowIso();
|
|
820
|
+
const receivedAt = options.receivedAtIso ?? now;
|
|
821
|
+
const eventId = event.eventId.trim();
|
|
822
|
+
if (!eventId) {
|
|
823
|
+
throw new Error("automation event requires eventId");
|
|
824
|
+
}
|
|
825
|
+
const eventType = event.eventType.trim();
|
|
826
|
+
if (!eventType) {
|
|
827
|
+
throw new Error("automation event requires eventType");
|
|
828
|
+
}
|
|
829
|
+
const source = event.source.trim();
|
|
830
|
+
if (!source) {
|
|
831
|
+
throw new Error("automation event requires source");
|
|
832
|
+
}
|
|
833
|
+
const occurredAt = event.occurredAt.trim() || receivedAt;
|
|
834
|
+
const changes =
|
|
835
|
+
this.db
|
|
836
|
+
.prepare(
|
|
837
|
+
`INSERT OR IGNORE INTO cron_event_log (
|
|
838
|
+
event_id, event_type, source, subject,
|
|
839
|
+
occurred_at, received_at, workspace_root, dedupe_key,
|
|
840
|
+
payload_json, attributes_json, processing_status,
|
|
841
|
+
matched_spec_count, queued_run_count, suppressed_count,
|
|
842
|
+
error, created_at, updated_at
|
|
843
|
+
) VALUES (?,?,?,?, ?,?,?,?, ?,?,?, ?,?,?, ?,?,?)`,
|
|
844
|
+
)
|
|
845
|
+
.run(
|
|
846
|
+
eventId,
|
|
847
|
+
eventType,
|
|
848
|
+
source,
|
|
849
|
+
event.subject?.trim() || null,
|
|
850
|
+
occurredAt,
|
|
851
|
+
receivedAt,
|
|
852
|
+
event.workspaceRoot?.trim() || null,
|
|
853
|
+
event.dedupeKey?.trim() || null,
|
|
854
|
+
jsonOrNull(event.payload),
|
|
855
|
+
jsonOrNull(event.attributes),
|
|
856
|
+
"received",
|
|
857
|
+
0,
|
|
858
|
+
0,
|
|
859
|
+
0,
|
|
860
|
+
null,
|
|
861
|
+
now,
|
|
862
|
+
now,
|
|
863
|
+
).changes ?? 0;
|
|
864
|
+
const record = this.getEventLog(eventId);
|
|
865
|
+
if (!record) throw new Error("failed to insert cron_event_log row");
|
|
866
|
+
return { record, created: changes === 1 };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
public getEventLog(eventId: string): CronEventLogRecord | undefined {
|
|
870
|
+
const row = this.db
|
|
871
|
+
.prepare("SELECT * FROM cron_event_log WHERE event_id = ?")
|
|
872
|
+
.get(eventId);
|
|
873
|
+
return row ? eventLogToRecord(row) : undefined;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
public listEventLogs(
|
|
877
|
+
options: ListEventLogsOptions = {},
|
|
878
|
+
): CronEventLogRecord[] {
|
|
879
|
+
const where: string[] = [];
|
|
880
|
+
const params: unknown[] = [];
|
|
881
|
+
if (options.eventType) {
|
|
882
|
+
where.push("event_type = ?");
|
|
883
|
+
params.push(options.eventType);
|
|
884
|
+
}
|
|
885
|
+
if (options.source) {
|
|
886
|
+
where.push("source = ?");
|
|
887
|
+
params.push(options.source);
|
|
888
|
+
}
|
|
889
|
+
if (options.processingStatus) {
|
|
890
|
+
where.push("processing_status = ?");
|
|
891
|
+
params.push(options.processingStatus);
|
|
892
|
+
}
|
|
893
|
+
const whereClause = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
|
|
894
|
+
const limit = Math.max(1, Math.floor(options.limit ?? 200));
|
|
895
|
+
const rows = this.db
|
|
896
|
+
.prepare(
|
|
897
|
+
`SELECT * FROM cron_event_log ${whereClause}
|
|
898
|
+
ORDER BY received_at DESC, created_at DESC
|
|
899
|
+
LIMIT ?`,
|
|
900
|
+
)
|
|
901
|
+
.all(...params, limit);
|
|
902
|
+
return rows.map((row) => eventLogToRecord(row));
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
public updateEventLogProcessing(
|
|
906
|
+
eventId: string,
|
|
907
|
+
update: {
|
|
908
|
+
status: CronEventProcessingStatus;
|
|
909
|
+
matchedSpecCount?: number;
|
|
910
|
+
queuedRunCount?: number;
|
|
911
|
+
suppressedCount?: number;
|
|
912
|
+
error?: string;
|
|
913
|
+
},
|
|
914
|
+
): boolean {
|
|
915
|
+
const changes =
|
|
916
|
+
this.db
|
|
917
|
+
.prepare(
|
|
918
|
+
`UPDATE cron_event_log SET
|
|
919
|
+
processing_status = ?,
|
|
920
|
+
matched_spec_count = COALESCE(?, matched_spec_count),
|
|
921
|
+
queued_run_count = COALESCE(?, queued_run_count),
|
|
922
|
+
suppressed_count = COALESCE(?, suppressed_count),
|
|
923
|
+
error = ?,
|
|
924
|
+
updated_at = ?
|
|
925
|
+
WHERE event_id = ?`,
|
|
926
|
+
)
|
|
927
|
+
.run(
|
|
928
|
+
update.status,
|
|
929
|
+
update.matchedSpecCount ?? null,
|
|
930
|
+
update.queuedRunCount ?? null,
|
|
931
|
+
update.suppressedCount ?? null,
|
|
932
|
+
update.error ?? null,
|
|
933
|
+
nowIso(),
|
|
934
|
+
eventId,
|
|
935
|
+
).changes ?? 0;
|
|
936
|
+
return changes === 1;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
public listRuns(options: ListRunsOptions = {}): CronRunRecord[] {
|
|
940
|
+
const where: string[] = [];
|
|
941
|
+
const params: unknown[] = [];
|
|
942
|
+
if (options.specId) {
|
|
943
|
+
where.push("spec_id = ?");
|
|
944
|
+
params.push(options.specId);
|
|
945
|
+
}
|
|
946
|
+
if (options.status) {
|
|
947
|
+
const statuses = Array.isArray(options.status)
|
|
948
|
+
? options.status
|
|
949
|
+
: [options.status];
|
|
950
|
+
if (statuses.length > 0) {
|
|
951
|
+
const placeholders = statuses.map(() => "?").join(",");
|
|
952
|
+
where.push(`status IN (${placeholders})`);
|
|
953
|
+
for (const s of statuses) params.push(s);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
const whereClause = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
|
|
957
|
+
const limit = Math.max(1, Math.floor(options.limit ?? 200));
|
|
958
|
+
const rows = this.db
|
|
959
|
+
.prepare(
|
|
960
|
+
`SELECT * FROM cron_runs ${whereClause} ORDER BY created_at DESC LIMIT ?`,
|
|
961
|
+
)
|
|
962
|
+
.all(...params, limit);
|
|
963
|
+
return rows.map((row) => runToRecord(row));
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
public hasRecentEventRunForDedupe(options: {
|
|
967
|
+
specId: string;
|
|
968
|
+
dedupeKey: string;
|
|
969
|
+
sinceIso: string;
|
|
970
|
+
}): boolean {
|
|
971
|
+
const row = this.db
|
|
972
|
+
.prepare(
|
|
973
|
+
`SELECT r.run_id FROM cron_runs r
|
|
974
|
+
INNER JOIN cron_event_log e ON e.event_id = r.trigger_event_id
|
|
975
|
+
WHERE r.spec_id = ?
|
|
976
|
+
AND r.trigger_kind = 'event'
|
|
977
|
+
AND e.dedupe_key = ?
|
|
978
|
+
AND e.received_at >= ?
|
|
979
|
+
LIMIT 1`,
|
|
980
|
+
)
|
|
981
|
+
.get(options.specId, options.dedupeKey, options.sinceIso);
|
|
982
|
+
return !!row;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
public hasRecentEventRunForSpec(options: {
|
|
986
|
+
specId: string;
|
|
987
|
+
sinceIso: string;
|
|
988
|
+
}): boolean {
|
|
989
|
+
const row = this.db
|
|
990
|
+
.prepare(
|
|
991
|
+
`SELECT r.run_id FROM cron_runs r
|
|
992
|
+
INNER JOIN cron_event_log e ON e.event_id = r.trigger_event_id
|
|
993
|
+
WHERE r.spec_id = ?
|
|
994
|
+
AND r.trigger_kind = 'event'
|
|
995
|
+
AND e.received_at >= ?
|
|
996
|
+
LIMIT 1`,
|
|
997
|
+
)
|
|
998
|
+
.get(options.specId, options.sinceIso);
|
|
999
|
+
return !!row;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
public findQueuedEventRunForDedupe(options: {
|
|
1003
|
+
specId: string;
|
|
1004
|
+
dedupeKey: string;
|
|
1005
|
+
}): CronRunRecord | undefined {
|
|
1006
|
+
const row = this.db
|
|
1007
|
+
.prepare(
|
|
1008
|
+
`SELECT r.* FROM cron_runs r
|
|
1009
|
+
INNER JOIN cron_event_log e ON e.event_id = r.trigger_event_id
|
|
1010
|
+
WHERE r.spec_id = ?
|
|
1011
|
+
AND r.trigger_kind = 'event'
|
|
1012
|
+
AND r.status = 'queued'
|
|
1013
|
+
AND e.dedupe_key = ?
|
|
1014
|
+
ORDER BY COALESCE(r.scheduled_for, r.created_at) DESC
|
|
1015
|
+
LIMIT 1`,
|
|
1016
|
+
)
|
|
1017
|
+
.get(options.specId, options.dedupeKey);
|
|
1018
|
+
return row ? runToRecord(row) : undefined;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
public updateQueuedEventRunForDebounce(options: {
|
|
1022
|
+
runId: string;
|
|
1023
|
+
triggerEventId: string;
|
|
1024
|
+
scheduledFor: string;
|
|
1025
|
+
}): CronRunRecord | undefined {
|
|
1026
|
+
const updatedAt = nowIso();
|
|
1027
|
+
const changes =
|
|
1028
|
+
this.db
|
|
1029
|
+
.prepare(
|
|
1030
|
+
`UPDATE cron_runs SET
|
|
1031
|
+
trigger_event_id = ?,
|
|
1032
|
+
scheduled_for = ?,
|
|
1033
|
+
updated_at = ?
|
|
1034
|
+
WHERE run_id = ?
|
|
1035
|
+
AND trigger_kind = 'event'
|
|
1036
|
+
AND status = 'queued'`,
|
|
1037
|
+
)
|
|
1038
|
+
.run(
|
|
1039
|
+
options.triggerEventId,
|
|
1040
|
+
options.scheduledFor,
|
|
1041
|
+
updatedAt,
|
|
1042
|
+
options.runId,
|
|
1043
|
+
).changes ?? 0;
|
|
1044
|
+
if (changes !== 1) return undefined;
|
|
1045
|
+
return this.getRun(options.runId);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
public hasOneOffRunForRevision(specId: string, revision: number): boolean {
|
|
1049
|
+
const row = this.db
|
|
1050
|
+
.prepare(
|
|
1051
|
+
`SELECT run_id FROM cron_runs
|
|
1052
|
+
WHERE spec_id = ? AND spec_revision = ?
|
|
1053
|
+
AND trigger_kind = 'one_off'
|
|
1054
|
+
LIMIT 1`,
|
|
1055
|
+
)
|
|
1056
|
+
.get(specId, revision);
|
|
1057
|
+
return !!row;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
public enqueueRun(input: EnqueueRunInput): CronRunRecord {
|
|
1061
|
+
const runId = `crun_${randomUUID()}`;
|
|
1062
|
+
const now = nowIso();
|
|
1063
|
+
this.db
|
|
1064
|
+
.prepare(
|
|
1065
|
+
`INSERT INTO cron_runs (
|
|
1066
|
+
run_id, spec_id, spec_revision, trigger_kind, status,
|
|
1067
|
+
scheduled_for, trigger_event_id, attempt_count,
|
|
1068
|
+
created_at, updated_at
|
|
1069
|
+
) VALUES (?,?,?,?,?, ?,?,?, ?,?)`,
|
|
1070
|
+
)
|
|
1071
|
+
.run(
|
|
1072
|
+
runId,
|
|
1073
|
+
input.specId,
|
|
1074
|
+
input.specRevision,
|
|
1075
|
+
input.triggerKind,
|
|
1076
|
+
"queued",
|
|
1077
|
+
input.scheduledFor ?? null,
|
|
1078
|
+
input.triggerEventId ?? null,
|
|
1079
|
+
0,
|
|
1080
|
+
now,
|
|
1081
|
+
now,
|
|
1082
|
+
);
|
|
1083
|
+
this.updateLastMaterializedRunId(input.specId, runId);
|
|
1084
|
+
const run = this.getRun(runId);
|
|
1085
|
+
if (!run) throw new Error("failed to insert cron_run row");
|
|
1086
|
+
return run;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
public cancelQueuedRunsForSpec(specId: string): number {
|
|
1090
|
+
const changes =
|
|
1091
|
+
this.db
|
|
1092
|
+
.prepare(
|
|
1093
|
+
`UPDATE cron_runs SET status = 'cancelled', updated_at = ?
|
|
1094
|
+
WHERE spec_id = ? AND status = 'queued'`,
|
|
1095
|
+
)
|
|
1096
|
+
.run(nowIso(), specId).changes ?? 0;
|
|
1097
|
+
return changes;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
public claimDueRuns(options: ClaimRunOptions): ClaimedCronRun[] {
|
|
1101
|
+
const referenceIso = options.nowIso;
|
|
1102
|
+
const boundedLease = Math.max(1_000, Math.floor(options.leaseMs));
|
|
1103
|
+
const leaseUntilIso = new Date(
|
|
1104
|
+
new Date(referenceIso).getTime() + boundedLease,
|
|
1105
|
+
).toISOString();
|
|
1106
|
+
const limit = Math.max(1, Math.floor(options.limit ?? 25));
|
|
1107
|
+
const claimed: ClaimedCronRun[] = [];
|
|
1108
|
+
this.db.exec("BEGIN IMMEDIATE;");
|
|
1109
|
+
try {
|
|
1110
|
+
const rows = this.db
|
|
1111
|
+
.prepare(
|
|
1112
|
+
`SELECT * FROM cron_runs
|
|
1113
|
+
WHERE (
|
|
1114
|
+
status = 'queued'
|
|
1115
|
+
OR (
|
|
1116
|
+
status = 'running'
|
|
1117
|
+
AND claim_until_at IS NOT NULL
|
|
1118
|
+
AND claim_until_at <= ?
|
|
1119
|
+
AND completed_at IS NULL
|
|
1120
|
+
)
|
|
1121
|
+
)
|
|
1122
|
+
AND (scheduled_for IS NULL OR scheduled_for <= ?)
|
|
1123
|
+
ORDER BY COALESCE(scheduled_for, created_at) ASC
|
|
1124
|
+
LIMIT ?`,
|
|
1125
|
+
)
|
|
1126
|
+
.all(referenceIso, referenceIso, limit);
|
|
1127
|
+
for (const row of rows) {
|
|
1128
|
+
const runId = asString(row.run_id);
|
|
1129
|
+
if (!runId) continue;
|
|
1130
|
+
const claimToken = `cclaim_${randomUUID()}`;
|
|
1131
|
+
const changes =
|
|
1132
|
+
this.db
|
|
1133
|
+
.prepare(
|
|
1134
|
+
`UPDATE cron_runs SET
|
|
1135
|
+
status = 'running',
|
|
1136
|
+
claim_token = ?,
|
|
1137
|
+
claim_started_at = ?,
|
|
1138
|
+
claim_until_at = ?,
|
|
1139
|
+
started_at = ?,
|
|
1140
|
+
completed_at = NULL,
|
|
1141
|
+
session_id = NULL,
|
|
1142
|
+
report_path = NULL,
|
|
1143
|
+
error = NULL,
|
|
1144
|
+
attempt_count = attempt_count + 1,
|
|
1145
|
+
updated_at = ?
|
|
1146
|
+
WHERE run_id = ?
|
|
1147
|
+
AND (
|
|
1148
|
+
status = 'queued'
|
|
1149
|
+
OR (
|
|
1150
|
+
status = 'running'
|
|
1151
|
+
AND claim_until_at IS NOT NULL
|
|
1152
|
+
AND claim_until_at <= ?
|
|
1153
|
+
AND completed_at IS NULL
|
|
1154
|
+
)
|
|
1155
|
+
)`,
|
|
1156
|
+
)
|
|
1157
|
+
.run(
|
|
1158
|
+
claimToken,
|
|
1159
|
+
referenceIso,
|
|
1160
|
+
leaseUntilIso,
|
|
1161
|
+
referenceIso,
|
|
1162
|
+
referenceIso,
|
|
1163
|
+
runId,
|
|
1164
|
+
referenceIso,
|
|
1165
|
+
).changes ?? 0;
|
|
1166
|
+
if (changes !== 1) continue;
|
|
1167
|
+
const run = this.getRun(runId);
|
|
1168
|
+
if (!run) continue;
|
|
1169
|
+
claimed.push({ run, claimToken, claimUntilAt: leaseUntilIso });
|
|
1170
|
+
}
|
|
1171
|
+
this.db.exec("COMMIT;");
|
|
1172
|
+
} catch (err) {
|
|
1173
|
+
this.db.exec("ROLLBACK;");
|
|
1174
|
+
throw err;
|
|
1175
|
+
}
|
|
1176
|
+
return claimed;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
public renewClaim(
|
|
1180
|
+
runId: string,
|
|
1181
|
+
claimToken: string,
|
|
1182
|
+
leaseUntilAt: string,
|
|
1183
|
+
): boolean {
|
|
1184
|
+
const changes =
|
|
1185
|
+
this.db
|
|
1186
|
+
.prepare(
|
|
1187
|
+
`UPDATE cron_runs SET claim_until_at = ?, updated_at = ?
|
|
1188
|
+
WHERE run_id = ? AND claim_token = ?`,
|
|
1189
|
+
)
|
|
1190
|
+
.run(leaseUntilAt, nowIso(), runId, claimToken).changes ?? 0;
|
|
1191
|
+
return changes === 1;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
public completeRun(
|
|
1195
|
+
runId: string,
|
|
1196
|
+
update: {
|
|
1197
|
+
status: "done" | "failed" | "cancelled";
|
|
1198
|
+
sessionId?: string;
|
|
1199
|
+
reportPath?: string;
|
|
1200
|
+
error?: string;
|
|
1201
|
+
completedAtIso?: string;
|
|
1202
|
+
claimToken?: string;
|
|
1203
|
+
},
|
|
1204
|
+
): boolean {
|
|
1205
|
+
const completedAt = update.completedAtIso ?? nowIso();
|
|
1206
|
+
const whereClause = update.claimToken
|
|
1207
|
+
? "WHERE run_id = ? AND claim_token = ?"
|
|
1208
|
+
: "WHERE run_id = ?";
|
|
1209
|
+
const changes =
|
|
1210
|
+
this.db
|
|
1211
|
+
.prepare(
|
|
1212
|
+
`UPDATE cron_runs SET
|
|
1213
|
+
status = ?,
|
|
1214
|
+
session_id = COALESCE(?, session_id),
|
|
1215
|
+
report_path = COALESCE(?, report_path),
|
|
1216
|
+
error = ?,
|
|
1217
|
+
completed_at = ?,
|
|
1218
|
+
claim_started_at = NULL,
|
|
1219
|
+
claim_token = NULL,
|
|
1220
|
+
claim_until_at = NULL,
|
|
1221
|
+
updated_at = ?
|
|
1222
|
+
${whereClause}`,
|
|
1223
|
+
)
|
|
1224
|
+
.run(
|
|
1225
|
+
update.status,
|
|
1226
|
+
update.sessionId ?? null,
|
|
1227
|
+
update.reportPath ?? null,
|
|
1228
|
+
update.error ?? null,
|
|
1229
|
+
completedAt,
|
|
1230
|
+
completedAt,
|
|
1231
|
+
runId,
|
|
1232
|
+
...(update.claimToken ? [update.claimToken] : []),
|
|
1233
|
+
).changes ?? 0;
|
|
1234
|
+
return changes > 0;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
public requeueRun(
|
|
1238
|
+
update: ClaimBoundUpdate & {
|
|
1239
|
+
error?: string;
|
|
1240
|
+
scheduledFor?: string;
|
|
1241
|
+
},
|
|
1242
|
+
): boolean {
|
|
1243
|
+
const updatedAt = nowIso();
|
|
1244
|
+
const changes =
|
|
1245
|
+
this.db
|
|
1246
|
+
.prepare(
|
|
1247
|
+
`UPDATE cron_runs SET
|
|
1248
|
+
status = 'queued',
|
|
1249
|
+
claim_started_at = NULL,
|
|
1250
|
+
claim_token = NULL,
|
|
1251
|
+
claim_until_at = NULL,
|
|
1252
|
+
started_at = NULL,
|
|
1253
|
+
completed_at = NULL,
|
|
1254
|
+
session_id = NULL,
|
|
1255
|
+
report_path = NULL,
|
|
1256
|
+
error = ?,
|
|
1257
|
+
scheduled_for = COALESCE(?, scheduled_for),
|
|
1258
|
+
updated_at = ?
|
|
1259
|
+
WHERE run_id = ? AND claim_token = ?`,
|
|
1260
|
+
)
|
|
1261
|
+
.run(
|
|
1262
|
+
update.error ?? null,
|
|
1263
|
+
update.scheduledFor ?? null,
|
|
1264
|
+
updatedAt,
|
|
1265
|
+
update.runId,
|
|
1266
|
+
update.claimToken,
|
|
1267
|
+
).changes ?? 0;
|
|
1268
|
+
return changes > 0;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
public attachSessionIdToRun(runId: string, sessionId: string): void {
|
|
1272
|
+
this.db
|
|
1273
|
+
.prepare(
|
|
1274
|
+
`UPDATE cron_runs SET session_id = ?, updated_at = ? WHERE run_id = ?`,
|
|
1275
|
+
)
|
|
1276
|
+
.run(sessionId, nowIso(), runId);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
public attachReportPathToRun(runId: string, reportPath: string): void {
|
|
1280
|
+
this.db
|
|
1281
|
+
.prepare(
|
|
1282
|
+
`UPDATE cron_runs SET report_path = ?, updated_at = ? WHERE run_id = ?`,
|
|
1283
|
+
)
|
|
1284
|
+
.run(reportPath, nowIso(), runId);
|
|
1285
|
+
}
|
|
1286
|
+
}
|