@hailer/mcp 1.3.9 → 1.3.11
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.
- package/CHANGELOG.md +16 -0
- package/dist/bot/bot.d.ts +10 -0
- package/dist/bot/bot.d.ts.map +1 -1
- package/dist/bot/bot.js +189 -17
- package/dist/bot/bot.js.map +1 -1
- package/dist/bot/services/helper-prompt.d.ts +1 -1
- package/dist/bot/services/helper-prompt.d.ts.map +1 -1
- package/dist/bot/services/helper-prompt.js +60 -5
- package/dist/bot/services/helper-prompt.js.map +1 -1
- package/dist/bot/services/system-prompt.d.ts.map +1 -1
- package/dist/bot/services/system-prompt.js +13 -2
- package/dist/bot/services/system-prompt.js.map +1 -1
- package/dist/mcp/publish-auth-injector.d.ts +37 -9
- package/dist/mcp/publish-auth-injector.d.ts.map +1 -1
- package/dist/mcp/publish-auth-injector.js +77 -26
- package/dist/mcp/publish-auth-injector.js.map +1 -1
- package/dist/mcp/tool-profiles.d.ts.map +1 -1
- package/dist/mcp/tool-profiles.js +9 -0
- package/dist/mcp/tool-profiles.js.map +1 -1
- package/dist/mcp/tools/app-marketplace.d.ts +4 -0
- package/dist/mcp/tools/app-marketplace.d.ts.map +1 -1
- package/dist/mcp/tools/app-marketplace.js +299 -42
- package/dist/mcp/tools/app-marketplace.js.map +1 -1
- package/dist/mcp/tools/workflow.d.ts +1 -0
- package/dist/mcp/tools/workflow.d.ts.map +1 -1
- package/dist/mcp/tools/workflow.js +57 -1
- package/dist/mcp/tools/workflow.js.map +1 -1
- package/dist/public-chat/graduate.d.ts.map +1 -1
- package/dist/public-chat/graduate.js +175 -49
- package/dist/public-chat/graduate.js.map +1 -1
- package/dist/public-chat/rate-limit.js +1 -1
- package/dist/public-chat/rate-limit.js.map +1 -1
- package/dist/public-chat/studio-prewarm.js +2 -2
- package/dist/public-chat/studio-prewarm.js.map +1 -1
- package/dist/public-chat/system-prompt.d.ts.map +1 -1
- package/dist/public-chat/system-prompt.js +511 -286
- package/dist/public-chat/system-prompt.js.map +1 -1
- package/package.json +1 -1
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;
|
package/dist/bot/bot.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bot.d.ts","sourceRoot":"","sources":["../../src/bot/bot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,YAAY,EAAa,MAAM,sBAAsB,CAAC;AAwB/D,OAAO,EAAkC,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAEH,SAAS,EAMZ,MAAM,eAAe,CAAC;AAIvB,iEAAiE;AACjE,UAAU,aAAa;IACrB,yBAAyB,CAAC,WAAW,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;CAC7D;AAED,mEAAmE;AACnE,MAAM,WAAW,UAAU;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,YAAY,CAAC;IAC3B,UAAU,CAAC,EAAE,aAAa,CAAC;CAC5B;
|
|
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
|
-
//
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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') {
|