aiden-runtime 4.0.2 → 4.1.1

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 (113) hide show
  1. package/README.md +19 -11
  2. package/config/hardware.json +2 -2
  3. package/dist/api/server.js +50 -52
  4. package/dist/cli/v4/aidenCLI.js +424 -7
  5. package/dist/cli/v4/aidenPrompt.js +317 -0
  6. package/dist/cli/v4/box.js +105 -39
  7. package/dist/cli/v4/callbacks.js +39 -6
  8. package/dist/cli/v4/chatSession.js +256 -55
  9. package/dist/cli/v4/citationFooter.js +97 -0
  10. package/dist/cli/v4/commands/channel.js +656 -0
  11. package/dist/cli/v4/commands/clear.js +1 -1
  12. package/dist/cli/v4/commands/compress.js +1 -1
  13. package/dist/cli/v4/commands/cron.js +44 -16
  14. package/dist/cli/v4/commands/fanout.js +236 -0
  15. package/dist/cli/v4/commands/help.js +15 -4
  16. package/dist/cli/v4/commands/history.js +84 -0
  17. package/dist/cli/v4/commands/index.js +16 -1
  18. package/dist/cli/v4/commands/mcp.js +358 -0
  19. package/dist/cli/v4/commands/show.js +43 -0
  20. package/dist/cli/v4/commands/skills.js +169 -4
  21. package/dist/cli/v4/commands/status.js +84 -0
  22. package/dist/cli/v4/commands/subagent.js +78 -0
  23. package/dist/cli/v4/commands/verbose.js +1 -1
  24. package/dist/cli/v4/commands/voice.js +218 -0
  25. package/dist/cli/v4/cronCli.js +103 -0
  26. package/dist/cli/v4/display.js +297 -13
  27. package/dist/cli/v4/doctor.js +102 -1
  28. package/dist/cli/v4/doctorLiveness.js +329 -0
  29. package/dist/cli/v4/envSources.js +105 -0
  30. package/dist/cli/v4/ghostMatch.js +74 -0
  31. package/dist/cli/v4/historyStore.js +163 -0
  32. package/dist/cli/v4/pasteCompression.js +124 -0
  33. package/dist/cli/v4/pasteIntercept.js +203 -0
  34. package/dist/cli/v4/replyRenderer.js +209 -0
  35. package/dist/cli/v4/resizeGuard.js +92 -0
  36. package/dist/cli/v4/shellInterpolation.js +139 -0
  37. package/dist/cli/v4/skinEngine.js +21 -1
  38. package/dist/cli/v4/streamingPrefix.js +121 -0
  39. package/dist/cli/v4/syntaxHighlight.js +345 -0
  40. package/dist/cli/v4/table.js +216 -0
  41. package/dist/cli/v4/themeDetect.js +81 -0
  42. package/dist/cli/v4/uiBuild.js +74 -0
  43. package/dist/cli/v4/voiceCli.js +113 -0
  44. package/dist/cli/v4/voicePromptApi.js +196 -0
  45. package/dist/core/channels/discord.js +16 -10
  46. package/dist/core/channels/email.js +13 -9
  47. package/dist/core/channels/imessage.js +13 -9
  48. package/dist/core/channels/manager.js +25 -7
  49. package/dist/core/channels/pdf-extract.js +180 -0
  50. package/dist/core/channels/photo-vision.js +157 -0
  51. package/dist/core/channels/signal.js +11 -7
  52. package/dist/core/channels/slack.js +13 -10
  53. package/dist/core/channels/telegram-commands.js +154 -0
  54. package/dist/core/channels/telegram-groups.js +198 -0
  55. package/dist/core/channels/telegram-rate-limit.js +124 -0
  56. package/dist/core/channels/telegram.js +1980 -0
  57. package/dist/core/channels/twilio.js +11 -7
  58. package/dist/core/channels/webhook.js +9 -5
  59. package/dist/core/channels/whatsapp.js +15 -11
  60. package/dist/core/channels/whisper-transcribe.js +163 -0
  61. package/dist/core/cronManager.js +33 -294
  62. package/dist/core/gateway.js +29 -8
  63. package/dist/core/playwrightBridge.js +90 -0
  64. package/dist/core/v4/aidenAgent.js +35 -0
  65. package/dist/core/v4/auxiliaryClient.js +2 -2
  66. package/dist/core/v4/cron/atomicWrite.js +18 -4
  67. package/dist/core/v4/cron/cronExecute.js +300 -0
  68. package/dist/core/v4/cron/cronManager.js +502 -0
  69. package/dist/core/v4/cron/cronState.js +314 -0
  70. package/dist/core/v4/cron/cronTick.js +90 -0
  71. package/dist/core/v4/cron/diagnostics.js +104 -0
  72. package/dist/core/v4/cron/graceWindow.js +79 -0
  73. package/dist/core/v4/logger/factory.js +110 -0
  74. package/dist/core/v4/logger/index.js +22 -0
  75. package/dist/core/v4/logger/logger.js +101 -0
  76. package/dist/core/v4/logger/sinks/fileSink.js +110 -0
  77. package/dist/core/v4/logger/sinks/multiSink.js +43 -0
  78. package/dist/core/v4/logger/sinks/nullSink.js +53 -0
  79. package/dist/core/v4/logger/sinks/stdSink.js +81 -0
  80. package/dist/core/v4/mcp/server/diagnostics.js +40 -0
  81. package/dist/core/v4/mcp/server/skillBridge.js +94 -0
  82. package/dist/core/v4/mcp/server/stdioServer.js +119 -0
  83. package/dist/core/v4/mcp/server/toolBridge.js +168 -0
  84. package/dist/core/v4/platformPaths.js +105 -0
  85. package/dist/core/v4/providerFallback.js +25 -0
  86. package/dist/core/v4/skillLoader.js +21 -5
  87. package/dist/core/v4/skillMining/candidateStore.js +164 -0
  88. package/dist/core/v4/skillMining/extractorPrompt.js +118 -0
  89. package/dist/core/v4/skillMining/proposalBuilder.js +140 -0
  90. package/dist/core/v4/skillMining/skillMiner.js +191 -0
  91. package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
  92. package/dist/core/v4/subagent/budget.js +76 -0
  93. package/dist/core/v4/subagent/diagnostics.js +22 -0
  94. package/dist/core/v4/subagent/fanout.js +216 -0
  95. package/dist/core/v4/subagent/merger.js +148 -0
  96. package/dist/core/v4/subagent/providerRotation.js +54 -0
  97. package/dist/core/v4/voice/audioStream.js +373 -0
  98. package/dist/core/v4/voice/cliVoice.js +393 -0
  99. package/dist/core/v4/voice/diagnostics.js +66 -0
  100. package/dist/core/v4/voice/ttsStream.js +193 -0
  101. package/dist/core/version.js +1 -1
  102. package/dist/core/visionAnalyze.js +291 -90
  103. package/dist/core/voice/audio.js +61 -5
  104. package/dist/core/voice/audioBackend.js +134 -0
  105. package/dist/core/voice/stt.js +61 -6
  106. package/dist/core/voice/tts.js +19 -3
  107. package/dist/moat/dangerousPatterns.js +1 -1
  108. package/dist/providers/v4/codexResponsesAdapter.js +7 -2
  109. package/dist/providers/v4/errors.js +51 -1
  110. package/dist/providers/v4/ollamaPromptToolsAdapter.js +9 -2
  111. package/dist/tools/v4/index.js +32 -1
  112. package/dist/tools/v4/subagent/subagentFanout.js +190 -0
  113. package/package.json +11 -2
