aiden-runtime 4.0.2 → 4.1.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 (108) hide show
  1. package/README.md +11 -7
  2. package/config/hardware.json +2 -2
  3. package/dist/api/server.js +50 -52
  4. package/dist/cli/v4/aidenCLI.js +421 -5
  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 +41 -0
  28. package/dist/cli/v4/envSources.js +105 -0
  29. package/dist/cli/v4/ghostMatch.js +74 -0
  30. package/dist/cli/v4/historyStore.js +163 -0
  31. package/dist/cli/v4/pasteCompression.js +124 -0
  32. package/dist/cli/v4/pasteIntercept.js +203 -0
  33. package/dist/cli/v4/replyRenderer.js +209 -0
  34. package/dist/cli/v4/resizeGuard.js +92 -0
  35. package/dist/cli/v4/shellInterpolation.js +139 -0
  36. package/dist/cli/v4/skinEngine.js +21 -1
  37. package/dist/cli/v4/streamingPrefix.js +121 -0
  38. package/dist/cli/v4/syntaxHighlight.js +345 -0
  39. package/dist/cli/v4/table.js +216 -0
  40. package/dist/cli/v4/themeDetect.js +81 -0
  41. package/dist/cli/v4/uiBuild.js +74 -0
  42. package/dist/cli/v4/voiceCli.js +113 -0
  43. package/dist/cli/v4/voicePromptApi.js +196 -0
  44. package/dist/core/channels/discord.js +16 -10
  45. package/dist/core/channels/email.js +13 -9
  46. package/dist/core/channels/imessage.js +13 -9
  47. package/dist/core/channels/manager.js +25 -7
  48. package/dist/core/channels/pdf-extract.js +180 -0
  49. package/dist/core/channels/photo-vision.js +157 -0
  50. package/dist/core/channels/signal.js +11 -7
  51. package/dist/core/channels/slack.js +13 -10
  52. package/dist/core/channels/telegram-commands.js +154 -0
  53. package/dist/core/channels/telegram-groups.js +198 -0
  54. package/dist/core/channels/telegram-rate-limit.js +124 -0
  55. package/dist/core/channels/telegram.js +1980 -0
  56. package/dist/core/channels/twilio.js +11 -7
  57. package/dist/core/channels/webhook.js +9 -5
  58. package/dist/core/channels/whatsapp.js +15 -11
  59. package/dist/core/channels/whisper-transcribe.js +163 -0
  60. package/dist/core/cronManager.js +33 -294
  61. package/dist/core/gateway.js +29 -8
  62. package/dist/core/playwrightBridge.js +90 -0
  63. package/dist/core/v4/aidenAgent.js +35 -0
  64. package/dist/core/v4/auxiliaryClient.js +2 -2
  65. package/dist/core/v4/cron/atomicWrite.js +18 -4
  66. package/dist/core/v4/cron/cronExecute.js +300 -0
  67. package/dist/core/v4/cron/cronManager.js +502 -0
  68. package/dist/core/v4/cron/cronState.js +314 -0
  69. package/dist/core/v4/cron/cronTick.js +90 -0
  70. package/dist/core/v4/cron/diagnostics.js +104 -0
  71. package/dist/core/v4/cron/graceWindow.js +79 -0
  72. package/dist/core/v4/logger/factory.js +110 -0
  73. package/dist/core/v4/logger/index.js +22 -0
  74. package/dist/core/v4/logger/logger.js +101 -0
  75. package/dist/core/v4/logger/sinks/fileSink.js +110 -0
  76. package/dist/core/v4/logger/sinks/multiSink.js +43 -0
  77. package/dist/core/v4/logger/sinks/nullSink.js +53 -0
  78. package/dist/core/v4/logger/sinks/stdSink.js +81 -0
  79. package/dist/core/v4/mcp/server/diagnostics.js +40 -0
  80. package/dist/core/v4/mcp/server/skillBridge.js +94 -0
  81. package/dist/core/v4/mcp/server/stdioServer.js +119 -0
  82. package/dist/core/v4/mcp/server/toolBridge.js +168 -0
  83. package/dist/core/v4/platformPaths.js +105 -0
  84. package/dist/core/v4/providerFallback.js +25 -0
  85. package/dist/core/v4/skillLoader.js +21 -5
  86. package/dist/core/v4/skillMining/candidateStore.js +164 -0
  87. package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
  88. package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
  89. package/dist/core/v4/skillMining/skillMiner.js +191 -0
  90. package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
  91. package/dist/core/v4/subagent/budget.js +76 -0
  92. package/dist/core/v4/subagent/diagnostics.js +22 -0
  93. package/dist/core/v4/subagent/fanout.js +216 -0
  94. package/dist/core/v4/subagent/merger.js +148 -0
  95. package/dist/core/v4/subagent/providerRotation.js +54 -0
  96. package/dist/core/v4/voice/audioStream.js +373 -0
  97. package/dist/core/v4/voice/cliVoice.js +393 -0
  98. package/dist/core/v4/voice/diagnostics.js +66 -0
  99. package/dist/core/v4/voice/ttsStream.js +193 -0
  100. package/dist/core/version.js +1 -1
  101. package/dist/core/visionAnalyze.js +291 -90
  102. package/dist/core/voice/audio.js +61 -5
  103. package/dist/core/voice/audioBackend.js +134 -0
  104. package/dist/core/voice/stt.js +61 -6
  105. package/dist/core/voice/tts.js +19 -3
  106. package/dist/tools/v4/index.js +32 -1
  107. package/dist/tools/v4/subagent/subagentFanout.js +166 -0
  108. package/package.json +11 -2
