funolio-agent 1.0.7 → 1.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/dist/approval.d.ts +1 -0
  2. package/dist/approval.d.ts.map +1 -1
  3. package/dist/approval.js +12 -0
  4. package/dist/approval.js.map +1 -1
  5. package/dist/auth/auto-detect.d.ts +11 -3
  6. package/dist/auth/auto-detect.d.ts.map +1 -1
  7. package/dist/auth/auto-detect.js +136 -168
  8. package/dist/auth/auto-detect.js.map +1 -1
  9. package/dist/auth/subscription-runtime.js +1 -1
  10. package/dist/auth/subscription-runtime.js.map +1 -1
  11. package/dist/backfill.d.ts.map +1 -1
  12. package/dist/backfill.js +31 -28
  13. package/dist/backfill.js.map +1 -1
  14. package/dist/bot-manager.d.ts +16 -4
  15. package/dist/bot-manager.d.ts.map +1 -1
  16. package/dist/bot-manager.js +280 -73
  17. package/dist/bot-manager.js.map +1 -1
  18. package/dist/clerk-model.d.ts +10 -6
  19. package/dist/clerk-model.d.ts.map +1 -1
  20. package/dist/clerk-model.js +39 -16
  21. package/dist/clerk-model.js.map +1 -1
  22. package/dist/cli.js +7 -2
  23. package/dist/cli.js.map +1 -1
  24. package/dist/commands/import-history.js +5 -1
  25. package/dist/commands/import-history.js.map +1 -1
  26. package/dist/commands/start.d.ts.map +1 -1
  27. package/dist/commands/start.js +57 -225
  28. package/dist/commands/start.js.map +1 -1
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +13 -2
  31. package/dist/config.js.map +1 -1
  32. package/dist/context-window.js +1 -1
  33. package/dist/context-window.js.map +1 -1
  34. package/dist/import-parser-core.d.ts.map +1 -1
  35. package/dist/import-parser-core.js +74 -8
  36. package/dist/import-parser-core.js.map +1 -1
  37. package/dist/local-data.d.ts +59 -6
  38. package/dist/local-data.d.ts.map +1 -1
  39. package/dist/local-data.js +544 -62
  40. package/dist/local-data.js.map +1 -1
  41. package/dist/local-db.d.ts.map +1 -1
  42. package/dist/local-db.js +21 -1
  43. package/dist/local-db.js.map +1 -1
  44. package/dist/local-funnel.d.ts +0 -7
  45. package/dist/local-funnel.d.ts.map +1 -1
  46. package/dist/local-funnel.js +19 -28
  47. package/dist/local-funnel.js.map +1 -1
  48. package/dist/local-import-worker.d.ts.map +1 -1
  49. package/dist/local-import-worker.js +49 -4
  50. package/dist/local-import-worker.js.map +1 -1
  51. package/dist/local-memory-search.d.ts.map +1 -1
  52. package/dist/local-memory-search.js +6 -3
  53. package/dist/local-memory-search.js.map +1 -1
  54. package/dist/local-server.d.ts +37 -0
  55. package/dist/local-server.d.ts.map +1 -1
  56. package/dist/local-server.js +731 -227
  57. package/dist/local-server.js.map +1 -1
  58. package/dist/mcp/local-memory-server.d.ts +1 -1
  59. package/dist/mcp/local-memory-server.d.ts.map +1 -1
  60. package/dist/mcp/local-memory-server.js +23 -11
  61. package/dist/mcp/local-memory-server.js.map +1 -1
  62. package/dist/mcp/manager.d.ts.map +1 -1
  63. package/dist/mcp/manager.js +86 -18
  64. package/dist/mcp/manager.js.map +1 -1
  65. package/dist/message-loop.d.ts +5 -10
  66. package/dist/message-loop.d.ts.map +1 -1
  67. package/dist/message-loop.js +393 -289
  68. package/dist/message-loop.js.map +1 -1
  69. package/dist/mqtt-client.d.ts +4 -1
  70. package/dist/mqtt-client.d.ts.map +1 -1
  71. package/dist/mqtt-client.js.map +1 -1
  72. package/dist/orchestration/orchestrator-blocked-prompt.d.ts +2 -1
  73. package/dist/orchestration/orchestrator-blocked-prompt.d.ts.map +1 -1
  74. package/dist/orchestration/orchestrator-blocked-prompt.js +11 -0
  75. package/dist/orchestration/orchestrator-blocked-prompt.js.map +1 -1
  76. package/dist/orchestration/orchestrator-final-response-prompt.d.ts +4 -1
  77. package/dist/orchestration/orchestrator-final-response-prompt.d.ts.map +1 -1
  78. package/dist/orchestration/orchestrator-final-response-prompt.js +8 -6
  79. package/dist/orchestration/orchestrator-final-response-prompt.js.map +1 -1
  80. package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
  81. package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
  82. package/dist/orchestration/worker-operating-prompt.js +39 -0
  83. package/dist/orchestration/worker-operating-prompt.js.map +1 -1
  84. package/dist/orchestrator.d.ts +13 -0
  85. package/dist/orchestrator.d.ts.map +1 -1
  86. package/dist/orchestrator.js +235 -133
  87. package/dist/orchestrator.js.map +1 -1
  88. package/dist/providers/anthropic.d.ts +0 -5
  89. package/dist/providers/anthropic.d.ts.map +1 -1
  90. package/dist/providers/anthropic.js +29 -75
  91. package/dist/providers/anthropic.js.map +1 -1
  92. package/dist/providers/claude-cli.d.ts.map +1 -1
  93. package/dist/providers/claude-cli.js +16 -140
  94. package/dist/providers/claude-cli.js.map +1 -1
  95. package/dist/providers/codex-cli.d.ts.map +1 -1
  96. package/dist/providers/codex-cli.js +78 -59
  97. package/dist/providers/codex-cli.js.map +1 -1
  98. package/dist/providers/google.d.ts.map +1 -1
  99. package/dist/providers/google.js +4 -2
  100. package/dist/providers/google.js.map +1 -1
  101. package/dist/providers/index.d.ts +2 -0
  102. package/dist/providers/index.d.ts.map +1 -1
  103. package/dist/providers/index.js.map +1 -1
  104. package/dist/providers/openai.d.ts.map +1 -1
  105. package/dist/providers/openai.js +27 -2
  106. package/dist/providers/openai.js.map +1 -1
  107. package/dist/storage-mode.d.ts +5 -0
  108. package/dist/storage-mode.d.ts.map +1 -0
  109. package/dist/storage-mode.js +21 -0
  110. package/dist/storage-mode.js.map +1 -0
  111. package/dist/summarization-pipeline.d.ts +9 -0
  112. package/dist/summarization-pipeline.d.ts.map +1 -1
  113. package/dist/summarization-pipeline.js +61 -17
  114. package/dist/summarization-pipeline.js.map +1 -1
  115. package/dist/tools/analyze-image.js +2 -2
  116. package/dist/tools/analyze-image.js.map +1 -1
  117. package/dist/tools/edit-file.js +3 -3
  118. package/dist/tools/edit-file.js.map +1 -1
  119. package/dist/tools/index.d.ts.map +1 -1
  120. package/dist/tools/index.js +36 -0
  121. package/dist/tools/index.js.map +1 -1
  122. package/dist/tools/list-directory.js +7 -4
  123. package/dist/tools/list-directory.js.map +1 -1
  124. package/dist/tools/read-file.js +3 -3
  125. package/dist/tools/read-file.js.map +1 -1
  126. package/dist/tools/run-command.js +3 -3
  127. package/dist/tools/run-command.js.map +1 -1
  128. package/dist/tools/sandbox.d.ts +10 -5
  129. package/dist/tools/sandbox.d.ts.map +1 -1
  130. package/dist/tools/sandbox.js +41 -13
  131. package/dist/tools/sandbox.js.map +1 -1
  132. package/dist/tools/search-codebase.js +2 -2
  133. package/dist/tools/search-codebase.js.map +1 -1
  134. package/dist/tools/search-local-memory.d.ts.map +1 -1
  135. package/dist/tools/search-local-memory.js +19 -8
  136. package/dist/tools/search-local-memory.js.map +1 -1
  137. package/dist/tools/spawn-subagent.js.map +1 -1
  138. package/dist/tools/write-file.js +3 -3
  139. package/dist/tools/write-file.js.map +1 -1
  140. package/dist/types.d.ts +2 -0
  141. package/dist/types.d.ts.map +1 -1
  142. package/dist/verification/index.js +2 -2
  143. package/dist/verification/index.js.map +1 -1
  144. package/dist/wizard-state.d.ts.map +1 -1
  145. package/dist/wizard-state.js +16 -2
  146. package/dist/wizard-state.js.map +1 -1
  147. package/dist/wizard-support.d.ts +2 -2
  148. package/dist/wizard-support.d.ts.map +1 -1
  149. package/dist/wizard-support.js +80 -93
  150. package/dist/wizard-support.js.map +1 -1
  151. package/dist/workflow-engine.d.ts +3 -1
  152. package/dist/workflow-engine.d.ts.map +1 -1
  153. package/dist/workflow-engine.js +131 -12
  154. package/dist/workflow-engine.js.map +1 -1
  155. package/package.json +1 -1
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.startLocalServer = startLocalServer;
40
40
  exports.stopLocalServer = stopLocalServer;