@@ -1,300 +1,39 @@
1
1
  "use strict";
2
2
  // ============================================================
3
- // DevOS Autonomous AI Execution System
4
- // Copyright (c) 2026 Shiva Deore. All rights reserved.
3
+ // Copyright (c) 2026 Shiva Deore (Taracod). Licensed under AGPL-3.0.
5
4
  // ============================================================
6
5
  //
7
- // core/cronManager.ts — scheduled task engine.
6
+ // core/cronManager.ts — backward-compat shim.
8
7
  //
9
- // Public API (kept stable): createJob, listJobs, getJob,
10
- // pauseJob, resumeJob, deleteJob, triggerJob, parseSchedule,
11
- // loadJobs.
12
- //
13
- // Capabilities:
14
- // - "every N minutes/hours/days", "hourly", "daily", "30m"
15
- // - 5-field cron expressions ("0 9 * * *", "*/30 * * * *")
16
- // - One-shot ISO timestamps ("2026-02-03T14:00")
17
- // Persistence:
18
- // - ~/.aiden/cron_jobs.json atomic temp-then-rename + fsync,
19
- // guarded by a per-path mutex. cron_jobs.json never observed
20
- // in a half-written state, even if the process is killed
21
- // mid-write.
22
- // Scheduling:
23
- // - lastRun-anchored chained setTimeout (no setInterval drift).
24
- // Each fire schedules the next from `lastRun + intervalMs`,
25
- // so a process restart resumes the cadence from where it
26
- // left off. Stale anchors (lastRun + interval < now) fire
27
- // immediately, then the regular cadence resumes.
28
- // Per-run logs:
29
- // - ~/.aiden/cron-logs/<job-id>.log gets a full STARTED /
30
- // output / DONE block per fire. cron_jobs.json carries a
31
- // 4 KB summary on the job record.
32
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
33
- if (k2 === undefined) k2 = k;
34
- var desc = Object.getOwnPropertyDescriptor(m, k);
35
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
36
- desc = { enumerable: true, get: function() { return m[k]; } };
37
- }
38
- Object.defineProperty(o, k2, desc);
39
- }) : (function(o, m, k, k2) {
40
- if (k2 === undefined) k2 = k;
41
- o[k2] = m[k];
42
- }));
43
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
44
- Object.defineProperty(o, "default", { enumerable: true, value: v });
45
- }) : function(o, v) {
46
- o["default"] = v;
47
- });
48
- var __importStar = (this && this.__importStar) || (function () {
49
- var ownKeys = function(o) {
50
- ownKeys = Object.getOwnPropertyNames || function (o) {
51
- var ar = [];
52
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
53
- return ar;
54
- };
55
- return ownKeys(o);
56
- };
57
- return function (mod) {
58
- if (mod && mod.__esModule) return mod;
59
- var result = {};
60
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
61
- __setModuleDefault(result, mod);
62
- return result;
63
- };
64
- })();
8
+ // The legacy in-process scheduler this file used to host has been
9
+ // replaced by the hardened scheduler at
10
+ // `core/v4/cron/cronManager.ts` (Phase v4.1-cron).
11
+ // Public function names + parseSchedule are preserved so existing
12
+ // importers (notably cli/v4/commands/cron.ts) keep working
13
+ // unchanged. The legacy synchronous `loadJobs()` / `createJob()`
14
+ // signatures were always followed by a `save()` which itself was
15
+ // async-fire-and-forget; the new module is async-end-to-end.
16
+ // Callers that previously did `cron.createJob(...)` and assumed
17
+ // instant disk persistence now MUST `await` the call.
65
18
  Object.defineProperty(exports, "__esModule", { value: true });
