funolio-agent 1.0.7 → 1.0.47

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 (155) hide show
  1. package/dist/approval.d.ts +1 -0
  2. package/dist/approval.d.ts.map +1 -1
  3. package/dist/approval.js +12 -0
  4. package/dist/approval.js.map +1 -1
  5. package/dist/auth/auto-detect.d.ts +11 -3
  6. package/dist/auth/auto-detect.d.ts.map +1 -1
  7. package/dist/auth/auto-detect.js +136 -168
  8. package/dist/auth/auto-detect.js.map +1 -1
  9. package/dist/auth/subscription-runtime.js +1 -1
  10. package/dist/auth/subscription-runtime.js.map +1 -1
  11. package/dist/backfill.d.ts.map +1 -1
  12. package/dist/backfill.js +31 -28
  13. package/dist/backfill.js.map +1 -1
  14. package/dist/bot-manager.d.ts +16 -4
  15. package/dist/bot-manager.d.ts.map +1 -1
  16. package/dist/bot-manager.js +280 -73
  17. package/dist/bot-manager.js.map +1 -1
  18. package/dist/clerk-model.d.ts +10 -6
  19. package/dist/clerk-model.d.ts.map +1 -1
  20. package/dist/clerk-model.js +39 -16
  21. package/dist/clerk-model.js.map +1 -1
  22. package/dist/cli.js +7 -2
  23. package/dist/cli.js.map +1 -1
  24. package/dist/commands/import-history.js +5 -1
  25. package/dist/commands/import-history.js.map +1 -1
  26. package/dist/commands/start.d.ts.map +1 -1
  27. package/dist/commands/start.js +57 -225
  28. package/dist/commands/start.js.map +1 -1
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +13 -2
  31. package/dist/config.js.map +1 -1
  32. package/dist/context-window.js +1 -1
  33. package/dist/context-window.js.map +1 -1
  34. package/dist/import-parser-core.d.ts.map +1 -1
  35. package/dist/import-parser-core.js +74 -8
  36. package/dist/import-parser-core.js.map +1 -1
  37. package/dist/local-data.d.ts +59 -6
  38. package/dist/local-data.d.ts.map +1 -1
  39. package/dist/local-data.js +544 -62
  40. package/dist/local-data.js.map +1 -1
  41. package/dist/local-db.d.ts.map +1 -1
  42. package/dist/local-db.js +21 -1
  43. package/dist/local-db.js.map +1 -1
  44. package/dist/local-funnel.d.ts +0 -7
  45. package/dist/local-funnel.d.ts.map +1 -1
  46. package/dist/local-funnel.js +19 -28
  47. package/dist/local-funnel.js.map +1 -1
  48. package/dist/local-import-worker.d.ts.map +1 -1
  49. package/dist/local-import-worker.js +49 -4
  50. package/dist/local-import-worker.js.map +1 -1
  51. package/dist/local-memory-search.d.ts.map +1 -1
  52. package/dist/local-memory-search.js +6 -3
  53. package/dist/local-memory-search.js.map +1 -1
  54. package/dist/local-server.d.ts +37 -0
  55. package/dist/local-server.d.ts.map +1 -1
  56. package/dist/local-server.js +731 -227
  57. package/dist/local-server.js.map +1 -1
  58. package/dist/mcp/local-memory-server.d.ts +1 -1
  59. package/dist/mcp/local-memory-server.d.ts.map +1 -1
  60. package/dist/mcp/local-memory-server.js +23 -11
  61. package/dist/mcp/local-memory-server.js.map +1 -1
  62. package/dist/mcp/manager.d.ts.map +1 -1
  63. package/dist/mcp/manager.js +86 -18
  64. package/dist/mcp/manager.js.map +1 -1
  65. package/dist/message-loop.d.ts +5 -10
  66. package/dist/message-loop.d.ts.map +1 -1
  67. package/dist/message-loop.js +393 -289
  68. package/dist/message-loop.js.map +1 -1
  69. package/dist/mqtt-client.d.ts +4 -1
  70. package/dist/mqtt-client.d.ts.map +1 -1
  71. package/dist/mqtt-client.js.map +1 -1
  72. package/dist/orchestration/orchestrator-blocked-prompt.d.ts +2 -1
  73. package/dist/orchestration/orchestrator-blocked-prompt.d.ts.map +1 -1
  74. package/dist/orchestration/orchestrator-blocked-prompt.js +11 -0
  75. package/dist/orchestration/orchestrator-blocked-prompt.js.map +1 -1
  76. package/dist/orchestration/orchestrator-final-response-prompt.d.ts +4 -1
  77. package/dist/orchestration/orchestrator-final-response-prompt.d.ts.map +1 -1
  78. package/dist/orchestration/orchestrator-final-response-prompt.js +8 -6
  79. package/dist/orchestration/orchestrator-final-response-prompt.js.map +1 -1
  80. package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
  81. package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
  82. package/dist/orchestration/worker-operating-prompt.js +39 -0
  83. package/dist/orchestration/worker-operating-prompt.js.map +1 -1
  84. package/dist/orchestrator.d.ts +13 -0
  85. package/dist/orchestrator.d.ts.map +1 -1
  86. package/dist/orchestrator.js +235 -133
  87. package/dist/orchestrator.js.map +1 -1
  88. package/dist/providers/anthropic.d.ts +0 -5
  89. package/dist/providers/anthropic.d.ts.map +1 -1
  90. package/dist/providers/anthropic.js +29 -75
  91. package/dist/providers/anthropic.js.map +1 -1
  92. package/dist/providers/claude-cli.d.ts.map +1 -1
  93. package/dist/providers/claude-cli.js +16 -140
  94. package/dist/providers/claude-cli.js.map +1 -1
  95. package/dist/providers/codex-cli.d.ts.map +1 -1
  96. package/dist/providers/codex-cli.js +78 -59
  97. package/dist/providers/codex-cli.js.map +1 -1
  98. package/dist/providers/google.d.ts.map +1 -1
  99. package/dist/providers/google.js +4 -2
  100. package/dist/providers/google.js.map +1 -1
  101. package/dist/providers/index.d.ts +2 -0
  102. package/dist/providers/index.d.ts.map +1 -1
  103. package/dist/providers/index.js.map +1 -1
  104. package/dist/providers/openai.d.ts.map +1 -1
  105. package/dist/providers/openai.js +27 -2
  106. package/dist/providers/openai.js.map +1 -1
  107. package/dist/storage-mode.d.ts +5 -0
  108. package/dist/storage-mode.d.ts.map +1 -0
  109. package/dist/storage-mode.js +21 -0
  110. package/dist/storage-mode.js.map +1 -0
  111. package/dist/summarization-pipeline.d.ts +9 -0
  112. package/dist/summarization-pipeline.d.ts.map +1 -1
  113. package/dist/summarization-pipeline.js +61 -17
  114. package/dist/summarization-pipeline.js.map +1 -1
  115. package/dist/tools/analyze-image.js +2 -2
  116. package/dist/tools/analyze-image.js.map +1 -1
  117. package/dist/tools/edit-file.js +3 -3
  118. package/dist/tools/edit-file.js.map +1 -1
  119. package/dist/tools/index.d.ts.map +1 -1
  120. package/dist/tools/index.js +36 -0
  121. package/dist/tools/index.js.map +1 -1
  122. package/dist/tools/list-directory.js +7 -4
  123. package/dist/tools/list-directory.js.map +1 -1
  124. package/dist/tools/read-file.js +3 -3
  125. package/dist/tools/read-file.js.map +1 -1
  126. package/dist/tools/run-command.js +3 -3
  127. package/dist/tools/run-command.js.map +1 -1
  128. package/dist/tools/sandbox.d.ts +10 -5
  129. package/dist/tools/sandbox.d.ts.map +1 -1
  130. package/dist/tools/sandbox.js +41 -13
  131. package/dist/tools/sandbox.js.map +1 -1
  132. package/dist/tools/search-codebase.js +2 -2
  133. package/dist/tools/search-codebase.js.map +1 -1
  134. package/dist/tools/search-local-memory.d.ts.map +1 -1
  135. package/dist/tools/search-local-memory.js +19 -8
  136. package/dist/tools/search-local-memory.js.map +1 -1
  137. package/dist/tools/spawn-subagent.js.map +1 -1
  138. package/dist/tools/write-file.js +3 -3
  139. package/dist/tools/write-file.js.map +1 -1
  140. package/dist/types.d.ts +2 -0
  141. package/dist/types.d.ts.map +1 -1
  142. package/dist/verification/index.js +2 -2
  143. package/dist/verification/index.js.map +1 -1
  144. package/dist/wizard-state.d.ts.map +1 -1
  145. package/dist/wizard-state.js +16 -2
  146. package/dist/wizard-state.js.map +1 -1
  147. package/dist/wizard-support.d.ts +2 -2
  148. package/dist/wizard-support.d.ts.map +1 -1
  149. package/dist/wizard-support.js +80 -93
  150. package/dist/wizard-support.js.map +1 -1
  151. package/dist/workflow-engine.d.ts +3 -1
  152. package/dist/workflow-engine.d.ts.map +1 -1
  153. package/dist/workflow-engine.js +131 -12
  154. package/dist/workflow-engine.js.map +1 -1
  155. package/package.json +1 -1
