@hailer/mcp 1.0.28 → 1.1.2

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 (233) hide show
  1. package/.claude/.session-checked +1 -0
  2. package/.claude/agents/agent-ada-skill-builder.md +10 -2
  3. package/.claude/agents/agent-alejandro-function-fields.md +104 -37
  4. package/.claude/agents/agent-bjorn-config-audit.md +41 -21
  5. package/.claude/agents/agent-builder-agent-creator.md +13 -3
  6. package/.claude/agents/agent-code-simplifier.md +53 -0
  7. package/.claude/agents/agent-dmitri-activity-crud.md +126 -11
  8. package/.claude/agents/agent-giuseppe-app-builder.md +212 -22
  9. package/.claude/agents/agent-gunther-mcp-tools.md +7 -36
  10. package/.claude/agents/agent-helga-workflow-config.md +75 -10
  11. package/.claude/agents/agent-igor-activity-mover-automation.md +125 -0
  12. package/.claude/agents/agent-ingrid-doc-templates.md +164 -36
  13. package/.claude/agents/agent-ivan-monolith.md +154 -0
  14. package/.claude/agents/agent-kenji-data-reader.md +15 -8
  15. package/.claude/agents/agent-lars-code-inspector.md +56 -8
  16. package/.claude/agents/agent-marco-mockup-builder.md +110 -0
  17. package/.claude/agents/agent-marcus-api-documenter.md +323 -0
  18. package/.claude/agents/agent-marketplace-publisher.md +232 -72
  19. package/.claude/agents/agent-marketplace-reviewer.md +255 -79
  20. package/.claude/agents/agent-permissions-handler.md +208 -0
  21. package/.claude/agents/agent-simple-writer.md +48 -0
  22. package/.claude/agents/agent-svetlana-code-review.md +127 -14
  23. package/.claude/agents/agent-tanya-test-runner.md +333 -0
  24. package/.claude/agents/agent-ui-designer.md +100 -0
  25. package/.claude/agents/agent-viktor-sql-insights.md +19 -6
  26. package/.claude/agents/agent-web-search.md +55 -0
  27. package/.claude/agents/agent-yevgeni-discussions.md +7 -1
  28. package/.claude/agents/agent-zara-zapier.md +159 -0
  29. package/.claude/commands/app-squad.md +135 -0
  30. package/.claude/commands/audit-squad.md +158 -0
  31. package/.claude/commands/autoplan.md +563 -0
  32. package/.claude/commands/cleanup-squad.md +98 -0
  33. package/.claude/commands/config-squad.md +106 -0
  34. package/.claude/commands/crud-squad.md +87 -0
  35. package/.claude/commands/data-squad.md +97 -0
  36. package/.claude/commands/debug-squad.md +303 -0
  37. package/.claude/commands/doc-squad.md +65 -0
  38. package/.claude/commands/handoff.md +137 -0
  39. package/.claude/commands/health.md +49 -0
  40. package/.claude/commands/help.md +2 -1
  41. package/.claude/commands/help:agents.md +96 -16
  42. package/.claude/commands/help:commands.md +55 -11
  43. package/.claude/commands/help:faq.md +16 -1
  44. package/.claude/commands/help:skills.md +93 -0
  45. package/.claude/commands/hotfix-squad.md +112 -0
  46. package/.claude/commands/integration-squad.md +82 -0
  47. package/.claude/commands/janitor-squad.md +167 -0
  48. package/.claude/commands/learn-auto.md +120 -0
  49. package/.claude/commands/learn.md +120 -0
  50. package/.claude/commands/mcp-list.md +27 -0
  51. package/.claude/commands/onboard-squad.md +140 -0
  52. package/.claude/commands/plan-workspace.md +732 -0
  53. package/.claude/commands/prd.md +131 -0
  54. package/.claude/commands/project-status.md +82 -0
  55. package/.claude/commands/publish.md +138 -0
  56. package/.claude/commands/recap.md +69 -0
  57. package/.claude/commands/restore.md +64 -0
  58. package/.claude/commands/review-squad.md +152 -0
  59. package/.claude/commands/save.md +24 -0
  60. package/.claude/commands/stats.md +19 -0
  61. package/.claude/commands/swarm.md +210 -0
  62. package/.claude/commands/tool-builder.md +3 -1
  63. package/.claude/commands/ws-pull.md +1 -1
  64. package/.claude/commands/yolo-off.md +17 -0
  65. package/.claude/commands/yolo.md +82 -0
  66. package/.claude/hooks/_shared-memory.cjs +305 -0
  67. package/.claude/hooks/_utils.cjs +134 -0
  68. package/.claude/hooks/agent-failure-detector.cjs +164 -79
  69. package/.claude/hooks/agent-usage-logger.cjs +204 -0
  70. package/.claude/hooks/app-edit-guard.cjs +20 -4
  71. package/.claude/hooks/auto-learn.cjs +316 -0
  72. package/.claude/hooks/bash-guard.cjs +282 -0
  73. package/.claude/hooks/builder-mode-manager.cjs +183 -54
  74. package/.claude/hooks/bulk-activity-guard.cjs +283 -0
  75. package/.claude/hooks/context-watchdog.cjs +292 -0
  76. package/.claude/hooks/delegation-reminder.cjs +478 -0
  77. package/.claude/hooks/design-system-lint.cjs +283 -0
  78. package/.claude/hooks/post-scaffold-hook.cjs +16 -3
  79. package/.claude/hooks/prompt-guard.cjs +366 -0
  80. package/.claude/hooks/publish-template-guard.cjs +16 -0
  81. package/.claude/hooks/session-start.cjs +35 -0
  82. package/.claude/hooks/shared-memory-writer.cjs +147 -0
  83. package/.claude/hooks/skill-injector.cjs +140 -0
  84. package/.claude/hooks/skill-usage-logger.cjs +258 -0
  85. package/.claude/hooks/src-edit-guard.cjs +16 -1
  86. package/.claude/hooks/sync-marketplace-agents.cjs +53 -8
  87. package/.claude/scripts/yolo-toggle.cjs +142 -0
  88. package/.claude/settings.json +141 -14
  89. package/.claude/skills/SDK-activity-patterns/SKILL.md +428 -0
  90. package/.claude/skills/SDK-document-templates/SKILL.md +1033 -0
  91. package/.claude/skills/SDK-function-fields/SKILL.md +542 -0
  92. package/.claude/skills/SDK-generate-skill/SKILL.md +92 -0
  93. package/.claude/skills/SDK-init-skill/SKILL.md +127 -0
  94. package/.claude/skills/SDK-insight-queries/SKILL.md +787 -0
  95. package/.claude/skills/SDK-ws-config-skill/SKILL.md +1139 -0
  96. package/.claude/skills/agent-structure/SKILL.md +98 -0
  97. package/.claude/skills/api-documentation-patterns/SKILL.md +474 -0
  98. package/.claude/skills/chrome-mcp-reference/SKILL.md +370 -0
  99. package/.claude/skills/delegation-routing/SKILL.md +202 -0
  100. package/.claude/skills/frontend-design/SKILL.md +254 -0
  101. package/.claude/skills/hailer-activity-mover/SKILL.md +213 -0
  102. package/.claude/skills/hailer-api-client/SKILL.md +518 -0
  103. package/.claude/skills/hailer-app-builder/SKILL.md +939 -11
  104. package/.claude/skills/hailer-apps-pictures/SKILL.md +269 -0
  105. package/.claude/skills/hailer-design-system/SKILL.md +235 -0
  106. package/.claude/skills/hailer-monolith-automations/SKILL.md +686 -0
  107. package/.claude/skills/hailer-permissions-system/SKILL.md +121 -0
  108. package/.claude/skills/hailer-project-protocol/SKILL.md +488 -0
  109. package/.claude/skills/hailer-rest-api/SKILL.md +61 -0
  110. package/.claude/skills/hailer-rest-api/hailer-activities.md +184 -0
  111. package/.claude/skills/hailer-rest-api/hailer-admin.md +473 -0
  112. package/.claude/skills/hailer-rest-api/hailer-calendar.md +256 -0
  113. package/.claude/skills/hailer-rest-api/hailer-feed.md +249 -0
  114. package/.claude/skills/hailer-rest-api/hailer-insights.md +195 -0
  115. package/.claude/skills/hailer-rest-api/hailer-messaging.md +276 -0
  116. package/.claude/skills/hailer-rest-api/hailer-workflows.md +283 -0
  117. package/.claude/skills/insight-join-patterns/SKILL.md +3 -0
  118. package/.claude/skills/integration-patterns/SKILL.md +421 -0
  119. package/.claude/skills/json-only-output/SKILL.md +52 -12
  120. package/.claude/skills/lsp-setup/SKILL.md +160 -0
  121. package/.claude/skills/mcp-direct-tools/SKILL.md +153 -0
  122. package/.claude/skills/optional-parameters/SKILL.md +32 -23
  123. package/.claude/skills/publish-hailer-app/SKILL.md +76 -12
  124. package/.claude/skills/testing-patterns/SKILL.md +630 -0
  125. package/.claude/skills/tool-builder/SKILL.md +250 -0
  126. package/.claude/skills/tool-parameter-usage/SKILL.md +59 -45
  127. package/.claude/skills/tool-response-verification/SKILL.md +82 -48
  128. package/.claude/skills/zapier-hailer-patterns/SKILL.md +581 -0
  129. package/.env.example +26 -7
  130. package/CLAUDE.md +290 -224
  131. package/dist/CLAUDE.md +370 -0
  132. package/dist/app.d.ts +1 -1
  133. package/dist/app.js +101 -101
  134. package/dist/bot/bot-config.d.ts +26 -0
  135. package/dist/bot/bot-config.js +135 -0
  136. package/dist/bot/bot-manager.d.ts +40 -0
  137. package/dist/bot/bot-manager.js +137 -0
  138. package/dist/bot/bot.d.ts +127 -0
  139. package/dist/bot/bot.js +1328 -0
  140. package/dist/bot/operation-logger.d.ts +28 -0
  141. package/dist/bot/operation-logger.js +132 -0
  142. package/dist/bot/services/conversation-manager.d.ts +60 -0
  143. package/dist/bot/services/conversation-manager.js +246 -0
  144. package/dist/bot/services/index.d.ts +9 -0
  145. package/dist/bot/services/index.js +18 -0
  146. package/dist/bot/services/message-classifier.d.ts +42 -0
  147. package/dist/bot/services/message-classifier.js +228 -0
  148. package/dist/bot/services/message-formatter.d.ts +88 -0
  149. package/dist/bot/services/message-formatter.js +411 -0
  150. package/dist/bot/services/session-logger.d.ts +162 -0
  151. package/dist/bot/services/session-logger.js +724 -0
  152. package/dist/bot/services/token-billing.d.ts +78 -0
  153. package/dist/bot/services/token-billing.js +233 -0
  154. package/dist/bot/services/types.d.ts +169 -0
  155. package/dist/bot/services/types.js +12 -0
  156. package/dist/bot/services/typing-indicator.d.ts +23 -0
  157. package/dist/bot/services/typing-indicator.js +60 -0
  158. package/dist/bot/services/workspace-schema-cache.d.ts +122 -0
  159. package/dist/bot/services/workspace-schema-cache.js +506 -0
  160. package/dist/bot/tool-executor.d.ts +28 -0
  161. package/dist/bot/tool-executor.js +48 -0
  162. package/dist/bot/workspace-overview.d.ts +12 -0
  163. package/dist/bot/workspace-overview.js +94 -0
  164. package/dist/cli.d.ts +1 -8
  165. package/dist/cli.js +1 -249
  166. package/dist/config.d.ts +96 -3
  167. package/dist/config.js +148 -37
  168. package/dist/core.d.ts +5 -0
  169. package/dist/core.js +61 -8
  170. package/dist/lib/discussion-lock.d.ts +42 -0
  171. package/dist/lib/discussion-lock.js +110 -0
  172. package/dist/lib/logger.d.ts +0 -1
  173. package/dist/lib/logger.js +39 -23
  174. package/dist/lib/request-logger.d.ts +77 -0
  175. package/dist/lib/request-logger.js +147 -0
  176. package/dist/mcp/UserContextCache.js +16 -13
  177. package/dist/mcp/hailer-clients.js +18 -17
  178. package/dist/mcp/signal-handler.js +29 -13
  179. package/dist/mcp/tool-registry.d.ts +4 -15
  180. package/dist/mcp/tool-registry.js +94 -32
  181. package/dist/mcp/tools/activity.js +28 -69
  182. package/dist/mcp/tools/app-core.js +9 -4
  183. package/dist/mcp/tools/app-marketplace.js +22 -12
  184. package/dist/mcp/tools/app-member.js +5 -2
  185. package/dist/mcp/tools/app-scaffold.js +32 -18
  186. package/dist/mcp/tools/bot-config/constants.d.ts +23 -0
  187. package/dist/mcp/tools/bot-config/constants.js +94 -0
  188. package/dist/mcp/tools/bot-config/core.d.ts +253 -0
  189. package/dist/mcp/tools/bot-config/core.js +2456 -0
  190. package/dist/mcp/tools/bot-config/index.d.ts +10 -0
  191. package/dist/mcp/tools/bot-config/index.js +59 -0
  192. package/dist/mcp/tools/bot-config/tools.d.ts +7 -0
  193. package/dist/mcp/tools/bot-config/tools.js +15 -0
  194. package/dist/mcp/tools/bot-config/types.d.ts +50 -0
  195. package/dist/mcp/tools/bot-config/types.js +6 -0
  196. package/dist/mcp/tools/discussion.js +107 -77
  197. package/dist/mcp/tools/document.d.ts +11 -0
  198. package/dist/mcp/tools/document.js +741 -0
  199. package/dist/mcp/tools/file.js +5 -2
  200. package/dist/mcp/tools/insight.js +36 -12
  201. package/dist/mcp/tools/investigate.d.ts +9 -0
  202. package/dist/mcp/tools/investigate.js +254 -0
  203. package/dist/mcp/tools/user.d.ts +2 -4
  204. package/dist/mcp/tools/user.js +9 -50
  205. package/dist/mcp/tools/workflow.d.ts +1 -0
  206. package/dist/mcp/tools/workflow.js +164 -52
  207. package/dist/mcp/utils/hailer-api-client.js +26 -17
  208. package/dist/mcp/webhook-handler.d.ts +64 -3
  209. package/dist/mcp/webhook-handler.js +219 -9
  210. package/dist/mcp-server.d.ts +4 -0
  211. package/dist/mcp-server.js +237 -25
  212. package/dist/plugins/bug-fixer/index.d.ts +2 -0
  213. package/dist/plugins/bug-fixer/index.js +18 -0
  214. package/dist/plugins/bug-fixer/tools.d.ts +45 -0
  215. package/dist/plugins/bug-fixer/tools.js +1096 -0
  216. package/package.json +10 -10
  217. package/scripts/test-hal-tools.ts +154 -0
  218. package/.claude/agents/agent-nora-name-functions.md +0 -123
  219. package/.claude/assistant-knowledge.md +0 -23
  220. package/.claude/commands/install-plugin.md +0 -261
  221. package/.claude/commands/list-plugins.md +0 -42
  222. package/.claude/commands/marketplace-setup.md +0 -33
  223. package/.claude/commands/publish-plugin.md +0 -55
  224. package/.claude/commands/uninstall-plugin.md +0 -87
  225. package/.claude/hooks/interactive-mode.cjs +0 -87
  226. package/.claude/hooks/mcp-server-guard.cjs +0 -108
  227. package/.claude/skills/marketplace-publishing.md +0 -155
  228. package/dist/bot/chat-bot.d.ts +0 -31
  229. package/dist/bot/chat-bot.js +0 -357
  230. package/dist/mcp/tools/metrics.d.ts +0 -13
  231. package/dist/mcp/tools/metrics.js +0 -546
  232. package/dist/stdio-server.d.ts +0 -14
  233. package/dist/stdio-server.js +0 -114