41
+ exports.buildChatRuntimeForTest = buildChatRuntimeForTest;
41
42
  /**
42
43
  * Local HTTP server for desktop-first operation.
43
44
  *
@@ -50,7 +51,6 @@ const index_2 = require("./index");
50
51
  const approval_1 = require("./approval");
51
52
  const config_1 = require("./config");
52
53
  const data = __importStar(require("./local-data"));
53
- const local_funnel_1 = require("./local-funnel");
54
54
  const local_import_worker_1 = require("./local-import-worker");
55
55
  const clerk_model_1 = require("./clerk-model");
56
56
  const workflow_engine_1 = require("./workflow-engine");
@@ -72,9 +72,11 @@ const marketplace_1 = require("./mcp/marketplace");
72
72
  const claude_config_writer_1 = require("./mcp/claude-config-writer");
73
73
  const subscription_runtime_1 = require("./auth/subscription-runtime");
74
74
  const local_memory_search_1 = require("./local-memory-search");
75
+ const local_funnel_1 = require("./local-funnel");
75
76
  const orchestrator_profile_1 = require("./orchestrator-profile");
76
77
  const policy_detection_1 = require("./policy-detection");
77
78
  const server_runtime_1 = require("./server-runtime");
79
+ const storage_mode_1 = require("./storage-mode");
78
80
  const server_adapter_1 = require("./server-adapter");
79
81
  const wizard_support_1 = require("./wizard-support");
80
82
  const chalk_1 = __importDefault(require("chalk"));
@@ -96,6 +98,9 @@ function startLocalServer(opts) {
96
98
  const express = requireExpress();
97
99
  const app = express();
98
100
  const port = opts.port ?? LOCAL_PORT;
101
+ if ((0, storage_mode_1.isLocalStorageMode)()) {
102
+ data.purgeLegacyExtractionDataOnce();
103
+ }
99
104
  const cliNormalization = data.normalizeCliProviderConnections();
100
105
  if (cliNormalization.updatedIds.length > 0) {
101
106
  console.info(`[local-server] normalized ${cliNormalization.updatedIds.length} CLI provider connection(s): ${cliNormalization.updatedIds.join(', ')}`);
@@ -183,6 +188,15 @@ function startLocalServer(opts) {
183
188
  catch (err) {
184
189
  console.error(chalk_1.default.yellow(` Failed to reconcile conversation/topic project consistency: ${err}`));
185
190
  }
191
+ try {
192
+ const interrupted = data.markRunningChatJobsInterrupted();
193
+ if (interrupted > 0) {
194
+ console.log(chalk_1.default.gray(` Marked ${interrupted} interrupted chat job(s) as failed`));
195
+ }
196
+ }
197
+ catch (err) {
198
+ console.error(chalk_1.default.yellow(` Failed to recover interrupted chat jobs: ${err}`));
199
+ }
186
200
  // ─── Health ──────────────────────────────────────────────────
187
201
  app.get('/api/health', (_req, res) => {
188
202
  try {
@@ -533,6 +547,181 @@ function startLocalServer(opts) {
533
547
  function isConnectedMode() {
534
548
  return (0, server_runtime_1.getRuntimeConnectionConfig)().connectionMode === 'server';
535
549
  }
550
+ function localTimestamp() {
551
+ return new Date().toISOString().replace('T', ' ').replace('Z', '');
552
+ }
553
+ const MAX_LOCAL_CHAT_JOBS = 3;
554
+ const runningChatJobControllers = new Map();
555
+ const finalizeCancelledChatJobMessage = (job) => {
556
+ const existing = data.getMessage(job.assistant_message_id);
557
+ const existingContent = (existing?.content || '').trim();
558
+ data.updateMessage(job.assistant_message_id, {
559
+ content: existingContent ? `${existingContent}\n\n[Stopped by user]` : '[Stopped by user]',
560
+ botId: job.bot_id,
561
+ });
562
+ data.touchConversationActivity(job.conversation_id);
563
+ };
564
+ const parseSsePayloads = async (response, handlers) => {
565
+ const reader = response.body?.getReader();
566
+ if (!reader)
567
+ return;
568
+ const decoder = new TextDecoder();
569
+ let buffer = '';
570
+ let currentEvent = '';
571
+ let receivedDone = false;
572
+ while (true) {
573
+ const { done, value } = await reader.read();
574
+ if (done)
575
+ break;
576
+ buffer += decoder.decode(value, { stream: true });
577
+ const lines = buffer.split('\n');
578
+ buffer = lines.pop() || '';
579
+ for (const line of lines) {
580
+ if (line.startsWith('event: ')) {
581
+ currentEvent = line.slice(7);
582
+ }
583
+ else if (line.startsWith('data: ') && currentEvent) {
584
+ let dataPayload = null;
585
+ try {
586
+ dataPayload = JSON.parse(line.slice(6));
587
+ }
588
+ catch {
589
+ currentEvent = '';
590
+ continue;
591
+ }
592
+ if (currentEvent === 'done') {
593
+ receivedDone = true;
594
+ await handlers.onDone?.(dataPayload);
595
+ }
596
+ else if (currentEvent === 'error') {
597
+ receivedDone = true;
598
+ await handlers.onError?.(dataPayload);
599
+ }
600
+ currentEvent = '';
601
+ }
602
+ }
603
+ }
604
+ if (!receivedDone) {
605
+ await handlers.onDone?.({});
606
+ }
607
+ };
608
+ const runQueuedChatJobs = async () => {
609
+ if (isConnectedMode())
610
+ return;
611
+ while (runningChatJobControllers.size < MAX_LOCAL_CHAT_JOBS) {
612
+ const next = data.listQueuedChatJobs(1)[0];
613
+ if (!next)
614
+ return;
615
+ if (runningChatJobControllers.has(next.id))
616
+ return;
617
+ const controller = new AbortController();
618
+ runningChatJobControllers.set(next.id, controller);
619
+ data.updateChatJob(next.id, {
620
+ status: 'running',
621
+ startedAt: localTimestamp(),
622
+ error: null,
623
+ });
624
+ void (async () => {
625
+ try {
626
+ const job = data.getChatJob(next.id);
627
+ if (!job)
628
+ return;
629
+ const userMessage = data.getMessage(job.user_message_id);
630
+ const requestPayload = (() => {
631
+ try {
632
+ return job.request_json ? JSON.parse(job.request_json) : {};
633
+ }
634
+ catch {
635
+ return {};
636
+ }
637
+ })();
638
+ const response = await fetch(`http://127.0.0.1:${port}/api/chat`, {
639
+ method: 'POST',
640
+ headers: { 'Content-Type': 'application/json' },
641
+ signal: controller.signal,
642
+ body: JSON.stringify({
643
+ conversationId: job.conversation_id,
644
+ message: userMessage?.content || '',
645
+ botId: job.bot_id,
646
+ skipUserMessage: true,
647
+ pinnedMessageIds: Array.isArray(requestPayload?.pinnedMessageIds) ? requestPayload.pinnedMessageIds : undefined,
648
+ topicId: requestPayload?.topicId || undefined,
649
+ projectId: requestPayload?.projectId || undefined,
650
+ orchestrationEnabled: requestPayload?.orchestrationEnabled !== false,
651
+ chatJobId: job.id,
652
+ assistantMessageId: job.assistant_message_id,
653
+ }),
654
+ });
655
+ if (!response.ok) {
656
+ const body = await response.json().catch(() => ({ error: `HTTP ${response.status}` }));
657
+ throw new Error(body.error || `HTTP ${response.status}`);
658
+ }
659
+ await parseSsePayloads(response, {
660
+ onDone: async () => {
661
+ const latest = data.getChatJob(next.id);
662
+ if (!latest || latest.status === 'cancelled')
663
+ return;
664
+ data.updateChatJob(next.id, {
665
+ status: 'completed',
666
+ completedAt: localTimestamp(),
667
+ error: null,
668
+ });
669
+ data.touchConversationActivity(next.conversation_id);
670
+ },
671
+ onError: async (payload) => {
672
+ const latest = data.getChatJob(next.id);
673
+ if (!latest || latest.status === 'cancelled')
674
+ return;
675
+ const errorText = String(payload?.error || 'Background chat failed');
676
+ const existing = data.getMessage(next.assistant_message_id);
677
+ const existingContent = (existing?.content || '').trim();
678
+ data.updateMessage(next.assistant_message_id, {
679
+ content: existingContent ? `${existingContent}\n\n**Error:** ${errorText}` : `**Error:** ${errorText}`,
680
+ botId: next.bot_id,
681
+ });
682
+ data.updateChatJob(next.id, {
683
+ status: 'failed',
684
+ error: errorText,
685
+ completedAt: localTimestamp(),
686
+ });
687
+ data.touchConversationActivity(next.conversation_id);
688
+ },
689
+ });
690
+ }
691
+ catch (err) {
692
+ const latest = data.getChatJob(next.id);
693
+ if (!latest)
694
+ return;
695
+ if (latest.status === 'cancelled' || err?.name === 'AbortError') {
696
+ finalizeCancelledChatJobMessage(latest);
697
+ data.updateChatJob(next.id, {
698
+ status: 'cancelled',
699
+ cancelledAt: latest.cancelled_at || localTimestamp(),
700
+ completedAt: latest.completed_at || localTimestamp(),
701
+ });
702
+ return;
703
+ }
704
+ const errorText = err?.message || 'Background chat failed';
705
+ const existing = data.getMessage(next.assistant_message_id);
706
+ const existingContent = (existing?.content || '').trim();
707
+ data.updateMessage(next.assistant_message_id, {
708
+ content: existingContent ? `${existingContent}\n\n**Error:** ${errorText}` : `**Error:** ${errorText}`,
709
+ botId: next.bot_id,
710
+ });
711
+ data.updateChatJob(next.id, {
712
+ status: 'failed',
713
+ error: errorText,
714
+ completedAt: localTimestamp(),
715
+ });
716
+ data.touchConversationActivity(next.conversation_id);
717
+ }
718
+ finally {
719
+ runningChatJobControllers.delete(next.id);
720
+ void runQueuedChatJobs();
721
+ }
722
+ })();
723
+ }
724
+ };
536
725
  async function relayConnectedChat(req, res) {
537
726
  if (!isConnectedMode())
538
727
  return false;
@@ -678,6 +867,18 @@ function startLocalServer(opts) {
678
867
  });
679
868
  return state.global.auth;
680
869
  }
870
+ async function exchangeDesktopAuthCode(code, redirectUri) {
871
+ const response = await fetch(`${config_1.FUNOLIO_API_URL}/api/v1/agent/auth/token`, {
872
+ method: 'POST',
873
+ headers: { 'Content-Type': 'application/json' },
874
+ body: JSON.stringify({ code, redirectUri }),
875
+ });
876
+ const body = await response.json().catch(() => ({}));
877
+ if (!response.ok) {
878
+ throw new Error(body.error || 'Code exchange failed');
879
+ }
880
+ return persistDesktopAuth(body);
881
+ }
681
882
  app.post('/api/auth/clear-session', (_req, res) => {
682
883
  try {
683
884
  const current = (0, wizard_state_1.loadWizardState)();
@@ -718,8 +919,8 @@ function startLocalServer(opts) {
718
919
  app.post('/api/auth/google/start', (_req, res) => {
719
920
  try {
720
921
  const state = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
721
- const redirectUri = 'funolio://wizard?step=login';
722
- const authUrl = `${config_1.FUNOLIO_API_URL}/auth/agent?state=${encodeURIComponent(state)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
922
+ const redirectUri = `http://127.0.0.1:${port}/auth/complete`;
923
+ const authUrl = `${config_1.FUNOLIO_API_URL}/auth/agent-google?state=${encodeURIComponent(state)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
723
924
  res.json({ authUrl, state, redirectUri });
724
925
  }
725
926
  catch (err) {
@@ -732,21 +933,55 @@ function startLocalServer(opts) {
732
933
  if (!code) {
733
934
  return res.status(400).json({ error: 'Code is required' });
734
935
  }
735
- const response = await fetch(`${config_1.FUNOLIO_API_URL}/api/v1/agent/auth/token`, {
736
- method: 'POST',
737
- headers: { 'Content-Type': 'application/json' },
738
- body: JSON.stringify({ code, redirectUri: 'funolio://wizard?step=login' }),
739
- });
740
- const body = await response.json().catch(() => ({}));
741
- if (!response.ok) {
742
- return res.status(response.status).json({ error: body.error || 'Code exchange failed' });
743
- }
744
- res.json(await persistDesktopAuth(body));
936
+ res.json(await exchangeDesktopAuthCode(code, 'funolio://wizard?step=login'));
745
937
  }
746
938
  catch (err) {
747
939
  res.status(500).json({ error: err.message || 'Code exchange failed' });
748
940
  }
749
941
  });
942
+ app.get('/auth/complete', async (req, res) => {
943
+ const code = typeof req.query?.code === 'string' ? req.query.code.trim() : '';
944
+ const state = typeof req.query?.state === 'string' ? req.query.state.trim() : '';
945
+ const escapeHtml = (value) => value
946
+ .replace(/&/g, '&amp;')
947
+ .replace(/</g, '&lt;')
948
+ .replace(/>/g, '&gt;')
949
+ .replace(/"/g, '&quot;')
950
+ .replace(/'/g, '&#39;');
951
+ if (!code) {
952
+ return res.status(400).send(`<!doctype html>
953
+ <html><head><meta charset="utf-8" /><title>Funolio Login</title></head>
954
+ <body style="font-family:system-ui,sans-serif;background:#0b1020;color:#f8fafc;display:flex;min-height:100vh;align-items:center;justify-content:center;padding:32px;">
955
+ <div style="max-width:480px;background:#111827;border:1px solid rgba(148,163,184,.2);border-radius:16px;padding:28px;text-align:center;">
956
+ <h1 style="margin:0 0 12px;font-size:24px;">Missing login code</h1>
957
+ <p style="margin:0;color:#cbd5e1;line-height:1.6;">Funolio did not receive an auth code from the website. Close this tab and try Google sign-in again.</p>
958
+ </div>
959
+ </body></html>`);
960
+ }
961
+ try {
962
+ await exchangeDesktopAuthCode(code, `http://127.0.0.1:${port}/auth/complete`);
963
+ return res.send(`<!doctype html>
964
+ <html><head><meta charset="utf-8" /><title>Funolio Login Complete</title></head>
965
+ <body style="font-family:system-ui,sans-serif;background:#0b1020;color:#f8fafc;display:flex;min-height:100vh;align-items:center;justify-content:center;padding:32px;">
966
+ <div style="max-width:480px;background:#111827;border:1px solid rgba(148,163,184,.2);border-radius:16px;padding:28px;text-align:center;">
967
+ <h1 style="margin:0 0 12px;font-size:24px;">Funolio login complete</h1>
968
+ <p style="margin:0 0 16px;color:#cbd5e1;line-height:1.6;">Your desktop session is now authenticated${state ? ` for request <code style="color:#a78bfa;">${escapeHtml(state)}</code>` : ''}. Return to the Funolio app.</p>
969
+ <p style="margin:0;font-size:13px;color:#94a3b8;">You can close this browser tab.</p>
970
+ </div>
971
+ </body></html>`);
972
+ }
973
+ catch (err) {
974
+ return res.status(500).send(`<!doctype html>
975
+ <html><head><meta charset="utf-8" /><title>Funolio Login Failed</title></head>
976
+ <body style="font-family:system-ui,sans-serif;background:#0b1020;color:#f8fafc;display:flex;min-height:100vh;align-items:center;justify-content:center;padding:32px;">
977
+ <div style="max-width:560px;background:#111827;border:1px solid rgba(239,68,68,.28);border-radius:16px;padding:28px;text-align:center;">
978
+ <h1 style="margin:0 0 12px;font-size:24px;color:#f87171;">Funolio login failed</h1>
979
+ <p style="margin:0 0 16px;color:#fecaca;line-height:1.6;">${escapeHtml(err?.message || 'Code exchange failed')}</p>
980
+ <p style="margin:0;font-size:13px;color:#94a3b8;">Close this tab and try again.</p>
981
+ </div>
982
+ </body></html>`);
983
+ }
984
+ });
750
985
  app.post('/api/auth/forgot-password', async (req, res) => {
751
986
  try {
752
987
  const email = typeof req.body?.email === 'string' ? req.body.email.trim().toLowerCase() : '';
@@ -771,6 +1006,22 @@ function startLocalServer(opts) {
771
1006
  res.status(500).json({ error: err.message || 'Failed to send reset email' });
772
1007
  }
773
1008
  });
1009
+ app.use(async (req, res, next) => {
1010
+ const publicPrefixes = ['/api/health', '/api/runtime/', '/api/auth/', '/api/wizard/'];
1011
+ if (publicPrefixes.some((prefix) => req.path === prefix || req.path.startsWith(prefix))) {
1012
+ return next();
1013
+ }
1014
+ try {
1015
+ const auth = await getHydratedDesktopAuth();
1016
+ if (!auth?.hasSession || !auth?.token) {
1017
+ return res.status(401).json({ error: 'Authentication required' });
1018
+ }
1019
+ return next();
1020
+ }
1021
+ catch (err) {
1022
+ return res.status(500).json({ error: err.message || 'Authentication check failed' });
1023
+ }
1024
+ });
774
1025
  app.get('/api/providers/catalog', (_req, res) => {
775
1026
  try {
776
1027
  res.json({ providers: (0, wizard_support_1.getProviderCatalog)() });
@@ -1113,13 +1364,25 @@ function startLocalServer(opts) {
1113
1364
  const runtime = (0, server_runtime_1.getRuntimeConnectionConfig)();
1114
1365
  const auth = await getHydratedDesktopAuth();
1115
1366
  const profile = await (0, server_adapter_1.getServerBot)(auth, runtime, req.params.id);
1116
- return res.json({ profile, conversationCount: 0 });
1367
+ return res.json({ profile, conversationCount: 0, messageCount: 0, recentConversations: [] });
1117
1368
  }
1118
1369
  const profile = data.getAgentProfile(req.params.id);
1119
1370
  if (!profile)
1120
1371
  return res.status(404).json({ error: 'Not found' });
1121
1372
  const convCount = data.countConversations(req.params.id);
1122
- res.json({ profile, conversationCount: convCount });
1373
+ const messageCount = data.countMessagesForBot(req.params.id);
1374
+ const recentConversations = data.listBotConversationActivity(req.params.id, 8).map((conversation) => ({
1375
+ id: conversation.id,
1376
+ agent_id: conversation.agent_id,
1377
+ title: conversation.title,
1378
+ updated_at: conversation.updated_at,
1379
+ created_at: conversation.created_at,
1380
+ message_count: conversation.message_count,
1381
+ bot_message_count: conversation.bot_message_count,
1382
+ bot_last_message_at: conversation.bot_last_message_at,
1383
+ project_name: conversation.project_name,
1384
+ }));
1385
+ res.json({ profile, conversationCount: convCount, messageCount, recentConversations });
1123
1386
  }
1124
1387
  catch (err) {
1125
1388
  res.status(500).json({ error: err.message });
@@ -2141,11 +2404,158 @@ function startLocalServer(opts) {
2141
2404
  res.status(500).json({ error: err.message });
2142
2405
  }
2143
2406
  });
2407
+ app.post('/api/chat/jobs', async (req, res) => {
2408
+ try {
2409
+ if (isConnectedMode()) {
2410
+ return res.status(501).json({ error: 'Background chat jobs are local-mode only' });
2411
+ }
2412
+ const { conversationId, message, botId, pinnedMessageIds, topicId, projectId, orchestrationEnabled, } = req.body || {};
2413
+ if (!message || !String(message).trim()) {
2414
+ return res.status(400).json({ error: 'message is required' });
2415
+ }
2416
+ let profile = botId ? data.getAgentProfile(String(botId)) : data.getDefaultAgentProfile();
2417
+ if (!profile) {
2418
+ return res.status(400).json({ error: 'No bot configured. Create one first.' });
2419
+ }
2420
+ const shouldUseOrchestratorMode = orchestrationEnabled !== false && (0, orchestrator_profile_1.isOrchestratorProfile)(profile);
2421
+ if (shouldUseOrchestratorMode) {
2422
+ return res.status(400).json({ error: 'Background chat jobs do not support orchestrator mode yet.' });
2423
+ }
2424
+ let convId = conversationId ? String(conversationId) : '';
2425
+ if (convId) {
2426
+ const latestJob = data.getLatestConversationChatJob(convId);
2427
+ if (latestJob && (latestJob.status === 'queued' || latestJob.status === 'running')) {
2428
+ return res.status(409).json({ error: 'This conversation already has a pending response.' });
2429
+ }
2430
+ }
2431
+ if (!convId) {
2432
+ let topicProjectId = null;
2433
+ if (topicId) {
2434
+ const topic = data.getTopic(String(topicId));
2435
+ topicProjectId = topic?.project_id ?? null;
2436
+ }
2437
+ const requestedProjectId = (projectId ? String(projectId) : null) || topicProjectId;
2438
+ const conv = data.createConversation(profile.id, '', 'local', {
2439
+ projectId: requestedProjectId,
2440
+ });
2441
+ convId = conv.id;
2442
+ }
2443
+ if (topicId && convId) {
2444
+ try {
2445
+ data.upsertConversationTopicSegment(convId, String(topicId));
2446
+ }
2447
+ catch { /* best effort */ }
2448
+ }
2449
+ if (!topicId && projectId && convId) {
2450
+ const selectedProject = data.getProject(String(projectId));
2451
+ if (selectedProject) {
2452
+ data.updateConversation(convId, {
2453
+ projectId: selectedProject.id,
2454
+ projectName: selectedProject.name,
2455
+ });
2456
+ }
2457
+ }
2458
+ const savedUserMessage = data.addMessage(convId, 'user', String(message));
2459
+ (0, context_window_1.incrementTurnCount)(convId);
2460
+ const assistantMessage = data.addMessage(convId, 'assistant', '', profile.model || undefined, undefined, profile.id, profile.name);
2461
+ const conv = data.getConversation(convId);
2462
+ if (conv && !conv.title?.trim() && (conv.turn_count || 0) <= 1) {
2463
+ const shortTitle = String(message).slice(0, 60).replace(/\n/g, ' ').trim();
2464
+ data.updateConversation(convId, { title: shortTitle || 'New Chat' });
2465
+ }
2466
+ const job = data.createChatJob({
2467
+ conversationId: convId,
2468
+ userMessageId: savedUserMessage.id,
2469
+ assistantMessageId: assistantMessage.id,
2470
+ botId: profile.id,
2471
+ status: 'queued',
2472
+ requestJson: JSON.stringify({
2473
+ pinnedMessageIds: Array.isArray(pinnedMessageIds) ? pinnedMessageIds : [],
2474
+ topicId: topicId ? String(topicId) : null,
2475
+ projectId: projectId ? String(projectId) : null,
2476
+ orchestrationEnabled: orchestrationEnabled !== false,
2477
+ }),
2478
+ });
2479
+ void runQueuedChatJobs();
2480
+ res.status(201).json({
2481
+ ok: true,
2482
+ conversationId: convId,
2483
+ userMessageId: savedUserMessage.id,
2484
+ assistantMessageId: assistantMessage.id,
2485
+ jobId: job.id,
2486
+ status: job.status,
2487
+ });
2488
+ }
2489
+ catch (err) {
2490
+ res.status(500).json({ error: err.message });
2491
+ }
2492
+ });
2493
+ app.post('/api/chat/jobs/:id/cancel', async (req, res) => {
2494
+ try {
2495
+ const job = data.getChatJob(req.params.id);
2496
+ if (!job)
2497
+ return res.status(404).json({ error: 'Not found' });
2498
+ if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
2499
+ return res.json({ ok: true, status: job.status });
2500
+ }
2501
+ const ts = localTimestamp();
2502
+ data.updateChatJob(job.id, {
2503
+ status: 'cancelled',
2504
+ cancelledAt: ts,
2505
+ completedAt: ts,
2506
+ });
2507
+ const controller = runningChatJobControllers.get(job.id);
2508
+ if (controller) {
2509
+ controller.abort();
2510
+ }
2511
+ else {
2512
+ finalizeCancelledChatJobMessage(job);
2513
+ data.touchConversationActivity(job.conversation_id);
2514
+ }
2515
+ res.json({ ok: true, status: 'cancelled' });
2516
+ }
2517
+ catch (err) {
2518
+ res.status(500).json({ error: err.message });
2519
+ }
2520
+ });
2144
2521
  // ─── Chat (SSE streaming) ──────────────────────────────────
