aiden-runtime 4.1.5 → 4.5.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 (163) hide show
  1. package/README.md +250 -847
  2. package/dist/api/server.js +32 -5
  3. package/dist/cli/v4/aidenCLI.js +351 -53
  4. package/dist/cli/v4/callbacks.js +170 -0
  5. package/dist/cli/v4/chatSession.js +138 -3
  6. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +92 -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/help.js +7 -0
  12. package/dist/cli/v4/commands/index.js +20 -1
  13. package/dist/cli/v4/commands/runs.js +203 -0
  14. package/dist/cli/v4/commands/sandbox.js +48 -0
  15. package/dist/cli/v4/commands/suggestions.js +68 -0
  16. package/dist/cli/v4/commands/tce.js +41 -0
  17. package/dist/cli/v4/commands/trigger.js +378 -0
  18. package/dist/cli/v4/commands/update.js +95 -3
  19. package/dist/cli/v4/daemonAgentBuilder.js +142 -0
  20. package/dist/cli/v4/defaultSoul.js +1 -1
  21. package/dist/cli/v4/display/capabilityCard.js +26 -0
  22. package/dist/cli/v4/display.js +18 -8
  23. package/dist/cli/v4/replyRenderer.js +31 -23
  24. package/dist/cli/v4/updateBootPrompt.js +170 -0
  25. package/dist/core/playwrightBridge.js +129 -0
  26. package/dist/core/v4/aidenAgent.js +308 -4
  27. package/dist/core/v4/browserState.js +436 -0
  28. package/dist/core/v4/checkpoint.js +79 -0
  29. package/dist/core/v4/daemon/bootstrap.js +604 -0
  30. package/dist/core/v4/daemon/cleanShutdown.js +154 -0
  31. package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
  32. package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
  33. package/dist/core/v4/daemon/cron/migration.js +199 -0
  34. package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
  35. package/dist/core/v4/daemon/daemonConfig.js +90 -0
  36. package/dist/core/v4/daemon/db/connection.js +106 -0
  37. package/dist/core/v4/daemon/db/migrations.js +296 -0
  38. package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
  39. package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
  40. package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
  41. package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
  42. package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
  43. package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
  44. package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
  45. package/dist/core/v4/daemon/dispatcher/index.js +53 -0
  46. package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
  47. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
  48. package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
  49. package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
  50. package/dist/core/v4/daemon/drain.js +156 -0
  51. package/dist/core/v4/daemon/eventLoopLag.js +73 -0
  52. package/dist/core/v4/daemon/health.js +159 -0
  53. package/dist/core/v4/daemon/idempotencyStore.js +204 -0
  54. package/dist/core/v4/daemon/index.js +179 -0
  55. package/dist/core/v4/daemon/instanceTracker.js +99 -0
  56. package/dist/core/v4/daemon/resourceRegistry.js +150 -0
  57. package/dist/core/v4/daemon/restartCode.js +32 -0
  58. package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
  59. package/dist/core/v4/daemon/runStore.js +114 -0
  60. package/dist/core/v4/daemon/runtimeLock.js +167 -0
  61. package/dist/core/v4/daemon/signals.js +50 -0
  62. package/dist/core/v4/daemon/supervisor.js +272 -0
  63. package/dist/core/v4/daemon/triggerBus.js +279 -0
  64. package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
  65. package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
  66. package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
  67. package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
  68. package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
  69. package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
  70. package/dist/core/v4/daemon/triggers/email/index.js +332 -0
  71. package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
  72. package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
  73. package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
  74. package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
  75. package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
  76. package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
  77. package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
  78. package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
  79. package/dist/core/v4/daemon/triggers/webhook.js +376 -0
  80. package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
  81. package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
  82. package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
  83. package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
  84. package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
  85. package/dist/core/v4/daemon/types.js +15 -0
  86. package/dist/core/v4/dockerSession.js +461 -0
  87. package/dist/core/v4/dryRun.js +117 -0
  88. package/dist/core/v4/failureClassifier.js +779 -0
  89. package/dist/core/v4/recoveryReport.js +449 -0
  90. package/dist/core/v4/runtimeToggles.js +187 -0
  91. package/dist/core/v4/sandboxConfig.js +285 -0
  92. package/dist/core/v4/sandboxFs.js +316 -0
  93. package/dist/core/v4/suggestionCatalog.js +41 -0
  94. package/dist/core/v4/suggestionEngine.js +210 -0
  95. package/dist/core/v4/toolRegistry.js +18 -0
  96. package/dist/core/v4/turnState.js +587 -0
  97. package/dist/core/v4/update/checkUpdate.js +63 -3
  98. package/dist/core/v4/update/installMethodDetect.js +115 -0
  99. package/dist/core/v4/update/registryClient.js +121 -0
  100. package/dist/core/v4/update/skipState.js +75 -0
  101. package/dist/core/v4/verifier.js +448 -0
  102. package/dist/core/version.js +1 -1
  103. package/dist/tools/v4/browser/_observer.js +224 -0
  104. package/dist/tools/v4/browser/browserBlocker.js +396 -0
  105. package/dist/tools/v4/browser/browserClick.js +18 -1
  106. package/dist/tools/v4/browser/browserClose.js +18 -1
  107. package/dist/tools/v4/browser/browserExtract.js +5 -1
  108. package/dist/tools/v4/browser/browserFill.js +17 -1
  109. package/dist/tools/v4/browser/browserGetUrl.js +5 -1
  110. package/dist/tools/v4/browser/browserNavigate.js +16 -1
  111. package/dist/tools/v4/browser/browserScreenshot.js +5 -1
  112. package/dist/tools/v4/browser/browserScroll.js +18 -1
  113. package/dist/tools/v4/browser/browserType.js +17 -1
  114. package/dist/tools/v4/browser/captchaCheck.js +5 -1
  115. package/dist/tools/v4/executeCode.js +1 -0
  116. package/dist/tools/v4/files/fileCopy.js +56 -2
  117. package/dist/tools/v4/files/fileDelete.js +38 -1
  118. package/dist/tools/v4/files/fileList.js +12 -1
  119. package/dist/tools/v4/files/fileMove.js +59 -2
  120. package/dist/tools/v4/files/filePatch.js +43 -1
  121. package/dist/tools/v4/files/fileRead.js +12 -1
  122. package/dist/tools/v4/files/fileWrite.js +41 -1
  123. package/dist/tools/v4/index.js +71 -58
  124. package/dist/tools/v4/memory/memoryAdd.js +14 -0
  125. package/dist/tools/v4/memory/memoryRemove.js +14 -0
  126. package/dist/tools/v4/memory/memoryReplace.js +15 -0
  127. package/dist/tools/v4/memory/sessionSummary.js +12 -0
  128. package/dist/tools/v4/process/processKill.js +19 -0
  129. package/dist/tools/v4/process/processList.js +1 -0
  130. package/dist/tools/v4/process/processLogRead.js +1 -0
  131. package/dist/tools/v4/process/processSpawn.js +13 -0
  132. package/dist/tools/v4/process/processWait.js +1 -0
  133. package/dist/tools/v4/sessions/recallSession.js +1 -0
  134. package/dist/tools/v4/sessions/sessionList.js +1 -0
  135. package/dist/tools/v4/sessions/sessionSearch.js +1 -0
  136. package/dist/tools/v4/skills/lookupToolSchema.js +2 -0
  137. package/dist/tools/v4/skills/skillManage.js +13 -0
  138. package/dist/tools/v4/skills/skillView.js +1 -0
  139. package/dist/tools/v4/skills/skillsList.js +1 -0
  140. package/dist/tools/v4/subagent/subagentFanout.js +1 -0
  141. package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
  142. package/dist/tools/v4/system/appClose.js +13 -0
  143. package/dist/tools/v4/system/appInput.js +13 -0
  144. package/dist/tools/v4/system/appLaunch.js +13 -0
  145. package/dist/tools/v4/system/clipboardRead.js +1 -0
  146. package/dist/tools/v4/system/clipboardWrite.js +14 -0
  147. package/dist/tools/v4/system/mediaKey.js +12 -0
  148. package/dist/tools/v4/system/mediaSessions.js +1 -0
  149. package/dist/tools/v4/system/mediaTransport.js +13 -0
  150. package/dist/tools/v4/system/naturalEvents.js +1 -0
  151. package/dist/tools/v4/system/nowPlaying.js +1 -0
  152. package/dist/tools/v4/system/osProcessList.js +1 -0
  153. package/dist/tools/v4/system/screenshot.js +1 -0
  154. package/dist/tools/v4/system/systemInfo.js +1 -0
  155. package/dist/tools/v4/system/volumeSet.js +17 -0
  156. package/dist/tools/v4/terminal/shellExec.js +81 -9
  157. package/dist/tools/v4/web/deepResearch.js +1 -0
  158. package/dist/tools/v4/web/openUrl.js +1 -0
  159. package/dist/tools/v4/web/webFetch.js +1 -0
  160. package/dist/tools/v4/web/webPage.js +1 -0
  161. package/dist/tools/v4/web/webSearch.js +1 -0
  162. package/dist/tools/v4/web/youtubeSearch.js +1 -0
  163. package/package.json +7 -1
