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
@@ -39,6 +39,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.startLocalServer = startLocalServer;
40
40
  exports.stopLocalServer = stopLocalServer;
41
41
  exports.buildChatRuntimeForTest = buildChatRuntimeForTest;
42
+ exports.resolveDirectCliSessionTransportForTest = resolveDirectCliSessionTransportForTest;
43
+ exports.buildLocalDesktopDirectPromptForTest = buildLocalDesktopDirectPromptForTest;
42
44
  /**
43
45
  * Local HTTP server for desktop-first operation.
44
46
  *
@@ -47,6 +49,7 @@ exports.buildChatRuntimeForTest = buildChatRuntimeForTest;
47
49
  * Reuses existing LLM providers and tool execution from message-loop/providers.
48
50
  */
49
51
  const index_1 = require("./providers/index");
52
+ const completion_marker_1 = require("./completion-marker");
50
53
  const index_2 = require("./index");
51
54
  const approval_1 = require("./approval");
52
55
  const config_1 = require("./config");
@@ -61,7 +64,6 @@ const config_cleanup_1 = require("./config-cleanup");
61
64
  const status_parser_1 = require("./orchestration/status-parser");
62
65
  const guided_actions_1 = require("./orchestration/guided-actions");
63
66
  const topic_normalizer_1 = require("./orchestration/topic-normalizer");
64
- const policy_prompt_1 = require("./orchestration/policy-prompt");
65
67
  const safeguards_1 = require("./orchestration/safeguards");
66
68
  const token_counter_1 = require("./token-counter");
67
69
  const response_guard_1 = require("./response-guard");
@@ -78,6 +80,7 @@ const policy_detection_1 = require("./policy-detection");
78
80
  const server_runtime_1 = require("./server-runtime");
79
81
  const storage_mode_1 = require("./storage-mode");
80
82
  const local_cli_pty_manager_1 = require("./local-cli-pty-manager");
83
+ const codex_app_server_manager_1 = require("./codex-app-server-manager");
81
84
  const cli_session_epoch_1 = require("./cli-session-epoch");
82
85
  const server_adapter_1 = require("./server-adapter");
83
86
  const wizard_support_1 = require("./wizard-support");