@@ -48,16 +48,14 @@ 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
50
  const clerk_model_1 = require("./clerk-model");
51
- const local_funnel_1 = require("./local-funnel");
52
51
  const auto_organizer_1 = require("./auto-organizer");
53
52
  const orchestrator_profile_1 = require("./orchestrator-profile");
54
53
  const response_guard_1 = require("./response-guard");
55
54
  const context_compressor_1 = require("./context-compressor");
56
55
  const crypto = __importStar(require("crypto"));
57
56
  const data = __importStar(require("./local-data"));
58
- const agent_config_1 = require("./agent-config");
59
- const subscription_runtime_1 = require("./auth/subscription-runtime");
60
57
  const prompt_template_1 = require("./prompt-template");
58
+ const local_funnel_1 = require("./local-funnel");
61
59
  /** Determine priority from an AgentCommand */
62
60
  function getCommandPriority(command) {
63
61
  if (command.priority)
@@ -72,11 +70,53 @@ function getCommandPriority(command) {
72
70
  return 'medium';
73
71
  }
74
72
  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
+ }
75
114
  class MessageLoop {
76
115
  options;
77
116
  llmProvider;
78
117
  activeCommandId = null;
79
118
  activeCommandSource = null;
119
+ chunkSeq = 0;
80
120
  toolContext;
81
121
  toolDefinitions;
82
122
  builtinToolDefinitions;
@@ -97,34 +137,31 @@ class MessageLoop {
97
137
  static SCHEDULED_TASK_TIMEOUT_MS = 5 * 60 * 1000;
98
138
  scheduledTaskTimer = null;
99
139
  static IDLE_THRESHOLD_MS = 15 * 60 * 1000; // 15 minutes
100
- static SERVER_SYNC_RETRY_MAX = 1;
101
140
  constructor(options) {
102
141
  this.options = options;
103
- // DO NOT remap Claude CLI OAuth sessions to the public Anthropic runtime
104
- // without explicit user approval.
105
- const effectiveKey = options.oauthToken || options.apiKey || '';
142
+ const effectiveKey = options.apiKey || '';
106
143
  const effectiveProvider = options.provider;
107
- const isOAuthToken = effectiveKey.startsWith('sk-ant-oat01-');
144
+ const llmAuthMode = options.authMode || (effectiveKey.startsWith('sk-ant-oat') ? 'oauth-bearer' : 'api-key');
108
145
  this.llmProvider = (0, index_1.createProvider)(effectiveProvider, {
109
146
  apiKey: effectiveKey,
110
147
  model: options.model,
111
- ...(isOAuthToken ? { authMode: 'oauth-bearer' } : {}),
148
+ authMode: llmAuthMode,
149
+ ...(options.baseUrl ? { baseUrl: options.baseUrl } : {}),
150
+ ...(options.apiStyle ? { apiStyle: options.apiStyle } : {}),
112
151
  });
113
- // Async: attempt to resolve Anthropic subscription auth (create_api_key exchange)
114
- if (isOAuthToken && effectiveProvider === 'anthropic') {
115
- this.resolveAndUpgradeAuth(effectiveKey, options.model);
116
- }
117
152
  // Resolve Funolio API credentials for bot status reporting
118
153
  const cfg = (0, config_1.loadConfig)();
119
154
  const funoliApiKey = cfg.auth?.token || process.env.FUNOLIO_API_KEY || '';
120
155
  const funoliApiBaseUrl = config_1.FUNOLIO_API_URL;
156
+ const unrestrictedCliProvider = index_1.CLI_PROVIDERS.has(effectiveProvider);
121
157
  this.toolContext = (0, index_2.createToolContext)(options.projectDir, {
122
158
  actorType: 'llm',
123
159
  actorId: options.agentName || options.userId,
124
160
  llmProvider: effectiveProvider,
125
161
  llmModel: options.model,
126
162
  llmApiKey: effectiveKey,
127
- llmAuthMode: isOAuthToken ? 'oauth-bearer' : 'api-key',
163
+ llmAuthMode,
164
+ restrictFileAccessToProject: unrestrictedCliProvider ? false : undefined,
128
165
  apiKey: funoliApiKey,
129
166
  apiBaseUrl: funoliApiBaseUrl,
130
167
  botName: options.agentName || 'funolio-agent',
@@ -132,7 +169,7 @@ class MessageLoop {
132
169
  let allTools = (0, index_2.getAllToolDefinitions)(options.mcpManager);
133
170
  let builtinTools = (0, index_2.getToolDefinitions)();
134
171
  // Filter tools by enabledTools whitelist if specified
135
- if (options.enabledTools && options.enabledTools.length > 0) {
172
+ if (!unrestrictedCliProvider && options.enabledTools && options.enabledTools.length > 0) {
136
173
  const allowed = new Set(options.enabledTools);
137
174
  allTools = allTools.filter((t) => allowed.has(t.name));
138
175
  builtinTools = builtinTools.filter((t) => allowed.has(t.name));
@@ -161,147 +198,6 @@ class MessageLoop {
161
198
  // Session idle detection timer (checks every 60s)
162
199
  this.idleTimer = setInterval(() => this.checkIdleSession(), 60_000);
163
200
  }
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
- }
305
201
  /** Check if a session has gone idle and trigger auto-organize */
306
202
  async checkIdleSession() {
307
203
  if (!this.lastMessageAt || !(0, auto_organizer_1.isAutoOrganizeEnabled)())
@@ -419,48 +315,19 @@ class MessageLoop {
419
315
  const overrideModel = command.model?.model;
420
316
  let provider = overrideProvider || this.options.provider;
421
317
  const model = overrideModel || this.options.model;
422
- const apiKey = this.options.oauthToken || this.options.apiKey;
318
+ const apiKey = this.options.apiKey;
423
319
  // Only create a new provider if provider or model actually changed (ignore apiKey overrides from server)
424
320
  const hasOverride = (overrideProvider && overrideProvider !== this.options.provider) ||
425
321
  (overrideModel && overrideModel !== this.options.model);
426
322
  let llm;
427
323
  if (hasOverride) {
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
- }
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
+ });
464
331
  }
465
332
  else {
466
333
  llm = this.llmProvider;
@@ -478,6 +345,7 @@ class MessageLoop {
478
345
  }
479
346
  this.activeCommandId = command.id;
480
347
  this.activeCommandSource = command.source || 'user';
348
+ this.chunkSeq = 0;
481
349
  this.processing = true;
482
350
  this.lastMessageAt = Date.now();
483
351
  const commandStartMs = Date.now();
@@ -545,6 +413,8 @@ class MessageLoop {
545
413
  ...this.toolContext,
546
414
  projectId: effectiveProjectId ?? null,
547
415
  abortSignal: commandAbortController.signal,
416
+ // Use project folder as working directory if available
417
+ ...(effectiveProject?.folder ? { projectDir: effectiveProject.folder } : {}),
548
418
  };
549
419
  // ─── Orchestrator Mode Branch ─────────────────────────
550
420
  // Resolve the selected bot profile — from command.bot, conversation bot_id, or default
@@ -583,27 +453,33 @@ class MessageLoop {
583
453
  await this.options.mqttClient.publishResult({
584
454
  commandId: command.id,
585
455
  type: 'chunk',
456
+ seq: this.chunkSeq++,
586
457
  content: status + '\n',
587
458
  timestamp: Date.now(),
588
459
  });
589
460
  },
590
461
  mqttPublish: (result) => this.options.mqttClient.publishResult(result),
591
462
  });
592
- // Save O's response to local DB
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
593
468
  if (localConvId && localData && activeProfile) {
594
- localData.addMessage(localConvId, 'assistant', response, undefined, undefined, activeProfile.id, 'Project Manager');
469
+ localData.addMessage(localConvId, 'assistant', response, finalModelLabel, undefined, finalBotId, finalAgentName);
595
470
  }
596
471
  // Publish final response and complete
597
472
  await this.options.mqttClient.publishResult({
598
473
  commandId: command.id,
599
474
  type: 'chunk',
475
+ seq: this.chunkSeq++,
600
476
  content: response,
601
477
  timestamp: Date.now(),
602
478
  });
603
479
  await this.options.mqttClient.publishResult({
604
480
  commandId: command.id,
605
481
  type: 'complete',
606
- content: '',
482
+ content: response || '',
607
483
  timestamp: Date.now(),
608
484
  });
609
485
  }
@@ -648,7 +524,7 @@ class MessageLoop {
648
524
  includeKeyDecisions: false,
649
525
  });
650
526
  systemPrompt = built.systemPrompt;
651
- console.log(chalk_1.default.gray(` [clerk] Context: ${built.injectedFacts} facts, ${built.injectedDecisions} decisions, ${built.injectedSummaries} summaries (${built.contextTokensUsed} tokens)`));
527
+ console.log(chalk_1.default.gray(` [clerk] Context: ${built.injectedSummaries} summaries (${built.contextTokensUsed} tokens)`));
652
528
  }
653
529
  else {
654
530
  systemPrompt = this.buildFallbackSystemPrompt(command);
@@ -667,12 +543,12 @@ class MessageLoop {
667
543
  systemPrompt += deadServerNote;
668
544
  }
669
545
  systemPrompt += `\n\nCONTEXT PRIORITY RULES:
670
- 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.
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.
671
547
  2. Do NOT browse local files or run shell commands unless:
672
548
  a. The provided context does not contain the answer, OR
673
549
  b. The user explicitly asks you to look at local files or run a command, OR
674
550
  c. The user asks about current runtime state (logs, running processes, file contents right now)
675
- 3. When you use provided context, briefly cite the source (e.g. "Based on your stored decisions..." or "From your conversation history...").
551
+ 3. When you use provided context, briefly cite the source (e.g. "Based on your stored summary..." or "From your conversation history...").
676
552
  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.
677
553
 
678
554
  TOOL EFFICIENCY RULES:
@@ -680,7 +556,7 @@ TOOL EFFICIENCY RULES:
680
556
  - PREFER one broad command over many narrow ones (e.g. "find . -name '*.json' -exec grep 'error' {} +" instead of checking files one by one)
681
557
  - PLAN your approach before starting — outline what you need to find, then execute with minimal tool calls`;
682
558
  // Count context sections injected into system prompt
683
- const contextSectionCount = ['[Key Decisions]', '[Memory Facts]', '[Relevant Conversations]', '[Code References]', '[Relevant Files]', '[Bot Memory]', '[Retrieved Context]']
559
+ const contextSectionCount = ['[Relevant Conversations]', '[Code References]', '[Relevant Files]', '[Bot Memory]', '[Retrieved Context]', '[Summaries]']
684
560
  .filter(header => systemPrompt.includes(header)).length;
685
561
  // Add the current prompt (with image attachments if present)
686
562
  if (command.attachments && command.attachments.length > 0) {
@@ -717,15 +593,40 @@ TOOL EFFICIENCY RULES:
717
593
  const str = JSON.stringify(args || {}).slice(0, 200);
718
594
  return crypto.createHash('md5').update(str).digest('hex').slice(0, 12);
719
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
+ }
720
602
  function trackToolFailure(name, args) {
721
- const key = `${name}:${getArgsHash(args)}`;
603
+ const key = `${getRetryKey(name, args)}:${getArgsHash(args)}`;
604
+ const retryKey = getRetryKey(name, args);
722
605
  toolFailuresByKey.set(key, (toolFailuresByKey.get(key) || 0) + 1);
723
- toolFailuresByName.set(name, (toolFailuresByName.get(name) || 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;
724
624
  }
725
625
  function checkRetryBudget(name, args) {
726
- const key = `${name}:${getArgsHash(args)}`;
626
+ const key = `${getRetryKey(name, args)}:${getArgsHash(args)}`;
627
+ const retryKey = getRetryKey(name, args);
727
628
  const keyCount = toolFailuresByKey.get(key) || 0;
728
- const nameCount = toolFailuresByName.get(name) || 0;
629
+ const nameCount = toolFailuresByName.get(retryKey) || 0;
729
630
  if (keyCount >= 2) {
730
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.`;
731
632
  }
@@ -736,15 +637,20 @@ TOOL EFFICIENCY RULES:
736
637
  }
737
638
  // Agentic loop - keep calling LLM until no more tool calls
738
639
  let iteration = 0;
739
- const MAX_ITERATIONS = 100;
740
- const GLOBAL_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes wall-clock timeout
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();
741
648
  let totalTokensUsed = 0;
742
649
  let totalInputTokens = 0;
743
650
  let totalOutputTokens = 0;
744
651
  let totalCacheReadTokens = 0;
745
652
  let totalCacheCreationTokens = 0;
746
653
  let totalToolCallDurationMs = 0;
747
- let serverSyncRetries = 0;
748
654
  const deniedTools = new Set(); // Track tools denied by user — don't re-ask
749
655
  const commandStartMs = Date.now();
750
656
  // Publish prompt context for "View Prompt" panel
@@ -765,16 +671,93 @@ TOOL EFFICIENCY RULES:
765
671
  catch { }
766
672
  while (iteration < MAX_ITERATIONS && this.activeCommandId === command.id) {
767
673
  iteration++;
768
- // Global wall-clock timeout check
769
- if (Date.now() - commandStartMs > GLOBAL_TIMEOUT_MS) {
770
- console.log(chalk_1.default.red(` ⏱ Global timeout exceeded (${GLOBAL_TIMEOUT_MS / 1000}s) — breaking out of loop`));
674
+ // Emit progress every 60 seconds
675
+ if (Date.now() - lastProgressMs >= 60_000) {
676
+ lastProgressMs = Date.now();
771
677
  await this.options.mqttClient.publishResult({
772
678
  commandId: command.id,
773
- type: 'error',
774
- error: `Command execution exceeded the ${GLOBAL_TIMEOUT_MS / 60000} minute global timeout limit.`,
679
+ type: 'progress',
680
+ elapsed: Math.round((Date.now() - commandStartMs) / 1000),
681
+ iteration,
682
+ toolsUsed: totalToolCalls,
775
683
  timestamp: Date.now(),
776
684
  });
777
- commandAbortController.abort();
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
+ }
778
761
  break;
779
762
  }
780
763
  // Send a separator between agentic loop iterations so streamed text
@@ -783,41 +766,24 @@ TOOL EFFICIENCY RULES:
783
766
  await this.options.mqttClient.publishResult({
784
767
  commandId: command.id,
785
768
  type: 'chunk',
769
+ seq: this.chunkSeq++,
786
770
  content: '\n\n',
787
771
  timestamp: Date.now(),
788
772
  });
789
773
  }
790
- // Refresh OAuth token before each LLM call if needed
774
+ // API keys don't expire ensureFreshToken is a no-op for api-key source
791
775
  if (this.resolvedAuth) {
792
776
  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
- }
810
777
  if (refreshed.apiKey !== this.resolvedAuth.apiKey) {
811
778
  this.resolvedAuth = refreshed;
812
779
  this.updateToken(refreshed.apiKey);
813
780
  }
814
781
  }
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.
818
782
  // Smart tool filtering: only send relevant tools to reduce token usage
819
- const filteredTools = (0, tool_filter_1.filterToolsForMessage)(this.toolDefinitions, this.builtinToolDefinitions, command.prompt);
820
- if (filteredTools.length < this.toolDefinitions.length) {
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) {
821
787
  console.log(chalk_1.default.gray(` [tool-filter] ${filteredTools.length}/${this.toolDefinitions.length} tools selected`));
822
788
  }
823
789
  // Compress context before sending to LLM (Optimizations 3+4)
@@ -838,6 +804,7 @@ TOOL EFFICIENCY RULES:
838
804
  await this.options.mqttClient.publishResult({
839
805
  commandId: command.id,
840
806
  type: 'chunk',
807
+ seq: this.chunkSeq++,
841
808
  content: chunk,
842
809
  timestamp: Date.now(),
843
810
  });
@@ -848,26 +815,19 @@ TOOL EFFICIENCY RULES:
848
815
  const msg = llmError?.message || String(llmError);
849
816
  const isAuthError = /401|403|unauthorized|forbidden|authentication/i.test(msg);
850
817
  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
- }
858
818
  console.error(chalk_1.default.red(` [auth] LLM API authentication failed: ${msg}`));
859
819
  await this.options.mqttClient.publishResult({
860
820
  commandId: command.id,
861
821
  type: 'error',
862
- error: 'API authentication failed. Your token may have expired run `funolio-agent login` to refresh.',
822
+ error: 'API authentication failed. Check your API key or run `funolio-agent configure` to update it.',
863
823
  timestamp: Date.now(),
864
824
  });
865
825
  break;
866
826
  }
867
827
  throw llmError;
868
828
  }
869
- // Any successful provider response clears one-shot sync retry budget
870
- serverSyncRetries = 0;
829
+ // Reset inactivity timer after LLM response
830
+ lastActivityMs = Date.now();
871
831
  // Track token usage across iterations
872
832
  if (response.usage) {
873
833
  totalTokensUsed += response.usage.inputTokens + response.usage.outputTokens;
@@ -876,6 +836,60 @@ TOOL EFFICIENCY RULES:
876
836
  totalCacheReadTokens += response.usage.cacheReadTokens || 0;
877
837
  totalCacheCreationTokens += response.usage.cacheCreationTokens || 0;
878
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
+ }
879
893
  // If there are tool calls, publish them and continue the loop
880
894
  if (response.toolCalls && response.toolCalls.length > 0) {
881
895
  // Add assistant message with tool calls to history
@@ -887,9 +901,13 @@ TOOL EFFICIENCY RULES:
887
901
  for (const toolCall of response.toolCalls) {
888
902
  totalToolCalls++;
889
903
  console.log(chalk_1.default.cyan(` 🔧 Tool: ${toolCall.name}(${JSON.stringify(toolCall.arguments).slice(0, 60)}...)`));
904
+ const toolSummary = (0, approval_1.generateToolSummary)(toolCall.name, toolCall.arguments);
905
+ const toolLocation = extractToolLocation(toolCall.name, toolCall.arguments, commandToolContext.projectDir);
890
906
  // Check permission before execution
891
- const permMode = this.options.permissionMode || 'autopilot';
892
- let approval = (0, approval_1.checkPermission)(toolCall.name, permMode, this.options.enabledTools);
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);
893
911
  // If tool was already denied once in this command, auto-deny without re-asking
894
912
  if (!approval.approved && deniedTools.has(toolCall.name)) {
895
913
  approval = { approved: false, reason: `Tool "${toolCall.name}" was already denied in this session.` };
@@ -897,20 +915,29 @@ TOOL EFFICIENCY RULES:
897
915
  // If not auto-approved, request interactive approval via MQTT
898
916
  if (!approval.approved && permMode !== 'autopilot' && !deniedTools.has(toolCall.name)) {
899
917
  console.log(chalk_1.default.yellow(` 🔐 Requesting user approval for: ${toolCall.name}`));
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,
918
+ approval = await this.approvalManager.requestApproval(command.id, toolCall.name, toolCall.arguments, {
919
+ onRequest: async (request) => {
920
+ await this.options.mqttClient.publishResult({
921
+ commandId: command.id,
922
+ type: 'approval_request',
923
+ requestId: request.requestId,
924
+ riskLevel: request.riskLevel,
925
+ summary: request.summary,
926
+ toolCall: {
927
+ id: toolCall.id,
928
+ name: toolCall.name,
929
+ arguments: toolCall.arguments,
930
+ classification: classifyToolName(toolCall.name),
931
+ summary: toolSummary,
932
+ path: toolLocation.path,
933
+ cwd: toolLocation.cwd,
934
+ url: toolLocation.url,
935
+ importance: request.riskLevel === 'destructive' ? 'high' : 'medium',
936
+ },
937
+ timestamp: Date.now(),
938
+ });
910
939
  },
911
- timestamp: Date.now(),
912
940
  });
913
- approval = await this.approvalManager.requestApproval(command.id, toolCall.name, toolCall.arguments);
914
941
  // If user chose "always allow", persist it
915
942
  if (approval.approved && approval.remember) {
916
943
  (0, approval_1.rememberTool)(toolCall.name);
@@ -920,19 +947,26 @@ TOOL EFFICIENCY RULES:
920
947
  if (!approval.approved) {
921
948
  deniedTools.add(toolCall.name);
922
949
  console.log(chalk_1.default.yellow(` ⚠ Tool denied: ${approval.reason}`));
950
+ const deniedOutput = `PERMISSION_DENIED: ${approval.reason}`;
923
951
  await this.options.mqttClient.publishResult({
924
952
  commandId: command.id,
925
953
  type: 'tool_result',
926
954
  toolResult: {
927
955
  callId: toolCall.id,
928
- output: `PERMISSION_DENIED: ${approval.reason}`,
956
+ output: deniedOutput,
929
957
  isError: true,
958
+ success: false,
959
+ summary: summarizeToolResult(deniedOutput, true),
960
+ path: toolLocation.path,
961
+ cwd: toolLocation.cwd,
962
+ url: toolLocation.url,
963
+ pathsTouched: toolLocation.path ? [toolLocation.path] : [],
930
964
  },
931
965
  timestamp: Date.now(),
932
966
  });
933
967
  messages.push({
934
968
  role: 'tool',
935
- content: `PERMISSION_DENIED: ${approval.reason}`,
969
+ content: deniedOutput,
936
970
  toolCallId: toolCall.id,
937
971
  toolName: toolCall.name,
938
972
  });
@@ -945,6 +979,12 @@ TOOL EFFICIENCY RULES:
945
979
  id: toolCall.id,
946
980
  name: toolCall.name,
947
981
  arguments: toolCall.arguments,
982
+ classification: classifyToolName(toolCall.name),
983
+ summary: toolSummary,
984
+ path: toolLocation.path,
985
+ cwd: toolLocation.cwd,
986
+ url: toolLocation.url,
987
+ importance: (0, approval_1.getRiskLevel)(toolCall.name) === 'destructive' ? 'high' : 'medium',
948
988
  },
949
989
  timestamp: Date.now(),
950
990
  });
@@ -977,9 +1017,16 @@ TOOL EFFICIENCY RULES:
977
1017
  const toolStartMs = Date.now();
978
1018
  const rawResult = await (0, tools_1.executeToolWithMCP)({ id: toolCall.id, name: toolCall.name, arguments: toolCall.arguments }, commandToolContext, this.options.mcpManager);
979
1019
  const toolResult = await (0, verification_1.verifyToolResult)(rawResult, toolCall.arguments, commandToolContext);
980
- const resultOutput = toolResult.success
1020
+ let resultOutput = toolResult.success
981
1021
  ? toolResult.output
982
1022
  : `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
+ }
983
1030
  // Track failures for retry budget (Optimization 2)
984
1031
  if (!toolResult.success) {
985
1032
  trackToolFailure(toolCall.name, toolCall.arguments);
@@ -987,6 +1034,7 @@ TOOL EFFICIENCY RULES:
987
1034
  console.log(chalk_1.default.gray(` Result: ${toolResult.success ? '✓' : '✗'} ${resultOutput.slice(0, 100)}${resultOutput.length > 100 ? '...' : ''}`));
988
1035
  const toolDurationMs = Date.now() - toolStartMs;
989
1036
  totalToolCallDurationMs += toolDurationMs;
1037
+ lastActivityMs = Date.now(); // Reset inactivity timer after tool completion
990
1038
  await this.options.mqttClient.publishResult({
991
1039
  commandId: command.id,
992
1040
  type: 'tool_result',
@@ -996,6 +1044,13 @@ TOOL EFFICIENCY RULES:
996
1044
  output: resultOutput,
997
1045
  isError: !toolResult.success,
998
1046
  durationMs: toolDurationMs,
1047
+ success: toolResult.success,
1048
+ summary: summarizeToolResult(resultOutput, !toolResult.success),
1049
+ path: toolLocation.path,
1050
+ cwd: toolLocation.cwd,
1051
+ url: toolLocation.url,
1052
+ pathsTouched: toolLocation.path ? [toolLocation.path] : [],
1053
+ exitCode: typeof toolResult.exit_code === 'number' ? toolResult.exit_code : undefined,
999
1054
  },
1000
1055
  timestamp: Date.now(),
1001
1056
  });
@@ -1006,6 +1061,22 @@ TOOL EFFICIENCY RULES:
1006
1061
  toolCallId: toolCall.id,
1007
1062
  toolName: toolCall.name,
1008
1063
  });
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
+ });
1009
1080
  }
1010
1081
  // --- Rate limit checks ---
1011
1082
  // Warn at 80% of tool call limit
@@ -1032,6 +1103,7 @@ TOOL EFFICIENCY RULES:
1032
1103
  await this.options.mqttClient.publishResult({
1033
1104
  commandId: command.id,
1034
1105
  type: 'chunk',
1106
+ seq: this.chunkSeq++,
1035
1107
  content: chunk,
1036
1108
  timestamp: Date.now(),
1037
1109
  });
@@ -1059,7 +1131,7 @@ TOOL EFFICIENCY RULES:
1059
1131
  await this.options.mqttClient.publishResult({
1060
1132
  commandId: command.id,
1061
1133
  type: 'complete',
1062
- content: '',
1134
+ content: response.content || '',
1063
1135
  timestamp: Date.now(),
1064
1136
  });
1065
1137
  console.log(chalk_1.default.green(` Command completed: ${iteration} iterations, ${totalToolCalls} tool calls, ~${totalTokensUsed} tokens`));
@@ -1072,9 +1144,9 @@ TOOL EFFICIENCY RULES:
1072
1144
  continue;
1073
1145
  }
