funolio-agent 1.0.52 → 1.0.75

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 (115) hide show
  1. package/dist/approval.d.ts +1 -6
  2. package/dist/approval.d.ts.map +1 -1
  3. package/dist/approval.js +2 -7
  4. package/dist/approval.js.map +1 -1
  5. package/dist/bot-manager.d.ts +5 -1
  6. package/dist/bot-manager.d.ts.map +1 -1
  7. package/dist/bot-manager.js +23 -13
  8. package/dist/bot-manager.js.map +1 -1
  9. package/dist/cli-session-epoch.d.ts +1 -1
  10. package/dist/cli-session-epoch.d.ts.map +1 -1
  11. package/dist/cli-session-epoch.js +1 -1
  12. package/dist/cli-session-epoch.js.map +1 -1
  13. package/dist/cli-session-registry.d.ts +35 -0
  14. package/dist/cli-session-registry.d.ts.map +1 -0
  15. package/dist/cli-session-registry.js +177 -0
  16. package/dist/cli-session-registry.js.map +1 -0
  17. package/dist/cli.js +62 -0
  18. package/dist/cli.js.map +1 -1
  19. package/dist/codex-app-server-manager.d.ts +129 -0
  20. package/dist/codex-app-server-manager.d.ts.map +1 -0
  21. package/dist/codex-app-server-manager.js +768 -0
  22. package/dist/codex-app-server-manager.js.map +1 -0
  23. package/dist/commands/init.d.ts.map +1 -1
  24. package/dist/commands/init.js +8 -30
  25. package/dist/commands/init.js.map +1 -1
  26. package/dist/commands/setup.d.ts +4 -1
  27. package/dist/commands/setup.d.ts.map +1 -1
  28. package/dist/commands/setup.js +9 -25
  29. package/dist/commands/setup.js.map +1 -1
  30. package/dist/commands/start.d.ts.map +1 -1
  31. package/dist/commands/start.js +77 -2
  32. package/dist/commands/start.js.map +1 -1
  33. package/dist/completion-marker.d.ts +7 -0
  34. package/dist/completion-marker.d.ts.map +1 -0
  35. package/dist/completion-marker.js +28 -0
  36. package/dist/completion-marker.js.map +1 -0
  37. package/dist/config.d.ts +6 -2
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/config.js +15 -3
  40. package/dist/config.js.map +1 -1
  41. package/dist/context-window.d.ts.map +1 -1
  42. package/dist/context-window.js +8 -1
  43. package/dist/context-window.js.map +1 -1
  44. package/dist/live-activity.d.ts +29 -0
  45. package/dist/live-activity.d.ts.map +1 -0
  46. package/dist/live-activity.js +36 -0
  47. package/dist/live-activity.js.map +1 -0
  48. package/dist/local-cli-pty-manager.d.ts +51 -0
  49. package/dist/local-cli-pty-manager.d.ts.map +1 -1
  50. package/dist/local-cli-pty-manager.js +1227 -114
  51. package/dist/local-cli-pty-manager.js.map +1 -1
  52. package/dist/local-data.d.ts +41 -0
  53. package/dist/local-data.d.ts.map +1 -1
  54. package/dist/local-data.js +140 -4
  55. package/dist/local-data.js.map +1 -1
  56. package/dist/local-db.d.ts.map +1 -1
  57. package/dist/local-db.js +55 -1
  58. package/dist/local-db.js.map +1 -1
  59. package/dist/local-server.d.ts +25 -0
  60. package/dist/local-server.d.ts.map +1 -1
  61. package/dist/local-server.js +528 -267
  62. package/dist/local-server.js.map +1 -1
  63. package/dist/message-loop.d.ts +6 -0
  64. package/dist/message-loop.d.ts.map +1 -1
  65. package/dist/message-loop.js +247 -89
  66. package/dist/message-loop.js.map +1 -1
  67. package/dist/mqtt-client.d.ts +10 -1
  68. package/dist/mqtt-client.d.ts.map +1 -1
  69. package/dist/mqtt-client.js +14 -1
  70. package/dist/mqtt-client.js.map +1 -1
  71. package/dist/oauth.d.ts.map +1 -1
  72. package/dist/oauth.js +69 -29
  73. package/dist/oauth.js.map +1 -1
  74. package/dist/orchestration/orchestrator-operating-prompt.d.ts +1 -0
  75. package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
  76. package/dist/orchestration/orchestrator-operating-prompt.js +60 -0
  77. package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
  78. package/dist/orchestration/validation.d.ts +40 -0
  79. package/dist/orchestration/validation.d.ts.map +1 -0
  80. package/dist/orchestration/validation.js +203 -0
  81. package/dist/orchestration/validation.js.map +1 -0
  82. package/dist/orchestrator.d.ts +21 -32
  83. package/dist/orchestrator.d.ts.map +1 -1
  84. package/dist/orchestrator.js +287 -725
  85. package/dist/orchestrator.js.map +1 -1
  86. package/dist/providers/claude-cli-prompt.d.ts.map +1 -1
  87. package/dist/providers/claude-cli-prompt.js +49 -5
  88. package/dist/providers/claude-cli-prompt.js.map +1 -1
  89. package/dist/providers/claude-cli.d.ts.map +1 -1
  90. package/dist/providers/claude-cli.js +56 -5
  91. package/dist/providers/claude-cli.js.map +1 -1
  92. package/dist/providers/codex-cli.d.ts.map +1 -1
  93. package/dist/providers/codex-cli.js +15 -10
  94. package/dist/providers/codex-cli.js.map +1 -1
  95. package/dist/response-guard.js +1 -1
  96. package/dist/response-guard.js.map +1 -1
  97. package/dist/tools/admin-tools.d.ts.map +1 -1
  98. package/dist/tools/admin-tools.js +8 -2
  99. package/dist/tools/admin-tools.js.map +1 -1
  100. package/dist/tools/index.d.ts.map +1 -1
  101. package/dist/tools/index.js +2 -1
  102. package/dist/tools/index.js.map +1 -1
  103. package/dist/tools/search-conversation-history.d.ts +16 -0
  104. package/dist/tools/search-conversation-history.d.ts.map +1 -0
  105. package/dist/tools/search-conversation-history.js +324 -0
  106. package/dist/tools/search-conversation-history.js.map +1 -0
  107. package/dist/wizard-state.d.ts +7 -0
  108. package/dist/wizard-state.d.ts.map +1 -1
  109. package/dist/wizard-state.js +31 -2
  110. package/dist/wizard-state.js.map +1 -1
  111. package/dist/workflow-engine.d.ts +4 -1
  112. package/dist/workflow-engine.d.ts.map +1 -1
  113. package/dist/workflow-engine.js +190 -29
  114. package/dist/workflow-engine.js.map +1 -1
  115. package/package.json +1 -1
