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
@@ -62,7 +62,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
62
62
  return (mod && mod.__esModule) ? mod : { "default": mod };
63
63
  };
64
64
  Object.defineProperty(exports, "__esModule", { value: true });
65
- exports.getEnvSource = exports.loadAidenEnvFile = void 0;
65
+ exports.getEnvSource = exports.loadAidenEnvFile = exports.AIDEN_UI_BUILD = void 0;
66
66
  exports.main = main;
67
67
  exports.buildAgentRuntime = buildAgentRuntime;
68
68
  exports.runInteractiveChat = runInteractiveChat;
@@ -81,6 +81,10 @@ const display_1 = require("./display");
81
81
  const skinEngine_1 = require("./skinEngine");
82
82
  const commandRegistry_1 = require("./commandRegistry");
83
83
  const callbacks_1 = require("./callbacks");
84
+ // Tier-3.1 (v4.1-tier3.1) — re-export the build fingerprint so the
85
+ // runtime smoke can find it in the bundled artifact.
86
+ const uiBuild_1 = require("./uiBuild");
87
+ Object.defineProperty(exports, "AIDEN_UI_BUILD", { enumerable: true, get: function () { return uiBuild_1.AIDEN_UI_BUILD; } });
84
88
  const setupWizard_1 = require("./setupWizard");
85
89
  const doctor_1 = require("./doctor");
86
90
  const modelPicker_1 = require("./commands/modelPicker");
@@ -92,6 +96,7 @@ const sessionStore_1 = require("../../core/v4/sessionStore");
92
96
  const sessionManager_1 = require("../../core/v4/sessionManager");
93
97
  const toolRegistry_1 = require("../../core/v4/toolRegistry");
94
98
  const skillLoader_1 = require("../../core/v4/skillLoader");
99
+ const index_1 = require("../../tools/v4/index");
95
100
  const skillCommands_1 = require("../../core/v4/skillCommands");
96
101
  const aidenAgent_1 = require("../../core/v4/aidenAgent");
97
102
  const promptBuilder_1 = require("../../core/v4/promptBuilder");
@@ -102,18 +107,31 @@ const approvalEngine_1 = require("../../moat/approvalEngine");
102
107
  const plannerGuard_1 = require("../../moat/plannerGuard");
103
108
  const honestyEnforcement_1 = require("../../moat/honestyEnforcement");
104
109
  const skillTeacher_1 = require("../../moat/skillTeacher");
110
+ const skillMiner_1 = require("../../core/v4/skillMining/skillMiner");
111
+ const uiBuild_2 = require("./uiBuild");
105
112
  const memoryGuard_1 = require("../../moat/memoryGuard");
106
113
  const ssrfProtection_1 = require("../../moat/ssrfProtection");
107
114
  const tirithScanner_1 = require("../../moat/tirithScanner");
108
115
  const credentialResolver_1 = require("../../providers/v4/credentialResolver");
109
116
  const runtimeResolver_1 = require("../../providers/v4/runtimeResolver");
110
117
  const chatCompletionsAdapter_1 = require("../../providers/v4/chatCompletionsAdapter");
118
+ const modelCatalog_1 = require("../../providers/v4/modelCatalog");
111
119
  const providerFallback_1 = require("../../core/v4/providerFallback");
112
120
  const skillBundledRestore_1 = require("../../core/v4/skillBundledRestore");
113
121
  const providerDetection_1 = require("../../core/v4/firstRun/providerDetection");
114
122
  const aidenLogger_1 = require("../../core/v4/aidenLogger");
115
123
  const plugins_1 = require("../../core/v4/plugins");
116
124
  const providerAuth_1 = require("../../core/v4/auth/providerAuth");
125
+ // Phase v4.1-1.1 — CLI-side ChannelManager. The same singleton lives in
126
+ // the API server process; in a CLI-only session we host it here so
127
+ // /channel commands operate without a separate server. When `aiden serve`
128
+ // runs in the SAME process tree on the same machine, polling-based
129
+ // adapters (Telegram) must only be started in one of the two — we
130
+ // gate startup on a heuristic below to avoid 409 polling-conflict.
131
+ const manager_1 = require("../../core/channels/manager");
132
+ const telegram_1 = require("../../core/channels/telegram");
133
+ const gateway_1 = require("../../core/gateway");
134
+ const logger_1 = require("../../core/v4/logger");
117
135
  const v4_1 = require("../../tools/v4");