@@ -0,0 +1,1328 @@
1
+ "use strict";
2
+ /**
3
+ * Bot - Single class replacing the agents/ hierarchy.
4
+ *
5
+ * Connects to Hailer, subscribes to messenger signals,
6
+ * processes messages through the Anthropic API with tool use,
7
+ * and posts responses back to discussions.
8
+ */
9
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.Bot = void 0;
14
+ const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
15
+ const tool_registry_1 = require("../mcp/tool-registry");
16
+ const hailer_clients_1 = require("../mcp/hailer-clients");
17
+ const hailer_api_client_1 = require("../mcp/utils/hailer-api-client");
18
+ const workspace_cache_1 = require("../mcp/workspace-cache");
19
+ const tool_executor_1 = require("./tool-executor");
20
+ const workspace_overview_1 = require("./workspace-overview");
21
+ const operation_logger_1 = require("./operation-logger");
22
+ const services_1 = require("./services");
23
+ const logger_1 = require("../lib/logger");
24
+ /**
25
+ * Tools available to the bot. Add/remove tool names here to control what the LLM can use.
26
+ * The MCP server (Claude Code terminal) is unaffected - it keeps full access to all tools.
27
+ */
28
+ const BOT_TOOLS = new Set([
29
+ // Data reading
30
+ 'list_workflows',
31
+ 'list_workflows_minimal',
32
+ 'list_workflow_phases',
33
+ 'get_workflow_schema',
34
+ 'list_activities',
35
+ 'show_activity_by_id',
36
+ 'count_activities',
37
+ 'search_workspace_users',
38
+ 'get_workspace_balance',
39
+ // Data writing
40
+ 'create_activity',
41
+ 'update_activity',
42
+ // Workflow management
43
+ 'install_workflow',
44
+ // Discussions
45
+ 'list_my_discussions',
46
+ 'fetch_discussion_messages',
47
+ 'fetch_previous_discussion_messages',
48
+ 'add_discussion_message',
49
+ 'join_discussion',
50
+ 'leave_discussion',
51
+ 'invite_discussion_members',
52
+ 'get_activity_from_discussion',
53
+ // Files
54
+ 'upload_files',
55
+ 'download_file',
56
+ // Insights
57
+ 'list_insights',
58
+ 'get_insight_data',
59
+ 'preview_insight',
60
+ 'create_insight',
61
+ 'update_insight',
62
+ ]);
63
+ const WORKFLOW_SCOPED_TOOLS = new Set([
64
+ 'list_activities', 'count_activities',
65
+ 'get_workflow_schema', 'list_workflow_phases',
66
+ 'create_activity', 'update_activity',
67
+ 'install_workflow',
68
+ ]);
69
+ /** Tools that bypass the workflowId arg gate and need post-execution permission checks */
70
+ const UNGATED_TOOLS = new Set([
71
+ 'show_activity_by_id',
72
+ 'fetch_discussion_messages',
73
+ 'get_activity_from_discussion',
74
+ ]);
75
+ const MODEL_HAIKU = 'claude-haiku-4-5-20251001';
76
+ const MODEL_SONNET = 'claude-sonnet-4-5-20250929';
77
+ const MAX_TOOL_ITERATIONS = 10;
78
+ const RATE_LIMIT_WINDOW = 60_000; // 60 seconds
79
+ const RATE_LIMIT_PER_DISCUSSION = 30; // max signals per discussion per window
80
+ const RATE_LIMIT_GLOBAL = 100; // max signals total per window
81
+ class Bot {
82
+ logger;
83
+ clientManager;
84
+ client = null;
85
+ anthropic = null;
86
+ toolExecutor;
87
+ userContext = null;
88
+ // Hailer data
89
+ init = null;
90
+ workspaceCache = null;
91
+ workspaceOverview = '';
92
+ userId = '';
93
+ _workspaceId;
94
+ // Services
95
+ conversationManager = null;
96
+ messageClassifier = null;
97
+ messageFormatter = null;
98
+ typingIndicator = null;
99
+ tokenBilling = null;
100
+ sessionLogger = null;
101
+ opLogger = new operation_logger_1.OperationLogger();
102
+ // State
103
+ discussionStates = new Map();
104
+ processingDiscussions = new Set(); // concurrency guard only
105
+ static DISENGAGE_THRESHOLD = 5;
106
+ processedMessageIds = new Set();
107
+ signalTimestampsPerDiscussion = new Map();
108
+ globalSignalTimestamps = [];
109
+ signalHandler = null;
110
+ processUpdatedHandler = null;
111
+ workspaceUpdatedHandler = null;
112
+ cacheInvalidateHandler = null;
113
+ _connected = false;
114
+ refreshTimer = null;
115
+ refreshDirty = false;
116
+ refreshScope = new Set();
117
+ isRefreshing = false;
118
+ refreshQueued = false;
119
+ permissionIndex = new Map();
120
+ adminUserIds = new Set();
121
+ ownerUserIds = new Set();
122
+ // Config
123
+ config;
124
+ constructor(config) {
125
+ this.config = {
126
+ ...config,
127
+ model: config.model || 'claude-haiku-4-5-20251001',
128
+ };
129
+ this.logger = (0, logger_1.createLogger)({ component: 'Bot' });
130
+ this.clientManager = new hailer_clients_1.HailerClientManager(config.apiHost, config.email, config.password);
131
+ this.toolExecutor = new tool_executor_1.ToolExecutor(config.toolRegistry);
132
+ }
133
+ get connected() {
134
+ return this._connected;
135
+ }
136
+ get workspaceId() {
137
+ return this._workspaceId;
138
+ }
139
+ // ===== LIFECYCLE =====
140
+ async start() {
141
+ this.logger.debug('Starting bot', { email: this.config.email });
142
+ // 1. Connect to Hailer
143
+ this.client = await this.clientManager.connect();
144
+ // Clear password from memory now that connection is established
145
+ this.config.password = '';
146
+ // 2. Get user ID
147
+ const userInit = await this.client.socket.request('v2.core.init', [['user']]);
148
+ this.userId = userInit?.user?._id;
149
+ if (!this.userId)
150
+ throw new Error('Could not determine bot user ID');
151
+ // 3. Fetch workspace data
152
+ this.init = await this.client.socket.request('v2.core.init', [
153
+ ['processes', 'users', 'network', 'networks', 'teams', 'groups'],
154
+ ]);
155
+ this._workspaceId = this.init.network?._id || this.config.workspaceId;
156
+ this.buildPermissionIndex();
157
+ // 4. Build workspace cache and overview
158
+ this.workspaceCache = (0, workspace_cache_1.createWorkspaceCache)(this.init, {
159
+ excludeTranslations: true,
160
+ excludeSystemMessages: true,
161
+ excludeEmptyFields: true,
162
+ compactUserData: true,
163
+ includeWorkspaceNamesInTools: false,
164
+ });
165
+ this.workspaceOverview = (0, workspace_overview_1.generateWorkspaceOverview)(this.init);
166
+ // 5. Create Anthropic client
167
+ this.anthropic = new sdk_1.default({ apiKey: this.config.anthropicApiKey });
168
+ // 6. Build BotConnection adapter for services
169
+ const botConnection = {
170
+ client: this.client,
171
+ workspaceCache: this.workspaceCache ? {
172
+ usersById: this.workspaceCache.usersById,
173
+ users: this.workspaceCache.users,
174
+ rawInit: this.workspaceCache.rawInit,
175
+ } : undefined,
176
+ };
177
+ // 7. Create services
178
+ this.conversationManager = new services_1.ConversationManager(100, 50, this.logger);
179
+ this.messageClassifier = new services_1.MessageClassifier(this.userId, botConnection, this.logger);
180
+ this.typingIndicator = new services_1.TypingIndicatorService(botConnection, this.logger);
181
+ // Token billing
182
+ const hailerApi = new hailer_api_client_1.HailerApiClient(this.client);
183
+ this.tokenBilling = new services_1.TokenBillingService(this.logger, hailerApi);
184
+ // Message formatter - wrap ToolExecutor as MCP-style callback
185
+ const toolCallback = async (name, args) => {
186
+ return this.toolExecutor.execute(name, args, this.getUserContext());
187
+ };
188
+ this.messageFormatter = new services_1.MessageFormatterService(botConnection, this.logger, toolCallback);
189
+ // Session logger
190
+ this.sessionLogger = new services_1.SessionLoggerService(null, this.logger, toolCallback, () => this.getDefaultTeamId());
191
+ this.sessionLogger.setAnthropicClient(this.anthropic);
192
+ // 8. Build UserContext for tool execution
193
+ this.userContext = {
194
+ client: this.client,
195
+ hailer: hailerApi,
196
+ init: this.init,
197
+ workspaceCache: this.workspaceCache,
198
+ apiKey: '',
199
+ createdAt: Date.now(),
200
+ email: this.config.email,
201
+ password: this.config.password,
202
+ };
203
+ // 9. Subscribe to messenger.new signals
204
+ this.signalHandler = (eventData) => {
205
+ const signal = {
206
+ type: 'messenger.new',
207
+ data: eventData,
208
+ timestamp: Date.now(),
209
+ workspaceId: eventData.sid,
210
+ };
211
+ this.handleSignal(signal);
212
+ };
213
+ this.clientManager.onSignal('messenger.new', this.signalHandler);
214
+ // 10. Subscribe to workspace change signals for live updates
215
+ this.processUpdatedHandler = () => this.scheduleRefresh('processes');
216
+ this.workspaceUpdatedHandler = () => this.scheduleRefresh('network');
217
+ this.cacheInvalidateHandler = () => this.scheduleRefresh('full');
218
+ this.clientManager.onSignal('process.updated', this.processUpdatedHandler);
219
+ this.clientManager.onSignal('workspace.updated', this.workspaceUpdatedHandler);
220
+ this.clientManager.onSignal('cache.invalidate', this.cacheInvalidateHandler);
221
+ this._connected = true;
222
+ this.logger.debug('Bot started', {
223
+ userId: this.userId,
224
+ workspaceId: this._workspaceId,
225
+ });
226
+ }
227
+ async stop() {
228
+ this.logger.debug('Stopping bot');
229
+ // Clear refresh timer
230
+ if (this.refreshTimer) {
231
+ clearTimeout(this.refreshTimer);
232
+ this.refreshTimer = null;
233
+ }
234
+ // Flush sessions
235
+ this.sessionLogger?.stopIdleCheckTimer();
236
+ await this.sessionLogger?.flushAllSessions();
237
+ // Abort all in-flight discussions before clearing state
238
+ for (const [, state] of this.discussionStates) {
239
+ state.abortController?.abort();
240
+ }
241
+ this.discussionStates.clear();
242
+ // Clear rate limit tracking
243
+ this.signalTimestampsPerDiscussion.clear();
244
+ this.globalSignalTimestamps = [];
245
+ // Unsubscribe from signals
246
+ if (this.signalHandler) {
247
+ this.clientManager.offSignal('messenger.new', this.signalHandler);
248
+ this.signalHandler = null;
249
+ }
250
+ if (this.processUpdatedHandler) {
251
+ this.clientManager.offSignal('process.updated', this.processUpdatedHandler);
252
+ this.processUpdatedHandler = null;
253
+ }
254
+ if (this.workspaceUpdatedHandler) {
255
+ this.clientManager.offSignal('workspace.updated', this.workspaceUpdatedHandler);
256
+ this.workspaceUpdatedHandler = null;
257
+ }
258
+ if (this.cacheInvalidateHandler) {
259
+ this.clientManager.offSignal('cache.invalidate', this.cacheInvalidateHandler);
260
+ this.cacheInvalidateHandler = null;
261
+ }
262
+ // Disconnect
263
+ this.clientManager.disconnect();
264
+ this._connected = false;
265
+ this.client = null;
266
+ this.logger.debug('Bot stopped');
267
+ }
268
+ // ===== SIGNAL HANDLING =====
269
+ async handleSignal(signal) {
270
+ if (!this._connected || !this.messageClassifier)
271
+ return;
272
+ try {
273
+ const rawMsgId = signal.data.msg_id;
274
+ if (rawMsgId) {
275
+ if (this.processedMessageIds.has(rawMsgId))
276
+ return;
277
+ this.processedMessageIds.add(rawMsgId);
278
+ }
279
+ const message = await this.messageClassifier.extractIncomingMessage(signal);
280
+ if (!message) {
281
+ if (rawMsgId)
282
+ this.processedMessageIds.delete(rawMsgId);
283
+ return;
284
+ }
285
+ // Self-message guard: prevent bot feedback loops
286
+ if (message.senderId === this.userId)
287
+ return;
288
+ // Rate limiting
289
+ const now = Date.now();
290
+ const cutoff = now - RATE_LIMIT_WINDOW;
291
+ // Per-discussion rate limit
292
+ const discId = message.discussionId;
293
+ let discTimestamps = this.signalTimestampsPerDiscussion.get(discId);
294
+ if (!discTimestamps) {
295
+ discTimestamps = [];
296
+ this.signalTimestampsPerDiscussion.set(discId, discTimestamps);
297
+ }
298
+ // Prune old entries
299
+ while (discTimestamps.length > 0 && discTimestamps[0] < cutoff) {
300
+ discTimestamps.shift();
301
+ }
302
+ if (discTimestamps.length >= RATE_LIMIT_PER_DISCUSSION) {
303
+ this.logger.warn('Per-discussion rate limit exceeded', {
304
+ discussionId: discId,
305
+ count: discTimestamps.length,
306
+ window: RATE_LIMIT_WINDOW,
307
+ });
308
+ return;
309
+ }
310
+ discTimestamps.push(now);
311
+ // Global rate limit
312
+ while (this.globalSignalTimestamps.length > 0 && this.globalSignalTimestamps[0] < cutoff) {
313
+ this.globalSignalTimestamps.shift();
314
+ }
315
+ if (this.globalSignalTimestamps.length >= RATE_LIMIT_GLOBAL) {
316
+ this.logger.warn('Global rate limit exceeded', {
317
+ count: this.globalSignalTimestamps.length,
318
+ window: RATE_LIMIT_WINDOW,
319
+ });
320
+ return;
321
+ }
322
+ this.globalSignalTimestamps.push(now);
323
+ // Membership check - only respond to users who are members of the bot's workspace
324
+ if (this.init?.users && !this.init.users[message.senderId]) {
325
+ this.logger.debug('Ignoring message from non-workspace member', {
326
+ senderId: message.senderId,
327
+ senderName: message.senderName,
328
+ });
329
+ if (rawMsgId)
330
+ this.processedMessageIds.delete(rawMsgId);
331
+ return;
332
+ }
333
+ // Prune dedup set
334
+ if (this.processedMessageIds.size > 500) {
335
+ const ids = Array.from(this.processedMessageIds);
336
+ this.processedMessageIds = new Set(ids.slice(-250));
337
+ }
338
+ const state = this.getOrCreateDiscussionState(discId);
339
+ // Always buffer the message
340
+ state.contextBuffer.push(message);
341
+ // Determine if this should trigger processing
342
+ const isExplicitTrigger = message.isMention || message.isReplyToBot || message.isDirectMessage;
343
+ if (state.state === 'idle') {
344
+ if (isExplicitTrigger) {
345
+ state.state = 'engaged';
346
+ state.consecutiveNonResponses = 0;
347
+ this.opLogger.engage(discId, message.isMention ? 'mention' : message.isReplyToBot ? 'reply' : 'dm');
348
+ }
349
+ else {
350
+ // Not addressed while idle — message sits in buffer as context only
351
+ return;
352
+ }
353
+ }
354
+ // State is 'engaged' — trigger processing if not already running
355
+ if (!this.processingDiscussions.has(discId)) {
356
+ this.processDiscussion(discId).catch(err => {
357
+ this.logger.error('processDiscussion failed', { discussionId: discId, error: err });
358
+ this.processingDiscussions.delete(discId);
359
+ });
360
+ }
361
+ else {
362
+ // Already processing — message stays in contextBuffer.
363
+ // injectPendingContext delivers it between tool calls as <context>,
364
+ // and the LLM naturally decides whether to continue or stop.
365
+ }
366
+ }
367
+ catch (error) {
368
+ this.logger.error('Failed to handle signal', error);
369
+ }
370
+ }
371
+ async processDiscussion(discussionId) {
372
+ if (this.processingDiscussions.has(discussionId))
373
+ return;
374
+ this.processingDiscussions.add(discussionId);
375
+ const state = this.getOrCreateDiscussionState(discussionId);
376
+ try {
377
+ while (true) {
378
+ // Drain buffer
379
+ const messages = state.contextBuffer.splice(0);
380
+ if (messages.length === 0)
381
+ break;
382
+ if (messages.length > 1) {
383
+ this.opLogger.coalesce(discussionId, messages.length);
384
+ }
385
+ // Merge all buffered messages into a single user message (conversation alternation)
386
+ const conversation = this.conversationManager.getConversation(discussionId);
387
+ const parts = messages.map(msg => this.formatIncomingMessage(msg));
388
+ let merged = parts.join('\n\n');
389
+ if (messages.length > 1) {
390
+ merged = `<context type="coalesced" count="${messages.length}">These ${messages.length} messages arrived while you were processing. Respond to the most recent/relevant.</context>\n\n${merged}`;
391
+ }
392
+ conversation.push({ role: 'user', content: merged });
393
+ // Use last message for routing/billing context
394
+ const primaryMessage = messages[messages.length - 1];
395
+ state.lastProgressTime = 0;
396
+ state.abortController = new AbortController();
397
+ // Log each incoming message
398
+ for (const msg of messages) {
399
+ this.opLogger.messageIn(msg.discussionId, msg.senderName, msg.content);
400
+ }
401
+ // Process — the LLM decides whether to respond
402
+ let responded;
403
+ try {
404
+ responded = await this.processMessage(primaryMessage, state.abortController.signal);
405
+ }
406
+ catch (error) {
407
+ if (error instanceof sdk_1.default.APIUserAbortError) {
408
+ // Aborted — new message arrived, loop will restart with updated buffer
409
+ this.typingIndicator?.stop(discussionId);
410
+ continue;
411
+ }
412
+ throw error;
413
+ }
414
+ if (responded) {
415
+ state.consecutiveNonResponses = 0;
416
+ }
417
+ else {
418
+ state.consecutiveNonResponses++;
419
+ if (state.consecutiveNonResponses >= Bot.DISENGAGE_THRESHOLD) {
420
+ state.state = 'idle';
421
+ this.opLogger.disengage(discussionId, state.consecutiveNonResponses);
422
+ break;
423
+ }
424
+ }
425
+ // Check if more messages arrived during processing
426
+ if (state.contextBuffer.length === 0)
427
+ break;
428
+ // If disengaged, only continue if there's an explicit trigger
429
+ if (state.state === 'idle') {
430
+ const hasExplicit = state.contextBuffer.some(m => m.isMention || m.isReplyToBot || m.isDirectMessage);
431
+ if (!hasExplicit)
432
+ break;
433
+ state.state = 'engaged';
434
+ state.consecutiveNonResponses = 0;
435
+ this.opLogger.engage(discussionId, 'reengaged');
436
+ }
437
+ }
438
+ }
439
+ finally {
440
+ this.processingDiscussions.delete(discussionId);
441
+ state.abortController = null;
442
+ }
443
+ }
444
+ // ===== MODEL ROUTING =====
445
+ /**
446
+ * Classify message complexity using a lightweight Haiku call.
447
+ * Returns model + max_tokens to use for the main LLM loop.
448
+ * Falls back to Haiku on any error.
449
+ */
450
+ async routeMessage(message, conversation, signal) {
451
+ const defaultRoute = {
452
+ model: MODEL_HAIKU,
453
+ maxTokens: 2000,
454
+ classification: 'SIMPLE',
455
+ };
456
+ try {
457
+ const wsName = this.init?.network?.name || 'Workspace';
458
+ const recentContext = this.getRecentContext(conversation);
459
+ const contextBlock = recentContext
460
+ ? `\nRecent context:\n${recentContext}\n`
461
+ : '';
462
+ const response = await this.anthropic.messages.create({
463
+ model: MODEL_HAIKU,
464
+ max_tokens: 10,
465
+ temperature: 0,
466
+ messages: [
467
+ {
468
+ role: 'user',
469
+ content: `Classify this chat message as SIMPLE or COMPLEX.
470
+
471
+ SIMPLE = can be answered in 1-2 tool calls with straightforward logic.
472
+ COMPLEX = needs 3+ tool calls, reasoning across multiple data sources, fixing/creating structured data, or investigation.
473
+
474
+ When in doubt, classify as COMPLEX.
475
+
476
+ Workspace: ${wsName}
477
+ ${contextBlock}Current message: ${message.content}
478
+
479
+ Reply with exactly one word: SIMPLE or COMPLEX`,
480
+ },
481
+ ],
482
+ }, { signal });
483
+ // Bill the router call
484
+ this.trackRouterTokenUsage(response, message);
485
+ // Parse classification
486
+ const text = response.content
487
+ .filter((b) => b.type === 'text')
488
+ .map(b => b.text)
489
+ .join('')
490
+ .trim()
491
+ .toUpperCase();
492
+ if (text.includes('COMPLEX')) {
493
+ const route = {
494
+ model: MODEL_SONNET,
495
+ maxTokens: 16384,
496
+ classification: 'COMPLEX',
497
+ };
498
+ this.opLogger.route(message.discussionId, route.classification, route.model, message.content);
499
+ return route;
500
+ }
501
+ this.opLogger.route(message.discussionId, defaultRoute.classification, defaultRoute.model, message.content);
502
+ return defaultRoute;
503
+ }
504
+ catch (error) {
505
+ this.logger.warn('Router failed, falling back to Haiku', {
506
+ error: error instanceof Error ? error.message : String(error),
507
+ });
508
+ return defaultRoute;
509
+ }
510
+ }
511
+ /**
512
+ * Extract recent conversation text for the router prompt.
513
+ * Walks backwards, extracts only text blocks (no tool calls/results),
514
+ * truncates each to 200 chars. Returns up to 3 recent exchanges.
515
+ */
516
+ getRecentContext(conversation) {
517
+ const lines = [];
518
+ let count = 0;
519
+ for (let i = conversation.length - 1; i >= 0 && count < 3; i--) {
520
+ const msg = conversation[i];
521
+ const content = msg.content;
522
+ let text = '';
523
+ if (typeof content === 'string') {
524
+ text = content;
525
+ }
526
+ else if (Array.isArray(content)) {
527
+ text = content
528
+ .filter((b) => b.type === 'text')
529
+ .map((b) => b.text)
530
+ .join(' ');
531
+ }
532
+ if (!text)
533
+ continue;
534
+ const truncated = text.length > 200 ? text.slice(0, 200) + '...' : text;
535
+ const role = msg.role === 'user' ? 'User' : 'Assistant';
536
+ lines.unshift(`${role}: ${truncated}`);
537
+ count++;
538
+ }
539
+ return lines.join('\n');
540
+ }
541
+ /**
542
+ * Bill the router classification call the same way as main LLM calls.
543
+ */
544
+ trackRouterTokenUsage(response, message) {
545
+ if (!response.usage)
546
+ return;
547
+ const { input_tokens, output_tokens } = response.usage;
548
+ const cacheCreation = response.usage.cache_creation_input_tokens || 0;
549
+ const cacheRead = response.usage.cache_read_input_tokens || 0;
550
+ const burnWorkspaceId = this._workspaceId || message.workspaceId;
551
+ if (this.tokenBilling && burnWorkspaceId) {
552
+ const cost = this.tokenBilling.calculateCost(input_tokens, output_tokens, cacheCreation, cacheRead, MODEL_HAIKU);
553
+ this.tokenBilling.burnTokens({
554
+ workspaceId: burnWorkspaceId,
555
+ inputTokens: input_tokens,
556
+ outputTokens: output_tokens,
557
+ cacheCreationTokens: cacheCreation,
558
+ cacheReadTokens: cacheRead,
559
+ costUsd: cost,
560
+ sessionId: message.discussionId,
561
+ model: MODEL_HAIKU,
562
+ });
563
+ }
564
+ }
565
+ // ===== MESSAGE PROCESSING =====
566
+ async processMessage(message, signal) {
567
+ this.typingIndicator?.start(message.discussionId);
568
+ // Check token balance
569
+ const billingWorkspaceId = this._workspaceId || message.workspaceId;
570
+ if (this.tokenBilling && billingWorkspaceId) {
571
+ const balance = await this.tokenBilling.checkBalance(billingWorkspaceId);
572
+ const balanceStatus = balance.hasBalance ? (balance.balance < 5 ? 'LOW' : 'OK') : 'EMPTY';
573
+ this.opLogger.balanceCheck(message.discussionId, billingWorkspaceId, balance.balance, balanceStatus);
574
+ if (!balance.hasBalance) {
575
+ this.typingIndicator?.stop(message.discussionId);
576
+ await this.sendMessage(message.discussionId, 'Insufficient balance. Please top up your workspace AI credits to continue.');
577
+ return true; // We did respond (with balance error)
578
+ }
579
+ }
580
+ // Manage context size
581
+ this.conversationManager.manageContextSize(message.discussionId);
582
+ const conversation = this.conversationManager.getConversation(message.discussionId);
583
+ const snapshotLength = conversation.length;
584
+ try {
585
+ const route = await this.routeMessage(message, conversation, signal);
586
+ return await this.runLlmLoop(message, route, signal);
587
+ }
588
+ catch (error) {
589
+ if (error instanceof sdk_1.default.APIUserAbortError) {
590
+ throw error; // Let processDiscussion handle abort
591
+ }
592
+ this.logger.error('Message processing failed', error);
593
+ this.typingIndicator?.stop(message.discussionId);
594
+ conversation.length = snapshotLength;
595
+ return false;
596
+ }
597
+ }
598
+ async runLlmLoop(message, route, signal) {
599
+ const conversation = this.conversationManager.getConversation(message.discussionId);
600
+ const systemPrompt = this.buildSystemPrompt();
601
+ const tools = this.getAnthropicTools();
602
+ const processingStartTime = Date.now();
603
+ for (let i = 0; i < MAX_TOOL_ITERATIONS; i++) {
604
+ const cachedConversation = this.conversationManager.prepareForCaching(conversation);
605
+ let response;
606
+ let llmStart = Date.now();
607
+ try {
608
+ response = await this.anthropic.messages.create({
609
+ model: route.model,
610
+ max_tokens: route.maxTokens,
611
+ temperature: 0,
612
+ system: [{ type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } }],
613
+ messages: cachedConversation,
614
+ tools,
615
+ }, { signal });
616
+ }
617
+ catch (error) {
618
+ // Handle abort — new message arrived, restart processing loop
619
+ if (error instanceof sdk_1.default.APIUserAbortError) {
620
+ this.cleanupIncompleteExchange(conversation);
621
+ this.opLogger.interrupt(message.discussionId, 'new message received');
622
+ return false; // processDiscussion loop restarts with full context
623
+ }
624
+ // If Sonnet fails, fall back to Haiku and retry once
625
+ if (route.model !== MODEL_HAIKU) {
626
+ this.logger.warn('Sonnet API call failed, falling back to Haiku', {
627
+ error: error instanceof Error ? error.message : String(error),
628
+ });
629
+ route = { model: MODEL_HAIKU, maxTokens: 2000, classification: 'SIMPLE' };
630
+ llmStart = Date.now();
631
+ response = await this.anthropic.messages.create({
632
+ model: MODEL_HAIKU,
633
+ max_tokens: 2000,
634
+ temperature: 0,
635
+ system: [{ type: 'text', text: systemPrompt, cache_control: { type: 'ephemeral' } }],
636
+ messages: cachedConversation,
637
+ tools,
638
+ }, { signal });
639
+ }
640
+ else {
641
+ throw error;
642
+ }
643
+ }
644
+ // Track token usage
645
+ this.trackTokenUsage(response, message, route.model);
646
+ // Log LLM call with timing and cache stats
647
+ const llmDuration = (Date.now() - llmStart) / 1000;
648
+ const usage = response.usage;
649
+ const cacheRead = usage.cache_read_input_tokens || 0;
650
+ const totalInput = usage.input_tokens + cacheRead + (usage.cache_creation_input_tokens || 0);
651
+ const cacheHitPct = totalInput > 0 ? Math.round((cacheRead / totalInput) * 100) : 0;
652
+ this.opLogger.llmCall(message.discussionId, route.model, usage.input_tokens, usage.output_tokens, cacheHitPct, llmDuration);
653
+ // Validate assistant response has content before adding to conversation
654
+ if (!response.content || (Array.isArray(response.content) && response.content.length === 0)) {
655
+ this.logger.warn('LLM returned empty content, skipping', {
656
+ discussionId: message.discussionId,
657
+ model: route.model,
658
+ stopReason: response.stop_reason,
659
+ });
660
+ break;
661
+ }
662
+ // Add assistant response to conversation
663
+ conversation.push({
664
+ role: 'assistant',
665
+ content: response.content,
666
+ });
667
+ // Check for output truncation — if max_tokens was hit, tool call JSON is likely broken
668
+ if (response.stop_reason === 'max_tokens') {
669
+ this.logger.warn('LLM output truncated at max_tokens', {
670
+ discussionId: message.discussionId,
671
+ model: route.model,
672
+ maxTokens: route.maxTokens,
673
+ outputTokens: usage.output_tokens,
674
+ });
675
+ // Don't execute broken tool calls — tell the LLM to use smaller batches
676
+ conversation.push({
677
+ role: 'user',
678
+ content: 'Your previous response was truncated because it exceeded the output token limit. Do NOT try to generate all items at once. Break it into smaller batches of 25-50 items per tool call. You have multiple iterations available.',
679
+ });
680
+ continue;
681
+ }
682
+ // Check for tool calls
683
+ const toolUseBlocks = response.content.filter((b) => b.type === 'tool_use');
684
+ if (toolUseBlocks.length > 0) {
685
+ const toolResults = await this.executeTools(toolUseBlocks, message, signal);
686
+ conversation.push({ role: 'user', content: toolResults });
687
+ // Inject any messages that arrived during tool execution
688
+ this.injectPendingContext(message.discussionId, conversation);
689
+ // Auto-escalate: if Haiku is failing tool calls, switch to Sonnet
690
+ if (route.model === MODEL_HAIKU) {
691
+ const failedCount = toolResults
692
+ .filter(r => r.is_error).length;
693
+ if (failedCount >= 2) {
694
+ this.logger.info('Escalating to Sonnet — Haiku failing tool calls', {
695
+ discussionId: message.discussionId,
696
+ failedTools: failedCount,
697
+ iteration: i,
698
+ });
699
+ route = { model: MODEL_SONNET, maxTokens: 16384, classification: 'COMPLEX' };
700
+ await this.sendMessage(message.discussionId, `Switching to a more capable model to handle this.`);
701
+ }
702
+ }
703
+ // Progress feedback: send contextual update every 20s during long operations
704
+ const state = this.discussionStates.get(message.discussionId);
705
+ const now = Date.now();
706
+ const elapsed = now - processingStartTime;
707
+ const lastProgress = state?.lastProgressTime || processingStartTime;
708
+ if (state && elapsed > 8000 && (now - lastProgress) >= 20000) {
709
+ state.lastProgressTime = now;
710
+ const toolCounts = new Map();
711
+ for (const b of toolUseBlocks) {
712
+ const name = b.name.replace(/_/g, ' ');
713
+ toolCounts.set(name, (toolCounts.get(name) || 0) + 1);
714
+ }
715
+ const toolSummary = Array.from(toolCounts.entries())
716
+ .map(([name, count]) => count > 1 ? `${name} x${count}` : name)
717
+ .join(', ');
718
+ const progressMsg = `Still working (step ${i + 1}) — ${toolSummary}`;
719
+ await this.sendMessage(message.discussionId, progressMsg);
720
+ this.opLogger.progress(message.discussionId);
721
+ }
722
+ continue; // Loop back for more tool calls or final response
723
+ }
724
+ // Extract text response
725
+ const textBlocks = response.content.filter((b) => b.type === 'text');
726
+ const responseText = textBlocks.map(b => b.text).join('\n').trim();
727
+ if (!responseText) {
728
+ // Remove the empty assistant message we just pushed
729
+ conversation.pop();
730
+ break;
731
+ }
732
+ // Post the response
733
+ this.typingIndicator?.stop(message.discussionId);
734
+ const formatted = await this.formatOutgoingMessage(responseText);
735
+ const links = this.messageFormatter.extractTagLinks(formatted);
736
+ await this.sendMessage(message.discussionId, formatted, links);
737
+ return true;
738
+ }
739
+ // Max iterations reached or empty response
740
+ this.typingIndicator?.stop(message.discussionId);
741
+ return false;
742
+ }
743
+ async executeTools(toolUseBlocks, message, signal) {
744
+ const discussionId = message.discussionId;
745
+ const results = [];
746
+ const WRITE_TOOLS = new Set(['create_activity', 'update_activity']);
747
+ for (const block of toolUseBlocks) {
748
+ const toolStart = Date.now();
749
+ // Permission gate: check if user has access to the workflow
750
+ const args = block.input;
751
+ const workflowId = args.workflowId;
752
+ if (workflowId && WORKFLOW_SCOPED_TOOLS.has(block.name)) {
753
+ const allowed = this.permissionIndex.get(workflowId);
754
+ if (allowed && !allowed.has(message.senderId) && !this.adminUserIds.has(message.senderId) && !this.ownerUserIds.has(message.senderId)) {
755
+ this.opLogger.permDenied(discussionId, block.name, workflowId, message.senderId);
756
+ results.push({
757
+ type: 'tool_result',
758
+ tool_use_id: block.id,
759
+ content: `Permission denied: This user does not have access to workflow ${workflowId}. This is a workspace permission restriction on the user, not on you (the bot).`,
760
+ is_error: true,
761
+ });
762
+ continue;
763
+ }
764
+ }
765
+ try {
766
+ let toolArgs = args;
767
+ // Auto-inject context for join_discussion calls
768
+ if (block.name === 'join_discussion') {
769
+ toolArgs = { ...toolArgs };
770
+ // Auto-inject inviteUserId from the sender if not already set
771
+ if (!toolArgs.inviteUserId && message.senderId) {
772
+ toolArgs.inviteUserId = message.senderId;
773
+ }
774
+ // Auto-inject sourceActivityId for wormhole (only if from an activity discussion, not DMs)
775
+ if (!toolArgs.sourceActivityId && message.linkedActivityId) {
776
+ toolArgs.sourceActivityId = message.linkedActivityId;
777
+ }
778
+ // Auto-inject welcomeReason with the actual message content for context
779
+ if (!toolArgs.welcomeReason && message.content) {
780
+ toolArgs.welcomeReason = message.content;
781
+ }
782
+ }
783
+ const result = await this.toolExecutor.execute(block.name, toolArgs, this.getUserContext());
784
+ const text = result?.content?.[0]?.text ?? JSON.stringify(result);
785
+ let contentStr = typeof text === 'string' ? text : JSON.stringify(text);
786
+ const toolDuration = (Date.now() - toolStart) / 1000;
787
+ // Post-filter list_workflows results to only show accessible workflows
788
+ if ((block.name === 'list_workflows' || block.name === 'list_workflows_minimal') && this.permissionIndex.size > 0 && !this.adminUserIds.has(message.senderId) && !this.ownerUserIds.has(message.senderId)) {
789
+ try {
790
+ const parsed = JSON.parse(contentStr);
791
+ if (parsed.workflows && Array.isArray(parsed.workflows)) {
792
+ const before = parsed.workflows.length;
793
+ parsed.workflows = parsed.workflows.filter((wf) => {
794
+ const wfId = wf.id || wf._id || wf.workflowId;
795
+ if (!wfId)
796
+ return true;
797
+ const allowed = this.permissionIndex.get(wfId);
798
+ return !allowed || allowed.has(message.senderId);
799
+ });
800
+ if (parsed.workflows.length < before) {
801
+ this.opLogger.permFiltered(discussionId, block.name, before, parsed.workflows.length);
802
+ }
803
+ contentStr = JSON.stringify(parsed);
804
+ }
805
+ }
806
+ catch {
807
+ // Not JSON or unexpected format, pass through unfiltered
808
+ }
809
+ }
810
+ // Post-execution permission check for tools not gated by workflowId arg
811
+ if (this.permissionIndex.size > 0 && UNGATED_TOOLS.has(block.name)) {
812
+ const extractedWorkflowId = this.extractWorkflowIdFromResult(block.name, contentStr);
813
+ if (extractedWorkflowId) {
814
+ const allowed = this.permissionIndex.get(extractedWorkflowId);
815
+ if (allowed && !allowed.has(message.senderId) && !this.adminUserIds.has(message.senderId) && !this.ownerUserIds.has(message.senderId)) {
816
+ this.opLogger.permDenied(discussionId, block.name, extractedWorkflowId, message.senderId);
817
+ results.push({
818
+ type: 'tool_result',
819
+ tool_use_id: block.id,
820
+ content: `Permission denied: This user does not have access to the resource in workflow ${extractedWorkflowId}. This is a workspace permission restriction on the user, not on you (the bot).`,
821
+ is_error: true,
822
+ });
823
+ continue;
824
+ }
825
+ }
826
+ }
827
+ // Check if tool returned an error in its response text
828
+ const lower = contentStr.toLowerCase();
829
+ const stripped = contentStr.replace(/[^\w]/g, ' ').trimStart().toLowerCase();
830
+ const isToolError = lower.includes('"error"') || lower.includes('❌') || stripped.startsWith('error');
831
+ const summary = (0, operation_logger_1.summarizeToolResult)(block.name, contentStr);
832
+ if (isToolError) {
833
+ this.opLogger.toolCall(discussionId, block.name, 'error', toolDuration, 'FAIL', summary);
834
+ }
835
+ else {
836
+ this.opLogger.toolCall(discussionId, block.name, summary, toolDuration, 'OK');
837
+ }
838
+ // Warn if a write tool completed before abort — not idempotent
839
+ if (signal?.aborted && WRITE_TOOLS.has(block.name)) {
840
+ this.logger.warn('Write tool completed before abort', {
841
+ discussionId, tool: block.name,
842
+ });
843
+ }
844
+ results.push({
845
+ type: 'tool_result',
846
+ tool_use_id: block.id,
847
+ content: contentStr,
848
+ });
849
+ }
850
+ catch (error) {
851
+ const toolDuration = (Date.now() - toolStart) / 1000;
852
+ const errMsg = error instanceof Error ? error.message : String(error);
853
+ this.opLogger.toolCall(discussionId, block.name, 'error', toolDuration, 'FAIL', errMsg);
854
+ results.push({
855
+ type: 'tool_result',
856
+ tool_use_id: block.id,
857
+ content: `Error: ${errMsg}`,
858
+ is_error: true,
859
+ });
860
+ }
861
+ }
862
+ return results;
863
+ }
864
+ /**
865
+ * Inject pending messages from the context buffer into the conversation
866
+ * between LLM iterations. Merges into the existing user message to preserve
867
+ * Anthropic's alternation rule (user/assistant/user/assistant).
868
+ */
869
+ injectPendingContext(discussionId, conversation) {
870
+ const state = this.discussionStates.get(discussionId);
871
+ if (!state || state.contextBuffer.length === 0)
872
+ return;
873
+ const pending = state.contextBuffer.splice(0);
874
+ const lastMsg = conversation[conversation.length - 1];
875
+ if (lastMsg?.role === 'user' && Array.isArray(lastMsg.content)) {
876
+ const contextText = pending.map(m => this.formatIncomingMessage(m)).join('\n\n');
877
+ lastMsg.content.push({
878
+ type: 'text',
879
+ text: `<context type="messages-during-processing">\n${contextText}\n</context>`,
880
+ });
881
+ this.opLogger.contextInject(discussionId, pending.length);
882
+ }
883
+ }
884
+ /**
885
+ * Remove orphaned tool_use/tool_result pairs from conversation after abort.
886
+ * Walks backwards and removes incomplete exchanges to keep conversation valid.
887
+ */
888
+ cleanupIncompleteExchange(conversation) {
889
+ while (conversation.length > 0) {
890
+ const last = conversation[conversation.length - 1];
891
+ const content = Array.isArray(last.content) ? last.content : [];
892
+ if (last.role === 'assistant' && content.some((b) => b.type === 'tool_use')) {
893
+ conversation.pop();
894
+ continue;
895
+ }
896
+ if (last.role === 'user' && content.some((b) => b.type === 'tool_result')) {
897
+ conversation.pop();
898
+ continue;
899
+ }
900
+ break;
901
+ }
902
+ }
903
+ // ===== FORMATTING =====
904
+ formatIncomingMessage(message) {
905
+ const activityAttr = message.linkedActivityId ? ` activity_id="${message.linkedActivityId}"` : '';
906
+ const discName = message.discussionName || message.linkedActivityName;
907
+ const nameAttr = discName ? ` discussion_name="${discName}"` : '';
908
+ let fileInfo = '';
909
+ if (message.fileAttachments?.length) {
910
+ const ids = message.fileAttachments.map(f => f.fileId).join(', ');
911
+ fileInfo = `\n[File attached: ${message.fileAttachments.length} file(s) - IDs: ${ids}]\nUse download_file tool with fileId to read file contents.`;
912
+ }
913
+ return `<incoming discussion="${message.discussionId}"${nameAttr}${activityAttr} from="${message.senderName}" user_id="${message.senderId}" timestamp="${new Date(message.timestamp).toISOString()}">\n${message.content}${fileInfo}\n</incoming>`;
914
+ }
915
+ async formatOutgoingMessage(text) {
916
+ let formatted = await this.messageFormatter.resolveUserTags(text);
917
+ formatted = await this.messageFormatter.resolveActivityTags(formatted);
918
+ formatted = await this.messageFormatter.resolveHailerUrls(formatted);
919
+ formatted = this.messageFormatter.convertMentionsToTags(formatted);
920
+ // Remove redundant name after tags
921
+ formatted = formatted.replace(/(\[hailerTag\|[^\]]+\]\([a-f0-9]{24}\)\uFEFF?)\s*\([^)]+\)/gi, '$1');
922
+ // Auto-resolve bare 24-char hex IDs not already in hailerTag format
923
+ formatted = await this.resolveBareIds(formatted);
924
+ return formatted;
925
+ }
926
+ /**
927
+ * Find bare 24-char hex IDs in text that aren't already inside hailerTags
928
+ * and try to resolve them to clickable hailerTags.
929
+ */
930
+ async resolveBareIds(text) {
931
+ // Match 24-char hex IDs that are NOT preceded by ( which would mean they're already in a hailerTag
932
+ const bareIdPattern = /(?<!\()([a-f0-9]{24})(?!\)[^\[]*\[hailerTag)/gi;
933
+ const matches = [...text.matchAll(bareIdPattern)];
934
+ // Collect IDs that aren't already in hailerTags, limit to 5
935
+ const idsToResolve = [];
936
+ for (const match of matches) {
937
+ const id = match[1];
938
+ const tagCheck = new RegExp(`\\[hailerTag\\|[^\\]]+\\]\\(${id}\\)`, 'i');
939
+ if (tagCheck.test(text))
940
+ continue;
941
+ idsToResolve.push(id);
942
+ if (idsToResolve.length >= 5)
943
+ break;
944
+ }
945
+ if (idsToResolve.length === 0)
946
+ return text;
947
+ // Resolve all IDs in parallel
948
+ const resolutions = await Promise.all(idsToResolve.map(async (id) => {
949
+ try {
950
+ const result = await this.toolExecutor.execute('show_activity_by_id', { activityId: id }, this.getUserContext());
951
+ const resultText = result?.content?.[0]?.text;
952
+ if (resultText) {
953
+ const jsonMatch = resultText.match(/\{[\s\S]*\}/);
954
+ if (jsonMatch) {
955
+ const parsed = JSON.parse(jsonMatch[0]);
956
+ if (parsed.name)
957
+ return { id, name: parsed.name };
958
+ }
959
+ }
960
+ }
961
+ catch {
962
+ // Not an activity, skip
963
+ }
964
+ return null;
965
+ }));
966
+ // Apply replacements
967
+ for (const resolved of resolutions) {
968
+ if (!resolved)
969
+ continue;
970
+ const ZWNBSP = '\uFEFF';
971
+ const tag = `${ZWNBSP}[hailerTag|${resolved.name}](${resolved.id})${ZWNBSP}`;
972
+ text = text.replace(resolved.id, tag);
973
+ }
974
+ return text;
975
+ }
976
+ // ===== SYSTEM PROMPT =====
977
+ getBotDisplayName() {
978
+ if (!this.init?.users || !this.userId)
979
+ return 'Assistant';
980
+ const user = this.init.users[this.userId];
981
+ if (!user)
982
+ return 'Assistant';
983
+ const name = user.display_name || user.fullName ||
984
+ `${user.firstname || ''} ${user.lastname || ''}`.trim();
985
+ return name || 'Assistant';
986
+ }
987
+ buildSystemPrompt() {
988
+ const wsName = this.init?.network?.name || 'Workspace';
989
+ const botName = this.getBotDisplayName();
990
+ return `You are a workspace assistant for "${wsName}" on Hailer.
991
+
992
+ <bot-identity>
993
+ Your user ID: ${this.userId}
994
+ Your display name: ${botName}
995
+ </bot-identity>
996
+
997
+ <rules>
998
+ - Be concise. Short answers, no filler.
999
+ - Never list your capabilities unprompted. If asked what you can do, give a 1-2 sentence summary based on the workspace context below, not a feature dump.
1000
+ - Never use emojis unless the user does first.
1001
+ - Use tools to answer questions - don't guess or make up data.
1002
+ - When a task will require multiple steps (investigating errors, fixing data, bulk lookups), briefly tell the user what you're specifically about to do before starting. Be concrete: "I'll check each insight's SQL and fix any broken column references." NEVER use vague filler like "Let me look into that" — always name the actual action.
1003
+ - When showing data, use clean formatting (tables, bullet points). Don't over-explain.
1004
+ - If a request is ambiguous, ask a clarifying question instead of guessing.
1005
+ - Always reference actual workspace data (workflow names, field values) - never speak in generic terms.
1006
+ - For bulk operations (creating/updating many items), batch into groups of 25-50 per tool call. Never try to create more than 50 items in a single tool call.
1007
+ - When a tool call fails, ALWAYS tell the user what failed and why. Never silently try alternatives. If an insight has a SQL error, tell the user the insight is broken, explain what's wrong (e.g. missing column), and offer to fix it using update_insight. Fix the root cause, don't work around it.
1008
+ - NEVER output raw URLs like https://app.hailer.com/...
1009
+ - When referencing activities, discussions, or users, use hailerTag format: [hailerTag|Display Name](objectId)
1010
+ Examples: [hailerTag|AC Milan](691ffe654217e9e8434e578a) or [hailerTag|John Smith](691ffe654217e9e8434e5123)
1011
+ You always have the name and ID from tool responses - use them directly in this format.
1012
+ </rules>
1013
+
1014
+ <workspace-context>
1015
+ ${this.workspaceOverview}
1016
+ </workspace-context>`;
1017
+ }
1018
+ // ===== TOOLS =====
1019
+ getAnthropicTools() {
1020
+ const defs = this.toolExecutor.getToolDefinitions({
1021
+ allowedGroups: [tool_registry_1.ToolGroup.READ, tool_registry_1.ToolGroup.WRITE, tool_registry_1.ToolGroup.PLAYGROUND, tool_registry_1.ToolGroup.BOT_INTERNAL],
1022
+ });
1023
+ return defs
1024
+ .filter(d => BOT_TOOLS.has(d.name))
1025
+ .map(d => ({
1026
+ name: d.name,
1027
+ description: d.description,
1028
+ input_schema: d.inputSchema,
1029
+ }));
1030
+ }
1031
+ // ===== HAILER MESSAGING =====
1032
+ async sendMessage(discussionId, text, links) {
1033
+ try {
1034
+ const msgData = { msg: text };
1035
+ if (links?.length)
1036
+ msgData.links = links;
1037
+ await this.client.socket.request('messenger.send', [msgData, discussionId]);
1038
+ const tagCount = (text.match(/\[hailerTag\|/g) || []).length;
1039
+ this.opLogger.messageOut(discussionId, text.length, tagCount);
1040
+ }
1041
+ catch (error) {
1042
+ this.logger.error('Failed to send message', error);
1043
+ }
1044
+ }
1045
+ // ===== HELPERS =====
1046
+ /**
1047
+ * Schedule a workspace data refresh with debounce.
1048
+ * Signals can fire in bursts (e.g., bulk field edits), so we debounce
1049
+ * to avoid hammering the API. 2-second delay is effectively instant
1050
+ * from a user perspective but handles bursts.
1051
+ */
1052
+ scheduleRefresh(scope) {
1053
+ this.refreshDirty = true;
1054
+ if (scope === 'full') {
1055
+ this.refreshScope.clear();
1056
+ this.refreshScope.add('full');
1057
+ }
1058
+ else if (!this.refreshScope.has('full')) {
1059
+ this.refreshScope.add(scope);
1060
+ }
1061
+ if (this.refreshTimer)
1062
+ clearTimeout(this.refreshTimer);
1063
+ this.refreshTimer = setTimeout(() => this.refreshWorkspaceData(), 2000);
1064
+ }
1065
+ /**
1066
+ * Re-fetch workspace data and rebuild the overview.
1067
+ * Called when process.updated, workspace.updated, or cache.invalidate signals fire.
1068
+ */
1069
+ async refreshWorkspaceData() {
1070
+ if (!this.refreshDirty || !this.client)
1071
+ return;
1072
+ if (this.isRefreshing) {
1073
+ this.refreshQueued = true;
1074
+ return;
1075
+ }
1076
+ this.isRefreshing = true;
1077
+ this.refreshDirty = false;
1078
+ const scopes = new Set(this.refreshScope);
1079
+ this.refreshScope.clear();
1080
+ this.refreshTimer = null;
1081
+ try {
1082
+ // Determine what to re-fetch based on signal types
1083
+ const isFull = scopes.has('full');
1084
+ const keys = isFull
1085
+ ? ['processes', 'users', 'network', 'networks', 'teams', 'groups']
1086
+ : Array.from(scopes);
1087
+ // Always include users with processes (membership check depends on it)
1088
+ if (keys.includes('processes') && !keys.includes('users')) {
1089
+ keys.push('users');
1090
+ }
1091
+ this.logger.debug('Refreshing workspace data', { keys, trigger: isFull ? 'full' : Array.from(scopes).join(',') });
1092
+ const freshData = await this.client.socket.request('v2.core.init', [keys]);
1093
+ // Merge fresh data into existing init (partial update)
1094
+ if (!this.init) {
1095
+ this.init = freshData;
1096
+ }
1097
+ else {
1098
+ if (freshData.processes)
1099
+ this.init.processes = freshData.processes;
1100
+ if (freshData.users)
1101
+ this.init.users = freshData.users;
1102
+ if (freshData.network)
1103
+ this.init.network = freshData.network;
1104
+ if (freshData.networks)
1105
+ this.init.networks = freshData.networks;
1106
+ if (freshData.teams)
1107
+ this.init.teams = freshData.teams;
1108
+ if (freshData.groups)
1109
+ this.init.groups = freshData.groups;
1110
+ }
1111
+ this._workspaceId = this.init.network?._id || this.config.workspaceId;
1112
+ // Rebuild workspace cache
1113
+ this.workspaceCache = (0, workspace_cache_1.createWorkspaceCache)(this.init, {
1114
+ excludeTranslations: true,
1115
+ excludeSystemMessages: true,
1116
+ excludeEmptyFields: true,
1117
+ compactUserData: true,
1118
+ includeWorkspaceNamesInTools: false,
1119
+ });
1120
+ // Rebuild workspace overview for system prompt
1121
+ this.workspaceOverview = (0, workspace_overview_1.generateWorkspaceOverview)(this.init);
1122
+ // Update UserContext with fresh data
1123
+ if (this.userContext) {
1124
+ this.userContext.init = this.init;
1125
+ this.userContext.workspaceCache = this.workspaceCache;
1126
+ }
1127
+ // Rebuild permission index
1128
+ this.buildPermissionIndex();
1129
+ this.logger.debug('Workspace data refreshed', {
1130
+ keys,
1131
+ workflowCount: this.init.processes?.length || 0,
1132
+ userCount: Object.keys(this.init.users || {}).length,
1133
+ });
1134
+ }
1135
+ catch (error) {
1136
+ this.logger.error('Failed to refresh workspace data', error);
1137
+ }
1138
+ finally {
1139
+ this.isRefreshing = false;
1140
+ if (this.refreshQueued) {
1141
+ this.refreshQueued = false;
1142
+ this.refreshWorkspaceData();
1143
+ }
1144
+ }
1145
+ }
1146
+ buildPermissionIndex() {
1147
+ this.permissionIndex.clear();
1148
+ const wsId = this._workspaceId;
1149
+ if (!this.init?.processes || !wsId)
1150
+ return;
1151
+ // Extract workspace admins from network members
1152
+ this.adminUserIds.clear();
1153
+ const network = this.init.network;
1154
+ if (network?.members) {
1155
+ const members = Array.isArray(network.members) ? network.members : Object.values(network.members);
1156
+ for (const member of members) {
1157
+ if (member.admin === true) {
1158
+ this.adminUserIds.add(member.uid);
1159
+ }
1160
+ }
1161
+ }
1162
+ // Extract workspace owners from network members
1163
+ this.ownerUserIds.clear();
1164
+ if (network?.members) {
1165
+ const ownerMembers = Array.isArray(network.members) ? network.members : Object.values(network.members);
1166
+ for (const member of ownerMembers) {
1167
+ if (member.owner === true) {
1168
+ this.ownerUserIds.add(member.uid);
1169
+ }
1170
+ }
1171
+ }
1172
+ const teams = this.init.teams?.[wsId] || {};
1173
+ const groups = this.init.groups?.[wsId] || {};
1174
+ let networkOpenCount = 0;
1175
+ for (const proc of this.init.processes) {
1176
+ const members = proc.members || [];
1177
+ // If any member is network_ (all workspace members), skip indexing — everyone has access
1178
+ if (members.some((m) => m.id.startsWith('network_'))) {
1179
+ networkOpenCount++;
1180
+ continue;
1181
+ }
1182
+ // If any member references a public team, treat as open — all workspace members have access
1183
+ let hasPublicTeam = false;
1184
+ for (const m of members) {
1185
+ if (!m.id.startsWith('team_'))
1186
+ continue;
1187
+ const team = teams[m.id.slice(5)];
1188
+ if (team) {
1189
+ if (team.public) {
1190
+ hasPublicTeam = true;
1191
+ break;
1192
+ }
1193
+ }
1194
+ }
1195
+ if (hasPublicTeam) {
1196
+ networkOpenCount++;
1197
+ continue;
1198
+ }
1199
+ const userIds = new Set();
1200
+ for (const member of members) {
1201
+ const id = member.id;
1202
+ if (id.startsWith('user_')) {
1203
+ userIds.add(id.slice(5));
1204
+ }
1205
+ else if (id.startsWith('team_')) {
1206
+ const team = teams[id.slice(5)];
1207
+ if (team?.members) {
1208
+ for (const uid of team.members) {
1209
+ userIds.add(typeof uid === 'string' ? uid : uid._id || uid.id || uid.uid);
1210
+ }
1211
+ }
1212
+ }
1213
+ else if (id.startsWith('group_')) {
1214
+ const group = groups[id.slice(6)];
1215
+ if (group?.members) {
1216
+ for (const uid of group.members) {
1217
+ userIds.add(typeof uid === 'string' ? uid : uid._id || uid.id || uid.uid);
1218
+ }
1219
+ }
1220
+ }
1221
+ }
1222
+ // Workflow creator always has access
1223
+ if (proc.uid)
1224
+ userIds.add(proc.uid);
1225
+ this.permissionIndex.set(proc._id, userIds);
1226
+ }
1227
+ this.logger.debug('Permission index built', {
1228
+ restricted: this.permissionIndex.size,
1229
+ open: networkOpenCount,
1230
+ totalWorkflows: this.init.processes.length,
1231
+ totalEntries: Array.from(this.permissionIndex.values()).reduce((n, s) => n + s.size, 0),
1232
+ admins: this.adminUserIds.size,
1233
+ owners: this.ownerUserIds.size,
1234
+ });
1235
+ }
1236
+ getOrCreateDiscussionState(discussionId) {
1237
+ let state = this.discussionStates.get(discussionId);
1238
+ if (state) {
1239
+ // LRU: move to end
1240
+ this.discussionStates.delete(discussionId);
1241
+ this.discussionStates.set(discussionId, state);
1242
+ return state;
1243
+ }
1244
+ // Evict oldest if at capacity
1245
+ if (this.discussionStates.size >= 200) {
1246
+ const oldest = this.discussionStates.keys().next().value;
1247
+ if (oldest)
1248
+ this.discussionStates.delete(oldest);
1249
+ }
1250
+ state = {
1251
+ contextBuffer: [],
1252
+ abortController: null,
1253
+ state: 'idle',
1254
+ consecutiveNonResponses: 0,
1255
+ lastProgressTime: 0,
1256
+ };
1257
+ this.discussionStates.set(discussionId, state);
1258
+ return state;
1259
+ }
1260
+ getUserContext() {
1261
+ if (!this.userContext)
1262
+ throw new Error('Bot not started');
1263
+ return this.userContext;
1264
+ }
1265
+ getDefaultTeamId() {
1266
+ const teams = this.init?.teams;
1267
+ if (!teams)
1268
+ return undefined;
1269
+ const workspaceTeams = Object.values(teams)[0];
1270
+ if (!workspaceTeams || typeof workspaceTeams !== 'object')
1271
+ return undefined;
1272
+ return Object.keys(workspaceTeams)[0];
1273
+ }
1274
+ /**
1275
+ * Extract workflow ID from tool result for post-execution permission checks.
1276
+ * Returns the workflow ID if found, or undefined if extraction fails.
1277
+ */
1278
+ extractWorkflowIdFromResult(toolName, resultStr) {
1279
+ try {
1280
+ const jsonMatch = resultStr.match(/\{[\s\S]*\}/);
1281
+ if (!jsonMatch)
1282
+ return undefined;
1283
+ const parsed = JSON.parse(jsonMatch[0]);
1284
+ // show_activity_by_id: workflowId is in the activity result
1285
+ if (toolName === 'show_activity_by_id') {
1286
+ return parsed.workflowId || parsed.workflow_id || parsed.processId;
1287
+ }
1288
+ // get_activity_from_discussion: returns activity with workflow info
1289
+ if (toolName === 'get_activity_from_discussion') {
1290
+ return parsed.workflowId || parsed.workflow_id || parsed.processId
1291
+ || parsed.activity?.workflowId || parsed.activity?.processId;
1292
+ }
1293
+ // fetch_discussion_messages: result may contain a workflowId from linked activity
1294
+ if (toolName === 'fetch_discussion_messages') {
1295
+ return parsed.workflowId || parsed.workflow_id || parsed.processId
1296
+ || parsed.activity?.workflowId || parsed.activity?.processId;
1297
+ }
1298
+ }
1299
+ catch {
1300
+ // JSON parse failed, cannot extract
1301
+ }
1302
+ return undefined;
1303
+ }
1304
+ trackTokenUsage(response, message, model) {
1305
+ if (!response.usage)
1306
+ return;
1307
+ const { input_tokens, output_tokens } = response.usage;
1308
+ const cacheCreation = response.usage.cache_creation_input_tokens || 0;
1309
+ const cacheRead = response.usage.cache_read_input_tokens || 0;
1310
+ // Burn tokens - always use bot's workspace, not the discussion's workspace
1311
+ const burnWorkspaceId = this._workspaceId || message.workspaceId;
1312
+ if (this.tokenBilling && burnWorkspaceId) {
1313
+ const cost = this.tokenBilling.calculateCost(input_tokens, output_tokens, cacheCreation, cacheRead, model);
1314
+ this.tokenBilling.burnTokens({
1315
+ workspaceId: burnWorkspaceId,
1316
+ inputTokens: input_tokens,
1317
+ outputTokens: output_tokens,
1318
+ cacheCreationTokens: cacheCreation,
1319
+ cacheReadTokens: cacheRead,
1320
+ costUsd: cost,
1321
+ sessionId: message.discussionId,
1322
+ model,
1323
+ });
1324
+ }
1325
+ }
1326
+ }
1327
+ exports.Bot = Bot;
1328
+ //# sourceMappingURL=bot.js.map