funolio-agent 1.0.47 → 1.0.48

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 (169) hide show
  1. package/dist/agent-config.d.ts +9 -1
  2. package/dist/agent-config.d.ts.map +1 -1
  3. package/dist/agent-config.js +4 -1
  4. package/dist/agent-config.js.map +1 -1
  5. package/dist/auto-organizer.d.ts.map +1 -1
  6. package/dist/auto-organizer.js +4 -3
  7. package/dist/auto-organizer.js.map +1 -1
  8. package/dist/backfill.d.ts.map +1 -1
  9. package/dist/backfill.js +3 -2
  10. package/dist/backfill.js.map +1 -1
  11. package/dist/bot-manager.d.ts +7 -23
  12. package/dist/bot-manager.d.ts.map +1 -1
  13. package/dist/bot-manager.js +52 -388
  14. package/dist/bot-manager.js.map +1 -1
  15. package/dist/clerk-model.d.ts +5 -1
  16. package/dist/clerk-model.d.ts.map +1 -1
  17. package/dist/clerk-model.js +40 -28
  18. package/dist/clerk-model.js.map +1 -1
  19. package/dist/cli-session-epoch.d.ts +10 -0
  20. package/dist/cli-session-epoch.d.ts.map +1 -0
  21. package/dist/cli-session-epoch.js +61 -0
  22. package/dist/cli-session-epoch.js.map +1 -0
  23. package/dist/commands/init.d.ts.map +1 -1
  24. package/dist/commands/init.js +30 -1
  25. package/dist/commands/init.js.map +1 -1
  26. package/dist/commands/pool.js +1 -1
  27. package/dist/commands/pool.js.map +1 -1
  28. package/dist/commands/setup.d.ts +37 -0
  29. package/dist/commands/setup.d.ts.map +1 -1
  30. package/dist/commands/setup.js +146 -43
  31. package/dist/commands/setup.js.map +1 -1
  32. package/dist/commands/start.d.ts.map +1 -1
  33. package/dist/commands/start.js +194 -164
  34. package/dist/commands/start.js.map +1 -1
  35. package/dist/config-cleanup.d.ts.map +1 -1
  36. package/dist/config-cleanup.js +2 -1
  37. package/dist/config-cleanup.js.map +1 -1
  38. package/dist/config.d.ts +6 -9
  39. package/dist/config.d.ts.map +1 -1
  40. package/dist/config.js +8 -30
  41. package/dist/config.js.map +1 -1
  42. package/dist/context-window.d.ts +33 -5
  43. package/dist/context-window.d.ts.map +1 -1
  44. package/dist/context-window.js +121 -20
  45. package/dist/context-window.js.map +1 -1
  46. package/dist/eval/orchestrator-front-door-replay.js +1 -1
  47. package/dist/eval/orchestrator-front-door-replay.js.map +1 -1
  48. package/dist/eval/policy-detection-replay.js +1 -1
  49. package/dist/eval/policy-detection-replay.js.map +1 -1
  50. package/dist/integration-tokens.d.ts +1 -6
  51. package/dist/integration-tokens.d.ts.map +1 -1
  52. package/dist/integration-tokens.js +38 -40
  53. package/dist/integration-tokens.js.map +1 -1
  54. package/dist/local-cli-pty-manager.d.ts +50 -0
  55. package/dist/local-cli-pty-manager.d.ts.map +1 -0
  56. package/dist/local-cli-pty-manager.js +645 -0
  57. package/dist/local-cli-pty-manager.js.map +1 -0
  58. package/dist/local-data.d.ts +30 -0
  59. package/dist/local-data.d.ts.map +1 -1
  60. package/dist/local-data.js +56 -1
  61. package/dist/local-data.js.map +1 -1
  62. package/dist/local-db.d.ts.map +1 -1
  63. package/dist/local-db.js +54 -1
  64. package/dist/local-db.js.map +1 -1
  65. package/dist/local-funnel.d.ts.map +1 -1
  66. package/dist/local-funnel.js +3 -2
  67. package/dist/local-funnel.js.map +1 -1
  68. package/dist/local-memory-search.d.ts +1 -0
  69. package/dist/local-memory-search.d.ts.map +1 -1
  70. package/dist/local-memory-search.js +101 -18
  71. package/dist/local-memory-search.js.map +1 -1
  72. package/dist/local-server.d.ts +0 -16
  73. package/dist/local-server.d.ts.map +1 -1
  74. package/dist/local-server.js +339 -287
  75. package/dist/local-server.js.map +1 -1
  76. package/dist/mcp/bridge-server.d.ts.map +1 -1
  77. package/dist/mcp/bridge-server.js +2 -1
  78. package/dist/mcp/bridge-server.js.map +1 -1
  79. package/dist/mcp/local-memory-server.d.ts +5 -0
  80. package/dist/mcp/local-memory-server.d.ts.map +1 -1
  81. package/dist/mcp/local-memory-server.js +15 -2
  82. package/dist/mcp/local-memory-server.js.map +1 -1
  83. package/dist/mcp/manager.d.ts +3 -22
  84. package/dist/mcp/manager.d.ts.map +1 -1
  85. package/dist/mcp/manager.js +66 -388
  86. package/dist/mcp/manager.js.map +1 -1
  87. package/dist/memory-extraction.d.ts +2 -0
  88. package/dist/memory-extraction.d.ts.map +1 -1
  89. package/dist/memory-extraction.js +3 -1
  90. package/dist/memory-extraction.js.map +1 -1
  91. package/dist/message-loop.d.ts +9 -6
  92. package/dist/message-loop.d.ts.map +1 -1
  93. package/dist/message-loop.js +217 -538
  94. package/dist/message-loop.js.map +1 -1
  95. package/dist/mqtt-client.d.ts +2 -31
  96. package/dist/mqtt-client.d.ts.map +1 -1
  97. package/dist/mqtt-client.js +2 -2
  98. package/dist/mqtt-client.js.map +1 -1
  99. package/dist/oauth.d.ts +6 -0
  100. package/dist/oauth.d.ts.map +1 -1
  101. package/dist/oauth.js +91 -0
  102. package/dist/oauth.js.map +1 -1
  103. package/dist/orchestration/front-door-policy.d.ts +5 -2
  104. package/dist/orchestration/front-door-policy.d.ts.map +1 -1
  105. package/dist/orchestration/front-door-policy.js +25 -28
  106. package/dist/orchestration/front-door-policy.js.map +1 -1
  107. package/dist/orchestration/orchestrator-blocked-prompt.js +1 -1
  108. package/dist/orchestration/orchestrator-final-response-prompt.js +1 -1
  109. package/dist/orchestration/orchestrator-operating-prompt.d.ts +11 -0
  110. package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
  111. package/dist/orchestration/orchestrator-operating-prompt.js +67 -44
  112. package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
  113. package/dist/orchestration/worker-operating-prompt.js +3 -3
  114. package/dist/orchestration/worker-operating-prompt.js.map +1 -1
  115. package/dist/orchestrator.d.ts +5 -1
  116. package/dist/orchestrator.d.ts.map +1 -1
  117. package/dist/orchestrator.js +141 -81
  118. package/dist/orchestrator.js.map +1 -1
  119. package/dist/prompt-template.js +3 -3
  120. package/dist/prompt-template.js.map +1 -1
  121. package/dist/providers/claude-cli-prompt.d.ts.map +1 -1
  122. package/dist/providers/claude-cli-prompt.js +22 -6
  123. package/dist/providers/claude-cli-prompt.js.map +1 -1
  124. package/dist/providers/claude-cli.d.ts.map +1 -1
  125. package/dist/providers/claude-cli.js +20 -2
  126. package/dist/providers/claude-cli.js.map +1 -1
  127. package/dist/providers/codex-cli.d.ts.map +1 -1
  128. package/dist/providers/codex-cli.js +71 -16
  129. package/dist/providers/codex-cli.js.map +1 -1
  130. package/dist/providers/index.d.ts +11 -0
  131. package/dist/providers/index.d.ts.map +1 -1
  132. package/dist/providers/index.js.map +1 -1
  133. package/dist/runtime-context.d.ts +10 -0
  134. package/dist/runtime-context.d.ts.map +1 -0
  135. package/dist/runtime-context.js +30 -0
  136. package/dist/runtime-context.js.map +1 -0
  137. package/dist/subagent/queue.d.ts.map +1 -1
  138. package/dist/subagent/queue.js +1 -0
  139. package/dist/subagent/queue.js.map +1 -1
  140. package/dist/summarization-pipeline.d.ts +1 -0
  141. package/dist/summarization-pipeline.d.ts.map +1 -1
  142. package/dist/summarization-pipeline.js +94 -25
  143. package/dist/summarization-pipeline.js.map +1 -1
  144. package/dist/tool-permissions.d.ts +2 -0
  145. package/dist/tool-permissions.d.ts.map +1 -0
  146. package/dist/tool-permissions.js +25 -0
  147. package/dist/tool-permissions.js.map +1 -0
  148. package/dist/tools/index.d.ts +7 -8
  149. package/dist/tools/index.d.ts.map +1 -1
  150. package/dist/tools/index.js +70 -60
  151. package/dist/tools/index.js.map +1 -1
  152. package/dist/tools/search-memory.d.ts.map +1 -1
  153. package/dist/tools/search-memory.js +9 -3
  154. package/dist/tools/search-memory.js.map +1 -1
  155. package/dist/tools/spawn-subagent.d.ts.map +1 -1
  156. package/dist/tools/spawn-subagent.js +1 -0
  157. package/dist/tools/spawn-subagent.js.map +1 -1
  158. package/dist/types.d.ts +3 -0
  159. package/dist/types.d.ts.map +1 -1
  160. package/dist/types.js +0 -3
  161. package/dist/types.js.map +1 -1
  162. package/dist/wizard-support.d.ts.map +1 -1
  163. package/dist/wizard-support.js +8 -6
  164. package/dist/wizard-support.js.map +1 -1
  165. package/dist/workflow-engine.d.ts +6 -2
  166. package/dist/workflow-engine.d.ts.map +1 -1
  167. package/dist/workflow-engine.js +254 -77
  168. package/dist/workflow-engine.js.map +1 -1
  169. package/package.json +2 -1