2522
+ app.post('/api/conversations/:id/chat-job/cancel', async (req, res) => {
2523
+ try {
2524
+ const latestJob = data.getLatestConversationChatJob(req.params.id);
2525
+ if (!latestJob)
2526
+ return res.status(404).json({ error: 'Chat job not found' });
2527
+ if (latestJob.status === 'completed' || latestJob.status === 'failed' || latestJob.status === 'cancelled') {
2528
+ return res.json({ ok: true, status: latestJob.status, jobId: latestJob.id });
2529
+ }
2530
+ const ts = localTimestamp();
2531
+ data.updateChatJob(latestJob.id, {
2532
+ status: 'cancelled',
2533
+ cancelledAt: ts,
2534
+ completedAt: ts,
2535
+ });
2536
+ const controller = runningChatJobControllers.get(latestJob.id);
2537
+ if (controller) {
2538
+ controller.abort();
2539
+ }
2540
+ else {
2541
+ finalizeCancelledChatJobMessage(latestJob);
2542
+ data.touchConversationActivity(latestJob.conversation_id);
2543
+ void runQueuedChatJobs();
2544
+ }
2545
+ res.json({ ok: true, status: 'cancelled', jobId: latestJob.id });
2546
+ }
2547
+ catch (err) {
2548
+ res.status(500).json({ error: err.message });
2549
+ }
2550
+ });
2145
2551
  app.post('/api/chat', async (req, res) => {
2146
2552
  const activityErrorContext = {};
2553
+ const routeAbortController = new AbortController();
2554
+ let responseEnded = false;
2555
+ const abortOnClientClose = () => routeAbortController.abort();
2556
+ req.on('close', abortOnClientClose);
2147
2557
  try {
2148
- const { conversationId, message, botId, skipUserMessage, pinnedMessageIds, topicId, projectId, workflowTemplateId } = req.body;
2558
+ const { conversationId, message, botId, skipUserMessage, pinnedMessageIds, topicId, projectId, workflowTemplateId, orchestrationEnabled, chatJobId, assistantMessageId, } = req.body;
2149
2559
  if (!message)
2150
2560
  return res.status(400).json({ error: 'message is required' });
2151
2561
  if (await relayConnectedChat(req, res)) {
@@ -2223,6 +2633,35 @@ function startLocalServer(opts) {
2223
2633
  // Best effort only
2224
2634
  }
2225
2635
  };
2636
+ const workerBotByName = new Map();
2637
+ const resolveWorkerBotId = (agentName) => {
2638
+ const key = String(agentName || '').trim().toLowerCase();
2639
+ if (!key)
2640
+ return null;
2641
+ if (workerBotByName.has(key))
2642
+ return workerBotByName.get(key) || null;
2643
+ const match = data.listAgentProfiles().find((agent) => agent.name.trim().toLowerCase() === key);
2644
+ const resolved = match?.id || null;
2645
+ workerBotByName.set(key, resolved || '');
2646
+ return resolved;
2647
+ };
2648
+ const recordWorkerActivity = (activityType, event, payload, summary) => {
2649
+ try {
2650
+ data.createMessageActivity({
2651
+ conversationId: convId,
2652
+ streamId: activityStreamId,
2653
+ botId: resolveWorkerBotId(event.agentName) || null,
2654
+ agentName: event.agentName || null,
2655
+ activityType,
2656
+ summary: summary || null,
2657
+ payload,
2658
+ expiresAt: activityExpiresAt,
2659
+ });
2660
+ }
2661
+ catch {
2662
+ // Best effort only
2663
+ }
2664
+ };
2226
2665
  // Save user message (skip if multi-bot call where first bot already saved it)
2227
2666
  let savedUserMessage = null;
2228
2667
  if (!skipUserMessage) {
@@ -2252,7 +2691,8 @@ function startLocalServer(opts) {
2252
2691
  }
2253
2692
  }
2254
2693
  // ─── Orchestrator Mode Branch ─────────────────────────
2255
- if ((0, orchestrator_profile_1.isOrchestratorProfile)(profile)) {
2694
+ const shouldUseOrchestratorMode = orchestrationEnabled !== false && (0, orchestrator_profile_1.isOrchestratorProfile)(profile);
2695
+ if (shouldUseOrchestratorMode) {
2256
2696
  const clerk = (0, clerk_model_1.getClerk)();
2257
2697
  if (!clerk) {
2258
2698
  // Fix #2: Do not silently fall through to direct chat — return a clear error
@@ -2280,6 +2720,8 @@ function startLocalServer(opts) {
2280
2720
  'X-Conversation-Id': convId,
2281
2721
  });
2282
2722
  const sendEvent = (event, payload) => {
2723
+ if (responseEnded)
2724
+ return;
2283
2725
  res.write(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`);
2284
2726
  };
2285
2727
  let orchestratorRuntimeLabel = '';
@@ -2302,6 +2744,7 @@ function startLocalServer(opts) {
2302
2744
  let lastProgressChat = '';
2303
2745
  let lastProgressActivity = '';
2304
2746
  let selfExecuteStreamed = false;
2747
+ let hasWorkerActivity = false;
2305
2748
  try {
2306
2749
  let lastInterimMessage = '';
2307
2750
  const response = await orchestrator.handleUserMessage(message, convId, {
@@ -2311,21 +2754,102 @@ function startLocalServer(opts) {
2311
2754
  workflowTemplateId: workflowTemplateId || undefined,
2312
2755
  onWorkerChunk: (event) => {
2313
2756
  if (event.type === 'step_start') {
2757
+ hasWorkerActivity = true;
2758
+ recordWorkerActivity('worker_step_start', event, {
2759
+ stepId: event.stepId,
2760
+ agentName: event.agentName,
2761
+ description: event.description,
2762
+ stepIndex: event.stepIndex,
2763
+ totalSteps: event.totalSteps,
2764
+ botId: resolveWorkerBotId(event.agentName),
2765
+ });
2314
2766
  sendEvent('worker_step_start', {
2315
2767
  stepId: event.stepId,
2768
+ botId: resolveWorkerBotId(event.agentName),
2769
+ agentName: event.agentName,
2770
+ description: event.description,
2771
+ stepIndex: event.stepIndex,
2772
+ totalSteps: event.totalSteps,
2773
+ });
2774
+ }
2775
+ else if (event.type === 'worker_chunk') {
2776
+ hasWorkerActivity = true;
2777
+ recordWorkerActivity('worker_chunk', event, {
2778
+ stepId: event.stepId,
2779
+ agentName: event.agentName,
2780
+ description: event.description,
2781
+ stepIndex: event.stepIndex,
2782
+ totalSteps: event.totalSteps,
2783
+ text: event.text,
2784
+ botId: resolveWorkerBotId(event.agentName),
2785
+ });
2786
+ sendEvent('worker_chunk', {
2787
+ stepId: event.stepId,
2788
+ botId: resolveWorkerBotId(event.agentName),
2789
+ agentName: event.agentName,
2790
+ description: event.description,
2791
+ stepIndex: event.stepIndex,
2792
+ totalSteps: event.totalSteps,
2793
+ text: event.text,
2794
+ });
2795
+ }
2796
+ else if (event.type === 'worker_tool_call') {
2797
+ hasWorkerActivity = true;
2798
+ recordWorkerActivity('worker_tool_call', event, {
2799
+ stepId: event.stepId,
2800
+ agentName: event.agentName,
2801
+ description: event.description,
2802
+ stepIndex: event.stepIndex,
2803
+ totalSteps: event.totalSteps,
2804
+ toolCallId: event.toolCallId,
2805
+ toolName: event.toolName,
2806
+ toolArguments: event.toolArguments,
2807
+ botId: resolveWorkerBotId(event.agentName),
2808
+ });
2809
+ sendEvent('worker_tool_call', {
2810
+ stepId: event.stepId,
2811
+ botId: resolveWorkerBotId(event.agentName),
2812
+ agentName: event.agentName,
2813
+ description: event.description,
2814
+ stepIndex: event.stepIndex,
2815
+ totalSteps: event.totalSteps,
2816
+ toolCallId: event.toolCallId,
2817
+ toolName: event.toolName,
2818
+ toolArguments: event.toolArguments,
2819
+ });
2820
+ }
2821
+ else if (event.type === 'worker_tool_result') {
2822
+ hasWorkerActivity = true;
2823
+ recordWorkerActivity('worker_tool_result', event, {
2824
+ stepId: event.stepId,
2825
+ agentName: event.agentName,
2826
+ description: event.description,
2827
+ stepIndex: event.stepIndex,
2828
+ totalSteps: event.totalSteps,
2829
+ toolCallId: event.toolCallId,
2830
+ toolName: event.toolName,
2831
+ toolOutput: event.toolOutput,
2832
+ toolIsError: event.toolIsError,
2833
+ botId: resolveWorkerBotId(event.agentName),
2834
+ });
2835
+ sendEvent('worker_tool_result', {
2836
+ stepId: event.stepId,
2837
+ botId: resolveWorkerBotId(event.agentName),
2316
2838
  agentName: event.agentName,
2317
2839
  description: event.description,
2318
2840
  stepIndex: event.stepIndex,
2319
2841
  totalSteps: event.totalSteps,
2842
+ toolCallId: event.toolCallId,
2843
+ toolName: event.toolName,
2844
+ toolOutput: event.toolOutput,
2845
+ toolIsError: event.toolIsError,
2320
2846
  });
2321
2847
  }
2322
2848
  else if (event.type === 'chunk') {
2323
- // Stream all text directly to main bubble — no worker cards
2324
2849
  selfExecuteStreamed = true;
2325
2850
  sendEvent('chunk', { text: event.text });
2326
2851
  }
2327
2852
  else if (event.type === 'tool_call') {
2328
- // Show tool activity inline in main bubble
2329
2853
  selfExecuteStreamed = true;
2330
2854
  sendEvent('chunk', { text: `\n> Running ${event.toolName}...\n` });
2331
2855
  }
@@ -2334,7 +2858,27 @@ function startLocalServer(opts) {
2334
2858
  sendEvent('chunk', { text: `> [${icon}] ${event.toolName} completed\n` });
2335
2859
  }
2336
2860
  else if (event.type === 'step_done') {
2337
- // Step lifecycle — no special handling needed
2861
+ hasWorkerActivity = true;
2862
+ recordWorkerActivity('worker_step_done', event, {
2863
+ stepId: event.stepId,
2864
+ agentName: event.agentName,
2865
+ description: event.description,
2866
+ stepIndex: event.stepIndex,
2867
+ totalSteps: event.totalSteps,
2868
+ status: event.status,
2869
+ summary: event.summary,
2870
+ botId: resolveWorkerBotId(event.agentName),
2871
+ });
2872
+ sendEvent('worker_step_done', {
2873
+ stepId: event.stepId,
2874
+ botId: resolveWorkerBotId(event.agentName),
2875
+ agentName: event.agentName,
2876
+ description: event.description,
2877
+ stepIndex: event.stepIndex,
2878
+ totalSteps: event.totalSteps,
2879
+ status: event.status,
2880
+ summary: event.summary,
2881
+ });
2338
2882
  }
2339
2883
  },
2340
2884
  onProgress: (status) => {
@@ -2358,7 +2902,7 @@ function startLocalServer(opts) {
2358
2902
  sendEvent('orchestrator_message', {
2359
2903
  text: interimMessage,
2360
2904
  botId: profile.id,
2361
- agentName: profile.name || 'Project Manager',
2905
+ agentName: 'Orchestrator',
2362
2906
  });
2363
2907
  }
2364
2908
  },
@@ -2369,40 +2913,73 @@ function startLocalServer(opts) {
2369
2913
  },
2370
2914
  });
2371
2915
  // Save O's response (no incrementTurnCount — Fix #1: user message already incremented it)
2372
- const savedMessage = data.addMessage(convId, 'assistant', response, orchestratorRuntimeLabel || undefined, undefined, profile.id, profile.name || 'Project Manager');
2373
- data.attachMessageActivitiesToMessage(activityStreamId, savedMessage.id);
2374
- data.createMessageActivity({
2375
- conversationId: convId,
2376
- messageId: savedMessage.id,
2377
- botId: profile.id,
2378
- agentName: profile.name || 'Project Manager',
2379
- activityType: 'message',
2380
- summary: 'Final orchestrator response',
2381
- payload: { content: response },
2382
- expiresAt: activityExpiresAt,
2383
- });
2916
+ const responseMeta = orchestrator.getLastResponseMeta();
2917
+ const finalAgentName = responseMeta?.agentName || 'Orchestrator';
2918
+ const finalBotId = responseMeta?.botId || (finalAgentName === 'Orchestrator' ? profile.id : undefined);
2919
+ const finalModelLabel = responseMeta?.modelLabel || orchestratorRuntimeLabel || undefined;
2920
+ const splitFinalMessage = hasWorkerActivity && finalAgentName === 'Clerk';
2921
+ if (splitFinalMessage) {
2922
+ const orchestratorMessage = data.addMessage(convId, 'assistant', '', orchestratorRuntimeLabel || buildConfiguredMessageModel(profile), undefined, profile.id, 'Orchestrator');
2923
+ data.attachMessageActivitiesToMessage(activityStreamId, orchestratorMessage.id);
2924
+ const clerkMessage = data.addMessage(convId, 'assistant', response, finalModelLabel, undefined, finalBotId, finalAgentName);
2925
+ data.createMessageActivity({
2926
+ conversationId: convId,
2927
+ messageId: clerkMessage.id,
2928
+ botId: finalBotId,
2929
+ agentName: finalAgentName,
2930
+ activityType: 'message',
2931
+ summary: 'Final assistant response',
2932
+ payload: { content: response },
2933
+ expiresAt: activityExpiresAt,
2934
+ });
2935
+ }
2936
+ else {
2937
+ const savedMessage = data.addMessage(convId, 'assistant', response, finalModelLabel, undefined, finalBotId, finalAgentName);
2938
+ data.attachMessageActivitiesToMessage(activityStreamId, savedMessage.id);
2939
+ data.createMessageActivity({
2940
+ conversationId: convId,
2941
+ messageId: savedMessage.id,
2942
+ botId: finalBotId,
2943
+ agentName: finalAgentName,
2944
+ activityType: 'message',
2945
+ summary: 'Final orchestrator response',
2946
+ payload: { content: response },
2947
+ expiresAt: activityExpiresAt,
2948
+ });
2949
+ }
2384
2950
  // Emit chunk + done events using the same SSE contract as normal chat
2385
2951
  // Skip the final bulk chunk if we already streamed via worker_chunk (execute_self)
2386
- if (!selfExecuteStreamed) {
2952
+ if (!selfExecuteStreamed && !splitFinalMessage) {
2387
2953
  sendEvent('chunk', { text: response });
2388
2954
  }
2389
2955
  sendEvent('done', {
2390
2956
  conversationId: convId,
2391
2957
  content: response,
2392
- ...(orchestratorRuntimePayload ? { runtime: orchestratorRuntimePayload } : {}),
2958
+ agentName: finalAgentName,
2959
+ botId: finalBotId,
2960
+ separateFinalMessage: splitFinalMessage,
2961
+ ...((responseMeta?.modelLabel || orchestratorRuntimePayload)
2962
+ ? {
2963
+ runtime: {
2964
+ ...(orchestratorRuntimePayload || {}),
2965
+ ...(responseMeta?.modelLabel ? { model: responseMeta.modelLabel } : {}),
2966
+ },
2967
+ }
2968
+ : {}),
2393
2969
  tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, isApproximate: true, source: 'heuristic' },
2394
2970
  });
2971
+ responseEnded = true;
2395
2972
  res.end();
2396
- // Background post-response processing
2397
- (0, local_funnel_1.scheduleFunnelProcessing)(convId);
2398
2973
  if ((0, context_window_1.shouldSummarize)(convId)) {
2399
2974
  const catalog = (0, backfill_1.buildProjectTopicCatalog)();
2400
2975
  (0, summarization_pipeline_1.runSummarization)(convId, catalog).catch(err => console.error(chalk_1.default.yellow(` [summarization] ${err.message}`)));
2401
2976
  }
2977
+ (0, local_funnel_1.scheduleFunnelProcessing)(convId);
2402
2978
  }
2403
2979
  catch (orchErr) {
2404
2980
  recordActivity('error', { error: orchErr.message }, orchErr.message);
2405
2981
  sendEvent('error', { type: 'error', error: orchErr.message });
2982
+ responseEnded = true;
2406
2983
  res.end();
2407
2984
  }
2408
2985
  return;
@@ -2414,10 +2991,13 @@ function startLocalServer(opts) {
2414
2991
  const effectiveTimezone = configuredTz && configuredTz.toLowerCase() !== 'system'
2415
2992
  ? configuredTz
2416
2993
  : Intl.DateTimeFormat().resolvedOptions().timeZone;
2994
+ const unrestrictedCliProfile = index_1.CLI_PROVIDERS.has(profile.provider);
2417
2995
  const allToolDefs = (0, index_2.getAllToolDefinitions)(mcpManager);
2418
2996
  const configuredBuiltinTools = parseToolSelectionJson(profile.enabled_builtin_tools_json);
2419
2997
  const configuredMcpTools = parseToolSelectionJson(profile.enabled_mcp_tools_json);
2420
- const allowedToolNames = expandAllowedToolNames(allToolDefs, configuredBuiltinTools, configuredMcpTools);
2998
+ const allowedToolNames = unrestrictedCliProfile
2999
+ ? new Set(allToolDefs.map((tool) => tool.name))
3000
+ : expandAllowedToolNames(allToolDefs, configuredBuiltinTools, configuredMcpTools);
2421
3001
  const toolDefs = allToolDefs.filter((tool) => allowedToolNames.has(tool.name));
2422
3002
  // Build system prompt via clerk (token-budgeted context injection)
2423
3003
  let systemPrompt;
@@ -2443,7 +3023,7 @@ function startLocalServer(opts) {
2443
3023
  availableTools: toolDefs.map((tool) => ({ name: tool.name, description: tool.description })),
2444
3024
  });
2445
3025
  systemPrompt = built.systemPrompt;
2446
- console.log(chalk_1.default.gray(` [clerk] Context: ${built.injectedFacts} facts, ${built.injectedDecisions} decisions, ${built.injectedSummaries} summaries (${built.contextTokensUsed} tokens)`));
3026
+ console.log(chalk_1.default.gray(` [clerk] Context: ${built.injectedSummaries} summaries (${built.contextTokensUsed} tokens)`));
2447
3027
  }
2448
3028
  else {
2449
3029
  // Fallback: manual prompt building
@@ -2508,8 +3088,11 @@ function startLocalServer(opts) {
2508
3088
  }
2509
3089
  catch { /* best-effort */ }
2510
3090
  }
2511
- // Resolve LLM runtime: for claude-cli/codex-cli, try Subscription-API first
2512
- // and fall back to Subscription-CLI automatically if API fails.
3091
+ // Resolve LLM runtime.
3092
+ // Desktop local mode intentionally supports only:
3093
+ // - Subscription CLI
3094
+ // - API Key
3095
+ // We do not use subscription-token API routing for local CLI bots.
2513
3096
  const runtime = await buildChatRuntime(profile);
2514
3097
  let activeProviderName = runtime.providerName;
2515
3098
  let activeModelName = runtime.model;
@@ -2537,12 +3120,16 @@ function startLocalServer(opts) {
2537
3120
  projectId: convId ? (data.getConversation(convId)?.project_id ?? null) : null,
2538
3121
  actorType: 'llm',
2539
3122
  actorId: profile?.name || profile?.id || 'LLM',
3123
+ restrictFileAccessToProject: unrestrictedCliProfile ? false : undefined,
3124
+ abortSignal: routeAbortController.signal,
2540
3125
  });
2541
3126
  const toolManifest = toolDefs
2542
3127
  .map(t => `- ${t.name}: ${t.description}`)
2543
3128
  .join('\n');
2544
3129
  if (toolManifest.trim()) {
2545
- systemPrompt += '\n\n[Available Tools]\nOnly the following tools are enabled for this bot in the current runtime:\n' + toolManifest;
3130
+ systemPrompt += unrestrictedCliProfile
3131
+ ? '\n\n[Available Tools]\nThe following tools are available in the current runtime:\n' + toolManifest
3132
+ : '\n\n[Available Tools]\nOnly the following tools are enabled for this bot in the current runtime:\n' + toolManifest;
2546
3133
  }
2547
3134
  // Inject pinned messages as context (user-selected cross-bot references)
2548
3135
  if (pinnedMessageIds && Array.isArray(pinnedMessageIds) && pinnedMessageIds.length > 0) {
@@ -2580,6 +3167,8 @@ function startLocalServer(opts) {
2580
3167
  'X-Conversation-Id': convId,
2581
3168
  });
2582
3169
  const sendEvent = (event, payload) => {
3170
+ if (responseEnded)
3171
+ return;
2583
3172
  res.write(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`);
2584
3173
  };
2585
3174
  const approxInputTokens = (0, token_counter_1.estimatePromptInputTokens)({
@@ -2608,12 +3197,47 @@ function startLocalServer(opts) {
2608
3197
  detail: `Sending request to ${activeProviderName}...`,
2609
3198
  runtime: runtimePayload(),
2610
3199
  }, `Sending request to ${activeProviderName}...`);
3200
+ let partialPersistedContent = '';
3201
+ let partialPersistedAt = 0;
3202
+ const throwIfChatJobCancelled = () => {
3203
+ if (!chatJobId)
3204
+ return;
3205
+ const currentJob = data.getChatJob(String(chatJobId));
3206
+ if (currentJob?.status === 'cancelled') {
3207
+ routeAbortController.abort();
3208
+ const abortErr = new Error('aborted');
3209
+ abortErr.name = 'AbortError';
3210
+ throw abortErr;
3211
+ }
3212
+ };
3213
+ const persistAssistantPartial = (force = false) => {
3214
+ if (!assistantMessageId)
3215
+ return;
3216
+ const nowMs = Date.now();
3217
+ const nextContent = streamedContent || '';
3218
+ if (!force && nextContent === partialPersistedContent)
3219
+ return;
3220
+ if (!force && nowMs - partialPersistedAt < 1500)
3221
+ return;
3222
+ partialPersistedContent = nextContent;
3223
+ partialPersistedAt = nowMs;
3224
+ const modelWithRuntime = [
3225
+ activeModelName || profile.model || '',
3226
+ runtimeModeLabel(activeRuntimeMode, activeRuntimeSource),
3227
+ ].filter(Boolean).join(' | ');
3228
+ data.updateMessage(assistantMessageId, {
3229
+ content: nextContent,
3230
+ model: modelWithRuntime || null,
3231
+ botId: profile.id,
3232
+ agentName: profile.name,
3233
+ });
3234
+ };
2611
3235
  // Agentic loop
2612
3236
  let fullContent = '';
2613
3237
  let streamedContent = '';
2614
3238
  let streamedAnyChunk = false;
2615
3239
  let iteration = 0;
2616
- const MAX_ITERATIONS = 20;
3240
+ const MAX_ITERATIONS = 10; // Phase 1d: reduced from 20
2617
3241
  let totalInputTokens = 0;
2618
3242
  let totalOutputTokens = 0;
2619
3243
  let hasExactUsage = false;
@@ -2624,6 +3248,7 @@ function startLocalServer(opts) {
2624
3248
  while (iteration < MAX_ITERATIONS) {
2625
3249
  iteration++;
2626
3250
  let iterationFirstChunk = true;
3251
+ throwIfChatJobCancelled();
2627
3252
  if (iteration > 1) {
2628
3253
  sendEvent('status', { phase: 'thinking', detail: 'Processing tool results...' });
2629
3254
  recordActivity('status', { phase: 'thinking', detail: 'Processing tool results...' }, 'Processing tool results...');
@@ -2635,8 +3260,10 @@ function startLocalServer(opts) {
2635
3260
  stream: true,
2636
3261
  tools: toolDefs,
2637
3262
  cwd: llmSpawnCwd,
3263
+ abortSignal: routeAbortController.signal,
2638
3264
  thinkingEnabled,
2639
3265
  onChunk: async (chunk) => {
3266
+ throwIfChatJobCancelled();
2640
3267
  if (iterationFirstChunk) {
2641
3268
  iterationFirstChunk = false;
2642
3269
  sendEvent('status', { phase: 'generating' });
@@ -2644,6 +3271,7 @@ function startLocalServer(opts) {
2644
3271
  }
2645
3272
  streamedAnyChunk = true;
2646
3273
  streamedContent += chunk;
3274
+ persistAssistantPartial(false);
2647
3275
  sendEvent('chunk', { text: chunk });
2648
3276
  },
2649
3277
  ...(thinkingEnabled ? {
@@ -2660,6 +3288,9 @@ function startLocalServer(opts) {
2660
3288
  response = await activeLlm.chat(chatOptions);
2661
3289
  }
2662
3290
  catch (primaryErr) {
3291
+ if (routeAbortController.signal.aborted || primaryErr?.name === 'AbortError') {
3292
+ throw primaryErr;
3293
+ }
2663
3294
  if (cliFallback && !switchedToCliFallback) {
2664
3295
  switchedToCliFallback = true;
2665
3296
  activeProviderName = cliFallback.providerName;
@@ -2770,6 +3401,7 @@ function startLocalServer(opts) {
2770
3401
  throw primaryErr;
2771
3402
  }
2772
3403
  }
3404
+ throwIfChatJobCancelled();
2773
3405
  const authFailure = detectInteractiveAuthFailure(response?.content || '', activeProviderName, profile.provider);
2774
3406
  if (authFailure && (!response?.toolCalls || response.toolCalls.length === 0)) {
2775
3407
  sendEvent('status', {
@@ -2806,6 +3438,7 @@ function startLocalServer(opts) {
2806
3438
  toolCalls: response.toolCalls,
2807
3439
  });
2808
3440
  for (const tc of response.toolCalls) {
3441
+ throwIfChatJobCancelled();
2809
3442
  sendEvent('status', { phase: 'calling_tool', detail: `Running ${tc.name}...`, toolName: tc.name });
2810
3443
  sendEvent('tool_call', { id: tc.id, name: tc.name, arguments: tc.arguments });
2811
3444
  recordActivity('tool_call', { id: tc.id, name: tc.name, arguments: tc.arguments }, `Tool call: ${tc.name}`);
@@ -2816,7 +3449,9 @@ function startLocalServer(opts) {
2816
3449
  llmMessages.push({ role: 'tool', content: errMsg, toolCallId: tc.id, toolName: tc.name });
2817
3450
  continue;
2818
3451
  }
2819
- const approval = (0, approval_1.checkPermission)(tc.name, (profile.permission_mode || 'autopilot'));
3452
+ const approval = unrestrictedCliProfile
3453
+ ? { approved: true }
3454
+ : (0, approval_1.checkPermission)(tc.name, (profile.permission_mode || 'autopilot'));
2820
3455
  if (!approval.approved) {
2821
3456
  const errMsg = `PERMISSION_DENIED: ${approval.reason}`;
2822
3457
  sendEvent('tool_result', { callId: tc.id, output: errMsg, isError: true });
@@ -2865,6 +3500,10 @@ function startLocalServer(opts) {
2865
3500
  break;
2866
3501
  }
2867
3502
  const persistedContent = fullContent || streamedContent.trim();
3503
+ if (!persistedContent) {
3504
+ throw new Error('Assistant returned no final response');
3505
+ }
3506
+ persistAssistantPartial(true);
2868
3507
  // Emit thinking_done event if we accumulated any thinking
2869
3508
  if (accumulatedThinking) {
2870
3509
  sendEvent('thinking_done', {
@@ -2885,7 +3524,14 @@ function startLocalServer(opts) {
2885
3524
  activeModelName || profile.model || '',
2886
3525
  runtimeModeLabel(activeRuntimeMode, activeRuntimeSource),
2887
3526
  ].filter(Boolean).join(' | ');
2888
- const savedMessage = data.addMessage(convId, 'assistant', persistedContent, modelWithRuntime || undefined, undefined, profile.id, profile.name);
3527
+ const savedMessage = assistantMessageId
3528
+ ? (data.updateMessage(assistantMessageId, {
3529
+ content: persistedContent,
3530
+ model: modelWithRuntime || null,
3531
+ botId: profile.id,
3532
+ agentName: profile.name,
3533
+ }) || data.addMessage(convId, 'assistant', persistedContent, modelWithRuntime || undefined, undefined, profile.id, profile.name))
3534
+ : data.addMessage(convId, 'assistant', persistedContent, modelWithRuntime || undefined, undefined, profile.id, profile.name);
2889
3535
  data.attachMessageActivitiesToMessage(activityStreamId, savedMessage.id);
2890
3536
  data.createMessageActivity({
2891
3537
  conversationId: convId,
@@ -2913,6 +3559,7 @@ function startLocalServer(opts) {
2913
3559
  approxInputTokens,
2914
3560
  },
2915
3561
  });
3562
+ responseEnded = true;
2916
3563
  res.end();
2917
3564
  // Background processing: auto-title + funnel extraction
2918
3565
  const conv = data.getConversation(convId);
@@ -2926,13 +3573,13 @@ function startLocalServer(opts) {
2926
3573
  autoTitleConversation(convId, message, persistedContent, activeProviderName, activeModelName, activeApiKey);
2927
3574
  }
2928
3575
  }
2929
- (0, local_funnel_1.scheduleFunnelProcessing)(convId);
2930
3576
  // Summarization: trigger after every 5-turn boundary (fire-and-forget)
2931
3577
  // Includes topic creation/linking + project verification
2932
3578
  if ((0, context_window_1.shouldSummarize)(convId)) {
2933
3579
  const catalog = (0, backfill_1.buildProjectTopicCatalog)();
2934
3580
  (0, summarization_pipeline_1.runSummarization)(convId, catalog).catch(err => console.error(chalk_1.default.yellow(` [summarization] ${err.message}`)));
2935
3581
  }
3582
+ (0, local_funnel_1.scheduleFunnelProcessing)(convId);
2936
3583
  }
2937
3584
  catch (err) {
2938
3585
  console.error(chalk_1.default.red(`Chat error: ${err.message}`));
@@ -2969,64 +3616,19 @@ function startLocalServer(opts) {
2969
3616
  providerId: err?.providerId || null,
2970
3617
  cli: err?.cli || null,
2971
3618
  })}\n\n`);
3619
+ responseEnded = true;
2972
3620
  res.end();
2973
3621
  }
2974
3622
  catch { /* connection already closed */ }
2975
3623
  }
2976
3624
  }
3625
+ finally {
3626
+ req.off?.('close', abortOnClientClose);
3627
+ }
2977
3628
  });
2978
3629
  // ─── Memory Facts ───────────────────────────────────────────
2979
- app.get('/api/memory/facts', (req, res) => {
2980
- try {
2981
- const { agentId, factType, limit, offset, search, groupByProject } = req.query;
2982
- const limitN = limit ? parseInt(limit) : 100;
2983
- const offsetN = offset ? parseInt(offset) : 0;
2984
- const grouped = groupByProject === '1' || groupByProject === 'true';
2985
- if (grouped) {
2986
- const db = data.getDb();
2987
- const wheres = [];
2988
- const params = [];
2989
- if (agentId) {
2990
- wheres.push('f.agent_id = ?');
2991
- params.push(agentId);
2992
- }
2993
- if (factType) {
2994
- wheres.push('f.fact_type = ?');
2995
- params.push(factType);
2996
- }
2997
- if (search) {
2998
- wheres.push('(f.content LIKE ? OR COALESCE(c.title, \'\') LIKE ?)');
2999
- params.push(`%${search}%`, `%${search}%`);
3000
- }
3001
- let sql = `
3002
- SELECT f.*, c.title as conversation_title, c.project_id,
3003
- COALESCE(p.name, '${data.UNASSIGNED_PROJECT_NAME}') as project_name
3004
- FROM memory_fact f
3005
- LEFT JOIN conversation c ON f.conversation_id = c.id
3006
- LEFT JOIN project p ON c.project_id = p.id
3007
- `;
3008
- if (wheres.length > 0)
3009
- sql += ` WHERE ${wheres.join(' AND ')}`;
3010
- sql += ' ORDER BY f.created_at DESC LIMIT ? OFFSET ?';
3011
- params.push(limitN, offsetN);
3012
- const rows = db.prepare(sql).all(...params);
3013
- return res.json(rows);
3014
- }
3015
- if (search && agentId) {
3016
- const facts = data.searchMemoryFacts(search, agentId, limitN);
3017
- return res.json(facts);
3018
- }
3019
- const facts = data.listMemoryFacts({
3020
- agentId: agentId || undefined,
3021
- factType: factType || undefined,
3022
- limit: limitN,
3023
- offset: offsetN,
3024
- });
3025
- res.json(facts);
3026
- }
3027
- catch (err) {
3028
- res.status(500).json({ error: err.message });
3029
- }
3630
+ app.get('/api/memory/facts', (_req, res) => {
3631
+ res.json([]);
3030
3632
  });
3031
3633
  app.get('/api/memory/search', (req, res) => {
3032
3634
  try {
@@ -3054,120 +3656,23 @@ function startLocalServer(opts) {
3054
3656
  res.status(500).json({ error: err.message });
3055
3657
  }
3056
3658
  });
3057
- app.delete('/api/memory/facts/:id', (req, res) => {
3058
- try {
3059
- const deleted = data.deleteMemoryFact(req.params.id);
3060
- if (!deleted)
3061
- return res.status(404).json({ error: 'Not found' });
3062
- res.json({ ok: true });
3063
- }
3064
- catch (err) {
3065
- res.status(500).json({ error: err.message });
3066
- }
3659
+ app.delete('/api/memory/facts/:id', (_req, res) => {
3660
+ res.json({ ok: true });
3067
3661
  });
3068
- app.get('/api/memory/entities', (req, res) => {
3069
- try {
3070
- const { agentId } = req.query;
3071
- if (!agentId)
3072
- return res.status(400).json({ error: 'agentId required' });
3073
- const graph = data.getEntityGraph(agentId);
3074
- res.json(graph);
3075
- }
3076
- catch (err) {
3077
- res.status(500).json({ error: err.message });
3078
- }
3662
+ app.get('/api/memory/entities', (_req, res) => {
3663
+ res.json({ nodes: [], edges: [] });
3079
3664
  });
3080
- app.get('/api/memory/decisions', (req, res) => {
3081
- try {
3082
- const { conversationId, limit, groupByProject } = req.query;
3083
- const grouped = groupByProject === '1' || groupByProject === 'true';
3084
- if (grouped) {
3085
- const db = data.getDb();
3086
- const params = [];
3087
- let sql = `
3088
- SELECT d.*, c.title as conversation_title, c.project_id,
3089
- COALESCE(p.name, '${data.UNASSIGNED_PROJECT_NAME}') as project_name
3090
- FROM decision d
3091
- LEFT JOIN conversation c ON d.conversation_id = c.id
3092
- LEFT JOIN project p ON c.project_id = p.id
3093
- `;
3094
- if (conversationId) {
3095
- sql += ' WHERE d.conversation_id = ?';
3096
- params.push(conversationId);
3097
- }
3098
- sql += ' ORDER BY d.created_at DESC LIMIT ?';
3099
- params.push(limit ? parseInt(limit) : 200);
3100
- const rows = db.prepare(sql).all(...params);
3101
- return res.json(rows);
3102
- }
3103
- const decisions = data.listDecisions({
3104
- conversationId: conversationId || undefined,
3105
- limit: limit ? parseInt(limit) : undefined,
3106
- });
3107
- res.json(decisions);
3108
- }
3109
- catch (err) {
3110
- res.status(500).json({ error: err.message });
3111
- }
3665
+ app.get('/api/memory/decisions', (_req, res) => {
3666
+ res.json([]);
3112
3667
  });
3113
- app.delete('/api/memory/decisions/:id', (req, res) => {
3114
- try {
3115
- const deleted = data.deleteDecision(req.params.id);
3116
- if (!deleted)
3117
- return res.status(404).json({ error: 'Not found' });
3118
- res.json({ ok: true });
3119
- }
3120
- catch (err) {
3121
- res.status(500).json({ error: err.message });
3122
- }
3668
+ app.delete('/api/memory/decisions/:id', (_req, res) => {
3669
+ res.json({ ok: true });
3123
3670
  });
3124
- app.get('/api/memory/action-items', (req, res) => {
3125
- try {
3126
- const { status, limit, groupByProject } = req.query;
3127
- const grouped = groupByProject === '1' || groupByProject === 'true';
3128
- if (grouped) {
3129
- const db = data.getDb();
3130
- const params = [];
3131
- const wheres = [];
3132
- if (status) {
3133
- wheres.push('a.status = ?');
3134
- params.push(status);
3135
- }
3136
- let sql = `
3137
- SELECT a.*, c.title as conversation_title, c.project_id,
3138
- COALESCE(p.name, '${data.UNASSIGNED_PROJECT_NAME}') as project_name
3139
- FROM action_item a
3140
- LEFT JOIN conversation c ON a.conversation_id = c.id
3141
- LEFT JOIN project p ON c.project_id = p.id
3142
- `;
3143
- if (wheres.length > 0) {
3144
- sql += ` WHERE ${wheres.join(' AND ')}`;
3145
- }
3146
- sql += ' ORDER BY a.created_at DESC LIMIT ?';
3147
- params.push(limit ? parseInt(limit) : 200);
3148
- const rows = db.prepare(sql).all(...params);
3149
- return res.json(rows);
3150
- }
3151
- const items = data.listActionItems({
3152
- status: status || undefined,
3153
- limit: limit ? parseInt(limit) : undefined,
3154
- });
3155
- res.json(items);
3156
- }
3157
- catch (err) {
3158
- res.status(500).json({ error: err.message });
3159
- }
3671
+ app.get('/api/memory/action-items', (_req, res) => {
3672
+ res.json([]);
3160
3673
  });
3161
- app.delete('/api/memory/action-items/:id', (req, res) => {
3162
- try {
3163
- const deleted = data.deleteActionItem(req.params.id);
3164
- if (!deleted)
3165
- return res.status(404).json({ error: 'Not found' });
3166
- res.json({ ok: true });
3167
- }
3168
- catch (err) {
3169
- res.status(500).json({ error: err.message });
3170
- }
3674
+ app.delete('/api/memory/action-items/:id', (_req, res) => {
3675
+ res.json({ ok: true });
3171
3676
  });
3172
3677
  // ─── Settings ───────────────────────────────────────────────
3173
3678
  app.get('/api/settings', (_req, res) => {
@@ -3610,24 +4115,9 @@ function startLocalServer(opts) {
3610
4115
  const ids = convIds.map(c => c.id);
3611
4116
  const placeholders = ids.map(() => '?').join(',');
3612
4117
  const result = {};
3613
- if (!type || type === 'decisions') {
3614
- result.decisions = db.prepare(`SELECT d.*, c.title as conversation_title FROM decision d
3615
- LEFT JOIN conversation c ON d.conversation_id = c.id
3616
- WHERE d.conversation_id IN (${placeholders})
3617
- ORDER BY d.created_at DESC`).all(...ids);
3618
- }
3619
- if (!type || type === 'actionItems') {
3620
- result.actionItems = db.prepare(`SELECT a.*, c.title as conversation_title FROM action_item a
3621
- LEFT JOIN conversation c ON a.conversation_id = c.id
3622
- WHERE a.conversation_id IN (${placeholders})
3623
- ORDER BY a.created_at DESC`).all(...ids);
3624
- }
3625
- if (!type || type === 'facts') {
3626
- result.facts = db.prepare(`SELECT f.*, c.title as conversation_title FROM memory_fact f
3627
- LEFT JOIN conversation c ON f.conversation_id = c.id
3628
- WHERE f.conversation_id IN (${placeholders})
3629
- ORDER BY f.created_at DESC`).all(...ids);
3630
- }
4118
+ result.decisions = [];
4119
+ result.actionItems = [];
4120
+ result.facts = [];
3631
4121
  if (!type || type === 'summaries') {
3632
4122
  result.summaries = db.prepare(`SELECT s.*, c.title as conversation_title FROM conversation_summary s
3633
4123
  LEFT JOIN conversation c ON s.conversation_id = c.id
@@ -4032,6 +4522,7 @@ function startLocalServer(opts) {
4032
4522
  mcpManager.autoLaunch().catch((err) => {
4033
4523
  console.error(chalk_1.default.yellow(`[MCP] Auto-launch error: ${err.message}`));
4034
4524
  });
4525
+ void runQueuedChatJobs();
4035
4526
  /**
4036
4527
  * GET /api/mcp/catalog — full marketplace catalog with install status
4037
4528
  */
@@ -4196,6 +4687,8 @@ async function buildChatRuntime(profile) {
4196
4687
  function runtimeModeLabel(mode, runtimeSource) {
4197
4688
  if (mode === 'subscription-cli')
4198
4689
  return 'Subscription CLI';
4690
+ if (mode === 'subscription-api')
4691
+ return (0, subscription_runtime_1.claudeSubscriptionRuntimeLabel)(runtimeSource);
4199
4692
  return 'API Key';
4200
4693
  }
4201
4694
  function runtimePayloadForDisplay(providerName, model, runtimeMode, runtimeSource, fallbackUsed) {
@@ -4214,17 +4707,22 @@ function configuredRuntimeLabelForProfile(profile) {
4214
4707
  const directConnection = profile.provider_connection_id
4215
4708
  ? data.listProviderConnections().find((row) => row.id === profile.provider_connection_id)
4216
4709
  : undefined;
4710
+ if (profile.provider === 'claude-cli' || profile.provider === 'codex-cli') {
4711
+ return 'Subscription CLI';
4712
+ }
4217
4713
  const connection = directConnection;
4218
4714
  if (connection) {
4219
4715
  if (connection.access_mode === 'cli')
4220
4716
  return 'Subscription CLI';
4717
+ if (connection.access_mode === 'oauth')
4718
+ return 'Subscription API (Token)';
4221
4719
  return 'API Key';
4222
4720
  }
4223
- if (profile.provider === 'claude-cli' || profile.provider === 'codex-cli') {
4224
- return 'Subscription CLI';
4225
- }
4226
4721
  return 'API Key';
4227
4722
  }
4723
+ async function buildChatRuntimeForTest(profile) {
4724
+ return buildChatRuntime(profile);
4725
+ }
4228
4726
  function buildConfiguredMessageModel(profile) {
4229
4727
  if (!profile)
4230
4728
  return '';
@@ -4398,6 +4896,12 @@ function resolveApiKey(profile) {
4398
4896
  // Check profile-stored key first
4399
4897
  if (profile.api_key_enc)
4400
4898
  return profile.api_key_enc;
4899
+ // Prefer the bot's explicitly assigned provider connection when present.
4900
+ if (profile.provider_connection_id) {
4901
+ const directConnection = data.getProviderConnection(profile.provider_connection_id);
4902
+ if (directConnection?.api_key_enc)
4903
+ return directConnection.api_key_enc;
4904
+ }
4401
4905
  // Fall back to DB-backed provider connections
4402
4906
  const providerConnection = data.findProviderConnection(profile.provider);
4403
4907
  if (providerConnection?.api_key_enc)