66
- exports.parseSchedule = void 0;
67
- exports.awaitPendingSaves = awaitPendingSaves;
68
- exports.loadJobs = loadJobs;
69
- exports.createJob = createJob;
70
- exports.listJobs = listJobs;
71
- exports.getJob = getJob;
72
- exports.pauseJob = pauseJob;
73
- exports.resumeJob = resumeJob;
74
- exports.deleteJob = deleteJob;
75
- exports.triggerJob = triggerJob;
76
- exports.__resetForTests = __resetForTests;
77
- const fs = __importStar(require("fs"));
78
- const path = __importStar(require("path"));
79
- const os = __importStar(require("os"));
80
- const atomicWrite_1 = require("./v4/cron/atomicWrite");
81
- const scheduleParser_1 = require("./v4/cron/scheduleParser");
82
- const outputCapture_1 = require("./v4/cron/outputCapture");
83
- // ── State ─────────────────────────────────────────────────────────────────────
84
- const jobs = new Map();
85
- const timers = new Map();
86
- let jobSeq = 1;
87
- let pendingSave = Promise.resolve();
88
- const DATA_DIR = path.join(os.homedir(), '.aiden');
89
- const DATA_FILE = path.join(DATA_DIR, 'cron_jobs.json');
90
- const LOGS_DIR = path.join(DATA_DIR, 'cron-logs');
91
- // Re-export so callers can keep `import { parseSchedule } from './cronManager'`.
92
- var scheduleParser_2 = require("./v4/cron/scheduleParser");
93
- Object.defineProperty(exports, "parseSchedule", { enumerable: true, get: function () { return scheduleParser_2.parseSchedule; } });
94
- // ── Persistence ───────────────────────────────────────────────────────────────
95
- function save() {
96
- // Fire-and-forget, but chain off the previous save so writes serialise.
97
- pendingSave = pendingSave.catch(() => undefined).then(() => (0, atomicWrite_1.writeJsonAtomic)(DATA_FILE, Array.from(jobs.values())));
98
- }
99
- // Test/shutdown hook — drain any queued writes.
100
- async function awaitPendingSaves() {
101
- await pendingSave;
102
- }
103
- function loadJobs() {
104
- try {
105
- if (!fs.existsSync(DATA_FILE))
106
- return;
107
- const data = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
108
- if (!Array.isArray(data))
109
- return;
110
- for (const raw of data) {
111
- const job = migrateJob(raw);
112
- if (!job)
113
- continue;
114
- jobs.set(job.id, job);
115
- const num = parseInt(job.id, 10);
116
- if (!isNaN(num) && num >= jobSeq)
117
- jobSeq = num + 1;
118
- if (job.enabled)
119
- _scheduleJob(job);
120
- }
121
- }
122
- catch { /* corrupt file → start with empty registry */ }
123
- }
124
- function migrateJob(raw) {
125
- if (!raw || typeof raw !== 'object' || !raw.id)
126
- return null;
127
- // Old shape (pre-Phase-24.1) had no `kind`; everything was an interval.
128
- if (!raw.kind) {
129
- raw.kind = raw.cronExpr ? 'cron'
130
- : raw.oneshotIso ? 'oneshot'
131
- : 'interval';
132
- }
133
- return raw;
134
- }
135
- // ── Scheduling ────────────────────────────────────────────────────────────────
136
- function _scheduleJob(job) {
137
- if (timers.has(job.id))
138
- return; // already armed
139
- if (!job.enabled)
140
- return;
141
- const target = (0, scheduleParser_1.computeNextFire)(job);
142
- if (!target) {
143
- // Nothing to fire (e.g. completed one-shot). Make sure no stale timer.
144
- return;
145
- }
146
- const delay = Math.max(0, target.getTime() - Date.now());
147
- job.nextRun = new Date(Date.now() + delay).toISOString();
148
- jobs.set(job.id, { ...job });
149
- save();
150
- const handle = setTimeout(async () => {
151
- timers.delete(job.id);
152
- try {
153
- await _fireJob(job.id);
154
- }
155
- catch { /* errors already captured into the run log */ }
156
- const fresh = jobs.get(job.id);
157
- if (!fresh)
158
- return;
159
- if (fresh.kind === 'oneshot') {
160
- fresh.enabled = false;
161
- jobs.set(fresh.id, { ...fresh });
162
- save();
163
- return;
164
- }
165
- if (fresh.enabled)
166
- _scheduleJob(fresh);
167
- }, delay);
168
- if (typeof handle.unref === 'function')
169
- handle.unref();
170
- timers.set(job.id, handle);
171
- }
172
- async function _fireJob(id) {
173
- const job = jobs.get(id);
174
- if (!job)
175
- return;
176
- const startedMs = Date.now();
177
- const cap = await (0, outputCapture_1.captureRun)(job.id, job.description || job.id, LOGS_DIR, async () => {
178
- try {
179
- const { executeTool } = await Promise.resolve().then(() => __importStar(require('./toolRegistry')));
180
- const r = await executeTool('shell_exec', { command: job.action }, 0);
181
- const text = typeof r === 'string' ? r : (r?.output ?? JSON.stringify(r));
182
- const failed = r?.success === false;
183
- return { output: String(text ?? ''), failed };
184
- }
185
- catch (e) {
186
- return { output: e?.stack ?? e?.message ?? String(e), failed: true };
187
- }
188
- });
189
- const fresh = jobs.get(id);
190
- if (!fresh)
191
- return;
192
- fresh.lastRun = new Date(startedMs).toISOString();
193
- fresh.lastResult = cap.result;
194
- fresh.lastOutput = cap.output;
195
- fresh.runCount = (fresh.runCount ?? 0) + 1;
196
- jobs.set(fresh.id, { ...fresh });
197
- save();
198
- }
199
- function clearTimer(id) {
200
- const h = timers.get(id);
201
- if (h) {
202
- clearTimeout(h);
203
- timers.delete(id);
204
- }
205
- }
206
- // ── Public API ────────────────────────────────────────────────────────────────
207
- function createJob(description, schedule, action) {
208
- const spec = (0, scheduleParser_1.parseSchedule)(schedule);
209
- const id = String(jobSeq++);
210
- const job = {
211
- id,
212
- description,
213
- schedule: spec.display,
214
- kind: spec.kind,
215
- action,
216
- enabled: true,
217
- createdAt: new Date().toISOString(),
218
- runCount: 0,
219
- ...attachKindFields(spec),
220
- };
221
- const first = (0, scheduleParser_1.computeFirstFire)(job);
222
- if (first)
223
- job.nextRun = first.toISOString();
224
- jobs.set(id, job);
225
- _scheduleJob(job);
226
- save();
227
- return job;
228
- }
229
- function attachKindFields(spec) {
230
- if (spec.kind === 'interval')
231
- return { intervalMs: spec.intervalMs };
232
- if (spec.kind === 'cron')
233
- return { cronExpr: spec.cronExpr };
234
- return { oneshotIso: spec.runAtIso };
235
- }
236
- function listJobs() {
237
- return Array.from(jobs.values());
238
- }
239
- function getJob(id) {
240
- return jobs.get(id);
241
- }
242
- function pauseJob(id) {
243
- const job = jobs.get(id);
244
- if (!job)
245
- return false;
246
- job.enabled = false;
247
- clearTimer(id);
248
- jobs.set(id, { ...job });
249
- save();
250
- return true;
251
- }
252
- function resumeJob(id) {
253
- const job = jobs.get(id);
254
- if (!job)
255
- return false;
256
- job.enabled = true;
257
- jobs.set(id, { ...job });
258
- _scheduleJob(job);
259
- save();
260
- return true;
261
- }
262
- function deleteJob(id) {
263
- if (!jobs.has(id))
264
- return false;
265
- clearTimer(id);
266
- jobs.delete(id);
267
- save();
268
- return true;
269
- }
270
- async function triggerJob(id) {
271
- const job = jobs.get(id);
272
- if (!job)
273
- return false;
274
- // A manual trigger should not double-fire alongside a pending timer; we
275
- // cancel and re-arm after the run so the cadence anchors on the fresh
276
- // lastRun set by _fireJob.
277
- clearTimer(id);
278
- await _fireJob(id);
279
- const fresh = jobs.get(id);
280
- if (fresh && fresh.enabled && fresh.kind !== 'oneshot')
281
- _scheduleJob(fresh);
282
- if (fresh && fresh.kind === 'oneshot') {
283
- fresh.enabled = false;
284
- jobs.set(fresh.id, { ...fresh });
285
- save();
286
- }
287
- return true;
288
- }
289
- // ── Test hook ─────────────────────────────────────────────────────────────────
290
- //
291
- // Tests need to reset module state between cases. Production code never
292
- // reaches for this — it lives behind a name that signals "for tests" so
293
- // nobody depends on it accidentally.
294
- function __resetForTests() {
295
- for (const id of timers.keys())
296
- clearTimer(id);
297
- jobs.clear();
298
- jobSeq = 1;
299
- pendingSave = Promise.resolve();
300
- }
19
+ exports.isHeartbeatActive = exports.stopHeartbeat = exports.startHeartbeat = exports.getDiagnostics = exports.AIDEN_CRON_BUILD = exports.__resetForTests = exports.awaitPendingSaves = exports.parseSchedule = exports.triggerJob = exports.deleteJob = exports.resumeJob = exports.pauseJob = exports.createJobAsync = exports.createJob = exports.getJobAsync = exports.getJob = exports.listJobsAsync = exports.listJobs = exports.loadJobs = void 0;
20
+ const cronManager_1 = require("./v4/cron/cronManager");
21
+ Object.defineProperty(exports, "loadJobs", { enumerable: true, get: function () { return cronManager_1.loadJobs; } });
22
+ Object.defineProperty(exports, "listJobs", { enumerable: true, get: function () { return cronManager_1.listJobs; } });
23
+ Object.defineProperty(exports, "listJobsAsync", { enumerable: true, get: function () { return cronManager_1.listJobsAsync; } });
24
+ Object.defineProperty(exports, "getJob", { enumerable: true, get: function () { return cronManager_1.getJob; } });
25
+ Object.defineProperty(exports, "getJobAsync", { enumerable: true, get: function () { return cronManager_1.getJobAsync; } });
26
+ Object.defineProperty(exports, "createJob", { enumerable: true, get: function () { return cronManager_1.createJob; } });
27
+ Object.defineProperty(exports, "createJobAsync", { enumerable: true, get: function () { return cronManager_1.createJobAsync; } });
28
+ Object.defineProperty(exports, "pauseJob", { enumerable: true, get: function () { return cronManager_1.pauseJob; } });
29
+ Object.defineProperty(exports, "resumeJob", { enumerable: true, get: function () { return cronManager_1.resumeJob; } });
30
+ Object.defineProperty(exports, "deleteJob", { enumerable: true, get: function () { return cronManager_1.deleteJob; } });
31
+ Object.defineProperty(exports, "triggerJob", { enumerable: true, get: function () { return cronManager_1.triggerJob; } });
32
+ Object.defineProperty(exports, "parseSchedule", { enumerable: true, get: function () { return cronManager_1.parseSchedule; } });
33
+ Object.defineProperty(exports, "awaitPendingSaves", { enumerable: true, get: function () { return cronManager_1.awaitPendingSaves; } });
34
+ Object.defineProperty(exports, "__resetForTests", { enumerable: true, get: function () { return cronManager_1.__resetForTests; } });
35
+ Object.defineProperty(exports, "AIDEN_CRON_BUILD", { enumerable: true, get: function () { return cronManager_1.AIDEN_CRON_BUILD; } });
36
+ Object.defineProperty(exports, "getDiagnostics", { enumerable: true, get: function () { return cronManager_1.getDiagnostics; } });
37
+ Object.defineProperty(exports, "startHeartbeat", { enumerable: true, get: function () { return cronManager_1.startHeartbeat; } });
38
+ Object.defineProperty(exports, "stopHeartbeat", { enumerable: true, get: function () { return cronManager_1.stopHeartbeat; } });
39
+ Object.defineProperty(exports, "isHeartbeatActive", { enumerable: true, get: function () { return cronManager_1.isHeartbeatActive; } });
@@ -9,13 +9,34 @@ exports.gateway = void 0;
9
9
  // All inbound messages (dashboard, Telegram, API, future channels)