1074
1146
  // No tool calls - final response.
1075
- // DEDUP FIX: Content was already streamed via onChunk callbacks above.
1076
- // The 'complete' message signals end-of-response; we send empty content
1077
- // to avoid the receiver concatenating streamed chunks + full content.
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.
1078
1150
  // Fix 1: Detect phantom tool use — LLM claims it did something but made 0 tool calls
1079
1151
  const phantom = (0, response_guard_1.detectPhantomToolUse)(response.content || '', totalToolCalls);
1080
1152
  if (phantom.detected) {
@@ -1083,6 +1155,7 @@ TOOL EFFICIENCY RULES:
1083
1155
  await this.options.mqttClient.publishResult({
1084
1156
  commandId: command.id,
1085
1157
  type: 'chunk',
1158
+ seq: this.chunkSeq++,
1086
1159
  content: '\n\n---\n⚠️ ' + phantom.correction,
1087
1160
  timestamp: Date.now(),
1088
1161
  });
@@ -1108,7 +1181,7 @@ TOOL EFFICIENCY RULES:
1108
1181
  await this.options.mqttClient.publishResult({
1109
1182
  commandId: command.id,
1110
1183
  type: 'complete',
1111
- content: '',
1184
+ content: response.content || '',
1112
1185
  timestamp: Date.now(),
1113
1186
  });
1114
1187
  // Local-first: save assistant response to SQLite + trigger funnel
@@ -1116,7 +1189,6 @@ TOOL EFFICIENCY RULES:
1116
1189
  try {
1117
1190
  const localData = await Promise.resolve().then(() => __importStar(require('./local-data')));
1118
1191
  localData.addMessage(localConvId, 'assistant', response.content || '', model);
1119
- // Trigger background memory extraction
1120
1192
  (0, local_funnel_1.scheduleFunnelProcessing)(localConvId);
1121
1193
  // Track for session idle auto-organize
1122
1194
  this.lastOrganizedConvId = localConvId;
@@ -1137,7 +1209,22 @@ TOOL EFFICIENCY RULES:
1137
1209
  }
1138
1210
  }