@@ -5,15 +5,31 @@
5
5
  // ============================================================
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.channelManager = exports.ChannelManager = void 0;
8
- // ── ChannelManager ─────────────────────────────────────────
8
+ const logger_1 = require("../v4/logger");
9
9
  class ChannelManager {
10
- constructor() {
10
+ constructor(opts = {}) {
11
11
  this.adapters = new Map();
12
12
  this.lastActivity = new Map();
13
+ this.log = opts.logger ?? (0, logger_1.noopLogger)();
13
14
  }
14
- /** Register an adapter — must be called before startAll() */
15
+ /**
16
+ * Phase v4.1-1.3a — late-binding logger setter. Lets the singleton
17
+ * pick up a real logger after construction (api/server.ts boot path
18
+ * imports the singleton; we can't change its constructor without
19
+ * breaking every existing import site).
20
+ */
21
+ attachLogger(logger) {
22
+ this.log = logger;
23
+ }
24
+ /** Register an adapter — must be called before startAll(). */
15
25
  register(adapter) {
16
26
  this.adapters.set(adapter.name, adapter);
27
+ // Phase v4.1-1.3a — hand the adapter a scoped child logger so its
28
+ // own diagnostics route through the same sink chain as the manager.
29
+ // Adapters that haven't been migrated yet skip silently (no method).
30
+ if (typeof adapter.attachLogger === 'function') {
31
+ adapter.attachLogger(this.log.child(adapter.name));
32
+ }
17
33
  }
18
34
  /**
19
35
  * Start all registered adapters.
@@ -29,11 +45,13 @@ class ChannelManager {
29
45
  results.push({ name, status });
30
46
  }
31
47
  catch (error) {
32
- console.error(`[ChannelManager] ${name} failed to start:`, error.message);
48
+ this.log.error(`${name} failed to start: ${error.message}`);
33
49
  results.push({ name, status: 'failed', error: String(error.message) });
34
50
  }
35
51
  }
36
- // Summary line
52
+ // Summary line — single info record so log files capture the
53
+ // boot snapshot. CLI mode: routes to file only (REPL stays clean).
54
+ // Serve mode: routes to NDJSON stdout for log aggregators.
37
55
  const summary = results
38
56
  .map(r => {
39
57
  if (r.status === 'started')
@@ -44,7 +62,7 @@ class ChannelManager {
44
62
  })
45
63
  .join(' | ');
46
64
  if (results.length > 0)
47
- console.log(`[Channels] ${summary}`);
65
+ this.log.info(`startup: ${summary}`);
48
66
  return results;
49
67
  }
50
68
  /** Gracefully stop all adapters — called on SIGTERM / shutdown */
@@ -54,7 +72,7 @@ class ChannelManager {
54
72
  await adapter.stop();
55
73
  }
56
74
  catch (e) {
57
- console.error(`[ChannelManager] Error stopping ${adapter.name}:`, e.message);
75
+ this.log.error(`Error stopping ${adapter.name}: ${e.message}`);
58
76
  }
59
77
  }
60
78
  }
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // DevOS — Autonomous AI Execution System
4
+ // Copyright (c) 2026 Shiva Deore. All rights reserved.
5
+ // ============================================================
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.HARD_CHAR_CAP = exports.MAX_PDF_BYTES = void 0;
8
+ exports.extractPdfForChannel = extractPdfForChannel;
9
+ // core/channels/pdf-extract.ts — Phase v4.1-4.
10
+ //
11
+ // Channel-side adapter for inbound PDFs. Wraps the existing
12
+ // `core/fileIngestion.ts` `extractPDF` function with the policies
13
+ // the Telegram adapter cares about:
14
+ //
15
+ // 1. 20 MB pre-download size cap matching Telegram's documented
16
+ // Bot API getFile attachment limit. Refusing here keeps the
17
+ // polling loop from spending bandwidth on a payload Telegram
18
+ // would refuse to deliver anyway.
19
+ // 2. Token-budget truncation. The agent loop has to fit the PDF
20
+ // content INSIDE the user turn alongside system prompts, prior
21
+ // history, and a reserve for the response. We cap injected text
22
+ // at the lesser of:
23
+ // - 50,000 characters (hard ceiling — keeps pathological
24
+ // 200-page PDFs from blowing the budget on small models)
25
+ // - (modelContextWindow - 8K reserved-for-response) * 4
26
+ // chars/token (rough OpenAI heuristic, errs on the safe side)
27
+ // and report `{ truncated: true, originalChars }` so the channel
28
+ // adapter can append a "PDF truncated to fit context, original
29
+ // was N chars" note inside the agent annotation.
30
+ // 3. Result-shape contract that maps onto the bracketed user-turn
31
+ // annotation the adapter emits. Failures return `success:false`
32
+ // with a human-readable `error` so the agent gets a directive
33
+ // ("[transcription failed: …. Apologize and ask them to send a
34
+ // shorter file.]") instead of an empty message.
35
+ //
36
+ // Logger comes from the v4.1-1.3a contract; defaults to a noop sink.
37
+ const node_fs_1 = require("node:fs");
38
+ const fileIngestion_1 = require("../fileIngestion");
39
+ const logger_1 = require("../v4/logger");
40
+ // 20 MiB. Telegram's documented Bot API attachment download limit.
41
+ // Anything bigger would have been refused by getFile upstream, so
42
+ // we reject here without spending bandwidth on the round-trip.
43
+ exports.MAX_PDF_BYTES = 20 * 1024 * 1024;
44
+ /** Hard ceiling on injected PDF text — protects small-context models. */
45
+ exports.HARD_CHAR_CAP = 50000;
46
+ /** Reserved tokens for the agent's response when computing context budget. */
47
+ const RESPONSE_RESERVED_TOKENS = 8000;
48
+ /** Conservative chars-per-token estimate for budget math. */
49
+ const CHARS_PER_TOKEN = 4;
50
+ /**
51
+ * Extract a PDF and return text bounded by the channel-layer's
52
+ * truncation policy. Never throws — failures land on
53
+ * `success: false` with a human-readable `error`.
54
+ */
55
+ async function extractPdfForChannel(opts) {
56
+ const log = opts.logger ?? (0, logger_1.noopLogger)();
57
+ const cap = opts.maxBytesOverride ?? exports.MAX_PDF_BYTES;
58
+ // ── 1. Size precheck ────────────────────────────────────────────
59
+ let sizeBytes;
60
+ try {
61
+ const st = await node_fs_1.promises.stat(opts.filePath);
62
+ sizeBytes = st.size;
63
+ }
64
+ catch (e) {
65
+ log.warn('pdf file not readable', { path: opts.filePath, error: e?.message });
66
+ return { success: false, truncated: false, error: `pdf file not readable: ${e?.message ?? 'unknown error'}` };
67
+ }
68
+ if (sizeBytes > cap) {
69
+ log.warn('pdf file too large', { sizeBytes, cap });
70
+ return {
71
+ success: false,
72
+ truncated: false,
73
+ error: `PDF too large: ${(sizeBytes / (1024 * 1024)).toFixed(1)} MB ` +
74
+ `(cap is ${(cap / (1024 * 1024)).toFixed(0)} MB).`,
75
+ };
76
+ }
77
+ // ── 2. Hand off to the local extractor ──────────────────────────
78
+ const extractor = opts.extractFn ?? fileIngestion_1.extractPDF;
79
+ let extracted;
80
+ try {
81
+ extracted = await extractor(opts.filePath);
82
+ }
83
+ catch (e) {
84
+ log.error('pdf extraction threw', { error: e?.message ?? String(e) });
85
+ return {
86
+ success: false,
87
+ truncated: false,
88
+ error: `pdf extraction failed: ${e?.message ?? 'unknown error'}`,
89
+ };
90
+ }
91
+ const fullText = (extracted.text ?? '').trim();
92
+ if (!fullText) {
93
+ log.warn('pdf extracted empty text', { pageCount: extracted.pageCount });
94
+ return {
95
+ success: false,
96
+ truncated: false,
97
+ pageCount: extracted.pageCount,
98
+ error: 'pdf extraction returned empty text (scanned image PDF?)',
99
+ };
100
+ }
101
+ // ── 3. Truncation budget ────────────────────────────────────────
102
+ const charBudget = computeCharBudget(opts.modelContextWindow);
103
+ const originalChars = fullText.length;
104
+ if (originalChars <= charBudget) {
105
+ log.info('pdf extracted', {
106
+ pageCount: extracted.pageCount,
107
+ chars: originalChars,
108
+ truncated: false,
109
+ });
110
+ return {
111
+ success: true,
112
+ text: fullText,
113
+ truncated: false,
114
+ originalChars,
115
+ pageCount: extracted.pageCount,
116
+ wordCount: extracted.wordCount,
117
+ };
118
+ }
119
+ // Truncate to the budget — slice on a sentence/paragraph boundary
120
+ // when one's available within the last 1 KB of the budget so we
121
+ // don't mid-word the agent's view of the text.
122
+ const truncatedText = truncateOnBoundary(fullText, charBudget);
123
+ log.info('pdf extracted (truncated)', {
124
+ pageCount: extracted.pageCount,
125
+ originalChars,
126
+ finalChars: truncatedText.length,
127
+ budget: charBudget,
128
+ });
129
+ return {
130
+ success: true,
131
+ text: truncatedText,
132
+ truncated: true,
133
+ originalChars,
134
+ pageCount: extracted.pageCount,
135
+ wordCount: countWords(truncatedText),
136
+ };
137
+ }
138
+ /**
139
+ * Compute the truncation budget. When `modelContextWindow` is given,
140
+ * subtract 8K reserved-for-response tokens, multiply by 4 chars/token,
141
+ * and cap at the hard 50K ceiling. When not given, fall back to the
142
+ * hard ceiling. Always returns at least 4 KB so a tiny-context model
143
+ * doesn't end up with nothing.
144
+ */
145
+ function computeCharBudget(modelContextWindow) {
146
+ if (typeof modelContextWindow !== 'number' || !Number.isFinite(modelContextWindow) || modelContextWindow <= 0) {
147
+ return exports.HARD_CHAR_CAP;
148
+ }
149
+ const usableTokens = Math.max(0, modelContextWindow - RESPONSE_RESERVED_TOKENS);
150
+ const usableChars = usableTokens * CHARS_PER_TOKEN;
151
+ const budget = Math.min(exports.HARD_CHAR_CAP, usableChars);
152
+ return Math.max(budget, 4000);
153
+ }
154
+ /**
155
+ * Slice `text` at `cap` characters but try to land on the last newline
156
+ * in the trailing 1 KB of the cap, falling back to the last sentence
157
+ * terminator, finally a hard cut.
158
+ */
159
+ function truncateOnBoundary(text, cap) {
160
+ if (text.length <= cap)
161
+ return text;
162
+ const window = text.slice(0, cap);
163
+ const tailStart = Math.max(0, cap - 1024);
164
+ const lastNewline = window.lastIndexOf('\n', cap);
165
+ if (lastNewline >= tailStart)
166
+ return window.slice(0, lastNewline);
167
+ // Match common sentence terminators; English-centric but Devanagari
168
+ // and Latin punctuation both work since `.` is in the regex.
169
+ const terminator = window.slice(tailStart).search(/[.!?।]\s/);
170
+ if (terminator >= 0) {
171
+ const cutAt = tailStart + terminator + 1;
172
+ return window.slice(0, cutAt);
173
+ }
174
+ return window;
175
+ }
176
+ function countWords(text) {
177
+ if (!text)
178
+ return 0;
179
+ return text.trim().split(/\s+/).filter(Boolean).length;
180
+ }
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ // ============================================================
3
+ // DevOS — Autonomous AI Execution System
4
+ // Copyright (c) 2026 Shiva Deore. All rights reserved.
5
+ // ============================================================
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.MAX_PHOTO_BYTES = void 0;
8
+ exports.analyzePhotoForChannel = analyzePhotoForChannel;
9
+ // core/channels/photo-vision.ts — Phase v4.1-4.
10
+ //
11
+ // Channel-side adapter for inbound photos. Wraps the existing
12
+ // `core/visionAnalyze.ts` chain with Telegram-specific concerns:
13
+ //
14
+ // 1. 25 MB pre-download size cap. Mirrors the voice path's policy
15
+ // (Telegram Bot API getFile limit is the binding constraint).
16
+ // Refusing here saves the round-trip when we already know the
17
+ // payload is too big for the providers.
18
+ // 2. Mode decision — `native` vs `text` — based on whether the
19
+ // currently active model carries a `supportsVision: true` flag in
20
+ // `providers/v4/modelCatalog.ts`. When vision is supported, the
21
+ // channel adapter attaches the file path on the user turn so the
22
+ // provider sees pixels directly. Otherwise we pre-analyze with
23
+ // the auxiliary `analyzeImage` chain (Anthropic / OpenAI / Ollama
24
+ // llava) and prepend a description annotation — same "smuggle
25
+ // into agent turn" pattern as voice transcripts.
26
+ // 3. Result-shape contract that matches the channel adapter's
27
+ // expectations so the Telegram adapter's `handlePhotoMessage`
28
+ // can branch on `mode` and assemble the right outbound payload.
29
+ //
30
+ // Logger comes from the v4.1-1.3a contract; defaults to a noop sink
31
+ // so anything that calls into this module without a wired logger
32
+ // stays REPL-clean.
33
+ const node_fs_1 = require("node:fs");
34
+ const modelCatalog_1 = require("../../providers/v4/modelCatalog");
35
+ const visionAnalyze_1 = require("../visionAnalyze");
36
+ const logger_1 = require("../v4/logger");
37
+ // 25 MiB. Matches the voice cap and the OpenAI / Anthropic vision
38
+ // request-size envelopes; Telegram's getFile cap is 20 MB so a 25 MB
39
+ // payload would already have been rejected upstream — but keeping
40
+ // these caps consistent simplifies the operator mental model.
41
+ exports.MAX_PHOTO_BYTES = 25 * 1024 * 1024;
42
+ /**
43
+ * Default text-mode prompt. Single source so smokes and the adapter
44
+ * agree on what gets sent to the auxiliary vision chain. Phrased
45
+ * for the agent's perspective — the description ends up bracketed
46
+ * inside an "[The user sent an image. Description: ...]" annotation.
47
+ */
48
+ const DEFAULT_DESCRIBE_PROMPT = 'Describe everything visible in this image in detail. Include any ' +
49
+ 'text, code, layout, objects, people, colors, and any other notable ' +
50
+ 'visual information.';
51
+ /**
52
+ * Decide how an inbound photo should be presented to the model and
53
+ * (in text mode) generate the description the channel adapter will
54
+ * smuggle into the agent's user turn.
55
+ *
56
+ * Never throws — failures land on `success: false` with a
57
+ * human-readable `error`. Callers downstream decide whether to:
58
+ * - hand a `[The user sent an image but description failed: ...]`
59
+ * directive to the agent, or
60
+ * - render a friendly user-facing reject reply.
61
+ */
62
+ async function analyzePhotoForChannel(opts) {
63
+ const log = opts.logger ?? (0, logger_1.noopLogger)();
64
+ const cap = opts.maxBytesOverride ?? exports.MAX_PHOTO_BYTES;
65
+ // ── 1. Size precheck ────────────────────────────────────────────
66
+ let sizeBytes;
67
+ try {
68
+ const st = await node_fs_1.promises.stat(opts.filePath);
69
+ sizeBytes = st.size;
70
+ }
71
+ catch (e) {
72
+ log.warn('photo file not readable', { path: opts.filePath, error: e?.message });
73
+ return { success: false, mode: 'text', error: `photo file not readable: ${e?.message ?? 'unknown error'}` };
74
+ }
75
+ if (sizeBytes > cap) {
76
+ log.warn('photo file too large', { sizeBytes, cap });
77
+ return {
78
+ success: false,
79
+ mode: 'text',
80
+ error: `Photo too large: ${(sizeBytes / (1024 * 1024)).toFixed(1)} MB ` +
81
+ `(cap is ${(cap / (1024 * 1024)).toFixed(0)} MB).`,
82
+ };
83
+ }
84
+ // ── 2. Mode decision ────────────────────────────────────────────
85
+ const mode = decideMode(opts.providerId, opts.modelId, log);
86
+ if (mode === 'native') {
87
+ log.info('photo routed native', {
88
+ providerId: opts.providerId,
89
+ modelId: opts.modelId,
90
+ sizeBytes,
91
+ });
92
+ return { success: true, mode: 'native', nativePath: opts.filePath };
93
+ }
94
+ // ── 3. Text mode: pre-analyze via the auxiliary vision chain ────
95
+ const analyze = opts.analyzeFn ?? visionAnalyze_1.analyzeImage;
96
+ try {
97
+ const visionResult = await analyze(opts.filePath, opts.prompt ?? DEFAULT_DESCRIBE_PROMPT, log);
98
+ const description = (visionResult.description ?? '').trim();
99
+ if (!description) {
100
+ log.warn('vision chain returned empty description', { provider: visionResult.provider });
101
+ return {
102
+ success: false,
103
+ mode: 'text',
104
+ error: 'vision chain returned an empty description',
105
+ provider: visionResult.provider,
106
+ modelUsed: visionResult.modelUsed,
107
+ durationMs: visionResult.durationMs,
108
+ };
109
+ }
110
+ return {
111
+ success: true,
112
+ mode: 'text',
113
+ description,
114
+ provider: visionResult.provider,
115
+ modelUsed: visionResult.modelUsed,
116
+ durationMs: visionResult.durationMs,
117
+ };
118
+ }
119
+ catch (e) {
120
+ log.error('vision chain threw', { error: e?.message ?? String(e) });
121
+ return {
122
+ success: false,
123
+ mode: 'text',
124
+ error: `vision chain failed: ${e?.message ?? 'unknown error'}`,
125
+ };
126
+ }
127
+ }
128
+ /**
129
+ * Resolve native-vs-text routing for the active model. Returns
130
+ * `'text'` whenever:
131
+ * - either id is missing (caller didn't tell us)
132
+ * - the model isn't in `MODEL_CATALOG` (registry drift)
133
+ * - the model's `supportsVision` is false
134
+ * - the lookup throws for any reason
135
+ *
136
+ * `'text'` is the safe fallback — it always works because the
137
+ * auxiliary `analyzeImage` chain runs against its own provider keys
138
+ * independent of whatever the user has selected for the agent loop.
139
+ */
140
+ function decideMode(providerId, modelId, log) {
141
+ if (!providerId || !modelId) {
142
+ log.debug('photo mode: missing provider/model id, defaulting to text');
143
+ return 'text';
144
+ }
145
+ try {
146
+ const entry = (0, modelCatalog_1.findModel)(providerId, modelId);
147
+ if (!entry) {
148
+ log.debug('photo mode: model not in catalog, defaulting to text', { providerId, modelId });
149
+ return 'text';
150
+ }
151
+ return entry.supportsVision ? 'native' : 'text';
152
+ }
153
+ catch (e) {
154
+ log.debug('photo mode: lookup threw, defaulting to text', { error: e?.message });
155
+ return 'text';
156
+ }
157
+ }
@@ -25,9 +25,12 @@ exports.SignalAdapter = void 0;
25
25
  // SIGNAL_ALLOWED_NUMBERS — optional comma-separated allowlist
