funolio-agent 1.0.47 → 1.0.49

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 (173) hide show
  1. package/dist/agent-config.d.ts +9 -1
  2. package/dist/agent-config.d.ts.map +1 -1
  3. package/dist/agent-config.js +4 -1
  4. package/dist/agent-config.js.map +1 -1
  5. package/dist/auth/auto-detect.d.ts +1 -0
  6. package/dist/auth/auto-detect.d.ts.map +1 -1
  7. package/dist/auth/auto-detect.js +16 -13
  8. package/dist/auth/auto-detect.js.map +1 -1
  9. package/dist/auto-organizer.d.ts.map +1 -1
  10. package/dist/auto-organizer.js +4 -3
  11. package/dist/auto-organizer.js.map +1 -1
  12. package/dist/backfill.d.ts.map +1 -1
  13. package/dist/backfill.js +3 -2
  14. package/dist/backfill.js.map +1 -1
  15. package/dist/bot-manager.d.ts +8 -23
  16. package/dist/bot-manager.d.ts.map +1 -1
  17. package/dist/bot-manager.js +61 -388
  18. package/dist/bot-manager.js.map +1 -1
  19. package/dist/clerk-model.d.ts +5 -1
  20. package/dist/clerk-model.d.ts.map +1 -1
  21. package/dist/clerk-model.js +40 -28
  22. package/dist/clerk-model.js.map +1 -1
  23. package/dist/cli-session-epoch.d.ts +10 -0
  24. package/dist/cli-session-epoch.d.ts.map +1 -0
  25. package/dist/cli-session-epoch.js +61 -0
  26. package/dist/cli-session-epoch.js.map +1 -0
  27. package/dist/commands/init.d.ts.map +1 -1
  28. package/dist/commands/init.js +30 -1
  29. package/dist/commands/init.js.map +1 -1
  30. package/dist/commands/pool.js +1 -1
  31. package/dist/commands/pool.js.map +1 -1
  32. package/dist/commands/setup.d.ts +37 -0
  33. package/dist/commands/setup.d.ts.map +1 -1
  34. package/dist/commands/setup.js +154 -43
  35. package/dist/commands/setup.js.map +1 -1
  36. package/dist/commands/start.d.ts.map +1 -1
  37. package/dist/commands/start.js +195 -164
  38. package/dist/commands/start.js.map +1 -1
  39. package/dist/config-cleanup.d.ts.map +1 -1
  40. package/dist/config-cleanup.js +2 -1
  41. package/dist/config-cleanup.js.map +1 -1
  42. package/dist/config.d.ts +6 -9
  43. package/dist/config.d.ts.map +1 -1
  44. package/dist/config.js +8 -30
  45. package/dist/config.js.map +1 -1
  46. package/dist/context-window.d.ts +33 -5
  47. package/dist/context-window.d.ts.map +1 -1
  48. package/dist/context-window.js +121 -20
  49. package/dist/context-window.js.map +1 -1
  50. package/dist/eval/orchestrator-front-door-replay.js +1 -1
  51. package/dist/eval/orchestrator-front-door-replay.js.map +1 -1
  52. package/dist/eval/policy-detection-replay.js +1 -1
  53. package/dist/eval/policy-detection-replay.js.map +1 -1
  54. package/dist/integration-tokens.d.ts +1 -6
  55. package/dist/integration-tokens.d.ts.map +1 -1
  56. package/dist/integration-tokens.js +38 -40
  57. package/dist/integration-tokens.js.map +1 -1
  58. package/dist/local-cli-pty-manager.d.ts +50 -0
  59. package/dist/local-cli-pty-manager.d.ts.map +1 -0
  60. package/dist/local-cli-pty-manager.js +645 -0
  61. package/dist/local-cli-pty-manager.js.map +1 -0
  62. package/dist/local-data.d.ts +30 -0
  63. package/dist/local-data.d.ts.map +1 -1
  64. package/dist/local-data.js +56 -1
  65. package/dist/local-data.js.map +1 -1
  66. package/dist/local-db.d.ts.map +1 -1
  67. package/dist/local-db.js +54 -1
  68. package/dist/local-db.js.map +1 -1
  69. package/dist/local-funnel.d.ts.map +1 -1
  70. package/dist/local-funnel.js +3 -2
  71. package/dist/local-funnel.js.map +1 -1
  72. package/dist/local-memory-search.d.ts +1 -0
  73. package/dist/local-memory-search.d.ts.map +1 -1
  74. package/dist/local-memory-search.js +101 -18
  75. package/dist/local-memory-search.js.map +1 -1
  76. package/dist/local-server.d.ts +0 -16
  77. package/dist/local-server.d.ts.map +1 -1
  78. package/dist/local-server.js +339 -287
  79. package/dist/local-server.js.map +1 -1
  80. package/dist/mcp/bridge-server.d.ts.map +1 -1
  81. package/dist/mcp/bridge-server.js +2 -1
  82. package/dist/mcp/bridge-server.js.map +1 -1
  83. package/dist/mcp/local-memory-server.d.ts +5 -0
  84. package/dist/mcp/local-memory-server.d.ts.map +1 -1
  85. package/dist/mcp/local-memory-server.js +15 -2
  86. package/dist/mcp/local-memory-server.js.map +1 -1
  87. package/dist/mcp/manager.d.ts +3 -22
  88. package/dist/mcp/manager.d.ts.map +1 -1
  89. package/dist/mcp/manager.js +66 -388
  90. package/dist/mcp/manager.js.map +1 -1
  91. package/dist/memory-extraction.d.ts +2 -0
  92. package/dist/memory-extraction.d.ts.map +1 -1
  93. package/dist/memory-extraction.js +3 -1
  94. package/dist/memory-extraction.js.map +1 -1
  95. package/dist/message-loop.d.ts +10 -6
  96. package/dist/message-loop.d.ts.map +1 -1
  97. package/dist/message-loop.js +241 -540
  98. package/dist/message-loop.js.map +1 -1
  99. package/dist/mqtt-client.d.ts +2 -31
  100. package/dist/mqtt-client.d.ts.map +1 -1
  101. package/dist/mqtt-client.js +2 -2
  102. package/dist/mqtt-client.js.map +1 -1
  103. package/dist/oauth.d.ts +6 -0
  104. package/dist/oauth.d.ts.map +1 -1
  105. package/dist/oauth.js +91 -0
  106. package/dist/oauth.js.map +1 -1
  107. package/dist/orchestration/front-door-policy.d.ts +5 -2
  108. package/dist/orchestration/front-door-policy.d.ts.map +1 -1
  109. package/dist/orchestration/front-door-policy.js +25 -28
  110. package/dist/orchestration/front-door-policy.js.map +1 -1
  111. package/dist/orchestration/orchestrator-blocked-prompt.js +1 -1
  112. package/dist/orchestration/orchestrator-final-response-prompt.js +1 -1
  113. package/dist/orchestration/orchestrator-operating-prompt.d.ts +11 -0
  114. package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
  115. package/dist/orchestration/orchestrator-operating-prompt.js +67 -44
  116. package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
  117. package/dist/orchestration/worker-operating-prompt.js +3 -3
  118. package/dist/orchestration/worker-operating-prompt.js.map +1 -1
  119. package/dist/orchestrator.d.ts +5 -1
  120. package/dist/orchestrator.d.ts.map +1 -1
  121. package/dist/orchestrator.js +141 -81
  122. package/dist/orchestrator.js.map +1 -1
  123. package/dist/prompt-template.js +3 -3
  124. package/dist/prompt-template.js.map +1 -1
  125. package/dist/providers/claude-cli-prompt.d.ts.map +1 -1
  126. package/dist/providers/claude-cli-prompt.js +22 -6
  127. package/dist/providers/claude-cli-prompt.js.map +1 -1
  128. package/dist/providers/claude-cli.d.ts.map +1 -1
  129. package/dist/providers/claude-cli.js +20 -2
  130. package/dist/providers/claude-cli.js.map +1 -1
  131. package/dist/providers/codex-cli.d.ts.map +1 -1
  132. package/dist/providers/codex-cli.js +71 -16
  133. package/dist/providers/codex-cli.js.map +1 -1
  134. package/dist/providers/index.d.ts +11 -0
  135. package/dist/providers/index.d.ts.map +1 -1
  136. package/dist/providers/index.js.map +1 -1
  137. package/dist/runtime-context.d.ts +10 -0
  138. package/dist/runtime-context.d.ts.map +1 -0
  139. package/dist/runtime-context.js +30 -0
  140. package/dist/runtime-context.js.map +1 -0
  141. package/dist/subagent/queue.d.ts.map +1 -1
  142. package/dist/subagent/queue.js +1 -0
  143. package/dist/subagent/queue.js.map +1 -1
  144. package/dist/summarization-pipeline.d.ts +1 -0
  145. package/dist/summarization-pipeline.d.ts.map +1 -1
  146. package/dist/summarization-pipeline.js +94 -25
  147. package/dist/summarization-pipeline.js.map +1 -1
  148. package/dist/tool-permissions.d.ts +2 -0
  149. package/dist/tool-permissions.d.ts.map +1 -0
  150. package/dist/tool-permissions.js +25 -0
  151. package/dist/tool-permissions.js.map +1 -0
  152. package/dist/tools/index.d.ts +7 -8
  153. package/dist/tools/index.d.ts.map +1 -1
  154. package/dist/tools/index.js +70 -60
  155. package/dist/tools/index.js.map +1 -1
  156. package/dist/tools/search-memory.d.ts.map +1 -1
  157. package/dist/tools/search-memory.js +9 -3
  158. package/dist/tools/search-memory.js.map +1 -1
  159. package/dist/tools/spawn-subagent.d.ts.map +1 -1
  160. package/dist/tools/spawn-subagent.js +1 -0
  161. package/dist/tools/spawn-subagent.js.map +1 -1
  162. package/dist/types.d.ts +3 -0
  163. package/dist/types.d.ts.map +1 -1
  164. package/dist/types.js +0 -3
  165. package/dist/types.js.map +1 -1
  166. package/dist/wizard-support.d.ts.map +1 -1
  167. package/dist/wizard-support.js +8 -6
  168. package/dist/wizard-support.js.map +1 -1
  169. package/dist/workflow-engine.d.ts +6 -2
  170. package/dist/workflow-engine.d.ts.map +1 -1
  171. package/dist/workflow-engine.js +254 -77
  172. package/dist/workflow-engine.js.map +1 -1
  173. package/package.json +2 -1
