funolio-agent 1.0.4 → 1.0.6

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 (50) hide show
  1. package/dist/auth/anthropic-subscription.d.ts +29 -9
  2. package/dist/auth/anthropic-subscription.d.ts.map +1 -1
  3. package/dist/auth/anthropic-subscription.js +133 -12
  4. package/dist/auth/anthropic-subscription.js.map +1 -1
  5. package/dist/auth/auto-detect.d.ts +6 -28
  6. package/dist/auth/auto-detect.d.ts.map +1 -1
  7. package/dist/auth/auto-detect.js +200 -57
  8. package/dist/auth/auto-detect.js.map +1 -1
  9. package/dist/auth/credential-reader.d.ts +4 -24
  10. package/dist/auth/credential-reader.d.ts.map +1 -1
  11. package/dist/auth/credential-reader.js +256 -31
  12. package/dist/auth/credential-reader.js.map +1 -1
  13. package/dist/auth/credential-status.d.ts +27 -7
  14. package/dist/auth/credential-status.d.ts.map +1 -1
  15. package/dist/auth/credential-status.js +95 -7
  16. package/dist/auth/credential-status.js.map +1 -1
  17. package/dist/auth/index.d.ts +2 -9
  18. package/dist/auth/index.d.ts.map +1 -1
  19. package/dist/auth/index.js +10 -10
  20. package/dist/auth/index.js.map +1 -1
  21. package/dist/auth/subscription-runtime.d.ts +23 -19
  22. package/dist/auth/subscription-runtime.d.ts.map +1 -1
  23. package/dist/auth/subscription-runtime.js +292 -24
  24. package/dist/auth/subscription-runtime.js.map +1 -1
  25. package/dist/auth/token-refresh.d.ts +28 -19
  26. package/dist/auth/token-refresh.d.ts.map +1 -1
  27. package/dist/auth/token-refresh.js +464 -26
  28. package/dist/auth/token-refresh.js.map +1 -1
  29. package/dist/bot-manager.d.ts +6 -6
  30. package/dist/bot-manager.d.ts.map +1 -1
  31. package/dist/bot-manager.js +61 -26
  32. package/dist/bot-manager.js.map +1 -1
  33. package/dist/commands/start.d.ts.map +1 -1
  34. package/dist/commands/start.js +223 -49
  35. package/dist/commands/start.js.map +1 -1
  36. package/dist/message-loop.d.ts +10 -2
  37. package/dist/message-loop.d.ts.map +1 -1
  38. package/dist/message-loop.js +249 -184
  39. package/dist/message-loop.js.map +1 -1
  40. package/dist/providers/anthropic.d.ts +5 -0
  41. package/dist/providers/anthropic.d.ts.map +1 -1
  42. package/dist/providers/anthropic.js +48 -13
  43. package/dist/providers/anthropic.js.map +1 -1
  44. package/dist/providers/index.d.ts +2 -2
  45. package/dist/providers/index.d.ts.map +1 -1
  46. package/dist/wizard-support.d.ts +2 -2
  47. package/dist/wizard-support.d.ts.map +1 -1
  48. package/dist/wizard-support.js +93 -80
  49. package/dist/wizard-support.js.map +1 -1
  50. package/package.json +1 -1
@@ -55,6 +55,8 @@ const response_guard_1 = require("./response-guard");
55
55
  const context_compressor_1 = require("./context-compressor");
56
56
  const crypto = __importStar(require("crypto"));
57
57
  const data = __importStar(require("./local-data"));
58
+ const agent_config_1 = require("./agent-config");
59
+ const subscription_runtime_1 = require("./auth/subscription-runtime");
58
60
  const prompt_template_1 = require("./prompt-template");
59
61
  /** Determine priority from an AgentCommand */
