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
@@ -20,21 +20,27 @@ exports.DiscordAdapter = void 0;
20
20
  // - Graceful degradation: missing token → disabled, no crash
21
21
  const discord_js_1 = require("discord.js");
22
22
  const gateway_1 = require("../gateway");
23
+ const logger_1 = require("../v4/logger");
23
24
  class DiscordAdapter {
24
25
  constructor() {
25
26
  this.name = 'discord';
26
27
  this.client = null;
27
28
  this.healthy = false;
29
+ // Phase v4.1-1.3a — diagnostics route through the channel scope
30
+ // logger; ChannelManager.register injects it. Default noop keeps
31
+ // pre-attach calls silent.
32
+ this.log = (0, logger_1.noopLogger)();
28
33
  this.token = process.env.DISCORD_BOT_TOKEN ?? '';
29
34
  const rawGuilds = process.env.DISCORD_ALLOWED_GUILDS ?? '';
30
35
  const rawChannels = process.env.DISCORD_ALLOWED_CHANNELS ?? '';
31
36
  this.allowedGuilds = rawGuilds ? new Set(rawGuilds.split(',').map(s => s.trim()).filter(Boolean)) : new Set();
32
37
  this.allowedChannels = rawChannels ? new Set(rawChannels.split(',').map(s => s.trim()).filter(Boolean)) : new Set();
33
38
  }
39
+ attachLogger(logger) { this.log = logger; }
34
40
  // ── Lifecycle ──────────────────────────────────────────────
35
41
  async start() {
36
42
  if (!this.token) {
37
- console.log('[Discord] Disabled — set DISCORD_BOT_TOKEN to enable');
43
+ this.log.info('Disabled — set DISCORD_BOT_TOKEN to enable');
38
44
  return;
39
45
  }
40
46
  this.client = new discord_js_1.Client({
@@ -46,14 +52,14 @@ class DiscordAdapter {
46
52
  ],
47
53
  });
48
54
  this.client.once(discord_js_1.Events.ClientReady, async (c) => {
49
- console.log(`[Discord] Connected as ${c.user.tag}`);
55
+ this.log.info(`Connected as ${c.user.tag}`);
50
56
  this.healthy = true;
51
57
  // Register outbound delivery so gateway.deliver() and broadcast() work
52
58
  gateway_1.gateway.registerChannel('discord', async (msg) => {
53
59
  return this.deliverToChannel(msg.channelId, msg.text);
54
60
  });
55
61
  // Register slash commands globally (takes ~1h to propagate on first run)
56
- await this.registerSlashCommands(c.user.id).catch((e) => console.warn('[Discord] Slash command registration failed:', e.message));
62
+ await this.registerSlashCommands(c.user.id).catch((e) => this.log.warn(`Slash command registration failed: ${e.message}`));
57
63
  });
58
64
  this.client.on(discord_js_1.Events.MessageCreate, async (message) => {
59
65
  if (!this.shouldHandle(message.author.id, message.guildId, message.channelId, message.author.bot))
@@ -63,7 +69,7 @@ class DiscordAdapter {
63
69
  }
64
70
  catch { }
65
71
  const response = await this.processMessage(message.channelId, message.author.id, message.content);
66
- await message.reply(response.substring(0, 2000)).catch((e) => console.error('[Discord] Reply error:', e.message));
72
+ await message.reply(response.substring(0, 2000)).catch((e) => this.log.error(`Reply error: ${e.message}`));
67
73
  });
68
74
  this.client.on(discord_js_1.Events.InteractionCreate, async (interaction) => {
69
75
  if (!interaction.isChatInputCommand())
@@ -84,7 +90,7 @@ class DiscordAdapter {
84
90
  const prompt = interaction.options.getString('prompt', true);
85
91
  await interaction.deferReply();
86
92
  const response = await this.processMessage(channelId, userId, prompt);
87
- await interaction.editReply(response.substring(0, 2000)).catch((e) => console.error('[Discord] editReply error:', e.message));
93
+ await interaction.editReply(response.substring(0, 2000)).catch((e) => this.log.error(`editReply error: ${e.message}`));
88
94
  }
89
95
  else if (interaction.commandName === 'aiden-help') {
90
96
  await interaction.reply({
@@ -97,7 +103,7 @@ class DiscordAdapter {
97
103
  await this.client.login(this.token);
98
104
  }
99
105
  catch (e) {
100
- console.error('[Discord] Login failed:', e.message);
106
+ this.log.error(`Login failed: ${e.message}`);
101
107
  this.healthy = false;
102
108
  }
103
109
  }
@@ -108,7 +114,7 @@ class DiscordAdapter {
108
114
  await this.client.destroy();
109
115
  this.client = null;
110
116
  }
111
- console.log('[Discord] Disconnected');
117
+ this.log.info('Disconnected');
112
118
  }
113
119
  async send(channelId, message) {
114
120
  await this.deliverToChannel(channelId, message);
@@ -135,7 +141,7 @@ class DiscordAdapter {
135
141
  });
136
142
  }
137
143
  catch (e) {
138
- console.error('[Discord] routeMessage error:', e.message);
144
+ this.log.error(`routeMessage error: ${e.message}`);
139
145
  return '❌ Something went wrong. Try again.';
140
146
  }
141
147
  }
@@ -149,7 +155,7 @@ class DiscordAdapter {
149
155
  return false;
150
156
  }
151
157
  catch (e) {
152
- console.error('[Discord] Delivery error:', e.message);
158
+ this.log.error(`Delivery error: ${e.message}`);
153
159
  return false;
154
160
  }
155
161
  }
@@ -167,7 +173,7 @@ class DiscordAdapter {
167
173
  .toJSON(),
168
174
  ];
169
175
  await rest.put(discord_js_1.Routes.applicationCommands(appId), { body: commands });
170
- console.log('[Discord] Slash commands registered globally');
176
+ this.log.info('Slash commands registered globally');
171
177
  }
172
178
  }