@@ -77,6 +77,8 @@ const orchestrator_profile_1 = require("./orchestrator-profile");
77
77
  const policy_detection_1 = require("./policy-detection");
78
78
  const server_runtime_1 = require("./server-runtime");
79
79
  const storage_mode_1 = require("./storage-mode");
80
+ const local_cli_pty_manager_1 = require("./local-cli-pty-manager");
81
+ const cli_session_epoch_1 = require("./cli-session-epoch");
80
82
  const server_adapter_1 = require("./server-adapter");
81
83
  const wizard_support_1 = require("./wizard-support");
82
84
  const chalk_1 = __importDefault(require("chalk"));
@@ -1828,7 +1830,7 @@ function startLocalServer(opts) {
1828
1830
  if (msgs.length < 3)
1829
1831
  return res.json({ suggestion: null, reason: 'Not enough messages' });
1830
1832
  const conv = data.getConversation(conversationId);
1831
- const clerk = (0, clerk_model_1.getClerk)();
1833
+ const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: 'local_desktop' });
1832
1834
  if (!clerk) {
1833
1835
  // Fallback: use first few words of first user message
1834
1836
  const firstUser = msgs.find(m => m.role === 'user');
@@ -2037,6 +2039,28 @@ function startLocalServer(opts) {
2037
2039
  res.status(500).json({ error: err.message });
2038
2040
  }
2039
2041
  });