@@ -0,0 +1,115 @@
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/misfirePolicy.ts — v4.5 Phase 5b.
10
+ *
11
+ * What to do when a scheduled workflow's `next_fire_at` is in the
12
+ * past at tick time. Four policies, configurable per workflow.
13
+ *
14
+ * Default `skip_stale` per Q-P5-6(a) — matches the prior-systems
15
+ * lesson that "missing one run beats firing dozens of stale runs"
16
+ * after a long suspend or laptop sleep.
17
+ *
18
+ * skip_stale — skip the run when scheduled > graceMs ago.
19
+ * Fire ONLY when scheduledFor is within the
20
+ * grace window (or in the future).
21
+ * run_once_if_late — fire ONCE if missed, regardless of how
22
+ * stale. Useful for one-shots that MUST run.
23
+ * catch_up_with_limit — fire N times (capped at catchUpLimit) to
24
+ * walk forward through the missed schedule.
25
+ * For interval jobs that want to backfill.
26
+ * manual_review — DON'T fire. Log + leave next_fire_at in
27
+ * the past so the operator notices and
28
+ * decides via the upcoming CLI surface.
29
+ *
30
+ * Pure module — no I/O, no Date.now(). Both `now` and `scheduledFor`
31
+ * are passed by the caller. Returns a deterministic decision so
32
+ * unit tests can assert exact firing behaviour.
33
+ */
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.applyMisfirePolicy = applyMisfirePolicy;
36
+ exports.isMisfirePolicy = isMisfirePolicy;
37
+ const DEFAULT_GRACE_MS = 60000;
38
+ const DEFAULT_CATCH_UP_LIMIT = 10;
39
+ /**
40
+ * Pure policy resolver. The caller owns the side-effects (insert
41
+ * trigger_event, advance next_fire_at, log).
42
+ *
43
+ * Edge cases handled:
44
+ * - scheduledFor in the FUTURE → always `fire:false` ("not yet").
45
+ * - scheduledFor within graceMs of now → always fires once.
46
+ * - catch_up_with_limit with missing periodMs/limit → safe
47
+ * fallback to single fire.
48
+ */
49
+ function applyMisfirePolicy(input) {
50
+ const graceMs = input.graceMs ?? DEFAULT_GRACE_MS;
51
+ const lateBy = input.now - input.scheduledFor;
52
+ // Future-scheduled — never fire this tick (the heartbeat will
53
+ // come back around when the time arrives).
54
+ if (lateBy < 0) {
55
+ return { fire: false, fireCount: 0, reason: 'not_yet_due' };
56
+ }
57
+ // Within grace window — every policy honours the standard fire.
58
+ if (lateBy <= graceMs) {
59
+ return { fire: true, fireCount: 1, reason: 'on_time' };
60
+ }
61
+ // Past the grace window → policy decides.
62
+ switch (input.policy) {
63
+ case 'skip_stale':
64
+ return {
65
+ fire: false,
66
+ fireCount: 0,
67
+ reason: `skip_stale: late by ${Math.round(lateBy / 1000)}s`,
68
+ };
69
+ case 'run_once_if_late':
70
+ return {
71
+ fire: true,
72
+ fireCount: 1,
73
+ reason: `run_once_if_late: late by ${Math.round(lateBy / 1000)}s`,
74
+ };
75
+ case 'catch_up_with_limit': {
76
+ const limit = input.catchUpLimit ?? DEFAULT_CATCH_UP_LIMIT;
77
+ const period = input.periodMs;
78
+ if (!period || period <= 0) {
79
+ // No period info — safe fallback: single fire.
80
+ return { fire: true, fireCount: 1, reason: 'catch_up: no period info, firing once' };
81
+ }
82
+ // Number of full periods between scheduledFor and now, plus one
83
+ // for the original missed slot. Capped at `limit`.
84
+ const missed = Math.floor(lateBy / period) + 1;
85
+ const fireCount = Math.min(missed, limit);
86
+ return {
87
+ fire: true,
88
+ fireCount,
89
+ reason: fireCount === missed
90
+ ? `catch_up: ${fireCount} missed slot${fireCount === 1 ? '' : 's'}`
91
+ : `catch_up: ${missed} missed, capped at ${limit}`,
92
+ };
93
+ }
94
+ case 'manual_review':
95
+ return {
96
+ fire: false,
97
+ fireCount: 0,
98
+ reason: `manual_review: late by ${Math.round(lateBy / 1000)}s, awaiting operator action`,
99
+ };
100
+ default:
101
+ // Defensive — unknown policy. Be conservative: don't fire.
102
+ return {
103
+ fire: false,
104
+ fireCount: 0,
105
+ reason: `unknown_policy: ${input.policy}`,
106
+ };
107
+ }
108
+ }
109
+ /** Type guard for runtime spec validation. */
110
+ function isMisfirePolicy(s) {
111
+ return s === 'skip_stale'
112
+ || s === 'run_once_if_late'
113
+ || s === 'catch_up_with_limit'
114
+ || s === 'manual_review';
115
+ }
@@ -0,0 +1,90 @@
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/daemonConfig.ts — v4.5 Phase 1: daemon configuration.
10
+ *
11
+ * Env-driven config (same pattern as v4.4 sandboxConfig). Strict
12
+ * `=== '1'` opt-in for AIDEN_DAEMON in Phases 1-5; Phase 6 will
13
+ * flip to `!== '0'` (default-on).
14
+ *
15
+ * Sub-flags:
16
+ * AIDEN_DAEMON_PORT — daemon API port (default reuses AIDEN_PORT)
17
+ * AIDEN_DAEMON_AUTO_RESTART — '0' disables the internal supervisor
18
+ * (use when running under systemd/launchd)
19
+ * AIDEN_DAEMON_DRAIN_TIMEOUT_MS — drain timeout for in-flight runs (default 30000)
20
+ * AIDEN_DAEMON_RESTART_FAILURE_THRESHOLD — per-session stuck-loop threshold (default 3)
21
+ */
22
+ var __importDefault = (this && this.__importDefault) || function (mod) {
23
+ return (mod && mod.__esModule) ? mod : { "default": mod };
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.readDaemonConfig = readDaemonConfig;
27
+ exports.getDaemonConfig = getDaemonConfig;
28
+ exports._resetDaemonConfigForTests = _resetDaemonConfigForTests;
29
+ exports.daemonDir = daemonDir;
30
+ exports.daemonDbPath = daemonDbPath;
31
+ exports.daemonRuntimeLockPath = daemonRuntimeLockPath;
32
+ exports.daemonCleanShutdownMarkerPath = daemonCleanShutdownMarkerPath;
33
+ exports.getHostname = getHostname;
34
+ const node_path_1 = __importDefault(require("node:path"));
35
+ const node_os_1 = __importDefault(require("node:os"));
36
+ const DEFAULT_PORT = 4200;
37
+ const DEFAULT_DRAIN_TIMEOUT_MS = 30000;
38
+ const DEFAULT_RESTART_FAILURE_LIMIT = 3;
39
+ function parseIntSafe(raw, fallback) {
40
+ if (raw === undefined || raw === null || raw === '')
41
+ return fallback;
42
+ const n = Number.parseInt(raw, 10);
43
+ return Number.isFinite(n) && n > 0 ? n : fallback;
44
+ }
45
+ function readDaemonConfig(env = process.env) {
46
+ // Phase 1-5 strict opt-in. Phase 6 will flip to `!== '0'`.
47
+ const enabled = env.AIDEN_DAEMON === '1';
48
+ const port = parseIntSafe(env.AIDEN_DAEMON_PORT ?? env.AIDEN_PORT, DEFAULT_PORT);
49
+ const autoRestart = env.AIDEN_DAEMON_AUTO_RESTART !== '0'; // default true
50
+ const drainTimeoutMs = parseIntSafe(env.AIDEN_DAEMON_DRAIN_TIMEOUT_MS, DEFAULT_DRAIN_TIMEOUT_MS);
51
+ const restartFailureThreshold = parseIntSafe(env.AIDEN_DAEMON_RESTART_FAILURE_THRESHOLD, DEFAULT_RESTART_FAILURE_LIMIT);
52
+ return { enabled, port, autoRestart, drainTimeoutMs, restartFailureThreshold };
53
+ }
54
+ let _singleton = null;
55
+ function getDaemonConfig() {
56
+ if (!_singleton)
57
+ _singleton = readDaemonConfig();
58
+ return _singleton;
59
+ }
60
+ function _resetDaemonConfigForTests() {
61
+ _singleton = null;
62
+ }
63
+ /**
64
+ * Resolve the daemon's on-disk root directory:
65
+ * <aidenHome>/daemon/
66
+ * Caller passes the resolved Aiden root (from `core/v4/paths.ts`);
67
+ * we don't import paths.ts here to keep the dependency graph clean.
68
+ */
69
+ function daemonDir(aidenRoot) {
70
+ return node_path_1.default.join(aidenRoot, 'daemon');
71
+ }
72
+ function daemonDbPath(aidenRoot) {
73
+ return node_path_1.default.join(daemonDir(aidenRoot), 'daemon.db');
74
+ }
75
+ function daemonRuntimeLockPath(aidenRoot) {
76
+ return node_path_1.default.join(daemonDir(aidenRoot), 'runtime.lock');
77
+ }
78
+ function daemonCleanShutdownMarkerPath(aidenRoot) {
79
+ return node_path_1.default.join(daemonDir(aidenRoot), '.clean_shutdown');
80
+ }
81
+ /** Hostname for instance records — short, never empty. */
82
+ function getHostname() {
83
+ try {
84
+ const h = node_os_1.default.hostname();
85
+ return h && h.length > 0 ? h : 'unknown';
86
+ }
87
+ catch {
88
+ return 'unknown';
89
+ }
90
+ }
@@ -0,0 +1,106 @@
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/db/connection.ts — v4.5 Phase 1: SQLite handle.
10
+ *
11
+ * Single better-sqlite3 connection per process keyed by db path.
12
+ * Synchronous — better-sqlite3 is sync by design. Wraps the
13
+ * connection with the daemon's standard pragma set:
14
+ *
15
+ * journal_mode = WAL — durable + concurrent reads with one writer
16
+ * synchronous = NORMAL — fsync at WAL checkpoint, not per commit
17
+ * (durable across process crash, may lose
18
+ * last commits on OS crash — acceptable)
19
+ * foreign_keys = ON — enforce FK constraints
20
+ * busy_timeout = 5000 — auto-retry on SQLITE_BUSY for 5s
21
+ *
22
+ * On test isolation: use `:memory:` to get a per-test database.
23
+ *
24
+ * The connection is registered with the resource registry (Phase 1)
25
+ * so shutdown drain closes it via the standard reap path.
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.openDaemonDb = openDaemonDb;
32
+ exports.closeDaemonDb = closeDaemonDb;
33
+ exports._closeAllDaemonDbsForTests = _closeAllDaemonDbsForTests;
34
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
35
+ const node_fs_1 = __importDefault(require("node:fs"));
36
+ const node_path_1 = __importDefault(require("node:path"));
37
+ const migrations_1 = require("./migrations");
38
+ const _open = new Map();
39
+ /**
40
+ * Open (or return cached) database at `dbPath`. Creates parent dirs
41
+ * if missing. Runs migrations to latest version. Idempotent for a
42
+ * given path.
43
+ *
44
+ * Pass ':memory:' for tests.
45
+ */
46
+ function openDaemonDb(dbPath) {
47
+ const cached = _open.get(dbPath);
48
+ if (cached && cached.open)
49
+ return cached;
50
+ if (dbPath !== ':memory:') {
51
+ try {
52
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(dbPath), { recursive: true });
53
+ }
54
+ catch { /* tolerate — open will surface a clearer error */ }
55
+ }
56
+ const db = new better_sqlite3_1.default(dbPath);
57
+ db.pragma('journal_mode = WAL');
58
+ db.pragma('synchronous = NORMAL');
59
+ db.pragma('foreign_keys = ON');
60
+ db.pragma('busy_timeout = 5000');
61
+ (0, migrations_1.runMigrations)(db);
62
+ _open.set(dbPath, db);
63
+ // v4.5 Phase 3 — daemon.db stores webhook secrets (raw, required
64
+ // for HMAC computation). Lock the file mode to user-only on POSIX
65
+ // so a co-tenant on the same machine can't read the secrets out.
66
+ // On Windows the file already lives in user-private
67
+ // %LOCALAPPDATA%; chmod is a no-op there (Node ignores Unix bits).
68
+ // Idempotent — safe to call on every boot.
69
+ if (dbPath !== ':memory:' && process.platform !== 'win32') {
70
+ try {
71
+ node_fs_1.default.chmodSync(dbPath, 0o600);
72
+ }
73
+ catch { /* best-effort */ }
74
+ // WAL/SHM siblings receive identical protection.
75
+ for (const ext of ['-wal', '-shm']) {
76
+ try {
77
+ node_fs_1.default.chmodSync(dbPath + ext, 0o600);
78
+ }
79
+ catch { /* may not exist yet */ }
80
+ }
81
+ }
82
+ return db;
83
+ }
84
+ /** Close the cached connection at `dbPath`. Idempotent. */
85
+ function closeDaemonDb(dbPath) {
86
+ const db = _open.get(dbPath);
87
+ if (!db)
88
+ return;
89
+ try {
90
+ if (db.open)
91
+ db.close();
92
+ }
93
+ catch { /* best-effort */ }
94
+ _open.delete(dbPath);
95
+ }
96
+ /** Test-only — close every open handle. */
97
+ function _closeAllDaemonDbsForTests() {
98
+ for (const [p, db] of _open) {
99
+ try {
100
+ if (db.open)
101
+ db.close();
102
+ }
103
+ catch { /* noop */ }
104
+ _open.delete(p);
105
+ }
106
+ }
@@ -0,0 +1,296 @@
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/db/migrations.ts — v4.5 Phase 1: schema migration runner.
10
+ *
11
+ * Version-tracked. Idempotent. Each migration is a string of DDL
12
+ * statements (CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS)
13
+ * wrapped in a transaction. The runner reads the current version
14
+ * from `schema_version` and applies every migration with a higher
15
+ * version number.
16
+ *
17
+ * Phase 1 ships v1 (`v1.sql`). Future phases append migrations:
18
+ *
19
+ * const MIGRATIONS: ReadonlyArray<Migration> = [
20
+ * { version: 1, name: 'phase 1 — daemon foundation', sql: V1_SQL },
21
+ * { version: 2, name: 'phase 2 — file watcher trigger', sql: V2_SQL },
22
+ * ...
23
+ * ];
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.LATEST_SCHEMA_VERSION = void 0;
27
+ exports.runMigrations = runMigrations;
28
+ // Embedded v1 schema. Source of truth lives at
29
+ // `core/v4/daemon/db/schema/v1.sql` — kept in sync via the
30
+ // `tests/v4/daemon/db/migrations.test.ts` snapshot check.
31
+ const V1_SQL = `
32
+ CREATE TABLE IF NOT EXISTS schema_version (
33
+ id INTEGER PRIMARY KEY CHECK (id = 1),
34
+ version INTEGER NOT NULL,
35
+ applied_at INTEGER NOT NULL
36
+ );
37
+
38
+ CREATE TABLE IF NOT EXISTS daemon_instances (
39
+ instance_id TEXT PRIMARY KEY,
40
+ pid INTEGER NOT NULL,
41
+ hostname TEXT NOT NULL,
42
+ started_at INTEGER NOT NULL,
43
+ last_heartbeat INTEGER NOT NULL,
44
+ shutdown_at INTEGER,
45
+ shutdown_reason TEXT,
46
+ exit_code INTEGER,
47
+ version TEXT NOT NULL
48
+ );
49
+ CREATE INDEX IF NOT EXISTS idx_daemon_instances_alive
50
+ ON daemon_instances(shutdown_at) WHERE shutdown_at IS NULL;
51
+
52
+ CREATE TABLE IF NOT EXISTS runs (
53
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
54
+ trigger_event_id INTEGER,
55
+ session_id TEXT NOT NULL,
56
+ instance_id TEXT NOT NULL,
57
+ status TEXT NOT NULL,
58
+ finish_reason TEXT,
59
+ started_at INTEGER NOT NULL,
60
+ completed_at INTEGER,
61
+ resume_pending INTEGER NOT NULL DEFAULT 0,
62
+ resume_reason TEXT,
63
+ FOREIGN KEY (instance_id) REFERENCES daemon_instances(instance_id) ON DELETE CASCADE
64
+ );
65
+ CREATE INDEX IF NOT EXISTS idx_runs_session ON runs(session_id, started_at);
66
+ CREATE INDEX IF NOT EXISTS idx_runs_active
67
+ ON runs(status) WHERE status IN ('queued','running');
68
+
69
+ CREATE TABLE IF NOT EXISTS trigger_events (
70
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
71
+ source TEXT NOT NULL,
72
+ source_key TEXT NOT NULL,
73
+ idempotency_key TEXT,
74
+ payload_json TEXT NOT NULL,
75
+ status TEXT NOT NULL,
76
+ attempts INTEGER NOT NULL DEFAULT 0,
77
+ claim_owner TEXT,
78
+ claim_expires_at INTEGER,
79
+ last_error TEXT,
80
+ created_at INTEGER NOT NULL,
81
+ updated_at INTEGER NOT NULL,
82
+ completed_at INTEGER,
83
+ run_id INTEGER,
84
+ FOREIGN KEY (run_id) REFERENCES runs(id) ON DELETE SET NULL
85
+ );
86
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_trigger_events_idem
87
+ ON trigger_events(source, idempotency_key) WHERE idempotency_key IS NOT NULL;
88
+ CREATE INDEX IF NOT EXISTS idx_trigger_events_pending
89
+ ON trigger_events(status, created_at) WHERE status IN ('pending','claimed');
90
+ CREATE INDEX IF NOT EXISTS idx_trigger_events_claim_expiry
91
+ ON trigger_events(claim_expires_at) WHERE status = 'claimed';
92
+
93
+ CREATE TABLE IF NOT EXISTS run_events (
94
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
95
+ run_id INTEGER NOT NULL,
96
+ ts INTEGER NOT NULL,
97
+ kind TEXT NOT NULL,
98
+ payload TEXT NOT NULL,
99
+ FOREIGN KEY (run_id) REFERENCES runs(id) ON DELETE CASCADE
100
+ );
101
+ CREATE INDEX IF NOT EXISTS idx_run_events_run ON run_events(run_id, ts);
102
+
103
+ CREATE TABLE IF NOT EXISTS idempotency_keys (
104
+ scope TEXT NOT NULL,
105
+ key TEXT NOT NULL,
106
+ fingerprint TEXT,
107
+ response_json TEXT NOT NULL,
108
+ status_code INTEGER NOT NULL DEFAULT 200,
109
+ created_at INTEGER NOT NULL,
110
+ expires_at INTEGER NOT NULL,
111
+ PRIMARY KEY (scope, key)
112
+ );
113
+ CREATE INDEX IF NOT EXISTS idx_idem_expiry ON idempotency_keys(expires_at);
114
+
115
+ CREATE TABLE IF NOT EXISTS crash_reports (
116
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
117
+ instance_id TEXT NOT NULL,
118
+ detected_at INTEGER NOT NULL,
119
+ prev_started_at INTEGER,
120
+ prev_last_heartbeat INTEGER,
121
+ prev_pid INTEGER,
122
+ affected_sessions TEXT NOT NULL,
123
+ ps_snapshot TEXT,
124
+ details TEXT NOT NULL,
125
+ FOREIGN KEY (instance_id) REFERENCES daemon_instances(instance_id) ON DELETE CASCADE
126
+ );
127
+
128
+ CREATE TABLE IF NOT EXISTS restart_failure_counts (
129
+ session_id TEXT PRIMARY KEY,
130
+ count INTEGER NOT NULL,
131
+ last_failure INTEGER NOT NULL,
132
+ auto_suspended INTEGER NOT NULL DEFAULT 0
133
+ );
134
+
135
+ CREATE TABLE IF NOT EXISTS triggers (
136
+ id TEXT PRIMARY KEY,
137
+ source TEXT NOT NULL,
138
+ name TEXT NOT NULL,
139
+ spec_json TEXT NOT NULL,
140
+ enabled INTEGER NOT NULL DEFAULT 1,
141
+ fire_rate_limit INTEGER,
142
+ prompt_template TEXT,
143
+ deliver_only INTEGER NOT NULL DEFAULT 0,
144
+ created_at INTEGER NOT NULL,
145
+ updated_at INTEGER NOT NULL
146
+ );
147
+ CREATE INDEX IF NOT EXISTS idx_triggers_source_enabled ON triggers(source, enabled);
148
+ `;
149
+ // v4.5 Phase 2 — file_observations table. Source of truth lives at
150
+ // `core/v4/daemon/db/schema/v2.sql`; kept in sync via the migrations
151
+ // test snapshot check.
152
+ const V2_SQL = `
153
+ CREATE TABLE IF NOT EXISTS file_observations (
154
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
155
+ watcher_id TEXT NOT NULL,
156
+ abs_path TEXT NOT NULL,
157
+ file_key TEXT NOT NULL DEFAULT '',
158
+ size INTEGER,
159
+ mtime_ms INTEGER NOT NULL,
160
+ content_hash TEXT,
161
+ last_event_type TEXT,
162
+ last_seen_at INTEGER NOT NULL,
163
+ last_processed_at INTEGER,
164
+ last_event_id INTEGER,
165
+ last_status TEXT NOT NULL DEFAULT 'pending',
166
+ coalesced_count INTEGER NOT NULL DEFAULT 0,
167
+ FOREIGN KEY (watcher_id) REFERENCES triggers(id) ON DELETE CASCADE,
168
+ FOREIGN KEY (last_event_id) REFERENCES trigger_events(id) ON DELETE SET NULL
169
+ );
170
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_file_obs_watcher_path
171
+ ON file_observations(watcher_id, abs_path);
172
+ CREATE INDEX IF NOT EXISTS idx_file_obs_last_seen
173
+ ON file_observations(last_seen_at);
174
+ CREATE INDEX IF NOT EXISTS idx_file_obs_pending
175
+ ON file_observations(watcher_id, last_status) WHERE last_status = 'pending';
176
+ `;
177
+ // v4.5 Phase 3 — webhook_deliveries log.
178
+ const V3_SQL = `
179
+ CREATE TABLE IF NOT EXISTS webhook_deliveries (
180
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
181
+ route_id TEXT NOT NULL,
182
+ delivery_id TEXT,
183
+ signature_verified INTEGER NOT NULL,
184
+ status_code INTEGER NOT NULL,
185
+ response_body TEXT,
186
+ client_ip TEXT,
187
+ headers_json TEXT,
188
+ body_hash TEXT NOT NULL,
189
+ received_at INTEGER NOT NULL,
190
+ processed_at INTEGER,
191
+ trigger_event_id INTEGER,
192
+ FOREIGN KEY (route_id) REFERENCES triggers(id) ON DELETE CASCADE,
193
+ FOREIGN KEY (trigger_event_id) REFERENCES trigger_events(id) ON DELETE SET NULL
194
+ );
195
+ CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_route_time
196
+ ON webhook_deliveries(route_id, received_at);
197
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_webhook_deliveries_delivery
198
+ ON webhook_deliveries(route_id, delivery_id) WHERE delivery_id IS NOT NULL;
199
+ CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_received
200
+ ON webhook_deliveries(received_at);
201
+ `;
202
+ // v4.5 Phase 4a — email_seen forensic table.
203
+ const V4_SQL = `
204
+ CREATE TABLE IF NOT EXISTS email_seen (
205
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
206
+ route_id TEXT NOT NULL,
207
+ mailbox TEXT NOT NULL,
208
+ uid_validity INTEGER NOT NULL,
209
+ uid INTEGER NOT NULL,
210
+ message_id TEXT,
211
+ from_address TEXT,
212
+ subject TEXT,
213
+ received_at INTEGER NOT NULL,
214
+ processed_at INTEGER,
215
+ trigger_event_id INTEGER,
216
+ status TEXT NOT NULL,
217
+ FOREIGN KEY (route_id) REFERENCES triggers(id) ON DELETE CASCADE,
218
+ FOREIGN KEY (trigger_event_id) REFERENCES trigger_events(id) ON DELETE SET NULL
219
+ );
220
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_email_seen_route_uid
221
+ ON email_seen(route_id, uid_validity, uid);
222
+ CREATE INDEX IF NOT EXISTS idx_email_seen_received
223
+ ON email_seen(received_at);
224
+ CREATE INDEX IF NOT EXISTS idx_email_seen_message_id
225
+ ON email_seen(message_id) WHERE message_id IS NOT NULL;
226
+ `;
227
+ // v4.5 Phase 5b — scheduled_workflows table (cron migration from JSON
228
+ // to SQLite). One-shot data migration from `cron_jobs.json` runs from
229
+ // the daemon bootstrap after this schema applies, not from inside the
230
+ // DDL transaction itself — keeps schema-only migrations idempotent.
231
+ const V5_SQL = `
232
+ CREATE TABLE IF NOT EXISTS scheduled_workflows (
233
+ id TEXT PRIMARY KEY,
234
+ name TEXT NOT NULL,
235
+ schedule_expression TEXT NOT NULL,
236
+ timezone TEXT NOT NULL DEFAULT 'UTC',
237
+ enabled INTEGER NOT NULL DEFAULT 1,
238
+ payload_json TEXT NOT NULL,
239
+ prompt_template TEXT,
240
+ deliver_only INTEGER NOT NULL DEFAULT 0,
241
+ misfire_policy TEXT NOT NULL DEFAULT 'skip_stale',
242
+ fire_rate_limit INTEGER,
243
+ catch_up_limit INTEGER,
244
+ grace_ms INTEGER,
245
+ last_fired_at INTEGER,
246
+ next_fire_at INTEGER,
247
+ created_at INTEGER NOT NULL,
248
+ updated_at INTEGER NOT NULL
249
+ );
250
+ CREATE INDEX IF NOT EXISTS idx_scheduled_workflows_next_fire
251
+ ON scheduled_workflows(next_fire_at) WHERE enabled = 1;
252
+ CREATE INDEX IF NOT EXISTS idx_scheduled_workflows_enabled
253
+ ON scheduled_workflows(enabled);
254
+ `;
255
+ const MIGRATIONS = [
256
+ { version: 1, name: 'phase 1 — daemon foundation', sql: V1_SQL },
257
+ { version: 2, name: 'phase 2 — file watcher observations', sql: V2_SQL },
258
+ { version: 3, name: 'phase 3 — webhook deliveries log', sql: V3_SQL },
259
+ { version: 4, name: 'phase 4a — email seen forensic table', sql: V4_SQL },
260
+ { version: 5, name: 'phase 5b — scheduled workflows', sql: V5_SQL },
261
+ ];
262
+ exports.LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
263
+ function getCurrentVersion(db) {
264
+ // The schema_version table may not exist yet on first boot. Detect
265
+ // via sqlite_master so we don't trip the migration runner on a
266
+ // fresh database.
267
+ const row = db
268
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'")
269
+ .get();
270
+ if (!row?.name)
271
+ return 0;
272
+ const verRow = db
273
+ .prepare('SELECT version FROM schema_version WHERE id = 1')
274
+ .get();
275
+ return verRow?.version ?? 0;
276
+ }
277
+ /**
278
+ * Apply every pending migration. Idempotent: re-running a database
279
+ * already at the latest version is a no-op.
280
+ */
281
+ function runMigrations(db) {
282
+ const from = getCurrentVersion(db);
283
+ const pending = MIGRATIONS.filter((m) => m.version > from);
284
+ if (pending.length === 0)
285
+ return { from, to: from };
286
+ const apply = db.transaction((m) => {
287
+ db.exec(m.sql);
288
+ db.prepare('INSERT OR REPLACE INTO schema_version (id, version, applied_at) VALUES (1, ?, ?)').run(m.version, Date.now());
289
+ });
290
+ let to = from;
291
+ for (const m of pending) {
292
+ apply(m);
293
+ to = m.version;
294
+ }
295
+ return { from, to };
296
+ }
@@ -0,0 +1,18 @@
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/db/schema/v1.spec.ts — v4.5 Phase 1: typed shapes
10
+ * matching the columns in `v1.sql`. Used by the daemon modules
11
+ * that read/write SQLite rows so we have one type per table that
12
+ * stays in sync with the DDL.
13
+ *
14
+ * Naming convention: column names from SQL stay snake_case; TS
15
+ * row interfaces use camelCase aliases via the mapping helpers
16
+ * inside each module. These raw interfaces match the wire shape.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });