aiden-runtime 4.1.5 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +265 -847
- package/dist/api/server.js +32 -5
- package/dist/cli/v4/aidenCLI.js +536 -152
- package/dist/cli/v4/callbacks.js +170 -0
- package/dist/cli/v4/chatSession.js +245 -3
- package/dist/cli/v4/commands/_runtimeToggleHelpers.js +94 -0
- package/dist/cli/v4/commands/browserDepth.js +45 -0
- package/dist/cli/v4/commands/cron.js +264 -0
- package/dist/cli/v4/commands/daemon.js +541 -0
- package/dist/cli/v4/commands/daemonStatus.js +253 -0
- package/dist/cli/v4/commands/fanout.js +42 -59
- package/dist/cli/v4/commands/help.js +13 -0
- package/dist/cli/v4/commands/index.js +35 -1
- package/dist/cli/v4/commands/mcp.js +80 -54
- package/dist/cli/v4/commands/plannerGuard.js +53 -0
- package/dist/cli/v4/commands/recovery.js +122 -0
- package/dist/cli/v4/commands/runs.js +223 -0
- package/dist/cli/v4/commands/sandbox.js +48 -0
- package/dist/cli/v4/commands/spawnPause.js +93 -0
- package/dist/cli/v4/commands/suggestions.js +68 -0
- package/dist/cli/v4/commands/tce.js +41 -0
- package/dist/cli/v4/commands/trigger.js +378 -0
- package/dist/cli/v4/commands/update.js +95 -3
- package/dist/cli/v4/daemonAgentBuilder.js +145 -0
- package/dist/cli/v4/defaultSoul.js +1 -1
- package/dist/cli/v4/display/capabilityCard.js +26 -0
- package/dist/cli/v4/display.js +18 -8
- package/dist/cli/v4/replyRenderer.js +31 -23
- package/dist/cli/v4/updateBootPrompt.js +170 -0
- package/dist/core/playwrightBridge.js +129 -0
- package/dist/core/v4/aidenAgent.js +527 -5
- package/dist/core/v4/browserState.js +436 -0
- package/dist/core/v4/checkpoint.js +79 -0
- package/dist/core/v4/daemon/bootstrap.js +651 -0
- package/dist/core/v4/daemon/cleanShutdown.js +154 -0
- package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
- package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
- package/dist/core/v4/daemon/cron/migration.js +199 -0
- package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
- package/dist/core/v4/daemon/daemonConfig.js +90 -0
- package/dist/core/v4/daemon/db/connection.js +106 -0
- package/dist/core/v4/daemon/db/migrations.js +362 -0
- package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
- package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
- package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
- package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
- package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
- package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
- package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
- package/dist/core/v4/daemon/dispatcher/index.js +53 -0
- package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
- package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
- package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
- package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
- package/dist/core/v4/daemon/drain.js +156 -0
- package/dist/core/v4/daemon/eventLoopLag.js +73 -0
- package/dist/core/v4/daemon/health.js +159 -0
- package/dist/core/v4/daemon/idempotencyStore.js +204 -0
- package/dist/core/v4/daemon/index.js +179 -0
- package/dist/core/v4/daemon/instanceTracker.js +99 -0
- package/dist/core/v4/daemon/resourceRegistry.js +150 -0
- package/dist/core/v4/daemon/restartCode.js +32 -0
- package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
- package/dist/core/v4/daemon/runStore.js +144 -0
- package/dist/core/v4/daemon/runtimeLock.js +167 -0
- package/dist/core/v4/daemon/signals.js +50 -0
- package/dist/core/v4/daemon/supervisor.js +272 -0
- package/dist/core/v4/daemon/triggerBus.js +279 -0
- package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
- package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
- package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
- package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
- package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
- package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
- package/dist/core/v4/daemon/triggers/email/index.js +332 -0
- package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
- package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
- package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
- package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
- package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
- package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
- package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
- package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
- package/dist/core/v4/daemon/triggers/webhook.js +376 -0
- package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
- package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
- package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
- package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
- package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
- package/dist/core/v4/daemon/types.js +15 -0
- package/dist/core/v4/dockerSession.js +461 -0
- package/dist/core/v4/dryRun.js +117 -0
- package/dist/core/v4/failureClassifier.js +779 -0
- package/dist/core/v4/providerFallback.js +35 -2
- package/dist/core/v4/recoveryReport.js +449 -0
- package/dist/core/v4/runtimeToggles.js +214 -0
- package/dist/core/v4/sandboxConfig.js +285 -0
- package/dist/core/v4/sandboxFs.js +316 -0
- package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
- package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
- package/dist/core/v4/subagent/childBuilder.js +391 -0
- package/dist/core/v4/subagent/fanout.js +75 -51
- package/dist/core/v4/subagent/spawnPause.js +191 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
- package/dist/core/v4/suggestionCatalog.js +41 -0
- package/dist/core/v4/suggestionEngine.js +210 -0
- package/dist/core/v4/toolRegistry.js +37 -3
- package/dist/core/v4/turnState.js +587 -0
- package/dist/core/v4/update/checkUpdate.js +63 -3
- package/dist/core/v4/update/installMethodDetect.js +115 -0
- package/dist/core/v4/update/registryClient.js +121 -0
- package/dist/core/v4/update/skipState.js +75 -0
- package/dist/core/v4/verifier.js +448 -0
- package/dist/core/version.js +1 -1
- package/dist/moat/plannerGuard.js +29 -0
- package/dist/providers/v4/anthropicAdapter.js +31 -3
- package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
- package/dist/providers/v4/codexResponsesAdapter.js +25 -2
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
- package/dist/tools/v4/browser/_observer.js +224 -0
- package/dist/tools/v4/browser/browserBlocker.js +396 -0
- package/dist/tools/v4/browser/browserClick.js +18 -1
- package/dist/tools/v4/browser/browserClose.js +18 -1
- package/dist/tools/v4/browser/browserExtract.js +5 -1
- package/dist/tools/v4/browser/browserFill.js +17 -1
- package/dist/tools/v4/browser/browserGetUrl.js +5 -1
- package/dist/tools/v4/browser/browserNavigate.js +16 -1
- package/dist/tools/v4/browser/browserScreenshot.js +5 -1
- package/dist/tools/v4/browser/browserScroll.js +18 -1
- package/dist/tools/v4/browser/browserType.js +17 -1
- package/dist/tools/v4/browser/captchaCheck.js +5 -1
- package/dist/tools/v4/executeCode.js +1 -0
- package/dist/tools/v4/files/fileCopy.js +56 -2
- package/dist/tools/v4/files/fileDelete.js +38 -1
- package/dist/tools/v4/files/fileList.js +12 -1
- package/dist/tools/v4/files/fileMove.js +59 -2
- package/dist/tools/v4/files/filePatch.js +43 -1
- package/dist/tools/v4/files/fileRead.js +12 -1
- package/dist/tools/v4/files/fileWrite.js +41 -1
- package/dist/tools/v4/index.js +88 -61
- package/dist/tools/v4/memory/memoryAdd.js +14 -0
- package/dist/tools/v4/memory/memoryRemove.js +14 -0
- package/dist/tools/v4/memory/memoryReplace.js +15 -0
- package/dist/tools/v4/memory/sessionSummary.js +12 -0
- package/dist/tools/v4/process/processKill.js +19 -0
- package/dist/tools/v4/process/processList.js +1 -0
- package/dist/tools/v4/process/processLogRead.js +1 -0
- package/dist/tools/v4/process/processSpawn.js +13 -0
- package/dist/tools/v4/process/processWait.js +1 -0
- package/dist/tools/v4/sessions/recallSession.js +1 -0
- package/dist/tools/v4/sessions/sessionList.js +1 -0
- package/dist/tools/v4/sessions/sessionSearch.js +1 -0
- package/dist/tools/v4/skills/lookupToolSchema.js +7 -0
- package/dist/tools/v4/skills/skillManage.js +13 -0
- package/dist/tools/v4/skills/skillView.js +1 -0
- package/dist/tools/v4/skills/skillsList.js +1 -0
- package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
- package/dist/tools/v4/subagent/subagentFanout.js +54 -1
- package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
- package/dist/tools/v4/system/appClose.js +13 -0
- package/dist/tools/v4/system/appInput.js +13 -0
- package/dist/tools/v4/system/appLaunch.js +13 -0
- package/dist/tools/v4/system/clipboardRead.js +1 -0
- package/dist/tools/v4/system/clipboardWrite.js +14 -0
- package/dist/tools/v4/system/mediaKey.js +12 -0
- package/dist/tools/v4/system/mediaSessions.js +1 -0
- package/dist/tools/v4/system/mediaTransport.js +13 -0
- package/dist/tools/v4/system/naturalEvents.js +1 -0
- package/dist/tools/v4/system/nowPlaying.js +1 -0
- package/dist/tools/v4/system/osProcessList.js +1 -0
- package/dist/tools/v4/system/screenshot.js +1 -0
- package/dist/tools/v4/system/systemInfo.js +1 -0
- package/dist/tools/v4/system/volumeSet.js +17 -0
- package/dist/tools/v4/terminal/shellExec.js +81 -9
- package/dist/tools/v4/web/deepResearch.js +1 -0
- package/dist/tools/v4/web/openUrl.js +1 -0
- package/dist/tools/v4/web/webFetch.js +1 -0
- package/dist/tools/v4/web/webPage.js +1 -0
- package/dist/tools/v4/web/webSearch.js +1 -0
- package/dist/tools/v4/web/youtubeSearch.js +1 -0
- package/package.json +13 -3
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/cleanShutdown.ts — v4.5 Phase 1: clean-shutdown marker.
|
|
10
|
+
*
|
|
11
|
+
* Two-tier crash safety net (paired with `restartFailureCounter.ts`):
|
|
12
|
+
*
|
|
13
|
+
* - Marker file `<daemonDir>/.clean_shutdown` — touched as the
|
|
14
|
+
* LAST step of a graceful drain. Empty file; the presence is
|
|
15
|
+
* the signal.
|
|
16
|
+
* - On boot: if the marker exists, consume + unlink it. Boot is
|
|
17
|
+
* "clean," meaning the previous instance exited gracefully and
|
|
18
|
+
* any active sessions can be considered for normal resume.
|
|
19
|
+
* - On boot: if the marker is ABSENT, scan `daemon_instances` for
|
|
20
|
+
* rows with `shutdown_at IS NULL` and stale `last_heartbeat`
|
|
21
|
+
* (default > 30s). Each such row is a crash candidate; we
|
|
22
|
+
* write a `crash_reports` entry, mark the row's
|
|
23
|
+
* `shutdown_reason='crash'`, and increment
|
|
24
|
+
* `restart_failure_counts` for every still-active session that
|
|
25
|
+
* was owned by the crashed instance.
|
|
26
|
+
*/
|
|
27
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
28
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
29
|
+
};
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.touchCleanShutdownMarker = touchCleanShutdownMarker;
|
|
32
|
+
exports.isCleanShutdown = isCleanShutdown;
|
|
33
|
+
exports.consumeCleanShutdownMarker = consumeCleanShutdownMarker;
|
|
34
|
+
exports.evaluateBootState = evaluateBootState;
|
|
35
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
36
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
37
|
+
const CRASH_STALE_HEARTBEAT_MS = 30000;
|
|
38
|
+
/** Touch the marker. Call from the final step of `drain()`. */
|
|
39
|
+
function touchCleanShutdownMarker(markerPath) {
|
|
40
|
+
try {
|
|
41
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(markerPath), { recursive: true });
|
|
42
|
+
const fd = node_fs_1.default.openSync(markerPath, 'w');
|
|
43
|
+
node_fs_1.default.closeSync(fd);
|
|
44
|
+
}
|
|
45
|
+
catch { /* best-effort */ }
|
|
46
|
+
}
|
|
47
|
+
/** True when the marker file currently exists on disk. */
|
|
48
|
+
function isCleanShutdown(markerPath) {
|
|
49
|
+
try {
|
|
50
|
+
return node_fs_1.default.existsSync(markerPath);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Atomic "read + delete" of the marker. Returns true iff present. */
|
|
57
|
+
function consumeCleanShutdownMarker(markerPath) {
|
|
58
|
+
if (!isCleanShutdown(markerPath))
|
|
59
|
+
return false;
|
|
60
|
+
try {
|
|
61
|
+
node_fs_1.default.unlinkSync(markerPath);
|
|
62
|
+
}
|
|
63
|
+
catch { /* best-effort */ }
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Top-level boot-state evaluator. Returns the decision the caller
|
|
68
|
+
* should act on, AND writes any required crash-report + instance-row
|
|
69
|
+
* cleanup to the database.
|
|
70
|
+
*
|
|
71
|
+
* Idempotent: rerunning on the same boot is safe (the marker is
|
|
72
|
+
* already consumed, and crashed rows already have `shutdown_at`
|
|
73
|
+
* filled).
|
|
74
|
+
*/
|
|
75
|
+
function evaluateBootState(opts) {
|
|
76
|
+
const now = opts.now ?? Date.now();
|
|
77
|
+
const staleMs = opts.staleHeartbeatMs ?? CRASH_STALE_HEARTBEAT_MS;
|
|
78
|
+
// Marker check first — it's the fast happy path.
|
|
79
|
+
if (consumeCleanShutdownMarker(opts.markerPath)) {
|
|
80
|
+
return {
|
|
81
|
+
cleanShutdown: true,
|
|
82
|
+
suspendActiveSessions: false,
|
|
83
|
+
crashDetected: false,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// Find every prior instance that didn't mark shutdown_at and whose
|
|
87
|
+
// heartbeat is stale enough to be considered crashed. The newly-
|
|
88
|
+
// booted instance is excluded (instance_id mismatch).
|
|
89
|
+
const cutoff = now - staleMs;
|
|
90
|
+
const candidates = opts.db
|
|
91
|
+
.prepare(`SELECT instance_id, pid, started_at, last_heartbeat
|
|
92
|
+
FROM daemon_instances
|
|
93
|
+
WHERE shutdown_at IS NULL
|
|
94
|
+
AND instance_id != ?
|
|
95
|
+
AND last_heartbeat < ?`)
|
|
96
|
+
.all(opts.instanceId, cutoff);
|
|
97
|
+
if (candidates.length === 0) {
|
|
98
|
+
// No marker AND no crashed siblings — could be the very first
|
|
99
|
+
// boot OR a quick restart that didn't generate a stale row yet.
|
|
100
|
+
// Treat as dirty boot to be safe (no active sessions to suspend
|
|
101
|
+
// when the table is empty, so this is harmless on a fresh
|
|
102
|
+
// install).
|
|
103
|
+
return {
|
|
104
|
+
cleanShutdown: false,
|
|
105
|
+
suspendActiveSessions: true,
|
|
106
|
+
crashDetected: false,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const tx = opts.db.transaction(() => {
|
|
110
|
+
for (const c of candidates) {
|
|
111
|
+
// Affected sessions: any run in 'queued' or 'running' status
|
|
112
|
+
// owned by the crashed instance.
|
|
113
|
+
const sessions = opts.db
|
|
114
|
+
.prepare(`SELECT DISTINCT session_id
|
|
115
|
+
FROM runs
|
|
116
|
+
WHERE instance_id = ?
|
|
117
|
+
AND status IN ('queued','running')`)
|
|
118
|
+
.all(c.instance_id);
|
|
119
|
+
const sessionIds = sessions.map((s) => s.session_id);
|
|
120
|
+
opts.db
|
|
121
|
+
.prepare(`INSERT INTO crash_reports
|
|
122
|
+
(instance_id, detected_at, prev_started_at,
|
|
123
|
+
prev_last_heartbeat, prev_pid,
|
|
124
|
+
affected_sessions, ps_snapshot, details)
|
|
125
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
126
|
+
.run(opts.instanceId, now, c.started_at, c.last_heartbeat, c.pid, JSON.stringify(sessionIds), opts.psSnapshot ?? null, JSON.stringify({
|
|
127
|
+
dirty_shutdown: true,
|
|
128
|
+
stuck_loop_sessions: sessionIds,
|
|
129
|
+
}));
|
|
130
|
+
opts.db
|
|
131
|
+
.prepare(`UPDATE daemon_instances
|
|
132
|
+
SET shutdown_at = COALESCE(shutdown_at, ?),
|
|
133
|
+
shutdown_reason = COALESCE(shutdown_reason, ?)
|
|
134
|
+
WHERE instance_id = ?`)
|
|
135
|
+
.run(now, 'crash', c.instance_id);
|
|
136
|
+
// Mark interrupted runs (they shouldn't appear "running" forever).
|
|
137
|
+
opts.db
|
|
138
|
+
.prepare(`UPDATE runs
|
|
139
|
+
SET status = 'interrupted',
|
|
140
|
+
resume_pending = 1,
|
|
141
|
+
resume_reason = 'crash_recovery',
|
|
142
|
+
completed_at = ?
|
|
143
|
+
WHERE instance_id = ?
|
|
144
|
+
AND status IN ('queued','running')`)
|
|
145
|
+
.run(now, c.instance_id);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
tx();
|
|
149
|
+
return {
|
|
150
|
+
cleanShutdown: false,
|
|
151
|
+
suspendActiveSessions: true,
|
|
152
|
+
crashDetected: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/cron/cronBridge.ts — v4.5 Phase 5b.
|
|
10
|
+
*
|
|
11
|
+
* Bidirectional converter between the existing `CronJobV2` shape
|
|
12
|
+
* (JSON-backed, see core/v4/cron/cronState.ts) and the SQLite
|
|
13
|
+
* `scheduled_workflows` row shape introduced in schema v5.
|
|
14
|
+
*
|
|
15
|
+
* Used in two places:
|
|
16
|
+
* 1. cron migration (one-shot on first v5 boot) — CronJobV2 →
|
|
17
|
+
* ScheduledWorkflowRow → INSERT.
|
|
18
|
+
* 2. cron emitter — when the cron heartbeat ticks, reads
|
|
19
|
+
* scheduled_workflows rows + maps back to CronJobV2-ish
|
|
20
|
+
* shape for the existing fire pipeline (until cron's storage
|
|
21
|
+
* layer fully migrates).
|
|
22
|
+
*
|
|
23
|
+
* Schedule expression encoding:
|
|
24
|
+
* - interval → "interval:<intervalMs>" (e.g. "interval:300000")
|
|
25
|
+
* - cron → "cron:<expr>" (e.g. "cron:0 9 * * *")
|
|
26
|
+
* - oneshot → "oneshot:<isoTimestamp>"
|
|
27
|
+
*
|
|
28
|
+
* This keeps `schedule_expression` a single text column that's
|
|
29
|
+
* round-trippable + greppable.
|
|
30
|
+
*
|
|
31
|
+
* Pure conversion — no I/O.
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.jobToRow = jobToRow;
|
|
35
|
+
exports.encodeScheduleExpression = encodeScheduleExpression;
|
|
36
|
+
exports.decodeScheduleExpression = decodeScheduleExpression;
|
|
37
|
+
// ── Encoders ───────────────────────────────────────────────────────────────
|
|
38
|
+
/**
|
|
39
|
+
* CronJobV2 → ScheduledWorkflowRow. Used by the one-shot migration
|
|
40
|
+
* from cron_jobs.json. The action string lands in payload_json as
|
|
41
|
+
* `{ action: "<cmd>" }` so the existing fire pipeline can recover
|
|
42
|
+
* it; future phases may add structured payload fields without
|
|
43
|
+
* breaking the round-trip.
|
|
44
|
+
*/
|
|
45
|
+
function jobToRow(job, nowMs = Date.now()) {
|
|
46
|
+
const scheduleExpression = encodeScheduleExpression(job);
|
|
47
|
+
const payloadJson = JSON.stringify({
|
|
48
|
+
action: job.action,
|
|
49
|
+
description: job.description,
|
|
50
|
+
runCount: job.runCount,
|
|
51
|
+
legacyState: job.state,
|
|
52
|
+
pausedAt: job.pausedAt ?? null,
|
|
53
|
+
pausedReason: job.pausedReason ?? null,
|
|
54
|
+
});
|
|
55
|
+
const createdAt = parseIsoToMs(job.createdAt) ?? nowMs;
|
|
56
|
+
const lastFiredAt = job.lastRun ? parseIsoToMs(job.lastRun) : null;
|
|
57
|
+
const nextFireAt = job.nextRun ? parseIsoToMs(job.nextRun) : null;
|
|
58
|
+
return {
|
|
59
|
+
id: job.id,
|
|
60
|
+
name: job.description || `cron-${job.id}`,
|
|
61
|
+
schedule_expression: scheduleExpression,
|
|
62
|
+
timezone: 'UTC',
|
|
63
|
+
enabled: job.enabled ? 1 : 0,
|
|
64
|
+
payload_json: payloadJson,
|
|
65
|
+
prompt_template: null,
|
|
66
|
+
deliver_only: 0,
|
|
67
|
+
misfire_policy: 'skip_stale',
|
|
68
|
+
fire_rate_limit: null,
|
|
69
|
+
catch_up_limit: null,
|
|
70
|
+
grace_ms: null,
|
|
71
|
+
last_fired_at: lastFiredAt,
|
|
72
|
+
next_fire_at: nextFireAt,
|
|
73
|
+
created_at: createdAt,
|
|
74
|
+
updated_at: nowMs,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Encode the schedule into a single text column. The decoder
|
|
79
|
+
* recovers kind + ms / expr / iso.
|
|
80
|
+
*/
|
|
81
|
+
function encodeScheduleExpression(job) {
|
|
82
|
+
if (job.kind === 'interval' && typeof job.intervalMs === 'number') {
|
|
83
|
+
return `interval:${job.intervalMs}`;
|
|
84
|
+
}
|
|
85
|
+
if (job.kind === 'cron' && typeof job.cronExpr === 'string' && job.cronExpr.length > 0) {
|
|
86
|
+
return `cron:${job.cronExpr}`;
|
|
87
|
+
}
|
|
88
|
+
if (job.kind === 'oneshot' && typeof job.oneshotIso === 'string') {
|
|
89
|
+
return `oneshot:${job.oneshotIso}`;
|
|
90
|
+
}
|
|
91
|
+
// Defensive — should never hit when migrating valid jobs.
|
|
92
|
+
return job.schedule || 'interval:0';
|
|
93
|
+
}
|
|
94
|
+
function decodeScheduleExpression(expr) {
|
|
95
|
+
if (typeof expr !== 'string' || expr.length === 0)
|
|
96
|
+
return null;
|
|
97
|
+
const colon = expr.indexOf(':');
|
|
98
|
+
if (colon <= 0)
|
|
99
|
+
return null;
|
|
100
|
+
const kind = expr.slice(0, colon);
|
|
101
|
+
const rest = expr.slice(colon + 1);
|
|
102
|
+
if (kind === 'interval') {
|
|
103
|
+
const ms = Number.parseInt(rest, 10);
|
|
104
|
+
if (!Number.isFinite(ms) || ms <= 0)
|
|
105
|
+
return null;
|
|
106
|
+
return { kind: 'interval', intervalMs: ms };
|
|
107
|
+
}
|
|
108
|
+
if (kind === 'cron') {
|
|
109
|
+
if (rest.length === 0)
|
|
110
|
+
return null;
|
|
111
|
+
return { kind: 'cron', cronExpr: rest };
|
|
112
|
+
}
|
|
113
|
+
if (kind === 'oneshot') {
|
|
114
|
+
if (rest.length === 0)
|
|
115
|
+
return null;
|
|
116
|
+
return { kind: 'oneshot', iso: rest };
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
121
|
+
function parseIsoToMs(iso) {
|
|
122
|
+
if (!iso)
|
|
123
|
+
return null;
|
|
124
|
+
const t = Date.parse(iso);
|
|
125
|
+
return Number.isFinite(t) ? t : null;
|
|
126
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/cron/cronEmitter.ts — v4.5 Phase 5b.
|
|
10
|
+
*
|
|
11
|
+
* The cron-mode trigger producer.
|
|
12
|
+
*
|
|
13
|
+
* When `AIDEN_DAEMON=1`, the existing cron heartbeat fires through
|
|
14
|
+
* a daemon-mode action runner that DOESN'T shell-out. Instead, it
|
|
15
|
+
* inserts a `trigger_event` into the bus + updates last_fired_at
|
|
16
|
+
* on the corresponding `scheduled_workflows` row. The Phase 5a
|
|
17
|
+
* dispatcher consumes the event and routes it through the agent
|
|
18
|
+
* loop (or the deliverOnly stub) just like every other trigger
|
|
19
|
+
* source.
|
|
20
|
+
*
|
|
21
|
+
* The misfire policy fires HERE, not in the dispatcher — the
|
|
22
|
+
* dispatcher should never see a stale event for a cron that the
|
|
23
|
+
* policy said to skip.
|
|
24
|
+
*
|
|
25
|
+
* Backward compat: `AIDEN_DAEMON=0` keeps the legacy
|
|
26
|
+
* `defaultRunAction` shell-exec path untouched. This module is a
|
|
27
|
+
* separate emitter the bootstrap installs as `cronManager`'s
|
|
28
|
+
* runAction override.
|
|
29
|
+
*
|
|
30
|
+
* Public API:
|
|
31
|
+
* - `createCronEmitter({triggerBus, db, log})` → RunActionFn
|
|
32
|
+
* compatible with `core/v4/cron/cronExecute.ts::RunActionFn`.
|
|
33
|
+
*/
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.createCronEmitter = createCronEmitter;
|
|
36
|
+
const misfirePolicy_1 = require("./misfirePolicy");
|
|
37
|
+
/**
|
|
38
|
+
* Build a daemon-mode runAction. Returns a function with the same
|
|
39
|
+
* signature as `core/v4/cron/cronExecute.ts::RunActionFn` so the
|
|
40
|
+
* existing cron firing pipeline (`fireJob`) can swap it in.
|
|
41
|
+
*
|
|
42
|
+
* Logic per fire:
|
|
43
|
+
* 1. Resolve the scheduled_workflows row by job.id.
|
|
44
|
+
* 2. Read misfire policy + scheduled-for instant.
|
|
45
|
+
* 3. Apply the policy. When fire=false → return immediately
|
|
46
|
+
* (the cron tick will not record an output but also won't
|
|
47
|
+
* treat it as a fire).
|
|
48
|
+
* 4. When fire=true, emit `fireCount` trigger_events into the
|
|
49
|
+
* bus (each with a distinct idempotency key so the dispatcher
|
|
50
|
+
* processes them as separate runs).
|
|
51
|
+
* 5. Update last_fired_at on the row.
|
|
52
|
+
*
|
|
53
|
+
* The cron pipeline records `last_status='ok'` when the action
|
|
54
|
+
* resolves without throwing. Daemon-mode insertion is fast (one
|
|
55
|
+
* SQL statement per fire) and synchronous — no actual work
|
|
56
|
+
* happens here.
|
|
57
|
+
*/
|
|
58
|
+
function createCronEmitter(opts) {
|
|
59
|
+
const log = opts.log ?? (() => { });
|
|
60
|
+
const now = opts.now ?? Date.now;
|
|
61
|
+
return async (job, _signal) => {
|
|
62
|
+
void _signal; // cron emitter is fast + sync; no cancellation needed
|
|
63
|
+
try {
|
|
64
|
+
const row = readWorkflowRow(opts.db, job.id);
|
|
65
|
+
if (!row) {
|
|
66
|
+
// Workflow missing from SQLite — fall back to a single-fire emit so
|
|
67
|
+
// operations that ran during the migration window aren't lost.
|
|
68
|
+
emitSingle(opts.triggerBus, job, now(), 'workflow_row_missing');
|
|
69
|
+
log('warn', `[cron-emitter] no scheduled_workflows row for job ${job.id} — falling back to single fire`);
|
|
70
|
+
return { output: 'enqueued (workflow row missing — single fire)', failed: false };
|
|
71
|
+
}
|
|
72
|
+
const policy = (0, misfirePolicy_1.isMisfirePolicy)(row.misfire_policy) ? row.misfire_policy : 'skip_stale';
|
|
73
|
+
const scheduledFor = row.next_fire_at ?? now();
|
|
74
|
+
// Decode interval (for catch_up_with_limit period math).
|
|
75
|
+
const periodMs = decodePeriodMs(row.schedule_expression);
|
|
76
|
+
const decision = (0, misfirePolicy_1.applyMisfirePolicy)({
|
|
77
|
+
policy: policy,
|
|
78
|
+
scheduledFor,
|
|
79
|
+
now: now(),
|
|
80
|
+
graceMs: row.grace_ms ?? undefined,
|
|
81
|
+
catchUpLimit: row.catch_up_limit ?? undefined,
|
|
82
|
+
periodMs: periodMs ?? undefined,
|
|
83
|
+
});
|
|
84
|
+
if (!decision.fire) {
|
|
85
|
+
log('info', `[cron-emitter] job ${job.id} ${decision.reason}`);
|
|
86
|
+
return { output: `skipped (${decision.reason})`, failed: false };
|
|
87
|
+
}
|
|
88
|
+
// Emit `fireCount` events. For catch_up_with_limit > 1, the
|
|
89
|
+
// idempotency key encodes the iteration index so each fire
|
|
90
|
+
// produces a distinct trigger_event row.
|
|
91
|
+
let inserted = 0;
|
|
92
|
+
for (let i = 0; i < decision.fireCount; i++) {
|
|
93
|
+
const idemKey = decision.fireCount === 1
|
|
94
|
+
? new Date(scheduledFor).toISOString()
|
|
95
|
+
: `${new Date(scheduledFor).toISOString()}#${i}`;
|
|
96
|
+
const r = opts.triggerBus.insert({
|
|
97
|
+
source: 'schedule',
|
|
98
|
+
sourceKey: job.id,
|
|
99
|
+
idempotencyKey: idemKey,
|
|
100
|
+
payload: {
|
|
101
|
+
workflowId: job.id,
|
|
102
|
+
name: row.name,
|
|
103
|
+
scheduledFor,
|
|
104
|
+
scheduledForIso: new Date(scheduledFor).toISOString(),
|
|
105
|
+
action: job.action,
|
|
106
|
+
description: job.description,
|
|
107
|
+
misfirePolicy: policy,
|
|
108
|
+
iteration: i,
|
|
109
|
+
fireCount: decision.fireCount,
|
|
110
|
+
fireReason: decision.reason,
|
|
111
|
+
promptTemplate: row.prompt_template,
|
|
112
|
+
deliverOnly: row.deliver_only === 1,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
if (r.inserted)
|
|
116
|
+
inserted += 1;
|
|
117
|
+
}
|
|
118
|
+
// Update last_fired_at. next_fire_at is recomputed by the
|
|
119
|
+
// existing cron pipeline (`computeNextFire` in cronExecute.ts)
|
|
120
|
+
// before this runAction is called, so we don't touch it here.
|
|
121
|
+
try {
|
|
122
|
+
opts.db.prepare(`UPDATE scheduled_workflows SET last_fired_at = ?, updated_at = ? WHERE id = ?`).run(now(), now(), job.id);
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
log('warn', `[cron-emitter] failed to update last_fired_at for ${job.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
126
|
+
}
|
|
127
|
+
const msg = decision.fireCount === 1
|
|
128
|
+
? `enqueued 1 event for job ${job.id}`
|
|
129
|
+
: `enqueued ${inserted}/${decision.fireCount} events for job ${job.id} (catch_up)`;
|
|
130
|
+
log('info', `[cron-emitter] ${msg}`);
|
|
131
|
+
return { output: msg, failed: false };
|
|
132
|
+
}
|
|
133
|
+
catch (e) {
|
|
134
|
+
const msg = e instanceof Error ? (e.stack ?? e.message) : String(e);
|
|
135
|
+
log('error', `[cron-emitter] job ${job.id} emit failed: ${msg}`);
|
|
136
|
+
return { output: msg, failed: true };
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
141
|
+
function readWorkflowRow(db, jobId) {
|
|
142
|
+
try {
|
|
143
|
+
const row = db
|
|
144
|
+
.prepare('SELECT * FROM scheduled_workflows WHERE id = ?')
|
|
145
|
+
.get(jobId);
|
|
146
|
+
return row ?? null;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function emitSingle(bus, job, scheduledFor, reason) {
|
|
153
|
+
bus.insert({
|
|
154
|
+
source: 'schedule',
|
|
155
|
+
sourceKey: job.id,
|
|
156
|
+
idempotencyKey: new Date(scheduledFor).toISOString(),
|
|
157
|
+
payload: {
|
|
158
|
+
workflowId: job.id,
|
|
159
|
+
scheduledFor,
|
|
160
|
+
scheduledForIso: new Date(scheduledFor).toISOString(),
|
|
161
|
+
action: job.action,
|
|
162
|
+
description: job.description,
|
|
163
|
+
fireReason: reason,
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/** Decode `interval:<ms>` → number. Returns null for other kinds. */
|
|
168
|
+
function decodePeriodMs(scheduleExpression) {
|
|
169
|
+
if (!scheduleExpression.startsWith('interval:'))
|
|
170
|
+
return null;
|
|
171
|
+
const n = Number.parseInt(scheduleExpression.slice('interval:'.length), 10);
|
|
172
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
173
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* core/v4/daemon/cron/migration.ts — v4.5 Phase 5b.
|
|
10
|
+
*
|
|
11
|
+
* One-shot data migration: read `cron_jobs.json` (existing JSON-
|
|
12
|
+
* backed cron store) → `scheduled_workflows` table (SQLite, schema
|
|
13
|
+
* v5). Runs on first daemon boot AFTER the v5 DDL migration applies.
|
|
14
|
+
*
|
|
15
|
+
* Behaviour (Q-P5-3a — automatic):
|
|
16
|
+
* 1. Skip if `scheduled_workflows` already has rows (idempotent).
|
|
17
|
+
* 2. Skip if `cron_jobs.json` doesn't exist (first boot, no cron).
|
|
18
|
+
* 3. Read the JSON via the existing `readCronState` reader (handles
|
|
19
|
+
* v1/v2 schema + corruption auto-repair).
|
|
20
|
+
* 4. Map each `CronJobV2` to a `scheduled_workflows` row via
|
|
21
|
+
* `cronBridge.jobToRow`.
|
|
22
|
+
* 5. Insert all rows in a single transaction.
|
|
23
|
+
* 6. Back up the source file: `cron_jobs.json.pre-v5-migration.<ts>.bak`.
|
|
24
|
+
* ORIGINAL FILE LEFT IN PLACE — non-daemon mode keeps working.
|
|
25
|
+
* 7. Log: `[cron] migrated <N> jobs to SQLite, backup at <path>`.
|
|
26
|
+
*
|
|
27
|
+
* Never throws. Migration failures log loudly but the daemon
|
|
28
|
+
* continues booting (cron will be empty in daemon mode; the
|
|
29
|
+
* operator can re-run the migration manually via a CLI command
|
|
30
|
+
* shipped in Phase 6).
|
|
31
|
+
*/
|
|
32
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
33
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
34
|
+
};
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runCronMigration = runCronMigration;
|
|
37
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
38
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
39
|
+
const cronState_1 = require("../../cron/cronState");
|
|
40
|
+
const cronState_2 = require("../../cron/cronState");
|
|
41
|
+
const cronBridge_1 = require("./cronBridge");
|
|
42
|
+
/**
|
|
43
|
+
* Run the one-shot migration. Returns a structured result so the
|
|
44
|
+
* caller can surface the outcome via daemon health endpoints.
|
|
45
|
+
*
|
|
46
|
+
* Synchronous by design — the bootstrap path is already sync and
|
|
47
|
+
* the migration touches one small JSON file + one batched SQL
|
|
48
|
+
* insert transaction. Keeping it sync avoids an async-edge in the
|
|
49
|
+
* daemon's deterministic boot ordering.
|
|
50
|
+
*/
|
|
51
|
+
function runCronMigration(opts) {
|
|
52
|
+
const log = opts.log ?? (() => { });
|
|
53
|
+
const now = opts.now ?? Date.now;
|
|
54
|
+
const errors = [];
|
|
55
|
+
// ── Step 1: idempotency check on the SQLite side ──────────────────────
|
|
56
|
+
let existingCount;
|
|
57
|
+
try {
|
|
58
|
+
existingCount = opts.db
|
|
59
|
+
.prepare('SELECT COUNT(*) AS c FROM scheduled_workflows')
|
|
60
|
+
.get().c;
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
64
|
+
log('error', `[cron-migration] count check failed: ${msg}`);
|
|
65
|
+
return {
|
|
66
|
+
ran: false,
|
|
67
|
+
migrated: 0,
|
|
68
|
+
skipped: 0,
|
|
69
|
+
backupPath: null,
|
|
70
|
+
reason: 'sqlite_count_failed',
|
|
71
|
+
errors: [msg],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (existingCount > 0) {
|
|
75
|
+
log('info', `[cron-migration] skipped — scheduled_workflows already has ${existingCount} rows`);
|
|
76
|
+
return {
|
|
77
|
+
ran: false,
|
|
78
|
+
migrated: 0,
|
|
79
|
+
skipped: existingCount,
|
|
80
|
+
backupPath: null,
|
|
81
|
+
reason: 'already_migrated',
|
|
82
|
+
errors,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// ── Step 2: source-file existence ─────────────────────────────────────
|
|
86
|
+
const sourcePath = opts.sourcePath ?? (0, cronState_1.defaultCronPaths)().stateFile;
|
|
87
|
+
if (!node_fs_1.default.existsSync(sourcePath)) {
|
|
88
|
+
log('info', `[cron-migration] skipped — no cron_jobs.json at ${sourcePath}`);
|
|
89
|
+
return {
|
|
90
|
+
ran: false,
|
|
91
|
+
migrated: 0,
|
|
92
|
+
skipped: 0,
|
|
93
|
+
backupPath: null,
|
|
94
|
+
reason: 'no_source_file',
|
|
95
|
+
errors,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// ── Step 3: read existing JSON (sync — small file) ────────────────────
|
|
99
|
+
let jobs;
|
|
100
|
+
try {
|
|
101
|
+
const raw = node_fs_1.default.readFileSync(sourcePath, 'utf-8');
|
|
102
|
+
let parsed;
|
|
103
|
+
try {
|
|
104
|
+
parsed = JSON.parse(raw);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Same auto-repair fallback as readCronState: strip trailing commas.
|
|
108
|
+
const stripped = raw
|
|
109
|
+
.replace(/,(\s*[}\]])/g, '$1')
|
|
110
|
+
.replace(/^\s*\/\/.*$/gm, '');
|
|
111
|
+
parsed = JSON.parse(stripped);
|
|
112
|
+
}
|
|
113
|
+
const state = (0, cronState_2.migrateToV2)(parsed);
|
|
114
|
+
jobs = state.jobs;
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
118
|
+
log('error', `[cron-migration] failed to read ${sourcePath}: ${msg}`);
|
|
119
|
+
return {
|
|
120
|
+
ran: false,
|
|
121
|
+
migrated: 0,
|
|
122
|
+
skipped: 0,
|
|
123
|
+
backupPath: null,
|
|
124
|
+
reason: 'read_failed',
|
|
125
|
+
errors: [msg],
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (jobs.length === 0) {
|
|
129
|
+
log('info', `[cron-migration] no jobs in ${sourcePath} — nothing to migrate`);
|
|
130
|
+
// Still create a backup so the operator has a clear "migration
|
|
131
|
+
// ran" signal (zero-row migrations are still events).
|
|
132
|
+
return {
|
|
133
|
+
ran: true,
|
|
134
|
+
migrated: 0,
|
|
135
|
+
skipped: 0,
|
|
136
|
+
backupPath: null,
|
|
137
|
+
errors,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// ── Step 4 + 5: map + insert in one transaction ───────────────────────
|
|
141
|
+
const rows = [];
|
|
142
|
+
for (const job of jobs) {
|
|
143
|
+
try {
|
|
144
|
+
rows.push((0, cronBridge_1.jobToRow)(job, now()));
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
148
|
+
errors.push(`job ${job.id}: ${msg}`);
|
|
149
|
+
log('warn', `[cron-migration] skipping malformed job ${job.id}: ${msg}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
let migrated = 0;
|
|
153
|
+
try {
|
|
154
|
+
const insert = opts.db.prepare(`INSERT INTO scheduled_workflows
|
|
155
|
+
(id, name, schedule_expression, timezone, enabled, payload_json,
|
|
156
|
+
prompt_template, deliver_only, misfire_policy, fire_rate_limit,
|
|
157
|
+
catch_up_limit, grace_ms, last_fired_at, next_fire_at,
|
|
158
|
+
created_at, updated_at)
|
|
159
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
160
|
+
const tx = opts.db.transaction((batch) => {
|
|
161
|
+
for (const r of batch) {
|
|
162
|
+
insert.run(r.id, r.name, r.schedule_expression, r.timezone, r.enabled, r.payload_json, r.prompt_template, r.deliver_only, r.misfire_policy, r.fire_rate_limit, r.catch_up_limit, r.grace_ms, r.last_fired_at, r.next_fire_at, r.created_at, r.updated_at);
|
|
163
|
+
migrated += 1;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
tx(rows);
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
170
|
+
log('error', `[cron-migration] SQL insert failed: ${msg}`);
|
|
171
|
+
return {
|
|
172
|
+
ran: false,
|
|
173
|
+
migrated: 0,
|
|
174
|
+
skipped: rows.length,
|
|
175
|
+
backupPath: null,
|
|
176
|
+
reason: 'insert_failed',
|
|
177
|
+
errors: [...errors, msg],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// ── Step 6: backup ────────────────────────────────────────────────────
|
|
181
|
+
const backupPath = `${sourcePath}.pre-v5-migration.${now()}.bak`;
|
|
182
|
+
try {
|
|
183
|
+
node_fs_1.default.copyFileSync(sourcePath, backupPath);
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
187
|
+
log('warn', `[cron-migration] backup copy failed (migration still applied): ${msg}`);
|
|
188
|
+
errors.push(`backup: ${msg}`);
|
|
189
|
+
}
|
|
190
|
+
// ── Step 7: log ───────────────────────────────────────────────────────
|
|
191
|
+
log('info', `[cron-migration] migrated ${migrated} job${migrated === 1 ? '' : 's'} to SQLite, backup at ${node_path_1.default.basename(backupPath)}`);
|
|
192
|
+
return {
|
|
193
|
+
ran: true,
|
|
194
|
+
migrated,
|
|
195
|
+
skipped: rows.length - migrated,
|
|
196
|
+
backupPath: backupPath,
|
|
197
|
+
errors,
|
|
198
|
+
};
|
|
199
|
+
}
|