2042
+ app.patch('/api/messages/:id', (req, res) => {
2043
+ try {
2044
+ if (isConnectedMode()) {
2045
+ return res.status(501).json({ error: 'Message updates are local-mode only' });
2046
+ }
2047
+ const updated = data.updateMessage(req.params.id, {
2048
+ content: req.body?.content,
2049
+ model: req.body?.model,
2050
+ botId: req.body?.botId,
2051
+ agentName: req.body?.agentName,
2052
+ resultArtifact: req.body?.resultArtifact,
2053
+ resultSummary: req.body?.resultSummary,
2054
+ resultStatus: req.body?.resultStatus,
2055
+ });
2056
+ if (!updated)
2057
+ return res.status(404).json({ error: 'Message not found' });
2058
+ res.json(updated);
2059
+ }
2060
+ catch (err) {
2061
+ res.status(500).json({ error: err.message });
2062
+ }
2063
+ });
2040
2064
  app.get('/api/conversations/:id/orchestration-audit', (req, res) => {
2041
2065
  try {
2042
2066
  if (isConnectedMode()) {
@@ -2327,7 +2351,13 @@ function startLocalServer(opts) {
2327
2351
  const limit = parseInt(req.query.limit, 10) || 25;
2328
2352
  const beforeSeq = req.query.beforeSeq ? parseInt(req.query.beforeSeq, 10) : 0;
2329
2353
  const rounds = req.query.rounds ? parseInt(req.query.rounds, 10) : 0;
2354
+ const startSeq = req.query.startSeq ? parseInt(req.query.startSeq, 10) : 0;
2355
+ const endSeq = req.query.endSeq ? parseInt(req.query.endSeq, 10) : 0;
2356
+ const hasDirectRange = startSeq > 0 && endSeq >= startSeq;
2330
2357
  if (isConnectedMode()) {
2358
+ if (hasDirectRange) {
2359
+ return res.status(400).json({ error: 'Direct message range fetch is only available in local storage mode' });
2360
+ }
2331
2361
  const runtime = (0, server_runtime_1.getRuntimeConnectionConfig)();
2332
2362
  const auth = await getHydratedDesktopAuth();
2333
2363
  const result = await (0, server_adapter_1.listServerConversationMessages)(auth, runtime, req.params.id, {
@@ -2336,6 +2366,9 @@ function startLocalServer(opts) {
2336
2366
  });
2337
2367
  return res.json(result.messages);
2338
2368
  }
2369
+ if (hasDirectRange) {
2370
+ return res.json(data.getMessagesInRange(req.params.id, startSeq, endSeq));
2371
+ }
2339
2372
  if (beforeSeq > 0) {
2340
2373
  // Backward paging: get N rounds or messages before given seq, returned in ASC order
2341
2374
  const msgs = rounds > 0
@@ -2552,10 +2585,13 @@ function startLocalServer(opts) {
2552
2585
  const activityErrorContext = {};
2553
2586
  const routeAbortController = new AbortController();
2554
2587
  let responseEnded = false;
2555
- const abortOnClientClose = () => routeAbortController.abort();
2588
+ const abortOnClientClose = () => {
2589
+ responseEnded = true;
2590
+ routeAbortController.abort();
2591
+ };
2556
2592
  req.on('close', abortOnClientClose);
2557
2593
  try {
2558
- const { conversationId, message, botId, skipUserMessage, pinnedMessageIds, topicId, projectId, workflowTemplateId, orchestrationEnabled, chatJobId, assistantMessageId, } = req.body;
2594
+ let { conversationId, message, botId, skipUserMessage, pinnedMessageIds, topicId, projectId, workflowTemplateId, orchestrationEnabled, chatJobId, assistantMessageId, persistAssistantPlaceholder, } = req.body;
2559
2595
  if (!message)
2560
2596
  return res.status(400).json({ error: 'message is required' });
2561
2597
  if (await relayConnectedChat(req, res)) {
@@ -2613,6 +2649,7 @@ function startLocalServer(opts) {
2613
2649
  const activityExpiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().replace('T', ' ').replace('Z', '');
2614
2650
  activityErrorContext.conversationId = convId;
2615
2651
  activityErrorContext.streamId = activityStreamId;
2652
+ activityErrorContext.messageId = assistantMessageId ? String(assistantMessageId) : null;
2616
2653
  activityErrorContext.botId = profile?.id ?? null;
2617
2654
  activityErrorContext.agentName = profile?.name ?? null;
2618
2655
  activityErrorContext.expiresAt = activityExpiresAt;
@@ -2620,6 +2657,7 @@ function startLocalServer(opts) {
2620
2657
  try {
2621
2658
  data.createMessageActivity({
2622
2659
  conversationId: convId,
2660
+ messageId: assistantMessageId ? String(assistantMessageId) : null,
2623
2661
  streamId: activityStreamId,
2624
2662
  botId: profile?.id ?? null,
2625
2663
  agentName: profile?.name ?? null,
@@ -2649,6 +2687,7 @@ function startLocalServer(opts) {
2649
2687
  try {
2650
2688
  data.createMessageActivity({
2651
2689
  conversationId: convId,
2690
+ messageId: assistantMessageId ? String(assistantMessageId) : null,
2652
2691
  streamId: activityStreamId,
2653
2692
  botId: resolveWorkerBotId(event.agentName) || null,
2654
2693
  agentName: event.agentName || null,
@@ -2671,7 +2710,7 @@ function startLocalServer(opts) {
2671
2710
  const effectiveProjectId = projectId ? String(projectId) : (convForPolicy?.project_id || undefined);
2672
2711
  const projectForPolicy = effectiveProjectId ? data.getProject(effectiveProjectId) : undefined;
2673
2712
  const currentPolicy = data.getEffectiveOrchestrationPolicy(effectiveProjectId);
2674
- const clerkForPolicy = (0, clerk_model_1.getClerk)();
2713
+ const clerkForPolicy = (0, clerk_model_1.getClerk)({ runtimeMode: 'local_desktop' });
2675
2714
  const agentNames = data.listAgentProfiles().map((agent) => agent.name);
2676
2715
  if (clerkForPolicy && savedUserMessage) {
2677
2716
  void (0, policy_detection_1.stagePolicyDetectionForMessage)({
@@ -2690,10 +2729,14 @@ function startLocalServer(opts) {
2690
2729
  });
2691
2730
  }
2692
2731
  }
2732
+ if (!assistantMessageId && persistAssistantPlaceholder === true) {
2733
+ const placeholder = data.addMessage(convId, 'assistant', '', buildConfiguredMessageModel(profile), undefined, profile?.id || undefined, profile?.name || undefined);
2734
+ assistantMessageId = placeholder.id;
2735
+ }
2693
2736
  // ─── Orchestrator Mode Branch ─────────────────────────
2694
2737
  const shouldUseOrchestratorMode = orchestrationEnabled !== false && (0, orchestrator_profile_1.isOrchestratorProfile)(profile);
2695
2738
  if (shouldUseOrchestratorMode) {
2696
- const clerk = (0, clerk_model_1.getClerk)();
2739
+ const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: 'local_desktop' });
2697
2740
  if (!clerk) {
2698
2741
  // Fix #2: Do not silently fall through to direct chat — return a clear error
2699
2742
  return res.status(400).json({
@@ -2702,7 +2745,7 @@ function startLocalServer(opts) {
2702
2745
  }
2703
2746
  const { OrchestratorAgent } = require('./orchestrator');
2704
2747
  const { getWorkflowEngine } = require('./workflow-engine');
2705
- const workflowEngine = getWorkflowEngine(opts.projectDir);
2748
+ const workflowEngine = getWorkflowEngine(opts.projectDir, 'local_desktop');
2706
2749
  const orchestrator = new OrchestratorAgent(clerk, workflowEngine);
2707
2750
  // Resolve effective project ID from request or existing conversation
2708
2751
  const conv = data.getConversation(convId);
@@ -2732,7 +2775,7 @@ function startLocalServer(opts) {
2732
2775
  orchestratorRuntime.model || profile.model || '',
2733
2776
  runtimeModeLabel(orchestratorRuntime.runtimeMode, orchestratorRuntime.runtimeSource),
2734
2777
  ].filter(Boolean).join(' | ');
2735
- orchestratorRuntimePayload = runtimePayloadForDisplay(profile.provider, orchestratorRuntime.model || profile.model || null, orchestratorRuntime.runtimeMode, orchestratorRuntime.runtimeSource || null, false);
2778
+ orchestratorRuntimePayload = runtimePayloadForDisplay(profile.provider, orchestratorRuntime.model || profile.model || null, orchestratorRuntime.runtimeMode, orchestratorRuntime.runtimeSource || null);
2736
2779
  }
2737
2780
  catch {
2738
2781
  orchestratorRuntimeLabel = buildConfiguredMessageModel(profile);
@@ -2992,7 +3035,7 @@ function startLocalServer(opts) {
2992
3035
  ? configuredTz
2993
3036
  : Intl.DateTimeFormat().resolvedOptions().timeZone;
2994
3037
  const unrestrictedCliProfile = index_1.CLI_PROVIDERS.has(profile.provider);
2995
- const allToolDefs = (0, index_2.getAllToolDefinitions)(mcpManager);
3038
+ const allToolDefs = (0, index_2.getAllToolDefinitions)('local_desktop', mcpManager);
2996
3039
  const configuredBuiltinTools = parseToolSelectionJson(profile.enabled_builtin_tools_json);
2997
3040
  const configuredMcpTools = parseToolSelectionJson(profile.enabled_mcp_tools_json);
2998
3041
  const allowedToolNames = unrestrictedCliProfile
@@ -3002,7 +3045,7 @@ function startLocalServer(opts) {
3002
3045
  // Build system prompt via clerk (token-budgeted context injection)
3003
3046
  let systemPrompt;
3004
3047
  let llmSpawnCwd = opts.projectDir;
3005
- const clerk = (0, clerk_model_1.getClerk)();
3048
+ const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: 'local_desktop' });
3006
3049
  if (clerk) {
3007
3050
  const conv = data.getConversation(convId);
3008
3051
  const topicTitle = topicId ? data.getTopic(topicId)?.title : undefined;
@@ -3028,9 +3071,10 @@ function startLocalServer(opts) {
3028
3071
  else {
3029
3072
  // Fallback: manual prompt building
3030
3073
  systemPrompt = '[Bot Identity]\n' + (profile.soul_md
3031
- || 'You are a Funolio AI agent running locally. You have access to project files and can execute code.');
3074
+ || 'You are an AI assistant running locally. You have access to project files and can execute code.');
3032
3075
  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.';
3033
3076
  systemPrompt += '\n\nWhen [Project Overview] is present, treat Project/Topic/Workspace values there as authoritative for the current turn and override stale prior-chat claims.';
3077
+ systemPrompt += '\n\n[Response Style]\nWrite in short readable paragraphs. Put a blank line between distinct ideas. Use bullets when listing findings, steps, or issues. Do not return one dense wall of text. For progress updates, keep them compact and clearly separate what you checked, what you found, and what you are doing next.';
3034
3078
  const convForFallback = data.getConversation(convId);
3035
3079
  const projectForFallback = convForFallback?.project_id ? data.getProject(convForFallback.project_id) : undefined;
3036
3080
  const workspaceForFallback = projectForFallback?.folder?.trim();
@@ -3101,18 +3145,25 @@ function startLocalServer(opts) {
3101
3145
  let activeRuntimeMode = runtime.runtimeMode;
3102
3146
  let activeRuntimeSource = runtime.runtimeSource;
3103
3147
  let activeIsCliProvider = index_1.CLI_PROVIDERS.has(activeProviderName);
3104
- const cliFallback = runtime.cliFallback;
3105
- const apiKeyFallback = runtime.apiKeyFallback;
3106
- let switchedToCliFallback = false;
3107
- let switchedToApiKeyFallback = false;
3108
3148
  const runtimePayload = () => ({
3109
3149
  mode: activeRuntimeMode,
3110
3150
  modeLabel: runtimeModeLabel(activeRuntimeMode, activeRuntimeSource),
3111
3151
  provider: activeProviderName,
3112
3152
  model: activeModelName || null,
3113
3153
  source: activeRuntimeSource || null,
3114
- fallbackUsed: switchedToCliFallback || switchedToApiKeyFallback,
3115
3154
  });
3155
+ const enableCliSessionEpoch = activeIsCliProvider
3156
+ && !shouldUseOrchestratorMode
3157
+ && !workflowTemplateId
3158
+ && !!convId
3159
+ && !!profile?.id;
3160
+ const cliSessionEpochPlan = enableCliSessionEpoch
3161
+ ? (0, cli_session_epoch_1.selectCliSessionEpoch)(convId, profile.id, activeProviderName)
3162
+ : { existing: undefined, resumeSessionId: null, resetReason: null };
3163
+ let activeCliSessionId = cliSessionEpochPlan.resumeSessionId;
3164
+ const cliEpochStartedAt = cliSessionEpochPlan.resumeSessionId
3165
+ ? (cliSessionEpochPlan.existing?.epoch_started_at || localTimestamp())
3166
+ : localTimestamp();
3116
3167
  if (!activeApiKey) {
3117
3168
  return res.status(400).json({ error: `No API key for provider ${profile.provider}. Configure one in Settings.` });
3118
3169
  }
@@ -3120,6 +3171,7 @@ function startLocalServer(opts) {
3120
3171
  projectId: convId ? (data.getConversation(convId)?.project_id ?? null) : null,
3121
3172
  actorType: 'llm',
3122
3173
  actorId: profile?.name || profile?.id || 'LLM',
3174
+ runtimeMode: 'local_desktop',
3123
3175
  restrictFileAccessToProject: unrestrictedCliProfile ? false : undefined,
3124
3176
  abortSignal: routeAbortController.signal,
3125
3177
  });
@@ -3180,6 +3232,7 @@ function startLocalServer(opts) {
3180
3232
  sendEvent('meta', {
3181
3233
  conversationId: convId,
3182
3234
  botId: profile.id,
3235
+ assistantMessageId: assistantMessageId || null,
3183
3236
  runtime: runtimePayload(),
3184
3237
  tokenUsage: {
3185
3238
  approxInputTokens,
@@ -3197,6 +3250,23 @@ function startLocalServer(opts) {
3197
3250
  detail: `Sending request to ${activeProviderName}...`,
3198
3251
  runtime: runtimePayload(),
3199
3252
  }, `Sending request to ${activeProviderName}...`);
3253
+ if (cliSessionEpochPlan.resetReason && enableCliSessionEpoch) {
3254
+ const resetDetail = cliSessionEpochPlan.resetReason === 'turn_limit'
3255
+ ? 'Resetting CLI session after reaching the turn limit.'
3256
+ : cliSessionEpochPlan.resetReason === 'token_limit'
3257
+ ? 'Resetting CLI session after reaching the context budget.'
3258
+ : 'Resetting CLI session because the runtime changed.';
3259
+ sendEvent('status', {
3260
+ phase: 'thinking',
3261
+ detail: resetDetail,
3262
+ runtime: runtimePayload(),
3263
+ });
3264
+ recordActivity('status', {
3265
+ phase: 'thinking',
3266
+ detail: resetDetail,
3267
+ runtime: runtimePayload(),
3268
+ }, resetDetail);
3269
+ }
3200
3270
  let partialPersistedContent = '';
3201
3271
  let partialPersistedAt = 0;
3202
3272
  const throwIfChatJobCancelled = () => {
@@ -3245,265 +3315,259 @@ function startLocalServer(opts) {
3245
3315
  // Thinking/reasoning accumulator across multi-turn tool loops
3246
3316
  let accumulatedThinking = '';
3247
3317
  const thinkingEnabled = !!profile?.show_thinking;
3248
- while (iteration < MAX_ITERATIONS) {
3249
- iteration++;
3250
- let iterationFirstChunk = true;
3251
- throwIfChatJobCancelled();
3252
- if (iteration > 1) {
3253
- sendEvent('status', { phase: 'thinking', detail: 'Processing tool results...' });
3254
- recordActivity('status', { phase: 'thinking', detail: 'Processing tool results...' }, 'Processing tool results...');
3255
- }
3256
- let response;
3257
- const chatOptions = {
3258
- messages: llmMessages,
3259
- system: systemPrompt,
3260
- stream: true,
3261
- tools: toolDefs,
3262
- cwd: llmSpawnCwd,
3263
- abortSignal: routeAbortController.signal,
3264
- thinkingEnabled,
3265
- onChunk: async (chunk) => {
3266
- throwIfChatJobCancelled();
3267
- if (iterationFirstChunk) {
3268
- iterationFirstChunk = false;
3269
- sendEvent('status', { phase: 'generating' });
3270
- recordActivity('status', { phase: 'generating' }, 'Generating response...');
3318
+ let useInteractiveCliSession = enableCliSessionEpoch;
3319
+ if (useInteractiveCliSession) {
3320
+ const ptyManager = (0, local_cli_pty_manager_1.getLocalCliPtySessionManager)();
3321
+ let ptyAttempt = 0;
3322
+ while (true) {
3323
+ ptyAttempt++;
3324
+ try {
3325
+ const result = await ptyManager.runTurn({
3326
+ conversationId: convId,
3327
+ botId: profile.id,
3328
+ provider: activeProviderName,
3329
+ cwd: llmSpawnCwd,
3330
+ systemPrompt,
3331
+ messages: llmMessages,
3332
+ forceFreshSession: !cliSessionEpochPlan.resumeSessionId,
3333
+ onDetail: async (detail) => {
3334
+ sendEvent('status', {
3335
+ phase: 'thinking',
3336
+ detail,
3337
+ runtime: runtimePayload(),
3338
+ });
3339
+ recordActivity('status', {
3340
+ phase: 'thinking',
3341
+ detail,
3342
+ runtime: runtimePayload(),
3343
+ }, detail);
3344
+ },
3345
+ });
3346
+ if (result.sessionId) {
3347
+ activeCliSessionId = result.sessionId;
3271
3348
  }
3272
- streamedAnyChunk = true;
3273
- streamedContent += chunk;
3274
- persistAssistantPartial(false);
3275
- sendEvent('chunk', { text: chunk });
3276
- },
3277
- ...(thinkingEnabled ? {
3278
- onThinkingChunk: async (chunk) => {
3279
- sendEvent('thinking_chunk', {
3280
- text: chunk,
3281
- botId: profile?.id || null,
3282
- agentName: profile?.name || null,
3283
- });
3284
- },
3285
- } : {}),
3286
- };
3287
- try {
3288
- response = await activeLlm.chat(chatOptions);
3289
- }
3290
- catch (primaryErr) {
3291
- if (routeAbortController.signal.aborted || primaryErr?.name === 'AbortError') {
3292
- throw primaryErr;
3349
+ if (result.usage) {
3350
+ totalInputTokens += result.usage.inputTokens || 0;
3351
+ totalOutputTokens += result.usage.outputTokens || 0;
3352
+ hasExactUsage = true;
3353
+ }
3354
+ fullContent = (result.content || '').trim();
3355
+ break;
3293
3356
  }
3294
- if (cliFallback && !switchedToCliFallback) {
3295
- switchedToCliFallback = true;
3296
- activeProviderName = cliFallback.providerName;
3297
- activeModelName = cliFallback.model;
3298
- activeApiKey = cliFallback.apiKey;
3299
- activeLlm = cliFallback.llm;
3300
- activeRuntimeMode = cliFallback.runtimeMode;
3301
- activeRuntimeSource = cliFallback.runtimeSource;
3302
- activeIsCliProvider = true;
3303
- const fallbackMsg = activeIsCliProvider
3304
- ? `CLI auth failed (${primaryErr?.message || primaryErr}); switching to fallback...`
3305
- : `Primary provider failed (${primaryErr?.message || primaryErr}); switching to fallback...`;
3306
- console.warn(chalk_1.default.yellow(` [chat] ${fallbackMsg}`));
3307
- if (activeIsCliProvider) {
3308
- console.warn(chalk_1.default.yellow(` [chat] If CLI auth keeps failing, run 'claude' or 'codex' in your terminal to re-authenticate.`));
3357
+ catch (ptyErr) {
3358
+ if (ptyAttempt >= LOCAL_RUNTIME_RETRY_LIMIT || !shouldRetrySelectedLocalRuntime(ptyErr)) {
3359
+ throw ptyErr;
3309
3360
  }
3361
+ const retryDetail = `Selected runtime failed (${ptyErr?.message || ptyErr}); retrying the same connection (${ptyAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`;
3362
+ console.warn(chalk_1.default.yellow(` [chat] ${retryDetail}`));
3310
3363
  sendEvent('status', {
3311
3364
  phase: 'thinking',
3312
- detail: fallbackMsg,
3365
+ detail: retryDetail,
3313
3366
  runtime: runtimePayload(),
3314
3367
  });
3315
3368
  recordActivity('status', {
3316
3369
  phase: 'thinking',
3317
- detail: fallbackMsg,
3370
+ detail: retryDetail,
3318
3371
  runtime: runtimePayload(),
3319
- }, fallbackMsg);
3320
- sendEvent('status', {
3321
- phase: 'thinking',
3322
- detail: `Sending request to ${activeProviderName}...`,
3323
- runtime: runtimePayload(),
3324
- });
3325
- recordActivity('status', {
3326
- phase: 'thinking',
3327
- detail: `Sending request to ${activeProviderName}...`,
3328
- runtime: runtimePayload(),
3329
- }, `Sending request to ${activeProviderName}...`);
3372
+ }, retryDetail);
3373
+ await pauseLocalRuntimeRetry(ptyAttempt);
3374
+ }
3375
+ }
3376
+ }
3377
+ if (!useInteractiveCliSession)
3378
+ while (iteration < MAX_ITERATIONS) {
3379
+ iteration++;
3380
+ let iterationFirstChunk = true;
3381
+ throwIfChatJobCancelled();
3382
+ if (iteration > 1) {
3383
+ sendEvent('status', { phase: 'thinking', detail: 'Processing tool results...' });
3384
+ recordActivity('status', { phase: 'thinking', detail: 'Processing tool results...' }, 'Processing tool results...');
3385
+ }
3386
+ let response;
3387
+ const chatOptions = {
3388
+ messages: llmMessages,
3389
+ system: systemPrompt,
3390
+ stream: true,
3391
+ tools: toolDefs,
3392
+ cwd: llmSpawnCwd,
3393
+ abortSignal: routeAbortController.signal,
3394
+ resumeSessionId: enableCliSessionEpoch ? activeCliSessionId : null,
3395
+ persistSession: enableCliSessionEpoch,
3396
+ thinkingEnabled,
3397
+ onChunk: async (chunk) => {
3398
+ throwIfChatJobCancelled();
3399
+ if (iterationFirstChunk) {
3400
+ iterationFirstChunk = false;
3401
+ sendEvent('status', { phase: 'generating' });
3402
+ recordActivity('status', { phase: 'generating' }, 'Generating response...');
3403
+ }
3404
+ streamedAnyChunk = true;
3405
+ streamedContent += chunk;
3406
+ persistAssistantPartial(false);
3407
+ sendEvent('chunk', { text: chunk });
3408
+ },
3409
+ ...(thinkingEnabled ? {
3410
+ onThinkingChunk: async (chunk) => {
3411
+ sendEvent('thinking_chunk', {
3412
+ text: chunk,
3413
+ botId: profile?.id || null,
3414
+ agentName: profile?.name || null,
3415
+ });
3416
+ },
3417
+ } : {}),
3418
+ };
3419
+ let chatAttempt = 0;
3420
+ while (true) {
3421
+ chatAttempt++;
3330
3422
  try {
3331
3423
  response = await activeLlm.chat(chatOptions);
3424
+ break;
3332
3425
  }
3333
- catch (cliErr) {
3334
- if (!apiKeyFallback || switchedToApiKeyFallback)
3335
- throw cliErr;
3336
- switchedToApiKeyFallback = true;
3337
- activeProviderName = apiKeyFallback.providerName;
3338
- activeModelName = apiKeyFallback.model;
3339
- activeApiKey = apiKeyFallback.apiKey;
3340
- activeLlm = apiKeyFallback.llm;
3341
- activeRuntimeMode = apiKeyFallback.runtimeMode;
3342
- activeRuntimeSource = apiKeyFallback.runtimeSource;
3343
- activeIsCliProvider = false;
3344
- console.warn(chalk_1.default.yellow(` [chat] CLI fallback failed (${cliErr?.message || cliErr}); switching to API key fallback (${activeProviderName})`));
3345
- sendEvent('status', {
3346
- phase: 'thinking',
3347
- detail: 'CLI fallback unavailable; switching to API key fallback...',
3348
- runtime: runtimePayload(),
3349
- });
3350
- recordActivity('status', {
3351
- phase: 'thinking',
3352
- detail: 'CLI fallback unavailable; switching to API key fallback...',
3353
- runtime: runtimePayload(),
3354
- }, 'CLI fallback unavailable; switching to API key fallback...');
3426
+ catch (primaryErr) {
3427
+ if (routeAbortController.signal.aborted || primaryErr?.name === 'AbortError') {
3428
+ throw primaryErr;
3429
+ }
3430
+ if (chatAttempt >= LOCAL_RUNTIME_RETRY_LIMIT || !shouldRetrySelectedLocalRuntime(primaryErr)) {
3431
+ throw primaryErr;
3432
+ }
3433
+ const retryDetail = `Selected runtime failed (${primaryErr?.message || primaryErr}); retrying the same connection (${chatAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`;
3434
+ console.warn(chalk_1.default.yellow(` [chat] ${retryDetail}`));
3355
3435
  sendEvent('status', {
3356
3436
  phase: 'thinking',
3357
- detail: `Sending request to ${activeProviderName}...`,
3437
+ detail: retryDetail,
3358
3438
  runtime: runtimePayload(),
3359
3439
  });
3360
3440
  recordActivity('status', {
3361
3441
  phase: 'thinking',
3362
- detail: `Sending request to ${activeProviderName}...`,
3442
+ detail: retryDetail,
3363
3443
  runtime: runtimePayload(),
3364
- }, `Sending request to ${activeProviderName}...`);
3365
- response = await activeLlm.chat(chatOptions);
3444
+ }, retryDetail);
3445
+ await pauseLocalRuntimeRetry(chatAttempt);
3366
3446
  }
3367
3447
  }
3368
- else if (apiKeyFallback && !switchedToApiKeyFallback) {
3369
- switchedToApiKeyFallback = true;
3370
- activeProviderName = apiKeyFallback.providerName;
3371
- activeModelName = apiKeyFallback.model;
3372
- activeApiKey = apiKeyFallback.apiKey;
3373
- activeLlm = apiKeyFallback.llm;
3374
- activeRuntimeMode = apiKeyFallback.runtimeMode;
3375
- activeRuntimeSource = apiKeyFallback.runtimeSource;
3376
- activeIsCliProvider = false;
3377
- console.warn(chalk_1.default.yellow(` [chat] Runtime failed (${primaryErr?.message || primaryErr}); switching to API key fallback (${activeProviderName})`));
3378
- sendEvent('status', {
3379
- phase: 'thinking',
3380
- detail: 'Switching to API key fallback...',
3381
- runtime: runtimePayload(),
3382
- });
3383
- recordActivity('status', {
3384
- phase: 'thinking',
3385
- detail: 'Switching to API key fallback...',
3386
- runtime: runtimePayload(),
3387
- }, 'Switching to API key fallback...');
3448
+ if (enableCliSessionEpoch && response?.session?.id) {
3449
+ activeCliSessionId = response.session.id;
3450
+ }
3451
+ throwIfChatJobCancelled();
3452
+ const authFailure = detectInteractiveAuthFailure(response?.content || '', activeProviderName, profile.provider);
3453
+ if (authFailure && (!response?.toolCalls || response.toolCalls.length === 0)) {
3388
3454
  sendEvent('status', {
3389
- phase: 'thinking',
3390
- detail: `Sending request to ${activeProviderName}...`,
3455
+ phase: 'auth_required',
3456
+ detail: authFailure.detail,
3391
3457
  runtime: runtimePayload(),
3458
+ auth: authFailure,
3392
3459
  });
3393
3460
  recordActivity('status', {
3394
- phase: 'thinking',
3395
- detail: `Sending request to ${activeProviderName}...`,
3461
+ phase: 'auth_required',
3462
+ detail: authFailure.detail,
3396
3463
  runtime: runtimePayload(),
3397
- }, `Sending request to ${activeProviderName}...`);
3398
- response = await activeLlm.chat(chatOptions);
3464
+ auth: authFailure,
3465
+ }, authFailure.detail);
3466
+ const authErr = new Error(authFailure.message);
3467
+ authErr.authRequired = true;
3468
+ authErr.providerId = authFailure.providerId;
3469
+ authErr.cli = authFailure.cli;
3470
+ throw authErr;
3399
3471
  }
3400
- else {
3401
- throw primaryErr;
3472
+ if (response.usage) {
3473
+ totalInputTokens += response.usage.inputTokens || 0;
3474
+ totalOutputTokens += response.usage.outputTokens || 0;
3475
+ hasExactUsage = true;
3402
3476
  }
3403
- }
3404
- throwIfChatJobCancelled();
3405
- const authFailure = detectInteractiveAuthFailure(response?.content || '', activeProviderName, profile.provider);
3406
- if (authFailure && (!response?.toolCalls || response.toolCalls.length === 0)) {
3407
- sendEvent('status', {
3408
- phase: 'auth_required',
3409
- detail: authFailure.detail,
3410
- runtime: runtimePayload(),
3411
- auth: authFailure,
3412
- });
3413
- recordActivity('status', {
3414
- phase: 'auth_required',
3415
- detail: authFailure.detail,
3416
- runtime: runtimePayload(),
3417
- auth: authFailure,
3418
- }, authFailure.detail);
3419
- const authErr = new Error(authFailure.message);
3420
- authErr.authRequired = true;
3421
- authErr.providerId = authFailure.providerId;
3422
- authErr.cli = authFailure.cli;
3423
- throw authErr;
3424
- }
3425
- if (response.usage) {
3426
- totalInputTokens += response.usage.inputTokens || 0;
3427
- totalOutputTokens += response.usage.outputTokens || 0;
3428
- hasExactUsage = true;
3429
- }
3430
- // Accumulate thinking/reasoning across multi-turn tool loops
3431
- if (response.thinking) {
3432
- accumulatedThinking += (accumulatedThinking ? '\n---\n' : '') + response.thinking;
3433
- }
3434
- if (response.toolCalls && response.toolCalls.length > 0) {
3435
- llmMessages.push({
3436
- role: 'assistant',
3437
- content: response.content || '',
3438
- toolCalls: response.toolCalls,
3439
- });
3440
- for (const tc of response.toolCalls) {
3441
- throwIfChatJobCancelled();
3442
- sendEvent('status', { phase: 'calling_tool', detail: `Running ${tc.name}...`, toolName: tc.name });
3443
- sendEvent('tool_call', { id: tc.id, name: tc.name, arguments: tc.arguments });
3444
- recordActivity('tool_call', { id: tc.id, name: tc.name, arguments: tc.arguments }, `Tool call: ${tc.name}`);
3445
- if (!allowedToolNames.has(tc.name)) {
3446
- const errMsg = `TOOL_DISABLED: ${tc.name} is not enabled for this bot.`;
3447
- sendEvent('tool_result', { callId: tc.id, output: errMsg, isError: true });
3448
- recordActivity('tool_result', { callId: tc.id, output: errMsg, isError: true }, `Tool failed: ${tc.name}`);
3449
- llmMessages.push({ role: 'tool', content: errMsg, toolCallId: tc.id, toolName: tc.name });
3450
- continue;
3451
- }
3452
- const approval = unrestrictedCliProfile
3453
- ? { approved: true }
3454
- : (0, approval_1.checkPermission)(tc.name, (profile.permission_mode || 'autopilot'));
3455
- if (!approval.approved) {
3456
- const errMsg = `PERMISSION_DENIED: ${approval.reason}`;
3457
- sendEvent('tool_result', { callId: tc.id, output: errMsg, isError: true });
3458
- recordActivity('tool_result', { callId: tc.id, output: errMsg, isError: true }, `Tool denied: ${tc.name}`);
3459
- llmMessages.push({ role: 'tool', content: errMsg, toolCallId: tc.id, toolName: tc.name });
3460
- continue;
3461
- }
3462
- let result;
3463
- try {
3464
- const raw = await (0, index_2.executeToolWithMCP)({ id: tc.id, name: tc.name, arguments: tc.arguments }, toolCtx, mcpManager);
3465
- const verified = await (0, index_2.verifyToolResult)(raw, tc.arguments, toolCtx);
3466
- result = {
3467
- success: verified.success,
3468
- output: verified.output,
3469
- error: verified.error,
3470
- };
3471
- }
3472
- catch (toolErr) {
3473
- result = { success: false, output: '', error: toolErr.message || 'Tool execution failed' };
3477
+ // Accumulate thinking/reasoning across multi-turn tool loops
3478
+ if (response.thinking) {
3479
+ accumulatedThinking += (accumulatedThinking ? '\n---\n' : '') + response.thinking;
3480
+ }
3481
+ if (response.toolCalls && response.toolCalls.length > 0) {
3482
+ llmMessages.push({
3483
+ role: 'assistant',
3484
+ content: response.content || '',
3485
+ toolCalls: response.toolCalls,
3486
+ });
3487
+ for (const tc of response.toolCalls) {
3488
+ throwIfChatJobCancelled();
3489
+ sendEvent('status', { phase: 'calling_tool', detail: `Running ${tc.name}...`, toolName: tc.name });
3490
+ sendEvent('tool_call', { id: tc.id, name: tc.name, arguments: tc.arguments });
3491
+ recordActivity('tool_call', { id: tc.id, name: tc.name, arguments: tc.arguments }, `Tool call: ${tc.name}`);
3492
+ if (!allowedToolNames.has(tc.name)) {
3493
+ const errMsg = `TOOL_DISABLED: ${tc.name} is not enabled for this bot.`;
3494
+ sendEvent('tool_result', { callId: tc.id, output: errMsg, isError: true });
3495
+ recordActivity('tool_result', { callId: tc.id, output: errMsg, isError: true }, `Tool failed: ${tc.name}`);
3496
+ llmMessages.push({ role: 'tool', content: errMsg, toolCallId: tc.id, toolName: tc.name });
3497
+ continue;
3498
+ }
3499
+ const approval = unrestrictedCliProfile
3500
+ ? { approved: true }
3501
+ : (0, approval_1.checkPermission)(tc.name, (profile.permission_mode || 'autopilot'));
3502
+ if (!approval.approved) {
3503
+ const errMsg = `PERMISSION_DENIED: ${approval.reason}`;
3504
+ sendEvent('tool_result', { callId: tc.id, output: errMsg, isError: true });
3505
+ recordActivity('tool_result', { callId: tc.id, output: errMsg, isError: true }, `Tool denied: ${tc.name}`);
3506
+ llmMessages.push({ role: 'tool', content: errMsg, toolCallId: tc.id, toolName: tc.name });
3507
+ continue;
3508
+ }
3509
+ let result;
3510
+ try {
3511
+ const raw = await (0, index_2.executeToolWithMCP)({ id: tc.id, name: tc.name, arguments: tc.arguments }, toolCtx, mcpManager);
3512
+ const verified = await (0, index_2.verifyToolResult)(raw, tc.arguments, toolCtx);
3513
+ result = {
3514
+ success: verified.success,
3515
+ output: verified.output,
3516
+ error: verified.error,
3517
+ };
3518
+ }
3519
+ catch (toolErr) {
3520
+ result = { success: false, output: '', error: toolErr.message || 'Tool execution failed' };
3521
+ }
3522
+ const output = result.success ? result.output : `ERROR: ${result.error || 'Unknown error'}`;
3523
+ sendEvent('tool_result', { callId: tc.id, output, isError: !result.success });
3524
+ recordActivity('tool_result', {
3525
+ callId: tc.id,
3526
+ output,
3527
+ isError: !result.success,
3528
+ }, `${result.success ? 'Tool completed' : 'Tool failed'}: ${tc.name}`);
3529
+ llmMessages.push({ role: 'tool', content: output, toolCallId: tc.id, toolName: tc.name });
3474
3530
  }
3475
- const output = result.success ? result.output : `ERROR: ${result.error || 'Unknown error'}`;
3476
- sendEvent('tool_result', { callId: tc.id, output, isError: !result.success });
3477
- recordActivity('tool_result', {
3478
- callId: tc.id,
3479
- output,
3480
- isError: !result.success,
3481
- }, `${result.success ? 'Tool completed' : 'Tool failed'}: ${tc.name}`);
3482
- llmMessages.push({ role: 'tool', content: output, toolCallId: tc.id, toolName: tc.name });
3531
+ continue;
3483
3532
  }
3484
- continue;
3485
- }
3486
- // Final response (guard against defer-only filler)
3487
- const candidate = (response.content || '').trim();
3488
- if (!forcedFinalizationPass && (0, response_guard_1.isLikelyDeferredReply)(candidate)) {
3489
- forcedFinalizationPass = true;
3490
- llmMessages.push({ role: 'assistant', content: candidate });
3491
- llmMessages.push({
3492
- role: 'user',
3493
- content: 'Provide the final answer now. Do not say you will check later. Either provide concrete results or explicitly say what is unavailable.',
3494
- });
3495
- sendEvent('status', { phase: 'thinking', detail: 'Finalizing response...' });
3496
- recordActivity('status', { phase: 'thinking', detail: 'Finalizing response...' }, 'Finalizing response...');
3497
- continue;
3533
+ // Final response (guard against defer-only filler)
3534
+ const candidate = (response.content || '').trim();
3535
+ if (!forcedFinalizationPass && (0, response_guard_1.isLikelyDeferredReply)(candidate)) {
3536
+ forcedFinalizationPass = true;
3537
+ llmMessages.push({ role: 'assistant', content: candidate });
3538
+ llmMessages.push({
3539
+ role: 'user',
3540
+ content: 'Provide the final answer now. Do not say you will check later. Either provide concrete results or explicitly say what is unavailable.',
3541
+ });
3542
+ sendEvent('status', { phase: 'thinking', detail: 'Finalizing response...' });
3543
+ recordActivity('status', { phase: 'thinking', detail: 'Finalizing response...' }, 'Finalizing response...');
3544
+ continue;
3545
+ }
3546
+ fullContent = candidate;
3547
+ break;
3498
3548
  }
3499
- fullContent = candidate;
3500
- break;
3501
- }
3502
3549
  const persistedContent = fullContent || streamedContent.trim();
3503
3550
  if (!persistedContent) {
3504
3551
  throw new Error('Assistant returned no final response');
3505
3552
  }
3506
3553
  persistAssistantPartial(true);
3554
+ if (enableCliSessionEpoch && activeCliSessionId) {
3555
+ const nextEpochTurnCount = cliSessionEpochPlan.resumeSessionId
3556
+ ? ((cliSessionEpochPlan.existing?.epoch_turn_count || 0) + 1)
3557
+ : 1;
3558
+ data.upsertCliSessionEpoch({
3559
+ conversationId: convId,
3560
+ botId: profile.id,
3561
+ provider: activeProviderName,
3562
+ sessionId: activeCliSessionId,
3563
+ epochTurnCount: nextEpochTurnCount,
3564
+ lastInputTokens: hasExactUsage ? totalInputTokens : approxInputTokens,
3565
+ lastOutputTokens: hasExactUsage ? totalOutputTokens : 0,
3566
+ resetReason: cliSessionEpochPlan.resetReason,
3567
+ epochStartedAt: cliEpochStartedAt,
3568
+ lastUsedAt: localTimestamp(),
3569
+ });
3570
+ }
3507
3571
  // Emit thinking_done event if we accumulated any thinking
3508
3572
  if (accumulatedThinking) {
3509
3573
  sendEvent('thinking_done', {
@@ -3587,6 +3651,7 @@ function startLocalServer(opts) {
3587
3651
  if (activityErrorContext.conversationId) {
3588
3652
  data.createMessageActivity({
3589
3653
  conversationId: activityErrorContext.conversationId,
3654
+ messageId: activityErrorContext.messageId ?? null,
3590
3655
  streamId: activityErrorContext.streamId ?? null,
3591
3656
  botId: activityErrorContext.botId ?? null,
3592
3657
  agentName: activityErrorContext.agentName ?? null,
@@ -3777,7 +3842,7 @@ function startLocalServer(opts) {
3777
3842
  const { prompt } = req.body;
3778
3843
  if (!prompt)
3779
3844
  return res.status(400).json({ error: 'prompt is required' });
3780
- const clerk = (0, clerk_model_1.getClerk)();
3845
+ const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: 'local_desktop' });
3781
3846
  if (!clerk)
3782
3847
  return res.json({ routing: 'default', reason: 'No clerk configured' });
3783
3848
  const agents = data.listAgentProfiles();
@@ -3794,7 +3859,7 @@ function startLocalServer(opts) {
3794
3859
  const { prompt, conversationId, agentId, pinnedMessageIds } = req.body;
3795
3860
  if (!prompt)
3796
3861
  return res.status(400).json({ error: 'prompt is required' });
3797
- const engine = (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir);
3862
+ const engine = (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir, 'local_desktop');
3798
3863
  const profile = agentId
3799
3864
  ? data.getAgentProfile(agentId)
3800
3865
  : data.getDefaultAgentProfile();
@@ -3812,6 +3877,7 @@ function startLocalServer(opts) {
3812
3877
  const result = await engine.execute(prompt, conversationId || null, profile.id, {
3813
3878
  onProgress: (p) => sendEvent('progress', p),
3814
3879
  pinnedMessageIds: pinnedMessageIds || undefined,
3880
+ runtimeMode: 'local_desktop',
3815
3881
  });
3816
3882
  sendEvent('done', result);
3817
3883
  res.end();
@@ -3839,7 +3905,7 @@ function startLocalServer(opts) {
3839
3905
  return res.status(400).json({ error: 'template id is required' });
3840
3906
  if (!Number.isFinite(taskId) || taskId <= 0)
3841
3907
  return res.status(400).json({ error: 'taskId is required' });
3842
- const engine = (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir);
3908
+ const engine = (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir, 'local_desktop');
3843
3909
  res.writeHead(200, {
3844
3910
  'Content-Type': 'text/event-stream',
3845
3911
  'Cache-Control': 'no-cache',
@@ -3871,7 +3937,7 @@ function startLocalServer(opts) {
3871
3937
  });
3872
3938
  app.get('/api/workflow/active', (_req, res) => {
3873
3939
  try {
3874
- const engine = (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir);
3940
+ const engine = (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir, 'local_desktop');
3875
3941
  const workflows = engine.getActiveWorkflows();
3876
3942
  res.json(workflows);
3877
3943
  }
@@ -3881,7 +3947,7 @@ function startLocalServer(opts) {
3881
3947
  });
3882
3948
  app.post('/api/workflow/:id/cancel', (req, res) => {
3883
3949
  try {
3884
- const engine = (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir);
3950
+ const engine = (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir, 'local_desktop');
3885
3951
  const cancelled = engine.cancel(req.params.id);
3886
3952
  res.json({ ok: cancelled });
3887
3953
  }
@@ -4612,7 +4678,7 @@ function startLocalServer(opts) {
4612
4678
  }
4613
4679
  });
4614
4680
  // Initialize workflow engine
4615
- (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir);
4681
+ (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir, 'local_desktop');
4616
4682
  // Start server
4617
4683
  return new Promise((resolve, reject) => {
4618
4684
  _server = app.listen(port, '127.0.0.1', () => {
@@ -4643,43 +4709,38 @@ function stopLocalServer() {
4643
4709
  }
4644
4710
  });
4645
4711
  }
4646
- function buildCliFallback(profile) {
4647
- const providerName = profile.provider;
4648
- const model = (profile.model || '').trim() || 'default';
4649
- return {
4650
- providerName,
4651
- apiKey: 'cli-auth',
4652
- model,
4653
- llm: (0, index_1.createProvider)(providerName, { apiKey: 'cli-auth', model }),
4654
- runtimeMode: 'subscription-cli',
4655
- runtimeSource: 'cli-direct',
4656
- };
4657
- }
4658
4712
  async function buildChatRuntime(profile) {
4659
4713
  const providerName = profile.provider;
4660
- const cliModel = (profile.model || '').trim() || 'default';
4661
- const apiKeyFallback = buildApiKeyFallback(profile);
4662
- if (providerName === 'claude-cli' || providerName === 'codex-cli') {
4714
+ const model = (profile.model || '').trim() || 'default';
4715
+ if (providerName === 'claude-cli') {
4663
4716
  return {
4664
4717
  providerName,
4665
4718
  apiKey: 'cli-auth',
4666
- model: cliModel,
4667
- llm: (0, index_1.createProvider)(providerName, { apiKey: 'cli-auth', model: cliModel }),
4719
+ model,
4720
+ llm: (0, index_1.createProvider)(providerName, { apiKey: 'cli-auth', model, runtimeMode: 'local_desktop' }),
4721
+ runtimeMode: 'subscription-cli',
4722
+ runtimeSource: 'cli-direct',
4723
+ };
4724
+ }
4725
+ if (providerName === 'codex-cli') {
4726
+ return {
4727
+ providerName,
4728
+ apiKey: 'cli-auth',
4729
+ model,
4730
+ llm: (0, index_1.createProvider)(providerName, { apiKey: 'cli-auth', model, runtimeMode: 'local_desktop' }),
4668
4731
  runtimeMode: 'subscription-cli',
4669
4732
  runtimeSource: 'cli-direct',
4670
- apiKeyFallback,
4671
4733
  };
4672
4734
  }
4673
4735
  const apiKey = resolveApiKey(profile);
4674
4736
  if (!apiKey) {
4675
4737
  throw new Error(`No API key for provider ${profile.provider}. Configure one in Settings.`);
4676
4738
  }
4677
- const model = (profile.model || '').trim() || 'default';
4678
4739
  return {
4679
4740
  providerName,
4680
4741
  apiKey,
4681
4742
  model,
4682
- llm: (0, index_1.createProvider)(providerName, { apiKey, model }),
4743
+ llm: (0, index_1.createProvider)(providerName, { apiKey, model, runtimeMode: 'local_desktop' }),
4683
4744
  runtimeMode: 'api-key',
4684
4745
  runtimeSource: 'api-key',
4685
4746
  };
@@ -4691,14 +4752,13 @@ function runtimeModeLabel(mode, runtimeSource) {
4691
4752
  return (0, subscription_runtime_1.claudeSubscriptionRuntimeLabel)(runtimeSource);
4692
4753
  return 'API Key';
4693
4754
  }
4694
- function runtimePayloadForDisplay(providerName, model, runtimeMode, runtimeSource, fallbackUsed) {
4755
+ function runtimePayloadForDisplay(providerName, model, runtimeMode, runtimeSource) {
4695
4756
  return {
4696
4757
  mode: runtimeMode,
4697
4758
  modeLabel: runtimeModeLabel(runtimeMode, runtimeSource),
4698
4759
  provider: providerName,
4699
4760
  model: model || null,
4700
4761
  source: runtimeSource || null,
4701
- fallbackUsed,
4702
4762
  };
4703
4763
  }
4704
4764
  function configuredRuntimeLabelForProfile(profile) {
@@ -4870,27 +4930,19 @@ function detectInteractiveAuthFailure(text, activeProviderName, configuredProvid
4870
4930
  cli,
4871
4931
  };
4872
4932
  }
4873
- function buildApiKeyFallback(profile) {
4874
- const providerName = profile.provider === 'codex-cli'
4875
- ? 'openai'
4876
- : profile.provider === 'claude-cli'
4877
- ? 'anthropic'
4878
- : profile.provider;
4879
- const configured = data.findProviderConnection(providerName);
4880
- const apiKey = configured?.api_key_enc || resolveApiKey({ ...profile, provider: providerName });
4881
- if (!apiKey)
4882
- return undefined;
4883
- const model = (0, subscription_runtime_1.resolveSubscriptionApiModel)(profile.model, configured?.default_model || undefined)
4884
- || (configured?.default_model || '').trim()
4885
- || 'default';
4886
- return {
4887
- providerName,
4888
- apiKey,
4889
- model,
4890
- llm: (0, index_1.createProvider)(providerName, { apiKey, model }),
4891
- runtimeMode: 'api-key',
4892
- runtimeSource: 'api-key-fallback',
4893
- };
4933
+ const LOCAL_RUNTIME_RETRY_LIMIT = 2;
4934
+ function shouldRetrySelectedLocalRuntime(err) {
4935
+ const text = String(err?.message || err || '').toLowerCase();
4936
+ if (!text)
4937
+ return false;
4938
+ if (/\b(no api key|configure one in settings|not available on this machine|not installed|please run \/login|not logged in|invalid api key)\b/i.test(text)) {
4939
+ return false;
4940
+ }
4941
+ return /\b(429|rate limit|timeout|timed out|temporar|temporarily|econnreset|etimedout|enotfound|econnrefused|socket hang up|network|try again|overloaded|busy)\b/i.test(text);
4942
+ }
4943
+ async function pauseLocalRuntimeRetry(attempt) {
4944
+ const delayMs = attempt <= 1 ? 750 : 1500;
4945
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
4894
4946
  }
4895
4947
  function resolveApiKey(profile) {
4896
4948
  // Check profile-stored key first
@@ -4965,7 +5017,7 @@ function expandAllowedToolNames(allToolDefs, configuredBuiltinTools, configuredM
4965
5017
  }
4966
5018
  async function autoTitleConversation(convId, userMsg, assistantMsg, providerName, modelName, apiKey) {
4967
5019
  try {
4968
- const llm = (0, index_1.createProvider)(providerName, { apiKey, model: modelName || 'default' });
5020
+ const llm = (0, index_1.createProvider)(providerName, { apiKey, model: modelName || 'default', runtimeMode: 'local_desktop' });
4969
5021
  const resp = await llm.chat({
4970
5022
  messages: [{ role: 'user', content: `Generate a short title (max 6 words, no quotes) for this conversation:\n\nUser: ${userMsg.slice(0, 200)}\nAssistant: ${assistantMsg.slice(0, 200)}` }],
4971
5023
  system: 'You generate short conversation titles. Return ONLY the title, nothing else.',