@hailer/mcp 1.3.10 → 1.3.12

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 (39) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/bot/bot.d.ts +10 -0
  3. package/dist/bot/bot.d.ts.map +1 -1
  4. package/dist/bot/bot.js +189 -17
  5. package/dist/bot/bot.js.map +1 -1
  6. package/dist/bot/services/helper-prompt.d.ts +1 -1
  7. package/dist/bot/services/helper-prompt.d.ts.map +1 -1
  8. package/dist/bot/services/helper-prompt.js +60 -5
  9. package/dist/bot/services/helper-prompt.js.map +1 -1
  10. package/dist/bot/services/system-prompt.d.ts.map +1 -1
  11. package/dist/bot/services/system-prompt.js +13 -2
  12. package/dist/bot/services/system-prompt.js.map +1 -1
  13. package/dist/lib/logger.js +2 -2
  14. package/dist/lib/logger.js.map +1 -1
  15. package/dist/mcp/publish-auth-injector.d.ts +37 -9
  16. package/dist/mcp/publish-auth-injector.d.ts.map +1 -1
  17. package/dist/mcp/publish-auth-injector.js +77 -26
  18. package/dist/mcp/publish-auth-injector.js.map +1 -1
  19. package/dist/mcp/tool-profiles.d.ts.map +1 -1
  20. package/dist/mcp/tool-profiles.js +1 -0
  21. package/dist/mcp/tool-profiles.js.map +1 -1
  22. package/dist/mcp/tools/app-marketplace.d.ts.map +1 -1
  23. package/dist/mcp/tools/app-marketplace.js +78 -41
  24. package/dist/mcp/tools/app-marketplace.js.map +1 -1
  25. package/dist/mcp/tools/workflow.d.ts +1 -0
  26. package/dist/mcp/tools/workflow.d.ts.map +1 -1
  27. package/dist/mcp/tools/workflow.js +57 -1
  28. package/dist/mcp/tools/workflow.js.map +1 -1
  29. package/dist/public-chat/graduate.d.ts.map +1 -1
  30. package/dist/public-chat/graduate.js +175 -49
  31. package/dist/public-chat/graduate.js.map +1 -1
  32. package/dist/public-chat/rate-limit.js +1 -1
  33. package/dist/public-chat/rate-limit.js.map +1 -1
  34. package/dist/public-chat/studio-prewarm.js +2 -2
  35. package/dist/public-chat/studio-prewarm.js.map +1 -1
  36. package/dist/public-chat/system-prompt.d.ts.map +1 -1
  37. package/dist/public-chat/system-prompt.js +511 -286
  38. package/dist/public-chat/system-prompt.js.map +1 -1
  39. package/package.json +14 -12
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.3.11] - 22-06-2026
8
+
9
+ ### Fix
10
+
11
+ **Workspace-aware local publish-auth injection**
12
+
13
+ `npx hailer-mcp` mints a user API key into the machine-global `~/.env` so app
14
+ publishing works flag-free. It previously skipped whenever *any* key was already
15
+ present, so the first project to boot claimed `~/.env` permanently — every other
16
+ project then published silently into the first project's workspace.
17
+
18
+ The injector now records the workspace in the `~/.env` marker and re-mints when
19
+ the booting project's workspace differs (last server to boot owns `~/.env`).
20
+ A key the user set themselves — exported, or hand-added without our marker — is
21
+ left untouched, and pre-1.3.11 marker blocks auto-upgrade on next boot.
22
+
7
23
  ## [1.1.7] - 18-02-2026
8
24
 
9
25
  ### Fix