118
136
  const mcpSetup_1 = require("../../tools/v4/mcpSetup");
119
137
  const skillCommandHandler_1 = require("./commands/skillCommandHandler");
@@ -196,7 +214,24 @@ async function main(argv, opts = {}) {
196
214
  .option('--planner-guard <mode>', 'PlannerGuard mode: off | rule_based | llm_classified')
197
215
  .option('--honesty <mode>', 'HonestyEnforcement mode: off | detect | enforce')
198
216
  .option('--skill-teacher <tier>', 'SkillTeacher tier: off | tier_3_propose | tier_4_auto')
217
+ .option('--no-ui', 'Disable Tier-3 UI polish (autosuggest ghost text, inline status line); fall back to legacy rendering')
199
218
  .action(async () => {
219
+ // Tier-3.1: argv discipline. If positional args were passed but
220
+ // no subcommand matched, error to stderr and exit non-zero
221
+ // rather than silently booting the REPL — a typo'd subcommand
222
+ // otherwise produces a hung foreground.
223
+ const leftover = program.args ?? [];
224
+ if (leftover.length > 0) {
225
+ process.stderr.write(`error: unknown command "${leftover[0]}"\n`);
226
+ process.stderr.write(`Run 'aiden --help' for available commands.\n`);
227
+ process.exit(2);
228
+ }
229
+ // Tier-3.1: surface --no-ui as an env var so downstream modules
230
+ // (which import uiBuild.ts) see the flag without threading it
231
+ // through every call site.
232
+ const o = program.opts();
233
+ if (o.ui === false)
234
+ process.env.AIDEN_NO_UI = '1';
200
235
  const cliOpts = program.opts();
201
236
  if (opts.runChatHook) {
202
237
  await opts.runChatHook(cliOpts);
@@ -204,6 +239,16 @@ async function main(argv, opts = {}) {
204
239
  }
205
240
  await runInteractiveChat(cliOpts, opts);
206
241
  });
242
+ // Tier-3.1: hidden one-shot — emits the UI build fingerprint and
243
+ // exits. Used by the runtime smoke to verify the bundled artifact
244
+ // matches the expected sub-phase without parsing other output.
245
+ program
246
+ .command('print-ui-build', { hidden: true })
247
+ .description('Print the v4.1 tier-3 UI build fingerprint and exit.')
248
+ .action(() => {
249
+ process.stdout.write(`${uiBuild_1.AIDEN_UI_BUILD}\n`);
250
+ process.exit(0);
251
+ });
207
252
  program
208
253
  .command('setup')
209
254
  .description('Run the setup wizard (provider + model + API key)')
@@ -237,12 +282,13 @@ async function main(argv, opts = {}) {
237
282
  program
238
283
  .command('doctor')
239
284
  .description('Run diagnostic checks')
240
- .action(async () => {
285
+ .option('--providers', 'Also ping each configured / authed provider and report live status (deep check). Slower; useful before shipping or when a provider regression is suspected.')
286
+ .action(async (cmdOpts) => {
241
287
  if (opts.runDoctorHook) {
242
288
  await opts.runDoctorHook();
243
289
  return;
244
290
  }
245
- await (0, doctor_1.runDoctorCli)();
291
+ await (0, doctor_1.runDoctorCli)({ liveness: cmdOpts.providers === true });
246
292
  });
247
293
  program
248
294
  .command('sessions <action> [arg]')
@@ -266,17 +312,82 @@ async function main(argv, opts = {}) {
266
312
  });
267
313
  program
268
314
  .command('mcp <action>')
269
- .description('Manage MCP servers (full impl deferred to v4.1 with the gateway).')
315
+ .description('MCP server mode (Phase v4.1-mcp). Actions: serve, status, tools.')
270
316
  .action(async (action) => {
271
317
  if (opts.runMcpHook) {
272
318
  await opts.runMcpHook(action);
273
319
  return;
274
320
  }
275
- const out = opts.writeOut ?? ((t) => process.stdout.write(t));
276
- out(`'aiden mcp ${action}' is deferred to v4.1 alongside the gateway.\n`);
321
+ // Lazy-load so the rest of the CLI does not pay the import cost
322
+ // for `setup`, `doctor`, `model`, etc. on every invocation.
323
+ const { runMcpSubcommand } = await Promise.resolve().then(() => __importStar(require('./commands/mcp')));
324
+ const code = await runMcpSubcommand(action, {
325
+ writeOut: opts.writeOut,
326
+ writeErr: (t) => process.stderr.write(t),
327
+ });
328
+ if (code !== 0)
329
+ process.exit(code);
330
+ });
331
+ program
332
+ .command('voice [args...]')
333
+ .description('Voice diagnostics + one-shot TTS / transcribe (Phase v4.1-voice-cli). ' +
334
+ 'Usage: aiden voice doctor | tts "<text>" | transcribe <file>')
335
+ .allowUnknownOption()
336
+ .action(async (args) => {
337
+ const { runVoiceSubcommand } = await Promise.resolve().then(() => __importStar(require('./voiceCli')));
338
+ const action = (args[0] ?? 'doctor').toLowerCase();
339
+ const rest = args.slice(1);
340
+ const code = await runVoiceSubcommand(action, rest, {
341
+ writeOut: opts.writeOut,
342
+ writeErr: (t) => process.stderr.write(t),
343
+ });
344
+ if (code !== 0)
345
+ process.exit(code);
346
+ });
347
+ program
348
+ .command('subagent <action>')
349
+ .description('Subagent fanout diagnostics (Phase v4.1-subagent). Actions: status, tools.')
350
+ .action(async (action) => {
351
+ const { runSubagentSubcommand } = await Promise.resolve().then(() => __importStar(require('./commands/subagent')));
352
+ const code = await runSubagentSubcommand(action, {
353
+ writeOut: opts.writeOut,
354
+ writeErr: (t) => process.stderr.write(t),
355
+ });
356
+ if (code !== 0)
357
+ process.exit(code);
358
+ });
359
+ program
360
+ .command('fanout [args...]')
361
+ .description('Run a parallel agent fanout (Phase v4.1-subagent). ' +
362
+ 'Usage: aiden fanout "<query>" --n=3 --merge=combine [--mode=ensemble] [--dry-run]')
363
+ .allowUnknownOption()
364
+ .action(async (args) => {
365
+ const { runFanoutCli } = await Promise.resolve().then(() => __importStar(require('./commands/fanout')));
366
+ const code = await runFanoutCli(args, {
367
+ writeOut: opts.writeOut,
368
+ writeErr: (t) => process.stderr.write(t),
369
+ });
370
+ if (code !== 0)
371
+ process.exit(code);
277
372
  });
278
373
  // v4.1 placeholders. (`tui` graduated to a real flag in Phase 15.)
279
- for (const cmd of ['batch', 'gateway', 'cron', 'pairing', 'update']) {
374
+ program
375
+ .command('cron [args...]')
376
+ .description('Cron diagnostics + one-shot list / run (Phase v4.1 hardened cron). ' +
377
+ 'Usage: aiden cron status | list | run <id>')
378
+ .allowUnknownOption()
379
+ .action(async (args) => {
380
+ const { runCronSubcommand } = await Promise.resolve().then(() => __importStar(require('./cronCli')));
381
+ const action = (args[0] ?? 'status').toLowerCase();
382
+ const rest = args.slice(1);
383
+ const code = await runCronSubcommand(action, rest, {
384
+ writeOut: opts.writeOut,
385
+ writeErr: (t) => process.stderr.write(t),
386
+ });
387
+ if (code !== 0)
388
+ process.exit(code);
389
+ });
390
+ for (const cmd of ['batch', 'gateway', 'pairing', 'update']) {
280
391
  program
281
392
  .command(cmd)
282
393
  .description(`(deferred to v4.1)`)
@@ -777,6 +888,14 @@ async function buildAgentRuntime(cliOpts, opts) {
777
888
  personalityOverlay: activeOverlay,
778
889
  modelId,
779
890
  };
891
+ // ── Phase v4.1-skill-mining ──────────────────────────────────────────
892
+ // Construct the miner once — it owns its in-memory session counter +
893
+ // CandidateStore handle. Skipped entirely in MCP serve mode (the
894
+ // serve binary doesn't run the agent loop the same way and shouldn't
895
+ // mutate skill state from inside JSON-RPC handling).
896
+ const skillMiner = (0, uiBuild_2.isMcpServeMode)()
897
+ ? undefined
898
+ : new skillMiner_1.SkillMiner({ auxiliaryClient });
780
899
  // ── Build agent with all moat layers attached ────────────────────────
781
900
  const agent = new aidenAgent_1.AidenAgent({
782
901
  provider: adapter,
@@ -787,6 +906,13 @@ async function buildAgentRuntime(cliOpts, opts) {
787
906
  plannerGuard,
788
907
  honestyEnforcement,
789
908
  skillTeacher,
909
+ skillMiner,
910
+ onSkillCandidate: (candidate) => {
911
+ try {
912
+ callbacks.onSkillCandidate?.(candidate);
913
+ }
914
+ catch { /* notification must not break the turn */ }
915
+ },
790
916
  // Phase 23.5: tool event rows. CliCallbacks.onToolCall
791
917
  // emits a single line per call — `· tool <name> <args> [running]`
792
918
  // mutates to `[ok 220ms]` / `[fail 1.4s]` / `[blocked]` on resolve.
@@ -840,6 +966,236 @@ async function buildAgentRuntime(cliOpts, opts) {
840
966
  memoryManager.onMutation((file) => {
841
967
  agent.markMemoryDirty(file === 'user' ? 'user' : 'memory');
842
968
  });
969
+ // ── Phase v4.1-subagent.1 — subagent_fanout wiring is below
970
+ // (after `bootLogger` is declared and the gateway processor is set
971
+ // up). Stub registered at boot is replaced there with the real
972
+ // closures over adapter / sessionManager / promptBuilder / etc.
973
+ // Phase v4.1-1.3a — boot a unified mode-aware Logger ('cli-interactive'
974
+ // mode = no stdout sinks, REPL is sacred). Declared here so the gateway
975
+ // processor + channel manager (later in the function) can both attach
976
+ // scoped child loggers to the same root.
977
+ const { logger: bootLogger } = (0, logger_1.createBootLogger)({
978
+ mode: 'cli-interactive',
979
+ logsDir: paths.logsDir,
980
+ });
981
+ // Wire the gateway singleton's logger BEFORE registering its processor
982
+ // so register / unregister channel events are scoped correctly.
983
+ gateway_1.gateway.attachLogger(bootLogger.child('gateway'));
984
+ // ── Phase v4.1-subagent.1 — replace subagent_fanout stub with wired version
985
+ //
986
+ // tools/v4/index.ts registers a stub at boot so the schema is visible
987
+ // to MCP / /tools immediately. NOW that the runtime has a provider
988
+ // adapter, an active model, sessions, memory, and a built agent, we
989
+ // re-register subagent_fanout with the real callbacks so live calls
990
+ // (from REPL, MCP, or `aiden fanout`) actually execute children
991
+ // instead of returning the "not wired" stub error.
992
+ //
993
+ // The closures capture parent runtime handles. Each fanout call
994
+ // builds a fresh AidenAgent per child — the AidenAgent constructor
995
+ // is cheap (per-instance state, no module singletons), so N=5
996
+ // children = 5 instances + 5 cloned FallbackAdapters. The heavy
997
+ // shared subsystems (registry, skillLoader, paths, memoryManager,
998
+ // promptBuilder, promptBuilderOptions) are read-only and pass by
999
+ // reference.
1000
+ toolRegistry.register((0, index_1.makeSubagentFanoutTool)({
1001
+ logger: bootLogger.child('subagent'),
1002
+ resolveActiveModel: () => ({ providerId, modelId }),
1003
+ aggregatorAdapter: adapter,
1004
+ resolveProviders: () => {
1005
+ // When the parent uses FallbackAdapter, expose every key-present
1006
+ // slot's (providerId, modelId) so rotation can spread children
1007
+ // across distinct providers / keys. Otherwise just the active
1008
+ // provider+model pair — single-provider rotation falls back to
1009
+ // slot rotation within the FallbackAdapter at run time, OR to
1010
+ // pure same-provider sampling (singleProviderWarning fires).
1011
+ if (adapter instanceof providerFallback_1.FallbackAdapter) {
1012
+ const diag = adapter.getDiagnostics();
1013
+ const live = diag.slots.filter((s) => s.keyPresent);
1014
+ if (live.length > 0) {
1015
+ return live.map((s) => ({
1016
+ providerId: s.providerId,
1017
+ modelId: s.modelId,
1018
+ label: s.id,
1019
+ }));
1020
+ }
1021
+ }
1022
+ return [{ providerId, modelId }];
1023
+ },
1024
+ runChild: async (childOpts) => {
1025
+ // Per-child context: paths / skillLoader / memoryManager / processes
1026
+ // are SAFE to share (read-only or per-call by design). The approval
1027
+ // engine is intentionally OMITTED — N children competing for one
1028
+ // stdin REPL would deadlock.
1029
+ const childCtx = {
1030
+ cwd: process.cwd(),
1031
+ paths,
1032
+ sessions: sessionManager,
1033
+ memory: memoryManager,
1034
+ skillLoader,
1035
+ // approvalEngine, ssrfProtection, tirithScanner, memoryGuard:
1036
+ // SSRF + Tirith would be safe to share but adding them now
1037
+ // expands the per-child surface; keep lean for v4.1-subagent.1
1038
+ // and revisit when fanout actually exercises network or shell
1039
+ // tools (gated by ALLOW_DESTRUCTIVE).
1040
+ };
1041
+ // Filter the tool surface. Default-safe: read-only tools only.
1042
+ // AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE=1 mirrors the MCP env from
1043
+ // v4.1-mcp — predictable, env-driven.
1044
+ const allowDestructive = process.env.AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE === '1' ||
1045
+ process.env.AIDEN_SUBAGENT_ALLOW_DESTRUCTIVE === 'true';
1046
+ const childToolNames = [];
1047
+ for (const name of toolRegistry.list()) {
1048
+ const h = toolRegistry.get(name);
1049
+ if (!h)
1050
+ continue;
1051
+ if (h.mutates && !allowDestructive)
1052
+ continue;
1053
+ // Avoid recursive fanout this phase — children cannot spawn
1054
+ // their own children. Recursion was capped at depth 1 by
1055
+ // default in prior multi-agent systems for the same reason;
1056
+ // v3 starved nested spawns.
1057
+ if (name === 'subagent_fanout')
1058
+ continue;
1059
+ childToolNames.push(name);
1060
+ }
1061
+ const childExecutor = toolRegistry.buildExecutor(childCtx);
1062
+ const childTools = childToolNames
1063
+ .map((n) => toolRegistry.get(n)?.schema)
1064
+ .filter((s) => !!s);
1065
+ // Provider isolation: clone the FallbackAdapter so per-child
1066
+ // rate-limit state doesn't pollute the parent or siblings.
1067
+ // Non-Fallback adapters are stateless by spec (providers/v4/
1068
+ // types.ts:190) so direct reuse is safe.
1069
+ const childProvider = adapter instanceof providerFallback_1.FallbackAdapter
1070
+ ? adapter.clone()
1071
+ : adapter;
1072
+ // Build per-child AidenAgent. Skip the moat layers (PlannerGuard,
1073
+ // HonestyEnforcement, SkillTeacher, SkillEnforcementTracker) —
1074
+ // they're parent-loop concerns and add cost without value at the
1075
+ // child scale. Skip promptBuilder too: children get a SHORT
1076
+ // system prompt (brief identity + role) instead of the parent's
1077
+ // full SOUL.md + 72-skills inventory + memory snapshot. The
1078
+ // tradeoff is deliberate — children answer the GOAL, not "be
1079
+ // Aiden". With the full prompt, trivial queries take 30s+ for
1080
+ // children to generate verbose self-introductions; the lean
1081
+ // child prompt brings n=2 trivial fanouts under 12s. Parent
1082
+ // should pass any context children genuinely need via the
1083
+ // `query` / `tasks[].context` argument.
1084
+ const child = new aidenAgent_1.AidenAgent({
1085
+ provider: childProvider,
1086
+ tools: childTools,
1087
+ toolExecutor: childExecutor,
1088
+ maxTurns: childOpts.maxIterations,
1089
+ providerId: childOpts.provider.providerId,
1090
+ modelId: childOpts.provider.modelId,
1091
+ // No promptBuilder — childSystemPrompt prepended manually below.
1092
+ // No fallback strategy — child failures bubble up to the
1093
+ // orchestrator, which surfaces them in the result envelope.
1094
+ });
1095
+ // Honour the abort signal — if the parent aborts mid-call (or the
1096
+ // per-child timeout fires), short-circuit before dispatching to
1097
+ // the provider. AidenAgent doesn't take an AbortSignal directly;
1098
+ // the AbortController plumbing through fetch is the
1099
+ // v4.1-subagent.2 / v4.2 hardening pass. Pre-check here for the
1100
+ // synchronous path.
1101
+ if (childOpts.signal.aborted) {
1102
+ throw new Error('aborted before dispatch');
1103
+ }
1104
+ // Brief, role-aware system prompt — drops 5KB+ of Aiden identity
1105
+ // boilerplate that would otherwise inflate every child to 30s+
1106
+ // wall-clock for a trivial query. The parent agent retains the
1107
+ // full prompt when it's the orchestrator; children answer the
1108
+ // goal directly.
1109
+ const roleLine = childOpts.role
1110
+ ? `Role: ${childOpts.role}. `
1111
+ : '';
1112
+ const childSystemPrompt = `You are one of ${childOpts.index >= 0 ? 'N' : '?'} parallel subagents. ` +
1113
+ `${roleLine}Answer the user's request concisely. Use available tools when ` +
1114
+ `the answer requires real-world information you don't have memorized.`;
1115
+ const history = [
1116
+ { role: 'system', content: childSystemPrompt },
1117
+ { role: 'user', content: childOpts.prompt },
1118
+ ];
1119
+ const result = await child.runConversation(history);
1120
+ return result.finalContent;
1121
+ },
1122
+ }));
1123
+ bootLogger.child('subagent').info('subagent_fanout: wired (replaces stub)', {
1124
+ providerId,
1125
+ modelId,
1126
+ fallback: adapter instanceof providerFallback_1.FallbackAdapter ? 'FallbackAdapter' : 'direct',
1127
+ });
1128
+ // ── Phase v4.1-2.1: gateway message processor ────────────────────
1129
+ //
1130
+ // Channel adapters call `gateway.routeMessage(...)` for every inbound
1131
+ // message; the gateway then invokes the registered processor — that's
1132
+ // the bridge from channel-side I/O to the agent loop. `aiden serve`
1133
+ // wires its own processor in `api/server.ts` (HTTP-hops to /api/chat
1134
+ // because Express is already up). The CLI host (Phase v4.1-1.1)
1135
+ // never had one, so every Telegram inbound was throwing
1136
+ // "No message processor registered" and the user saw the
1137
+ // friendly-fallback "Something went wrong" reply.
1138
+ //
1139
+ // The closure mirrors the api/server processor's intent — one agent
1140
+ // turn per inbound — but invokes `agent.runConversation()` directly
1141
+ // instead of round-tripping through HTTP. Per-(channel, channelId)
1142
+ // history persists through the same SessionStore the REPL uses, so
1143
+ // a Telegram conversation accumulates context across messages, and
1144
+ // a future `/sessions` listing surfaces those threads alongside REPL
1145
+ // sessions.
1146
+ const gatewayProcessorLog = bootLogger.child('gateway.processor');
1147
+ // gateway sessionId (`session_<ts>`) → sessionStore session id.
1148
+ // In-memory only; restart re-creates a fresh sessionStore session
1149
+ // for the same channel+user pair.
1150
+ const gatewaySessionMap = new Map();
1151
+ gateway_1.gateway.setProcessor(async (message) => {
1152
+ try {
1153
+ // 1. Resolve a sessionStore session for this gateway session.
1154
+ // sessionManager.startSession opens a new row; we cache the
1155
+ // mapping so subsequent messages from the same chat append
1156
+ // to the same history.
1157
+ const gatewaySid = message.sessionId
1158
+ ?? `${message.channel}_${message.channelId}`;
1159
+ let storeSid = gatewaySessionMap.get(gatewaySid);
1160
+ if (!storeSid) {
1161
+ const created = sessionManager.startSession({
1162
+ providerId,
1163
+ modelId,
1164
+ title: `${message.channel}:${message.channelId}`,
1165
+ });
1166
+ storeSid = created.id;
1167
+ gatewaySessionMap.set(gatewaySid, storeSid);
1168
+ }
1169
+ // 2. Load past messages for this session and append the new
1170
+ // user turn so the agent sees full context. We drop tool /
1171
+ // system rows on load — the agent's prompt builder rebuilds
1172
+ // those from scratch each call.
1173
+ // Provider Message union has tool-specific variants; we only
1174
+ // load the user/assistant turns and cast to satisfy the union.
1175
+ // Tool-call replay across adapter restarts isn't a feature we
1176
+ // need for chat-channel UX (Phase v4.1-2.1 scope).
1177
+ const past = store.getMessages(storeSid)
1178
+ .filter((r) => r.role === 'user' || r.role === 'assistant')
1179
+ .map((r) => ({ role: r.role, content: r.content }));
1180
+ const userTurn = { role: 'user', content: message.text };
1181
+ const history = [...past, userTurn];
1182
+ // 3. Run one agent turn.
1183
+ const result = await agent.runConversation(history);
1184
+ // 4. Persist the new tail (everything past the loaded history) so
1185
+ // the next inbound resumes seamlessly. Mirror chatSession's
1186
+ // record-turn slice convention.
1187
+ const newSlice = result.messages.slice(history.length - 1);
1188
+ sessionManager.recordTurn(storeSid, newSlice, result.totalUsage);
1189
+ return result.finalContent || '(no response)';
1190
+ }
1191
+ catch (err) {
1192
+ // Diagnostics route through the unified logger — nothing reaches
1193
+ // stdout / stderr so the REPL stays clean. The gateway's own
1194
+ // catch returns the friendly fallback to the user.
1195
+ gatewayProcessorLog.error(`processor failed: ${err?.message ?? String(err)}`, { channel: message.channel, channelId: message.channelId });
1196
+ throw err;
1197
+ }
1198
+ });
843
1199
  // Command registry.
844
1200
  const commandRegistry = new commandRegistry_1.CommandRegistry();
845
1201
  for (const cmd of commands_1.allCommands)
@@ -866,6 +1222,59 @@ async function buildAgentRuntime(cliOpts, opts) {
866
1222
  catch (err) {
867
1223
  display.dim(`(skill commands unavailable: ${err.message})`);
868
1224
  }
1225
+ // ── Phase v4.1-1.1: CLI-side channel manager ──────────────────────
1226
+ //
1227
+ // Build a manager local to this CLI process and register the env-driven
1228
+ // adapters that don't need an Express app (Telegram is the only one in
1229
+ // Phase 1 — Discord/Slack/etc. land iteratively). Webhook/Twilio stay
1230
+ // out of CLI scope because they need an HTTP listener.
1231
+ //
1232
+ // Conflict guard: when `aiden serve` is already running locally, BOTH
1233
+ // processes would race the same Telegram bot's long-poll, and the
1234
+ // server would lose every other update with a 409. Probe localhost:4200
1235
+ // briefly; if the server answers, skip auto-start in CLI but still
1236
+ // build the manager so /channel list / status work as a read-only view.
1237
+ //
1238
+ // Phase v4.1-1.3a — `bootLogger` + gateway logger were attached
1239
+ // earlier (right after the agent was constructed) so the gateway
1240
+ // processor closure could share the same scoped sink chain. Here
1241
+ // we just plumb the channels child logger into the manager.
1242
+ const channelManager = new manager_1.ChannelManager({ logger: bootLogger.child('channels') });
1243
+ // Phase v4.1-4.1 — wire active-model lookup into the Telegram
1244
+ // adapter. The closure captures `providerId` / `modelId` from
1245
+ // this scope so the photo-vision module can decide native vs
1246
+ // text routing (and pdf-extract can compute the truncation
1247
+ // budget) using the SAME model the chat path already uses.
1248
+ channelManager.register(new telegram_1.TelegramAdapter({
1249
+ activeModelInfo: () => ({
1250
+ providerId,
1251
+ modelId,
1252
+ contextWindow: (0, modelCatalog_1.findModel)(providerId, modelId)?.contextLength,
1253
+ }),
1254
+ }));
1255
+ let serverIsHosting = false;
1256
+ try {
1257
+ const ctrl = new AbortController();
1258
+ const timer = setTimeout(() => ctrl.abort(), 250);
1259
+ const probe = await fetch('http://127.0.0.1:4200/health', {
1260
+ signal: ctrl.signal,
1261
+ }).catch(() => null);
1262
+ clearTimeout(timer);
1263
+ if (probe && (probe.status >= 200 && probe.status < 500)) {
1264
+ serverIsHosting = true;
1265
+ }
1266
+ }
1267
+ catch {
1268
+ /* no server — fall through to CLI-host */
1269
+ }
1270
+ if (serverIsHosting) {
1271
+ display.dim('[channels] aiden serve is running locally — channel adapters hosted there, /channel commands stay read-only.');
1272
+ }
1273
+ else {
1274
+ // start() resolves quickly when no token is set (logs "Disabled" and
1275
+ // returns). Errors don't crash boot.
1276
+ channelManager.startAll().catch((e) => display.dim(`[channels] startAll error: ${e.message}`));
1277
+ }
869
1278
  return {
870
1279
  paths,
871
1280
  config,
@@ -900,6 +1309,7 @@ async function buildAgentRuntime(cliOpts, opts) {
900
1309
  personalityManager,
901
1310
  pluginLoader,
902
1311
  exploreMode,
1312
+ channelManager,
903
1313
  };
904
1314
  }
905
1315
  async function runInteractiveChat(cliOpts, opts) {
@@ -929,6 +1339,9 @@ async function runInteractiveChat(cliOpts, opts) {
929
1339
  // Phase 30.2.1 — boot card renders "model not configured" and
930
1340
  // chat attempts get the friendly NotConfiguredError message.
931
1341
  unconfigured: runtime.exploreMode,
1342
+ // Phase v4.1-1.1 — live ChannelManager so /channel commands can
1343
+ // list, add, remove, and inspect adapters without an external server.
1344
+ channelManager: runtime.channelManager,
932
1345
  };
933
1346
  if (cliOpts.tui) {
934
1347
  await (0, aidenTUI_1.runTuiMode)({
@@ -946,6 +1359,10 @@ async function runInteractiveChat(cliOpts, opts) {
946
1359
  // Phase 17 Task 5: fire onTeardown so plugins (e.g. CDP browser) can
947
1360
  // close their resources before the process exits.
948
1361
  await runtime.pluginLoader.teardown().catch(() => undefined);
1362
+ // Phase v4.1-1.1 — stop polling adapters before exit so Telegram's
1363
+ // long-poll TCP connection closes cleanly. stopAll() is best-effort
1364
+ // and never throws.
1365
+ await runtime.channelManager.stopAll().catch(() => undefined);
949
1366
  runtime.store.close?.();
950
1367
  }
951
1368
  // ─── setup ─────────────────────────────────────────────────────────────