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,211 @@
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/triggers/email/imapConnection.ts — v4.5 Phase 4a.
10
+ *
11
+ * Thin wrapper around imap-simple. Adds:
12
+ * - exponential backoff reconnect (1s → 60s capped at 60s)
13
+ * - IMAP ID command on connect (defends against servers that
14
+ * disconnect unidentified clients — see audit §8)
15
+ * - UIDVALIDITY tracking for cross-restart UID correctness
16
+ * - typed Promise interface (imap-simple's surface is mostly
17
+ * callback-shaped underneath)
18
+ *
19
+ * Lifecycle:
20
+ * const ic = createImapConnection(spec.imap, log);
21
+ * await ic.connect();
22
+ * await ic.openMailbox(spec.mailbox);
23
+ * const uids = await ic.searchAll(); // seed seenUids
24
+ * const unseen = await ic.searchUnseen(); // poll
25
+ * const msg = await ic.fetchMessage(uid);
26
+ * await ic.markSeen(uid);
27
+ * await ic.disconnect();
28
+ */
29
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
30
+ if (k2 === undefined) k2 = k;
31
+ var desc = Object.getOwnPropertyDescriptor(m, k);
32
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
33
+ desc = { enumerable: true, get: function() { return m[k]; } };
34
+ }
35
+ Object.defineProperty(o, k2, desc);
36
+ }) : (function(o, m, k, k2) {
37
+ if (k2 === undefined) k2 = k;
38
+ o[k2] = m[k];
39
+ }));
40
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
41
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
42
+ }) : function(o, v) {
43
+ o["default"] = v;
44
+ });
45
+ var __importStar = (this && this.__importStar) || (function () {
46
+ var ownKeys = function(o) {
47
+ ownKeys = Object.getOwnPropertyNames || function (o) {
48
+ var ar = [];
49
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
50
+ return ar;
51
+ };
52
+ return ownKeys(o);
53
+ };
54
+ return function (mod) {
55
+ if (mod && mod.__esModule) return mod;
56
+ var result = {};
57
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
58
+ __setModuleDefault(result, mod);
59
+ return result;
60
+ };
61
+ })();
62
+ Object.defineProperty(exports, "__esModule", { value: true });
63
+ exports.BACKOFF_CONSTANTS = void 0;
64
+ exports.createImapConnection = createImapConnection;
65
+ exports.nextBackoffMs = nextBackoffMs;
66
+ const imaps = __importStar(require("imap-simple"));
67
+ const version_1 = require("../../../../version");
68
+ const BACKOFF_INITIAL_MS = 1000;
69
+ const BACKOFF_MAX_MS = 60000;
70
+ const BACKOFF_MULTIPLIER = 2;
71
+ const noopLog = (_l, _m) => undefined;
72
+ function createImapConnection(opts) {
73
+ const cfg = opts.config;
74
+ const log = opts.log ?? noopLog;
75
+ let conn = null;
76
+ let backoffMs = BACKOFF_INITIAL_MS;
77
+ let currentMailbox = null;
78
+ let currentUidValidity = 0;
79
+ const buildConfig = () => ({
80
+ imap: {
81
+ host: cfg.host,
82
+ port: cfg.port,
83
+ user: cfg.user,
84
+ password: cfg.password,
85
+ tls: cfg.tls,
86
+ authTimeout: cfg.authTimeoutMs,
87
+ // Reasonable production defaults — imap-simple passes these to node-imap.
88
+ tlsOptions: { rejectUnauthorized: true },
89
+ },
90
+ });
91
+ const sendIdCommand = (c) => new Promise((resolve) => {
92
+ // imap-simple exposes the raw node-imap Connection via .imap.
93
+ // Some servers (NetEase 163) disconnect without an ID exchange:
94
+ // "BYE Unsafe Login. Please contact kefu@188.com for help".
95
+ // Apply unconditionally — other servers ignore it.
96
+ try {
97
+ const c2 = c;
98
+ if (c2.imap?.id) {
99
+ c2.imap.id({ name: 'Aiden', version: version_1.VERSION, vendor: 'Taracod' }, (_err) => resolve());
100
+ }
101
+ else {
102
+ resolve();
103
+ }
104
+ }
105
+ catch {
106
+ // Older imap-simple versions don't expose .id — treat as no-op.
107
+ resolve();
108
+ }
109
+ });
110
+ return {
111
+ async connect() {
112
+ try {
113
+ conn = await imaps.connect(buildConfig());
114
+ await sendIdCommand(conn);
115
+ backoffMs = BACKOFF_INITIAL_MS; // reset on success
116
+ log('info', `[email] imap connected (${cfg.host}:${cfg.port} ${cfg.user})`);
117
+ }
118
+ catch (e) {
119
+ const msg = e instanceof Error ? e.message : String(e);
120
+ log('error', `[email] imap connect failed: ${msg}`);
121
+ throw e;
122
+ }
123
+ },
124
+ async disconnect() {
125
+ if (!conn)
126
+ return;
127
+ try {
128
+ conn.end();
129
+ }
130
+ catch { /* best-effort */ }
131
+ conn = null;
132
+ currentMailbox = null;
133
+ currentUidValidity = 0;
134
+ },
135
+ isConnected() {
136
+ return conn !== null;
137
+ },
138
+ async openMailbox(mailbox) {
139
+ if (!conn)
140
+ throw new Error('[email] not connected');
141
+ // imap-simple openBox typed surface is awkward — accept Mailbox
142
+ // object back.
143
+ const box = await conn.openBox(mailbox);
144
+ currentMailbox = mailbox;
145
+ currentUidValidity = box.uidvalidity ?? 0;
146
+ return { uidValidity: currentUidValidity };
147
+ },
148
+ async searchAll() {
149
+ if (!conn)
150
+ throw new Error('[email] not connected');
151
+ const results = await conn.search(['ALL'], { bodies: ['HEADER.FIELDS (MESSAGE-ID)'], markSeen: false });
152
+ return results.map((r) => r.attributes.uid);
153
+ },
154
+ async searchUnseen() {
155
+ if (!conn)
156
+ throw new Error('[email] not connected');
157
+ const results = await conn.search(['UNSEEN'], { bodies: ['HEADER.FIELDS (MESSAGE-ID)'], markSeen: false });
158
+ return results.map((r) => r.attributes.uid);
159
+ },
160
+ async fetchMessage(uid) {
161
+ if (!conn)
162
+ throw new Error('[email] not connected');
163
+ try {
164
+ const fetched = await conn.search([['UID', String(uid)]], {
165
+ // Fetch full RFC822 source — mailparser handles MIME.
166
+ bodies: [''],
167
+ markSeen: false,
168
+ });
169
+ if (fetched.length === 0)
170
+ return null;
171
+ const m = fetched[0];
172
+ const part = m.parts.find((p) => p.which === '');
173
+ if (!part)
174
+ return null;
175
+ const raw = Buffer.isBuffer(part.body) ? part.body : Buffer.from(part.body, 'utf-8');
176
+ return {
177
+ uid: m.attributes.uid,
178
+ raw,
179
+ flags: m.attributes.flags ?? [],
180
+ internalDate: m.attributes.date ?? new Date(),
181
+ };
182
+ }
183
+ catch (e) {
184
+ log('warn', `[email] fetch uid ${uid} failed: ${e instanceof Error ? e.message : String(e)}`);
185
+ return null;
186
+ }
187
+ },
188
+ async markSeen(uid) {
189
+ if (!conn)
190
+ throw new Error('[email] not connected');
191
+ try {
192
+ await conn.addFlags(uid, '\\Seen');
193
+ }
194
+ catch (e) {
195
+ log('warn', `[email] markSeen uid ${uid} failed: ${e instanceof Error ? e.message : String(e)}`);
196
+ }
197
+ },
198
+ };
199
+ }
200
+ /**
201
+ * Compute the next exponential-backoff delay given the previous one.
202
+ * Pure — used by the orchestrator's reconnect loop.
203
+ */
204
+ function nextBackoffMs(prev) {
205
+ return Math.min(BACKOFF_MAX_MS, Math.max(BACKOFF_INITIAL_MS, prev * BACKOFF_MULTIPLIER));
206
+ }
207
+ exports.BACKOFF_CONSTANTS = Object.freeze({
208
+ initialMs: BACKOFF_INITIAL_MS,
209
+ maxMs: BACKOFF_MAX_MS,
210
+ multiplier: BACKOFF_MULTIPLIER,
211
+ });
@@ -0,0 +1,332 @@
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/triggers/email/index.ts — v4.5 Phase 4a.
10
+ *
11
+ * Email IMAP trigger orchestrator. Wires together the supporting
12
+ * modules:
13
+ * - imapConnection.ts — IMAP wrapper + reconnect
14
+ * - automatedSender.ts — noreply/bounce filter
15
+ * - allowlist.ts — per-trigger sender allowlist
16
+ * - bodyExtractor.ts — mailparser + text/attachment extraction
17
+ * - emailSeenStore.ts — SQLite forensic table
18
+ * - seenUids.ts — in-memory bounded dedup set
19
+ *
20
+ * Public API: `createEmailTrigger(opts) → EmailTriggerHandle`.
21
+ *
22
+ * Lifecycle per trigger (one trigger = one IMAP connection):
23
+ * 1. Connect, ID command, open mailbox, capture UIDVALIDITY.
24
+ * 2. Seed seenUids from UID SEARCH ALL — all pre-existing messages
25
+ * are SKIPPED on first run; reconcile-equivalent of the file
26
+ * watcher's skip_existing default.
27
+ * 3. Poll loop (spec.pollIntervalMs):
28
+ * a. UID SEARCH UNSEEN
29
+ * b. For each new UID not in seenUids:
30
+ * - fetchMessage(uid) → raw bytes
31
+ * - mailparser → headers + body + attachments
32
+ * - isAutomatedSender? → skip (status='skipped_automated')
33
+ * - allowlist.isAllowed? if not → skip (status='skipped_unauth')
34
+ * - allowedSubjectPatterns? if mismatched → skip
35
+ * (status='skipped_subject')
36
+ * - extractEmailBody(raw, policy)
37
+ * - INSIDE A DB TRANSACTION:
38
+ * triggerBus.insert(...) → eventId
39
+ * emailSeenStore.record(... status='processed', eventId)
40
+ * - Only AFTER the tx commit: markSeen(uid) on the server.
41
+ * Order matters — crash mid-write loses our record but
42
+ * the server flag is still unset, so next poll re-fetches.
43
+ * c. seenUids.add(uid) in every branch (skip + processed)
44
+ * 4. On error: log, close connection, backoff, reconnect.
45
+ * 5. On shutdown: resourceRegistry.close() → disconnect.
46
+ */
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.createEmailTrigger = createEmailTrigger;
49
+ const mailparser_1 = require("mailparser");
50
+ const imapConnection_1 = require("./imapConnection");
51
+ const emailSeenStore_1 = require("./emailSeenStore");
52
+ const seenUids_1 = require("./seenUids");
53
+ const allowlist_1 = require("./allowlist");
54
+ const automatedSender_1 = require("./automatedSender");
55
+ const bodyExtractor_1 = require("./bodyExtractor");
56
+ const noopLog = (_l, _m) => undefined;
57
+ const DEGRADED_FAILURE_THRESHOLD = 5;
58
+ function createEmailTrigger(opts) {
59
+ const log = opts.log ?? noopLog;
60
+ const seenUids = (0, seenUids_1.createSeenUids)();
61
+ const allowlist = (0, allowlist_1.compileSenderAllowlist)(opts.spec.allowedSenders);
62
+ const subjectRegexes = (opts.spec.allowedSubjectPatterns ?? [])
63
+ .map((s) => new RegExp(s));
64
+ const seenStore = opts.emailSeenStore ?? (0, emailSeenStore_1.createEmailSeenStore)({ db: opts.db });
65
+ const stats = {
66
+ connected: false,
67
+ totalPolls: 0,
68
+ totalMessages: 0,
69
+ skippedAutomated: 0,
70
+ skippedUnauth: 0,
71
+ skippedSubject: 0,
72
+ processed: 0,
73
+ consecutiveFailures: 0,
74
+ lastPollAt: null,
75
+ lastError: null,
76
+ degraded: false,
77
+ };
78
+ const connection = (opts.connectionFactory
79
+ ? opts.connectionFactory(opts.spec.imap, log)
80
+ : (0, imapConnection_1.createImapConnection)({ config: opts.spec.imap, log }));
81
+ let paused = false;
82
+ let stopped = false;
83
+ let pollTimer = null;
84
+ let backoffMs = imapConnection_1.BACKOFF_CONSTANTS.initialMs;
85
+ let currentUidValidity = 0;
86
+ const runConnect = async () => {
87
+ try {
88
+ await connection.connect();
89
+ const box = await connection.openMailbox(opts.spec.mailbox);
90
+ currentUidValidity = box.uidValidity;
91
+ stats.connected = true;
92
+ stats.consecutiveFailures = 0;
93
+ stats.degraded = false;
94
+ backoffMs = imapConnection_1.BACKOFF_CONSTANTS.initialMs;
95
+ // Seed seenUids from UID SEARCH ALL — all pre-existing UIDs
96
+ // are SKIPPED, not processed. (Matches the file-watcher
97
+ // skip_existing default.)
98
+ try {
99
+ const all = await connection.searchAll();
100
+ seenUids.seed(all);
101
+ log('info', `[email] ${opts.watcherId} seeded ${all.length} pre-existing UIDs (UIDVALIDITY=${currentUidValidity})`);
102
+ }
103
+ catch (e) {
104
+ log('warn', `[email] ${opts.watcherId} seed failed: ${e instanceof Error ? e.message : String(e)}`);
105
+ }
106
+ return true;
107
+ }
108
+ catch (e) {
109
+ stats.connected = false;
110
+ stats.consecutiveFailures += 1;
111
+ stats.lastError = e instanceof Error ? e.message : String(e);
112
+ if (stats.consecutiveFailures >= DEGRADED_FAILURE_THRESHOLD) {
113
+ stats.degraded = true;
114
+ }
115
+ return false;
116
+ }
117
+ };
118
+ const processOne = async (uid) => {
119
+ stats.totalMessages += 1;
120
+ const fetched = await connection.fetchMessage(uid);
121
+ if (!fetched) {
122
+ // Message vanished (deleted between search + fetch). Mark it
123
+ // seen so we don't loop.
124
+ seenUids.add(uid);
125
+ return;
126
+ }
127
+ // Parse just enough headers + body to make the filter decision.
128
+ // We pass the raw bytes onward to extractEmailBody (which calls
129
+ // simpleParser again — accepted tradeoff: mailparser is fast
130
+ // enough for typical email sizes and the surface stays clean).
131
+ const parsed = await (0, mailparser_1.simpleParser)(fetched.raw).catch(() => null);
132
+ if (!parsed) {
133
+ stats.lastError = `parse failure for uid ${uid}`;
134
+ seenStore.record({
135
+ routeId: opts.watcherId,
136
+ mailbox: opts.spec.mailbox,
137
+ uidValidity: currentUidValidity,
138
+ uid,
139
+ messageId: null,
140
+ fromAddress: null,
141
+ subject: null,
142
+ receivedAt: fetched.internalDate.getTime(),
143
+ triggerEventId: null,
144
+ status: 'failed',
145
+ });
146
+ seenUids.add(uid);
147
+ return;
148
+ }
149
+ const fromAddress = parsed.from?.value?.[0]?.address ?? '';
150
+ const messageId = parsed.messageId ?? null;
151
+ const subject = parsed.subject ?? null;
152
+ const receivedAt = (parsed.date ?? fetched.internalDate).getTime();
153
+ // Normalize headers to a plain string→string map for our filter.
154
+ const headers = {};
155
+ for (const [k, v] of parsed.headers.entries()) {
156
+ headers[k.toLowerCase()] = Array.isArray(v) ? String(v[0] ?? '') : String(v);
157
+ }
158
+ const skipReason = (0, automatedSender_1.isAutomatedSender)(fromAddress, headers) ? 'skipped_automated' :
159
+ !allowlist.isAllowed(fromAddress) ? 'skipped_unauth' :
160
+ (subjectRegexes.length > 0 && !subjectRegexes.some((r) => subject != null && r.test(subject)))
161
+ ? 'skipped_subject' :
162
+ null;
163
+ if (skipReason) {
164
+ if (skipReason === 'skipped_automated')
165
+ stats.skippedAutomated += 1;
166
+ if (skipReason === 'skipped_unauth')
167
+ stats.skippedUnauth += 1;
168
+ if (skipReason === 'skipped_subject')
169
+ stats.skippedSubject += 1;
170
+ seenStore.record({
171
+ routeId: opts.watcherId,
172
+ mailbox: opts.spec.mailbox,
173
+ uidValidity: currentUidValidity,
174
+ uid,
175
+ messageId,
176
+ fromAddress,
177
+ subject,
178
+ receivedAt,
179
+ triggerEventId: null,
180
+ status: skipReason,
181
+ });
182
+ seenUids.add(uid);
183
+ // For unauth/automated: DO NOT mark \Seen on server — leave
184
+ // the message as unread so the human user sees it in their
185
+ // mail client. (Per audit §6.)
186
+ return;
187
+ }
188
+ // Extract body + attachments per policy.
189
+ const body = await (0, bodyExtractor_1.extractEmailBody)({
190
+ raw: fetched.raw,
191
+ maxBodyBytes: opts.spec.maxBodyBytes,
192
+ attachmentPolicy: opts.spec.attachmentPolicy,
193
+ });
194
+ // Single tx: trigger_events insert + email_seen record. This
195
+ // sequence is the lesson from the audit — persist locally BEFORE
196
+ // marking \Seen on the server. Crash between tx and markSeen
197
+ // means next poll re-fetches; better than losing record.
198
+ const tx = opts.db.transaction(() => {
199
+ const insertResult = opts.triggerBus.insert({
200
+ source: 'email',
201
+ sourceKey: opts.watcherId,
202
+ idempotencyKey: `${currentUidValidity}::${uid}::${messageId ?? ''}`,
203
+ payload: {
204
+ from: fromAddress,
205
+ subject,
206
+ body: body.text,
207
+ truncated: body.truncated,
208
+ textKind: body.textKind,
209
+ quotedReplyStripped: body.quotedReplyStripped,
210
+ messageId,
211
+ inReplyTo: parsed.inReplyTo ?? null,
212
+ references: parsed.references ?? null,
213
+ receivedAt,
214
+ mailbox: opts.spec.mailbox,
215
+ attachments: body.attachments,
216
+ headers,
217
+ deliveryMode: opts.spec.deliverOnly ? 'deliver_only' : 'agent',
218
+ },
219
+ });
220
+ seenStore.record({
221
+ routeId: opts.watcherId,
222
+ mailbox: opts.spec.mailbox,
223
+ uidValidity: currentUidValidity,
224
+ uid,
225
+ messageId,
226
+ fromAddress,
227
+ subject,
228
+ receivedAt,
229
+ triggerEventId: insertResult.id,
230
+ status: 'processed',
231
+ });
232
+ return { eventId: insertResult.id };
233
+ });
234
+ tx();
235
+ // AFTER the tx commits: mark \Seen on the IMAP server.
236
+ await connection.markSeen(uid);
237
+ seenUids.add(uid);
238
+ stats.processed += 1;
239
+ };
240
+ const pollOnce = async () => {
241
+ if (paused || stopped)
242
+ return;
243
+ if (!connection.isConnected()) {
244
+ const ok = await runConnect();
245
+ if (!ok) {
246
+ log('warn', `[email] ${opts.watcherId} reconnect backoff ${backoffMs}ms`);
247
+ scheduleNext(backoffMs);
248
+ backoffMs = (0, imapConnection_1.nextBackoffMs)(backoffMs);
249
+ return;
250
+ }
251
+ }
252
+ stats.lastPollAt = Date.now();
253
+ stats.totalPolls += 1;
254
+ try {
255
+ const uids = await connection.searchUnseen();
256
+ for (const uid of uids) {
257
+ if (paused || stopped)
258
+ break;
259
+ if (seenUids.has(uid))
260
+ continue;
261
+ try {
262
+ await processOne(uid);
263
+ }
264
+ catch (e) {
265
+ stats.lastError = e instanceof Error ? e.message : String(e);
266
+ log('error', `[email] ${opts.watcherId} processOne uid ${uid} failed: ${stats.lastError}`);
267
+ }
268
+ }
269
+ stats.consecutiveFailures = 0;
270
+ backoffMs = imapConnection_1.BACKOFF_CONSTANTS.initialMs;
271
+ scheduleNext(opts.spec.pollIntervalMs);
272
+ }
273
+ catch (e) {
274
+ stats.connected = false;
275
+ stats.consecutiveFailures += 1;
276
+ stats.lastError = e instanceof Error ? e.message : String(e);
277
+ if (stats.consecutiveFailures >= DEGRADED_FAILURE_THRESHOLD) {
278
+ stats.degraded = true;
279
+ }
280
+ try {
281
+ await connection.disconnect();
282
+ }
283
+ catch { /* noop */ }
284
+ log('warn', `[email] ${opts.watcherId} poll error: ${stats.lastError}; backoff ${backoffMs}ms`);
285
+ scheduleNext(backoffMs);
286
+ backoffMs = (0, imapConnection_1.nextBackoffMs)(backoffMs);
287
+ }
288
+ };
289
+ const scheduleNext = (ms) => {
290
+ if (pollTimer) {
291
+ clearTimeout(pollTimer);
292
+ pollTimer = null;
293
+ }
294
+ if (stopped)
295
+ return;
296
+ pollTimer = setTimeout(() => { void pollOnce(); }, ms);
297
+ if (typeof pollTimer.unref === 'function')
298
+ pollTimer.unref();
299
+ };
300
+ // Kick off the first poll cycle async — return the handle synchronously.
301
+ void pollOnce();
302
+ const close = async () => {
303
+ stopped = true;
304
+ if (pollTimer) {
305
+ clearTimeout(pollTimer);
306
+ pollTimer = null;
307
+ }
308
+ try {
309
+ await connection.disconnect();
310
+ }
311
+ catch { /* best-effort */ }
312
+ stats.connected = false;
313
+ };
314
+ const resourceId = opts.registry.register({
315
+ kind: 'imap_connection',
316
+ owner: opts.watcherId,
317
+ metadata: { host: opts.spec.imap.host, user: opts.spec.imap.user, mailbox: opts.spec.mailbox },
318
+ close,
319
+ });
320
+ return {
321
+ watcherId: opts.watcherId,
322
+ resourceId,
323
+ pause() { paused = true; },
324
+ resume() {
325
+ paused = false;
326
+ // Re-arm immediately on resume.
327
+ scheduleNext(0);
328
+ },
329
+ close,
330
+ stats() { return { ...stats }; },
331
+ };
332
+ }
@@ -0,0 +1,60 @@
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/triggers/email/seenUids.ts — v4.5 Phase 4a.
10
+ *
11
+ * In-memory bounded UID dedup set. Hot-path "have we seen this
12
+ * UID in the current daemon session?" check.
13
+ *
14
+ * Bounded at MAX (default 2000) — when we cross MAX, drop the
15
+ * lower half. IMAP UIDs are monotonic per UIDVALIDITY, so old
16
+ * UIDs are safe to drop: the IMAP server's `\Seen` flag is the
17
+ * cross-restart authority, and our `email_seen` SQLite table
18
+ * is the cross-restart forensic trail. The in-memory set is
19
+ * just a fast filter to avoid round-tripping every UID to
20
+ * `email_seen` on every poll.
21
+ *
22
+ * Per-trigger instance — each email trigger has its own SeenUids.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.DEFAULT_MAX_SEEN_UIDS = void 0;
26
+ exports.createSeenUids = createSeenUids;
27
+ exports.DEFAULT_MAX_SEEN_UIDS = 2000;
28
+ function createSeenUids(maxSize = exports.DEFAULT_MAX_SEEN_UIDS) {
29
+ const set = new Set();
30
+ const trim = () => {
31
+ if (set.size <= maxSize)
32
+ return { dropped: 0 };
33
+ // Drop the LOWER half — IMAP UIDs monotonic per UIDVALIDITY.
34
+ // Sort ascending; keep the upper half (newer UIDs).
35
+ const sorted = [...set].sort((a, b) => a - b);
36
+ const keep = sorted.slice(Math.floor(sorted.length / 2));
37
+ const dropped = sorted.length - keep.length;
38
+ set.clear();
39
+ for (const u of keep)
40
+ set.add(u);
41
+ return { dropped };
42
+ };
43
+ return {
44
+ has(uid) { return set.has(uid); },
45
+ add(uid) {
46
+ set.add(uid);
47
+ if (set.size > maxSize)
48
+ trim();
49
+ },
50
+ seed(uids) {
51
+ for (const u of uids)
52
+ set.add(u);
53
+ if (set.size > maxSize)
54
+ trim();
55
+ },
56
+ size() { return set.size; },
57
+ trim,
58
+ reset() { set.clear(); },
59
+ };
60
+ }
@@ -0,0 +1,93 @@
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/triggers/fileObservationsStore.ts — v4.5 Phase 2.
10
+ *
11
+ * Writer for the `file_observations` table (schema v2). Each row
12
+ * tracks the most-recent state we've observed for one (watcher_id,
13
+ * abs_path) pair: size, mtime, file_key, optional content hash,
14
+ * timestamps + the linked trigger_event id.
15
+ *
16
+ * Reconciliation reads these on boot to decide which paths to skip
17
+ * vs which to emit catch-up events for.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.createFileObservationsStore = createFileObservationsStore;
21
+ function rowToTs(r) {
22
+ return {
23
+ id: r.id,
24
+ watcherId: r.watcher_id,
25
+ absPath: r.abs_path,
26
+ fileKey: r.file_key,
27
+ size: r.size,
28
+ mtimeMs: r.mtime_ms,
29
+ contentHash: r.content_hash,
30
+ lastEventType: r.last_event_type,
31
+ lastSeenAt: r.last_seen_at,
32
+ lastProcessedAt: r.last_processed_at,
33
+ lastEventId: r.last_event_id,
34
+ lastStatus: r.last_status,
35
+ coalescedCount: r.coalesced_count,
36
+ };
37
+ }
38
+ function createFileObservationsStore(opts) {
39
+ const db = opts.db;
40
+ return {
41
+ upsert(input) {
42
+ const now = Date.now();
43
+ const tx = db.transaction(() => {
44
+ const existing = db
45
+ .prepare('SELECT id, coalesced_count FROM file_observations WHERE watcher_id = ? AND abs_path = ?')
46
+ .get(input.watcherId, input.absPath);
47
+ if (existing) {
48
+ db.prepare(`UPDATE file_observations
49
+ SET file_key = ?,
50
+ size = ?,
51
+ mtime_ms = ?,
52
+ content_hash = COALESCE(?, content_hash),
53
+ last_event_type = ?,
54
+ last_seen_at = ?,
55
+ coalesced_count = coalesced_count + ?
56
+ WHERE id = ?`).run(input.fileKey, input.size, input.mtimeMs, input.contentHash, input.eventType, now, input.coalescedDelta ?? 0, existing.id);
57
+ return existing.id;
58
+ }
59
+ const r = db
60
+ .prepare(`INSERT INTO file_observations
61
+ (watcher_id, abs_path, file_key, size, mtime_ms,
62
+ content_hash, last_event_type, last_seen_at,
63
+ last_status, coalesced_count)
64
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)`)
65
+ .run(input.watcherId, input.absPath, input.fileKey, input.size, input.mtimeMs, input.contentHash, input.eventType, now, input.coalescedDelta ?? 0);
66
+ return Number(r.lastInsertRowid);
67
+ });
68
+ return tx();
69
+ },
70
+ markProcessed({ observationId, eventId, status }) {
71
+ db.prepare(`UPDATE file_observations
72
+ SET last_processed_at = ?,
73
+ last_event_id = COALESCE(?, last_event_id),
74
+ last_status = ?
75
+ WHERE id = ?`).run(Date.now(), eventId, status, observationId);
76
+ },
77
+ listForWatcher(watcherId) {
78
+ const rows = db
79
+ .prepare('SELECT * FROM file_observations WHERE watcher_id = ? ORDER BY abs_path')
80
+ .all(watcherId);
81
+ return rows.map(rowToTs);
82
+ },
83
+ get(watcherId, absPath) {
84
+ const r = db
85
+ .prepare('SELECT * FROM file_observations WHERE watcher_id = ? AND abs_path = ?')
86
+ .get(watcherId, absPath);
87
+ return r ? rowToTs(r) : null;
88
+ },
89
+ deleteForWatcher(watcherId) {
90
+ db.prepare('DELETE FROM file_observations WHERE watcher_id = ?').run(watcherId);
91
+ },
92
+ };
93
+ }