package/dist/bot/bot.d.ts CHANGED
@@ -57,6 +57,14 @@ export declare class Bot {
57
57
  private hailerApi;
58
58
  private opLogger;
59
59
  private cachedTools;
60
+ /**
61
+ * Per-turn tool result dedup cache for helperMode.
62
+ * Key includes senderId to prevent cross-user permission leaks.
63
+ * Cleared on each new user message AND when any write tool executes.
64
+ */
65
+ private toolDedupCache;
66
+ private static readonly DEDUP_TOOLS;
67
+ private static readonly WRITE_TOOLS_FOR_CACHE;
60
68
  private signalHandler;
61
69
  private processUpdatedHandler;
62
70
  private workspaceUpdatedHandler;
@@ -118,6 +126,8 @@ export declare class Bot {
118
126
  private handleSignal;
119
127
  private processDiscussion;
120
128
  private processMessage;
129
+ /** Short continuation messages used in helperMode onboarding (numbers, "ok", "next", etc.) */
130
+ private static readonly HELPER_TRIVIAL;
121
131
  private routeMessage;
122
132
  private getRecentContext;
123
133
  private runLlmLoop;
@@ -1 +1 @@
1
- {"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../src/bot/bot.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../src/bot/bot.ts"],"names":[],"mappings":"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"}
package/dist/bot/bot.js CHANGED
@@ -75,7 +75,7 @@ const config_1 = require("../config");
75
75
  */
76
76
  const SEED_TURNS_MAX = 24;
77
77
  function parseSeedTurns(raw, log) {
78
- const empty = { turns: [], viewedSlides: [] };
78
+ const empty = { turns: [], viewedSlides: [], templateIds: [], templateNames: [] };
79
79
  if (!raw) {
80
80
  return empty;
81
81
  }
@@ -89,23 +89,48 @@ function parseSeedTurns(raw, log) {
89
89
  });
90
90
  return empty;
91
91
  }
92
- // Two supported shapes:
92
+ // Three supported shapes:
93
93
  // - bare array of {role, content} turns (legacy)
94
94
  // - object { turns: [...], viewedSlides: [...] } (current)
95
+ // - object { summary: "...", viewedSlides: [...], templateIds: [...], templateNames: [...] } (summarized handoff)
95
96
  let turnSource;
96
97
  let viewedSlides = [];
98
+ let templateIds = [];
99
+ let templateNames = [];
97
100
  if (Array.isArray(parsed)) {
98
101
  turnSource = parsed;
99
102
  }
100
103
  else if (parsed && typeof parsed === 'object') {
101
- turnSource = parsed.turns;
102
104
  const slidesRaw = parsed.viewedSlides;
103
105
  if (Array.isArray(slidesRaw)) {
104
106
  viewedSlides = slidesRaw.filter((id) => typeof id === 'string' && id.length > 0);
105
107
  }
108
+ const tplRaw = parsed.templateIds;
109
+ if (Array.isArray(tplRaw)) {
110
+ templateIds = tplRaw.filter((id) => typeof id === 'string' && id.length > 0);
111
+ }
112
+ const namesRaw = parsed.templateNames;
113
+ if (Array.isArray(namesRaw)) {
114
+ templateNames = namesRaw.filter((n) => typeof n === 'string' && n.length > 0);
115
+ }
116
+ // Summary format: plain-text handoff from summarizeChat()
117
+ const summary = parsed.summary;
118
+ if (typeof summary === 'string' && summary.length > 0) {
119
+ // Append explicit template names so the bot sees them clearly
120
+ const namesHint = templateNames.length > 0
121
+ ? `\n\n[INSTALL THESE TEMPLATES BY NAME: ${templateNames.join(', ')}]`
122
+ : '';
123
+ return {
124
+ turns: [{ role: 'user', content: `[Demo conversation summary]\n${summary}${namesHint}` }],
125
+ viewedSlides,
126
+ templateIds,
127
+ templateNames,
128
+ };
129
+ }
130
+ turnSource = parsed.turns;
106
131
  }
107
132
  if (!Array.isArray(turnSource)) {
108
- return { turns: [], viewedSlides };
133
+ return { turns: [], viewedSlides, templateIds, templateNames };
109
134
  }
110
135
  const turns = [];
111
136
  for (const item of turnSource) {
@@ -122,7 +147,7 @@ function parseSeedTurns(raw, log) {
122
147
  break;
123
148
  }
124
149
  }
125
- return { turns, viewedSlides };
150
+ return { turns, viewedSlides, templateIds, templateNames };
126
151
  }