173
179
  exports.DiscordAdapter = DiscordAdapter;
@@ -33,9 +33,12 @@ exports.EmailAdapter = void 0;
33
33
  // EMAIL_POLL_INTERVAL — polling interval in seconds (default 60)
34
34
  const nodemailer_1 = __importDefault(require("nodemailer"));
35
35
  const gateway_1 = require("../gateway");
36
+ const logger_1 = require("../v4/logger");
36
37
  class EmailAdapter {
37
38
  constructor() {
38
39
  this.name = 'email';
40
+ // Phase v4.1-1.3a — diagnostics route through scope logger.
41
+ this.log = (0, logger_1.noopLogger)();
39
42
  this.healthy = false;
40
43
  this.pollTimer = null;
41
44
  this.processedIds = new Set();
@@ -52,14 +55,15 @@ class EmailAdapter {
52
55
  this.allowedSenders = raw ? new Set(raw.split(',').map(s => s.trim().toLowerCase()).filter(Boolean)) : new Set();
53
56
  this.pollIntervalMs = parseInt(process.env.EMAIL_POLL_INTERVAL ?? '60', 10) * 1000;
54
57
  }
58
+ attachLogger(logger) { this.log = logger; }
55
59
  // ── Lifecycle ──────────────────────────────────────────────
56
60
  async start() {
57
61
  if (!this.imapHost || !this.imapUser || !this.imapPassword) {
58
- console.log('[Email] Disabled — set EMAIL_IMAP_HOST, EMAIL_IMAP_USER, EMAIL_IMAP_PASSWORD to enable');
62
+ this.log.info('Disabled — set EMAIL_IMAP_HOST, EMAIL_IMAP_USER, EMAIL_IMAP_PASSWORD to enable');
59
63
  return;
60
64
  }
61
65
  if (!this.smtpHost || !this.smtpUser || !this.smtpPassword) {
62
- console.log('[Email] Disabled — set EMAIL_SMTP_HOST, EMAIL_SMTP_USER, EMAIL_SMTP_PASSWORD to enable');
66
+ this.log.info('Disabled — set EMAIL_SMTP_HOST, EMAIL_SMTP_USER, EMAIL_SMTP_PASSWORD to enable');
63
67
  return;
64
68
  }
65
69
  // Set up SMTP transporter
@@ -75,11 +79,11 @@ class EmailAdapter {
75
79
  // Verify SMTP connection
76
80
  const smtpOk = await this.transporter.verify().then(() => true).catch(() => false);
77
81
  if (!smtpOk) {
78
- console.log('[Email] Disabled — SMTP connection failed. Check EMAIL_SMTP_* settings.');
82
+ this.log.info('Disabled — SMTP connection failed. Check EMAIL_SMTP_* settings.');
79
83
  return;
80
84
  }
81
85
  this.healthy = true;
82
- console.log(`[Email] Ready — polling ${this.imapUser} every ${this.pollIntervalMs / 1000}s`);
86
+ this.log.info('Ready — polling ${this.imapUser} every ${this.pollIntervalMs / 1000}s');
83
87
  // Register outbound delivery
84
88
  gateway_1.gateway.registerChannel('email', async (msg) => {
85
89
  await this.send(msg.channelId, msg.text);
@@ -97,7 +101,7 @@ class EmailAdapter {
97
101
  }
98
102
  this.transporter = null;
99
103
  gateway_1.gateway.unregisterChannel('email');
100
- console.log('[Email] Disconnected');
104
+ this.log.info('Disconnected');
101
105
  }
102
106
  async send(target, message) {
103
107
  if (!this.transporter)
@@ -112,7 +116,7 @@ class EmailAdapter {
112
116
  });
113
117
  }
114
118
  catch (e) {
115
- console.error('[Email] send error:', e.message);
119
+ this.log.error(`send error:${e.message}`);
116
120
  }
117
121
  }
118
122
  isHealthy() { return this.healthy; }
@@ -191,7 +195,7 @@ class EmailAdapter {
191
195
  }
192
196
  catch (e) {
193
197
  if (this.healthy) {
194
- console.error('[Email] poll error:', e.message);
198
+ this.log.error(`poll error:${e.message}`);
195
199
  }
196
200
  }
197
201
  finally {
@@ -217,7 +221,7 @@ class EmailAdapter {
217
221
  });
218
222
  }
219
223
  catch (e) {
220
- console.error('[Email] reply error:', e.message);
224
+ this.log.error(`reply error:${e.message}`);
221
225
  }
222
226
  }
223
227
  extractText(raw) {
@@ -245,7 +249,7 @@ class EmailAdapter {
245
249
  });
246
250
  }