1139
1211
  catch (error) {
1140
- console.error(chalk_1.default.red(` ✗ Error: ${error.message}`));
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)));
1141
1228
  await this.publishError(command.id, error.message);
1142
1229
  }
1143
1230
  finally {
@@ -1183,7 +1270,8 @@ TOOL EFFICIENCY RULES:
1183
1270
  - If a matching MCP tool server exists, use \`request_mcp_install\` to request permission to install it.
1184
1271
  - You can discover and gain new capabilities on-the-fly through the marketplace.
1185
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.`;
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.';
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.';
1274
+ 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.';
1187
1275
  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.';
1188
1276
  systemPrompt += '\n\n' + (0, clerk_model_1.buildTodoInstructions)(this.options.agentName || 'LLM');
1189
1277
  // Inject model self-switch info
@@ -1205,13 +1293,7 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
1205
1293
  systemPrompt += '\n\n[Bot Memory]\n' + command.context.memoryMd;
1206
1294
  }
1207
1295
  const sc = command.context?.structuredContext;
1208
- if (sc && (sc.decisions?.length || sc.memoryFacts?.length || sc.codeRefs?.length || sc.conversations?.length)) {
1209
- if (sc.decisions?.length) {
1210
- systemPrompt += '\n\n[Key Decisions]\n' + sc.decisions.map(d => `- ${d}`).join('\n');
1211
- }
1212
- if (sc.memoryFacts?.length) {
1213
- systemPrompt += '\n\n[Memory Facts]\n' + sc.memoryFacts.map(f => `- ${f}`).join('\n');
1214
- }
1296
+ if (sc && (sc.codeRefs?.length || sc.conversations?.length)) {
1215
1297
  if (sc.conversations?.length) {
1216
1298
  systemPrompt += '\n\n[Relevant Conversations]\n' + sc.conversations.map(c => `- ${c}`).join('\n');
1217
1299
  }
@@ -1219,15 +1301,38 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
1219
1301
  systemPrompt += '\n\n[Code References]\n' + sc.codeRefs.map(r => `- ${r}`).join('\n');
1220
1302
  }
1221
1303
  }