127
152
  /**
128
153
  * Thrown by Bot.start() when the bot's own Agent Directory activity has been
@@ -177,6 +202,35 @@ const HEAP_BACKPRESSURE_THRESHOLD = 0.7;
177
202
  function fileSizeLimit(mime) {
178
203
  return EXCEL_MIME_TYPES.has(mime) || WORD_MIME_TYPES.has(mime) ? MAX_OFFICE_FILE_SIZE : MAX_FILE_SIZE;
179
204
  }
205
+ /**
206
+ * Detect a supported image's true media type from its leading magic bytes.
207
+ * Hailer derives a file's mime from its extension, so a renamed file (e.g. an
208
+ * AVIF saved as .jpeg) reports the wrong mime. Anthropic vision sniffs the real
209
+ * bytes and rejects a mismatch with a 400, so we sniff here and send the true
210
+ * type — returning null for anything that is not a vision-supported image
211
+ * (AVIF, HEIC, corrupt data) so the caller skips it instead of mislabeling it.
212
+ */
213
+ function sniffImageMime(base64) {
214
+ const buf = Buffer.from(base64.slice(0, 32), 'base64');
215
+ if (buf.length < 12) {
216
+ return null;
217
+ }
218
+ if (buf[0] === 0xFF && buf[1] === 0xD8 && buf[2] === 0xFF) {
219
+ return 'image/jpeg';
220
+ }
221
+ if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4E && buf[3] === 0x47) {
222
+ return 'image/png';
223
+ }
224
+ if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x38) {
225
+ return 'image/gif';
226
+ }
227
+ const isRiff = buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46;
228
+ const isWebp = buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50;
229
+ if (isRiff && isWebp) {
230
+ return 'image/webp';
231
+ }
232
+ return null;
233
+ }
180
234
  const ATTACHMENT_KINDS = [
181
235
  { label: 'Image', trailing: 'included below for vision', matches: (mimeType) => IMAGE_MIME_TYPES.has(mimeType) },
182
236
  { label: 'Document', trailing: 'included below', matches: (mimeType) => DOCUMENT_MIME_TYPES.has(mimeType) || TEXT_MIME_TYPES.has(mimeType) },
@@ -285,6 +339,24 @@ class Bot {
285
339
  hailerApi = null;
286
340
  opLogger = new operation_logger_1.OperationLogger();
287
341
  cachedTools = null;
342
+ /**
343
+ * Per-turn tool result dedup cache for helperMode.
344
+ * Key includes senderId to prevent cross-user permission leaks.
345
+ * Cleared on each new user message AND when any write tool executes.
346
+ */
347
+ toolDedupCache = new Map();
348
+ static DEDUP_TOOLS = new Set([
349
+ 'list_workflows_minimal', 'list_workflows', 'get_workflow_schema',
350
+ 'list_workflow_phases', 'list_calendars', 'search_workspace_users',
351
+ 'core_init', 'check_user_permissions', 'list_workflow_permissions',
352
+ ]);
353
+ static WRITE_TOOLS_FOR_CACHE = new Set([
354
+ 'create_activity', 'update_activity', 'install_workflow',
355
+ 'create_event', 'update_event', 'delete_event',
356
+ 'update_workflow_field', 'update_workflow_phase',
357
+ 'create_calendar', 'update_calendar',
358
+ 'grant_workflow_permission', 'revoke_workflow_permission',
359
+ ]);
288
360
  // Signal handlers (for cleanup)
289
361
  signalHandler = null;
290
362
  processUpdatedHandler = null;
@@ -602,7 +674,7 @@ class Bot {
602
674
  accessLevel: this.config.accessLevel,
603
675
  allowBotMessages: this.config.allowBotMessages,
604
676
  });
605
- const { turns: seedTurns, viewedSlides } = parseSeedTurns(this.config.seedContext, this.logger);
677
+ const { turns: seedTurns, viewedSlides, templateIds } = parseSeedTurns(this.config.seedContext, this.logger);
606
678
  const hasSeed = seedTurns.length > 0;
607
679
  // Map slide ids → short human labels for the prompt. Anything not in
608
680
  // the map is dropped (defence against stale ids leaking through).
@@ -629,6 +701,36 @@ class Bot {
629
701
  const slideHint = slideLabels.length > 0
630
702
  ? `\n\nAdditional signal: during the demo the visitor explicitly opened these overview slides — ${slideLabels.join(', ')}. Treat that as an interest signal. Weight your concrete suggestions toward those concepts when the demo conversation is ambiguous, but don't read more into it than "they paid attention here."`
631
703
  : '';
704
+ // Install marketplace templates BEFORE generating the intro so we
705
+ // can tell the bot what was installed and it doesn't ask the user.
706
+ const installedTemplateNames = [];
707
+ if (templateIds.length > 0 && this.client) {
708
+ for (const templateId of templateIds) {
709
+ try {
710
+ await this.client.socket.request('v2.network.product.install', [templateId, this.config.workspaceId]);
711
+ installedTemplateNames.push(templateId);
712
+ this.logger.info('Installed marketplace template from seedContext', {
713
+ templateId, workspaceId: this.config.workspaceId,
714
+ });
715
+ }
716
+ catch (err) {
717
+ this.logger.warn('Failed to install marketplace template, trying as app', { templateId, err });
718
+ try {
719
+ await this.client.socket.request('v3.app.product.install', [templateId, this.config.workspaceId]);
720
+ installedTemplateNames.push(templateId);
721
+ this.logger.info('Installed app product from seedContext', {
722
+ templateId, workspaceId: this.config.workspaceId,
723
+ });
724
+ }
725
+ catch (appErr) {
726
+ this.logger.warn('Template install failed', { templateId, err: appErr });
727
+ }
728
+ }
729
+ }
730
+ }
731
+ const templatesInstalledHint = installedTemplateNames.length > 0
732
+ ? `\n\nIMPORTANT: The marketplace template(s) the visitor chose during the demo have ALREADY been installed into this workspace automatically. Do NOT ask which template they want or offer to install anything — it's done. Just tell them it's installed and ready, and offer to walk them through what's inside (phases, fields, etc.).`
733
+ : '';
632
734
  // Only promise an automatic build when Studio session creation is
633
735
  // configured for this deployment; otherwise nothing builds the
634
736
  // workspace and the bot must not promise a build that never runs. The
@@ -650,19 +752,20 @@ What this first message needs to do:
650
752
  4. Mention in passing that they can shape how you behave (tone, persona, language, even your picture) just by asking you here, and that account-level settings live in the AI Hub app.
651
753
 
652
754
  Tone: conversational, friendly, reassuring. No section headers, no bullet lists. Keep it under ~150 words.`
653
- : `You are being posted in your own activity discussion for the first time. The user just graduated from a public demo chat with you — the messages above are the conversation you both had there. NOTE: there is no automatic workspace builder running in this deployment, so YOU are how they get set up.
654
-
655
- What this first message needs to do:
755
+ : `You are being posted in your own activity discussion for the first time. The user just graduated from a public demo chat with you — the messages above are the conversation you both had there.
656
756
 
657
- 1. Greet them warmly and acknowledge you remember the demo. Be specific name the thing they were exploring (e.g. "the plumbing-company job tracker you were sketching out" rather than "your business").
658
-
659
- 2. Scan the demo for concrete setup intent — a process with stages, or a list of records they want to manage. Offer to set it up for them now: be concrete, naming the phases you'd create (e.g. Quoted → Scheduled → In Progress → Invoiced → Paid) for a lifecycle, or describing it as a simple list for reference data. End with a clear yes/no like "Want me to set this up?" — don't start building unilaterally.
757
+ CRITICAL: This is a plain text greeting. No tool calls, no bot-action tags, no JSON, no XML, no code blocks. Just normal sentences.
660
758
 
661
- 3. If the demo is too vague to start, ask ONE or TWO concrete clarifying questions instead.
759
+ Write exactly this structure:
760
+ 1. "Welcome to Hailer!" — warm greeting, one sentence.
761
+ 2. If a template was mentioned in the demo context: "I see you chose [template name] — shall I install it?" Ask for confirmation, nothing more.
762
+ 3. If no template was mentioned: "What would you like to set up first?" — one sentence.
662
763
 
663
- 4. Mention in passing that they can shape how you behave (tone, persona, language, even your picture) just by asking you here, and that account-level settings live in the AI Hub app.
764
+ ${installedTemplateNames.length > 0
765
+ ? `The template(s) have been auto-installed. Say: "Welcome to Hailer! I've already installed [name] for you — want me to show you what's inside?"`
766
+ : ``}
664
767
 
665
- Tone: conversational, friendly, action-oriented. No section headers; short bullets are fine only when naming proposed phases or fields. Keep it under ~180 words.`)
768
+ Under 40 words. No headers, no lists.`)
666
769
  : `You are being posted in your own activity discussion for the first time. Introduce yourself briefly in your own voice.
667
770
 
668
771
  CRITICAL: do NOT invent a backstory or claim to be a real person, celebrity, or fictional character based on your name alone. Your name was just chosen by the user — they may have meant a specific person/character, or they may have just liked the sound of it. If you don't have explicit persona instructions, acknowledge your name but be honest that you're a fresh assistant without a defined persona yet, and ask what kind of assistant they want you to be (or invite them to configure you).
@@ -736,6 +839,7 @@ Keep it short and conversational. No bullet lists, no headers.`;
736
839
  activityId: this.config.activityId,
737
840
  discussionId,
738
841
  length: text.length,
842
+ templatesInstalled: installedTemplateNames.length,
739
843
  });
740
844
  }
741
845
  /**
@@ -937,6 +1041,19 @@ Keep it short and conversational. No bullet lists, no headers.`;
937
1041
  }
