funolio-agent 1.0.53 → 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 +239 -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;IA4qB7B,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,7 @@ TOOL EFFICIENCY RULES:
617
749
  let iteration = 0;
618
750
  const MAX_ITERATIONS = Infinity;
619
751
  let totalTokensUsed = 0;
752
+ let forcedFinalizationPass = false;
620
753
  const llmTimingSamples = [];
621
754
  while (iteration < MAX_ITERATIONS && this.activeCommandId === command.id) {
622
755
  iteration++;
@@ -659,6 +792,26 @@ TOOL EFFICIENCY RULES:
659
792
  let response;
660
793
  const llmCallStartMs = Date.now();
661
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);
662
815
  try {
663
816
  response = await llm.chat({
664
817
  messages,
@@ -679,10 +832,12 @@ TOOL EFFICIENCY RULES:
679
832
  });
680
833
  },
681
834
  });
835
+ clearTimeout(slowStatusTimer);
682
836
  const llmTotalMs = Date.now() - llmCallStartMs;
683
- llmTimingSamples.push(`iter${iteration}:firstChunkMs=${firstChunkLatencyMs ?? llmTotalMs},totalMs=${llmTotalMs},selectedTools=${filteredTools.length}`);
837
+ llmTimingSamples.push(`iter${iteration}:firstChunkMs=${firstChunkLatencyMs ?? llmTotalMs},totalMs=${llmTotalMs},selectedTools=${filteredTools.length},slowStatus=${emittedSlowStatus ? 1 : 0}`);
684
838
  }
685
839
  catch (llmError) {
840
+ clearTimeout(slowStatusTimer);
686
841
  const msg = llmError?.message || String(llmError);
687
842
  const isAuthError = /401|403|unauthorized|forbidden|authentication/i.test(msg);
688
843
  if (isAuthError) {
@@ -711,9 +866,14 @@ TOOL EFFICIENCY RULES:
711
866
  });
712
867
  for (const toolCall of response.toolCalls) {
713
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
+ });
714
876
  console.log(chalk_1.default.cyan(` 🔧 Tool: ${toolCall.name}(${JSON.stringify(toolCall.arguments).slice(0, 60)}...)`));
715
- const toolSummary = (0, approval_1.generateToolSummary)(toolCall.name, toolCall.arguments);
716
- const toolLocation = extractToolLocation(toolCall.name, toolCall.arguments, commandToolContext.projectDir);
717
877
  // Check permission before execution
718
878
  const permMode = this.options.permissionMode || 'autopilot';
719
879
  const allowedToolNames = this.options.enabledTools !== undefined || this.options.enabledMcpTools !== undefined
@@ -723,29 +883,17 @@ TOOL EFFICIENCY RULES:
723
883
  // If not auto-approved, request interactive approval via MQTT