1222
- else if (command.context?.facts?.length) {
1223
- systemPrompt += '\n\nRelevant memory/facts from the user\'s stored conversations:\n' + command.context.facts.map(f => `- ${f}`).join('\n');
1224
- }
1225
1304
  else {
1226
- 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.';
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.';
1227
1306
  }
1228
1307
  if (command.context?.files?.length) {
1229
1308
  systemPrompt += '\n\nRelevant files: ' + command.context.files.join(', ');
1230
1309
  }
1310
+ if (command.context?.stateContext?.summaryText) {
1311
+ systemPrompt += '\n\n[Recent Operational State]\n' + command.context.stateContext.summaryText;
1312
+ }
1313
+ if (command.context?.workspaceManifest) {
1314
+ const manifestLines = [];
1315
+ const manifest = command.context.workspaceManifest;
1316
+ if (manifest.projectRoot)
1317
+ manifestLines.push(`- Project root: ${manifest.projectRoot}`);
1318
+ if (manifest.likelyWorkingDirectory)
1319
+ manifestLines.push(`- Likely working directory: ${manifest.likelyWorkingDirectory}`);
1320
+ if (manifest.recentlyReadFiles?.length)
1321
+ manifestLines.push(`- Recently read files: ${manifest.recentlyReadFiles.join(', ')}`);
1322
+ if (manifest.recentlyWrittenFiles?.length)
1323
+ manifestLines.push(`- Recently written files: ${manifest.recentlyWrittenFiles.join(', ')}`);
1324
+ if (manifest.recentlyScannedDirectories?.length)
1325
+ manifestLines.push(`- Recently scanned directories: ${manifest.recentlyScannedDirectories.join(', ')}`);
1326
+ if (manifest.recentDownloads?.length)
1327
+ manifestLines.push(`- Recent downloads: ${manifest.recentDownloads.join(', ')}`);
1328
+ if (manifest.recentFailures?.length)
1329
+ manifestLines.push(`- Recent failures: ${manifest.recentFailures.join(' | ')}`);
1330
+ if (manifest.startHereHints?.length)
1331
+ manifestLines.push(`- Start here: ${manifest.startHereHints.join(', ')}`);
1332
+ if (manifestLines.length) {
1333
+ systemPrompt += '\n\n[Workspace Manifest]\n' + manifestLines.join('\n');
1334
+ }
1335
+ }
1231
1336
  return systemPrompt;