@@ -41,6 +41,10 @@ export declare class MessageLoop {
41
41
  /** Priority queue for commands when agent is busy */
42
42
  private commandQueue;
43
43
  private processing;
44
+ /** Idempotency guardrails */
45
+ private commandState;
46
+ private commandCompletedAt;
47
+ private static readonly COMMAND_DEDUPE_TTL_MS;
44
48
  /** Timeout for scheduled tasks (5 minutes) */
45
49
  private static readonly SCHEDULED_TASK_TIMEOUT_MS;
46
50
  private scheduledTaskTimer;
@@ -58,6 +62,8 @@ export declare class MessageLoop {
58
62
  /** Stop the idle detection timer */
59
63
  destroy(): void;
60
64
  isBusy(): boolean;
65
+ private pruneCommandDedupeCache;
66
+ private publishDuplicateIgnored;
61
67
  /**
62
68
  * Public entry point: enqueue or execute a command based on priority.
63
69
  * High-priority (user) commands execute immediately.
@@ -1 +1 @@
1
- {"version":3,"file":"message-loop.d.ts","sourceRoot":"","sources":["../src/message-loop.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,YAAY,EAAe,MAAM,eAAe,CAAC;AAM3E,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,EAAmB,cAAc,EAAqF,MAAM,YAAY,CAAC;AAGhJ,OAAO,EAAoB,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAQpE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,eAAe,CAAC;IAC5B,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA4ED,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,mBAAmB,CAAuB;IAClD,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,sBAAsB,CAAsB;IACpD,OAAO,CAAC,WAAW,CAAC,CAAa;IACjC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,mBAAmB,CAAuB;IAClD,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,eAAe,CAAkB;IAEzC,sCAAsC;IACtC,OAAO,CAAC,sBAAsB,CAAS;IACvC,OAAO,CAAC,mBAAmB,CAAS;IAEpC,qDAAqD;IACrD,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,UAAU,CAAS;IAE3B,8CAA8C;IAC9C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,CAAiB;IAClE,OAAO,CAAC,kBAAkB,CAA8C;IAExE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAkB;gBAE/C,OAAO,EAAE,kBAAkB;IAkFvC;;;;;OAKG;YACW,qBAAqB;IA2CnC,iEAAiE;YACnD,gBAAgB;IAiB9B,oCAAoC;IACpC,OAAO,IAAI,IAAI;IAOf,MAAM,IAAI,OAAO;IAIjB;;;;OAIG;IACG,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IA6BzD,4CAA4C;YAC9B,UAAU;IAOxB,iEAAiE;YACnD,iBAAiB;YAejB,eAAe;IAkqB7B,oEAAoE;IACpE,OAAO,CAAC,yBAAyB;YA0EnB,YAAY;IAW1B,yEAAyE;IACzE,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;CAkBjC"}
1
+ {"version":3,"file":"message-loop.d.ts","sourceRoot":"","sources":["../src/message-loop.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,YAAY,EAAe,MAAM,eAAe,CAAC;AAM3E,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,EAAmB,cAAc,EAAiC,MAAM,YAAY,CAAC;AAG5F,OAAO,EAAoB,YAAY,EAAE,MAAM,oBAAoB,CAAC;AASpE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,eAAe,CAAC;IAC5B,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAqFD,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,mBAAmB,CAAuB;IAClD,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,sBAAsB,CAAsB;IACpD,OAAO,CAAC,WAAW,CAAC,CAAa;IACjC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,mBAAmB,CAAuB;IAClD,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,SAAS,CAA+C;IAChE,OAAO,CAAC,eAAe,CAAkB;IAEzC,sCAAsC;IACtC,OAAO,CAAC,sBAAsB,CAAS;IACvC,OAAO,CAAC,mBAAmB,CAAS;IAEpC,qDAAqD;IACrD,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,UAAU,CAAS;IAE3B,6BAA6B;IAC7B,OAAO,CAAC,YAAY,CAAoE;IACxF,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAkB;IAE/D,8CAA8C;IAC9C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,yBAAyB,CAAiB;IAClE,OAAO,CAAC,kBAAkB,CAA8C;IAExE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAkB;gBAE/C,OAAO,EAAE,kBAAkB;IAkFvC;;;;;OAKG;YACW,qBAAqB;IA2CnC,iEAAiE;YACnD,gBAAgB;IAiB9B,oCAAoC;IACpC,OAAO,IAAI,IAAI;IAOf,MAAM,IAAI,OAAO;IAIjB,OAAO,CAAC,uBAAuB;YAYjB,uBAAuB;IAWrC;;;;OAIG;IACG,aAAa,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IA2CzD,4CAA4C;YAC9B,UAAU;IAOxB,iEAAiE;YACnD,iBAAiB;YAejB,eAAe;IAozB7B,oEAAoE;IACpE,OAAO,CAAC,yBAAyB;YAuDnB,YAAY;IAW1B,yEAAyE;IACzE,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;CAkBjC"}
@@ -52,6 +52,7 @@ const clerk_model_1 = require("./clerk-model");
52
52
  const local_funnel_1 = require("./local-funnel");
53
53
  const auto_organizer_1 = require("./auto-organizer");
54
54
  const tool_permissions_1 = require("./tool-permissions");
55
+ const response_guard_1 = require("./response-guard");
55
56
  const data = __importStar(require("./local-data"));
56
57
  /** Determine priority from an AgentCommand */
57
58
  function getCommandPriority(command) {
@@ -117,6 +118,15 @@ function normalizeMcpToolNames(toolNames) {
117
118
  .filter(Boolean);
118
119
  return [...new Set(trimmed)];
119
120
  }
121
+ function isFastPathEnvKeyCheck(prompt) {
122
+ const text = String(prompt || '').toLowerCase();
123
+ if (!text)
124
+ return false;
125
+ const asksOpenAiKey = /(openai[_\s-]*api[_\s-]*key|openai key)/i.test(text);
126
+ const asksPresence = /(is|check|whether|exists|set|available|configured|have access)/i.test(text);
127
+ const asksEnv = /(env|environment|variable|local)/i.test(text);
128
+ return asksOpenAiKey && (asksPresence || asksEnv);
129
+ }
120
130
  class MessageLoop {
121
131
  options;
122
132
  llmProvider;
@@ -137,6 +147,10 @@ class MessageLoop {
137
147
  /** Priority queue for commands when agent is busy */
138
148
  commandQueue = [];
139
149
  processing = false;
150
+ /** Idempotency guardrails */
151
+ commandState = new Map();
152
+ commandCompletedAt = new Map();
153
+ static COMMAND_DEDUPE_TTL_MS = 30 * 60 * 1000;
140
154
  /** Timeout for scheduled tasks (5 minutes) */
141
155
  static SCHEDULED_TASK_TIMEOUT_MS = 5 * 60 * 1000;
142
156
  scheduledTaskTimer = null;
@@ -284,6 +298,27 @@ class MessageLoop {
284
298
  isBusy() {
285
299
  return this.processing || this.activeCommandId !== null;
286
300
  }
301
+ pruneCommandDedupeCache() {
302
+ const cutoff = Date.now() - MessageLoop.COMMAND_DEDUPE_TTL_MS;
303
+ for (const [commandId, completedAt] of this.commandCompletedAt.entries()) {
304
+ if (completedAt < cutoff) {
305
+ this.commandCompletedAt.delete(commandId);
306
+ if (this.commandState.get(commandId) === 'completed') {
307
+ this.commandState.delete(commandId);
308
+ }
309
+ }
310
+ }
311
+ }
312
+ async publishDuplicateIgnored(command, reason) {
313
+ await this.options.mqttClient.publishResult({
314
+ commandId: command.id,
315
+ type: 'status',
316
+ phase: 'duplicate_ignored',
317
+ detail: reason,
318
+ status: 'duplicate_ignored',
319
+ timestamp: Date.now(),
320
+ });
321
+ }
287
322
  /**
288
323
  * Public entry point: enqueue or execute a command based on priority.
289
324
  * High-priority (user) commands execute immediately.
@@ -302,6 +337,18 @@ class MessageLoop {
302
337
  if (command.type === 'ping' || command.type === 'cancel') {
303
338
  return this._executeCommand(command);
304
339
  }
340
+ this.pruneCommandDedupeCache();
341
+ const existingState = this.commandState.get(command.id);
342
+ if (existingState === 'queued' || existingState === 'running') {
343
+ console.log(chalk_1.default.yellow(` ↺ Duplicate command ignored ${command.id.slice(0, 8)}... (state=${existingState})`));
344
+ await this.publishDuplicateIgnored(command, `Command already ${existingState}`);
345
+ return;
346
+ }
347
+ if (existingState === 'completed' || existingState === 'failed') {
348
+ console.log(chalk_1.default.yellow(` ↺ Duplicate command ignored ${command.id.slice(0, 8)}... (recently ${existingState})`));
349
+ await this.publishDuplicateIgnored(command, `Command already ${existingState}`);
350
+ return;
351
+ }
305
352
  const priority = getCommandPriority(command);
306
353
  // If not busy, execute directly
307
354
  if (!this.processing) {
@@ -310,6 +357,7 @@ class MessageLoop {
310
357
  // If busy but incoming is high priority and current is not high, queue current? No — just queue incoming.
311
358
  // Simple approach: queue everything when busy, drain by priority when done.
312
359
  console.log(chalk_1.default.yellow(` ⏳ Agent busy, queuing command ${command.id.slice(0, 8)}... (priority: ${priority})`));
360
+ this.commandState.set(command.id, 'queued');
313
361
  this.commandQueue.push({ command, priority, queuedAt: Date.now() });
314
362
  // Sort: high first, then medium, then low
315
363
  this.commandQueue.sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]);
@@ -359,6 +407,8 @@ class MessageLoop {
359
407
  return;
360
408
  }
361
409
  if (!command.prompt) {
410
+ this.commandState.set(command.id, 'failed');
411
+ this.commandCompletedAt.set(command.id, Date.now());
362
412
  await this.publishError(command.id, 'No prompt provided');
363
413
  return;
364
414
  }
@@ -424,14 +474,19 @@ class MessageLoop {
424
474
  this.toolDefinitions = (0, index_2.getAllToolDefinitions)(this._mcpManager);
425
475
  this.activeCommandId = command.id;
426
476
  this.activeCommandSource = command.source || 'user';
477
+ this.commandState.set(command.id, 'running');
427
478
  this.processing = true;
428
479
  this.lastMessageAt = Date.now();
429
480
  const commandStartMs = Date.now();
430
481
  let totalToolCalls = 0;
482
+ let totalToolErrors = 0; // Phase 3.3: track errors for step_progress events
483
+ let commandFailed = false;
431
484
  // Set timeout for scheduled tasks (5 minutes)
432
485
  if (command.source === 'scheduled') {
433
486
  this.scheduledTaskTimer = setTimeout(() => {
434
487
  console.log(chalk_1.default.red(` ⏰ Scheduled task ${command.id.slice(0, 8)} timed out after 5 minutes`));
488
+ this.commandState.set(command.id, 'failed');
489
+ this.commandCompletedAt.set(command.id, Date.now());
435
490
  this.activeCommandId = null;
436
491
  this.processing = false;
437
492
  this.publishTaskResult(command, 'Task timed out after 5 minutes', false);
@@ -492,21 +547,23 @@ class MessageLoop {
492
547
  // Resolve the selected bot profile — from command.bot, conversation bot_id, or default
493
548
  const selectedBotId = command.bot?.id || activeConversation?.bot_id || localAgentId;
494
549
  const activeProfile = selectedBotId ? localData?.getAgentProfile(selectedBotId) : null;
495
- if (activeProfile?.role_class === 'orchestrator' && localFirst) {
496
- const clerk = (0, clerk_model_1.getClerk)();
497
- if (!clerk) {
498
- // Do not silently fall through to direct chat — report error
499
- await this.publishError(command.id, 'Orchestrator mode requires a clerk model. Please add a provider connection in Settings.');
500
- this.activeCommandId = null;
501
- this.processing = false;
502
- this.drainQueue();
503
- return;
504
- }
550
+ if (localFirst && (activeProfile?.role_class === 'orchestrator' || localData?.isClerkOrchestratorEnabled?.())) {
505
551
  {
506
- const { OrchestratorAgent } = await Promise.resolve().then(() => __importStar(require('./orchestrator')));
552
+ const { OrchestratorAgent, buildLocalDesktopOrchestratorRuntime } = await Promise.resolve().then(() => __importStar(require('./orchestrator')));
507
553
  const { getWorkflowEngine } = await Promise.resolve().then(() => __importStar(require('./workflow-engine')));
508
554
  const workflowEngine = getWorkflowEngine(this.options.projectDir);
509
- const orchestrator = new OrchestratorAgent(clerk, workflowEngine);
555
+ let orchestratorRuntime;
556
+ try {
557
+ orchestratorRuntime = buildLocalDesktopOrchestratorRuntime(activeProfile || undefined);
558
+ }
559
+ catch (runtimeErr) {
560
+ await this.publishError(command.id, runtimeErr?.message || 'Orchestrator mode is not configured correctly.');
561
+ this.activeCommandId = null;
562
+ this.processing = false;
563
+ this.drainQueue();
564
+ return;
565
+ }
566
+ const orchestrator = new OrchestratorAgent(orchestratorRuntime, workflowEngine);
510
567
  try {
511
568
  const response = await orchestrator.handleUserMessage(command.prompt, localConvId || '', {
512
569
  projectDir: this.options.projectDir,
@@ -524,7 +581,7 @@ class MessageLoop {
524
581
  });
525
582
  // Save O's response to local DB
526
583
  if (localConvId && localData) {
527
- localData.addMessage(localConvId, 'assistant', response, undefined, undefined, activeProfile.id, 'Project Manager');
584
+ localData.addMessage(localConvId, 'assistant', response, undefined, undefined, activeProfile?.id, 'Project Manager');
528
585
  }
529
586
  // Publish final response and complete
530
587
  await this.options.mqttClient.publishResult({
@@ -555,6 +612,81 @@ class MessageLoop {
555
612
  return;
556
613
  }
557
614
  }
615
+ // Fast-path: simple environment API-key presence checks for Claude CLI.
616
+ // This avoids full LLM/tool orchestration latency for common operational asks.
617
+ if (provider === 'claude-cli' && isFastPathEnvKeyCheck(command.prompt)) {
618
+ await this.options.mqttClient.publishResult({
619
+ commandId: command.id,
620
+ type: 'status',
621
+ phase: 'thinking',
622
+ detail: 'Checking local environment…',
623
+ timestamp: Date.now(),
624
+ });
625
+ const checkCommand = `if [ -n "${'$'}OPENAI_API_KEY" ]; then echo "OPENAI_API_KEY=set"; else echo "OPENAI_API_KEY=not_set"; fi; if [ -f .env ]; then if grep -q '^OPENAI_API_KEY=' .env; then echo ".env_OPENAI_API_KEY=present"; else echo ".env_OPENAI_API_KEY=absent"; fi; else echo ".env=missing"; fi`;
626
+ const rawResult = await (0, tools_1.executeToolWithMCP)({ id: `fastpath-${Date.now()}`, name: 'run_command', arguments: { command: checkCommand, cwd: this.options.projectDir } }, commandToolContext, this.options.mcpManager);
627
+ const verified = await (0, verification_1.verifyToolResult)(rawResult, { command: checkCommand, cwd: this.options.projectDir }, commandToolContext);
628
+ const output = verified.success ? verified.output : `ERROR: ${verified.error || 'Unknown error'}`;
629
+ const keySet = /OPENAI_API_KEY=set/.test(output);
630
+ const envFilePresent = /\.env_OPENAI_API_KEY=present/.test(output);
631
+ const final = keySet || envFilePresent
632
+ ? 'OPENAI_API_KEY is set (environment and/or .env).'
633
+ : 'No OPENAI_API_KEY is set in the current environment or .env file.';
634
+ await this.options.mqttClient.publishResult({
635
+ commandId: command.id,
636
+ type: 'tool_call',
637
+ toolCall: {
638
+ id: `fastpath-${Date.now()}`,
639
+ name: 'run_command',
640
+ arguments: { command: 'check OPENAI_API_KEY presence' },
641
+ classification: 'command_run',
642
+ summary: 'Fast-path env key presence check',
643
+ cwd: this.options.projectDir,
644
+ importance: 'low',
645
+ },
646
+ timestamp: Date.now(),
647
+ });
648
+ await this.options.mqttClient.publishResult({
649
+ commandId: command.id,
650
+ type: 'tool_result',
651
+ toolResult: {
652
+ callId: `fastpath-${Date.now()}`,
653
+ output: output.slice(0, 500),
654
+ isError: !verified.success,
655
+ success: verified.success,
656
+ summary: summarizeToolResult(output, !verified.success),
657
+ cwd: this.options.projectDir,
658
+ },
659
+ timestamp: Date.now(),
660
+ });
661
+ await this.options.mqttClient.publishResult({
662
+ commandId: command.id,
663
+ type: 'chunk',
664
+ content: final,
665
+ timestamp: Date.now(),
666
+ });
667
+ await this.options.mqttClient.publishResult({
668
+ commandId: command.id,
669
+ type: 'complete',
670
+ content: '',
671
+ finalContent: final,
672
+ finalContentLength: final.length,
673
+ timestamp: Date.now(),
674
+ });
675
+ if (localFirst && localConvId && localData) {
676
+ try {
677
+ localData.addMessage(localConvId, 'assistant', final, model);
678
+ }
679
+ catch { }
680
+ }
681
+ this.activeCommandId = null;
682
+ this.processing = false;
683
+ if (this.scheduledTaskTimer) {
684
+ clearTimeout(this.scheduledTaskTimer);
685
+ this.scheduledTaskTimer = null;
686
+ }
687
+ this.drainQueue();
688
+ return;
689
+ }
558
690
  // Build messages from context
559
691
  const messages = [];
560
692
  // Add previous conversation context
@@ -617,6 +749,8 @@ TOOL EFFICIENCY RULES:
617
749
  let iteration = 0;
618
750
  const MAX_ITERATIONS = Infinity;
619
751
  let totalTokensUsed = 0;
752
+ let forcedFinalizationPass = false;
753
+ const llmTimingSamples = [];
620
754
  while (iteration < MAX_ITERATIONS && this.activeCommandId === command.id) {
621
755
  iteration++;
622
756
  // Send a separator between agentic loop iterations so streamed text
@@ -656,6 +790,28 @@ TOOL EFFICIENCY RULES:
656
790
  console.log(chalk_1.default.gray(` [tool-filter] ${filteredTools.length}/${this.toolDefinitions.length} tools selected`));
657
791
  }
658
792
  let response;
793
+ const llmCallStartMs = Date.now();
794
+ let firstChunkLatencyMs = null;
795
+ await this.options.mqttClient.publishResult({
796
+ commandId: command.id,
797
+ type: 'status',
798
+ phase: 'thinking',
799
+ detail: iteration === 1 ? 'Working on it…' : `Continuing… (pass ${iteration})`,
800
+ timestamp: Date.now(),
801
+ });
802
+ let emittedSlowStatus = false;
803
+ const slowStatusTimer = setTimeout(() => {
804
+ if (this.activeCommandId !== command.id || firstChunkLatencyMs !== null)
805
+ return;
806
+ emittedSlowStatus = true;
807
+ this.options.mqttClient.publishResult({
808
+ commandId: command.id,
809
+ type: 'status',
810
+ phase: 'thinking',
811
+ detail: 'Still working…',
812
+ timestamp: Date.now(),
813
+ }).catch(() => { });
814
+ }, 2000);
659
815
  try {
660
816
  response = await llm.chat({
661
817
  messages,
@@ -665,6 +821,9 @@ TOOL EFFICIENCY RULES:
665
821
  onChunk: async (chunk) => {
666
822
  if (this.activeCommandId !== command.id)
667
823
  return;
824
+ if (firstChunkLatencyMs === null) {
825
+ firstChunkLatencyMs = Date.now() - llmCallStartMs;
826
+ }
668
827
  await this.options.mqttClient.publishResult({
669
828
  commandId: command.id,
670
829
  type: 'chunk',
@@ -673,8 +832,12 @@ TOOL EFFICIENCY RULES:
673
832
  });
674
833
  },
675
834
  });
835
+ clearTimeout(slowStatusTimer);
836
+ const llmTotalMs = Date.now() - llmCallStartMs;
837
+ llmTimingSamples.push(`iter${iteration}:firstChunkMs=${firstChunkLatencyMs ?? llmTotalMs},totalMs=${llmTotalMs},selectedTools=${filteredTools.length},slowStatus=${emittedSlowStatus ? 1 : 0}`);
676
838
  }
677
839
  catch (llmError) {
840
+ clearTimeout(slowStatusTimer);
678
841
  const msg = llmError?.message || String(llmError);
679
842
  const isAuthError = /401|403|unauthorized|forbidden|authentication/i.test(msg);
680
843
  if (isAuthError) {
@@ -703,9 +866,14 @@ TOOL EFFICIENCY RULES:
703
866
  });
704
867
  for (const toolCall of response.toolCalls) {
705
868
  totalToolCalls++;
869
+ await this.options.mqttClient.publishResult({
870
+ commandId: command.id,
871
+ type: 'status',
872
+ phase: 'calling_tool',
873
+ detail: `Running ${toolCall.name}...`,
874
+ timestamp: Date.now(),
875
+ });
706
876
  console.log(chalk_1.default.cyan(` 🔧 Tool: ${toolCall.name}(${JSON.stringify(toolCall.arguments).slice(0, 60)}...)`));
707
- const toolSummary = (0, approval_1.generateToolSummary)(toolCall.name, toolCall.arguments);
708
- const toolLocation = extractToolLocation(toolCall.name, toolCall.arguments, commandToolContext.projectDir);
709
877
  // Check permission before execution
710
878
  const permMode = this.options.permissionMode || 'autopilot';
711
879
  const allowedToolNames = this.options.enabledTools !== undefined || this.options.enabledMcpTools !== undefined
@@ -715,29 +883,17 @@ TOOL EFFICIENCY RULES:
715
883
  // If not auto-approved, request interactive approval via MQTT
716
884
  if (!approval.approved && permMode !== 'autopilot') {
717
885
  console.log(chalk_1.default.yellow(` 🔐 Requesting user approval for: ${toolCall.name}`));
718
- approval = await this.approvalManager.requestApproval(command.id, toolCall.name, toolCall.arguments, {
719
- onRequest: async (request) => {
720
- await this.options.mqttClient.publishResult({
721
- commandId: command.id,
722
- type: 'approval_request',
723
- requestId: request.requestId,
724
- riskLevel: request.riskLevel,
725
- summary: request.summary,
726
- toolCall: {
727
- id: toolCall.id,
728
- name: toolCall.name,
729
- arguments: toolCall.arguments,
730
- classification: classifyToolName(toolCall.name),
731
- summary: toolSummary,
732
- path: toolLocation.path,
733
- cwd: toolLocation.cwd,
734
- url: toolLocation.url,
735
- importance: request.riskLevel === 'destructive' ? 'high' : 'medium',
736
- },
737
- timestamp: Date.now(),
738
- });
886
+ await this.options.mqttClient.publishResult({
887
+ commandId: command.id,
888
+ type: 'approval_request',
889
+ toolCall: {
890
+ id: toolCall.id,
891
+ name: toolCall.name,
892
+ arguments: toolCall.arguments,
739
893
  },
894
+ timestamp: Date.now(),
740
895
  });
896
+ approval = await this.approvalManager.requestApproval(command.id, toolCall.name, toolCall.arguments);
741
897
  // If user chose "always allow", persist it
742
898
  if (approval.approved && approval.remember) {
743
899
  (0, approval_1.rememberTool)(toolCall.name);
@@ -746,26 +902,19 @@ TOOL EFFICIENCY RULES:
746
902
  }
747
903
  if (!approval.approved) {
748
904
  console.log(chalk_1.default.yellow(` ⚠ Tool denied: ${approval.reason}`));
749
- const deniedOutput = `PERMISSION_DENIED: ${approval.reason}`;
750
905
  await this.options.mqttClient.publishResult({
751
906
  commandId: command.id,
752
907
  type: 'tool_result',
753
908
  toolResult: {
754
909
  callId: toolCall.id,
755
- output: deniedOutput,
910
+ output: `PERMISSION_DENIED: ${approval.reason}`,
756
911
  isError: true,
757
- success: false,
758
- summary: summarizeToolResult(deniedOutput, true),
759
- path: toolLocation.path,
760
- cwd: toolLocation.cwd,
761
- url: toolLocation.url,
762
- pathsTouched: toolLocation.path ? [toolLocation.path] : [],
763
912
  },
764
913
  timestamp: Date.now(),
765
914
  });
766
915
  messages.push({
767
916
  role: 'tool',
768
- content: deniedOutput,
917
+ content: `PERMISSION_DENIED: ${approval.reason}`,
769
918
  toolCallId: toolCall.id,
770
919
  toolName: toolCall.name,
771
920
  });
@@ -778,12 +927,6 @@ TOOL EFFICIENCY RULES:
778
927
  id: toolCall.id,
779
928
  name: toolCall.name,
780
929
  arguments: toolCall.arguments,
781
- classification: classifyToolName(toolCall.name),
782
- summary: toolSummary,
783
- path: toolLocation.path,
784
- cwd: toolLocation.cwd,
785
- url: toolLocation.url,
786
- importance: (0, approval_1.getRiskLevel)(toolCall.name) === 'destructive' ? 'high' : 'medium',
787
930
  },
788
931
  timestamp: Date.now(),
789
932
  });
@@ -793,6 +936,9 @@ TOOL EFFICIENCY RULES:
793
936
  const resultOutput = toolResult.success
794
937
  ? toolResult.output
795
938
  : `ERROR: ${toolResult.error || 'Unknown error'}`;
939
+ // Phase 3.3: Track tool errors
940
+ if (!toolResult.success)
941
+ totalToolErrors++;
796
942
  console.log(chalk_1.default.gray(` Result: ${toolResult.success ? '✓' : '✗'} ${resultOutput.slice(0, 100)}${resultOutput.length > 100 ? '...' : ''}`));
797
943
  await this.options.mqttClient.publishResult({
798
944
  commandId: command.id,
@@ -801,16 +947,37 @@ TOOL EFFICIENCY RULES:
801
947
  callId: toolCall.id,
802
948
  output: resultOutput,
803
949
  isError: !toolResult.success,
804
- success: toolResult.success,
805
- summary: summarizeToolResult(resultOutput, !toolResult.success),
806
- path: toolLocation.path,
807
- cwd: toolLocation.cwd,
808
- url: toolLocation.url,
809
- pathsTouched: toolLocation.path ? [toolLocation.path] : [],
810
- exitCode: typeof toolResult.exit_code === 'number' ? toolResult.exit_code : undefined,
811
950
  },
812
951
  timestamp: Date.now(),
813
952
  });
953
+ // Phase 3.3: Emit structured step_progress event for watchdog consumption
954
+ if (command.taskId) {
955
+ try {
956
+ await this.options.mqttClient.publishResult({
957
+ commandId: command.id,
958
+ type: 'tool_result',
959
+ toolResult: {
960
+ callId: `step_progress_${toolCall.id}`,
961
+ output: JSON.stringify({
962
+ type: 'step_progress',
963
+ taskId: command.taskId,
964
+ toolName: toolCall.name,
965
+ arguments: toolCall.arguments,
966
+ success: toolResult.success,
967
+ errorMessage: toolResult.success ? undefined : (toolResult.error || 'Unknown error'),
968
+ totalToolCalls,
969
+ errorCount: totalToolErrors,
970
+ timestamp: Date.now(),
971
+ }),
972
+ isError: false,
973
+ },
974
+ timestamp: Date.now(),
975
+ });
976
+ }
977
+ catch {
978
+ // Best effort — don't break the main loop for progress reporting
979
+ }
980
+ }
814
981
  // Add tool result to messages using proper 'tool' role for provider formatting
815
982
  messages.push({
816
983
  role: 'tool',
@@ -895,13 +1062,27 @@ TOOL EFFICIENCY RULES:
895
1062
  continue;
896
1063
  }
897
1064
  // No tool calls - final response.
1065
+ const candidate = (response.content || '').trim();
1066
+ if (!forcedFinalizationPass && (0, response_guard_1.isLikelyDeferredReply)(candidate)) {
1067
+ forcedFinalizationPass = true;
1068
+ messages.push({ role: 'assistant', content: candidate });
1069
+ messages.push({
1070
+ role: 'user',
1071
+ content: 'Provide the final answer now. Do not say you will check later. Either provide concrete results or explicitly say what is unavailable.',
1072
+ });
1073
+ console.log(chalk_1.default.yellow(' [response-guard] Deferred-style reply detected; forcing finalization pass'));
1074
+ continue;
1075
+ }
898
1076
  // DEDUP FIX: Content was already streamed via onChunk callbacks above.
899
- // The 'complete' message signals end-of-response; we send empty content
900
- // to avoid the receiver concatenating streamed chunks + full content.
1077
+ // The 'complete' message signals end-of-response; include finalContent
1078
+ // metadata so clients can reconcile if stream chunks were truncated.
1079
+ const finalResponseText = (response.content || '').trim();
901
1080
  await this.options.mqttClient.publishResult({
902
1081
  commandId: command.id,
903
1082
  type: 'complete',
904
- content: '',
1083
+ content: finalResponseText,
1084
+ finalContent: finalResponseText,
1085
+ finalContentLength: finalResponseText.length,
905
1086
  timestamp: Date.now(),
906
1087
  });
907
1088
  // Local-first: save assistant response to SQLite + trigger funnel
@@ -922,15 +1103,17 @@ TOOL EFFICIENCY RULES:
922
1103
  }
923
1104
  console.log(chalk_1.default.white(` Response: ${response.content?.slice(0, 200)}${(response.content?.length || 0) > 200 ? '...' : ''}`));
924
1105
  const latencyMs = Date.now() - commandStartMs;
925
- console.log(`[agent-metrics] commandId=${command.id} contextSections=${contextSectionCount} toolCallsMade=${totalToolCalls} iterations=${iteration} totalTokens=${totalTokensUsed} latencyMs=${latencyMs}`);
1106
+ console.log(`[agent-metrics] commandId=${command.id} contextSections=${contextSectionCount} toolCallsMade=${totalToolCalls} iterations=${iteration} totalTokens=${totalTokensUsed} latencyMs=${latencyMs} llmTimings="${llmTimingSamples.join(' | ')}"`);
926
1107
  console.log(chalk_1.default.green(` Command completed: ${iteration} iterations, ${totalToolCalls} tool calls, ~${totalTokensUsed} tokens`));
927
1108
  break;
928
1109
  }
929
1110
  if (iteration >= MAX_ITERATIONS) {
1111
+ commandFailed = true;
930
1112
  await this.publishError(command.id, `Max iterations (${MAX_ITERATIONS}) reached`);
931
1113
  }
932
1114
  }
933
1115
  catch (error) {
1116
+ commandFailed = true;
934
1117
  console.error(chalk_1.default.red(` ✗ Error: ${error.message}`));
935
1118
  await this.publishError(command.id, error.message);
936
1119
  }
@@ -938,6 +1121,8 @@ TOOL EFFICIENCY RULES:
938
1121
  this.activeCommandId = null;
939
1122
  this.activeCommandSource = null;
940
1123
  this.processing = false;
1124
+ this.commandState.set(command.id, commandFailed ? 'failed' : 'completed');
1125
+ this.commandCompletedAt.set(command.id, Date.now());
941
1126
  // Clear scheduled task timeout
942
1127
  if (this.scheduledTaskTimer) {
943
1128
  clearTimeout(this.scheduledTaskTimer);
@@ -957,7 +1142,6 @@ TOOL EFFICIENCY RULES:
957
1142
  || this.options.systemPrompt
958
1143
  || 'You are a Funolio AI agent running locally on the user\'s machine. You have access to their project files and can execute code.';
959
1144
  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.';
960
- 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.';
961
1145
  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.';
962
1146
  systemPrompt += '\n\n' + (0, clerk_model_1.buildTodoInstructions)(this.options.agentName || 'LLM');
963
1147
  // Inject model self-switch info
@@ -1002,32 +1186,6 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
1002
1186
  if (command.context?.files?.length) {
1003
1187
  systemPrompt += '\n\nRelevant files: ' + command.context.files.join(', ');
1004
1188
  }
1005
- if (command.context?.stateContext?.summaryText) {
1006
- systemPrompt += '\n\n[Recent Operational State]\n' + command.context.stateContext.summaryText;
1007
- }
1008
- if (command.context?.workspaceManifest) {
1009
- const manifestLines = [];
1010
- const manifest = command.context.workspaceManifest;
1011
- if (manifest.projectRoot)
1012
- manifestLines.push(`- Project root: ${manifest.projectRoot}`);
1013
- if (manifest.likelyWorkingDirectory)
1014
- manifestLines.push(`- Likely working directory: ${manifest.likelyWorkingDirectory}`);
1015
- if (manifest.recentlyReadFiles?.length)
1016
- manifestLines.push(`- Recently read files: ${manifest.recentlyReadFiles.join(', ')}`);
1017
- if (manifest.recentlyWrittenFiles?.length)
1018
- manifestLines.push(`- Recently written files: ${manifest.recentlyWrittenFiles.join(', ')}`);
1019
- if (manifest.recentlyScannedDirectories?.length)
1020
- manifestLines.push(`- Recently scanned directories: ${manifest.recentlyScannedDirectories.join(', ')}`);
1021
- if (manifest.recentDownloads?.length)
1022
- manifestLines.push(`- Recent downloads: ${manifest.recentDownloads.join(', ')}`);
1023
- if (manifest.recentFailures?.length)
1024
- manifestLines.push(`- Recent failures: ${manifest.recentFailures.join(' | ')}`);
1025
- if (manifest.startHereHints?.length)
1026
- manifestLines.push(`- Start here: ${manifest.startHereHints.join(', ')}`);
1027
- if (manifestLines.length) {
1028
- systemPrompt += '\n\n[Workspace Manifest]\n' + manifestLines.join('\n');
1029
- }
1030
- }
1031
1189
  return systemPrompt;
1032
1190
  }
1033
1191
  async publishError(commandId, error) {