938
1042
  // Build conversation entry
939
1043
  const conversation = this.conversationManager.getConversation(discussionId);
1044
+ // First message in a graduated bot's discussion: inject the
1045
+ // demo context so the LLM knows what was discussed before signup.
1046
+ if (conversation.length === 0 && this.config.seedContext) {
1047
+ const seed = parseSeedTurns(this.config.seedContext, this.logger);
1048
+ if (seed.turns.length > 0) {
1049
+ for (const turn of seed.turns) {
1050
+ conversation.push(turn);
1051
+ }
1052
+ // Close with a synthetic assistant ack so the next real
1053
+ // user message doesn't violate the user-user turn rule.
1054
+ conversation.push({ role: 'assistant', content: '[Context from demo chat loaded]' });
1055
+ }
1056
+ }
940
1057
  const parts = messages.map(msg => this.formatIncomingMessage(msg));
941
1058
  let merged = parts.join('\n\n');
942
1059
  if (messages.length > 1) {
@@ -1010,6 +1127,7 @@ Keep it short and conversational. No bullet lists, no headers.`;
1010
1127
  return true;
1011
1128
  }
1012
1129
  }
1130
+ this.toolDedupCache.clear();
1013
1131
  this.conversationManager.manageContextSize(message.discussionId);
1014
1132
  const conversation = this.conversationManager.getConversation(message.discussionId);
1015
1133
  const snapshotLength = conversation.length;
@@ -1024,12 +1142,37 @@ Keep it short and conversational. No bullet lists, no headers.`;
1024
1142
  this.logger.error('Message processing failed', error);
1025
1143
  this.typingIndicator?.stop(message.discussionId);
1026
1144
  conversation.length = snapshotLength;
1145
+ // Never fail silently — always tell the user something went wrong.
1146
+ const errMsg = error instanceof Error ? error.message : String(error);
1147
+ const isBadImage = error instanceof sdk_1.default.BadRequestError &&
1148
+ /image/i.test(errMsg) && /not supported|unsupported/i.test(errMsg);
1149
+ const notice = isBadImage
1150
+ ? 'I could not read that image — please send it as a PNG, JPEG, GIF, or WebP (formats like AVIF or HEIC are not supported).'
1151
+ : 'Sorry — something went wrong on my end and I could not finish that. Please try again.';
1152
+ await this.sendMessage(message.discussionId, notice);
1027
1153
  return false;
1028
1154
  }