247
251
  catch (e) {
248
- console.error('[Email] routeMessage error:', e.message);
252
+ this.log.error(`routeMessage error:${e.message}`);
249
253
  return 'Something went wrong processing your email. Please try again.';
250
254
  }
251
255
  }
@@ -26,9 +26,12 @@ exports.IMessageAdapter = void 0;
26
26
  const axios_1 = __importDefault(require("axios"));
27
27
  const ws_1 = require("ws");
28
28
  const gateway_1 = require("../gateway");
29
+ const logger_1 = require("../v4/logger");
29
30
  class IMessageAdapter {
30
31
  constructor() {
31
32
  this.name = 'imessage';
33
+ // Phase v4.1-1.3a — diagnostics route through scope logger.
34
+ this.log = (0, logger_1.noopLogger)();
32
35
  this.healthy = false;
33
36
  this.ws = null;
34
37
  this.reconnectTimer = null;
@@ -37,20 +40,21 @@ class IMessageAdapter {
37
40
  const raw = process.env.BLUEBUBBLES_ALLOWED_NUMBERS ?? '';
38
41
  this.allowedNumbers = raw ? new Set(raw.split(',').map(s => s.trim()).filter(Boolean)) : new Set();
39
42
  }
43
+ attachLogger(logger) { this.log = logger; }
40
44
  // ── Lifecycle ──────────────────────────────────────────────
41
45
  async start() {
42
46
  if (!this.baseUrl || !this.password) {
43
- console.log('[iMessage] Disabled — set BLUEBUBBLES_URL and BLUEBUBBLES_PASSWORD to enable');
47
+ this.log.info('Disabled — set BLUEBUBBLES_URL and BLUEBUBBLES_PASSWORD to enable');
44
48
  return;
45
49
  }
46
50
  // Verify BlueBubbles is reachable
47
51
  const reachable = await this.checkHealth();
48
52
  if (!reachable) {
49
- console.log(`[iMessage] Disabled — BlueBubbles server not reachable at ${this.baseUrl}`);
53
+ this.log.info('Disabled — BlueBubbles server not reachable at ${this.baseUrl}');
50
54
  return;
51
55
  }
52
56
  this.healthy = true;
53
- console.log(`[iMessage] Connected to BlueBubbles at ${this.baseUrl}`);
57
+ this.log.info('Connected to BlueBubbles at ${this.baseUrl}');
54
58
  // Register outbound delivery
55
59
  gateway_1.gateway.registerChannel('imessage', async (msg) => {
56
60
  await this.send(msg.channelId, msg.text);
@@ -70,7 +74,7 @@ class IMessageAdapter {
70
74
  this.ws = null;
71
75
  }
72
76
  gateway_1.gateway.unregisterChannel('imessage');
73
- console.log('[iMessage] Disconnected');
77
+ this.log.info('Disconnected');
74
78
  }
75
79
  async send(target, message) {
76
80
  if (!this.healthy)
@@ -82,7 +86,7 @@ class IMessageAdapter {
82
86
  });
83
87
  }
84
88
  catch (e) {
85
- console.error('[iMessage] send error:', e.message);
89
+ this.log.error(`send error:${e.message}`);
86
90
  }
87
91
  }
88
92
  isHealthy() { return this.healthy; }
@@ -105,7 +109,7 @@ class IMessageAdapter {
105
109
  const wsUrl = this.baseUrl.replace(/^http/, 'ws');
106
110
  this.ws = new ws_1.WebSocket(`${wsUrl}?password=${encodeURIComponent(this.password)}`);
107
111
  this.ws.on('open', () => {
108
- console.log('[iMessage] WebSocket connected');
112
+ this.log.info('WebSocket connected');
109
113
  });
110
114
  this.ws.on('message', async (raw) => {
111
115
  try {
@@ -127,11 +131,11 @@ class IMessageAdapter {
127
131
  await this.send(chatId || sender, response);
128
132
  }
129
133
  catch (e) {
130
- console.error('[iMessage] message parse error:', e.message);
134
+ this.log.error(`message parse error:${e.message}`);
131
135
  }
132
136
  });
133
137
  this.ws.on('error', (e) => {
134
- console.error('[iMessage] WebSocket error:', e.message);
138
+ this.log.error(`WebSocket error:${e.message}`);
135
139
  });
136
140
  this.ws.on('close', () => {
137
141
  if (this.healthy) {
@@ -156,7 +160,7 @@ class IMessageAdapter {
156
160
  });
157
161
  }
158
162
  catch (e) {
159
- console.error('[iMessage] routeMessage error:', e.message);
163
+ this.log.error(`routeMessage error:${e.message}`);
160
164
  return '❌ Something went wrong. Try again.';
161
165
  }
162
166
  }
@@ -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
+ }