1232
1337
  }
1233
1338
  async publishError(commandId, error) {
@@ -1240,19 +1345,18 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
1240
1345
  timestamp: Date.now(),
1241
1346
  });
1242
1347
  }
1243
- /** Update the OAuth token used for requests (called by token refresh) */
1348
+ /** Update the API key used for requests */
1244
1349
  updateToken(token) {
1245
- this.options.oauthToken = token;
1246
- // Recreate the default provider with the new token
1350
+ this.options.apiKey = token;
1247
1351
  const effectiveProvider = this.options.provider;
1352
+ const authMode = this.options.authMode || (token.startsWith('sk-ant-oat') ? 'oauth-bearer' : 'api-key');
1248
1353
  this.llmProvider = (0, index_1.createProvider)(effectiveProvider, {
1249
1354
  apiKey: token,
1250
1355
  model: this.options.model,
1251
- ...(token.startsWith('sk-ant-oat01-') ? { authMode: 'oauth-bearer' } : {}),
1356
+ authMode,
1357
+ ...(this.options.baseUrl ? { baseUrl: this.options.baseUrl } : {}),
1358
+ ...(this.options.apiStyle ? { apiStyle: this.options.apiStyle } : {}),
1252
1359
  });
1253
- if (token.startsWith('sk-ant-oat01-') && effectiveProvider === 'anthropic') {
1254
- this.resolveAndUpgradeAuth(token, this.options.model);
1255
- }
1256
1360
  }
1257
1361
  }
1258
1362
  exports.MessageLoop = MessageLoop;