10
10
  // are routed through a single processor so they share the same
11
11
  // memory, context, and tool pipeline.
12
+ //
13
+ // Phase v4.1-1.3a — replaced direct console.* writes with the
14
+ // Logger contract from `core/v4/logger`. The CLI's REPL is sacred:
15
+ // in cli-interactive mode the boot logger has no stdout sink, so
16
+ // route/register lines go to ~/.aiden/logs/aiden.log instead of
17
+ // corrupting the chat prompt. The legacy code path remains
18
+ // available — until `attachLogger()` is called the noopLogger
19
+ // silently drops every record (better than console.log for the
20
+ // REPL invariant). api/server.ts in serve mode wires a logger
21
+ // that writes NDJSON to stdout, preserving the daemon trace.
12
22
  const sessionRouter_1 = require("./sessionRouter");
23
+ const logger_1 = require("./v4/logger");
13
24
  // ── Gateway class ──────────────────────────────────────────────
14
25
  class Gateway {
15
26
  constructor() {
16
27
  this.handlers = new Map();
17
28
  this.messageProcessor = null;
18
29
  this.activeChannels = new Set();
30
+ this.log = (0, logger_1.noopLogger)();
31
+ }
32
+ // ── Logger injection ─────────────────────────────────────────
33
+ //
34
+ // Phase v4.1-1.3a — boot wires this once before any registerChannel
35
+ // / routeMessage call. Until then, noopLogger drops everything so
36
+ // accidentally-imported gateway code in tests / scripts can't leak
37
+ // anything to stdout.
38
+ attachLogger(logger) {
39
+ this.log = logger;
19
40
  }
20
41
  // ── Register the central message processor (Aiden's brain) ───
21
42
  setProcessor(handler) {
@@ -25,13 +46,13 @@ class Gateway {
25
46
  registerChannel(channel, deliveryHandler) {
26
47
  this.handlers.set(channel, deliveryHandler);
27
48
  this.activeChannels.add(channel);
28
- console.log(`[Gateway] Channel registered: ${channel}`);
49
+ this.log.info(`channel registered: ${channel}`);
29
50
  }
30
51
  // ── Unregister a channel ──────────────────────────────────────
31
52
  unregisterChannel(channel) {
32
53
  this.handlers.delete(channel);
33
54
  this.activeChannels.delete(channel);
34
- console.log(`[Gateway] Channel unregistered: ${channel}`);
55
+ this.log.info(`channel unregistered: ${channel}`);
35
56
  }
36
57
  // ── Route an incoming message through Aiden ───────────────────
37
58
  async routeMessage(message) {
@@ -42,13 +63,12 @@ class Gateway {
42
63
  const session = sessionRouter_1.sessionRouter.getSession(message.userId, message.channel);
43
64
  session.messageCount++;
44
65
  message.sessionId = session.sessionId;
45
- console.log(`[Gateway] ${message.channel}:${message.channelId} ` +
46
- `[${session.sessionId}] → "${message.text.substring(0, 60)}"`);
66
+ this.log.debug(`${message.channel}:${message.channelId} "${message.text.substring(0, 60)}"`, { sessionId: session.sessionId });
47
67
  const start = Date.now();
48
68
  try {
49
69
  let response = await this.messageProcessor(message);
50
70
  const duration = Date.now() - start;
51
- console.log(`[Gateway] Response ready (${duration}ms) → ${message.channel}`);
71
+ this.log.debug(`response ready → ${message.channel}`, { durationMs: duration });
52
72
  // Hint on Telegram first message: conversation continues on desktop
53
73
  if (message.channel === 'telegram' && session.messageCount === 1) {
54
74
  response += '\n\n_Tip: Continue this conversation on your desktop dashboard with full context._';
@@ -56,7 +76,7 @@ class Gateway {
56
76
  return response;
57
77
  }
58
78
  catch (error) {
59
- console.error(`[Gateway] Processing failed:`, error);
79
+ this.log.error(`processing failed: ${error instanceof Error ? error.message : String(error)}`);
60
80
  return 'Something went wrong processing your message. Try again.';
61
81
  }
62
82
  }
@@ -64,14 +84,15 @@ class Gateway {
64
84
  async deliver(message) {
65
85
  const handler = this.handlers.get(message.channel);
66
86
  if (!handler) {
67
- console.log(`[Gateway] No handler for channel: ${message.channel}`);
87
+ this.log.warn(`no handler for channel: ${message.channel}`);
68
88
  return false;
69
89
  }
70
90
  try {
71
91
  return await handler(message);
72
92
  }
73
93
  catch (error) {
74
- console.error(`[Gateway] Delivery failed to ${message.channel}:`, error);
94
+ this.log.error(`delivery failed to ${message.channel}: ` +
95
+ (error instanceof Error ? error.message : String(error)));
75
96
  return false;
76
97
  }
77
98
  }
@@ -47,6 +47,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
47
47
  return (mod && mod.__esModule) ? mod : { "default": mod };
48
48
  };
49
49
  Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.pwQueueDepth = pwQueueDepth;
51
+ exports.setPwLogger = setPwLogger;
52
+ exports.withPwLock = withPwLock;
53
+ exports.pwAcquire = pwAcquire;
50
54
  exports.pwNavigate = pwNavigate;
51
55
  exports.pwScreenshot = pwScreenshot;
52
56
  exports.pwClick = pwClick;
@@ -77,6 +81,92 @@ let _idleTimer = null;
77
81
  const IDLE_MS = 5 * 60 * 1000; // 5 min
78
82
  const NAV_TIMEOUT = parseInt(process.env.AIDEN_BROWSER_TIMEOUT ?? '15000', 10);
79
83
  const HEADLESS = process.env.AIDEN_BROWSER_HEADLESS === 'true';
84
+ // ── Phase v4.1-subagent — Browser mutex ──────────────────────
85
+ // One global browser context lives in this module. Subagent fanout
86
+ // can spin up N parallel agents — if two of them claim the browser
87
+ // at the same instant they'd collide on `_activePage` (one navigates
88
+ // while the other reads, racing on URL state).
89
+ //
90
+ // The mutex is a single-slot async lock: callers `await
91
+ // pwAcquire()`, do their work, then call the returned `release()`.
92
+ // First arrival runs immediately; subsequent arrivals queue. Because
93
+ // every browser tool already calls `ensureContext` / `ensurePage`
94
+ // first, the mutex wraps the whole tool body — release is idempotent
95
+ // so callers can call it from a `finally`.
96
+ //
97
+ // Common path (no contention) costs one extra microtask. A queued
98
+ // subagent waits exactly as long as the holder takes — no busy
99
+ // loops, no timers.
100
+ let _browserBusy = false;
101
+ const _browserWaiters = [];
102
+ /** Public observability — number of waiters currently queued plus
103
+ * the holder (if any). Used by subagent diagnostics; not part of
104
+ * the tool path. */
105
+ function pwQueueDepth() {
106
+ return _browserWaiters.length + (_browserBusy ? 1 : 0);
107
+ }
108
+ let _pwLogger = null;
109
+ function setPwLogger(logger) {
110
+ _pwLogger = logger;
111
+ }
112
+ /** Higher-order helper — wrap any browser-claiming code in this so
113
+ * all callers queue on the same mutex. Tag identifies the caller
114
+ * in the queued/granted log lines.
115
+ *
116
+ * Integration plan: subagent fanout (Phase v4.1-subagent) wraps its
117
+ * per-subagent browser tool dispatch with `withPwLock` so two
118
+ * subagents claiming the browser concurrently queue. The existing
119
+ * public pw* functions in this module are left as direct callers
120
+ * for now — the v3 single-loop path has no contention and the
121
+ * primitive can be added file-by-file as fanout flushes out the
122
+ * hot paths. The smoke for v4.1-subagent tests `pwAcquire` /
123
+ * `withPwLock` directly. */
124
+ async function withPwLock(tag, fn) {
125
+ const queued = _browserWaiters.length + (_browserBusy ? 1 : 0);
126
+ if (queued > 0 && _pwLogger) {
127
+ _pwLogger.info('browser mutex: queued', { tag, depth: queued });
128
+ }
129
+ const release = await pwAcquire();
130
+ if (_pwLogger) {
131
+ _pwLogger.info('browser mutex: granted', { tag });
132
+ }
133
+ try {
134
+ return await fn();
135
+ }
136
+ finally {
137
+ release();
138
+ }
139
+ }
140
+ /** Acquire the browser mutex. The returned `release` is idempotent —
141
+ * multiple calls are no-ops. Always call from a `finally` so a
142
+ * thrown tool body never strands the lock. */
143
+ async function pwAcquire() {
144
+ if (!_browserBusy) {
145
+ _browserBusy = true;
146
+ return makeRelease();
147
+ }
148
+ return new Promise((resolve) => {
149
+ _browserWaiters.push(() => {
150
+ _browserBusy = true;
151
+ resolve(makeRelease());
152
+ });
153
+ });
154
+ }
155
+ function makeRelease() {
156
+ let released = false;
157
+ return () => {
158
+ if (released)
159
+ return;
160
+ released = true;
161
+ _browserBusy = false;
162
+ const next = _browserWaiters.shift();
163
+ // Defer to a microtask so the releasing call chain finishes
164
+ // before the next claimant starts — keeps stack depth bounded
165
+ // under deeply queued fanouts.
166
+ if (next)
167
+ queueMicrotask(next);
168
+ };
169
+ }
80
170
  function getBrowserProfileDir() {
81
171
  const base = (0, paths_1.getUserDataDir)();
82
172
  const dir = path_1.default.join(base, 'browser-profile');
@@ -56,6 +56,7 @@ const EMPTY_RETRY_NOTE = '[System note: your previous turn returned empty conten
56
56
  // ── Class ────────────────────────────────────────────────────────────────
57
57
  class AidenAgent {
58
58
  constructor(opts) {
59
+ this.skillMinerTurnIdx = 0;
59
60
  // ── Cross-call state ─────────────────────────────────────────────────
60
61
  /** Cached system prompt — invalidated by setPersonalityOverlay/markMemoryDirty/explicit. */
61
62
  this.cachedSystemPrompt = null;
@@ -83,6 +84,8 @@ class AidenAgent {
83
84
  this.honestyEnforcement = opts.honestyEnforcement;
84
85
  this.skillTeacher = opts.skillTeacher;
85
86
  this.skillTeacherCallbacks = opts.skillTeacherCallbacks;
87
+ this.skillMiner = opts.skillMiner;
88
+ this.onSkillCandidate = opts.onSkillCandidate;
86
89
  this.resolveVerifiedFlag = opts.resolveVerifiedFlag;
87
90
  this.resolveToolset = opts.resolveToolset;
88
91
  this.promptBuilder = opts.promptBuilder;
@@ -264,6 +267,38 @@ class AidenAgent {
264
267
  /* SkillTeacher failures must not break the turn */
265
268
  }
266
269
  }
270
+ // 11. SkillMiner post-loop observation. Stages a candidate into
271
+ // `<aidenHome>/skills/learned/.candidates.json` for user
272
+ // review via `/skills review`. Complementary to SkillTeacher
273
+ // above (inline propose-and-write); the miner's queue is the
274
+ // deferred, audit-first path.
275
+ if (this.skillMiner) {
276
+ try {
277
+ const turnIdx = this.skillMinerTurnIdx;
278
+ this.skillMinerTurnIdx += 1;
279
+ const traceForMiner = loopResult.toolCallTrace.map((entry, i) => ({
280
+ name: entry.name,
281
+ args: loopResult.fullTrace[i]?.args ?? {},
282
+ result: entry.result,
283
+ error: entry.error,
284
+ toolset: this.resolveToolset?.(entry.name),
285
+ }));
286
+ const sessionId = this.sessionId ?? 'session';
287
+ const outcome = await this.skillMiner.observeTurn({
288
+ trace: traceForMiner,
289
+ sessionId,
290
+ sourceTurnIdx: turnIdx,
291
+ finishReason: loopResult.finishReason,
292
+ history,
293
+ });
294
+ if (outcome.status === 'queued' && outcome.candidate && this.onSkillCandidate) {
295
+ this.onSkillCandidate(outcome.candidate);
296
+ }
297
+ }
298
+ catch {
299
+ /* SkillMiner failures must not break the turn */
300
+ }
301
+ }
267
302
  return {
268
303
  finalContent,
269
304
  messages: loopResult.messages,
@@ -16,8 +16,8 @@
16
16
  * - SkillTeacher (purpose: 'skill_describe')
17
17
  * - smart approval (purpose: 'risk_assess', wired in Phase 14)
18
18
  *
19
- * resolution chain (main provider → OpenRouter → Nous Portal custom →
20
- * Anthropic). Aiden v4.0.0 keeps a single resolved adapter for simplicity;
19
+ * resolution chain (main provider → OpenRouter → portal subscription
20
+ * custom → Anthropic). Aiden v4.0.0 keeps a single resolved adapter for simplicity;
21
21
  * the multi-provider fallback chain comes back in v4.1.
22
22
  *
23
23
  * Failure mode: when the cheap model is unavailable, returns empty content
@@ -67,8 +67,22 @@ async function writeJsonAtomic(filePath, data) {
67
67
  }
68
68
  }
69
69
  async function doWrite(filePath, data) {
70
- const dir = path.dirname(filePath);
71
- const baseTmp = `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`;
70
+ // Phase v4.1-cron — symlink-aware rename. If the destination
71
+ // path is a symlink (e.g. cron_jobs.json points at a file in a
72
+ // shared directory), naive `fs.rename` *replaces* the symlink with
73
+ // a regular file, detaching it from the link target. Resolve the
74
+ // symlink first so the rename lands on the actual target. When the
75
+ // path is NOT a symlink (or doesn't exist yet), `realpath` returns
76
+ // the path itself or throws ENOENT — both safe to fall through.
77
+ let resolvedPath = filePath;
78
+ try {
79
+ resolvedPath = await fsp.realpath(filePath);
80
+ }
81
+ catch {
82
+ // ENOENT (file doesn't exist yet) or non-symlink — use original.
83
+ }
84
+ const dir = path.dirname(resolvedPath);
85
+ const baseTmp = `.${path.basename(resolvedPath)}.${process.pid}.${Date.now()}.tmp`;
72
86
  const tmpPath = path.join(dir, baseTmp);
73
87
  await fsp.mkdir(dir, { recursive: true });
74
88
  const json = JSON.stringify(data, null, 2);
@@ -86,7 +100,7 @@ async function doWrite(filePath, data) {
86
100
  await fh.close();
87
101
  }
88
102
  try {
89
- await fsp.rename(tmpPath, filePath);
103
+ await fsp.rename(tmpPath, resolvedPath);
90
104
  }
91
105
  catch (err) {
92
106
  // rename failed — clean up the orphan temp file before re-raising.
@@ -98,7 +112,7 @@ async function doWrite(filePath, data) {
98
112
  }
99
113
  // Owner-only access. Best-effort: NTFS may refuse but we try anyway.
100
114
  try {
101
- await fsp.chmod(filePath, 0o600);
115
+ await fsp.chmod(resolvedPath, 0o600);
102
116
  }
103
117
  catch { /* windows / non-POSIX */ }
104
118
  }