26
26
  const axios_1 = __importDefault(require("axios"));
27
27
  const gateway_1 = require("../gateway");
28
+ const logger_1 = require("../v4/logger");
28
29
  class SignalAdapter {
29
30
  constructor() {
30
31
  this.name = 'signal';
32
+ // Phase v4.1-1.3a — diagnostics route through scope logger.
33
+ this.log = (0, logger_1.noopLogger)();
31
34
  this.healthy = false;
32
35
  this.pollTimer = null;
33
36
  this.lastReceived = 0;
@@ -36,21 +39,22 @@ class SignalAdapter {
36
39
  const raw = process.env.SIGNAL_ALLOWED_NUMBERS ?? '';
37
40
  this.allowedNumbers = raw ? new Set(raw.split(',').map(s => s.trim()).filter(Boolean)) : new Set();
38
41
  }
42
+ attachLogger(logger) { this.log = logger; }
39
43
  // ── Lifecycle ──────────────────────────────────────────────
40
44
  async start() {
41
45
  if (!this.myNumber) {
42
- console.log('[Signal] Disabled — set SIGNAL_PHONE_NUMBER to enable');
46
+ this.log.info('Disabled — set SIGNAL_PHONE_NUMBER to enable');
43
47
  return;
44
48
  }
45
49
  // Verify signal-cli is reachable
46
50
  const reachable = await this.checkHealth();
47
51
  if (!reachable) {
48
- console.log(`[Signal] Disabled — signal-cli-rest-api not reachable at ${this.baseUrl}`);
52
+ this.log.info('Disabled — signal-cli-rest-api not reachable at ${this.baseUrl}');
49
53
  return;
50
54
  }
51
55
  this.healthy = true;
52
56
  this.lastReceived = Date.now();
53
- console.log(`[Signal] Connected — polling ${this.baseUrl}`);
57
+ this.log.info('Connected — polling ${this.baseUrl}');
54
58
  // Register outbound delivery
55
59
  gateway_1.gateway.registerChannel('signal', async (msg) => {
56
60
  await this.send(msg.channelId, msg.text);
@@ -66,7 +70,7 @@ class SignalAdapter {
66
70
  this.pollTimer = null;
67
71
  }
68
72
  gateway_1.gateway.unregisterChannel('signal');
69
- console.log('[Signal] Disconnected');
73
+ this.log.info('Disconnected');
70
74
  }
71
75
  async send(target, message) {
72
76
  if (!this.healthy)
@@ -75,7 +79,7 @@ class SignalAdapter {
75
79
  await axios_1.default.post(`${this.baseUrl}/v2/send`, { message, number: this.myNumber, recipients: [target] }, { timeout: 10000 });
76
80
  }
77
81
  catch (e) {
78
- console.error('[Signal] send error:', e.message);
82
+ this.log.error(`send error:${e.message}`);
79
83
  }
80
84
  }
81
85
  isHealthy() { return this.healthy; }
@@ -112,7 +116,7 @@ class SignalAdapter {
112
116
  catch (e) {
113
117
  // Don't spam logs on transient poll errors
114
118
  if (this.healthy) {
115
- console.error('[Signal] poll error:', e.message);
119
+ this.log.error(`poll error:${e.message}`);
116
120
  }
117
121
  }
118
122
  }
@@ -132,7 +136,7 @@ class SignalAdapter {
132
136
  });
133
137
  }
134
138
  catch (e) {
135
- console.error('[Signal] routeMessage error:', e.message);
139
+ this.log.error(`routeMessage error:${e.message}`);
136
140
  return '❌ Something went wrong. Try again.';
137
141
  }
138
142
  }
@@ -27,11 +27,13 @@ exports.SlackAdapter = void 0;
27
27
  // - Graceful degradation: missing creds → disabled, no crash
28
28
  const bolt_1 = require("@slack/bolt");
29
29
  const gateway_1 = require("../gateway");
30
+ const logger_1 = require("../v4/logger");
30
31
  class SlackAdapter {
31
32
  constructor() {
32
33
  this.name = 'slack';
33
34
  this.app = null;
34
35
  this.healthy = false;
36
+ this.log = (0, logger_1.noopLogger)(); // Phase v4.1-1.3a — wired by ChannelManager.register
35
37
  this.botToken = process.env.SLACK_BOT_TOKEN ?? '';
36
38
  this.signingSecret = process.env.SLACK_SIGNING_SECRET ?? '';
37
39
  this.appToken = process.env.SLACK_APP_TOKEN ?? '';
@@ -40,10 +42,11 @@ class SlackAdapter {
40
42
  ? new Set(rawChannels.split(',').map(s => s.trim()).filter(Boolean))
41
43
  : new Set();
42
44
  }
45
+ attachLogger(logger) { this.log = logger; }
43
46
  // ── Lifecycle ──────────────────────────────────────────────
44
47
  async start() {
45
48
  if (!this.botToken || !this.signingSecret) {
46
- console.log('[Slack] Disabled — set SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET to enable');
49
+ this.log.info('Disabled — set SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET to enable');
47
50
  return;
48
51
  }
49
52
  const useSocketMode = !!this.appToken;
@@ -63,7 +66,7 @@ class SlackAdapter {
63
66
  if (this.allowedChannels.size > 0 && !this.allowedChannels.has(msg.channel))
64
67
  return;
65
68
  const response = await this.processMessage(msg.channel, msg.user ?? 'unknown', msg.text ?? '');
66
- await say({ text: response, thread_ts: msg.ts }).catch((e) => console.error('[Slack] say error:', e.message));
69
+ await say({ text: response, thread_ts: msg.ts }).catch((e) => this.log.error(`say error: ${e.message}`));
67
70
  });
68
71
  // ── App mentions (@Aiden) ────────────────────────────────
69
72
  this.app.event('app_mention', async ({ event, say }) => {
@@ -72,7 +75,7 @@ class SlackAdapter {
72
75
  // Strip the @mention prefix from the message
73
76
  const text = (event.text ?? '').replace(/<@[^>]+>/g, '').trim();
74
77
  const response = await this.processMessage(event.channel, event.user, text);
75
- await say({ text: response, thread_ts: event.ts }).catch((e) => console.error('[Slack] mention reply error:', e.message));
78
+ await say({ text: response, thread_ts: event.ts }).catch((e) => this.log.error(`mention reply error: ${e.message}`));
76
79
  });
77
80
  // ── Slash command /aiden ─────────────────────────────────
78
81
  this.app.command('/aiden', async ({ command, ack, say }) => {
@@ -82,7 +85,7 @@ class SlackAdapter {
82
85
  return;
83
86
  }
84
87
  const response = await this.processMessage(command.channel_id, command.user_id, command.text);
85
- await say(response).catch((e) => console.error('[Slack] slash reply error:', e.message));
88
+ await say(response).catch((e) => this.log.error(`slash reply error: ${e.message}`));
86
89
  });
87
90
  // Register outbound delivery so gateway.deliver() and broadcast() work
88
91
  gateway_1.gateway.registerChannel('slack', async (msg) => {
@@ -91,7 +94,7 @@ class SlackAdapter {
91
94
  return true;
92
95
  }
93
96
  catch (e) {
94
- console.error('[Slack] Delivery error:', e.message);
97
+ this.log.error(`Delivery error: ${e.message}`);
95
98
  return false;
96
99
  }
97
100
  });
@@ -99,10 +102,10 @@ class SlackAdapter {
99
102
  try {
100
103
  await this.app.start(port);
101
104
  this.healthy = true;
102
- console.log(`[Slack] Connected (${useSocketMode ? 'socket mode' : `HTTP mode port ${port}`})`);
105
+ this.log.info(`Connected (${useSocketMode ? 'socket mode' : `HTTP mode port ${port}`})`);
103
106
  }
104
107
  catch (e) {
105
- console.error('[Slack] Start failed:', e.message);
108
+ this.log.error(`Start failed: ${e.message}`);
106
109
  this.healthy = false;
107
110
  }
108
111
  }
@@ -113,10 +116,10 @@ class SlackAdapter {
113
116
  await this.app.stop().catch(() => { });
114
117
  this.app = null;
115
118
  }
116
- console.log('[Slack] Disconnected');
119
+ this.log.info('Disconnected');
117
120
  }
118
121
  async send(channelId, message) {
119
- await this.app?.client.chat.postMessage({ channel: channelId, text: message }).catch((e) => console.error('[Slack] send error:', e.message));
122
+ await this.app?.client.chat.postMessage({ channel: channelId, text: message }).catch((e) => this.log.error(`send error: ${e.message}`));
120
123
  }
121
124
  isHealthy() { return this.healthy; }
122
125
  // ── Helpers ────────────────────────────────────────────────
@@ -131,7 +134,7 @@ class SlackAdapter {
131
134
  });
132
135
  }
133
136
  catch (e) {
134
- console.error('[Slack] routeMessage error:', e.message);
137
+ this.log.error(`routeMessage error: ${e.message}`);
135
138
  return '❌ Something went wrong. Try again.';
136
139
  }
137
140
  }