1029
1155
  }
1030
1156
  // ===== MODEL ROUTING =====
1157
+ /** Short continuation messages used in helperMode onboarding (numbers, "ok", "next", etc.) */
1158
+ static HELPER_TRIVIAL = /^(?:[1-9]|ok|yes|no|да|нет|ок|дальше|далее|next|continue|продолжай|го|давай|sure|yep|yeah|ладно|хорошо|понял|ага)\.?!?$/i;
1031
1159
  async routeMessage(message, conversation, signal) {
1032
1160
  const defaultRoute = { model: MODEL_HAIKU, maxTokens: 2000 };
1161
+ // helperMode: onboarding steps are templated — Haiku with higher budget.
1162
+ // File attachments still go to Sonnet (checked below).
1163
+ // Trivial continuations ("3", "дальше") skip the router call entirely.
1164
+ if (this.config.helperMode) {
1165
+ const helperRoute = { model: MODEL_HAIKU, maxTokens: 4096 };
1166
+ if (Bot.HELPER_TRIVIAL.test(message.content.trim())) {
1167
+ this.opLogger.route(message.discussionId, 'HELPER_TRIVIAL', helperRoute.model, message.content);
1168
+ return helperRoute;
1169
+ }
1170
+ // Non-trivial helper messages still skip router but use Haiku
1171
+ if (!message.fileAttachments?.length) {
1172
+ this.opLogger.route(message.discussionId, 'HELPER', helperRoute.model, message.content);
1173
+ return helperRoute;
1174
+ }
1175
+ }
1033
1176
  // Any file attachment forces COMPLEX: reading/analysing an attachment plus the
1034
1177
  // tool calls that typically follow easily blows Haiku's 2000-token budget,
1035
1178
  // and a truncated response with an unfinished tool_use poisons the conversation.
@@ -1132,7 +1275,10 @@ Keep it short and conversational. No bullet lists, no headers.`;
1132
1275
  llmStart = Date.now();
1133
1276
  continue;
1134
1277
  }
1135
- if (route.model !== MODEL_HAIKU && !isPromptTooLong) {
1278
+ // A non-prompt-length 400 (e.g. an unsupported image) is deterministic —
1279
+ // Haiku would fail identically, so don't waste a retry; surface it instead.
1280
+ const isBadRequest = error instanceof sdk_1.default.BadRequestError;
1281
+ if (route.model !== MODEL_HAIKU && !isPromptTooLong && !isBadRequest) {
1136
1282
  this.logger.warn('Sonnet failed, falling back to Haiku');
1137
1283
  route = { model: MODEL_HAIKU, maxTokens: 2000 };
1138
1284
  llmStart = Date.now();
@@ -1293,6 +1439,16 @@ Keep it short and conversational. No bullet lists, no headers.`;
1293
1439
  results.push({ type: 'tool_result', tool_use_id: block.id, content: 'You are already responding in this discussion. Your text response will be posted automatically — do not use add_discussion_message for the current discussion. Use it only for OTHER discussions.', is_error: true });
1294
1440
  continue;
1295
1441
  }