@@ -47,15 +47,12 @@ const approval_1 = require("./approval");
47
47
  const local_db_1 = require("./local-db");
48
48
  const config_1 = require("./config");
49
49
  const auto_detect_1 = require("./auth/auto-detect");
50
+ const anthropic_subscription_1 = require("./auth/anthropic-subscription");
50
51
  const clerk_model_1 = require("./clerk-model");
52
+ const local_funnel_1 = require("./local-funnel");
51
53
  const auto_organizer_1 = require("./auto-organizer");
52
- const orchestrator_profile_1 = require("./orchestrator-profile");
53
- const response_guard_1 = require("./response-guard");
54
- const context_compressor_1 = require("./context-compressor");
55
- const crypto = __importStar(require("crypto"));
54
+ const tool_permissions_1 = require("./tool-permissions");
56
55
  const data = __importStar(require("./local-data"));
57
- const prompt_template_1 = require("./prompt-template");
58
- const local_funnel_1 = require("./local-funnel");
59
56
  /** Determine priority from an AgentCommand */
60
57
  function getCommandPriority(command) {
61
58
  if (command.priority)
@@ -111,12 +108,20 @@ function summarizeToolResult(output, isError) {
111
108
  const compact = output.replace(/\s+/g, ' ').trim();
112
109
  return `${prefix}${compact}`.slice(0, 240);
113
110
  }
111
+ function normalizeMcpToolNames(toolNames) {
112
+ if (!Array.isArray(toolNames))
113
+ return undefined;
114
+ const trimmed = toolNames
115
+ .filter((toolName) => typeof toolName === 'string')
116
+ .map((toolName) => toolName.trim())
117
+ .filter(Boolean);
118
+ return [...new Set(trimmed)];
119
+ }
114
120
  class MessageLoop {
115
121
  options;
116
122
  llmProvider;
117
123
  activeCommandId = null;
118
124
  activeCommandSource = null;
119
- chunkSeq = 0;
120
125
  toolContext;
121
126
  toolDefinitions;
122
127
  builtinToolDefinitions;
@@ -126,7 +131,6 @@ class MessageLoop {
126
131
  resolvedAuth = null;
127
132
  idleTimer = null;
128
133
  approvalManager;
129
- _activeAbortController = null;
130
134
  /** Rate limiting / cost guardrails */
131
135
  maxToolCallsPerMessage;
132
136
  maxTokensPerMessage;
@@ -139,41 +143,42 @@ class MessageLoop {
139
143
  static IDLE_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes
140
144
  constructor(options) {
141
145
  this.options = options;
142
- const effectiveKey = options.apiKey || '';
146
+ // DO NOT remap Claude CLI OAuth sessions to the public Anthropic runtime
147
+ // without explicit user approval.
148
+ const effectiveKey = options.oauthToken || options.apiKey || '';
143
149
  const effectiveProvider = options.provider;
144
- const llmAuthMode = options.authMode || (effectiveKey.startsWith('sk-ant-oat') ? 'oauth-bearer' : 'api-key');
150
+ const isOAuthToken = effectiveKey.startsWith('sk-ant-oat01-');
145
151
  this.llmProvider = (0, index_1.createProvider)(effectiveProvider, {
146
152
  apiKey: effectiveKey,
147
153
  model: options.model,
148
- authMode: llmAuthMode,
149
- ...(options.baseUrl ? { baseUrl: options.baseUrl } : {}),
150
- ...(options.apiStyle ? { apiStyle: options.apiStyle } : {}),
154
+ ...(isOAuthToken ? { authMode: 'oauth-bearer' } : {}),
151
155
  });
152
- // Resolve Funolio API credentials for bot status reporting
153
- const cfg = (0, config_1.loadConfig)();
154
- const funoliApiKey = cfg.auth?.token || process.env.FUNOLIO_API_KEY || '';
155
- const funoliApiBaseUrl = config_1.FUNOLIO_API_URL;
156
- const unrestrictedCliProvider = index_1.CLI_PROVIDERS.has(effectiveProvider);
156
+ // Async: attempt to resolve Anthropic subscription auth (create_api_key exchange)
157
+ if (isOAuthToken && effectiveProvider === 'anthropic') {
158
+ this.resolveAndUpgradeAuth(effectiveKey, options.model);
159
+ }
157
160
  this.toolContext = (0, index_2.createToolContext)(options.projectDir, {
158
161
  actorType: 'llm',
159
162
  actorId: options.agentName || options.userId,
160
- llmProvider: effectiveProvider,
161
- llmModel: options.model,
162
- llmApiKey: effectiveKey,
163
- llmAuthMode,
164
- restrictFileAccessToProject: unrestrictedCliProvider ? false : undefined,
165
- apiKey: funoliApiKey,
166
- apiBaseUrl: funoliApiBaseUrl,
167
- botName: options.agentName || 'funolio-agent',
168
163
  });
164
+ this.options.enabledTools = (0, tool_permissions_1.normalizeConfiguredToolNames)(options.enabledTools);
165
+ this.options.enabledMcpTools = normalizeMcpToolNames(options.enabledMcpTools);
169
166
  let allTools = (0, index_2.getAllToolDefinitions)(options.mcpManager);
170
167
  let builtinTools = (0, index_2.getToolDefinitions)();
171
- // Filter tools by enabledTools whitelist if specified
172
- if (!unrestrictedCliProvider && options.enabledTools && options.enabledTools.length > 0) {
173
- const allowed = new Set(options.enabledTools);
174
- allTools = allTools.filter((t) => allowed.has(t.name));
175
- builtinTools = builtinTools.filter((t) => allowed.has(t.name));
176
- console.log(chalk_1.default.gray(` [${options.agentName || 'agent'}] Tool filter: ${allTools.length} tools enabled (${options.enabledTools.length} configured)`));
168
+ // Filter builtin and MCP tools independently.
169
+ // An explicit empty array means "deny all" for that category.
170
+ if (this.options.enabledTools !== undefined || this.options.enabledMcpTools !== undefined) {
171
+ const builtinNames = new Set(builtinTools.map((t) => t.name));
172
+ const allowedBuiltin = this.options.enabledTools !== undefined ? new Set(this.options.enabledTools) : null;
173
+ const allowedMcp = this.options.enabledMcpTools !== undefined ? new Set(this.options.enabledMcpTools) : null;
174
+ allTools = allTools.filter((t) => {
175
+ if (builtinNames.has(t.name)) {
176
+ return allowedBuiltin ? allowedBuiltin.has(t.name) : true;
177
+ }
178
+ return allowedMcp ? allowedMcp.has(t.name) : true;
179
+ });
180
+ builtinTools = builtinTools.filter((t) => (allowedBuiltin ? allowedBuiltin.has(t.name) : true));
181
+ console.log(chalk_1.default.gray(` [${options.agentName || 'agent'}] Tool filter: ${allTools.length} enabled (builtin=${this.options.enabledTools?.length ?? 'all'}, mcp=${this.options.enabledMcpTools?.length ?? 'all'})`));
177
182
  }
178
183
  this.toolDefinitions = allTools;
179
184
  this.builtinToolDefinitions = builtinTools;
@@ -192,12 +197,59 @@ class MessageLoop {
192
197
  });
193
198
  // Rate limiting defaults (can be overridden via options from agent config)
194
199
  this.maxToolCallsPerMessage = options.maxToolCallsPerMessage ?? 50;
195
- // maxTokensPerMessage retained for backwards compat but no longer enforced.
196
- // LLM providers handle their own context limits with proper error responses.
197
- this.maxTokensPerMessage = options.maxTokensPerMessage ?? 1_000_000;
200
+ this.maxTokensPerMessage = options.maxTokensPerMessage ?? 200_000;
198
201
  // Session idle detection timer (checks every 60s)
199
202
  this.idleTimer = setInterval(() => this.checkIdleSession(), 60_000);
200
203
  }
204
+ /**
205
+ * Attempt to resolve Anthropic subscription auth via create_api_key exchange.
206
+ * If successful, recreates the LLM provider with the exchanged API key (which
207
+ * works with standard x-api-key auth and unlocks all models on the subscription).
208
+ * Falls back to bearer mode silently if exchange fails.
209
+ */
210
+ async resolveAndUpgradeAuth(oauthToken, model) {
211
+ try {
212
+ const result = await (0, anthropic_subscription_1.resolveAnthropicSubscriptionAuth)({
213
+ accessToken: oauthToken,
214
+ refreshToken: this.options.resolvedAuth?.credential?.refreshToken || null,
215
+ expiresAt: this.options.resolvedAuth?.credential?.expiresAt || null,
216
+ onRefresh: (credential) => {
217
+ // Propagate refreshed credentials to BOTH auth stores
218
+ if (this.options.resolvedAuth) {
219
+ this.options.resolvedAuth.credential = credential;
220
+ }
221
+ if (this.resolvedAuth) {
222
+ this.resolvedAuth.credential = credential;
223
+ // Also update the apiKey field so ensureFreshToken() sees the new token
224
+ this.resolvedAuth.apiKey = credential.accessToken;
225
+ }
226
+ console.log('[message-loop] Anthropic OAuth token refreshed and propagated to runtime state');
227
+ },
228
+ });
229
+ if (result.source === 'api-key-exchange') {
230
+ // Successfully exchanged OAuth token for a temporary API key
231
+ console.log('[message-loop] Anthropic subscription auth: using exchanged API key');
232
+ this.llmProvider = (0, index_1.createProvider)('anthropic', {
233
+ apiKey: result.token,
234
+ model,
235
+ });
236
+ }
237
+ else {
238
+ // Bearer fallback — recreate provider with the (possibly refreshed) token
239
+ const currentToken = this.options.resolvedAuth?.credential?.accessToken || oauthToken;
240
+ console.log('[message-loop] Anthropic subscription auth: using bearer fallback' +
241
+ (currentToken !== oauthToken ? ' (with refreshed token)' : ''));
242
+ this.llmProvider = (0, index_1.createProvider)('anthropic', {
243
+ apiKey: currentToken,
244
+ model,
245
+ authMode: 'oauth-bearer',
246
+ });
247
+ }
248
+ }
249
+ catch (err) {
250
+ console.warn('[message-loop] Anthropic subscription auth resolution failed, keeping bearer mode:', err?.message || err);
251
+ }
252
+ }
201
253
  /** Check if a session has gone idle and trigger auto-organize */
202
254
  async checkIdleSession() {
203
255
  if (!this.lastMessageAt || !(0, auto_organizer_1.isAutoOrganizeEnabled)())
@@ -298,11 +350,6 @@ class MessageLoop {
298
350
  console.log(chalk_1.default.yellow(`Cancelling command ${command.id}`));
299
351
  this.activeCommandId = null;
300
352
  this.approvalManager.cancelAll();
301
- // Abort any running tool execution
302
- if (this._activeAbortController) {
303
- this._activeAbortController.abort();
304
- this._activeAbortController = null;
305
- }
306
353
  }
307
354
  return;
308
355
  }
@@ -315,37 +362,51 @@ class MessageLoop {
315
362
  const overrideModel = command.model?.model;
316
363
  let provider = overrideProvider || this.options.provider;
317
364
  const model = overrideModel || this.options.model;
318
- const apiKey = this.options.apiKey;
365
+ const apiKey = this.options.oauthToken || this.options.apiKey;
319
366
  // Only create a new provider if provider or model actually changed (ignore apiKey overrides from server)
320
367
  const hasOverride = (overrideProvider && overrideProvider !== this.options.provider) ||
321
368
  (overrideModel && overrideModel !== this.options.model);
322
369
  let llm;
323
370
  if (hasOverride) {
324
- llm = (0, index_1.createProvider)(provider, {
325
- apiKey,
326
- model,
327
- ...(provider === this.options.provider && this.options.authMode ? { authMode: this.options.authMode } : {}),
328
- ...(provider === this.options.provider && this.options.baseUrl ? { baseUrl: this.options.baseUrl } : {}),
329
- ...(provider === this.options.provider && this.options.apiStyle ? { apiStyle: this.options.apiStyle } : {}),
330
- });
371
+ // For overrides with OAuth tokens, try subscription auth resolution
372
+ if ((apiKey || '').startsWith('sk-ant-oat01-') && provider === 'anthropic') {
373
+ try {
374
+ const authResult = await (0, anthropic_subscription_1.resolveAnthropicSubscriptionAuth)({
375
+ accessToken: apiKey,
376
+ refreshToken: this.options.resolvedAuth?.credential?.refreshToken || null,
377
+ expiresAt: this.options.resolvedAuth?.credential?.expiresAt || null,
378
+ onRefresh: (credential) => {
379
+ if (this.options.resolvedAuth) {
380
+ this.options.resolvedAuth.credential = credential;
381
+ }
382
+ if (this.resolvedAuth) {
383
+ this.resolvedAuth.credential = credential;
384
+ this.resolvedAuth.apiKey = credential.accessToken;
385
+ }
386
+ console.log('[message-loop] Anthropic OAuth token refreshed (override path)');
387
+ },
388
+ });
389
+ llm = (0, index_1.createProvider)(provider, {
390
+ apiKey: authResult.token,
391
+ model,
392
+ ...(authResult.authMode === 'oauth-bearer' ? { authMode: 'oauth-bearer' } : {}),
393
+ });
394
+ }
395
+ catch {
396
+ llm = (0, index_1.createProvider)(provider, { apiKey, model, authMode: 'oauth-bearer' });
397
+ }
398
+ }
399
+ else {
400
+ llm = (0, index_1.createProvider)(provider, { apiKey, model });
401
+ }
331
402
  }
332
403
  else {
333
404
  llm = this.llmProvider;
334
405
  }
335
406
  // Refresh tool definitions each command so newly installed MCP tools are visible
336
407
  this.toolDefinitions = (0, index_2.getAllToolDefinitions)(this._mcpManager);
337
- // Fix 3: Check for dead MCP servers and note unavailable tools
338
- let deadServerNote = '';
339
- if (this._mcpManager) {
340
- const dead = this._mcpManager.getDeadServers();
341
- if (dead.length > 0) {
342
- deadServerNote = `\n\n⚠️ UNAVAILABLE TOOL SERVERS: The following MCP servers have crashed and are no longer available: ${dead.join(', ')}. Do NOT attempt to use tools from these servers — they will fail. Inform the user if they ask for functionality from these servers.`;
343
- console.log(chalk_1.default.yellow(` [MCP] Dead servers detected: ${dead.join(', ')}`));
344
- }
345
- }
346
408
  this.activeCommandId = command.id;
347
409
  this.activeCommandSource = command.source || 'user';
348
- this.chunkSeq = 0;
349
410
  this.processing = true;
350
411
  this.lastMessageAt = Date.now();
351
412
  const commandStartMs = Date.now();
@@ -406,21 +467,15 @@ class MessageLoop {
406
467
  : undefined;
407
468
  const effectiveProjectId = command.context?.projectId || activeConversation?.project_id || undefined;
408
469
  const effectiveProject = effectiveProjectId ? localData?.getProject(effectiveProjectId) : undefined;
409
- // Create abort controller for this command execution
410
- const commandAbortController = new AbortController();
411
- this._activeAbortController = commandAbortController;
412
470
  const commandToolContext = {
413
471
  ...this.toolContext,
414
472
  projectId: effectiveProjectId ?? null,
415
- abortSignal: commandAbortController.signal,
416
- // Use project folder as working directory if available
417
- ...(effectiveProject?.folder ? { projectDir: effectiveProject.folder } : {}),
418
473
  };
419
474
  // ─── Orchestrator Mode Branch ─────────────────────────
420
475
  // Resolve the selected bot profile — from command.bot, conversation bot_id, or default
421
476
  const selectedBotId = command.bot?.id || activeConversation?.bot_id || localAgentId;
422
477
  const activeProfile = selectedBotId ? localData?.getAgentProfile(selectedBotId) : null;
423
- if ((0, orchestrator_profile_1.isOrchestratorProfile)(activeProfile) && localFirst) {
478
+ if (activeProfile?.role_class === 'orchestrator' && localFirst) {
424
479
  const clerk = (0, clerk_model_1.getClerk)();
425
480
  if (!clerk) {
426
481
  // Do not silently fall through to direct chat — report error
@@ -435,15 +490,6 @@ class MessageLoop {
435
490
  const { getWorkflowEngine } = await Promise.resolve().then(() => __importStar(require('./workflow-engine')));
436
491
  const workflowEngine = getWorkflowEngine(this.options.projectDir);
437
492
  const orchestrator = new OrchestratorAgent(clerk, workflowEngine);
438
- // Orchestrator work is conversation-scoped and can continue in parallel.
439
- // Release the global message-loop lock here so another conversation can start.
440
- this.activeCommandId = null;
441
- this.processing = false;
442
- if (this.scheduledTaskTimer) {
443
- clearTimeout(this.scheduledTaskTimer);
444
- this.scheduledTaskTimer = null;
445
- }
446
- void this.drainQueue();
447
493
  try {
448
494
  const response = await orchestrator.handleUserMessage(command.prompt, localConvId || '', {
449
495
  projectDir: this.options.projectDir,
@@ -453,33 +499,27 @@ class MessageLoop {
453
499
  await this.options.mqttClient.publishResult({
454
500
  commandId: command.id,
455
501
  type: 'chunk',
456
- seq: this.chunkSeq++,
457
502
  content: status + '\n',
458
503
  timestamp: Date.now(),
459
504
  });
460
505
  },
461
506
  mqttPublish: (result) => this.options.mqttClient.publishResult(result),
462
507
  });
463
- const responseMeta = orchestrator.getLastResponseMeta();
464
- const finalAgentName = responseMeta?.agentName || 'Orchestrator';
465
- const finalBotId = responseMeta?.botId || (finalAgentName === 'Orchestrator' ? activeProfile?.id : undefined);
466
- const finalModelLabel = responseMeta?.modelLabel || undefined;
467
- // Save final response to local DB
468
- if (localConvId && localData && activeProfile) {
469
- localData.addMessage(localConvId, 'assistant', response, finalModelLabel, undefined, finalBotId, finalAgentName);
508
+ // Save O's response to local DB
509
+ if (localConvId && localData) {
510
+ localData.addMessage(localConvId, 'assistant', response, undefined, undefined, activeProfile.id, 'Project Manager');
470
511
  }
471
512
  // Publish final response and complete
472
513
  await this.options.mqttClient.publishResult({
473
514
  commandId: command.id,
474
515
  type: 'chunk',
475
- seq: this.chunkSeq++,
476
516
  content: response,
477
517
  timestamp: Date.now(),
478
518
  });
479
519
  await this.options.mqttClient.publishResult({
480
520
  commandId: command.id,
481
521
  type: 'complete',
482
- content: response || '',
522
+ content: '',
483
523
  timestamp: Date.now(),
484
524
  });
485
525
  }
@@ -493,7 +533,7 @@ class MessageLoop {
493
533
  clearTimeout(this.scheduledTaskTimer);
494
534
  this.scheduledTaskTimer = null;
495
535
  }
496
- void this.drainQueue();
536
+ this.drainQueue();
497
537
  }
498
538
  return;
499
539
  }
@@ -524,7 +564,7 @@ class MessageLoop {
524
564
  includeKeyDecisions: false,
525
565
  });
526
566
  systemPrompt = built.systemPrompt;
527
- console.log(chalk_1.default.gray(` [clerk] Context: ${built.injectedSummaries} summaries (${built.contextTokensUsed} tokens)`));
567
+ console.log(chalk_1.default.gray(` [clerk] Context: ${built.injectedFacts} facts, ${built.injectedDecisions} decisions, ${built.injectedSummaries} summaries (${built.contextTokensUsed} tokens)`));
528
568
  }
529
569
  else {
530
570
  systemPrompt = this.buildFallbackSystemPrompt(command);
@@ -538,17 +578,13 @@ class MessageLoop {
538
578
  systemPrompt = this.buildFallbackSystemPrompt(command);
539
579
  }
540
580
  // --- Context priority rules & tool fallback (AFTER context) ---
541
- // Fix 3: Inject dead server warnings into system prompt
542
- if (deadServerNote) {
543
- systemPrompt += deadServerNote;
544
- }
545
581
  systemPrompt += `\n\nCONTEXT PRIORITY RULES:
546
- 1. ALWAYS use the context provided above (summaries, workflow history, and conversations) to answer questions about past work, decisions, or projects. This is your PRIMARY knowledge source — it comes from the user's stored conversation history.
582
+ 1. ALWAYS use the context provided above (decisions, memory facts, conversations) to answer questions about past work, decisions, or projects. This is your PRIMARY knowledge source — it comes from the user's stored conversation history.
547
583
  2. Do NOT browse local files or run shell commands unless:
548
584
  a. The provided context does not contain the answer, OR
549
585
  b. The user explicitly asks you to look at local files or run a command, OR
550
586
  c. The user asks about current runtime state (logs, running processes, file contents right now)
551
- 3. When you use provided context, briefly cite the source (e.g. "Based on your stored summary..." or "From your conversation history...").
587
+ 3. When you use provided context, briefly cite the source (e.g. "Based on your stored decisions..." or "From your conversation history...").
552
588
  4. If no relevant context was provided AND the question is about past work/decisions, say "I don't have stored context about that topic yet" — don't guess or hallucinate.
553
589
 
554
590
  TOOL EFFICIENCY RULES:
@@ -556,245 +592,56 @@ TOOL EFFICIENCY RULES:
556
592
  - PREFER one broad command over many narrow ones (e.g. "find . -name '*.json' -exec grep 'error' {} +" instead of checking files one by one)
557
593
  - PLAN your approach before starting — outline what you need to find, then execute with minimal tool calls`;
558
594
  // Count context sections injected into system prompt
559
- const contextSectionCount = ['[Relevant Conversations]', '[Code References]', '[Relevant Files]', '[Bot Memory]', '[Retrieved Context]', '[Summaries]']
595
+ const contextSectionCount = ['[Key Decisions]', '[Memory Facts]', '[Relevant Conversations]', '[Code References]', '[Relevant Files]', '[Bot Memory]', '[Retrieved Context]']
560
596
  .filter(header => systemPrompt.includes(header)).length;
561
- // Add the current prompt (with image attachments if present)
562
- if (command.attachments && command.attachments.length > 0) {
563
- const contentParts = [];
564
- for (const att of command.attachments) {
565
- if (att.fileType === 'image' && att.dataUrl) {
566
- // Anthropic native format: source block with base64
567
- const commaIdx = att.dataUrl.indexOf(',');
568
- if (commaIdx >= 0) {
569
- const mimeMatch = att.dataUrl.match(/^data:([^;]+);/);
570
- const mediaType = mimeMatch?.[1] || att.mimeType || 'image/png';
571
- const base64Data = att.dataUrl.slice(commaIdx + 1);
572
- contentParts.push({ type: 'image', source: { type: 'base64', media_type: mediaType, data: base64Data } });
573
- }
574
- else {
575
- // Fallback: treat as URL
576
- contentParts.push({ type: 'image_url', image_url: { url: att.dataUrl, detail: 'auto' } });
577
- }
578
- }
579
- if (att.extractedText) {
580
- contentParts.push({ type: 'text', text: `[File: ${att.filename}]\n${att.extractedText}` });
581
- }
582
- }
583
- contentParts.push({ type: 'text', text: command.prompt });
584
- messages.push({ role: 'user', content: contentParts });
585
- }
586
- else {
587
- messages.push({ role: 'user', content: command.prompt });
588
- }
589
- // Retry budget tracker (Optimization 2)
590
- const toolFailuresByKey = new Map(); // toolName:argsHash → count
591
- const toolFailuresByName = new Map(); // toolName → count
592
- function getArgsHash(args) {
593
- const str = JSON.stringify(args || {}).slice(0, 200);
594
- return crypto.createHash('md5').update(str).digest('hex').slice(0, 12);
595
- }
596
- function getRetryKey(name, args) {
597
- // Bug 6: For meta-tools, include action in retry key so different actions don't share budget
598
- if (args?.action)
599
- return `${name}:${args.action}`;
600
- return name;
601
- }
602
- function trackToolFailure(name, args) {
603
- const key = `${getRetryKey(name, args)}:${getArgsHash(args)}`;
604
- const retryKey = getRetryKey(name, args);
605
- toolFailuresByKey.set(key, (toolFailuresByKey.get(key) || 0) + 1);
606
- toolFailuresByName.set(retryKey, (toolFailuresByName.get(retryKey) || 0) + 1);
607
- }
608
- // Futility detection: track consecutive low-value tool results
609
- let consecutiveEmptyResults = 0;
610
- const FUTILITY_THRESHOLD = 6; // after 6 consecutive empty/low-value results, nudge the LLM
611
- let futilityNudgeInjected = false;
612
- function isLowValueResult(output) {
613
- if (!output || output.length < 30)
614
- return true;
615
- const lower = output.toLowerCase();
616
- if (lower.includes('no matches found') || lower.includes('no results'))
617
- return true;
618
- if (lower.includes('0 bytes') || lower.includes('empty'))
619
- return true;
620
- // exit code 1 with no meaningful stdout
621
- if (/\[exit code: [^0]/.test(lower) && output.replace(/\[exit code:.*?\]/g, '').trim().length < 20)
622
- return true;
623
- return false;
624
- }
625
- function checkRetryBudget(name, args) {
626
- const key = `${getRetryKey(name, args)}:${getArgsHash(args)}`;
627
- const retryKey = getRetryKey(name, args);
628
- const keyCount = toolFailuresByKey.get(key) || 0;
629
- const nameCount = toolFailuresByName.get(retryKey) || 0;
630
- if (keyCount >= 2) {
631
- return `This exact operation has failed ${keyCount} times. Stop retrying and inform the user what went wrong. Suggest an alternative approach or ask for help.`;
632
- }
633
- if (nameCount >= 3) {
634
- return `The ${name} tool has failed ${nameCount} times in this conversation. Consider a completely different approach.`;
635
- }
636
- return null;
637
- }
597
+ // Add the current prompt
598
+ messages.push({ role: 'user', content: command.prompt });
638
599
  // Agentic loop - keep calling LLM until no more tool calls
639
600
  let iteration = 0;
640
- // Phase 1d: hard caps on iterations, tokens, and tool calls
641
- // Web/MQTT commands (source undefined or 'user') get tighter cap; scheduled/system tasks get more room
642
- const MAX_ITERATIONS = (command.source === 'scheduled' || command.source === 'system') ? 10 : 5;
643
- const MAX_INPUT_TOKENS = 150_000; // 150K cumulative input token budget
644
- const MAX_TOOL_CALLS = 20; // hard cap on tool calls per turn
645
- const INACTIVITY_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes of no LLM/tool activity
646
- let lastActivityMs = Date.now();
647
- let lastProgressMs = Date.now();
601
+ const MAX_ITERATIONS = Infinity;
648
602
  let totalTokensUsed = 0;
649
- let totalInputTokens = 0;
650
- let totalOutputTokens = 0;
651
- let totalCacheReadTokens = 0;
652
- let totalCacheCreationTokens = 0;
653
- let totalToolCallDurationMs = 0;
654
- const deniedTools = new Set(); // Track tools denied by user — don't re-ask
655
- const commandStartMs = Date.now();
656
- // Publish prompt context for "View Prompt" panel
657
- try {
658
- const promptSnapshot = [
659
- { role: 'system', content: systemPrompt.slice(0, 4000) + (systemPrompt.length > 4000 ? '\n...(truncated)' : '') },
660
- ...messages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content.slice(0, 2000) : '' })),
661
- ];
662
- await this.options.mqttClient.publishResult({
663
- commandId: command.id,
664
- type: 'prompt_messages',
665
- messages: promptSnapshot,
666
- contextSections: contextSectionCount,
667
- model,
668
- timestamp: Date.now(),
669
- });
670
- }
671
- catch { }
672
603
  while (iteration < MAX_ITERATIONS && this.activeCommandId === command.id) {
673
604
  iteration++;
674
- // Emit progress every 60 seconds
675
- if (Date.now() - lastProgressMs >= 60_000) {
676
- lastProgressMs = Date.now();
677
- await this.options.mqttClient.publishResult({
678
- commandId: command.id,
679
- type: 'progress',
680
- elapsed: Math.round((Date.now() - commandStartMs) / 1000),
681
- iteration,
682
- toolsUsed: totalToolCalls,
683
- timestamp: Date.now(),
684
- });
685
- }
686
- // Inactivity timeout check (5 min of no LLM response or tool completion)
687
- if (Date.now() - lastActivityMs > INACTIVITY_TIMEOUT_MS) {
688
- console.log(chalk_1.default.red(` ⏱ Inactivity timeout exceeded (${INACTIVITY_TIMEOUT_MS / 1000}s) — attempting graceful wind-down`));
689
- // Inject wind-down system message and do one final LLM call
690
- messages.push({
691
- role: 'user',
692
- content: '[System] You are running out of time. Stop using tools immediately. Summarize everything you have so far and provide your best answer now.',
693
- });
694
- let windDownSucceeded = false;
695
- let windDownResponse = null;
696
- try {
697
- windDownResponse = await llm.chat({
698
- messages,
699
- system: systemPrompt,
700
- stream: true,
701
- onChunk: async (chunk) => {
702
- if (this.activeCommandId !== command.id)
703
- return;
704
- await this.options.mqttClient.publishResult({
705
- commandId: command.id,
706
- type: 'chunk',
707
- seq: this.chunkSeq++,
708
- content: chunk,
709
- timestamp: Date.now(),
710
- });
711
- },
712
- });
713
- if (windDownResponse.usage) {
714
- totalTokensUsed += windDownResponse.usage.inputTokens + windDownResponse.usage.outputTokens;
715
- totalInputTokens += windDownResponse.usage.inputTokens;
716
- totalOutputTokens += windDownResponse.usage.outputTokens;
717
- }
718
- windDownSucceeded = true;
719
- }
720
- catch (windDownErr) {
721
- console.error(chalk_1.default.red(` Wind-down LLM call failed: ${windDownErr.message}`));
722
- commandAbortController.abort();
723
- }
724
- // Emit stats
725
- await this.options.mqttClient.publishResult({
726
- commandId: command.id,
727
- type: 'stats',
728
- llmCalls: iteration,
729
- toolsUsed: totalToolCalls,
730
- inputTokens: totalInputTokens,
731
- outputTokens: totalOutputTokens,
732
- cacheReadTokens: totalCacheReadTokens || undefined,
733
- cacheCreationTokens: totalCacheCreationTokens || undefined,
734
- totalTokens: totalTokensUsed,
735
- latencyMs: Date.now() - commandStartMs,
736
- toolCallDurationMs: totalToolCallDurationMs || undefined,
737
- contextSections: contextSectionCount || undefined,
738
- model,
739
- timestamp: Date.now(),
740
- });
741
- if (windDownSucceeded) {
742
- // Wind-down produced a summary — treat as completed
743
- await this.options.mqttClient.publishResult({
744
- commandId: command.id,
745
- type: 'complete',
746
- content: windDownResponse?.content || '',
747
- timestamp: Date.now(),
748
- });
749
- console.log(chalk_1.default.green(` Command completed (inactivity wind-down): ${iteration} iterations, ${totalToolCalls} tool calls`));
750
- }
751
- else {
752
- // Wind-down failed — emit error so the job is marked failed, not completed
753
- await this.options.mqttClient.publishResult({
754
- commandId: command.id,
755
- type: 'error',
756
- error: `Command timed out after ${Math.round((Date.now() - commandStartMs) / 60000)} minutes of inactivity and wind-down failed.`,
757
- timestamp: Date.now(),
758
- });
759
- console.log(chalk_1.default.red(` Command failed (wind-down failed): ${iteration} iterations, ${totalToolCalls} tool calls`));
760
- }
761
- break;
762
- }
763
605
  // Send a separator between agentic loop iterations so streamed text
764
606
  // from different LLM calls doesn't run together in the UI.
765
607
  if (iteration > 1) {
766
608
  await this.options.mqttClient.publishResult({
767
609
  commandId: command.id,
768
610
  type: 'chunk',
769
- seq: this.chunkSeq++,
770
611
  content: '\n\n',
771
612
  timestamp: Date.now(),
772
613
  });
773
614
  }
774
- // API keys don't expire ensureFreshToken is a no-op for api-key source
615
+ // Refresh OAuth token before each LLM call if needed
775
616
  if (this.resolvedAuth) {
776
617
  const refreshed = await (0, auto_detect_1.ensureFreshToken)(this.resolvedAuth);
618
+ if (refreshed.expired) {
619
+ console.error(chalk_1.default.red(` [auth] Token refresh failed: ${refreshed.error}`));
620
+ await this.options.mqttClient.publishResult({
621
+ commandId: command.id,
622
+ type: 'error',
623
+ error: 'Your authentication token has expired or been revoked. Please re-authenticate by running `funolio-agent login` or `funolio-agent configure`.',
624
+ timestamp: Date.now(),
625
+ });
626
+ break;
627
+ }
777
628
  if (refreshed.apiKey !== this.resolvedAuth.apiKey) {
778
629
  this.resolvedAuth = refreshed;
779
630
  this.updateToken(refreshed.apiKey);
780
631
  }
781
632
  }
633
+ // With OAuth auto-detection, all providers now use direct API calls
634
+ // with full tool support. The old CLI_PROVIDERS check is kept only
635
+ // for logging/diagnostics but no longer gates tool availability.
782
636
  // Smart tool filtering: only send relevant tools to reduce token usage
783
- const filteredTools = index_1.CLI_PROVIDERS.has(this.options.provider)
784
- ? this.toolDefinitions
785
- : (0, tool_filter_1.filterToolsForMessage)(this.toolDefinitions, this.builtinToolDefinitions, command.prompt);
786
- if (!index_1.CLI_PROVIDERS.has(this.options.provider) && filteredTools.length < this.toolDefinitions.length) {
637
+ const filteredTools = (0, tool_filter_1.filterToolsForMessage)(this.toolDefinitions, this.builtinToolDefinitions, command.prompt);
638
+ if (filteredTools.length < this.toolDefinitions.length) {
787
639
  console.log(chalk_1.default.gray(` [tool-filter] ${filteredTools.length}/${this.toolDefinitions.length} tools selected`));
788
640
  }
789
- // Compress context before sending to LLM (Optimizations 3+4)
790
- const compressedMessages = (0, context_compressor_1.compressContext)(messages, 40, 3, 200);
791
- if (compressedMessages.length < messages.length) {
792
- console.log(chalk_1.default.gray(` [context] Compressed ${messages.length} → ${compressedMessages.length} messages`));
793
- }
794
641
  let response;
795
642
  try {
796
643
  response = await llm.chat({
797
- messages: compressedMessages,
644
+ messages,
798
645
  system: systemPrompt,
799
646
  stream: true,
800
647
  tools: filteredTools,
@@ -804,7 +651,6 @@ TOOL EFFICIENCY RULES:
804
651
  await this.options.mqttClient.publishResult({
805
652
  commandId: command.id,
806
653
  type: 'chunk',
807
- seq: this.chunkSeq++,
808
654
  content: chunk,
809
655
  timestamp: Date.now(),
810
656
  });
@@ -819,76 +665,16 @@ TOOL EFFICIENCY RULES:
819
665
  await this.options.mqttClient.publishResult({
820
666
  commandId: command.id,
821
667
  type: 'error',
822
- error: 'API authentication failed. Check your API key or run `funolio-agent configure` to update it.',
668
+ error: 'API authentication failed. Your token may have expired run `funolio-agent login` to refresh.',
823
669
  timestamp: Date.now(),
824
670
  });
825
671
  break;
826
672
  }
827
673
  throw llmError;
828
674
  }
829
- // Reset inactivity timer after LLM response
830
- lastActivityMs = Date.now();
831
675
  // Track token usage across iterations
832
676
  if (response.usage) {
833
677
  totalTokensUsed += response.usage.inputTokens + response.usage.outputTokens;
834
- totalInputTokens += response.usage.inputTokens;
835
- totalOutputTokens += response.usage.outputTokens;
836
- totalCacheReadTokens += response.usage.cacheReadTokens || 0;
837
- totalCacheCreationTokens += response.usage.cacheCreationTokens || 0;
838
- }
839
- // Phase 1d: Check cumulative input token budget
840
- if (totalInputTokens >= MAX_INPUT_TOKENS) {
841
- console.log(`[token-cap] Input token budget exceeded: ${totalInputTokens}/${MAX_INPUT_TOKENS} — triggering wind-down`);
842
- messages.push({
843
- role: 'user',
844
- content: '[System] Token budget exceeded. Stop using tools immediately. Summarize everything you have so far and provide your best answer now.',
845
- });
846
- try {
847
- const capResponse = await llm.chat({
848
- messages, system: systemPrompt, stream: true,
849
- onChunk: async (chunk) => {
850
- if (this.activeCommandId !== command.id)
851
- return;
852
- await this.options.mqttClient.publishResult({ commandId: command.id, type: 'chunk', seq: this.chunkSeq++, content: chunk, timestamp: Date.now() });
853
- },
854
- });
855
- if (capResponse.usage) {
856
- totalTokensUsed += capResponse.usage.inputTokens + capResponse.usage.outputTokens;
857
- totalInputTokens += capResponse.usage.inputTokens;
858
- totalOutputTokens += capResponse.usage.outputTokens;
859
- }
860
- }
861
- catch (e) {
862
- console.error(`[token-cap] Wind-down LLM call failed: ${e.message}`);
863
- }
864
- break;
865
- }
866
- // Phase 1d: Check tool call hard cap
867
- if (totalToolCalls >= MAX_TOOL_CALLS) {
868
- console.log(`[token-cap] Tool call cap exceeded: ${totalToolCalls}/${MAX_TOOL_CALLS} — triggering wind-down`);
869
- messages.push({
870
- role: 'user',
871
- content: '[System] Tool call limit reached. Stop using tools immediately. Summarize everything you have so far and provide your best answer now.',
872
- });
873
- try {
874
- const capResponse = await llm.chat({
875
- messages, system: systemPrompt, stream: true,
876
- onChunk: async (chunk) => {
877
- if (this.activeCommandId !== command.id)
878
- return;
879
- await this.options.mqttClient.publishResult({ commandId: command.id, type: 'chunk', seq: this.chunkSeq++, content: chunk, timestamp: Date.now() });
880
- },
881
- });
882
- if (capResponse.usage) {
883
- totalTokensUsed += capResponse.usage.inputTokens + capResponse.usage.outputTokens;
884
- totalInputTokens += capResponse.usage.inputTokens;
885
- totalOutputTokens += capResponse.usage.outputTokens;
886
- }
887
- }
888
- catch (e) {
889
- console.error(`[token-cap] Wind-down LLM call failed: ${e.message}`);
890
- }
891
- break;
892
678
  }
893
679
  // If there are tool calls, publish them and continue the loop
894
680
  if (response.toolCalls && response.toolCalls.length > 0) {
@@ -904,16 +690,13 @@ TOOL EFFICIENCY RULES:
904
690
  const toolSummary = (0, approval_1.generateToolSummary)(toolCall.name, toolCall.arguments);
905
691
  const toolLocation = extractToolLocation(toolCall.name, toolCall.arguments, commandToolContext.projectDir);
906
692
  // Check permission before execution
907
- const permMode = index_1.CLI_PROVIDERS.has(this.options.provider)
908
- ? 'autopilot'
909
- : (this.options.permissionMode || 'autopilot');
910
- let approval = (0, approval_1.checkPermission)(toolCall.name, permMode, index_1.CLI_PROVIDERS.has(this.options.provider) ? undefined : this.options.enabledTools);
911
- // If tool was already denied once in this command, auto-deny without re-asking
912
- if (!approval.approved && deniedTools.has(toolCall.name)) {
913
- approval = { approved: false, reason: `Tool "${toolCall.name}" was already denied in this session.` };
914
- }
693
+ const permMode = this.options.permissionMode || 'autopilot';
694
+ const allowedToolNames = this.options.enabledTools !== undefined || this.options.enabledMcpTools !== undefined
695
+ ? [...(this.options.enabledTools || []), ...(this.options.enabledMcpTools || [])]
696
+ : undefined;
697
+ let approval = (0, approval_1.checkPermission)(toolCall.name, permMode, allowedToolNames);
915
698
  // If not auto-approved, request interactive approval via MQTT
916
- if (!approval.approved && permMode !== 'autopilot' && !deniedTools.has(toolCall.name)) {
699
+ if (!approval.approved && permMode !== 'autopilot') {
917
700
  console.log(chalk_1.default.yellow(` 🔐 Requesting user approval for: ${toolCall.name}`));
918
701
  approval = await this.approvalManager.requestApproval(command.id, toolCall.name, toolCall.arguments, {
919
702
  onRequest: async (request) => {
@@ -945,7 +728,6 @@ TOOL EFFICIENCY RULES:
945
728
  }
946
729
  }
947
730
  if (!approval.approved) {
948
- deniedTools.add(toolCall.name);
949
731
  console.log(chalk_1.default.yellow(` ⚠ Tool denied: ${approval.reason}`));
950
732
  const deniedOutput = `PERMISSION_DENIED: ${approval.reason}`;
951
733
  await this.options.mqttClient.publishResult({
@@ -988,62 +770,20 @@ TOOL EFFICIENCY RULES:
988
770
  },
989
771
  timestamp: Date.now(),
990
772
  });
991
- // Check retry budget before execution (Optimization 2)
992
- const retryBlock = checkRetryBudget(toolCall.name, toolCall.arguments);
993
- if (retryBlock) {
994
- console.log(chalk_1.default.yellow(` ⚠ Retry budget exceeded for ${toolCall.name}`));
995
- const retryOutput = `ERROR: ${retryBlock}`;
996
- await this.options.mqttClient.publishResult({
997
- commandId: command.id,
998
- type: 'tool_result',
999
- toolResult: {
1000
- callId: toolCall.id,
1001
- name: toolCall.name,
1002
- output: retryOutput,
1003
- isError: true,
1004
- durationMs: 0,
1005
- },
1006
- timestamp: Date.now(),
1007
- });
1008
- messages.push({
1009
- role: 'tool',
1010
- content: retryOutput,
1011
- toolCallId: toolCall.id,
1012
- toolName: toolCall.name,
1013
- });
1014
- continue;
1015
- }
1016
773
  // Execute the tool (with MCP fallback)
1017
- const toolStartMs = Date.now();
1018
774
  const rawResult = await (0, tools_1.executeToolWithMCP)({ id: toolCall.id, name: toolCall.name, arguments: toolCall.arguments }, commandToolContext, this.options.mcpManager);
1019
775
  const toolResult = await (0, verification_1.verifyToolResult)(rawResult, toolCall.arguments, commandToolContext);
1020
- let resultOutput = toolResult.success
776
+ const resultOutput = toolResult.success
1021
777
  ? toolResult.output
1022
778
  : `ERROR: ${toolResult.error || 'Unknown error'}`;
1023
- // Bug 2 & 7: Cap tool result size to prevent context blowup
1024
- const MAX_TOOL_RESULT_CHARS = 50_000;
1025
- if (resultOutput.length > MAX_TOOL_RESULT_CHARS) {
1026
- const originalLength = resultOutput.length;
1027
- resultOutput = resultOutput.slice(0, MAX_TOOL_RESULT_CHARS) +
1028
- `\n\n[OUTPUT TRUNCATED — was ${originalLength} chars. Use more specific commands to read smaller portions.]`;
1029
- }
1030
- // Track failures for retry budget (Optimization 2)
1031
- if (!toolResult.success) {
1032
- trackToolFailure(toolCall.name, toolCall.arguments);
1033
- }
1034
779
  console.log(chalk_1.default.gray(` Result: ${toolResult.success ? '✓' : '✗'} ${resultOutput.slice(0, 100)}${resultOutput.length > 100 ? '...' : ''}`));
1035
- const toolDurationMs = Date.now() - toolStartMs;
1036
- totalToolCallDurationMs += toolDurationMs;
1037
- lastActivityMs = Date.now(); // Reset inactivity timer after tool completion
1038
780
  await this.options.mqttClient.publishResult({
1039
781
  commandId: command.id,
1040
782
  type: 'tool_result',
1041
783
  toolResult: {
1042
784
  callId: toolCall.id,
1043
- name: toolCall.name,
1044
785
  output: resultOutput,
1045
786
  isError: !toolResult.success,
1046
- durationMs: toolDurationMs,
1047
787
  success: toolResult.success,
1048
788
  summary: summarizeToolResult(resultOutput, !toolResult.success),
1049
789
  path: toolLocation.path,
@@ -1061,32 +801,15 @@ TOOL EFFICIENCY RULES:
1061
801
  toolCallId: toolCall.id,
1062
802
  toolName: toolCall.name,
1063
803
  });
1064
- // Futility detection: track consecutive low-value results
1065
- if (isLowValueResult(resultOutput)) {
1066
- consecutiveEmptyResults++;
1067
- }
1068
- else {
1069
- consecutiveEmptyResults = 0; // reset on any meaningful result
1070
- }
1071
- }
1072
- // Futility check: if too many consecutive empty results, nudge the LLM to stop spinning
1073
- if (consecutiveEmptyResults >= FUTILITY_THRESHOLD && !futilityNudgeInjected) {
1074
- futilityNudgeInjected = true;
1075
- console.log(chalk_1.default.yellow(` ⚠ Futility detected: ${consecutiveEmptyResults} consecutive low-value tool results — nudging LLM`));
1076
- messages.push({
1077
- role: 'user',
1078
- content: '[System] Your last several tool calls returned empty or no-match results. Stop searching and work with what you have. Summarize what you found (or could not find) and suggest next steps to the user. Do not make more search attempts.',
1079
- });
1080
804
  }
1081
805
  // --- Rate limit checks ---
1082
806
  // Warn at 80% of tool call limit
1083
807
  if (totalToolCalls >= this.maxToolCallsPerMessage * 0.8 && totalToolCalls < this.maxToolCallsPerMessage) {
1084
808
  console.log(chalk_1.default.yellow(` ⚠ Approaching tool call limit: ${totalToolCalls}/${this.maxToolCallsPerMessage}`));
1085
809
  }
1086
- // Log context window usage for monitoring (no artificial cap — let the LLM
1087
- // provider return its own error if context exceeds the model's real limit)
1088
- if (response.usage?.inputTokens) {
1089
- console.log(chalk_1.default.gray(` [tokens] iteration ${iteration}: ${response.usage.inputTokens} input, ${response.usage.outputTokens} output`));
810
+ // Warn at 80% of token limit
811
+ if (totalTokensUsed >= this.maxTokensPerMessage * 0.8 && totalTokensUsed < this.maxTokensPerMessage) {
812
+ console.log(chalk_1.default.yellow(` ⚠ Approaching token limit: ${totalTokensUsed}/${this.maxTokensPerMessage}`));
1090
813
  }
1091
814
  // Check tool call limit
1092
815
  if (totalToolCalls >= this.maxToolCallsPerMessage) {
@@ -1103,7 +826,6 @@ TOOL EFFICIENCY RULES:
1103
826
  await this.options.mqttClient.publishResult({
1104
827
  commandId: command.id,
1105
828
  type: 'chunk',
1106
- seq: this.chunkSeq++,
1107
829
  content: chunk,
1108
830
  timestamp: Date.now(),
1109
831
  });
@@ -1114,74 +836,55 @@ TOOL EFFICIENCY RULES:
1114
836
  }
1115
837
  await this.options.mqttClient.publishResult({
1116
838
  commandId: command.id,
1117
- type: 'stats',
1118
- llmCalls: iteration,
1119
- toolsUsed: totalToolCalls,
1120
- inputTokens: totalInputTokens,
1121
- outputTokens: totalOutputTokens,
1122
- cacheReadTokens: totalCacheReadTokens || undefined,
1123
- cacheCreationTokens: totalCacheCreationTokens || undefined,
1124
- totalTokens: totalTokensUsed,
1125
- latencyMs: Date.now() - commandStartMs,
1126
- toolCallDurationMs: totalToolCallDurationMs || undefined,
1127
- contextSections: contextSectionCount || undefined,
1128
- model,
839
+ type: 'complete',
840
+ content: '',
1129
841
  timestamp: Date.now(),
1130
842
  });
843
+ console.log(chalk_1.default.green(` Command completed: ${iteration} iterations, ${totalToolCalls} tool calls, ~${totalTokensUsed} tokens`));
844
+ break;
845
+ }
846
+ // Check token limit
847
+ if (totalTokensUsed >= this.maxTokensPerMessage) {
848
+ console.log(chalk_1.default.red(` 🛑 Token limit reached: ${totalTokensUsed}/${this.maxTokensPerMessage}`));
849
+ messages.push({ role: 'user', content: 'Token limit reached. Please summarize what you\'ve done so far.' });
850
+ const summaryResponse = await llm.chat({
851
+ messages,
852
+ system: systemPrompt,
853
+ stream: true,
854
+ onChunk: async (chunk) => {
855
+ if (this.activeCommandId !== command.id)
856
+ return;
857
+ await this.options.mqttClient.publishResult({
858
+ commandId: command.id,
859
+ type: 'chunk',
860
+ content: chunk,
861
+ timestamp: Date.now(),
862
+ });
863
+ },
864
+ });
865
+ if (summaryResponse.usage) {
866
+ totalTokensUsed += summaryResponse.usage.inputTokens + summaryResponse.usage.outputTokens;
867
+ }
1131
868
  await this.options.mqttClient.publishResult({
1132
869
  commandId: command.id,
1133
870
  type: 'complete',
1134
- content: response.content || '',
871
+ content: '',
1135
872
  timestamp: Date.now(),
1136
873
  });
1137
874
  console.log(chalk_1.default.green(` Command completed: ${iteration} iterations, ${totalToolCalls} tool calls, ~${totalTokensUsed} tokens`));
1138
875
  break;
1139
876
  }
1140
- // No artificial token limit — LLM providers return proper errors (400/413)
1141
- // when context exceeds the model's real limit, and those are caught by the
1142
- // error handler above which surfaces them to the user.
1143
877
  // Continue the loop for the next LLM call
1144
878
  continue;
1145
879
  }
1146
880
  // No tool calls - final response.
1147
- // Phase 2a: The 'complete' message includes the full response content.
1148
- // The server-side relay (mqtt-agent-relay.ts) prefers this over
1149
- // reassembled chunks for higher-fidelity persistence.
1150
- // Fix 1: Detect phantom tool use — LLM claims it did something but made 0 tool calls
1151
- const phantom = (0, response_guard_1.detectPhantomToolUse)(response.content || '', totalToolCalls);
1152
- if (phantom.detected) {
1153
- console.log(chalk_1.default.yellow(` ⚠ Phantom tool use detected — injecting correction`));
1154
- // Stream the correction to the user
1155
- await this.options.mqttClient.publishResult({
1156
- commandId: command.id,
1157
- type: 'chunk',
1158
- seq: this.chunkSeq++,
1159
- content: '\n\n---\n⚠️ ' + phantom.correction,
1160
- timestamp: Date.now(),
1161
- });
1162
- }
1163
- // Publish stats before complete
1164
- const latencyMs = Date.now() - commandStartMs;
1165
- await this.options.mqttClient.publishResult({
1166
- commandId: command.id,
1167
- type: 'stats',
1168
- llmCalls: iteration,
1169
- toolsUsed: totalToolCalls,
1170
- inputTokens: totalInputTokens,
1171
- outputTokens: totalOutputTokens,
1172
- cacheReadTokens: totalCacheReadTokens || undefined,
1173
- cacheCreationTokens: totalCacheCreationTokens || undefined,
1174
- totalTokens: totalTokensUsed,
1175
- latencyMs,
1176
- toolCallDurationMs: totalToolCallDurationMs || undefined,
1177
- contextSections: contextSectionCount || undefined,
1178
- model,
1179
- timestamp: Date.now(),
1180
- });
881
+ // DEDUP FIX: Content was already streamed via onChunk callbacks above.
882
+ // The 'complete' message signals end-of-response; we send empty content
883
+ // to avoid the receiver concatenating streamed chunks + full content.
1181
884
  await this.options.mqttClient.publishResult({
1182
885
  commandId: command.id,
1183
886
  type: 'complete',
1184
- content: response.content || '',
887
+ content: '',
1185
888
  timestamp: Date.now(),
1186
889
  });
1187
890
  // Local-first: save assistant response to SQLite + trigger funnel
@@ -1189,6 +892,7 @@ TOOL EFFICIENCY RULES:
1189
892
  try {
1190
893
  const localData = await Promise.resolve().then(() => __importStar(require('./local-data')));
1191
894
  localData.addMessage(localConvId, 'assistant', response.content || '', model);
895
+ // Trigger background memory extraction
1192
896
  (0, local_funnel_1.scheduleFunnelProcessing)(localConvId);
1193
897
  // Track for session idle auto-organize
1194
898
  this.lastOrganizedConvId = localConvId;
@@ -1200,6 +904,7 @@ TOOL EFFICIENCY RULES:
1200
904
  }
1201
905
  }
1202
906
  console.log(chalk_1.default.white(` Response: ${response.content?.slice(0, 200)}${(response.content?.length || 0) > 200 ? '...' : ''}`));
907
+ const latencyMs = Date.now() - commandStartMs;
1203
908
  console.log(`[agent-metrics] commandId=${command.id} contextSections=${contextSectionCount} toolCallsMade=${totalToolCalls} iterations=${iteration} totalTokens=${totalTokensUsed} latencyMs=${latencyMs}`);
1204
909
  console.log(chalk_1.default.green(` Command completed: ${iteration} iterations, ${totalToolCalls} tool calls, ~${totalTokensUsed} tokens`));
1205
910
  break;
@@ -1209,22 +914,7 @@ TOOL EFFICIENCY RULES:
1209
914
  }
1210
915
  }
1211
916
  catch (error) {
1212
- // Capture detailed provider error info for diagnosis
1213
- const details = [` ✗ Error: ${error.message}`];
1214
- if (error.status)
1215
- details.push(` ✗ HTTP status: ${error.status}`);
1216
- if (error.type)
1217
- details.push(` ✗ Error type: ${error.type}`);
1218
- if (error.response?.body)
1219
- details.push(` ✗ Response body: ${JSON.stringify(error.response.body).slice(0, 500)}`);
1220
- if (error.error?.type)
1221
- details.push(` ✗ Provider error type: ${error.error.type}`);
1222
- if (error.cause)
1223
- details.push(` ✗ Cause: ${error.cause}`);
1224
- // For Anthropic "terminated" / stop_reason issues
1225
- if (error.stop_reason)
1226
- details.push(` ✗ Stop reason: ${error.stop_reason}`);
1227
- details.forEach(d => console.error(chalk_1.default.red(d)));
917
+ console.error(chalk_1.default.red(` ✗ Error: ${error.message}`));
1228
918
  await this.publishError(command.id, error.message);
1229
919
  }
1230
920
  finally {
@@ -1246,31 +936,10 @@ TOOL EFFICIENCY RULES:
1246
936
  }
1247
937
  /** Build system prompt manually (without clerk) — original logic */
1248
938
  buildFallbackSystemPrompt(command) {
1249
- let systemPrompt = (0, prompt_template_1.buildDefaultSystemPrompt)({
1250
- botName: this.options.agentName,
1251
- model: this.options.model,
1252
- soulMd: command.context?.soulMd || this.options.systemPrompt,
1253
- });
1254
- // Auto-enumerate available tools so the bot always knows its capabilities
1255
- const toolDefs = (0, index_2.getToolDefinitions)();
1256
- const toolList = toolDefs.map(t => `- **${t.name}**: ${t.description.slice(0, 120)}`).join('\n');
1257
- systemPrompt += `\n\n## Your Available Tools\nYou have ${toolDefs.length} tools available. Use them proactively:\n${toolList}`;
1258
- systemPrompt += `\n\n## Working Style
1259
- - You are autonomous — take action, don't just describe what you could do.
1260
- - For complex tasks, use spawn_subagent to parallelize work.
1261
- - Read project files to understand context before making changes.
1262
- - Commit your work with git_commit when you've made meaningful progress.
1263
- - Use web_search and web_fetch to research unfamiliar topics.
1264
- - Write important context to files so you remember it across sessions.
1265
- - If you're unsure about something destructive, ask first. Otherwise, just do it.`;
1266
- // Platform awareness — always present regardless of soulMd content
1267
- systemPrompt += `\n\n## Extending Your Capabilities
1268
- - You have built-in tools (file ops, shell, git, web search, web fetch, tasks, sub-agents, memory search).
1269
- - If the user asks you to do something you don't have a tool for (e.g. Google Drive, GitHub issues, Slack, email, databases), search the Funolio marketplace with \`search_marketplace\`.
1270
- - If a matching MCP tool server exists, use \`request_mcp_install\` to request permission to install it.
1271
- - You can discover and gain new capabilities on-the-fly through the marketplace.
1272
- - **NEVER tell the user to "do it manually" or "upload it yourself"** — always check the marketplace first and offer to install the right tool.`;
1273
- systemPrompt += '\n\nIMPORTANT: When the user references a project, topic, or past work, use the relevant summaries and conversation history provided below. If no relevant context is available, say so honestly rather than guessing. Use your tools (file browsing, commands) to find project files on the local machine.';
939
+ let systemPrompt = command.context?.soulMd
940
+ || this.options.systemPrompt
941
+ || 'You are a Funolio AI agent running locally on the user\'s machine. You have access to their project files and can execute code.';
942
+ systemPrompt += '\n\nIMPORTANT: When the user references a project, topic, or past work, use the relevant memory/facts provided below. If no relevant facts are available, say so honestly rather than guessing. Use your tools (file browsing, commands) to find project files on the local machine.';
1274
943
  systemPrompt += '\n\nIMPORTANT: If a Workspace Manifest or Recent Operational State section is present, use it before repeating discovery work. Check known local files and directories before fetching from external sources. Do not re-fetch from Drive, GitHub, or the web if the state context shows the artifact is already local unless the user explicitly asks for a fresh external copy.';
1275
944
  systemPrompt += '\n\nDo not end with a deferred promise (for example: "Let me check..."). Return a final answer in this turn, or state exactly what is unavailable.';
1276
945
  systemPrompt += '\n\n' + (0, clerk_model_1.buildTodoInstructions)(this.options.agentName || 'LLM');
@@ -1293,7 +962,13 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
1293
962
  systemPrompt += '\n\n[Bot Memory]\n' + command.context.memoryMd;
1294
963
  }
1295
964
  const sc = command.context?.structuredContext;
1296
- if (sc && (sc.codeRefs?.length || sc.conversations?.length)) {
965
+ if (sc && (sc.decisions?.length || sc.memoryFacts?.length || sc.codeRefs?.length || sc.conversations?.length)) {
966
+ if (sc.decisions?.length) {
967
+ systemPrompt += '\n\n[Key Decisions]\n' + sc.decisions.map(d => `- ${d}`).join('\n');
968
+ }
969
+ if (sc.memoryFacts?.length) {
970
+ systemPrompt += '\n\n[Memory Facts]\n' + sc.memoryFacts.map(f => `- ${f}`).join('\n');
971
+ }
1297
972
  if (sc.conversations?.length) {
1298
973
  systemPrompt += '\n\n[Relevant Conversations]\n' + sc.conversations.map(c => `- ${c}`).join('\n');
1299
974
  }
@@ -1301,8 +976,11 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
1301
976
  systemPrompt += '\n\n[Code References]\n' + sc.codeRefs.map(r => `- ${r}`).join('\n');
1302
977
  }
1303
978
  }
979
+ else if (command.context?.facts?.length) {
980
+ systemPrompt += '\n\nRelevant memory/facts from the user\'s stored conversations:\n' + command.context.facts.map(f => `- ${f}`).join('\n');
981
+ }
1304
982
  else {
1305
- systemPrompt += '\n\nNo relevant stored summaries were found for this query. If the user is asking about a specific project or past work, let them know you don\'t have stored context about it and offer to search their local files instead.';
983
+ systemPrompt += '\n\nNo relevant memories were found for this query. If the user is asking about a specific project or past work, let them know you don\'t have stored context about it and offer to search their local files instead.';
1306
984
  }
1307
985
  if (command.context?.files?.length) {
1308
986
  systemPrompt += '\n\nRelevant files: ' + command.context.files.join(', ');
@@ -1345,18 +1023,19 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
1345
1023
  timestamp: Date.now(),
1346
1024
  });
1347
1025
  }
1348
- /** Update the API key used for requests */
1026
+ /** Update the OAuth token used for requests (called by token refresh) */
1349
1027
  updateToken(token) {
1350
- this.options.apiKey = token;
1028
+ this.options.oauthToken = token;
1029
+ // Recreate the default provider with the new token
1351
1030
  const effectiveProvider = this.options.provider;
1352
- const authMode = this.options.authMode || (token.startsWith('sk-ant-oat') ? 'oauth-bearer' : 'api-key');
1353
1031
  this.llmProvider = (0, index_1.createProvider)(effectiveProvider, {
1354
1032
  apiKey: token,
1355
1033
  model: this.options.model,
1356
- authMode,
1357
- ...(this.options.baseUrl ? { baseUrl: this.options.baseUrl } : {}),
1358
- ...(this.options.apiStyle ? { apiStyle: this.options.apiStyle } : {}),
1034
+ ...(token.startsWith('sk-ant-oat01-') ? { authMode: 'oauth-bearer' } : {}),
1359
1035
  });
1036
+ if (token.startsWith('sk-ant-oat01-') && effectiveProvider === 'anthropic') {
1037
+ this.resolveAndUpgradeAuth(token, this.options.model);
1038
+ }
1360
1039
  }
1361
1040
  }
1362
1041
  exports.MessageLoop = MessageLoop;