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.
- package/dist/approval.d.ts +1 -0
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js +12 -0
- package/dist/approval.js.map +1 -1
- package/dist/auth/auto-detect.d.ts +11 -3
- package/dist/auth/auto-detect.d.ts.map +1 -1
- package/dist/auth/auto-detect.js +136 -168
- package/dist/auth/auto-detect.js.map +1 -1
- package/dist/auth/subscription-runtime.js +1 -1
- package/dist/auth/subscription-runtime.js.map +1 -1
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +31 -28
- package/dist/backfill.js.map +1 -1
- package/dist/bot-manager.d.ts +16 -4
- package/dist/bot-manager.d.ts.map +1 -1
- package/dist/bot-manager.js +280 -73
- package/dist/bot-manager.js.map +1 -1
- package/dist/clerk-model.d.ts +10 -6
- package/dist/clerk-model.d.ts.map +1 -1
- package/dist/clerk-model.js +39 -16
- package/dist/clerk-model.js.map +1 -1
- package/dist/cli.js +7 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/import-history.js +5 -1
- package/dist/commands/import-history.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +57 -225
- package/dist/commands/start.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +13 -2
- package/dist/config.js.map +1 -1
- package/dist/context-window.js +1 -1
- package/dist/context-window.js.map +1 -1
- package/dist/import-parser-core.d.ts.map +1 -1
- package/dist/import-parser-core.js +74 -8
- package/dist/import-parser-core.js.map +1 -1
- package/dist/local-data.d.ts +59 -6
- package/dist/local-data.d.ts.map +1 -1
- package/dist/local-data.js +544 -62
- package/dist/local-data.js.map +1 -1
- package/dist/local-db.d.ts.map +1 -1
- package/dist/local-db.js +21 -1
- package/dist/local-db.js.map +1 -1
- package/dist/local-funnel.d.ts +0 -7
- package/dist/local-funnel.d.ts.map +1 -1
- package/dist/local-funnel.js +19 -28
- package/dist/local-funnel.js.map +1 -1
- package/dist/local-import-worker.d.ts.map +1 -1
- package/dist/local-import-worker.js +49 -4
- package/dist/local-import-worker.js.map +1 -1
- package/dist/local-memory-search.d.ts.map +1 -1
- package/dist/local-memory-search.js +6 -3
- package/dist/local-memory-search.js.map +1 -1
- package/dist/local-server.d.ts +37 -0
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +731 -227
- package/dist/local-server.js.map +1 -1
- package/dist/mcp/local-memory-server.d.ts +1 -1
- package/dist/mcp/local-memory-server.d.ts.map +1 -1
- package/dist/mcp/local-memory-server.js +23 -11
- package/dist/mcp/local-memory-server.js.map +1 -1
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +86 -18
- package/dist/mcp/manager.js.map +1 -1
- package/dist/message-loop.d.ts +5 -10
- package/dist/message-loop.d.ts.map +1 -1
- package/dist/message-loop.js +393 -289
- package/dist/message-loop.js.map +1 -1
- package/dist/mqtt-client.d.ts +4 -1
- package/dist/mqtt-client.d.ts.map +1 -1
- package/dist/mqtt-client.js.map +1 -1
- package/dist/orchestration/orchestrator-blocked-prompt.d.ts +2 -1
- package/dist/orchestration/orchestrator-blocked-prompt.d.ts.map +1 -1
- package/dist/orchestration/orchestrator-blocked-prompt.js +11 -0
- package/dist/orchestration/orchestrator-blocked-prompt.js.map +1 -1
- package/dist/orchestration/orchestrator-final-response-prompt.d.ts +4 -1
- package/dist/orchestration/orchestrator-final-response-prompt.d.ts.map +1 -1
- package/dist/orchestration/orchestrator-final-response-prompt.js +8 -6
- package/dist/orchestration/orchestrator-final-response-prompt.js.map +1 -1
- package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
- package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/worker-operating-prompt.js +39 -0
- package/dist/orchestration/worker-operating-prompt.js.map +1 -1
- package/dist/orchestrator.d.ts +13 -0
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +235 -133
- package/dist/orchestrator.js.map +1 -1
- package/dist/providers/anthropic.d.ts +0 -5
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +29 -75
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/claude-cli.d.ts.map +1 -1
- package/dist/providers/claude-cli.js +16 -140
- package/dist/providers/claude-cli.js.map +1 -1
- package/dist/providers/codex-cli.d.ts.map +1 -1
- package/dist/providers/codex-cli.js +78 -59
- package/dist/providers/codex-cli.js.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +4 -2
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +27 -2
- package/dist/providers/openai.js.map +1 -1
- package/dist/storage-mode.d.ts +5 -0
- package/dist/storage-mode.d.ts.map +1 -0
- package/dist/storage-mode.js +21 -0
- package/dist/storage-mode.js.map +1 -0
- package/dist/summarization-pipeline.d.ts +9 -0
- package/dist/summarization-pipeline.d.ts.map +1 -1
- package/dist/summarization-pipeline.js +61 -17
- package/dist/summarization-pipeline.js.map +1 -1
- package/dist/tools/analyze-image.js +2 -2
- package/dist/tools/analyze-image.js.map +1 -1
- package/dist/tools/edit-file.js +3 -3
- package/dist/tools/edit-file.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +36 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/list-directory.js +7 -4
- package/dist/tools/list-directory.js.map +1 -1
- package/dist/tools/read-file.js +3 -3
- package/dist/tools/read-file.js.map +1 -1
- package/dist/tools/run-command.js +3 -3
- package/dist/tools/run-command.js.map +1 -1
- package/dist/tools/sandbox.d.ts +10 -5
- package/dist/tools/sandbox.d.ts.map +1 -1
- package/dist/tools/sandbox.js +41 -13
- package/dist/tools/sandbox.js.map +1 -1
- package/dist/tools/search-codebase.js +2 -2
- package/dist/tools/search-codebase.js.map +1 -1
- package/dist/tools/search-local-memory.d.ts.map +1 -1
- package/dist/tools/search-local-memory.js +19 -8
- package/dist/tools/search-local-memory.js.map +1 -1
- package/dist/tools/spawn-subagent.js.map +1 -1
- package/dist/tools/write-file.js +3 -3
- package/dist/tools/write-file.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/verification/index.js +2 -2
- package/dist/verification/index.js.map +1 -1
- package/dist/wizard-state.d.ts.map +1 -1
- package/dist/wizard-state.js +16 -2
- package/dist/wizard-state.js.map +1 -1
- package/dist/wizard-support.d.ts +2 -2
- package/dist/wizard-support.d.ts.map +1 -1
- package/dist/wizard-support.js +80 -93
- package/dist/wizard-support.js.map +1 -1
- package/dist/workflow-engine.d.ts +3 -1
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +131 -12
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +1 -1
package/dist/local-server.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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, '&')
|
|
947
|
+
.replace(/</g, '<')
|
|
948
|
+
.replace(/>/g, '>')
|
|
949
|
+
.replace(/"/g, '"')
|
|
950
|
+
.replace(/'/g, ''');
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
|
2512
|
-
//
|
|
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 +=
|
|
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 =
|
|
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 =
|
|
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', (
|
|
2980
|
-
|
|
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', (
|
|
3058
|
-
|
|
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', (
|
|
3069
|
-
|
|
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', (
|
|
3081
|
-
|
|
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', (
|
|
3114
|
-
|
|
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', (
|
|
3125
|
-
|
|
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', (
|
|
3162
|
-
|
|
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
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
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)
|