724
884
  if (!approval.approved && permMode !== 'autopilot') {
725
885
  console.log(chalk_1.default.yellow(` 🔐 Requesting user approval for: ${toolCall.name}`));
726
- approval = await this.approvalManager.requestApproval(command.id, toolCall.name, toolCall.arguments, {
727
- onRequest: async (request) => {
728
- await this.options.mqttClient.publishResult({
729
- commandId: command.id,
730
- type: 'approval_request',
731
- requestId: request.requestId,
732
- riskLevel: request.riskLevel,
733
- summary: request.summary,
734
- toolCall: {
735
- id: toolCall.id,
736
- name: toolCall.name,
737
- arguments: toolCall.arguments,
738
- classification: classifyToolName(toolCall.name),
739
- summary: toolSummary,
740
- path: toolLocation.path,
741
- cwd: toolLocation.cwd,
742
- url: toolLocation.url,
743
- importance: request.riskLevel === 'destructive' ? 'high' : 'medium',
744
- },
745
- timestamp: Date.now(),
746
- });
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,
747
893
  },
894
+ timestamp: Date.now(),
748
895
  });
896
+ approval = await this.approvalManager.requestApproval(command.id, toolCall.name, toolCall.arguments);
749
897
  // If user chose "always allow", persist it
750
898
  if (approval.approved && approval.remember) {
751
899
  (0, approval_1.rememberTool)(toolCall.name);
@@ -754,26 +902,19 @@ TOOL EFFICIENCY RULES:
754
902
  }
755
903
  if (!approval.approved) {
756
904
  console.log(chalk_1.default.yellow(` ⚠ Tool denied: ${approval.reason}`));
757
- const deniedOutput = `PERMISSION_DENIED: ${approval.reason}`;
758
905
  await this.options.mqttClient.publishResult({
759
906
  commandId: command.id,
760
907
  type: 'tool_result',
761
908
  toolResult: {
762
909
  callId: toolCall.id,
763
- output: deniedOutput,
910
+ output: `PERMISSION_DENIED: ${approval.reason}`,
764
911
  isError: true,
765
- success: false,
766
- summary: summarizeToolResult(deniedOutput, true),
767
- path: toolLocation.path,
768
- cwd: toolLocation.cwd,
769
- url: toolLocation.url,
770
- pathsTouched: toolLocation.path ? [toolLocation.path] : [],
771
912
  },
772
913
  timestamp: Date.now(),
773
914
  });
774
915
  messages.push({
775
916
  role: 'tool',
776
- content: deniedOutput,
917
+ content: `PERMISSION_DENIED: ${approval.reason}`,
777
918
  toolCallId: toolCall.id,
778
919
  toolName: toolCall.name,
779
920
  });
@@ -786,12 +927,6 @@ TOOL EFFICIENCY RULES:
786
927
  id: toolCall.id,
787
928
  name: toolCall.name,
788
929
  arguments: toolCall.arguments,
789
- classification: classifyToolName(toolCall.name),
790
- summary: toolSummary,
791
- path: toolLocation.path,
792
- cwd: toolLocation.cwd,
793
- url: toolLocation.url,
794
- importance: (0, approval_1.getRiskLevel)(toolCall.name) === 'destructive' ? 'high' : 'medium',
795
930
  },
796
931
  timestamp: Date.now(),
797
932
  });
@@ -801,6 +936,9 @@ TOOL EFFICIENCY RULES:
801
936
  const resultOutput = toolResult.success
802
937
  ? toolResult.output
803
938
  : `ERROR: ${toolResult.error || 'Unknown error'}`;
939
+ // Phase 3.3: Track tool errors
940
+ if (!toolResult.success)
941
+ totalToolErrors++;
804
942
  console.log(chalk_1.default.gray(` Result: ${toolResult.success ? '✓' : '✗'} ${resultOutput.slice(0, 100)}${resultOutput.length > 100 ? '...' : ''}`));
805
943
  await this.options.mqttClient.publishResult({
806
944
  commandId: command.id,
@@ -809,16 +947,37 @@ TOOL EFFICIENCY RULES:
809
947
  callId: toolCall.id,
810
948
  output: resultOutput,
811
949
  isError: !toolResult.success,
812
- success: toolResult.success,
813
- summary: summarizeToolResult(resultOutput, !toolResult.success),
814
- path: toolLocation.path,
815
- cwd: toolLocation.cwd,
816
- url: toolLocation.url,
817
- pathsTouched: toolLocation.path ? [toolLocation.path] : [],
818
- exitCode: typeof toolResult.exit_code === 'number' ? toolResult.exit_code : undefined,
819
950
  },
820
951
  timestamp: Date.now(),
821
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
+ }
822
981
  // Add tool result to messages using proper 'tool' role for provider formatting
823
982
  messages.push({
824
983
  role: 'tool',
@@ -903,13 +1062,27 @@ TOOL EFFICIENCY RULES:
903
1062
  continue;
904
1063
  }
905
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
+ }
906
1076
  // DEDUP FIX: Content was already streamed via onChunk callbacks above.
907
- // The 'complete' message signals end-of-response; we send empty content
908
- // 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();
909
1080
  await this.options.mqttClient.publishResult({
910
1081
  commandId: command.id,
911
1082
  type: 'complete',
912
- content: '',
1083
+ content: finalResponseText,
1084
+ finalContent: finalResponseText,
1085
+ finalContentLength: finalResponseText.length,
913
1086
  timestamp: Date.now(),
914
1087
  });