@@ -611,7 +614,10 @@ function startLocalServer(opts) {
611
614
  if (isConnectedMode())
612
615
  return;
613
616
  while (runningChatJobControllers.size < MAX_LOCAL_CHAT_JOBS) {
614
- const next = data.listQueuedChatJobs(1)[0];
617
+ const runningKeys = new Set(data.listRunningChatJobs(MAX_LOCAL_CHAT_JOBS + 20).map((job) => `${job.conversation_id}::${job.bot_id}`));
618
+ const next = data
619
+ .listQueuedChatJobs(50)
620
+ .find((job) => !runningKeys.has(`${job.conversation_id}::${job.bot_id}`));
615
621
  if (!next)
616
622
  return;
617
623
  if (runningChatJobControllers.has(next.id))
@@ -1233,7 +1239,7 @@ function startLocalServer(opts) {
1233
1239
  app.post('/api/bots', (req, res) => {
1234
1240
  (async () => {
1235
1241
  try {
1236
- const { provider, model, name, soulMd, memoryMd, toolsMd, skillsMd, apiKeyEnc, permissionMode, isDefault, roleLabel, roleClass, isActive, priority, isOrchestrator, is_orchestrator } = req.body;
1242
+ const { provider, model, name, soulMd, memoryMd, toolsMd, skillsMd, apiKeyEnc, permissionMode, isDefault, roleLabel, roleClass, isActive, priority, isOrchestrator, is_orchestrator, codexReasoningEffort, codex_reasoning_effort, codexReasoningSummary, codex_reasoning_summary, codexPersonality, codex_personality, codexServiceTier, codex_service_tier, codexSandboxPolicy, codex_sandbox_policy, codexApprovalPolicy, codex_approval_policy, } = req.body;
1237
1243
  if (!provider || !model || !name) {
1238
1244
  return res.status(400).json({ error: 'provider, model, and name are required' });
1239
1245
  }
@@ -1263,6 +1269,12 @@ function startLocalServer(opts) {
1263
1269
  isActive,
1264
1270
  priority,
1265
1271
  isOrchestrator: isOrchestrator ?? is_orchestrator,
1272
+ codexReasoningEffort: codexReasoningEffort ?? codex_reasoning_effort,
1273
+ codexReasoningSummary: codexReasoningSummary ?? codex_reasoning_summary,
1274
+ codexPersonality: codexPersonality ?? codex_personality,
1275
+ codexServiceTier: codexServiceTier ?? codex_service_tier,
1276
+ codexSandboxPolicy: codexSandboxPolicy ?? codex_sandbox_policy,
1277
+ codexApprovalPolicy: codexApprovalPolicy ?? codex_approval_policy,
1266
1278
  });
1267
1279
  res.status(201).json(profile);
1268
1280
  }
@@ -1309,6 +1321,18 @@ function startLocalServer(opts) {
1309
1321
  fields.showThinking = b.showThinking ?? b.show_thinking;
1310
1322
  if (b.isOrchestrator !== undefined || b.is_orchestrator !== undefined)
1311
1323
  fields.isOrchestrator = b.isOrchestrator ?? b.is_orchestrator;
1324
+ if (b.codexReasoningEffort !== undefined || b.codex_reasoning_effort !== undefined)
1325
+ fields.codexReasoningEffort = b.codexReasoningEffort ?? b.codex_reasoning_effort;
1326
+ if (b.codexReasoningSummary !== undefined || b.codex_reasoning_summary !== undefined)
1327
+ fields.codexReasoningSummary = b.codexReasoningSummary ?? b.codex_reasoning_summary;
1328
+ if (b.codexPersonality !== undefined || b.codex_personality !== undefined)
1329
+ fields.codexPersonality = b.codexPersonality ?? b.codex_personality;
1330
+ if (b.codexServiceTier !== undefined || b.codex_service_tier !== undefined)
1331
+ fields.codexServiceTier = b.codexServiceTier ?? b.codex_service_tier;
1332
+ if (b.codexSandboxPolicy !== undefined || b.codex_sandbox_policy !== undefined)
1333
+ fields.codexSandboxPolicy = b.codexSandboxPolicy ?? b.codex_sandbox_policy;
1334
+ if (b.codexApprovalPolicy !== undefined || b.codex_approval_policy !== undefined)
1335
+ fields.codexApprovalPolicy = b.codexApprovalPolicy ?? b.codex_approval_policy;
1312
1336
  if (isConnectedMode()) {
1313
1337
  const runtime = (0, server_runtime_1.getRuntimeConnectionConfig)();
1314
1338
  const auth = await getHydratedDesktopAuth();
@@ -2450,15 +2474,15 @@ function startLocalServer(opts) {
2450
2474
  if (!profile) {
2451
2475
  return res.status(400).json({ error: 'No bot configured. Create one first.' });
2452
2476
  }
2453
- const shouldUseOrchestratorMode = orchestrationEnabled !== false && (0, orchestrator_profile_1.isOrchestratorProfile)(profile);
2477
+ const shouldUseOrchestratorMode = orchestrationEnabled !== false && (data.isClerkOrchestratorEnabled() || (0, orchestrator_profile_1.isOrchestratorProfile)(profile));
2454
2478
  if (shouldUseOrchestratorMode) {
2455
2479
  return res.status(400).json({ error: 'Background chat jobs do not support orchestrator mode yet.' });
2456
2480
  }
2457
2481
  let convId = conversationId ? String(conversationId) : '';
2458
2482
  if (convId) {
2459
- const latestJob = data.getLatestConversationChatJob(convId);
2483
+ const latestJob = data.getLatestConversationBotChatJob(convId, profile.id);
2460
2484
  if (latestJob && (latestJob.status === 'queued' || latestJob.status === 'running')) {
2461
- return res.status(409).json({ error: 'This conversation already has a pending response.' });
2485
+ return res.status(409).json({ error: 'This bot already has a pending response in this conversation.' });
2462
2486
  }
2463
2487
  }
2464
2488
  if (!convId) {
@@ -2554,7 +2578,10 @@ function startLocalServer(opts) {
2554
2578
  // ─── Chat (SSE streaming) ──────────────────────────────────
2555
2579
  app.post('/api/conversations/:id/chat-job/cancel', async (req, res) => {
2556
2580
  try {
2557
- const latestJob = data.getLatestConversationChatJob(req.params.id);
2581
+ const requestedBotId = String(req.body?.botId || '').trim();
2582
+ const latestJob = requestedBotId
2583
+ ? data.getLatestConversationBotChatJob(req.params.id, requestedBotId)
2584
+ : data.getLatestConversationChatJob(req.params.id);
2558
2585
  if (!latestJob)
2559
2586
  return res.status(404).json({ error: 'Chat job not found' });
2560
2587
  if (latestJob.status === 'completed' || latestJob.status === 'failed' || latestJob.status === 'cancelled') {
@@ -2590,6 +2617,7 @@ function startLocalServer(opts) {
2590
2617
  routeAbortController.abort();
2591
2618
  };
2592
2619
  req.on('close', abortOnClientClose);
2620
+ res.on('close', abortOnClientClose);
2593
2621
  try {
2594
2622
  let { conversationId, message, botId, skipUserMessage, pinnedMessageIds, topicId, projectId, workflowTemplateId, orchestrationEnabled, chatJobId, assistantMessageId, persistAssistantPlaceholder, } = req.body;
2595
2623
  if (!message)
@@ -2734,19 +2762,22 @@ function startLocalServer(opts) {
2734
2762
  assistantMessageId = placeholder.id;
2735
2763
  }
2736
2764
  // ─── Orchestrator Mode Branch ─────────────────────────
2737
- const shouldUseOrchestratorMode = orchestrationEnabled !== false && (0, orchestrator_profile_1.isOrchestratorProfile)(profile);
2765
+ const shouldUseOrchestratorMode = orchestrationEnabled !== false && (data.isClerkOrchestratorEnabled() || (0, orchestrator_profile_1.isOrchestratorProfile)(profile));
2738
2766
  if (shouldUseOrchestratorMode) {
2739
- const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: 'local_desktop' });
2740
- if (!clerk) {
2741
- // Fix #2: Do not silently fall through to direct chat — return a clear error
2742
- return res.status(400).json({
2743
- error: 'Orchestrator mode requires a clerk model to be configured. Please add a provider connection in Settings.',
2744
- });
2745
- }
2746
2767
  const { OrchestratorAgent } = require('./orchestrator');
2768
+ const { buildLocalDesktopOrchestratorRuntime } = require('./orchestrator');
2747
2769
  const { getWorkflowEngine } = require('./workflow-engine');
2748
2770
  const workflowEngine = getWorkflowEngine(opts.projectDir, 'local_desktop');
2749
- const orchestrator = new OrchestratorAgent(clerk, workflowEngine);
2771
+ let orchestratorRuntime;
2772
+ try {
2773
+ orchestratorRuntime = buildLocalDesktopOrchestratorRuntime(profile);
2774
+ }
2775
+ catch (runtimeErr) {
2776
+ return res.status(400).json({
2777
+ error: runtimeErr?.message || 'Orchestrator mode is not configured correctly.',
2778
+ });
2779
+ }
2780
+ const orchestrator = new OrchestratorAgent(orchestratorRuntime, workflowEngine);
2750
2781
  // Resolve effective project ID from request or existing conversation
2751
2782
  const conv = data.getConversation(convId);
2752
2783
  const effectiveProjectId = projectId ? String(projectId) : (conv?.project_id || undefined);
@@ -2769,13 +2800,34 @@ function startLocalServer(opts) {
2769
2800
  };
2770
2801
  let orchestratorRuntimeLabel = '';
2771
2802
  let orchestratorRuntimePayload;
2803
+ const clerkSelectedAsOrchestrator = data.isClerkOrchestratorEnabled();
2772
2804
  try {
2773
- const orchestratorRuntime = await buildChatRuntime(profile);
2774
- orchestratorRuntimeLabel = [
2775
- orchestratorRuntime.model || profile.model || '',
2776
- runtimeModeLabel(orchestratorRuntime.runtimeMode, orchestratorRuntime.runtimeSource),
2777
- ].filter(Boolean).join(' | ');
2778
- orchestratorRuntimePayload = runtimePayloadForDisplay(profile.provider, orchestratorRuntime.model || profile.model || null, orchestratorRuntime.runtimeMode, orchestratorRuntime.runtimeSource || null);
2805
+ if (clerkSelectedAsOrchestrator) {
2806
+ const clerkProvider = data.getSetting('clerk_provider') || profile.provider;
2807
+ const clerkModel = data.getSetting('clerk_model') || profile.model || null;
2808
+ const clerkConnection = data.findProviderConnection(clerkProvider);
2809
+ const clerkRuntimeMode = clerkProvider === 'claude-cli' || clerkProvider === 'codex-cli'
2810
+ ? 'subscription-cli'
2811
+ : 'api-key';
2812
+ const clerkRuntimeSource = clerkConnection?.access_mode === 'oauth'
2813
+ ? 'oauth-token'
2814
+ : clerkConnection?.access_mode === 'cli'
2815
+ ? 'cli-direct'
2816
+ : 'api-key';
2817
+ orchestratorRuntimeLabel = [
2818
+ clerkModel || '',
2819
+ runtimeModeLabel(clerkRuntimeMode, clerkRuntimeSource),
2820
+ ].filter(Boolean).join(' | ');
2821
+ orchestratorRuntimePayload = runtimePayloadForDisplay(clerkProvider, clerkModel, clerkRuntimeMode, clerkRuntimeSource);
2822
+ }
2823
+ else {
2824
+ const orchestratorRuntime = await buildChatRuntime(profile);
2825
+ orchestratorRuntimeLabel = [
2826
+ orchestratorRuntime.model || profile.model || '',
2827
+ runtimeModeLabel(orchestratorRuntime.runtimeMode, orchestratorRuntime.runtimeSource),
2828
+ ].filter(Boolean).join(' | ');
2829
+ orchestratorRuntimePayload = runtimePayloadForDisplay(profile.provider, orchestratorRuntime.model || profile.model || null, orchestratorRuntime.runtimeMode, orchestratorRuntime.runtimeSource || null);
2830
+ }
2779
2831
  }
2780
2832
  catch {
2781
2833
  orchestratorRuntimeLabel = buildConfiguredMessageModel(profile);
@@ -2836,6 +2888,18 @@ function startLocalServer(opts) {
2836
2888
  text: event.text,
2837
2889
  });
2838
2890
  }
2891
+ else if (event.type === 'worker_terminal_chunk') {
2892
+ hasWorkerActivity = true;
2893
+ sendEvent('worker_terminal_chunk', {
2894
+ stepId: event.stepId,
2895
+ botId: resolveWorkerBotId(event.agentName),
2896
+ agentName: event.agentName,
2897
+ description: event.description,
2898
+ stepIndex: event.stepIndex,
2899
+ totalSteps: event.totalSteps,
2900
+ text: event.rawText,
2901
+ });
2902
+ }
2839
2903
  else if (event.type === 'worker_tool_call') {
2840
2904
  hasWorkerActivity = true;
2841
2905
  recordWorkerActivity('worker_tool_call', event, {
@@ -3027,9 +3091,6 @@ function startLocalServer(opts) {
3027
3091
  }
3028
3092
  return;
3029
3093
  }
3030
- // Prompt Contract v1: system carries summary + last 5 turns.
3031
- // Send only the current user request as the primary user message.
3032
- const llmMessages = [{ role: 'user', content: message }];
3033
3094
  const configuredTz = (data.getSetting('timezone') || '').trim();
3034
3095
  const effectiveTimezone = configuredTz && configuredTz.toLowerCase() !== 'system'
3035
3096
  ? configuredTz
@@ -3042,101 +3103,16 @@ function startLocalServer(opts) {
3042
3103
  ? new Set(allToolDefs.map((tool) => tool.name))
3043
3104
  : expandAllowedToolNames(allToolDefs, configuredBuiltinTools, configuredMcpTools);
3044
3105
  const toolDefs = allToolDefs.filter((tool) => allowedToolNames.has(tool.name));
3045
- // Build system prompt via clerk (token-budgeted context injection)
3046
- let systemPrompt;
3106
+ const conversation = data.getConversation(convId);
3107
+ const topicTitle = topicId ? data.getTopic(topicId)?.title : undefined;
3108
+ const project = conversation?.project_id ? data.getProject(conversation.project_id) : undefined;
3109
+ const workspacePath = project?.folder?.trim() || undefined;
3047
3110
  let llmSpawnCwd = opts.projectDir;
3048
- const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: 'local_desktop' });
3049
- if (clerk) {
3050
- const conv = data.getConversation(convId);
3051
- const topicTitle = topicId ? data.getTopic(topicId)?.title : undefined;
3052
- const project = conv?.project_id ? data.getProject(conv.project_id) : undefined;
3053
- const workspacePath = project?.folder?.trim() || undefined;
3054
- if (workspacePath && fs.existsSync(workspacePath)) {
3055
- llmSpawnCwd = workspacePath;
3056
- }
3057
- const built = clerk.buildPrompt(message, profile.id, profile, {
3058
- targetModel: profile.model,
3059
- conversationId: convId,
3060
- projectName: conv?.project_name || undefined,
3061
- projectId: conv?.project_id || undefined,
3062
- topicTitle: topicTitle || undefined,
3063
- workspacePath,
3064
- timezone: effectiveTimezone,
3065
- includeKeyDecisions: false,
3066
- availableTools: toolDefs.map((tool) => ({ name: tool.name, description: tool.description })),
3067
- });
3068
- systemPrompt = built.systemPrompt;
3069
- console.log(chalk_1.default.gray(` [clerk] Context: ${built.injectedSummaries} summaries (${built.contextTokensUsed} tokens)`));
3111
+ if (workspacePath && fs.existsSync(workspacePath)) {
3112
+ llmSpawnCwd = workspacePath;
3070
3113
  }
3071
- else {
3072
- // Fallback: manual prompt building
3073
- systemPrompt = '[Bot Identity]\n' + (profile.soul_md
3074
- || 'You are an AI assistant running locally. You have access to project files and can execute code.');
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.';
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.';
3078
- const convForFallback = data.getConversation(convId);
3079
- const projectForFallback = convForFallback?.project_id ? data.getProject(convForFallback.project_id) : undefined;
3080
- const workspaceForFallback = projectForFallback?.folder?.trim();
3081
- if (workspaceForFallback && fs.existsSync(workspaceForFallback)) {
3082
- llmSpawnCwd = workspaceForFallback;
3083
- }
3084
- const fallbackMeta = [];
3085
- if (convForFallback?.project_name)
3086
- fallbackMeta.push(`Project: ${convForFallback.project_name}`);
3087
- if (topicId) {
3088
- const fallbackTopic = data.getTopic(topicId);
3089
- if (fallbackTopic?.title)
3090
- fallbackMeta.push(`Topic: ${fallbackTopic.title}`);
3091
- }
3092
- if (convForFallback?.project_id) {
3093
- fallbackMeta.push(`Workspace: ${workspaceForFallback || '(project folder not configured)'}`);
3094
- }
3095
- if (fallbackMeta.length > 0) {
3096
- systemPrompt += '\n\n[Project Overview]\n' + fallbackMeta.join('\n');
3097
- }
3098
- const effectivePolicy = data.getEffectiveOrchestrationPolicy(convForFallback?.project_id || undefined);
3099
- systemPrompt += '\n\n' + (0, policy_prompt_1.buildEffectivePolicyPromptSection)(effectivePolicy, {
3100
- heading: '[Effective Policy]',
3101
- defaultLine: 'No confirmed special policy is set.',
3102
- });
3103
- try {
3104
- const todoStatus = data.getTodoStatusMarker(convForFallback?.project_id ?? undefined);
3105
- systemPrompt += `\n\n[TODO Coordination]\nTODO STATUS: ${todoStatus}`;
3106
- }
3107
- catch { /* best-effort */ }
3108
- systemPrompt += '\n\n' + (0, clerk_model_1.buildTodoInstructions)(profile?.name || profile?.id || 'LLM');
3109
- let hasSummary = false;
3110
- try {
3111
- const summaryContext = (0, context_window_1.getPromptContextWindow)(convId, safeguards_1.SAFEGUARDS.CONTEXT_WINDOW_TURNS);
3112
- if (summaryContext.summary?.summary_text) {
3113
- const summaryHeader = summaryContext.carriedForward
3114
- ? '[Context Summary (Carried Forward from Previous Conversation in This Topic)]'
3115
- : '[Context Summary]';
3116
- systemPrompt += `\n\n${summaryHeader}\n` + summaryContext.summary.summary_text;
3117
- hasSummary = true;
3118
- }
3119
- }
3120
- catch { /* best-effort */ }
3121
- try {
3122
- const turnWindow = hasSummary
3123
- ? safeguards_1.SAFEGUARDS.CONTEXT_WINDOW_TURNS
3124
- : safeguards_1.SAFEGUARDS.NO_SUMMARY_CONTEXT_WINDOW_TURNS;
3125
- const promptContext = (0, context_window_1.getPromptContextWindow)(convId, turnWindow);
3126
- if (promptContext.turns.length > 0) {
3127
- const turnsHeader = promptContext.carriedForward
3128
- ? `[Recent Messages (Last ${promptContext.turns.length} Turns from Previous Conversation in This Topic)]`
3129
- : `[Recent Messages (Last ${promptContext.turns.length} Turns)]`;
3130
- systemPrompt += `\n\n${turnsHeader}\n` + (0, context_window_1.formatTurnsForPrompt)(promptContext.turns);
3131
- }
3132
- }
3133
- catch { /* best-effort */ }
3134
- }
3135
- // Resolve LLM runtime.
3136
- // Desktop local mode intentionally supports only:
3137
- // - Subscription CLI
3138
- // - API Key
3139
- // We do not use subscription-token API routing for local CLI bots.
3114
+ // Resolve LLM runtime early so the local desktop prompt contract can differ
3115
+ // between API/fresh CLI and recurring CLI sessions without affecting server paths.
3140
3116
  const runtime = await buildChatRuntime(profile);
3141
3117
  let activeProviderName = runtime.providerName;
3142
3118
  let activeModelName = runtime.model;
@@ -3164,6 +3140,33 @@ function startLocalServer(opts) {
3164
3140
  const cliEpochStartedAt = cliSessionEpochPlan.resumeSessionId
3165
3141
  ? (cliSessionEpochPlan.existing?.epoch_started_at || localTimestamp())
3166
3142
  : localTimestamp();
3143
+ const primaryTopicId = topicId || data.getPrimaryTopicIdForConversation(convId) || undefined;
3144
+ const cliHistoryFilePath = activeIsCliProvider && !cliSessionEpochPlan.resumeSessionId
3145
+ ? writeCliBootstrapHistoryFile({
3146
+ conversationId: convId,
3147
+ botId: profile.id,
3148
+ projectPath: llmSpawnCwd,
3149
+ topicId: primaryTopicId,
3150
+ })
3151
+ : null;
3152
+ const directPrompt = buildLocalDesktopDirectPrompt({
3153
+ conversationId: convId,
3154
+ currentBotId: profile.id,
3155
+ currentBotName: profile.name,
3156
+ currentProvider: activeProviderName,
3157
+ userPrompt: message,
3158
+ soulMd: profile.soul_md || 'You are an AI assistant running locally. You have access to project files and can execute code.',
3159
+ projectName: conversation?.project_name || undefined,
3160
+ topicTitle: topicTitle || undefined,
3161
+ workspacePath,
3162
+ timezone: effectiveTimezone,
3163
+ availableTools: toolDefs.map((tool) => ({ name: tool.name, description: tool.description })),
3164
+ isCliRecurring: !!cliSessionEpochPlan.resumeSessionId,
3165
+ cliHistoryFilePath,
3166
+ useCompletionSentinel: resolveDirectCliSessionTransport(activeProviderName, enableCliSessionEpoch, (0, storage_mode_1.isLocalStorageMode)()) === 'pty',
3167
+ });
3168
+ const llmMessages = [{ role: 'user', content: directPrompt.userPrompt }];
3169
+ let systemPrompt = directPrompt.systemPrompt;
3167
3170
  if (!activeApiKey) {
3168
3171
  return res.status(400).json({ error: `No API key for provider ${profile.provider}. Configure one in Settings.` });
3169
3172
  }
@@ -3175,14 +3178,6 @@ function startLocalServer(opts) {
3175
3178
  restrictFileAccessToProject: unrestrictedCliProfile ? false : undefined,
3176
3179
  abortSignal: routeAbortController.signal,
3177
3180
  });
3178
- const toolManifest = toolDefs
3179
- .map(t => `- ${t.name}: ${t.description}`)
3180
- .join('\n');
3181
- if (toolManifest.trim()) {
3182
- systemPrompt += unrestrictedCliProfile
3183
- ? '\n\n[Available Tools]\nThe following tools are available in the current runtime:\n' + toolManifest
3184
- : '\n\n[Available Tools]\nOnly the following tools are enabled for this bot in the current runtime:\n' + toolManifest;
3185
- }
3186
3181
  // Inject pinned messages as context (user-selected cross-bot references)
3187
3182
  if (pinnedMessageIds && Array.isArray(pinnedMessageIds) && pinnedMessageIds.length > 0) {
3188
3183
  const pinnedLines = [];
@@ -3240,33 +3235,6 @@ function startLocalServer(opts) {
3240
3235
  isApproximate: true,
3241
3236
  },
3242
3237
  });
3243
- sendEvent('status', {
3244
- phase: 'thinking',
3245
- detail: `Sending request to ${activeProviderName}...`,
3246
- runtime: runtimePayload(),
3247
- });
3248
- recordActivity('status', {
3249
- phase: 'thinking',
3250
- detail: `Sending request to ${activeProviderName}...`,
3251
- runtime: runtimePayload(),
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
- }
3270
3238
  let partialPersistedContent = '';
3271
3239
  let partialPersistedAt = 0;
3272
3240
  const throwIfChatJobCancelled = () => {
@@ -3306,6 +3274,7 @@ function startLocalServer(opts) {
3306
3274
  let fullContent = '';
3307
3275
  let streamedContent = '';
3308
3276
  let streamedAnyChunk = false;
3277
+ let rawCliTranscript = '';
3309
3278
  let iteration = 0;
3310
3279
  const MAX_ITERATIONS = 10; // Phase 1d: reduced from 20
3311
3280
  let totalInputTokens = 0;
@@ -3316,31 +3285,141 @@ function startLocalServer(opts) {
3316
3285
  let accumulatedThinking = '';
3317
3286
  const thinkingEnabled = !!profile?.show_thinking;
3318
3287
  let useInteractiveCliSession = enableCliSessionEpoch;
3319
- if (useInteractiveCliSession) {
3288
+ const directCliSessionTransport = resolveDirectCliSessionTransport(activeProviderName, useInteractiveCliSession, (0, storage_mode_1.isLocalStorageMode)());
3289
+ const useCodexAppServerInteractive = directCliSessionTransport === 'codex-app-server';
3290
+ const usePtyInteractiveCliSession = directCliSessionTransport === 'pty';
3291
+ const startDetail = `Started response via ${runtimeModeLabel(activeRuntimeMode, activeRuntimeSource) || activeProviderName}`;
3292
+ sendEvent('status', { phase: 'thinking', detail: startDetail });
3293
+ recordActivity('status', { phase: 'thinking', detail: startDetail }, startDetail);
3294
+ if (useCodexAppServerInteractive) {
3295
+ const codexAppServerManager = (0, codex_app_server_manager_1.getCodexAppServerManager)();
3296
+ let appServerAttempt = 0;
3297
+ let forceFreshInteractiveCliSession = false;
3298
+ while (true) {
3299
+ appServerAttempt++;
3300
+ try {
3301
+ const isFreshSession = forceFreshInteractiveCliSession || !cliSessionEpochPlan.resumeSessionId;
3302
+ const result = await codexAppServerManager.runTurn({
3303
+ runtimeMode: 'local_desktop',
3304
+ conversationId: convId,
3305
+ botId: profile.id,
3306
+ botName: profile.name,
3307
+ cwd: llmSpawnCwd,
3308
+ systemPrompt,
3309
+ messages: llmMessages,
3310
+ forceFreshSession: isFreshSession,
3311
+ resumeSessionId: cliSessionEpochPlan.resumeSessionId || undefined,
3312
+ model: activeModelName || profile.model || null,
3313
+ projectId: conversation?.project_id ?? null,
3314
+ codexSettings: {
3315
+ reasoningEffort: profile.codex_reasoning_effort,
3316
+ reasoningSummary: profile.codex_reasoning_summary,
3317
+ personality: profile.codex_personality,
3318
+ serviceTier: profile.codex_service_tier,
3319
+ sandboxPolicy: profile.codex_sandbox_policy,
3320
+ approvalPolicy: profile.codex_approval_policy,
3321
+ },
3322
+ abortSignal: routeAbortController.signal,
3323
+ onChunk: async (chunk) => {
3324
+ streamedAnyChunk = true;
3325
+ streamedContent += chunk;
3326
+ persistAssistantPartial(false);
3327
+ sendEvent('chunk', { text: chunk });
3328
+ },
3329
+ onDetail: async (detail) => {
3330
+ const text = String(detail || '').trim();
3331
+ if (!text)
3332
+ return;
3333
+ sendEvent('status', { phase: 'thinking', detail: text });
3334
+ recordActivity('status', { phase: 'thinking', detail: text }, text);
3335
+ },
3336
+ });
3337
+ if (result.sessionId) {
3338
+ activeCliSessionId = result.sessionId;
3339
+ }
3340
+ if (result.usage) {
3341
+ totalInputTokens += result.usage.inputTokens || 0;
3342
+ totalOutputTokens += result.usage.outputTokens || 0;
3343
+ hasExactUsage = true;
3344
+ }
3345
+ rawCliTranscript = result.rawOutput || '';
3346
+ fullContent = (0, completion_marker_1.stripCompletionSentinel)((result.content || '').trim()).text.trim();
3347
+ if (!fullContent && appServerAttempt < LOCAL_RUNTIME_RETRY_LIMIT) {
3348
+ forceFreshInteractiveCliSession = true;
3349
+ codexAppServerManager.closeSessionByConversation(convId, profile.id);
3350
+ const retryDetail = `Selected runtime returned an empty response; retrying with a fresh ${activeProviderName} session (${appServerAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`;
3351
+ console.warn(chalk_1.default.yellow(` [chat] ${retryDetail}`));
3352
+ await pauseLocalRuntimeRetry(appServerAttempt);
3353
+ continue;
3354
+ }
3355
+ break;
3356
+ }
3357
+ catch (codexErr) {
3358
+ if (routeAbortController.signal.aborted || codexErr?.name === 'AbortError') {
3359
+ throw codexErr;
3360
+ }
3361
+ if (appServerAttempt >= LOCAL_RUNTIME_RETRY_LIMIT || !shouldRetrySelectedLocalRuntime(codexErr)) {
3362
+ throw codexErr;
3363
+ }
3364
+ forceFreshInteractiveCliSession = true;
3365
+ codexAppServerManager.closeSessionByConversation(convId, profile.id);
3366
+ const retryDetail = `Selected runtime failed (${codexErr?.message || codexErr}); retrying with a fresh ${activeProviderName} session (${appServerAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`;
3367
+ console.warn(chalk_1.default.yellow(` [chat] ${retryDetail}`));
3368
+ await pauseLocalRuntimeRetry(appServerAttempt);
3369
+ }
3370
+ }
3371
+ }
3372
+ if (usePtyInteractiveCliSession) {
3373
+ if (activeProviderName !== 'claude-cli') {
3374
+ throw new Error(`Legacy PTY interactive sessions are Claude-only in local desktop mode; expected ${activeProviderName} to use Codex app-server.`);
3375
+ }
3320
3376
  const ptyManager = (0, local_cli_pty_manager_1.getLocalCliPtySessionManager)();
3321
3377
  let ptyAttempt = 0;
3378
+ let forceFreshInteractiveCliSession = false;
3379
+ let currentAttemptSessionId = null;
3380
+ let currentAttemptWasFreshSession = false;
3322
3381
  while (true) {
3323
3382
  ptyAttempt++;
3324
3383
  try {
3384
+ // Pattern A (Claude CLI): generate a sequential session ID for new sessions
3385
+ // Pattern B (Codex CLI): let the CLI generate its own ID
3386
+ const isFreshSession = forceFreshInteractiveCliSession || !cliSessionEpochPlan.resumeSessionId;
3387
+ const newSessionId = isFreshSession && activeProviderName === 'claude-cli'
3388
+ ? data.generateNextSessionId()
3389
+ : undefined;
3390
+ currentAttemptWasFreshSession = isFreshSession;
3391
+ currentAttemptSessionId = newSessionId || cliSessionEpochPlan.resumeSessionId || activeCliSessionId || null;
3325
3392
  const result = await ptyManager.runTurn({
3326
3393
  conversationId: convId,
3327
3394
  botId: profile.id,
3328
- provider: activeProviderName,
3395
+ provider: 'claude-cli',
3329
3396
  cwd: llmSpawnCwd,
3330
3397
  systemPrompt,
3331
3398
  messages: llmMessages,
3332
- forceFreshSession: !cliSessionEpochPlan.resumeSessionId,
3333
- onDetail: async (detail) => {
3334
- sendEvent('status', {
3335
- phase: 'thinking',
3336
- detail,
3337
- runtime: runtimePayload(),
3399
+ forceFreshSession: isFreshSession,
3400
+ resumeSessionId: cliSessionEpochPlan.resumeSessionId || undefined,
3401
+ newSessionId,
3402
+ abortSignal: routeAbortController.signal,
3403
+ onRawChunk: async (chunk) => {
3404
+ sendEvent('terminal_chunk', {
3405
+ text: chunk,
3406
+ provider: activeProviderName,
3407
+ botId: profile?.id || null,
3408
+ agentName: profile?.name || null,
3338
3409
  });
3339
- recordActivity('status', {
3340
- phase: 'thinking',
3341
- detail,
3342
- runtime: runtimePayload(),
3343
- }, detail);
3410
+ },
3411
+ onChunk: async (chunk) => {
3412
+ streamedAnyChunk = true;
3413
+ streamedContent += chunk;
3414
+ persistAssistantPartial(false);
3415
+ sendEvent('chunk', { text: chunk });
3416
+ },
3417
+ onDetail: async (detail) => {
3418
+ const text = String(detail || '').trim();
3419
+ if (!text)
3420
+ return;
3421
+ sendEvent('status', { phase: 'thinking', detail: text });
3422
+ recordActivity('status', { phase: 'thinking', detail: text }, text);
3344
3423
  },
3345
3424
  });
3346
3425
  if (result.sessionId) {
@@ -3351,25 +3430,36 @@ function startLocalServer(opts) {
3351
3430
  totalOutputTokens += result.usage.outputTokens || 0;
3352
3431
  hasExactUsage = true;
3353
3432
  }
3354
- fullContent = (result.content || '').trim();
3433
+ rawCliTranscript = result.rawOutput || '';
3434
+ fullContent = (0, completion_marker_1.stripCompletionSentinel)((result.content || '').trim()).text.trim();
3435
+ if (!fullContent && ptyAttempt < LOCAL_RUNTIME_RETRY_LIMIT) {
3436
+ forceFreshInteractiveCliSession = true;
3437
+ ptyManager.closeSessionByConversation(convId, profile.id);
3438
+ const retryDetail = `Selected runtime returned an empty response; retrying with a fresh ${activeProviderName} session (${ptyAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`;
3439
+ console.warn(chalk_1.default.yellow(` [chat] ${retryDetail}`));
3440
+ await pauseLocalRuntimeRetry(ptyAttempt);
3441
+ continue;
3442
+ }
3355
3443
  break;
3356
3444
  }
3357
3445
  catch (ptyErr) {
3446
+ if (routeAbortController.signal.aborted || ptyErr?.name === 'AbortError') {
3447
+ throw ptyErr;
3448
+ }
3358
3449
  if (ptyAttempt >= LOCAL_RUNTIME_RETRY_LIMIT || !shouldRetrySelectedLocalRuntime(ptyErr)) {
3359
3450
  throw ptyErr;
3360
3451
  }
3361
- const retryDetail = `Selected runtime failed (${ptyErr?.message || ptyErr}); retrying the same connection (${ptyAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`;
3452
+ const startupRetry = isClaudeFreshSessionStartupFailure(ptyErr);
3453
+ if (startupRetry || currentAttemptWasFreshSession) {
3454
+ forceFreshInteractiveCliSession = true;
3455
+ ptyManager.closeSessionByConversation(convId, profile.id);
3456
+ }
3457
+ const retryDetail = startupRetry
3458
+ ? `Fresh ${activeProviderName} session ${currentAttemptSessionId || '(unknown)'} did not create a transcript within 7s; killing it and retrying with a new session (${ptyAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`
3459
+ : currentAttemptWasFreshSession && activeProviderName === 'claude-cli'
3460
+ ? `Fresh ${activeProviderName} session ${currentAttemptSessionId || '(unknown)'} failed (${ptyErr?.message || ptyErr}); killing it and retrying with a new session (${ptyAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`
3461
+ : `Selected runtime failed (${ptyErr?.message || ptyErr}); retrying the same connection (${ptyAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`;
3362
3462
  console.warn(chalk_1.default.yellow(` [chat] ${retryDetail}`));
3363
- sendEvent('status', {
3364
- phase: 'thinking',
3365
- detail: retryDetail,
3366
- runtime: runtimePayload(),
3367
- });
3368
- recordActivity('status', {
3369
- phase: 'thinking',
3370
- detail: retryDetail,
3371
- runtime: runtimePayload(),
3372
- }, retryDetail);
3373
3463
  await pauseLocalRuntimeRetry(ptyAttempt);
3374
3464
  }
3375
3465
  }
@@ -3379,10 +3469,6 @@ function startLocalServer(opts) {
3379
3469
  iteration++;
3380
3470
  let iterationFirstChunk = true;
3381
3471
  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
3472
  let response;
3387
3473
  const chatOptions = {
3388
3474
  messages: llmMessages,
@@ -3398,8 +3484,6 @@ function startLocalServer(opts) {
3398
3484
  throwIfChatJobCancelled();
3399
3485
  if (iterationFirstChunk) {
3400
3486
  iterationFirstChunk = false;
3401
- sendEvent('status', { phase: 'generating' });
3402
- recordActivity('status', { phase: 'generating' }, 'Generating response...');
3403
3487
  }
3404
3488
  streamedAnyChunk = true;
3405
3489
  streamedContent += chunk;
@@ -3531,7 +3615,7 @@ function startLocalServer(opts) {
3531
3615
  continue;
3532
3616
  }
3533
3617
  // Final response (guard against defer-only filler)
3534
- const candidate = (response.content || '').trim();
3618
+ const candidate = (0, completion_marker_1.stripCompletionSentinel)((response.content || '').trim()).text.trim();
3535
3619
  if (!forcedFinalizationPass && (0, response_guard_1.isLikelyDeferredReply)(candidate)) {
3536
3620
  forcedFinalizationPass = true;
3537
3621
  llmMessages.push({ role: 'assistant', content: candidate });
@@ -3539,14 +3623,12 @@ function startLocalServer(opts) {
3539
3623
  role: 'user',
3540
3624
  content: 'Provide the final answer now. Do not say you will check later. Either provide concrete results or explicitly say what is unavailable.',
3541
3625
  });
3542
- sendEvent('status', { phase: 'thinking', detail: 'Finalizing response...' });
3543
- recordActivity('status', { phase: 'thinking', detail: 'Finalizing response...' }, 'Finalizing response...');
3544
3626
  continue;
3545
3627
  }
3546
3628
  fullContent = candidate;
3547
3629
  break;
3548
3630
  }
3549
- const persistedContent = fullContent || streamedContent.trim();
3631
+ const persistedContent = (0, completion_marker_1.stripCompletionSentinel)(fullContent || streamedContent.trim()).text.trim();
3550
3632
  if (!persistedContent) {
3551
3633
  throw new Error('Assistant returned no final response');
3552
3634
  }
@@ -3594,6 +3676,7 @@ function startLocalServer(opts) {
3594
3676
  model: modelWithRuntime || null,
3595
3677
  botId: profile.id,
3596
3678
  agentName: profile.name,
3679
+ resultArtifact: useInteractiveCliSession ? (rawCliTranscript || null) : undefined,
3597
3680
  }) || data.addMessage(convId, 'assistant', persistedContent, modelWithRuntime || undefined, undefined, profile.id, profile.name))
3598
3681
  : data.addMessage(convId, 'assistant', persistedContent, modelWithRuntime || undefined, undefined, profile.id, profile.name);
3599
3682
  data.attachMessageActivitiesToMessage(activityStreamId, savedMessage.id);
@@ -3606,6 +3689,7 @@ function startLocalServer(opts) {
3606
3689
  summary: 'Final assistant response',
3607
3690
  payload: {
3608
3691
  content: persistedContent,
3692
+ ...(useInteractiveCliSession && rawCliTranscript ? { rawOutput: rawCliTranscript } : {}),
3609
3693
  runtime: runtimePayload(),
3610
3694
  },
3611
3695
  expiresAt: activityExpiresAt,
@@ -3689,6 +3773,7 @@ function startLocalServer(opts) {
3689
3773
  }
3690
3774
  finally {
3691
3775
  req.off?.('close', abortOnClientClose);
3776
+ res.off?.('close', abortOnClientClose);
3692
3777
  }
3693
3778
  });
3694
3779
  // ─── Memory Facts ───────────────────────────────────────────
@@ -3807,11 +3892,18 @@ function startLocalServer(opts) {
3807
3892
  const clerkProvider = data.getSetting('clerk_provider');
3808
3893
  const clerkModel = data.getSetting('clerk_model');
3809
3894
  const hasKey = !!data.getSetting('clerk_api_key');
3895
+ const currentOrchestrator = data.getCurrentOrchestratorSelection();
3896
+ const currentDefaultBot = data.getDefaultAgentProfile();
3810
3897
  res.json({
3811
3898
  provider: clerkProvider || null,
3812
3899
  model: clerkModel || null,
3813
3900
  hasApiKey: hasKey,
3814
3901
  configured: !!(clerkProvider && clerkModel && hasKey),
3902
+ isOrchestrator: data.isClerkOrchestratorEnabled(),
3903
+ currentOrchestrator,
3904
+ currentDefaultBot: currentDefaultBot
3905
+ ? { botId: currentDefaultBot.id, botName: currentDefaultBot.name }
3906
+ : null,
3815
3907
  });
3816
3908
  }
3817
3909
  catch (err) {
@@ -3820,13 +3912,16 @@ function startLocalServer(opts) {
3820
3912
  });
3821
3913
  app.put('/api/clerk/config', (req, res) => {
3822
3914
  try {
3823
- const { provider, model, apiKey } = req.body;
3915
+ const { provider, model, apiKey, isOrchestrator } = req.body;
3824
3916
  if (!provider || !model)
3825
3917
  return res.status(400).json({ error: 'provider and model are required' });
3826
3918
  data.setSetting('clerk_provider', provider);
3827
3919
  data.setSetting('clerk_model', model);
3828
3920
  if (apiKey)
3829
3921
  data.setSetting('clerk_api_key', apiKey);
3922
+ if (typeof isOrchestrator === 'boolean') {
3923
+ data.setClerkAsOrchestrator(isOrchestrator);
3924
+ }
3830
3925
  // Reset clerk instance so it picks up new config
3831
3926
  const { resetClerk } = require('./clerk-model');
3832
3927
  resetClerk();
@@ -3842,11 +3937,31 @@ function startLocalServer(opts) {
3842
3937
  const { prompt } = req.body;
3843
3938
  if (!prompt)
3844
3939
  return res.status(400).json({ error: 'prompt is required' });
3845
- const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: 'local_desktop' });
3846
- if (!clerk)
3847
- return res.json({ routing: 'default', reason: 'No clerk configured' });
3848
3940
  const agents = data.listAgentProfiles();
3849
- const route = await clerk.routeTask(prompt, agents);
3941
+ const normalized = String(prompt || '').toLowerCase();
3942
+ const defaultAgent = data.getDefaultAgentProfile() || agents[0];
3943
+ const namedAgent = agents.find((agent) => normalized.includes(agent.name.toLowerCase()));
3944
+ const roleMatchedAgent = /\b(qa|review|verify|test)\b/i.test(normalized)
3945
+ ? agents.find((agent) => /\bqa|review\b/i.test(String(agent.role_class || agent.role_label || '')))
3946
+ : /\b(research|brainstorm|analy[sz]e|evaluate|investigate)\b/i.test(normalized)
3947
+ ? agents.find((agent) => /\bresearch|analyst\b/i.test(String(agent.role_class || agent.role_label || '')))
3948
+ : /\b(build|code|implement|create|fix|write|update)\b/i.test(normalized)
3949
+ ? agents.find((agent) => /\bcode|coding|developer|builder\b/i.test(String(agent.role_class || agent.role_label || '')))
3950
+ : undefined;
3951
+ const routeAgent = namedAgent || roleMatchedAgent || defaultAgent;
3952
+ const route = routeAgent
3953
+ ? {
3954
+ agentId: routeAgent.id,
3955
+ agentName: routeAgent.name,
3956
+ provider: routeAgent.provider,
3957
+ model: routeAgent.model,
3958
+ reasoning: namedAgent
3959
+ ? 'Matched the explicitly named bot.'
3960
+ : roleMatchedAgent
3961
+ ? 'Matched the prompt to the configured bot role.'
3962
+ : 'Fell back to the configured default bot.',
3963
+ }
3964
+ : { routing: 'default', reason: 'No bot configured' };
3850
3965
  res.json(route);
3851
3966
  }
3852
3967
  catch (err) {
@@ -4752,6 +4867,13 @@ function runtimeModeLabel(mode, runtimeSource) {
4752
4867
  return (0, subscription_runtime_1.claudeSubscriptionRuntimeLabel)(runtimeSource);
4753
4868
  return 'API Key';
4754
4869
  }
4870
+ function resolveDirectCliSessionTransport(providerName, enableCliSessionEpoch, localStorageMode) {
4871
+ if (!enableCliSessionEpoch)
4872
+ return 'none';
4873
+ if (providerName === 'codex-cli' && localStorageMode)
4874
+ return 'codex-app-server';
4875
+ return 'pty';
4876
+ }
4755
4877
  function runtimePayloadForDisplay(providerName, model, runtimeMode, runtimeSource) {
4756
4878
  return {
4757
4879
  mode: runtimeMode,
@@ -4783,6 +4905,9 @@ function configuredRuntimeLabelForProfile(profile) {
4783
4905
  async function buildChatRuntimeForTest(profile) {
4784
4906
  return buildChatRuntime(profile);
4785
4907
  }
4908
+ function resolveDirectCliSessionTransportForTest(providerName, enableCliSessionEpoch, localStorageMode) {
4909
+ return resolveDirectCliSessionTransport(providerName, enableCliSessionEpoch, localStorageMode);
4910
+ }
4786
4911
  function buildConfiguredMessageModel(profile) {
4787
4912
  if (!profile)
4788
4913
  return '';
@@ -4808,100 +4933,231 @@ function hydrateMessageDisplayMetadata(message) {
4808
4933
  model: [modelBase, runtimeLabel].filter(Boolean).join(' | '),
4809
4934
  };
4810
4935
  }
4811
- function classifyOrchestratorProgress(status) {
4812
- const trimmed = String(status || '').trim();
4813
- if (!trimmed)
4814
- return {};
4815
- const match = trimmed.match(/^([a-z][a-z0-9_ -]{1,48})::\s*(.+)$/i);
4816
- if (!match) {
4817
- return {
4818
- activityText: trimmed.replace(/\*\*/g, '').replace(/\s+/g, ' ').trim(),
4819
- chatText: trimmed,
4820
- };
4936
+ function buildLocalDesktopDirectPrompt(input) {
4937
+ const isCliProvider = index_1.CLI_PROVIDERS.has(input.currentProvider);
4938
+ const useCompletionSentinel = !!input.useCompletionSentinel;
4939
+ const useCliHistoryBootstrap = isCliProvider && !input.isCliRecurring;
4940
+ const crossBotReplies = isCliProvider && input.isCliRecurring
4941
+ ? getLatestOtherBotReplies(input.conversationId, input.currentBotId, input.currentBotName)
4942
+ : [];
4943
+ const contract = !isCliProvider || !input.isCliRecurring
4944
+ ? 'api_or_fresh_cli'
4945
+ : crossBotReplies.length > 0
4946
+ ? 'cli_recurring_multibot'
4947
+ : 'cli_recurring_single';
4948
+ const lines = [
4949
+ '[Bot Identity]',
4950
+ input.soulMd.trim(),
4951
+ '',
4952
+ 'If the provided context is not enough, use available tools to get what you need.',
4953
+ '',
4954
+ '[Response Style]',
4955
+ 'Write in short readable paragraphs.',
4956
+ 'Put a blank line between distinct ideas.',
4957
+ 'Use bullets when listing findings, steps, or issues.',
4958
+ 'Do not return one dense wall of text.',
4959
+ 'Keep progress updates compact and factual.',
4960
+ 'Do not mention bootstrap files, history files, file paths, or that you loaded context unless the user explicitly asks about them.',
4961
+ ];
4962
+ if (useCompletionSentinel) {
4963
+ lines.push(completion_marker_1.CLI_COMPLETION_INSTRUCTION);
4821
4964
  }
4822
- const roleName = match[1].trim().toLowerCase();
4823
- const detail = match[2].trim();
4824
- if (roleName === 'intent_classifier') {
4825
- if (/decomposing request|classifying request/i.test(detail)) {
4826
- return { activityText: 'Classifying request', chatText: 'Classifying request..' };
4965
+ const projectLines = [];
4966
+ if (input.projectName)
4967
+ projectLines.push(`Project: ${input.projectName}`);
4968
+ if (input.topicTitle)
4969
+ projectLines.push(`Topic: ${input.topicTitle}`);
4970
+ projectLines.push(`Workspace: ${input.workspacePath || '(project folder not configured)'}`);
4971
+ if (input.timezone)
4972
+ projectLines.push(`Timezone: ${input.timezone}`);
4973
+ lines.push('', '[Project Overview]', ...projectLines);
4974
+ const toolManifest = isCliProvider ? '' : buildLocalDesktopToolManifest(input.availableTools, contract);
4975
+ if (toolManifest) {
4976
+ lines.push('', '[Available Tools]', toolManifest);
4977
+ if (/\bsearch_local_memory\b|\bsearch_memory\b/i.test(toolManifest)) {
4978
+ lines.push('If the user refers to prior project or conversation context that is not included here, search with these tools first.');
4827
4979
  }
4828
- if (/routed single intent as (.+)$/i.test(detail)) {
4829
- const classifiedAs = detail.replace(/^routed single intent as\s+/i, '').trim();
4830
- return { activityText: `Classified as ${classifiedAs}`, chatText: `Classified as ${classifiedAs}..` };
4980
+ }
4981
+ if (contract === 'api_or_fresh_cli' && !useCliHistoryBootstrap) {
4982
+ const summaryWindow = (0, context_window_1.getPromptContextWindow)(input.conversationId, safeguards_1.SAFEGUARDS.CONTEXT_WINDOW_TURNS);
4983
+ if (summaryWindow.summary?.summary_text?.trim()) {
4984
+ lines.push('', '[Context Summary]', summaryWindow.summary.summary_text.trim());
4831
4985
  }
4832
- const orderedCount = detail.match(/decomposed request into (\d+) ordered intents/i);
4833
- if (orderedCount) {
4834
- return { activityText: `${orderedCount[1]} ordered steps established` };
4986
+ const recentTurnsWindow = (0, context_window_1.getPromptContextWindow)(input.conversationId, summaryWindow.summary?.summary_text?.trim()
4987
+ ? safeguards_1.SAFEGUARDS.CONTEXT_WINDOW_TURNS
4988
+ : safeguards_1.SAFEGUARDS.NO_SUMMARY_CONTEXT_WINDOW_TURNS);
4989
+ if (recentTurnsWindow.turns.length > 0) {
4990
+ lines.push('', '[Recent Messages]', (0, context_window_1.formatTurnsForPrompt)(recentTurnsWindow.turns));
4835
4991
  }
4836
- return {};
4837
4992
  }
4838
- if (roleName === 'orchestration_planner') {
4839
- if (/planning multi-step workflow/i.test(detail)) {
4840
- return { activityText: 'Planning multi-step workflow', chatText: 'Planning multi-step workflow' };
4841
- }
4842
- const prepared = detail.match(/prepared (\d+) workflow steps/i);
4843
- if (prepared) {
4844
- return {
4845
- activityText: `${prepared[1]} workflow steps established`,
4846
- chatText: `${prepared[1]} workflow steps established`,
4847
- };
4848
- }
4849
- return {};
4993
+ let effectiveUserPrompt = input.userPrompt;
4994
+ if (useCliHistoryBootstrap && input.cliHistoryFilePath) {
4995
+ effectiveUserPrompt = [
4996
+ `Please read the history file at: ${input.cliHistoryFilePath}`,
4997
+ 'Use it for context only.',
4998
+ 'There is no need to mention the file, its path, or that you loaded context unless the user explicitly asks about it.',
4999
+ 'Then respond to the user request below.',
5000
+ '',
5001
+ 'Current user request:',
5002
+ input.userPrompt,
5003
+ ].join('\n');
4850
5004
  }
4851
- if (roleName === 'dispatch_controller') {
4852
- if (/still working:/i.test(detail))
4853
- return {};
4854
- if (/locked .* execution/i.test(detail))
4855
- return {};
4856
- return { activityText: detail };
5005
+ if (contract === 'cli_recurring_multibot' && crossBotReplies.length > 0) {
5006
+ effectiveUserPrompt = [
5007
+ '[Cross-Bot Context]',
5008
+ ...crossBotReplies.map((line) => `- ${line}`),
5009
+ '',
5010
+ 'Current user request:',
5011
+ input.userPrompt,
5012
+ ].join('\n');
4857
5013
  }
4858
- if (roleName === 'orchestrator') {
4859
- return {
4860
- activityText: detail,
4861
- };
5014
+ if (useCompletionSentinel) {
5015
+ effectiveUserPrompt = [
5016
+ effectiveUserPrompt,
5017
+ '',
5018
+ `Required final line when the response is fully complete: ${completion_marker_1.CLI_COMPLETION_SENTINEL}`,
5019
+ 'Do not mention or explain the tag.',
5020
+ ].join('\n');
4862
5021
  }
4863
- if (roleName === 'policy_interpreter' || roleName === 'verifier') {
4864
- return { activityText: detail };
5022
+ return {
5023
+ systemPrompt: lines.join('\n').trim(),
5024
+ userPrompt: effectiveUserPrompt,
5025
+ };
5026
+ }
5027
+ function buildLocalDesktopDirectPromptForTest(input) {
5028
+ return buildLocalDesktopDirectPrompt(input);
5029
+ }
5030
+ function writeCliBootstrapHistoryFile(input) {
5031
+ const historyDir = path.join(input.projectPath, 'history');
5032
+ const filename = `${input.conversationId}--${input.botId}.txt`;
5033
+ const historyFilePath = path.join(historyDir, filename);
5034
+ const historyContent = buildCliBootstrapHistoryContent(input.conversationId, input.topicId);
5035
+ try {
5036
+ fs.mkdirSync(historyDir, { recursive: true });
5037
+ cleanupStaleCliHistoryFiles(historyDir);
5038
+ fs.writeFileSync(historyFilePath, historyContent, 'utf8');
5039
+ return historyFilePath;
5040
+ }
5041
+ catch (err) {
5042
+ console.warn(chalk_1.default.yellow(` [cli-history] Failed to write bootstrap history file: ${err instanceof Error ? err.message : String(err)}`));
5043
+ return null;
4865
5044
  }
4866
- return {};
4867
5045
  }
4868
- /** Derive user-visible interim messages from orchestrator progress for key transitions */
4869
- function deriveOrchestratorInterimMessage(status) {
5046
+ function buildCliBootstrapHistoryContent(conversationId, topicId) {
5047
+ const lines = [];
5048
+ const previousConversationId = topicId ? data.getPreviousConversationInTopic(conversationId)?.id || null : null;
5049
+ lines.push('[Bootstrap History]');
5050
+ if (!topicId) {
5051
+ lines.push('No prior topic history is available for this conversation.');
5052
+ return lines.join('\n').trim();
5053
+ }
5054
+ if (!previousConversationId) {
5055
+ lines.push('No prior topic history is available for this conversation.');
5056
+ return lines.join('\n').trim();
5057
+ }
5058
+ const rollingSummary = (0, context_window_1.getLatestRollingSummary)(previousConversationId);
5059
+ const recentTurns = (0, context_window_1.getRecentTurns)(previousConversationId, 5);
5060
+ if (rollingSummary?.summary_text?.trim()) {
5061
+ lines.push('', '[Running Summary]', rollingSummary.summary_text.trim());
5062
+ }
5063
+ if (recentTurns.length > 0) {
5064
+ lines.push('', '[Last 5 Turns]', (0, context_window_1.formatTurnsForPrompt)(recentTurns));
5065
+ }
5066
+ else {
5067
+ lines.push('', '[Last 5 Turns]', '(no recent topic turns)');
5068
+ }
5069
+ return lines.join('\n').trim();
5070
+ }
5071
+ function cleanupStaleCliHistoryFiles(historyDir) {
5072
+ const maxAgeMs = 14 * 24 * 60 * 60 * 1000;
5073
+ const now = Date.now();
5074
+ try {
5075
+ const files = fs.readdirSync(historyDir);
5076
+ for (const file of files) {
5077
+ if (!file.toLowerCase().endsWith('.txt'))
5078
+ continue;
5079
+ const fullPath = path.join(historyDir, file);
5080
+ try {
5081
+ const stat = fs.statSync(fullPath);
5082
+ if (now - stat.mtimeMs > maxAgeMs) {
5083
+ fs.unlinkSync(fullPath);
5084
+ }
5085
+ }
5086
+ catch {
5087
+ // best effort cleanup only
5088
+ }
5089
+ }
5090
+ }
5091
+ catch {
5092
+ // best effort cleanup only
5093
+ }
5094
+ }
5095
+ function buildLocalDesktopToolManifest(availableTools, contract) {
5096
+ const selectedTools = contract === 'api_or_fresh_cli'
5097
+ ? availableTools
5098
+ : availableTools.filter((tool) => ['search_local_memory', 'search_memory'].includes(tool.name));
5099
+ return selectedTools
5100
+ .map((tool) => `- ${tool.name}: ${tool.description}`)
5101
+ .join('\n')
5102
+ .trim();
5103
+ }
5104
+ function getLatestOtherBotReplies(conversationId, currentBotId, currentBotName) {
5105
+ const turns = (0, context_window_1.getPromptContextWindow)(conversationId, safeguards_1.SAFEGUARDS.NO_SUMMARY_CONTEXT_WINDOW_TURNS).turns;
5106
+ const seen = new Set();
5107
+ const replies = [];
5108
+ for (let turnIndex = turns.length - 1; turnIndex >= 0; turnIndex -= 1) {
5109
+ const turn = turns[turnIndex];
5110
+ for (let responseIndex = turn.responses.length - 1; responseIndex >= 0; responseIndex -= 1) {
5111
+ const response = turn.responses[responseIndex];
5112
+ if (response.role !== 'assistant')
5113
+ continue;
5114
+ const identity = String(response.bot_id || response.agent_name || '').trim();
5115
+ if (!identity)
5116
+ continue;
5117
+ const sameBot = (response.bot_id && response.bot_id === currentBotId)
5118
+ || (response.agent_name && response.agent_name === currentBotName);
5119
+ if (sameBot || seen.has(identity))
5120
+ continue;
5121
+ const content = String(response.content || '').trim();
5122
+ if (!content)
5123
+ continue;
5124
+ seen.add(identity);
5125
+ replies.unshift(`${response.agent_name || 'Assistant'} (${response.created_at}): ${content}`);
5126
+ }
5127
+ }
5128
+ return replies;
5129
+ }
5130
+ function classifyOrchestratorProgress(status) {
4870
5131
  const trimmed = String(status || '').trim();
5132
+ if (!trimmed)
5133
+ return {};
4871
5134
  const match = trimmed.match(/^([a-z][a-z0-9_ -]{1,48})::\s*(.+)$/i);
4872
5135
  if (!match)
4873
- return null;
5136
+ return {};
4874
5137
  const roleName = match[1].trim().toLowerCase();
4875
5138
  const detail = match[2].trim();
4876
- // Intent classification → "I'll analyze this request..."
4877
- if (roleName === 'intent_classifier') {
4878
- if (/classif/i.test(detail))
4879
- return null; // skip classification (too early)
4880
- if (/decomposed request into (\d+)/i.test(detail)) {
4881
- const m = detail.match(/(\d+)/);
4882
- return `I've broken this down into ${m?.[1] || 'multiple'} steps. Let me work through them...`;
4883
- }
4884
- if (/routed single intent/i.test(detail))
4885
- return null; // handled by dispatch
5139
+ if (/(intent_classifier|orchestration_planner|dispatch_controller|policy_interpreter|verifier)/i.test(roleName)) {
5140
+ return {};
4886
5141
  }
4887
- // Dispatch "I'm sending this to [Bot Name]..."
4888
- if (roleName === 'dispatch_controller') {
4889
- const routingMatch = detail.match(/routing (?:direct )?request to (.+)/i);
4890
- if (routingMatch) {
4891
- return `Sending this to ${routingMatch[1]}...`;
5142
+ if (roleName === 'orchestrator') {
5143
+ if (/^understanding request$/i.test(detail)
5144
+ || /^still understanding the request$/i.test(detail)
5145
+ || /is working on the request/i.test(detail)
5146
+ || /completed the request/i.test(detail)) {
5147
+ return {};
4892
5148
  }
4893
- const connectMatch = detail.match(/connecting request to (.+)/i);
4894
- if (connectMatch) {
4895
- return `Connecting to ${connectMatch[1]}...`;
5149
+ if (/hit an issue while working/i.test(detail)) {
5150
+ return { activityText: detail };
4896
5151
  }
4897
5152
  }
5153
+ return {};
5154
+ }
5155
+ /** Derive user-visible interim messages from orchestrator progress for key transitions */
5156
+ function deriveOrchestratorInterimMessage(status) {
5157
+ void status;
5158
+ // Intent classification → "I'll analyze this request..."
5159
+ // Dispatch → "I'm sending this to [Bot Name]..."
4898
5160
  // Orchestration planner → workflow planning
4899
- if (roleName === 'orchestration_planner') {
4900
- const stepsMatch = detail.match(/prepared (\d+) workflow steps/i);
4901
- if (stepsMatch) {
4902
- return `I've prepared a ${stepsMatch[1]}-step workflow. Starting execution...`;
4903
- }
4904
- }
4905
5161
  return null;
4906
5162
  }
4907
5163
  function resolveCliNameForProvider(providerName) {
@@ -4931,6 +5187,11 @@ function detectInteractiveAuthFailure(text, activeProviderName, configuredProvid
4931
5187
  };
4932
5188
  }
4933
5189
  const LOCAL_RUNTIME_RETRY_LIMIT = 2;
5190
+ function isClaudeFreshSessionStartupFailure(err) {
5191
+ return err?.code === 'CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT'
5192
+ || err?.name === 'ClaudeFreshSessionStartupTimeoutError'
5193
+ || /fresh session startup timed out/i.test(String(err?.message || err || ''));
5194
+ }
4934
5195
  function shouldRetrySelectedLocalRuntime(err) {
4935
5196
  const text = String(err?.message || err || '').toLowerCase();
4936
5197
  if (!text)