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.
Files changed (181) hide show
  1. package/README.md +265 -847
  2. package/dist/api/server.js +32 -5
  3. package/dist/cli/v4/aidenCLI.js +536 -152
  4. package/dist/cli/v4/callbacks.js +170 -0
  5. package/dist/cli/v4/chatSession.js +245 -3
  6. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +94 -0
  7. package/dist/cli/v4/commands/browserDepth.js +45 -0
  8. package/dist/cli/v4/commands/cron.js +264 -0
  9. package/dist/cli/v4/commands/daemon.js +541 -0
  10. package/dist/cli/v4/commands/daemonStatus.js +253 -0
  11. package/dist/cli/v4/commands/fanout.js +42 -59
  12. package/dist/cli/v4/commands/help.js +13 -0
  13. package/dist/cli/v4/commands/index.js +35 -1
  14. package/dist/cli/v4/commands/mcp.js +80 -54
  15. package/dist/cli/v4/commands/plannerGuard.js +53 -0
  16. package/dist/cli/v4/commands/recovery.js +122 -0
  17. package/dist/cli/v4/commands/runs.js +223 -0
  18. package/dist/cli/v4/commands/sandbox.js +48 -0
  19. package/dist/cli/v4/commands/spawnPause.js +93 -0
  20. package/dist/cli/v4/commands/suggestions.js +68 -0
  21. package/dist/cli/v4/commands/tce.js +41 -0
  22. package/dist/cli/v4/commands/trigger.js +378 -0
  23. package/dist/cli/v4/commands/update.js +95 -3
  24. package/dist/cli/v4/daemonAgentBuilder.js +145 -0
  25. package/dist/cli/v4/defaultSoul.js +1 -1
  26. package/dist/cli/v4/display/capabilityCard.js +26 -0
  27. package/dist/cli/v4/display.js +18 -8
  28. package/dist/cli/v4/replyRenderer.js +31 -23
  29. package/dist/cli/v4/updateBootPrompt.js +170 -0
  30. package/dist/core/playwrightBridge.js +129 -0
  31. package/dist/core/v4/aidenAgent.js +527 -5
  32. package/dist/core/v4/browserState.js +436 -0
  33. package/dist/core/v4/checkpoint.js +79 -0
  34. package/dist/core/v4/daemon/bootstrap.js +651 -0
  35. package/dist/core/v4/daemon/cleanShutdown.js +154 -0
  36. package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
  37. package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
  38. package/dist/core/v4/daemon/cron/migration.js +199 -0
  39. package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
  40. package/dist/core/v4/daemon/daemonConfig.js +90 -0
  41. package/dist/core/v4/daemon/db/connection.js +106 -0
  42. package/dist/core/v4/daemon/db/migrations.js +362 -0
  43. package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
  44. package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
  45. package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
  46. package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
  47. package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
  48. package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
  49. package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
  50. package/dist/core/v4/daemon/dispatcher/index.js +53 -0
  51. package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
  52. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
  53. package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
  54. package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
  55. package/dist/core/v4/daemon/drain.js +156 -0
  56. package/dist/core/v4/daemon/eventLoopLag.js +73 -0
  57. package/dist/core/v4/daemon/health.js +159 -0
  58. package/dist/core/v4/daemon/idempotencyStore.js +204 -0
  59. package/dist/core/v4/daemon/index.js +179 -0
  60. package/dist/core/v4/daemon/instanceTracker.js +99 -0
  61. package/dist/core/v4/daemon/resourceRegistry.js +150 -0
  62. package/dist/core/v4/daemon/restartCode.js +32 -0
  63. package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
  64. package/dist/core/v4/daemon/runStore.js +144 -0
  65. package/dist/core/v4/daemon/runtimeLock.js +167 -0
  66. package/dist/core/v4/daemon/signals.js +50 -0
  67. package/dist/core/v4/daemon/supervisor.js +272 -0
  68. package/dist/core/v4/daemon/triggerBus.js +279 -0
  69. package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
  70. package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
  71. package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
  72. package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
  73. package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
  74. package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
  75. package/dist/core/v4/daemon/triggers/email/index.js +332 -0
  76. package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
  77. package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
  78. package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
  79. package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
  80. package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
  81. package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
  82. package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
  83. package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
  84. package/dist/core/v4/daemon/triggers/webhook.js +376 -0
  85. package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
  86. package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
  87. package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
  88. package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
  89. package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
  90. package/dist/core/v4/daemon/types.js +15 -0
  91. package/dist/core/v4/dockerSession.js +461 -0
  92. package/dist/core/v4/dryRun.js +117 -0
  93. package/dist/core/v4/failureClassifier.js +779 -0
  94. package/dist/core/v4/providerFallback.js +35 -2
  95. package/dist/core/v4/recoveryReport.js +449 -0
  96. package/dist/core/v4/runtimeToggles.js +214 -0
  97. package/dist/core/v4/sandboxConfig.js +285 -0
  98. package/dist/core/v4/sandboxFs.js +316 -0
  99. package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
  100. package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
  101. package/dist/core/v4/subagent/childBuilder.js +391 -0
  102. package/dist/core/v4/subagent/fanout.js +75 -51
  103. package/dist/core/v4/subagent/spawnPause.js +191 -0
  104. package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
  105. package/dist/core/v4/suggestionCatalog.js +41 -0
  106. package/dist/core/v4/suggestionEngine.js +210 -0
  107. package/dist/core/v4/toolRegistry.js +37 -3
  108. package/dist/core/v4/turnState.js +587 -0
  109. package/dist/core/v4/update/checkUpdate.js +63 -3
  110. package/dist/core/v4/update/installMethodDetect.js +115 -0
  111. package/dist/core/v4/update/registryClient.js +121 -0
  112. package/dist/core/v4/update/skipState.js +75 -0
  113. package/dist/core/v4/verifier.js +448 -0
  114. package/dist/core/version.js +1 -1
  115. package/dist/moat/plannerGuard.js +29 -0
  116. package/dist/providers/v4/anthropicAdapter.js +31 -3
  117. package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
  118. package/dist/providers/v4/codexResponsesAdapter.js +25 -2
  119. package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
  120. package/dist/tools/v4/browser/_observer.js +224 -0
  121. package/dist/tools/v4/browser/browserBlocker.js +396 -0
  122. package/dist/tools/v4/browser/browserClick.js +18 -1
  123. package/dist/tools/v4/browser/browserClose.js +18 -1
  124. package/dist/tools/v4/browser/browserExtract.js +5 -1
  125. package/dist/tools/v4/browser/browserFill.js +17 -1
  126. package/dist/tools/v4/browser/browserGetUrl.js +5 -1
  127. package/dist/tools/v4/browser/browserNavigate.js +16 -1
  128. package/dist/tools/v4/browser/browserScreenshot.js +5 -1
  129. package/dist/tools/v4/browser/browserScroll.js +18 -1
  130. package/dist/tools/v4/browser/browserType.js +17 -1
  131. package/dist/tools/v4/browser/captchaCheck.js +5 -1
  132. package/dist/tools/v4/executeCode.js +1 -0
  133. package/dist/tools/v4/files/fileCopy.js +56 -2
  134. package/dist/tools/v4/files/fileDelete.js +38 -1
  135. package/dist/tools/v4/files/fileList.js +12 -1
  136. package/dist/tools/v4/files/fileMove.js +59 -2
  137. package/dist/tools/v4/files/filePatch.js +43 -1
  138. package/dist/tools/v4/files/fileRead.js +12 -1
  139. package/dist/tools/v4/files/fileWrite.js +41 -1
  140. package/dist/tools/v4/index.js +88 -61
  141. package/dist/tools/v4/memory/memoryAdd.js +14 -0
  142. package/dist/tools/v4/memory/memoryRemove.js +14 -0
  143. package/dist/tools/v4/memory/memoryReplace.js +15 -0
  144. package/dist/tools/v4/memory/sessionSummary.js +12 -0
  145. package/dist/tools/v4/process/processKill.js +19 -0
  146. package/dist/tools/v4/process/processList.js +1 -0
  147. package/dist/tools/v4/process/processLogRead.js +1 -0
  148. package/dist/tools/v4/process/processSpawn.js +13 -0
  149. package/dist/tools/v4/process/processWait.js +1 -0
  150. package/dist/tools/v4/sessions/recallSession.js +1 -0
  151. package/dist/tools/v4/sessions/sessionList.js +1 -0
  152. package/dist/tools/v4/sessions/sessionSearch.js +1 -0
  153. package/dist/tools/v4/skills/lookupToolSchema.js +7 -0
  154. package/dist/tools/v4/skills/skillManage.js +13 -0
  155. package/dist/tools/v4/skills/skillView.js +1 -0
  156. package/dist/tools/v4/skills/skillsList.js +1 -0
  157. package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
  158. package/dist/tools/v4/subagent/subagentFanout.js +54 -1
  159. package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
  160. package/dist/tools/v4/system/appClose.js +13 -0
  161. package/dist/tools/v4/system/appInput.js +13 -0
  162. package/dist/tools/v4/system/appLaunch.js +13 -0
  163. package/dist/tools/v4/system/clipboardRead.js +1 -0
  164. package/dist/tools/v4/system/clipboardWrite.js +14 -0
  165. package/dist/tools/v4/system/mediaKey.js +12 -0
  166. package/dist/tools/v4/system/mediaSessions.js +1 -0
  167. package/dist/tools/v4/system/mediaTransport.js +13 -0
  168. package/dist/tools/v4/system/naturalEvents.js +1 -0
  169. package/dist/tools/v4/system/nowPlaying.js +1 -0
  170. package/dist/tools/v4/system/osProcessList.js +1 -0
  171. package/dist/tools/v4/system/screenshot.js +1 -0
  172. package/dist/tools/v4/system/systemInfo.js +1 -0
  173. package/dist/tools/v4/system/volumeSet.js +17 -0
  174. package/dist/tools/v4/terminal/shellExec.js +81 -9
  175. package/dist/tools/v4/web/deepResearch.js +1 -0
  176. package/dist/tools/v4/web/openUrl.js +1 -0
  177. package/dist/tools/v4/web/webFetch.js +1 -0
  178. package/dist/tools/v4/web/webPage.js +1 -0
  179. package/dist/tools/v4/web/webSearch.js +1 -0
  180. package/dist/tools/v4/web/youtubeSearch.js +1 -0
  181. 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
+ }