915
1088
  // Local-first: save assistant response to SQLite + trigger funnel
@@ -935,10 +1108,12 @@ TOOL EFFICIENCY RULES:
935
1108
  break;
936
1109
  }
937
1110
  if (iteration >= MAX_ITERATIONS) {
1111
+ commandFailed = true;
938
1112
  await this.publishError(command.id, `Max iterations (${MAX_ITERATIONS}) reached`);
939
1113
  }
940
1114
  }
941
1115
  catch (error) {
1116
+ commandFailed = true;
942
1117
  console.error(chalk_1.default.red(` ✗ Error: ${error.message}`));
943
1118
  await this.publishError(command.id, error.message);
944
1119
  }
@@ -946,6 +1121,8 @@ TOOL EFFICIENCY RULES:
946
1121
  this.activeCommandId = null;
947
1122
  this.activeCommandSource = null;
948
1123
  this.processing = false;
1124
+ this.commandState.set(command.id, commandFailed ? 'failed' : 'completed');
1125
+ this.commandCompletedAt.set(command.id, Date.now());
949
1126
  // Clear scheduled task timeout
950
1127
  if (this.scheduledTaskTimer) {
951
1128
  clearTimeout(this.scheduledTaskTimer);
@@ -965,7 +1142,6 @@ TOOL EFFICIENCY RULES:
965
1142
  || this.options.systemPrompt
966
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.';
967
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.';
968
- 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.';
969
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.';
970
1146
  systemPrompt += '\n\n' + (0, clerk_model_1.buildTodoInstructions)(this.options.agentName || 'LLM');
971
1147
  // Inject model self-switch info
@@ -1010,32 +1186,6 @@ When a user asks to "use Opus" or "switch to GPT-4o", identify the right model I
1010
1186
  if (command.context?.files?.length) {
1011
1187
  systemPrompt += '\n\nRelevant files: ' + command.context.files.join(', ');
1012
1188
  }
1013
- if (command.context?.stateContext?.summaryText) {
1014
- systemPrompt += '\n\n[Recent Operational State]\n' + command.context.stateContext.summaryText;
1015
- }
1016
- if (command.context?.workspaceManifest) {
1017
- const manifestLines = [];
1018
- const manifest = command.context.workspaceManifest;
1019
- if (manifest.projectRoot)
1020
- manifestLines.push(`- Project root: ${manifest.projectRoot}`);
1021
- if (manifest.likelyWorkingDirectory)
1022
- manifestLines.push(`- Likely working directory: ${manifest.likelyWorkingDirectory}`);
1023
- if (manifest.recentlyReadFiles?.length)
1024
- manifestLines.push(`- Recently read files: ${manifest.recentlyReadFiles.join(', ')}`);
1025
- if (manifest.recentlyWrittenFiles?.length)
1026
- manifestLines.push(`- Recently written files: ${manifest.recentlyWrittenFiles.join(', ')}`);
1027
- if (manifest.recentlyScannedDirectories?.length)
1028
- manifestLines.push(`- Recently scanned directories: ${manifest.recentlyScannedDirectories.join(', ')}`);
1029
- if (manifest.recentDownloads?.length)
1030
- manifestLines.push(`- Recent downloads: ${manifest.recentDownloads.join(', ')}`);
1031
- if (manifest.recentFailures?.length)
1032
- manifestLines.push(`- Recent failures: ${manifest.recentFailures.join(' | ')}`);
1033
- if (manifest.startHereHints?.length)
1034
- manifestLines.push(`- Start here: ${manifest.startHereHints.join(', ')}`);
1035
- if (manifestLines.length) {
1036
- systemPrompt += '\n\n[Workspace Manifest]\n' + manifestLines.join('\n');
1037
- }
1038
- }
1039
1189
  return systemPrompt;
1040
1190
  }
1041
1191
  async publishError(commandId, error) {