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.
- package/README.md +250 -847
- package/dist/api/server.js +32 -5
- package/dist/cli/v4/aidenCLI.js +351 -53
- package/dist/cli/v4/callbacks.js +170 -0
- package/dist/cli/v4/chatSession.js +138 -3
- package/dist/cli/v4/commands/_runtimeToggleHelpers.js +92 -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/help.js +7 -0
- package/dist/cli/v4/commands/index.js +20 -1
- package/dist/cli/v4/commands/runs.js +203 -0
- package/dist/cli/v4/commands/sandbox.js +48 -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 +142 -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 +308 -4
- package/dist/core/v4/browserState.js +436 -0
- package/dist/core/v4/checkpoint.js +79 -0
- package/dist/core/v4/daemon/bootstrap.js +604 -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 +296 -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 +114 -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/recoveryReport.js +449 -0
- package/dist/core/v4/runtimeToggles.js +187 -0
- package/dist/core/v4/sandboxConfig.js +285 -0
- package/dist/core/v4/sandboxFs.js +316 -0
- package/dist/core/v4/suggestionCatalog.js +41 -0
- package/dist/core/v4/suggestionEngine.js +210 -0
- package/dist/core/v4/toolRegistry.js +18 -0
- 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/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 +71 -58
- 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 +2 -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/subagentFanout.js +1 -0
- 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 +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 });
|