1442
+ // Dedup cache: serve cached result for read-only tools (same sender + same args)
1443
+ if (Bot.DEDUP_TOOLS.has(block.name)) {
1444
+ const dedupKey = `${block.name}:${message.senderId}:${JSON.stringify(args)}`;
1445
+ const cached = this.toolDedupCache.get(dedupKey);
1446
+ if (cached) {
1447
+ this.opLogger.toolCall(message.discussionId, block.name, 'dedup hit', 0, 'OK', undefined);
1448
+ results.push({ type: 'tool_result', tool_use_id: block.id, content: cached });
1449
+ continue;
1450
+ }
1451
+ }
1296
1452
  try {
1297
1453
  let toolArgs = args;
1298
1454
  // Auto-inject for join_discussion
@@ -1342,6 +1498,15 @@ Keep it short and conversational. No bullet lists, no headers.`;
1342
1498
  contentStr = `${contentStr.slice(0, MAX_TOOL_RESULT_CHARS)}\n\n[Tool result truncated. Original: ${originalLen} chars (~${Math.round(originalLen / 4)} tokens). Narrow your query: use 'fields' parameter, add filters, lower 'limit', or use run_insight with aggregate SQL.]`;
1343
1499
  this.logger.warn('Tool result truncated', { tool: block.name, originalLen, truncatedTo: MAX_TOOL_RESULT_CHARS });
1344
1500
  }
1501
+ // Invalidate dedup cache when a write tool executes
1502
+ if (Bot.WRITE_TOOLS_FOR_CACHE.has(block.name)) {
1503
+ this.toolDedupCache.clear();
1504
+ }
1505
+ // Store read-only results in dedup cache (post-filter, so permissions are baked in)
1506
+ if (Bot.DEDUP_TOOLS.has(block.name)) {
1507
+ const dedupKey = `${block.name}:${message.senderId}:${JSON.stringify(args)}`;
1508
+ this.toolDedupCache.set(dedupKey, contentStr);
1509
+ }
1345
1510
  results.push({ type: 'tool_result', tool_use_id: block.id, content: contentStr });
1346
1511
  }
1347
1512
  catch (error) {
@@ -1409,9 +1574,16 @@ Keep it short and conversational. No bullet lists, no headers.`;
1409
1574
  try {
1410
1575
  const result = await this.hailerApi.downloadFile(f.fileId);
1411
1576
  if (IMAGE_MIME_TYPES.has(f.mime) && result.encoding === 'base64') {
1577
+ // Trust the actual bytes, not the declared mime — a renamed file
1578
+ // (e.g. AVIF saved as .jpeg) would otherwise be sent mislabeled and 400.
1579
+ const sniffedMime = sniffImageMime(result.content);
1580
+ if (!sniffedMime) {
1581
+ skipped.push({ filename: f.filename || f.fileId, reason: 'not a supported image — use PNG, JPEG, GIF, or WebP (AVIF/HEIC are not supported)' });
1582
+ return null;
1583
+ }
1412
1584
  return {
1413
1585
  type: 'image',
1414
- source: { type: 'base64', media_type: f.mime, data: result.content },
1586
+ source: { type: 'base64', media_type: sniffedMime, data: result.content },
1415
1587
  };
1416
1588
  }
1417
1589
  if (DOCUMENT_MIME_TYPES.has(f.mime) && result.encoding === 'base64') {