60
62
  function getCommandPriority(command) {
@@ -70,47 +72,6 @@ function getCommandPriority(command) {
70
72
  return 'medium';
71
73
  }
72
74
  const PRIORITY_ORDER = { high: 0, medium: 1, low: 2 };
73
- function classifyToolName(toolName) {
74
- switch (toolName) {
75
- case 'read_file':
76
- return 'file_read';
77
- case 'list_directory':
78
- return 'directory_scan';
79
- case 'write_file':
80
- case 'edit_file':
81
- return 'file_write';
82
- case 'run_command':
83
- case 'git_status':
84
- case 'git_diff':
85
- case 'git_commit':
86
- return 'command_run';
87
- case 'web_fetch':
88
- return 'web_fetch';
89
- default:
90
- return 'workspace_hint';
91
- }
92
- }
93
- function extractToolLocation(toolName, args, projectDir) {
94
- const path = typeof args.path === 'string'
95
- ? args.path
96
- : typeof args.filePath === 'string'
97
- ? args.filePath
98
- : typeof args.directory === 'string'
99
- ? args.directory
100
- : undefined;
101
- const cwd = typeof args.cwd === 'string'
102
- ? args.cwd
103
- : toolName === 'run_command' || toolName.startsWith('git_')
104
- ? projectDir
105
- : undefined;
106
- const url = typeof args.url === 'string' ? args.url : undefined;
107
- return { path, cwd, url };
108
- }
109
- function summarizeToolResult(output, isError) {
110
- const prefix = isError ? 'Tool failed: ' : 'Tool completed: ';
111
- const compact = output.replace(/\s+/g, ' ').trim();
112
- return `${prefix}${compact}`.slice(0, 240);
113
- }
114
75
  class MessageLoop {
115
76
  options;
116
77
  llmProvider;
@@ -136,19 +97,23 @@ class MessageLoop {
136
97
  static SCHEDULED_TASK_TIMEOUT_MS = 5 * 60 * 1000;
137
98
  scheduledTaskTimer = null;
138
99
  static IDLE_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes
100
+ static SERVER_SYNC_RETRY_MAX = 1;
139
101
  constructor(options) {
140
102
  this.options = options;
141
- // Use API key or OAuth bearer token for LLM calls.
142
- // CLI providers (claude-cli, codex-cli) don't use this path at all.
103
+ // DO NOT remap Claude CLI OAuth sessions to the public Anthropic runtime
104
+ // without explicit user approval.
143
105
  const effectiveKey = options.oauthToken || options.apiKey || '';
144
106
  const effectiveProvider = options.provider;
145
- const isOAuthBearer = options.authMode === 'oauth-bearer'
146
- || (!!options.oauthToken && options.oauthToken.startsWith('sk-ant-oat'));
107
+ const isOAuthToken = effectiveKey.startsWith('sk-ant-oat01-');
147
108
  this.llmProvider = (0, index_1.createProvider)(effectiveProvider, {
148
109
  apiKey: effectiveKey,
149
110
  model: options.model,
150
- ...(isOAuthBearer ? { authMode: 'oauth-bearer' } : {}),
111
+ ...(isOAuthToken ? { authMode: 'oauth-bearer' } : {}),
151
112
  });
113
+ // Async: attempt to resolve Anthropic subscription auth (create_api_key exchange)
114
+ if (isOAuthToken && effectiveProvider === 'anthropic') {
115
+ this.resolveAndUpgradeAuth(effectiveKey, options.model);
116
+ }
152
117
  // Resolve Funolio API credentials for bot status reporting
153
118
  const cfg = (0, config_1.loadConfig)();
154
119
  const funoliApiKey = cfg.auth?.token || process.env.FUNOLIO_API_KEY || '';
@@ -159,7 +124,7 @@ class MessageLoop {
159
124
  llmProvider: effectiveProvider,
160
125
  llmModel: options.model,
161
126
  llmApiKey: effectiveKey,
162
- llmAuthMode: isOAuthBearer ? 'oauth-bearer' : 'api-key',
127
+ llmAuthMode: isOAuthToken ? 'oauth-bearer' : 'api-key',
163
128
  apiKey: funoliApiKey,
164
129
  apiBaseUrl: funoliApiBaseUrl,
165
130
  botName: options.agentName || 'funolio-agent',
@@ -196,6 +161,147 @@ class MessageLoop {
196
161
  // Session idle detection timer (checks every 60s)
197
162
  this.idleTimer = setInterval(() => this.checkIdleSession(), 60_000);
198
163
  }
164
+ /**
165
+ * Attempt to resolve Anthropic subscription auth via create_api_key exchange.
166
+ * If successful, recreates the LLM provider with the exchanged API key (which
167
+ * works with standard x-api-key auth and unlocks all models on the subscription).
168
+ * Falls back to bearer mode silently if exchange fails.
169
+ */
170
+ async resolveAndUpgradeAuth(oauthToken, model) {
171
+ try {
172
+ const runtime = await (0, subscription_runtime_1.resolveClaudeSubscriptionRuntime)({
173
+ preferredModel: model,
174
+ inputCredential: {
175
+ provider: 'anthropic',
176
+ accessToken: oauthToken,
177
+ refreshToken: this.options.resolvedAuth?.credential?.refreshToken || '',
178
+ expiresAt: this.options.resolvedAuth?.credential?.expiresAt || 0,
179
+ },
180
+ inputSource: 'request:anthropic',
181
+ persistInputCredential: true,
182
+ });
183
+ if (!runtime)
184
+ return;
185
+ if (this.options.resolvedAuth?.credential && runtime.authMode === 'oauth-bearer') {
186
+ this.options.resolvedAuth.credential.accessToken = runtime.apiKey;
187
+ }
188
+ if (this.resolvedAuth?.credential && runtime.authMode === 'oauth-bearer') {
189
+ this.resolvedAuth.credential.accessToken = runtime.apiKey;
190
+ this.resolvedAuth.apiKey = runtime.apiKey;
191
+ }
192
+ if (runtime.authMode !== 'oauth-bearer') {
193
+ // Successfully exchanged OAuth token for a temporary API key
194
+ console.log('[message-loop] Anthropic subscription auth: using exchanged API key');
195
+ this.llmProvider = (0, index_1.createProvider)('anthropic', {
196
+ apiKey: runtime.apiKey,
197
+ model,
198
+ });
199
+ }
200
+ else {
201
+ // Bearer fallback — recreate provider with the (possibly refreshed) token
202
+ const currentToken = runtime.apiKey || this.options.resolvedAuth?.credential?.accessToken || oauthToken;
203
+ console.log('[message-loop] Anthropic subscription auth: using bearer fallback' +
204
+ (currentToken !== oauthToken ? ' (with refreshed token)' : ''));
205
+ this.llmProvider = (0, index_1.createProvider)('anthropic', {
206
+ apiKey: currentToken,
207
+ model,
208
+ authMode: 'oauth-bearer',
209
+ });
210
+ }
211
+ }
212
+ catch (err) {
213
+ console.warn('[message-loop] Anthropic subscription auth resolution failed, keeping bearer mode:', err?.message || err);
214
+ }
215
+ }
216
+ async syncOAuthFromServer(reason) {
217
+ const providerId = this.options.provider;
218
+ if (!providerId || providerId.endsWith('-cli'))
219
+ return false;
220
+ const cfg = (0, config_1.loadConfig)();
221
+ const authToken = cfg.auth?.token;
222
+ if (!authToken)
223
+ return false;
224
+ try {
225
+ const res = await fetch(`${config_1.FUNOLIO_API_URL}/api/v1/agent/config`, {
226
+ headers: { Authorization: `Bearer ${authToken}` },
227
+ });
228
+ if (!res.ok)
229
+ return false;
230
+ const body = await res.json();
231
+ const provider = (body.providers || []).find((p) => p.id === providerId && p.connectionType === 'oauth');
232
+ if (!provider?.access_token)
233
+ return false;
234
+ const currentAccess = this.resolvedAuth?.credential?.accessToken || this.options.oauthToken || '';
235
+ const currentRefresh = this.resolvedAuth?.credential?.refreshToken || this.options.resolvedAuth?.credential?.refreshToken || '';
236
+ const currentExpiry = this.resolvedAuth?.credential?.expiresAt || this.options.resolvedAuth?.credential?.expiresAt || 0;
237
+ const nextExpiry = provider.expires_at || 0;
238
+ const changed = provider.access_token !== currentAccess
239
+ || (provider.refresh_token || '') !== currentRefresh
240
+ || nextExpiry > currentExpiry;
241
+ if (!changed)
242
+ return false;
243
+ if (this.resolvedAuth) {
244
+ this.resolvedAuth.apiKey = provider.access_token;
245
+ this.resolvedAuth.expired = false;
246
+ this.resolvedAuth.error = undefined;
247
+ if (this.resolvedAuth.credential) {
248
+ this.resolvedAuth.credential.accessToken = provider.access_token;
249
+ if (provider.refresh_token)
250
+ this.resolvedAuth.credential.refreshToken = provider.refresh_token;
251
+ if (provider.expires_at)
252
+ this.resolvedAuth.credential.expiresAt = provider.expires_at;
253
+ }
254
+ }
255
+ if (this.options.resolvedAuth) {
256
+ this.options.resolvedAuth.apiKey = provider.access_token;
257
+ this.options.resolvedAuth.expired = false;
258
+ this.options.resolvedAuth.error = undefined;
259
+ if (this.options.resolvedAuth.credential) {
260
+ this.options.resolvedAuth.credential.accessToken = provider.access_token;
261
+ if (provider.refresh_token)
262
+ this.options.resolvedAuth.credential.refreshToken = provider.refresh_token;
263
+ if (provider.expires_at)
264
+ this.options.resolvedAuth.credential.expiresAt = provider.expires_at;
265
+ }
266
+ }
267
+ this.options.oauthToken = provider.access_token;
268
+ const local = (0, config_1.loadConfig)();
269
+ if (!local.providers)
270
+ local.providers = [];
271
+ const idx = local.providers.findIndex((p) => p.id === providerId);
272
+ if (idx >= 0) {
273
+ local.providers[idx].authType = 'oauth';
274
+ local.providers[idx].oauthToken = provider.access_token;
275
+ if (provider.refresh_token)
276
+ local.providers[idx].oauthRefreshToken = provider.refresh_token;
277
+ if (provider.expires_at)
278
+ local.providers[idx].oauthExpiresAt = provider.expires_at;
279
+ if (provider.defaultModel)
280
+ local.providers[idx].defaultModel = provider.defaultModel;
281
+ }
282
+ (0, config_1.saveConfig)(local);
283
+ if (this.options.agentName) {
284
+ const agentLocal = (0, agent_config_1.loadAgentConfig)(this.options.agentName);
285
+ if (agentLocal && agentLocal.provider === providerId) {
286
+ agentLocal.oauthToken = provider.access_token;
287
+ if (provider.refresh_token)
288
+ agentLocal.oauthRefreshToken = provider.refresh_token;
289
+ if (provider.expires_at)
290
+ agentLocal.oauthExpiresAt = provider.expires_at;
291
+ if (provider.defaultModel)
292
+ agentLocal.model = provider.defaultModel;
293
+ (0, agent_config_1.saveAgentConfig)(this.options.agentName, agentLocal);
294
+ }
295
+ }
296
+ this.updateToken(provider.access_token);
297
+ console.log(chalk_1.default.green(` [auth] Synced updated ${providerId} OAuth credentials from server (${reason})`));
298
+ return true;
299
+ }
300
+ catch (err) {
301
+ console.log(chalk_1.default.gray(` [auth] Server OAuth sync skipped (${reason}): ${err?.message || err}`));
302
+ return false;
303
+ }
304
+ }
199
305
  /** Check if a session has gone idle and trigger auto-organize */
200
306
  async checkIdleSession() {
201
307
  if (!this.lastMessageAt || !(0, auto_organizer_1.isAutoOrganizeEnabled)())
@@ -313,13 +419,48 @@ class MessageLoop {
313
419
  const overrideModel = command.model?.model;
314
420
  let provider = overrideProvider || this.options.provider;
315
421
  const model = overrideModel || this.options.model;
316
- const apiKey = this.options.apiKey;
422
+ const apiKey = this.options.oauthToken || this.options.apiKey;
317
423
  // Only create a new provider if provider or model actually changed (ignore apiKey overrides from server)
318
424
  const hasOverride = (overrideProvider && overrideProvider !== this.options.provider) ||
319
425
  (overrideModel && overrideModel !== this.options.model);
320
426
  let llm;
321
427
  if (hasOverride) {
322
- llm = (0, index_1.createProvider)(provider, { apiKey, model });
428
+ // For overrides with OAuth tokens, try subscription auth resolution
429
+ if ((apiKey || '').startsWith('sk-ant-oat01-') && provider === 'anthropic') {
430
+ try {
431
+ const runtime = await (0, subscription_runtime_1.resolveClaudeSubscriptionRuntime)({
432
+ preferredModel: model,
433
+ inputCredential: {
434
+ provider: 'anthropic',
435
+ accessToken: apiKey,
436
+ refreshToken: this.options.resolvedAuth?.credential?.refreshToken || '',
437
+ expiresAt: this.options.resolvedAuth?.credential?.expiresAt || 0,
438
+ },
439
+ inputSource: 'request:anthropic',
440
+ persistInputCredential: true,
441
+ });
442
+ const resolvedKey = runtime?.apiKey || apiKey;
443
+ const authMode = runtime?.authMode;
444
+ if (this.options.resolvedAuth?.credential && authMode === 'oauth-bearer') {
445
+ this.options.resolvedAuth.credential.accessToken = resolvedKey;
446
+ }
447
+ if (this.resolvedAuth?.credential && authMode === 'oauth-bearer') {
448
+ this.resolvedAuth.credential.accessToken = resolvedKey;
449
+ this.resolvedAuth.apiKey = resolvedKey;
450
+ }
451
+ llm = (0, index_1.createProvider)(provider, {
452
+ apiKey: resolvedKey,
453
+ model,
454
+ ...(authMode === 'oauth-bearer' ? { authMode: 'oauth-bearer' } : {}),
455
+ });
456
+ }
457
+ catch {
458
+ llm = (0, index_1.createProvider)(provider, { apiKey, model, authMode: 'oauth-bearer' });
459
+ }
460
+ }
461
+ else {
462
+ llm = (0, index_1.createProvider)(provider, { apiKey, model });
463
+ }
323
464
  }
324
465
  else {
325
466
  llm = this.llmProvider;
@@ -404,8 +545,6 @@ class MessageLoop {
404
545
  ...this.toolContext,
405
546
  projectId: effectiveProjectId ?? null,
406
547
  abortSignal: commandAbortController.signal,
407
- // Use project folder as working directory if available
408
- ...(effectiveProject?.folder ? { projectDir: effectiveProject.folder } : {}),
409
548
  };
410
549
  // ─── Orchestrator Mode Branch ─────────────────────────
411
550
  // Resolve the selected bot profile — from command.bot, conversation bot_id, or default
@@ -578,40 +717,15 @@ TOOL EFFICIENCY RULES:
578
717
  const str = JSON.stringify(args || {}).slice(0, 200);
579
718
  return crypto.createHash('md5').update(str).digest('hex').slice(0, 12);
580
719
  }
581
- function getRetryKey(name, args) {
582
- // Bug 6: For meta-tools, include action in retry key so different actions don't share budget
583
- if (args?.action)
584
- return `${name}:${args.action}`;
585
- return name;
586
- }
587
720
  function trackToolFailure(name, args) {
588
- const key = `${getRetryKey(name, args)}:${getArgsHash(args)}`;
589
- const retryKey = getRetryKey(name, args);
721
+ const key = `${name}:${getArgsHash(args)}`;
590
722
  toolFailuresByKey.set(key, (toolFailuresByKey.get(key) || 0) + 1);
591
- toolFailuresByName.set(retryKey, (toolFailuresByName.get(retryKey) || 0) + 1);
592
- }
593
- // Futility detection: track consecutive low-value tool results
594
- let consecutiveEmptyResults = 0;
595
- const FUTILITY_THRESHOLD = 6; // after 6 consecutive empty/low-value results, nudge the LLM
596
- let futilityNudgeInjected = false;
597
- function isLowValueResult(output) {
598
- if (!output || output.length < 30)
599
- return true;
600
- const lower = output.toLowerCase();
601
- if (lower.includes('no matches found') || lower.includes('no results'))
602
- return true;
603
- if (lower.includes('0 bytes') || lower.includes('empty'))
604
- return true;
605
- // exit code 1 with no meaningful stdout
606
- if (/\[exit code: [^0]/.test(lower) && output.replace(/\[exit code:.*?\]/g, '').trim().length < 20)
607
- return true;
608
- return false;
723
+ toolFailuresByName.set(name, (toolFailuresByName.get(name) || 0) + 1);
609
724
  }
610
725
  function checkRetryBudget(name, args) {
611
- const key = `${getRetryKey(name, args)}:${getArgsHash(args)}`;
612
- const retryKey = getRetryKey(name, args);
726
+ const key = `${name}:${getArgsHash(args)}`;
613
727
  const keyCount = toolFailuresByKey.get(key) || 0;
614
- const nameCount = toolFailuresByName.get(retryKey) || 0;
728
+ const nameCount = toolFailuresByName.get(name) || 0;
615
729
  if (keyCount >= 2) {
616
730
  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.`;
617
731
  }
@@ -622,7 +736,7 @@ TOOL EFFICIENCY RULES:
622
736
  }
623
737
  // Agentic loop - keep calling LLM until no more tool calls
624
738
  let iteration = 0;
625
- const MAX_ITERATIONS = 30;
739
+ const MAX_ITERATIONS = 100;
626
740
  const GLOBAL_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes wall-clock timeout
627
741
  let totalTokensUsed = 0;
628
742
  let totalInputTokens = 0;
@@ -630,6 +744,7 @@ TOOL EFFICIENCY RULES:
630
744
  let totalCacheReadTokens = 0;
631
745
  let totalCacheCreationTokens = 0;
632
746
  let totalToolCallDurationMs = 0;
747
+ let serverSyncRetries = 0;
633
748
  const deniedTools = new Set(); // Track tools denied by user — don't re-ask
634
749
  const commandStartMs = Date.now();
635
750
  // Publish prompt context for "View Prompt" panel
@@ -672,14 +787,34 @@ TOOL EFFICIENCY RULES:
672
787
  timestamp: Date.now(),
673
788
  });
674
789
  }
675
- // API keys don't expire ensureFreshToken is a no-op for api-key source
790
+ // Refresh OAuth token before each LLM call if needed
676
791
  if (this.resolvedAuth) {
677
792
  const refreshed = await (0, auto_detect_1.ensureFreshToken)(this.resolvedAuth);
793
+ if (refreshed.expired) {
794
+ if (serverSyncRetries < MessageLoop.SERVER_SYNC_RETRY_MAX) {
795
+ const synced = await this.syncOAuthFromServer('refresh-failed');
796
+ if (synced) {
797
+ serverSyncRetries++;
798
+ continue;
799
+ }
800
+ }
801
+ console.error(chalk_1.default.red(` [auth] Token refresh failed: ${refreshed.error}`));
802
+ await this.options.mqttClient.publishResult({
803
+ commandId: command.id,
804
+ type: 'error',
805
+ error: 'Your authentication token has expired or been revoked. Please re-authenticate by running `funolio-agent login` or `funolio-agent configure`.',
806
+ timestamp: Date.now(),
807
+ });
808
+ break;
809
+ }
678
810
  if (refreshed.apiKey !== this.resolvedAuth.apiKey) {
679
811
  this.resolvedAuth = refreshed;
680
812
  this.updateToken(refreshed.apiKey);
681
813
  }
682
814
  }
815
+ // With OAuth auto-detection, all providers now use direct API calls
816
+ // with full tool support. The old CLI_PROVIDERS check is kept only
817
+ // for logging/diagnostics but no longer gates tool availability.
683
818
  // Smart tool filtering: only send relevant tools to reduce token usage
684
819
  const filteredTools = (0, tool_filter_1.filterToolsForMessage)(this.toolDefinitions, this.builtinToolDefinitions, command.prompt);
685
820
  if (filteredTools.length < this.toolDefinitions.length) {
@@ -713,17 +848,26 @@ TOOL EFFICIENCY RULES:
713
848
  const msg = llmError?.message || String(llmError);
714
849
  const isAuthError = /401|403|unauthorized|forbidden|authentication/i.test(msg);
715
850
  if (isAuthError) {
851
+ if (serverSyncRetries < MessageLoop.SERVER_SYNC_RETRY_MAX) {
852
+ const synced = await this.syncOAuthFromServer('llm-auth-error');
853
+ if (synced) {
854
+ serverSyncRetries++;
855
+ continue;
856
+ }
857
+ }
716
858
  console.error(chalk_1.default.red(` [auth] LLM API authentication failed: ${msg}`));
717
859
  await this.options.mqttClient.publishResult({
718
860
  commandId: command.id,
719
861
  type: 'error',
720
- error: 'API authentication failed. Check your API key or run `funolio-agent configure` to update it.',
862
+ error: 'API authentication failed. Your token may have expired run `funolio-agent login` to refresh.',
721
863
  timestamp: Date.now(),
722
864
  });
723
865
  break;
724
866
  }
725
867
  throw llmError;
726
868
  }
869
+ // Any successful provider response clears one-shot sync retry budget
870
+ serverSyncRetries = 0;
727
871
  // Track token usage across iterations
728
872
  if (response.usage) {
729
873
  totalTokensUsed += response.usage.inputTokens + response.usage.outputTokens;
@@ -743,8 +887,6 @@ TOOL EFFICIENCY RULES:
743
887
  for (const toolCall of response.toolCalls) {
744
888
  totalToolCalls++;
745
889
  console.log(chalk_1.default.cyan(` 🔧 Tool: ${toolCall.name}(${JSON.stringify(toolCall.arguments).slice(0, 60)}...)`));
746
- const toolSummary = (0, approval_1.generateToolSummary)(toolCall.name, toolCall.arguments);
747
- const toolLocation = extractToolLocation(toolCall.name, toolCall.arguments, commandToolContext.projectDir);
748
890
  // Check permission before execution
749
891
  const permMode = this.options.permissionMode || 'autopilot';
750
892
  let approval = (0, approval_1.checkPermission)(toolCall.name, permMode, this.options.enabledTools);
@@ -755,29 +897,20 @@ TOOL EFFICIENCY RULES:
755
897
  // If not auto-approved, request interactive approval via MQTT
756
898
  if (!approval.approved && permMode !== 'autopilot' && !deniedTools.has(toolCall.name)) {
757
899
  console.log(chalk_1.default.yellow(` 🔐 Requesting user approval for: ${toolCall.name}`));
758
- approval = await this.approvalManager.requestApproval(command.id, toolCall.name, toolCall.arguments, {
759
- onRequest: async (request) => {
760
- await this.options.mqttClient.publishResult({
761
- commandId: command.id,
762
- type: 'approval_request',
763
- requestId: request.requestId,
764
- riskLevel: request.riskLevel,
765
- summary: request.summary,
766
- toolCall: {
767
- id: toolCall.id,
768
- name: toolCall.name,
769
- arguments: toolCall.arguments,
770
- classification: classifyToolName(toolCall.name),
771
- summary: toolSummary,
772
- path: toolLocation.path,
773
- cwd: toolLocation.cwd,
774
- url: toolLocation.url,
775
- importance: request.riskLevel === 'destructive' ? 'high' : 'medium',
776
- },
777
- timestamp: Date.now(),
778
- });
900
+ // Generate requestId up front so the MQTT result and pending map use the same ID
901
+ const approvalRequestId = require('crypto').randomUUID();
902
+ await this.options.mqttClient.publishResult({
903
+ commandId: command.id,
904
+ type: 'approval_request',
905
+ requestId: approvalRequestId,
906
+ toolCall: {
907
+ id: toolCall.id,
908
+ name: toolCall.name,
909
+ arguments: toolCall.arguments,
779
910
  },
911
+ timestamp: Date.now(),
780
912
  });
913
+ approval = await this.approvalManager.requestApproval(command.id, toolCall.name, toolCall.arguments);
781
914
  // If user chose "always allow", persist it
782
915
  if (approval.approved && approval.remember) {
783
916
  (0, approval_1.rememberTool)(toolCall.name);
@@ -787,26 +920,19 @@ TOOL EFFICIENCY RULES:
787
920
  if (!approval.approved) {
788
921
  deniedTools.add(toolCall.name);
789
922
  console.log(chalk_1.default.yellow(` ⚠ Tool denied: ${approval.reason}`));
790
- const deniedOutput = `PERMISSION_DENIED: ${approval.reason}`;
791
923
  await this.options.mqttClient.publishResult({
792
924
  commandId: command.id,
793
925
  type: 'tool_result',
794
926
  toolResult: {
795
927
  callId: toolCall.id,
796
- output: deniedOutput,
928
+ output: `PERMISSION_DENIED: ${approval.reason}`,
797
929
  isError: true,
798
- success: false,
799
- summary: summarizeToolResult(deniedOutput, true),
800
- path: toolLocation.path,
801
- cwd: toolLocation.cwd,
802
- url: toolLocation.url,
803
- pathsTouched: toolLocation.path ? [toolLocation.path] : [],
804
930
  },
805
931
  timestamp: Date.now(),
806
932
  });
807
933
  messages.push({
808
934
  role: 'tool',
809
- content: deniedOutput,
935
+ content: `PERMISSION_DENIED: ${approval.reason}`,
810
936
  toolCallId: toolCall.id,
811
937
  toolName: toolCall.name,
812
938
  });
@@ -819,12 +945,6 @@ TOOL EFFICIENCY RULES:
819
945
  id: toolCall.id,
820
946
  name: toolCall.name,
821
947
  arguments: toolCall.arguments,
822
- classification: classifyToolName(toolCall.name),
823
- summary: toolSummary,
824
- path: toolLocation.path,
825
- cwd: toolLocation.cwd,
826
- url: toolLocation.url,
827
- importance: (0, approval_1.getRiskLevel)(toolCall.name) === 'destructive' ? 'high' : 'medium',
828
948
  },
829
949
  timestamp: Date.now(),
830
950
  });
@@ -857,16 +977,9 @@ TOOL EFFICIENCY RULES:
857
977
  const toolStartMs = Date.now();
858
978
  const rawResult = await (0, tools_1.executeToolWithMCP)({ id: toolCall.id, name: toolCall.name, arguments: toolCall.arguments }, commandToolContext, this.options.mcpManager);
859
979
  const toolResult = await (0, verification_1.verifyToolResult)(rawResult, toolCall.arguments, commandToolContext);
860
- let resultOutput = toolResult.success
980
+ const resultOutput = toolResult.success
861
981
  ? toolResult.output
862
982
  : `ERROR: ${toolResult.error || 'Unknown error'}`;
863
- // Bug 2 & 7: Cap tool result size to prevent context blowup
864
- const MAX_TOOL_RESULT_CHARS = 50_000;
865
- if (resultOutput.length > MAX_TOOL_RESULT_CHARS) {
866
- const originalLength = resultOutput.length;
867
- resultOutput = resultOutput.slice(0, MAX_TOOL_RESULT_CHARS) +
868
- `\n\n[OUTPUT TRUNCATED — was ${originalLength} chars. Use more specific commands to read smaller portions.]`;
869
- }
870
983
  // Track failures for retry budget (Optimization 2)
871
984
  if (!toolResult.success) {
872
985
  trackToolFailure(toolCall.name, toolCall.arguments);
@@ -883,13 +996,6 @@ TOOL EFFICIENCY RULES:
883
996
  output: resultOutput,
884
997
  isError: !toolResult.success,
885
998
  durationMs: toolDurationMs,
886
- success: toolResult.success,
887
- summary: summarizeToolResult(resultOutput, !toolResult.success),
888
- path: toolLocation.path,
889
- cwd: toolLocation.cwd,
890
- url: toolLocation.url,
891
- pathsTouched: toolLocation.path ? [toolLocation.path] : [],
892
- exitCode: typeof toolResult.exit_code === 'number' ? toolResult.exit_code : undefined,
893
999
  },
894
1000
  timestamp: Date.now(),
895
1001
  });
@@ -900,22 +1006,6 @@ TOOL EFFICIENCY RULES:
900
1006
  toolCallId: toolCall.id,
901
1007
  toolName: toolCall.name,
902
1008
  });
903
- // Futility detection: track consecutive low-value results
904
- if (isLowValueResult(resultOutput)) {
905
- consecutiveEmptyResults++;
906
- }
907
- else {
908
- consecutiveEmptyResults = 0; // reset on any meaningful result
909
- }
910
- }
911
- // Futility check: if too many consecutive empty results, nudge the LLM to stop spinning
912
- if (consecutiveEmptyResults >= FUTILITY_THRESHOLD && !futilityNudgeInjected) {
913
- futilityNudgeInjected = true;
914
- console.log(chalk_1.default.yellow(` ⚠ Futility detected: ${consecutiveEmptyResults} consecutive low-value tool results — nudging LLM`));
915
- messages.push({
916
- role: 'user',
917
- 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.',
918
- });
919
1009
  }
920
1010
  // --- Rate limit checks ---
921
1011
  // Warn at 80% of tool call limit
@@ -1094,7 +1184,6 @@ TOOL EFFICIENCY RULES:
1094
1184
  - You can discover and gain new capabilities on-the-fly through the marketplace.
1095
1185
  - **NEVER tell the user to "do it manually" or "upload it yourself"** — always check the marketplace first and offer to install the right tool.`;
1096
1186
  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.';
1097
- 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.';
1098
1187
  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.';
1099
1188
  systemPrompt += '\n\n' + (0, clerk_model_1.buildTodoInstructions)(this.options.agentName || 'LLM');
1100
1189
  // Inject model self-switch info
@@ -1139,32 +1228,6 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
1139
1228
  if (command.context?.files?.length) {
1140
1229
  systemPrompt += '\n\nRelevant files: ' + command.context.files.join(', ');
1141
1230
  }
1142
- if (command.context?.stateContext?.summaryText) {
1143
- systemPrompt += '\n\n[Recent Operational State]\n' + command.context.stateContext.summaryText;
1144
- }
1145
- if (command.context?.workspaceManifest) {
1146
- const manifestLines = [];
1147
- const manifest = command.context.workspaceManifest;
1148
- if (manifest.projectRoot)
1149
- manifestLines.push(`- Project root: ${manifest.projectRoot}`);
1150
- if (manifest.likelyWorkingDirectory)
1151
- manifestLines.push(`- Likely working directory: ${manifest.likelyWorkingDirectory}`);
1152
- if (manifest.recentlyReadFiles?.length)
1153
- manifestLines.push(`- Recently read files: ${manifest.recentlyReadFiles.join(', ')}`);
1154
- if (manifest.recentlyWrittenFiles?.length)
1155
- manifestLines.push(`- Recently written files: ${manifest.recentlyWrittenFiles.join(', ')}`);
1156
- if (manifest.recentlyScannedDirectories?.length)
1157
- manifestLines.push(`- Recently scanned directories: ${manifest.recentlyScannedDirectories.join(', ')}`);
1158
- if (manifest.recentDownloads?.length)
1159
- manifestLines.push(`- Recent downloads: ${manifest.recentDownloads.join(', ')}`);
1160
- if (manifest.recentFailures?.length)
1161
- manifestLines.push(`- Recent failures: ${manifest.recentFailures.join(' | ')}`);
1162
- if (manifest.startHereHints?.length)
1163
- manifestLines.push(`- Start here: ${manifest.startHereHints.join(', ')}`);
1164
- if (manifestLines.length) {
1165
- systemPrompt += '\n\n[Workspace Manifest]\n' + manifestLines.join('\n');
1166
- }
1167
- }
1168
1231
  return systemPrompt;
1169
1232
  }
1170
1233
  async publishError(commandId, error) {
@@ -1177,17 +1240,19 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
1177
1240
  timestamp: Date.now(),
1178
1241
  });
1179
1242
  }
1180
- /** Update the API key used for requests */
1243
+ /** Update the OAuth token used for requests (called by token refresh) */
1181
1244
  updateToken(token) {
1182
- this.options.apiKey = token;
1245
+ this.options.oauthToken = token;
1246
+ // Recreate the default provider with the new token
1183
1247
  const effectiveProvider = this.options.provider;
1184
- const isOAuthBearer = this.options.authMode === 'oauth-bearer'
1185
- || (token.startsWith('sk-ant-oat'));
1186
1248
  this.llmProvider = (0, index_1.createProvider)(effectiveProvider, {
1187
1249
  apiKey: token,
1188
1250
  model: this.options.model,
1189
- ...(isOAuthBearer ? { authMode: 'oauth-bearer' } : {}),
1251
+ ...(token.startsWith('sk-ant-oat01-') ? { authMode: 'oauth-bearer' } : {}),
1190
1252
  });
1253
+ if (token.startsWith('sk-ant-oat01-') && effectiveProvider === 'anthropic') {
1254
+ this.resolveAndUpgradeAuth(token, this.options.model);
1255
+ }
1191
1256
  }
1192
1257
  }
1193
1258
  exports.MessageLoop = MessageLoop;