funolio-agent 1.0.7 → 1.0.48
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/agent-config.d.ts +9 -1
- package/dist/agent-config.d.ts.map +1 -1
- package/dist/agent-config.js +4 -1
- package/dist/agent-config.js.map +1 -1
- 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/auto-organizer.d.ts.map +1 -1
- package/dist/auto-organizer.js +4 -3
- package/dist/auto-organizer.js.map +1 -1
- package/dist/backfill.d.ts.map +1 -1
- package/dist/backfill.js +34 -30
- package/dist/backfill.js.map +1 -1
- package/dist/bot-manager.d.ts +4 -8
- package/dist/bot-manager.d.ts.map +1 -1
- package/dist/bot-manager.js +31 -160
- package/dist/bot-manager.js.map +1 -1
- package/dist/clerk-model.d.ts +15 -7
- package/dist/clerk-model.d.ts.map +1 -1
- package/dist/clerk-model.js +78 -43
- package/dist/clerk-model.js.map +1 -1
- package/dist/cli-session-epoch.d.ts +10 -0
- package/dist/cli-session-epoch.d.ts.map +1 -0
- package/dist/cli-session-epoch.js +61 -0
- package/dist/cli-session-epoch.js.map +1 -0
- 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/init.d.ts.map +1 -1
- package/dist/commands/init.js +30 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/pool.js +1 -1
- package/dist/commands/pool.js.map +1 -1
- package/dist/commands/setup.d.ts +37 -0
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +146 -43
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +117 -255
- package/dist/commands/start.js.map +1 -1
- package/dist/config-cleanup.d.ts.map +1 -1
- package/dist/config-cleanup.js +2 -1
- package/dist/config-cleanup.js.map +1 -1
- package/dist/config.d.ts +6 -9
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -18
- package/dist/config.js.map +1 -1
- package/dist/context-window.d.ts +33 -5
- package/dist/context-window.d.ts.map +1 -1
- package/dist/context-window.js +122 -21
- package/dist/context-window.js.map +1 -1
- package/dist/eval/orchestrator-front-door-replay.js +1 -1
- package/dist/eval/orchestrator-front-door-replay.js.map +1 -1
- package/dist/eval/policy-detection-replay.js +1 -1
- package/dist/eval/policy-detection-replay.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/integration-tokens.d.ts +1 -6
- package/dist/integration-tokens.d.ts.map +1 -1
- package/dist/integration-tokens.js +38 -40
- package/dist/integration-tokens.js.map +1 -1
- package/dist/local-cli-pty-manager.d.ts +50 -0
- package/dist/local-cli-pty-manager.d.ts.map +1 -0
- package/dist/local-cli-pty-manager.js +645 -0
- package/dist/local-cli-pty-manager.js.map +1 -0
- package/dist/local-data.d.ts +89 -6
- package/dist/local-data.d.ts.map +1 -1
- package/dist/local-data.js +600 -63
- package/dist/local-data.js.map +1 -1
- package/dist/local-db.d.ts.map +1 -1
- package/dist/local-db.js +74 -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 +22 -30
- 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 +1 -0
- package/dist/local-memory-search.d.ts.map +1 -1
- package/dist/local-memory-search.js +107 -21
- package/dist/local-memory-search.js.map +1 -1
- package/dist/local-server.d.ts +21 -0
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +1057 -501
- package/dist/local-server.js.map +1 -1
- package/dist/mcp/bridge-server.d.ts.map +1 -1
- package/dist/mcp/bridge-server.js +2 -1
- package/dist/mcp/bridge-server.js.map +1 -1
- package/dist/mcp/local-memory-server.d.ts +6 -1
- package/dist/mcp/local-memory-server.d.ts.map +1 -1
- package/dist/mcp/local-memory-server.js +38 -13
- package/dist/mcp/local-memory-server.js.map +1 -1
- package/dist/mcp/manager.d.ts +3 -22
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +66 -320
- package/dist/mcp/manager.js.map +1 -1
- package/dist/memory-extraction.d.ts +2 -0
- package/dist/memory-extraction.d.ts.map +1 -1
- package/dist/memory-extraction.js +3 -1
- package/dist/memory-extraction.js.map +1 -1
- package/dist/message-loop.d.ts +1 -3
- package/dist/message-loop.d.ts.map +1 -1
- package/dist/message-loop.js +220 -437
- package/dist/message-loop.js.map +1 -1
- package/dist/mqtt-client.d.ts +2 -28
- package/dist/mqtt-client.d.ts.map +1 -1
- package/dist/mqtt-client.js +2 -2
- package/dist/mqtt-client.js.map +1 -1
- package/dist/oauth.d.ts +6 -0
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +91 -0
- package/dist/oauth.js.map +1 -1
- package/dist/orchestration/front-door-policy.d.ts +5 -2
- package/dist/orchestration/front-door-policy.d.ts.map +1 -1
- package/dist/orchestration/front-door-policy.js +25 -28
- package/dist/orchestration/front-door-policy.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 +12 -1
- 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 +9 -7
- package/dist/orchestration/orchestrator-final-response-prompt.js.map +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.d.ts +11 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.js +67 -44
- package/dist/orchestration/orchestrator-operating-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 +41 -2
- package/dist/orchestration/worker-operating-prompt.js.map +1 -1
- package/dist/orchestrator.d.ts +17 -0
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +328 -166
- package/dist/orchestrator.js.map +1 -1
- package/dist/prompt-template.js +3 -3
- package/dist/prompt-template.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-prompt.d.ts.map +1 -1
- package/dist/providers/claude-cli-prompt.js +22 -6
- package/dist/providers/claude-cli-prompt.js.map +1 -1
- package/dist/providers/claude-cli.d.ts.map +1 -1
- package/dist/providers/claude-cli.js +36 -142
- 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 +148 -74
- 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 +13 -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/runtime-context.d.ts +10 -0
- package/dist/runtime-context.d.ts.map +1 -0
- package/dist/runtime-context.js +30 -0
- package/dist/runtime-context.js.map +1 -0
- 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/subagent/queue.d.ts.map +1 -1
- package/dist/subagent/queue.js +1 -0
- package/dist/subagent/queue.js.map +1 -1
- package/dist/summarization-pipeline.d.ts +10 -0
- package/dist/summarization-pipeline.d.ts.map +1 -1
- package/dist/summarization-pipeline.js +147 -34
- package/dist/summarization-pipeline.js.map +1 -1
- package/dist/tool-permissions.d.ts +2 -0
- package/dist/tool-permissions.d.ts.map +1 -0
- package/dist/tool-permissions.js +25 -0
- package/dist/tool-permissions.js.map +1 -0
- 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 +7 -8
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +106 -60
- 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/search-memory.d.ts.map +1 -1
- package/dist/tools/search-memory.js +9 -3
- package/dist/tools/search-memory.js.map +1 -1
- package/dist/tools/spawn-subagent.d.ts.map +1 -1
- package/dist/tools/spawn-subagent.js +1 -0
- 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 +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -3
- package/dist/types.js.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 +88 -99
- package/dist/wizard-support.js.map +1 -1
- package/dist/workflow-engine.d.ts +9 -3
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +378 -82
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +2 -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,13 @@ 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");
|
|
80
|
+
const local_cli_pty_manager_1 = require("./local-cli-pty-manager");
|
|
81
|
+
const cli_session_epoch_1 = require("./cli-session-epoch");
|
|
78
82
|
const server_adapter_1 = require("./server-adapter");
|
|
79
83
|
const wizard_support_1 = require("./wizard-support");
|
|
80
84
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -96,6 +100,9 @@ function startLocalServer(opts) {
|
|
|
96
100
|
const express = requireExpress();
|
|
97
101
|
const app = express();
|
|
98
102
|
const port = opts.port ?? LOCAL_PORT;
|
|
103
|
+
if ((0, storage_mode_1.isLocalStorageMode)()) {
|
|
104
|
+
data.purgeLegacyExtractionDataOnce();
|
|
105
|
+
}
|
|
99
106
|
const cliNormalization = data.normalizeCliProviderConnections();
|
|
100
107
|
if (cliNormalization.updatedIds.length > 0) {
|
|
101
108
|
console.info(`[local-server] normalized ${cliNormalization.updatedIds.length} CLI provider connection(s): ${cliNormalization.updatedIds.join(', ')}`);
|
|
@@ -183,6 +190,15 @@ function startLocalServer(opts) {
|
|
|
183
190
|
catch (err) {
|
|
184
191
|
console.error(chalk_1.default.yellow(` Failed to reconcile conversation/topic project consistency: ${err}`));
|
|
185
192
|
}
|
|
193
|
+
try {
|
|
194
|
+
const interrupted = data.markRunningChatJobsInterrupted();
|
|
195
|
+
if (interrupted > 0) {
|
|
196
|
+
console.log(chalk_1.default.gray(` Marked ${interrupted} interrupted chat job(s) as failed`));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
console.error(chalk_1.default.yellow(` Failed to recover interrupted chat jobs: ${err}`));
|
|
201
|
+
}
|
|
186
202
|
// ─── Health ──────────────────────────────────────────────────
|
|
187
203
|
app.get('/api/health', (_req, res) => {
|
|
188
204
|
try {
|
|
@@ -533,6 +549,181 @@ function startLocalServer(opts) {
|
|
|
533
549
|
function isConnectedMode() {
|
|
534
550
|
return (0, server_runtime_1.getRuntimeConnectionConfig)().connectionMode === 'server';
|
|
535
551
|
}
|
|
552
|
+
function localTimestamp() {
|
|
553
|
+
return new Date().toISOString().replace('T', ' ').replace('Z', '');
|
|
554
|
+
}
|
|
555
|
+
const MAX_LOCAL_CHAT_JOBS = 3;
|
|
556
|
+
const runningChatJobControllers = new Map();
|
|
557
|
+
const finalizeCancelledChatJobMessage = (job) => {
|
|
558
|
+
const existing = data.getMessage(job.assistant_message_id);
|
|
559
|
+
const existingContent = (existing?.content || '').trim();
|
|
560
|
+
data.updateMessage(job.assistant_message_id, {
|
|
561
|
+
content: existingContent ? `${existingContent}\n\n[Stopped by user]` : '[Stopped by user]',
|
|
562
|
+
botId: job.bot_id,
|
|
563
|
+
});
|
|
564
|
+
data.touchConversationActivity(job.conversation_id);
|
|
565
|
+
};
|
|
566
|
+
const parseSsePayloads = async (response, handlers) => {
|
|
567
|
+
const reader = response.body?.getReader();
|
|
568
|
+
if (!reader)
|
|
569
|
+
return;
|
|
570
|
+
const decoder = new TextDecoder();
|
|
571
|
+
let buffer = '';
|
|
572
|
+
let currentEvent = '';
|
|
573
|
+
let receivedDone = false;
|
|
574
|
+
while (true) {
|
|
575
|
+
const { done, value } = await reader.read();
|
|
576
|
+
if (done)
|
|
577
|
+
break;
|
|
578
|
+
buffer += decoder.decode(value, { stream: true });
|
|
579
|
+
const lines = buffer.split('\n');
|
|
580
|
+
buffer = lines.pop() || '';
|
|
581
|
+
for (const line of lines) {
|
|
582
|
+
if (line.startsWith('event: ')) {
|
|
583
|
+
currentEvent = line.slice(7);
|
|
584
|
+
}
|
|
585
|
+
else if (line.startsWith('data: ') && currentEvent) {
|
|
586
|
+
let dataPayload = null;
|
|
587
|
+
try {
|
|
588
|
+
dataPayload = JSON.parse(line.slice(6));
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
currentEvent = '';
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (currentEvent === 'done') {
|
|
595
|
+
receivedDone = true;
|
|
596
|
+
await handlers.onDone?.(dataPayload);
|
|
597
|
+
}
|
|
598
|
+
else if (currentEvent === 'error') {
|
|
599
|
+
receivedDone = true;
|
|
600
|
+
await handlers.onError?.(dataPayload);
|
|
601
|
+
}
|
|
602
|
+
currentEvent = '';
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (!receivedDone) {
|
|
607
|
+
await handlers.onDone?.({});
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
const runQueuedChatJobs = async () => {
|
|
611
|
+
if (isConnectedMode())
|
|
612
|
+
return;
|
|
613
|
+
while (runningChatJobControllers.size < MAX_LOCAL_CHAT_JOBS) {
|
|
614
|
+
const next = data.listQueuedChatJobs(1)[0];
|
|
615
|
+
if (!next)
|
|
616
|
+
return;
|
|
617
|
+
if (runningChatJobControllers.has(next.id))
|
|
618
|
+
return;
|
|
619
|
+
const controller = new AbortController();
|
|
620
|
+
runningChatJobControllers.set(next.id, controller);
|
|
621
|
+
data.updateChatJob(next.id, {
|
|
622
|
+
status: 'running',
|
|
623
|
+
startedAt: localTimestamp(),
|
|
624
|
+
error: null,
|
|
625
|
+
});
|
|
626
|
+
void (async () => {
|
|
627
|
+
try {
|
|
628
|
+
const job = data.getChatJob(next.id);
|
|
629
|
+
if (!job)
|
|
630
|
+
return;
|
|
631
|
+
const userMessage = data.getMessage(job.user_message_id);
|
|
632
|
+
const requestPayload = (() => {
|
|
633
|
+
try {
|
|
634
|
+
return job.request_json ? JSON.parse(job.request_json) : {};
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
return {};
|
|
638
|
+
}
|
|
639
|
+
})();
|
|
640
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/chat`, {
|
|
641
|
+
method: 'POST',
|
|
642
|
+
headers: { 'Content-Type': 'application/json' },
|
|
643
|
+
signal: controller.signal,
|
|
644
|
+
body: JSON.stringify({
|
|
645
|
+
conversationId: job.conversation_id,
|
|
646
|
+
message: userMessage?.content || '',
|
|
647
|
+
botId: job.bot_id,
|
|
648
|
+
skipUserMessage: true,
|
|
649
|
+
pinnedMessageIds: Array.isArray(requestPayload?.pinnedMessageIds) ? requestPayload.pinnedMessageIds : undefined,
|
|
650
|
+
topicId: requestPayload?.topicId || undefined,
|
|
651
|
+
projectId: requestPayload?.projectId || undefined,
|
|
652
|
+
orchestrationEnabled: requestPayload?.orchestrationEnabled !== false,
|
|
653
|
+
chatJobId: job.id,
|
|
654
|
+
assistantMessageId: job.assistant_message_id,
|
|
655
|
+
}),
|
|
656
|
+
});
|
|
657
|
+
if (!response.ok) {
|
|
658
|
+
const body = await response.json().catch(() => ({ error: `HTTP ${response.status}` }));
|
|
659
|
+
throw new Error(body.error || `HTTP ${response.status}`);
|
|
660
|
+
}
|
|
661
|
+
await parseSsePayloads(response, {
|
|
662
|
+
onDone: async () => {
|
|
663
|
+
const latest = data.getChatJob(next.id);
|
|
664
|
+
if (!latest || latest.status === 'cancelled')
|
|
665
|
+
return;
|
|
666
|
+
data.updateChatJob(next.id, {
|
|
667
|
+
status: 'completed',
|
|
668
|
+
completedAt: localTimestamp(),
|
|
669
|
+
error: null,
|
|
670
|
+
});
|
|
671
|
+
data.touchConversationActivity(next.conversation_id);
|
|
672
|
+
},
|
|
673
|
+
onError: async (payload) => {
|
|
674
|
+
const latest = data.getChatJob(next.id);
|
|
675
|
+
if (!latest || latest.status === 'cancelled')
|
|
676
|
+
return;
|
|
677
|
+
const errorText = String(payload?.error || 'Background chat failed');
|
|
678
|
+
const existing = data.getMessage(next.assistant_message_id);
|
|
679
|
+
const existingContent = (existing?.content || '').trim();
|
|
680
|
+
data.updateMessage(next.assistant_message_id, {
|
|
681
|
+
content: existingContent ? `${existingContent}\n\n**Error:** ${errorText}` : `**Error:** ${errorText}`,
|
|
682
|
+
botId: next.bot_id,
|
|
683
|
+
});
|
|
684
|
+
data.updateChatJob(next.id, {
|
|
685
|
+
status: 'failed',
|
|
686
|
+
error: errorText,
|
|
687
|
+
completedAt: localTimestamp(),
|
|
688
|
+
});
|
|
689
|
+
data.touchConversationActivity(next.conversation_id);
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
catch (err) {
|
|
694
|
+
const latest = data.getChatJob(next.id);
|
|
695
|
+
if (!latest)
|
|
696
|
+
return;
|
|
697
|
+
if (latest.status === 'cancelled' || err?.name === 'AbortError') {
|
|
698
|
+
finalizeCancelledChatJobMessage(latest);
|
|
699
|
+
data.updateChatJob(next.id, {
|
|
700
|
+
status: 'cancelled',
|
|
701
|
+
cancelledAt: latest.cancelled_at || localTimestamp(),
|
|
702
|
+
completedAt: latest.completed_at || localTimestamp(),
|
|
703
|
+
});
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const errorText = err?.message || 'Background chat failed';
|
|
707
|
+
const existing = data.getMessage(next.assistant_message_id);
|
|
708
|
+
const existingContent = (existing?.content || '').trim();
|
|
709
|
+
data.updateMessage(next.assistant_message_id, {
|
|
710
|
+
content: existingContent ? `${existingContent}\n\n**Error:** ${errorText}` : `**Error:** ${errorText}`,
|
|
711
|
+
botId: next.bot_id,
|
|
712
|
+
});
|
|
713
|
+
data.updateChatJob(next.id, {
|
|
714
|
+
status: 'failed',
|
|
715
|
+
error: errorText,
|
|
716
|
+
completedAt: localTimestamp(),
|
|
717
|
+
});
|
|
718
|
+
data.touchConversationActivity(next.conversation_id);
|
|
719
|
+
}
|
|
720
|
+
finally {
|
|
721
|
+
runningChatJobControllers.delete(next.id);
|
|
722
|
+
void runQueuedChatJobs();
|
|
723
|
+
}
|
|
724
|
+
})();
|
|
725
|
+
}
|
|
726
|
+
};
|
|
536
727
|
async function relayConnectedChat(req, res) {
|
|
537
728
|
if (!isConnectedMode())
|
|
538
729
|
return false;
|
|
@@ -678,6 +869,18 @@ function startLocalServer(opts) {
|
|
|
678
869
|
});
|
|
679
870
|
return state.global.auth;
|
|
680
871
|
}
|
|
872
|
+
async function exchangeDesktopAuthCode(code, redirectUri) {
|
|
873
|
+
const response = await fetch(`${config_1.FUNOLIO_API_URL}/api/v1/agent/auth/token`, {
|
|
874
|
+
method: 'POST',
|
|
875
|
+
headers: { 'Content-Type': 'application/json' },
|
|
876
|
+
body: JSON.stringify({ code, redirectUri }),
|
|
877
|
+
});
|
|
878
|
+
const body = await response.json().catch(() => ({}));
|
|
879
|
+
if (!response.ok) {
|
|
880
|
+
throw new Error(body.error || 'Code exchange failed');
|
|
881
|
+
}
|
|
882
|
+
return persistDesktopAuth(body);
|
|
883
|
+
}
|
|
681
884
|
app.post('/api/auth/clear-session', (_req, res) => {
|
|
682
885
|
try {
|
|
683
886
|
const current = (0, wizard_state_1.loadWizardState)();
|
|
@@ -718,8 +921,8 @@ function startLocalServer(opts) {
|
|
|
718
921
|
app.post('/api/auth/google/start', (_req, res) => {
|
|
719
922
|
try {
|
|
720
923
|
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)}`;
|
|
924
|
+
const redirectUri = `http://127.0.0.1:${port}/auth/complete`;
|
|
925
|
+
const authUrl = `${config_1.FUNOLIO_API_URL}/auth/agent-google?state=${encodeURIComponent(state)}&redirect_uri=${encodeURIComponent(redirectUri)}`;
|
|
723
926
|
res.json({ authUrl, state, redirectUri });
|
|
724
927
|
}
|
|
725
928
|
catch (err) {
|
|
@@ -732,21 +935,55 @@ function startLocalServer(opts) {
|
|
|
732
935
|
if (!code) {
|
|
733
936
|
return res.status(400).json({ error: 'Code is required' });
|
|
734
937
|
}
|
|
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));
|
|
938
|
+
res.json(await exchangeDesktopAuthCode(code, 'funolio://wizard?step=login'));
|
|
745
939
|
}
|
|
746
940
|
catch (err) {
|
|
747
941
|
res.status(500).json({ error: err.message || 'Code exchange failed' });
|
|
748
942
|
}
|
|
749
943
|
});
|
|
944
|
+
app.get('/auth/complete', async (req, res) => {
|
|
945
|
+
const code = typeof req.query?.code === 'string' ? req.query.code.trim() : '';
|
|
946
|
+
const state = typeof req.query?.state === 'string' ? req.query.state.trim() : '';
|
|
947
|
+
const escapeHtml = (value) => value
|
|
948
|
+
.replace(/&/g, '&')
|
|
949
|
+
.replace(/</g, '<')
|
|
950
|
+
.replace(/>/g, '>')
|
|
951
|
+
.replace(/"/g, '"')
|
|
952
|
+
.replace(/'/g, ''');
|
|
953
|
+
if (!code) {
|
|
954
|
+
return res.status(400).send(`<!doctype html>
|
|
955
|
+
<html><head><meta charset="utf-8" /><title>Funolio Login</title></head>
|
|
956
|
+
<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;">
|
|
957
|
+
<div style="max-width:480px;background:#111827;border:1px solid rgba(148,163,184,.2);border-radius:16px;padding:28px;text-align:center;">
|
|
958
|
+
<h1 style="margin:0 0 12px;font-size:24px;">Missing login code</h1>
|
|
959
|
+
<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>
|
|
960
|
+
</div>
|
|
961
|
+
</body></html>`);
|
|
962
|
+
}
|
|
963
|
+
try {
|
|
964
|
+
await exchangeDesktopAuthCode(code, `http://127.0.0.1:${port}/auth/complete`);
|
|
965
|
+
return res.send(`<!doctype html>
|
|
966
|
+
<html><head><meta charset="utf-8" /><title>Funolio Login Complete</title></head>
|
|
967
|
+
<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;">
|
|
968
|
+
<div style="max-width:480px;background:#111827;border:1px solid rgba(148,163,184,.2);border-radius:16px;padding:28px;text-align:center;">
|
|
969
|
+
<h1 style="margin:0 0 12px;font-size:24px;">Funolio login complete</h1>
|
|
970
|
+
<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>
|
|
971
|
+
<p style="margin:0;font-size:13px;color:#94a3b8;">You can close this browser tab.</p>
|
|
972
|
+
</div>
|
|
973
|
+
</body></html>`);
|
|
974
|
+
}
|
|
975
|
+
catch (err) {
|
|
976
|
+
return res.status(500).send(`<!doctype html>
|
|
977
|
+
<html><head><meta charset="utf-8" /><title>Funolio Login Failed</title></head>
|
|
978
|
+
<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;">
|
|
979
|
+
<div style="max-width:560px;background:#111827;border:1px solid rgba(239,68,68,.28);border-radius:16px;padding:28px;text-align:center;">
|
|
980
|
+
<h1 style="margin:0 0 12px;font-size:24px;color:#f87171;">Funolio login failed</h1>
|
|
981
|
+
<p style="margin:0 0 16px;color:#fecaca;line-height:1.6;">${escapeHtml(err?.message || 'Code exchange failed')}</p>
|
|
982
|
+
<p style="margin:0;font-size:13px;color:#94a3b8;">Close this tab and try again.</p>
|
|
983
|
+
</div>
|
|
984
|
+
</body></html>`);
|
|
985
|
+
}
|
|
986
|
+
});
|
|
750
987
|
app.post('/api/auth/forgot-password', async (req, res) => {
|
|
751
988
|
try {
|
|
752
989
|
const email = typeof req.body?.email === 'string' ? req.body.email.trim().toLowerCase() : '';
|
|
@@ -771,6 +1008,22 @@ function startLocalServer(opts) {
|
|
|
771
1008
|
res.status(500).json({ error: err.message || 'Failed to send reset email' });
|
|
772
1009
|
}
|
|
773
1010
|
});
|
|
1011
|
+
app.use(async (req, res, next) => {
|
|
1012
|
+
const publicPrefixes = ['/api/health', '/api/runtime/', '/api/auth/', '/api/wizard/'];
|
|
1013
|
+
if (publicPrefixes.some((prefix) => req.path === prefix || req.path.startsWith(prefix))) {
|
|
1014
|
+
return next();
|
|
1015
|
+
}
|
|
1016
|
+
try {
|
|
1017
|
+
const auth = await getHydratedDesktopAuth();
|
|
1018
|
+
if (!auth?.hasSession || !auth?.token) {
|
|
1019
|
+
return res.status(401).json({ error: 'Authentication required' });
|
|
1020
|
+
}
|
|
1021
|
+
return next();
|
|
1022
|
+
}
|
|
1023
|
+
catch (err) {
|
|
1024
|
+
return res.status(500).json({ error: err.message || 'Authentication check failed' });
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
774
1027
|
app.get('/api/providers/catalog', (_req, res) => {
|
|
775
1028
|
try {
|
|
776
1029
|
res.json({ providers: (0, wizard_support_1.getProviderCatalog)() });
|
|
@@ -1113,13 +1366,25 @@ function startLocalServer(opts) {
|
|
|
1113
1366
|
const runtime = (0, server_runtime_1.getRuntimeConnectionConfig)();
|
|
1114
1367
|
const auth = await getHydratedDesktopAuth();
|
|
1115
1368
|
const profile = await (0, server_adapter_1.getServerBot)(auth, runtime, req.params.id);
|
|
1116
|
-
return res.json({ profile, conversationCount: 0 });
|
|
1369
|
+
return res.json({ profile, conversationCount: 0, messageCount: 0, recentConversations: [] });
|
|
1117
1370
|
}
|
|
1118
1371
|
const profile = data.getAgentProfile(req.params.id);
|
|
1119
1372
|
if (!profile)
|
|
1120
1373
|
return res.status(404).json({ error: 'Not found' });
|
|
1121
1374
|
const convCount = data.countConversations(req.params.id);
|
|
1122
|
-
|
|
1375
|
+
const messageCount = data.countMessagesForBot(req.params.id);
|
|
1376
|
+
const recentConversations = data.listBotConversationActivity(req.params.id, 8).map((conversation) => ({
|
|
1377
|
+
id: conversation.id,
|
|
1378
|
+
agent_id: conversation.agent_id,
|
|
1379
|
+
title: conversation.title,
|
|
1380
|
+
updated_at: conversation.updated_at,
|
|
1381
|
+
created_at: conversation.created_at,
|
|
1382
|
+
message_count: conversation.message_count,
|
|
1383
|
+
bot_message_count: conversation.bot_message_count,
|
|
1384
|
+
bot_last_message_at: conversation.bot_last_message_at,
|
|
1385
|
+
project_name: conversation.project_name,
|
|
1386
|
+
}));
|
|
1387
|
+
res.json({ profile, conversationCount: convCount, messageCount, recentConversations });
|
|
1123
1388
|
}
|
|
1124
1389
|
catch (err) {
|
|
1125
1390
|
res.status(500).json({ error: err.message });
|
|
@@ -1565,7 +1830,7 @@ function startLocalServer(opts) {
|
|
|
1565
1830
|
if (msgs.length < 3)
|
|
1566
1831
|
return res.json({ suggestion: null, reason: 'Not enough messages' });
|
|
1567
1832
|
const conv = data.getConversation(conversationId);
|
|
1568
|
-
const clerk = (0, clerk_model_1.getClerk)();
|
|
1833
|
+
const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: 'local_desktop' });
|
|
1569
1834
|
if (!clerk) {
|
|
1570
1835
|
// Fallback: use first few words of first user message
|
|
1571
1836
|
const firstUser = msgs.find(m => m.role === 'user');
|
|
@@ -1774,6 +2039,28 @@ function startLocalServer(opts) {
|
|
|
1774
2039
|
res.status(500).json({ error: err.message });
|
|
1775
2040
|
}
|
|
1776
2041
|
});
|
|
2042
|
+
app.patch('/api/messages/:id', (req, res) => {
|
|
2043
|
+
try {
|
|
2044
|
+
if (isConnectedMode()) {
|
|
2045
|
+
return res.status(501).json({ error: 'Message updates are local-mode only' });
|
|
2046
|
+
}
|
|
2047
|
+
const updated = data.updateMessage(req.params.id, {
|
|
2048
|
+
content: req.body?.content,
|
|
2049
|
+
model: req.body?.model,
|
|
2050
|
+
botId: req.body?.botId,
|
|
2051
|
+
agentName: req.body?.agentName,
|
|
2052
|
+
resultArtifact: req.body?.resultArtifact,
|
|
2053
|
+
resultSummary: req.body?.resultSummary,
|
|
2054
|
+
resultStatus: req.body?.resultStatus,
|
|
2055
|
+
});
|
|
2056
|
+
if (!updated)
|
|
2057
|
+
return res.status(404).json({ error: 'Message not found' });
|
|
2058
|
+
res.json(updated);
|
|
2059
|
+
}
|
|
2060
|
+
catch (err) {
|
|
2061
|
+
res.status(500).json({ error: err.message });
|
|
2062
|
+
}
|
|
2063
|
+
});
|
|
1777
2064
|
app.get('/api/conversations/:id/orchestration-audit', (req, res) => {
|
|
1778
2065
|
try {
|
|
1779
2066
|
if (isConnectedMode()) {
|
|
@@ -2064,7 +2351,13 @@ function startLocalServer(opts) {
|
|
|
2064
2351
|
const limit = parseInt(req.query.limit, 10) || 25;
|
|
2065
2352
|
const beforeSeq = req.query.beforeSeq ? parseInt(req.query.beforeSeq, 10) : 0;
|
|
2066
2353
|
const rounds = req.query.rounds ? parseInt(req.query.rounds, 10) : 0;
|
|
2354
|
+
const startSeq = req.query.startSeq ? parseInt(req.query.startSeq, 10) : 0;
|
|
2355
|
+
const endSeq = req.query.endSeq ? parseInt(req.query.endSeq, 10) : 0;
|
|
2356
|
+
const hasDirectRange = startSeq > 0 && endSeq >= startSeq;
|
|
2067
2357
|
if (isConnectedMode()) {
|
|
2358
|
+
if (hasDirectRange) {
|
|
2359
|
+
return res.status(400).json({ error: 'Direct message range fetch is only available in local storage mode' });
|
|
2360
|
+
}
|
|
2068
2361
|
const runtime = (0, server_runtime_1.getRuntimeConnectionConfig)();
|
|
2069
2362
|
const auth = await getHydratedDesktopAuth();
|
|
2070
2363
|
const result = await (0, server_adapter_1.listServerConversationMessages)(auth, runtime, req.params.id, {
|
|
@@ -2073,6 +2366,9 @@ function startLocalServer(opts) {
|
|
|
2073
2366
|
});
|
|
2074
2367
|
return res.json(result.messages);
|
|
2075
2368
|
}
|
|
2369
|
+
if (hasDirectRange) {
|
|
2370
|
+
return res.json(data.getMessagesInRange(req.params.id, startSeq, endSeq));
|
|
2371
|
+
}
|
|
2076
2372
|
if (beforeSeq > 0) {
|
|
2077
2373
|
// Backward paging: get N rounds or messages before given seq, returned in ASC order
|
|
2078
2374
|
const msgs = rounds > 0
|
|
@@ -2141,11 +2437,161 @@ function startLocalServer(opts) {
|
|
|
2141
2437
|
res.status(500).json({ error: err.message });
|
|
2142
2438
|
}
|
|
2143
2439
|
});
|
|
2440
|
+
app.post('/api/chat/jobs', async (req, res) => {
|
|
2441
|
+
try {
|
|
2442
|
+
if (isConnectedMode()) {
|
|
2443
|
+
return res.status(501).json({ error: 'Background chat jobs are local-mode only' });
|
|
2444
|
+
}
|
|
2445
|
+
const { conversationId, message, botId, pinnedMessageIds, topicId, projectId, orchestrationEnabled, } = req.body || {};
|
|
2446
|
+
if (!message || !String(message).trim()) {
|
|
2447
|
+
return res.status(400).json({ error: 'message is required' });
|
|
2448
|
+
}
|
|
2449
|
+
let profile = botId ? data.getAgentProfile(String(botId)) : data.getDefaultAgentProfile();
|
|
2450
|
+
if (!profile) {
|
|
2451
|
+
return res.status(400).json({ error: 'No bot configured. Create one first.' });
|
|
2452
|
+
}
|
|
2453
|
+
const shouldUseOrchestratorMode = orchestrationEnabled !== false && (0, orchestrator_profile_1.isOrchestratorProfile)(profile);
|
|
2454
|
+
if (shouldUseOrchestratorMode) {
|
|
2455
|
+
return res.status(400).json({ error: 'Background chat jobs do not support orchestrator mode yet.' });
|
|
2456
|
+
}
|
|
2457
|
+
let convId = conversationId ? String(conversationId) : '';
|
|
2458
|
+
if (convId) {
|
|
2459
|
+
const latestJob = data.getLatestConversationChatJob(convId);
|
|
2460
|
+
if (latestJob && (latestJob.status === 'queued' || latestJob.status === 'running')) {
|
|
2461
|
+
return res.status(409).json({ error: 'This conversation already has a pending response.' });
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
if (!convId) {
|
|
2465
|
+
let topicProjectId = null;
|
|
2466
|
+
if (topicId) {
|
|
2467
|
+
const topic = data.getTopic(String(topicId));
|
|
2468
|
+
topicProjectId = topic?.project_id ?? null;
|
|
2469
|
+
}
|
|
2470
|
+
const requestedProjectId = (projectId ? String(projectId) : null) || topicProjectId;
|
|
2471
|
+
const conv = data.createConversation(profile.id, '', 'local', {
|
|
2472
|
+
projectId: requestedProjectId,
|
|
2473
|
+
});
|
|
2474
|
+
convId = conv.id;
|
|
2475
|
+
}
|
|
2476
|
+
if (topicId && convId) {
|
|
2477
|
+
try {
|
|
2478
|
+
data.upsertConversationTopicSegment(convId, String(topicId));
|
|
2479
|
+
}
|
|
2480
|
+
catch { /* best effort */ }
|
|
2481
|
+
}
|
|
2482
|
+
if (!topicId && projectId && convId) {
|
|
2483
|
+
const selectedProject = data.getProject(String(projectId));
|
|
2484
|
+
if (selectedProject) {
|
|
2485
|
+
data.updateConversation(convId, {
|
|
2486
|
+
projectId: selectedProject.id,
|
|
2487
|
+
projectName: selectedProject.name,
|
|
2488
|
+
});
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
const savedUserMessage = data.addMessage(convId, 'user', String(message));
|
|
2492
|
+
(0, context_window_1.incrementTurnCount)(convId);
|
|
2493
|
+
const assistantMessage = data.addMessage(convId, 'assistant', '', profile.model || undefined, undefined, profile.id, profile.name);
|
|
2494
|
+
const conv = data.getConversation(convId);
|
|
2495
|
+
if (conv && !conv.title?.trim() && (conv.turn_count || 0) <= 1) {
|
|
2496
|
+
const shortTitle = String(message).slice(0, 60).replace(/\n/g, ' ').trim();
|
|
2497
|
+
data.updateConversation(convId, { title: shortTitle || 'New Chat' });
|
|
2498
|
+
}
|
|
2499
|
+
const job = data.createChatJob({
|
|
2500
|
+
conversationId: convId,
|
|
2501
|
+
userMessageId: savedUserMessage.id,
|
|
2502
|
+
assistantMessageId: assistantMessage.id,
|
|
2503
|
+
botId: profile.id,
|
|
2504
|
+
status: 'queued',
|
|
2505
|
+
requestJson: JSON.stringify({
|
|
2506
|
+
pinnedMessageIds: Array.isArray(pinnedMessageIds) ? pinnedMessageIds : [],
|
|
2507
|
+
topicId: topicId ? String(topicId) : null,
|
|
2508
|
+
projectId: projectId ? String(projectId) : null,
|
|
2509
|
+
orchestrationEnabled: orchestrationEnabled !== false,
|
|
2510
|
+
}),
|
|
2511
|
+
});
|
|
2512
|
+
void runQueuedChatJobs();
|
|
2513
|
+
res.status(201).json({
|
|
2514
|
+
ok: true,
|
|
2515
|
+
conversationId: convId,
|
|
2516
|
+
userMessageId: savedUserMessage.id,
|
|
2517
|
+
assistantMessageId: assistantMessage.id,
|
|
2518
|
+
jobId: job.id,
|
|
2519
|
+
status: job.status,
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
catch (err) {
|
|
2523
|
+
res.status(500).json({ error: err.message });
|
|
2524
|
+
}
|
|
2525
|
+
});
|
|
2526
|
+
app.post('/api/chat/jobs/:id/cancel', async (req, res) => {
|
|
2527
|
+
try {
|
|
2528
|
+
const job = data.getChatJob(req.params.id);
|
|
2529
|
+
if (!job)
|
|
2530
|
+
return res.status(404).json({ error: 'Not found' });
|
|
2531
|
+
if (job.status === 'completed' || job.status === 'failed' || job.status === 'cancelled') {
|
|
2532
|
+
return res.json({ ok: true, status: job.status });
|
|
2533
|
+
}
|
|
2534
|
+
const ts = localTimestamp();
|
|
2535
|
+
data.updateChatJob(job.id, {
|
|
2536
|
+
status: 'cancelled',
|
|
2537
|
+
cancelledAt: ts,
|
|
2538
|
+
completedAt: ts,
|
|
2539
|
+
});
|
|
2540
|
+
const controller = runningChatJobControllers.get(job.id);
|
|
2541
|
+
if (controller) {
|
|
2542
|
+
controller.abort();
|
|
2543
|
+
}
|
|
2544
|
+
else {
|
|
2545
|
+
finalizeCancelledChatJobMessage(job);
|
|
2546
|
+
data.touchConversationActivity(job.conversation_id);
|
|
2547
|
+
}
|
|
2548
|
+
res.json({ ok: true, status: 'cancelled' });
|
|
2549
|
+
}
|
|
2550
|
+
catch (err) {
|
|
2551
|
+
res.status(500).json({ error: err.message });
|
|
2552
|
+
}
|
|
2553
|
+
});
|
|
2144
2554
|
// ─── Chat (SSE streaming) ──────────────────────────────────
|
|
2555
|
+
app.post('/api/conversations/:id/chat-job/cancel', async (req, res) => {
|
|
2556
|
+
try {
|
|
2557
|
+
const latestJob = data.getLatestConversationChatJob(req.params.id);
|
|
2558
|
+
if (!latestJob)
|
|
2559
|
+
return res.status(404).json({ error: 'Chat job not found' });
|
|
2560
|
+
if (latestJob.status === 'completed' || latestJob.status === 'failed' || latestJob.status === 'cancelled') {
|
|
2561
|
+
return res.json({ ok: true, status: latestJob.status, jobId: latestJob.id });
|
|
2562
|
+
}
|
|
2563
|
+
const ts = localTimestamp();
|
|
2564
|
+
data.updateChatJob(latestJob.id, {
|
|
2565
|
+
status: 'cancelled',
|
|
2566
|
+
cancelledAt: ts,
|
|
2567
|
+
completedAt: ts,
|
|
2568
|
+
});
|
|
2569
|
+
const controller = runningChatJobControllers.get(latestJob.id);
|
|
2570
|
+
if (controller) {
|
|
2571
|
+
controller.abort();
|
|
2572
|
+
}
|
|
2573
|
+
else {
|
|
2574
|
+
finalizeCancelledChatJobMessage(latestJob);
|
|
2575
|
+
data.touchConversationActivity(latestJob.conversation_id);
|
|
2576
|
+
void runQueuedChatJobs();
|
|
2577
|
+
}
|
|
2578
|
+
res.json({ ok: true, status: 'cancelled', jobId: latestJob.id });
|
|
2579
|
+
}
|
|
2580
|
+
catch (err) {
|
|
2581
|
+
res.status(500).json({ error: err.message });
|
|
2582
|
+
}
|
|
2583
|
+
});
|
|
2145
2584
|
app.post('/api/chat', async (req, res) => {
|
|
2146
2585
|
const activityErrorContext = {};
|
|
2586
|
+
const routeAbortController = new AbortController();
|
|
2587
|
+
let responseEnded = false;
|
|
2588
|
+
const abortOnClientClose = () => {
|
|
2589
|
+
responseEnded = true;
|
|
2590
|
+
routeAbortController.abort();
|
|
2591
|
+
};
|
|
2592
|
+
req.on('close', abortOnClientClose);
|
|
2147
2593
|
try {
|
|
2148
|
-
|
|
2594
|
+
let { conversationId, message, botId, skipUserMessage, pinnedMessageIds, topicId, projectId, workflowTemplateId, orchestrationEnabled, chatJobId, assistantMessageId, persistAssistantPlaceholder, } = req.body;
|
|
2149
2595
|
if (!message)
|
|
2150
2596
|
return res.status(400).json({ error: 'message is required' });
|
|
2151
2597
|
if (await relayConnectedChat(req, res)) {
|
|
@@ -2203,6 +2649,7 @@ function startLocalServer(opts) {
|
|
|
2203
2649
|
const activityExpiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString().replace('T', ' ').replace('Z', '');
|
|
2204
2650
|
activityErrorContext.conversationId = convId;
|
|
2205
2651
|
activityErrorContext.streamId = activityStreamId;
|
|
2652
|
+
activityErrorContext.messageId = assistantMessageId ? String(assistantMessageId) : null;
|
|
2206
2653
|
activityErrorContext.botId = profile?.id ?? null;
|
|
2207
2654
|
activityErrorContext.agentName = profile?.name ?? null;
|
|
2208
2655
|
activityErrorContext.expiresAt = activityExpiresAt;
|
|
@@ -2210,6 +2657,7 @@ function startLocalServer(opts) {
|
|
|
2210
2657
|
try {
|
|
2211
2658
|
data.createMessageActivity({
|
|
2212
2659
|
conversationId: convId,
|
|
2660
|
+
messageId: assistantMessageId ? String(assistantMessageId) : null,
|
|
2213
2661
|
streamId: activityStreamId,
|
|
2214
2662
|
botId: profile?.id ?? null,
|
|
2215
2663
|
agentName: profile?.name ?? null,
|
|
@@ -2223,6 +2671,36 @@ function startLocalServer(opts) {
|
|
|
2223
2671
|
// Best effort only
|
|
2224
2672
|
}
|
|
2225
2673
|
};
|
|
2674
|
+
const workerBotByName = new Map();
|
|
2675
|
+
const resolveWorkerBotId = (agentName) => {
|
|
2676
|
+
const key = String(agentName || '').trim().toLowerCase();
|
|
2677
|
+
if (!key)
|
|
2678
|
+
return null;
|
|
2679
|
+
if (workerBotByName.has(key))
|
|
2680
|
+
return workerBotByName.get(key) || null;
|
|
2681
|
+
const match = data.listAgentProfiles().find((agent) => agent.name.trim().toLowerCase() === key);
|
|
2682
|
+
const resolved = match?.id || null;
|
|
2683
|
+
workerBotByName.set(key, resolved || '');
|
|
2684
|
+
return resolved;
|
|
2685
|
+
};
|
|
2686
|
+
const recordWorkerActivity = (activityType, event, payload, summary) => {
|
|
2687
|
+
try {
|
|
2688
|
+
data.createMessageActivity({
|
|
2689
|
+
conversationId: convId,
|
|
2690
|
+
messageId: assistantMessageId ? String(assistantMessageId) : null,
|
|
2691
|
+
streamId: activityStreamId,
|
|
2692
|
+
botId: resolveWorkerBotId(event.agentName) || null,
|
|
2693
|
+
agentName: event.agentName || null,
|
|
2694
|
+
activityType,
|
|
2695
|
+
summary: summary || null,
|
|
2696
|
+
payload,
|
|
2697
|
+
expiresAt: activityExpiresAt,
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
catch {
|
|
2701
|
+
// Best effort only
|
|
2702
|
+
}
|
|
2703
|
+
};
|
|
2226
2704
|
// Save user message (skip if multi-bot call where first bot already saved it)
|
|
2227
2705
|
let savedUserMessage = null;
|
|
2228
2706
|
if (!skipUserMessage) {
|
|
@@ -2232,7 +2710,7 @@ function startLocalServer(opts) {
|
|
|
2232
2710
|
const effectiveProjectId = projectId ? String(projectId) : (convForPolicy?.project_id || undefined);
|
|
2233
2711
|
const projectForPolicy = effectiveProjectId ? data.getProject(effectiveProjectId) : undefined;
|
|
2234
2712
|
const currentPolicy = data.getEffectiveOrchestrationPolicy(effectiveProjectId);
|
|
2235
|
-
const clerkForPolicy = (0, clerk_model_1.getClerk)();
|
|
2713
|
+
const clerkForPolicy = (0, clerk_model_1.getClerk)({ runtimeMode: 'local_desktop' });
|
|
2236
2714
|
const agentNames = data.listAgentProfiles().map((agent) => agent.name);
|
|
2237
2715
|
if (clerkForPolicy && savedUserMessage) {
|
|
2238
2716
|
void (0, policy_detection_1.stagePolicyDetectionForMessage)({
|
|
@@ -2251,9 +2729,14 @@ function startLocalServer(opts) {
|
|
|
2251
2729
|
});
|
|
2252
2730
|
}
|
|
2253
2731
|
}
|
|
2732
|
+
if (!assistantMessageId && persistAssistantPlaceholder === true) {
|
|
2733
|
+
const placeholder = data.addMessage(convId, 'assistant', '', buildConfiguredMessageModel(profile), undefined, profile?.id || undefined, profile?.name || undefined);
|
|
2734
|
+
assistantMessageId = placeholder.id;
|
|
2735
|
+
}
|
|
2254
2736
|
// ─── Orchestrator Mode Branch ─────────────────────────
|
|
2255
|
-
|
|
2256
|
-
|
|
2737
|
+
const shouldUseOrchestratorMode = orchestrationEnabled !== false && (0, orchestrator_profile_1.isOrchestratorProfile)(profile);
|
|
2738
|
+
if (shouldUseOrchestratorMode) {
|
|
2739
|
+
const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: 'local_desktop' });
|
|
2257
2740
|
if (!clerk) {
|
|
2258
2741
|
// Fix #2: Do not silently fall through to direct chat — return a clear error
|
|
2259
2742
|
return res.status(400).json({
|
|
@@ -2262,7 +2745,7 @@ function startLocalServer(opts) {
|
|
|
2262
2745
|
}
|
|
2263
2746
|
const { OrchestratorAgent } = require('./orchestrator');
|
|
2264
2747
|
const { getWorkflowEngine } = require('./workflow-engine');
|
|
2265
|
-
const workflowEngine = getWorkflowEngine(opts.projectDir);
|
|
2748
|
+
const workflowEngine = getWorkflowEngine(opts.projectDir, 'local_desktop');
|
|
2266
2749
|
const orchestrator = new OrchestratorAgent(clerk, workflowEngine);
|
|
2267
2750
|
// Resolve effective project ID from request or existing conversation
|
|
2268
2751
|
const conv = data.getConversation(convId);
|
|
@@ -2280,6 +2763,8 @@ function startLocalServer(opts) {
|
|
|
2280
2763
|
'X-Conversation-Id': convId,
|
|
2281
2764
|
});
|
|
2282
2765
|
const sendEvent = (event, payload) => {
|
|
2766
|
+
if (responseEnded)
|
|
2767
|
+
return;
|
|
2283
2768
|
res.write(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
2284
2769
|
};
|
|
2285
2770
|
let orchestratorRuntimeLabel = '';
|
|
@@ -2290,7 +2775,7 @@ function startLocalServer(opts) {
|
|
|
2290
2775
|
orchestratorRuntime.model || profile.model || '',
|
|
2291
2776
|
runtimeModeLabel(orchestratorRuntime.runtimeMode, orchestratorRuntime.runtimeSource),
|
|
2292
2777
|
].filter(Boolean).join(' | ');
|
|
2293
|
-
orchestratorRuntimePayload = runtimePayloadForDisplay(profile.provider, orchestratorRuntime.model || profile.model || null, orchestratorRuntime.runtimeMode, orchestratorRuntime.runtimeSource || null
|
|
2778
|
+
orchestratorRuntimePayload = runtimePayloadForDisplay(profile.provider, orchestratorRuntime.model || profile.model || null, orchestratorRuntime.runtimeMode, orchestratorRuntime.runtimeSource || null);
|
|
2294
2779
|
}
|
|
2295
2780
|
catch {
|
|
2296
2781
|
orchestratorRuntimeLabel = buildConfiguredMessageModel(profile);
|
|
@@ -2302,6 +2787,7 @@ function startLocalServer(opts) {
|
|
|
2302
2787
|
let lastProgressChat = '';
|
|
2303
2788
|
let lastProgressActivity = '';
|
|
2304
2789
|
let selfExecuteStreamed = false;
|
|
2790
|
+
let hasWorkerActivity = false;
|
|
2305
2791
|
try {
|
|
2306
2792
|
let lastInterimMessage = '';
|
|
2307
2793
|
const response = await orchestrator.handleUserMessage(message, convId, {
|
|
@@ -2311,21 +2797,102 @@ function startLocalServer(opts) {
|
|
|
2311
2797
|
workflowTemplateId: workflowTemplateId || undefined,
|
|
2312
2798
|
onWorkerChunk: (event) => {
|
|
2313
2799
|
if (event.type === 'step_start') {
|
|
2800
|
+
hasWorkerActivity = true;
|
|
2801
|
+
recordWorkerActivity('worker_step_start', event, {
|
|
2802
|
+
stepId: event.stepId,
|
|
2803
|
+
agentName: event.agentName,
|
|
2804
|
+
description: event.description,
|
|
2805
|
+
stepIndex: event.stepIndex,
|
|
2806
|
+
totalSteps: event.totalSteps,
|
|
2807
|
+
botId: resolveWorkerBotId(event.agentName),
|
|
2808
|
+
});
|
|
2314
2809
|
sendEvent('worker_step_start', {
|
|
2315
2810
|
stepId: event.stepId,
|
|
2811
|
+
botId: resolveWorkerBotId(event.agentName),
|
|
2316
2812
|
agentName: event.agentName,
|
|
2317
2813
|
description: event.description,
|
|
2318
2814
|
stepIndex: event.stepIndex,
|
|
2319
2815
|
totalSteps: event.totalSteps,
|
|
2320
2816
|
});
|
|
2321
2817
|
}
|
|
2818
|
+
else if (event.type === 'worker_chunk') {
|
|
2819
|
+
hasWorkerActivity = true;
|
|
2820
|
+
recordWorkerActivity('worker_chunk', event, {
|
|
2821
|
+
stepId: event.stepId,
|
|
2822
|
+
agentName: event.agentName,
|
|
2823
|
+
description: event.description,
|
|
2824
|
+
stepIndex: event.stepIndex,
|
|
2825
|
+
totalSteps: event.totalSteps,
|
|
2826
|
+
text: event.text,
|
|
2827
|
+
botId: resolveWorkerBotId(event.agentName),
|
|
2828
|
+
});
|
|
2829
|
+
sendEvent('worker_chunk', {
|
|
2830
|
+
stepId: event.stepId,
|
|
2831
|
+
botId: resolveWorkerBotId(event.agentName),
|
|
2832
|
+
agentName: event.agentName,
|
|
2833
|
+
description: event.description,
|
|
2834
|
+
stepIndex: event.stepIndex,
|
|
2835
|
+
totalSteps: event.totalSteps,
|
|
2836
|
+
text: event.text,
|
|
2837
|
+
});
|
|
2838
|
+
}
|
|
2839
|
+
else if (event.type === 'worker_tool_call') {
|
|
2840
|
+
hasWorkerActivity = true;
|
|
2841
|
+
recordWorkerActivity('worker_tool_call', event, {
|
|
2842
|
+
stepId: event.stepId,
|
|
2843
|
+
agentName: event.agentName,
|
|
2844
|
+
description: event.description,
|
|
2845
|
+
stepIndex: event.stepIndex,
|
|
2846
|
+
totalSteps: event.totalSteps,
|
|
2847
|
+
toolCallId: event.toolCallId,
|
|
2848
|
+
toolName: event.toolName,
|
|
2849
|
+
toolArguments: event.toolArguments,
|
|
2850
|
+
botId: resolveWorkerBotId(event.agentName),
|
|
2851
|
+
});
|
|
2852
|
+
sendEvent('worker_tool_call', {
|
|
2853
|
+
stepId: event.stepId,
|
|
2854
|
+
botId: resolveWorkerBotId(event.agentName),
|
|
2855
|
+
agentName: event.agentName,
|
|
2856
|
+
description: event.description,
|
|
2857
|
+
stepIndex: event.stepIndex,
|
|
2858
|
+
totalSteps: event.totalSteps,
|
|
2859
|
+
toolCallId: event.toolCallId,
|
|
2860
|
+
toolName: event.toolName,
|
|
2861
|
+
toolArguments: event.toolArguments,
|
|
2862
|
+
});
|
|
2863
|
+
}
|
|
2864
|
+
else if (event.type === 'worker_tool_result') {
|
|
2865
|
+
hasWorkerActivity = true;
|
|
2866
|
+
recordWorkerActivity('worker_tool_result', event, {
|
|
2867
|
+
stepId: event.stepId,
|
|
2868
|
+
agentName: event.agentName,
|
|
2869
|
+
description: event.description,
|
|
2870
|
+
stepIndex: event.stepIndex,
|
|
2871
|
+
totalSteps: event.totalSteps,
|
|
2872
|
+
toolCallId: event.toolCallId,
|
|
2873
|
+
toolName: event.toolName,
|
|
2874
|
+
toolOutput: event.toolOutput,
|
|
2875
|
+
toolIsError: event.toolIsError,
|
|
2876
|
+
botId: resolveWorkerBotId(event.agentName),
|
|
2877
|
+
});
|
|
2878
|
+
sendEvent('worker_tool_result', {
|
|
2879
|
+
stepId: event.stepId,
|
|
2880
|
+
botId: resolveWorkerBotId(event.agentName),
|
|
2881
|
+
agentName: event.agentName,
|
|
2882
|
+
description: event.description,
|
|
2883
|
+
stepIndex: event.stepIndex,
|
|
2884
|
+
totalSteps: event.totalSteps,
|
|
2885
|
+
toolCallId: event.toolCallId,
|
|
2886
|
+
toolName: event.toolName,
|
|
2887
|
+
toolOutput: event.toolOutput,
|
|
2888
|
+
toolIsError: event.toolIsError,
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2322
2891
|
else if (event.type === 'chunk') {
|
|
2323
|
-
// Stream all text directly to main bubble — no worker cards
|
|
2324
2892
|
selfExecuteStreamed = true;
|
|
2325
2893
|
sendEvent('chunk', { text: event.text });
|
|
2326
2894
|
}
|
|
2327
2895
|
else if (event.type === 'tool_call') {
|
|
2328
|
-
// Show tool activity inline in main bubble
|
|
2329
2896
|
selfExecuteStreamed = true;
|
|
2330
2897
|
sendEvent('chunk', { text: `\n> Running ${event.toolName}...\n` });
|
|
2331
2898
|
}
|
|
@@ -2334,7 +2901,27 @@ function startLocalServer(opts) {
|
|
|
2334
2901
|
sendEvent('chunk', { text: `> [${icon}] ${event.toolName} completed\n` });
|
|
2335
2902
|
}
|
|
2336
2903
|
else if (event.type === 'step_done') {
|
|
2337
|
-
|
|
2904
|
+
hasWorkerActivity = true;
|
|
2905
|
+
recordWorkerActivity('worker_step_done', event, {
|
|
2906
|
+
stepId: event.stepId,
|
|
2907
|
+
agentName: event.agentName,
|
|
2908
|
+
description: event.description,
|
|
2909
|
+
stepIndex: event.stepIndex,
|
|
2910
|
+
totalSteps: event.totalSteps,
|
|
2911
|
+
status: event.status,
|
|
2912
|
+
summary: event.summary,
|
|
2913
|
+
botId: resolveWorkerBotId(event.agentName),
|
|
2914
|
+
});
|
|
2915
|
+
sendEvent('worker_step_done', {
|
|
2916
|
+
stepId: event.stepId,
|
|
2917
|
+
botId: resolveWorkerBotId(event.agentName),
|
|
2918
|
+
agentName: event.agentName,
|
|
2919
|
+
description: event.description,
|
|
2920
|
+
stepIndex: event.stepIndex,
|
|
2921
|
+
totalSteps: event.totalSteps,
|
|
2922
|
+
status: event.status,
|
|
2923
|
+
summary: event.summary,
|
|
2924
|
+
});
|
|
2338
2925
|
}
|
|
2339
2926
|
},
|
|
2340
2927
|
onProgress: (status) => {
|
|
@@ -2358,7 +2945,7 @@ function startLocalServer(opts) {
|
|
|
2358
2945
|
sendEvent('orchestrator_message', {
|
|
2359
2946
|
text: interimMessage,
|
|
2360
2947
|
botId: profile.id,
|
|
2361
|
-
agentName:
|
|
2948
|
+
agentName: 'Orchestrator',
|
|
2362
2949
|
});
|
|
2363
2950
|
}
|
|
2364
2951
|
},
|
|
@@ -2369,40 +2956,73 @@ function startLocalServer(opts) {
|
|
|
2369
2956
|
},
|
|
2370
2957
|
});
|
|
2371
2958
|
// 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
|
-
|
|
2959
|
+
const responseMeta = orchestrator.getLastResponseMeta();
|
|
2960
|
+
const finalAgentName = responseMeta?.agentName || 'Orchestrator';
|
|
2961
|
+
const finalBotId = responseMeta?.botId || (finalAgentName === 'Orchestrator' ? profile.id : undefined);
|
|
2962
|
+
const finalModelLabel = responseMeta?.modelLabel || orchestratorRuntimeLabel || undefined;
|
|
2963
|
+
const splitFinalMessage = hasWorkerActivity && finalAgentName === 'Clerk';
|
|
2964
|
+
if (splitFinalMessage) {
|
|
2965
|
+
const orchestratorMessage = data.addMessage(convId, 'assistant', '', orchestratorRuntimeLabel || buildConfiguredMessageModel(profile), undefined, profile.id, 'Orchestrator');
|
|
2966
|
+
data.attachMessageActivitiesToMessage(activityStreamId, orchestratorMessage.id);
|
|
2967
|
+
const clerkMessage = data.addMessage(convId, 'assistant', response, finalModelLabel, undefined, finalBotId, finalAgentName);
|
|
2968
|
+
data.createMessageActivity({
|
|
2969
|
+
conversationId: convId,
|
|
2970
|
+
messageId: clerkMessage.id,
|
|
2971
|
+
botId: finalBotId,
|
|
2972
|
+
agentName: finalAgentName,
|
|
2973
|
+
activityType: 'message',
|
|
2974
|
+
summary: 'Final assistant response',
|
|
2975
|
+
payload: { content: response },
|
|
2976
|
+
expiresAt: activityExpiresAt,
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2979
|
+
else {
|
|
2980
|
+
const savedMessage = data.addMessage(convId, 'assistant', response, finalModelLabel, undefined, finalBotId, finalAgentName);
|
|
2981
|
+
data.attachMessageActivitiesToMessage(activityStreamId, savedMessage.id);
|
|
2982
|
+
data.createMessageActivity({
|
|
2983
|
+
conversationId: convId,
|
|
2984
|
+
messageId: savedMessage.id,
|
|
2985
|
+
botId: finalBotId,
|
|
2986
|
+
agentName: finalAgentName,
|
|
2987
|
+
activityType: 'message',
|
|
2988
|
+
summary: 'Final orchestrator response',
|
|
2989
|
+
payload: { content: response },
|
|
2990
|
+
expiresAt: activityExpiresAt,
|
|
2991
|
+
});
|
|
2992
|
+
}
|
|
2384
2993
|
// Emit chunk + done events using the same SSE contract as normal chat
|
|
2385
2994
|
// Skip the final bulk chunk if we already streamed via worker_chunk (execute_self)
|
|
2386
|
-
if (!selfExecuteStreamed) {
|
|
2995
|
+
if (!selfExecuteStreamed && !splitFinalMessage) {
|
|
2387
2996
|
sendEvent('chunk', { text: response });
|
|
2388
2997
|
}
|
|
2389
2998
|
sendEvent('done', {
|
|
2390
2999
|
conversationId: convId,
|
|
2391
3000
|
content: response,
|
|
2392
|
-
|
|
3001
|
+
agentName: finalAgentName,
|
|
3002
|
+
botId: finalBotId,
|
|
3003
|
+
separateFinalMessage: splitFinalMessage,
|
|
3004
|
+
...((responseMeta?.modelLabel || orchestratorRuntimePayload)
|
|
3005
|
+
? {
|
|
3006
|
+
runtime: {
|
|
3007
|
+
...(orchestratorRuntimePayload || {}),
|
|
3008
|
+
...(responseMeta?.modelLabel ? { model: responseMeta.modelLabel } : {}),
|
|
3009
|
+
},
|
|
3010
|
+
}
|
|
3011
|
+
: {}),
|
|
2393
3012
|
tokenUsage: { inputTokens: 0, outputTokens: 0, totalTokens: 0, isApproximate: true, source: 'heuristic' },
|
|
2394
3013
|
});
|
|
3014
|
+
responseEnded = true;
|
|
2395
3015
|
res.end();
|
|
2396
|
-
// Background post-response processing
|
|
2397
|
-
(0, local_funnel_1.scheduleFunnelProcessing)(convId);
|
|
2398
3016
|
if ((0, context_window_1.shouldSummarize)(convId)) {
|
|
2399
3017
|
const catalog = (0, backfill_1.buildProjectTopicCatalog)();
|
|
2400
3018
|
(0, summarization_pipeline_1.runSummarization)(convId, catalog).catch(err => console.error(chalk_1.default.yellow(` [summarization] ${err.message}`)));
|
|
2401
3019
|
}
|
|
3020
|
+
(0, local_funnel_1.scheduleFunnelProcessing)(convId);
|
|
2402
3021
|
}
|
|
2403
3022
|
catch (orchErr) {
|
|
2404
3023
|
recordActivity('error', { error: orchErr.message }, orchErr.message);
|
|
2405
3024
|
sendEvent('error', { type: 'error', error: orchErr.message });
|
|
3025
|
+
responseEnded = true;
|
|
2406
3026
|
res.end();
|
|
2407
3027
|
}
|
|
2408
3028
|
return;
|
|
@@ -2414,15 +3034,18 @@ function startLocalServer(opts) {
|
|
|
2414
3034
|
const effectiveTimezone = configuredTz && configuredTz.toLowerCase() !== 'system'
|
|
2415
3035
|
? configuredTz
|
|
2416
3036
|
: Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2417
|
-
const
|
|
3037
|
+
const unrestrictedCliProfile = index_1.CLI_PROVIDERS.has(profile.provider);
|
|
3038
|
+
const allToolDefs = (0, index_2.getAllToolDefinitions)('local_desktop', mcpManager);
|
|
2418
3039
|
const configuredBuiltinTools = parseToolSelectionJson(profile.enabled_builtin_tools_json);
|
|
2419
3040
|
const configuredMcpTools = parseToolSelectionJson(profile.enabled_mcp_tools_json);
|
|
2420
|
-
const allowedToolNames =
|
|
3041
|
+
const allowedToolNames = unrestrictedCliProfile
|
|
3042
|
+
? new Set(allToolDefs.map((tool) => tool.name))
|
|
3043
|
+
: expandAllowedToolNames(allToolDefs, configuredBuiltinTools, configuredMcpTools);
|
|
2421
3044
|
const toolDefs = allToolDefs.filter((tool) => allowedToolNames.has(tool.name));
|
|
2422
3045
|
// Build system prompt via clerk (token-budgeted context injection)
|
|
2423
3046
|
let systemPrompt;
|
|
2424
3047
|
let llmSpawnCwd = opts.projectDir;
|
|
2425
|
-
const clerk = (0, clerk_model_1.getClerk)();
|
|
3048
|
+
const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: 'local_desktop' });
|
|
2426
3049
|
if (clerk) {
|
|
2427
3050
|
const conv = data.getConversation(convId);
|
|
2428
3051
|
const topicTitle = topicId ? data.getTopic(topicId)?.title : undefined;
|
|
@@ -2443,14 +3066,15 @@ function startLocalServer(opts) {
|
|
|
2443
3066
|
availableTools: toolDefs.map((tool) => ({ name: tool.name, description: tool.description })),
|
|
2444
3067
|
});
|
|
2445
3068
|
systemPrompt = built.systemPrompt;
|
|
2446
|
-
console.log(chalk_1.default.gray(` [clerk] Context: ${built.
|
|
3069
|
+
console.log(chalk_1.default.gray(` [clerk] Context: ${built.injectedSummaries} summaries (${built.contextTokensUsed} tokens)`));
|
|
2447
3070
|
}
|
|
2448
3071
|
else {
|
|
2449
3072
|
// Fallback: manual prompt building
|
|
2450
3073
|
systemPrompt = '[Bot Identity]\n' + (profile.soul_md
|
|
2451
|
-
|| 'You are
|
|
3074
|
+
|| 'You are an AI assistant running locally. You have access to project files and can execute code.');
|
|
2452
3075
|
systemPrompt += '\n\nDo not end with a deferred promise (for example: "Let me check..."). Return a final answer in this turn, or state exactly what is unavailable.';
|
|
2453
3076
|
systemPrompt += '\n\nWhen [Project Overview] is present, treat Project/Topic/Workspace values there as authoritative for the current turn and override stale prior-chat claims.';
|
|
3077
|
+
systemPrompt += '\n\n[Response Style]\nWrite in short readable paragraphs. Put a blank line between distinct ideas. Use bullets when listing findings, steps, or issues. Do not return one dense wall of text. For progress updates, keep them compact and clearly separate what you checked, what you found, and what you are doing next.';
|
|
2454
3078
|
const convForFallback = data.getConversation(convId);
|
|
2455
3079
|
const projectForFallback = convForFallback?.project_id ? data.getProject(convForFallback.project_id) : undefined;
|
|
2456
3080
|
const workspaceForFallback = projectForFallback?.folder?.trim();
|
|
@@ -2508,8 +3132,11 @@ function startLocalServer(opts) {
|
|
|
2508
3132
|
}
|
|
2509
3133
|
catch { /* best-effort */ }
|
|
2510
3134
|
}
|
|
2511
|
-
// Resolve LLM runtime
|
|
2512
|
-
//
|
|
3135
|
+
// Resolve LLM runtime.
|
|
3136
|
+
// Desktop local mode intentionally supports only:
|
|
3137
|
+
// - Subscription CLI
|
|
3138
|
+
// - API Key
|
|
3139
|
+
// We do not use subscription-token API routing for local CLI bots.
|
|
2513
3140
|
const runtime = await buildChatRuntime(profile);
|
|
2514
3141
|
let activeProviderName = runtime.providerName;
|
|
2515
3142
|
let activeModelName = runtime.model;
|
|
@@ -2518,18 +3145,25 @@ function startLocalServer(opts) {
|
|
|
2518
3145
|
let activeRuntimeMode = runtime.runtimeMode;
|
|
2519
3146
|
let activeRuntimeSource = runtime.runtimeSource;
|
|
2520
3147
|
let activeIsCliProvider = index_1.CLI_PROVIDERS.has(activeProviderName);
|
|
2521
|
-
const cliFallback = runtime.cliFallback;
|
|
2522
|
-
const apiKeyFallback = runtime.apiKeyFallback;
|
|
2523
|
-
let switchedToCliFallback = false;
|
|
2524
|
-
let switchedToApiKeyFallback = false;
|
|
2525
3148
|
const runtimePayload = () => ({
|
|
2526
3149
|
mode: activeRuntimeMode,
|
|
2527
3150
|
modeLabel: runtimeModeLabel(activeRuntimeMode, activeRuntimeSource),
|
|
2528
3151
|
provider: activeProviderName,
|
|
2529
3152
|
model: activeModelName || null,
|
|
2530
3153
|
source: activeRuntimeSource || null,
|
|
2531
|
-
fallbackUsed: switchedToCliFallback || switchedToApiKeyFallback,
|
|
2532
3154
|
});
|
|
3155
|
+
const enableCliSessionEpoch = activeIsCliProvider
|
|
3156
|
+
&& !shouldUseOrchestratorMode
|
|
3157
|
+
&& !workflowTemplateId
|
|
3158
|
+
&& !!convId
|
|
3159
|
+
&& !!profile?.id;
|
|
3160
|
+
const cliSessionEpochPlan = enableCliSessionEpoch
|
|
3161
|
+
? (0, cli_session_epoch_1.selectCliSessionEpoch)(convId, profile.id, activeProviderName)
|
|
3162
|
+
: { existing: undefined, resumeSessionId: null, resetReason: null };
|
|
3163
|
+
let activeCliSessionId = cliSessionEpochPlan.resumeSessionId;
|
|
3164
|
+
const cliEpochStartedAt = cliSessionEpochPlan.resumeSessionId
|
|
3165
|
+
? (cliSessionEpochPlan.existing?.epoch_started_at || localTimestamp())
|
|
3166
|
+
: localTimestamp();
|
|
2533
3167
|
if (!activeApiKey) {
|
|
2534
3168
|
return res.status(400).json({ error: `No API key for provider ${profile.provider}. Configure one in Settings.` });
|
|
2535
3169
|
}
|
|
@@ -2537,12 +3171,17 @@ function startLocalServer(opts) {
|
|
|
2537
3171
|
projectId: convId ? (data.getConversation(convId)?.project_id ?? null) : null,
|
|
2538
3172
|
actorType: 'llm',
|
|
2539
3173
|
actorId: profile?.name || profile?.id || 'LLM',
|
|
3174
|
+
runtimeMode: 'local_desktop',
|
|
3175
|
+
restrictFileAccessToProject: unrestrictedCliProfile ? false : undefined,
|
|
3176
|
+
abortSignal: routeAbortController.signal,
|
|
2540
3177
|
});
|
|
2541
3178
|
const toolManifest = toolDefs
|
|
2542
3179
|
.map(t => `- ${t.name}: ${t.description}`)
|
|
2543
3180
|
.join('\n');
|
|
2544
3181
|
if (toolManifest.trim()) {
|
|
2545
|
-
systemPrompt +=
|
|
3182
|
+
systemPrompt += unrestrictedCliProfile
|
|
3183
|
+
? '\n\n[Available Tools]\nThe following tools are available in the current runtime:\n' + toolManifest
|
|
3184
|
+
: '\n\n[Available Tools]\nOnly the following tools are enabled for this bot in the current runtime:\n' + toolManifest;
|
|
2546
3185
|
}
|
|
2547
3186
|
// Inject pinned messages as context (user-selected cross-bot references)
|
|
2548
3187
|
if (pinnedMessageIds && Array.isArray(pinnedMessageIds) && pinnedMessageIds.length > 0) {
|
|
@@ -2580,6 +3219,8 @@ function startLocalServer(opts) {
|
|
|
2580
3219
|
'X-Conversation-Id': convId,
|
|
2581
3220
|
});
|
|
2582
3221
|
const sendEvent = (event, payload) => {
|
|
3222
|
+
if (responseEnded)
|
|
3223
|
+
return;
|
|
2583
3224
|
res.write(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
2584
3225
|
};
|
|
2585
3226
|
const approxInputTokens = (0, token_counter_1.estimatePromptInputTokens)({
|
|
@@ -2591,6 +3232,7 @@ function startLocalServer(opts) {
|
|
|
2591
3232
|
sendEvent('meta', {
|
|
2592
3233
|
conversationId: convId,
|
|
2593
3234
|
botId: profile.id,
|
|
3235
|
+
assistantMessageId: assistantMessageId || null,
|
|
2594
3236
|
runtime: runtimePayload(),
|
|
2595
3237
|
tokenUsage: {
|
|
2596
3238
|
approxInputTokens,
|
|
@@ -2608,12 +3250,64 @@ function startLocalServer(opts) {
|
|
|
2608
3250
|
detail: `Sending request to ${activeProviderName}...`,
|
|
2609
3251
|
runtime: runtimePayload(),
|
|
2610
3252
|
}, `Sending request to ${activeProviderName}...`);
|
|
3253
|
+
if (cliSessionEpochPlan.resetReason && enableCliSessionEpoch) {
|
|
3254
|
+
const resetDetail = cliSessionEpochPlan.resetReason === 'turn_limit'
|
|
3255
|
+
? 'Resetting CLI session after reaching the turn limit.'
|
|
3256
|
+
: cliSessionEpochPlan.resetReason === 'token_limit'
|
|
3257
|
+
? 'Resetting CLI session after reaching the context budget.'
|
|
3258
|
+
: 'Resetting CLI session because the runtime changed.';
|
|
3259
|
+
sendEvent('status', {
|
|
3260
|
+
phase: 'thinking',
|
|
3261
|
+
detail: resetDetail,
|
|
3262
|
+
runtime: runtimePayload(),
|
|
3263
|
+
});
|
|
3264
|
+
recordActivity('status', {
|
|
3265
|
+
phase: 'thinking',
|
|
3266
|
+
detail: resetDetail,
|
|
3267
|
+
runtime: runtimePayload(),
|
|
3268
|
+
}, resetDetail);
|
|
3269
|
+
}
|
|
3270
|
+
let partialPersistedContent = '';
|
|
3271
|
+
let partialPersistedAt = 0;
|
|
3272
|
+
const throwIfChatJobCancelled = () => {
|
|
3273
|
+
if (!chatJobId)
|
|
3274
|
+
return;
|
|
3275
|
+
const currentJob = data.getChatJob(String(chatJobId));
|
|
3276
|
+
if (currentJob?.status === 'cancelled') {
|
|
3277
|
+
routeAbortController.abort();
|
|
3278
|
+
const abortErr = new Error('aborted');
|
|
3279
|
+
abortErr.name = 'AbortError';
|
|
3280
|
+
throw abortErr;
|
|
3281
|
+
}
|
|
3282
|
+
};
|
|
3283
|
+
const persistAssistantPartial = (force = false) => {
|
|
3284
|
+
if (!assistantMessageId)
|
|
3285
|
+
return;
|
|
3286
|
+
const nowMs = Date.now();
|
|
3287
|
+
const nextContent = streamedContent || '';
|
|
3288
|
+
if (!force && nextContent === partialPersistedContent)
|
|
3289
|
+
return;
|
|
3290
|
+
if (!force && nowMs - partialPersistedAt < 1500)
|
|
3291
|
+
return;
|
|
3292
|
+
partialPersistedContent = nextContent;
|
|
3293
|
+
partialPersistedAt = nowMs;
|
|
3294
|
+
const modelWithRuntime = [
|
|
3295
|
+
activeModelName || profile.model || '',
|
|
3296
|
+
runtimeModeLabel(activeRuntimeMode, activeRuntimeSource),
|
|
3297
|
+
].filter(Boolean).join(' | ');
|
|
3298
|
+
data.updateMessage(assistantMessageId, {
|
|
3299
|
+
content: nextContent,
|
|
3300
|
+
model: modelWithRuntime || null,
|
|
3301
|
+
botId: profile.id,
|
|
3302
|
+
agentName: profile.name,
|
|
3303
|
+
});
|
|
3304
|
+
};
|
|
2611
3305
|
// Agentic loop
|
|
2612
3306
|
let fullContent = '';
|
|
2613
3307
|
let streamedContent = '';
|
|
2614
3308
|
let streamedAnyChunk = false;
|
|
2615
3309
|
let iteration = 0;
|
|
2616
|
-
const MAX_ITERATIONS = 20
|
|
3310
|
+
const MAX_ITERATIONS = 10; // Phase 1d: reduced from 20
|
|
2617
3311
|
let totalInputTokens = 0;
|
|
2618
3312
|
let totalOutputTokens = 0;
|
|
2619
3313
|
let hasExactUsage = false;
|
|
@@ -2621,250 +3315,259 @@ function startLocalServer(opts) {
|
|
|
2621
3315
|
// Thinking/reasoning accumulator across multi-turn tool loops
|
|
2622
3316
|
let accumulatedThinking = '';
|
|
2623
3317
|
const thinkingEnabled = !!profile?.show_thinking;
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
3318
|
+
let useInteractiveCliSession = enableCliSessionEpoch;
|
|
3319
|
+
if (useInteractiveCliSession) {
|
|
3320
|
+
const ptyManager = (0, local_cli_pty_manager_1.getLocalCliPtySessionManager)();
|
|
3321
|
+
let ptyAttempt = 0;
|
|
3322
|
+
while (true) {
|
|
3323
|
+
ptyAttempt++;
|
|
3324
|
+
try {
|
|
3325
|
+
const result = await ptyManager.runTurn({
|
|
3326
|
+
conversationId: convId,
|
|
3327
|
+
botId: profile.id,
|
|
3328
|
+
provider: activeProviderName,
|
|
3329
|
+
cwd: llmSpawnCwd,
|
|
3330
|
+
systemPrompt,
|
|
3331
|
+
messages: llmMessages,
|
|
3332
|
+
forceFreshSession: !cliSessionEpochPlan.resumeSessionId,
|
|
3333
|
+
onDetail: async (detail) => {
|
|
3334
|
+
sendEvent('status', {
|
|
3335
|
+
phase: 'thinking',
|
|
3336
|
+
detail,
|
|
3337
|
+
runtime: runtimePayload(),
|
|
3338
|
+
});
|
|
3339
|
+
recordActivity('status', {
|
|
3340
|
+
phase: 'thinking',
|
|
3341
|
+
detail,
|
|
3342
|
+
runtime: runtimePayload(),
|
|
3343
|
+
}, detail);
|
|
3344
|
+
},
|
|
3345
|
+
});
|
|
3346
|
+
if (result.sessionId) {
|
|
3347
|
+
activeCliSessionId = result.sessionId;
|
|
2644
3348
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
...(thinkingEnabled ? {
|
|
2650
|
-
onThinkingChunk: async (chunk) => {
|
|
2651
|
-
sendEvent('thinking_chunk', {
|
|
2652
|
-
text: chunk,
|
|
2653
|
-
botId: profile?.id || null,
|
|
2654
|
-
agentName: profile?.name || null,
|
|
2655
|
-
});
|
|
2656
|
-
},
|
|
2657
|
-
} : {}),
|
|
2658
|
-
};
|
|
2659
|
-
try {
|
|
2660
|
-
response = await activeLlm.chat(chatOptions);
|
|
2661
|
-
}
|
|
2662
|
-
catch (primaryErr) {
|
|
2663
|
-
if (cliFallback && !switchedToCliFallback) {
|
|
2664
|
-
switchedToCliFallback = true;
|
|
2665
|
-
activeProviderName = cliFallback.providerName;
|
|
2666
|
-
activeModelName = cliFallback.model;
|
|
2667
|
-
activeApiKey = cliFallback.apiKey;
|
|
2668
|
-
activeLlm = cliFallback.llm;
|
|
2669
|
-
activeRuntimeMode = cliFallback.runtimeMode;
|
|
2670
|
-
activeRuntimeSource = cliFallback.runtimeSource;
|
|
2671
|
-
activeIsCliProvider = true;
|
|
2672
|
-
const fallbackMsg = activeIsCliProvider
|
|
2673
|
-
? `CLI auth failed (${primaryErr?.message || primaryErr}); switching to fallback...`
|
|
2674
|
-
: `Primary provider failed (${primaryErr?.message || primaryErr}); switching to fallback...`;
|
|
2675
|
-
console.warn(chalk_1.default.yellow(` [chat] ${fallbackMsg}`));
|
|
2676
|
-
if (activeIsCliProvider) {
|
|
2677
|
-
console.warn(chalk_1.default.yellow(` [chat] If CLI auth keeps failing, run 'claude' or 'codex' in your terminal to re-authenticate.`));
|
|
3349
|
+
if (result.usage) {
|
|
3350
|
+
totalInputTokens += result.usage.inputTokens || 0;
|
|
3351
|
+
totalOutputTokens += result.usage.outputTokens || 0;
|
|
3352
|
+
hasExactUsage = true;
|
|
2678
3353
|
}
|
|
3354
|
+
fullContent = (result.content || '').trim();
|
|
3355
|
+
break;
|
|
3356
|
+
}
|
|
3357
|
+
catch (ptyErr) {
|
|
3358
|
+
if (ptyAttempt >= LOCAL_RUNTIME_RETRY_LIMIT || !shouldRetrySelectedLocalRuntime(ptyErr)) {
|
|
3359
|
+
throw ptyErr;
|
|
3360
|
+
}
|
|
3361
|
+
const retryDetail = `Selected runtime failed (${ptyErr?.message || ptyErr}); retrying the same connection (${ptyAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`;
|
|
3362
|
+
console.warn(chalk_1.default.yellow(` [chat] ${retryDetail}`));
|
|
2679
3363
|
sendEvent('status', {
|
|
2680
3364
|
phase: 'thinking',
|
|
2681
|
-
detail:
|
|
2682
|
-
runtime: runtimePayload(),
|
|
2683
|
-
});
|
|
2684
|
-
recordActivity('status', {
|
|
2685
|
-
phase: 'thinking',
|
|
2686
|
-
detail: fallbackMsg,
|
|
2687
|
-
runtime: runtimePayload(),
|
|
2688
|
-
}, fallbackMsg);
|
|
2689
|
-
sendEvent('status', {
|
|
2690
|
-
phase: 'thinking',
|
|
2691
|
-
detail: `Sending request to ${activeProviderName}...`,
|
|
3365
|
+
detail: retryDetail,
|
|
2692
3366
|
runtime: runtimePayload(),
|
|
2693
3367
|
});
|
|
2694
3368
|
recordActivity('status', {
|
|
2695
3369
|
phase: 'thinking',
|
|
2696
|
-
detail:
|
|
3370
|
+
detail: retryDetail,
|
|
2697
3371
|
runtime: runtimePayload(),
|
|
2698
|
-
},
|
|
3372
|
+
}, retryDetail);
|
|
3373
|
+
await pauseLocalRuntimeRetry(ptyAttempt);
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
if (!useInteractiveCliSession)
|
|
3378
|
+
while (iteration < MAX_ITERATIONS) {
|
|
3379
|
+
iteration++;
|
|
3380
|
+
let iterationFirstChunk = true;
|
|
3381
|
+
throwIfChatJobCancelled();
|
|
3382
|
+
if (iteration > 1) {
|
|
3383
|
+
sendEvent('status', { phase: 'thinking', detail: 'Processing tool results...' });
|
|
3384
|
+
recordActivity('status', { phase: 'thinking', detail: 'Processing tool results...' }, 'Processing tool results...');
|
|
3385
|
+
}
|
|
3386
|
+
let response;
|
|
3387
|
+
const chatOptions = {
|
|
3388
|
+
messages: llmMessages,
|
|
3389
|
+
system: systemPrompt,
|
|
3390
|
+
stream: true,
|
|
3391
|
+
tools: toolDefs,
|
|
3392
|
+
cwd: llmSpawnCwd,
|
|
3393
|
+
abortSignal: routeAbortController.signal,
|
|
3394
|
+
resumeSessionId: enableCliSessionEpoch ? activeCliSessionId : null,
|
|
3395
|
+
persistSession: enableCliSessionEpoch,
|
|
3396
|
+
thinkingEnabled,
|
|
3397
|
+
onChunk: async (chunk) => {
|
|
3398
|
+
throwIfChatJobCancelled();
|
|
3399
|
+
if (iterationFirstChunk) {
|
|
3400
|
+
iterationFirstChunk = false;
|
|
3401
|
+
sendEvent('status', { phase: 'generating' });
|
|
3402
|
+
recordActivity('status', { phase: 'generating' }, 'Generating response...');
|
|
3403
|
+
}
|
|
3404
|
+
streamedAnyChunk = true;
|
|
3405
|
+
streamedContent += chunk;
|
|
3406
|
+
persistAssistantPartial(false);
|
|
3407
|
+
sendEvent('chunk', { text: chunk });
|
|
3408
|
+
},
|
|
3409
|
+
...(thinkingEnabled ? {
|
|
3410
|
+
onThinkingChunk: async (chunk) => {
|
|
3411
|
+
sendEvent('thinking_chunk', {
|
|
3412
|
+
text: chunk,
|
|
3413
|
+
botId: profile?.id || null,
|
|
3414
|
+
agentName: profile?.name || null,
|
|
3415
|
+
});
|
|
3416
|
+
},
|
|
3417
|
+
} : {}),
|
|
3418
|
+
};
|
|
3419
|
+
let chatAttempt = 0;
|
|
3420
|
+
while (true) {
|
|
3421
|
+
chatAttempt++;
|
|
2699
3422
|
try {
|
|
2700
3423
|
response = await activeLlm.chat(chatOptions);
|
|
3424
|
+
break;
|
|
2701
3425
|
}
|
|
2702
|
-
catch (
|
|
2703
|
-
if (
|
|
2704
|
-
throw
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
activeRuntimeSource = apiKeyFallback.runtimeSource;
|
|
2712
|
-
activeIsCliProvider = false;
|
|
2713
|
-
console.warn(chalk_1.default.yellow(` [chat] CLI fallback failed (${cliErr?.message || cliErr}); switching to API key fallback (${activeProviderName})`));
|
|
2714
|
-
sendEvent('status', {
|
|
2715
|
-
phase: 'thinking',
|
|
2716
|
-
detail: 'CLI fallback unavailable; switching to API key fallback...',
|
|
2717
|
-
runtime: runtimePayload(),
|
|
2718
|
-
});
|
|
2719
|
-
recordActivity('status', {
|
|
2720
|
-
phase: 'thinking',
|
|
2721
|
-
detail: 'CLI fallback unavailable; switching to API key fallback...',
|
|
2722
|
-
runtime: runtimePayload(),
|
|
2723
|
-
}, 'CLI fallback unavailable; switching to API key fallback...');
|
|
3426
|
+
catch (primaryErr) {
|
|
3427
|
+
if (routeAbortController.signal.aborted || primaryErr?.name === 'AbortError') {
|
|
3428
|
+
throw primaryErr;
|
|
3429
|
+
}
|
|
3430
|
+
if (chatAttempt >= LOCAL_RUNTIME_RETRY_LIMIT || !shouldRetrySelectedLocalRuntime(primaryErr)) {
|
|
3431
|
+
throw primaryErr;
|
|
3432
|
+
}
|
|
3433
|
+
const retryDetail = `Selected runtime failed (${primaryErr?.message || primaryErr}); retrying the same connection (${chatAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`;
|
|
3434
|
+
console.warn(chalk_1.default.yellow(` [chat] ${retryDetail}`));
|
|
2724
3435
|
sendEvent('status', {
|
|
2725
3436
|
phase: 'thinking',
|
|
2726
|
-
detail:
|
|
3437
|
+
detail: retryDetail,
|
|
2727
3438
|
runtime: runtimePayload(),
|
|
2728
3439
|
});
|
|
2729
3440
|
recordActivity('status', {
|
|
2730
3441
|
phase: 'thinking',
|
|
2731
|
-
detail:
|
|
3442
|
+
detail: retryDetail,
|
|
2732
3443
|
runtime: runtimePayload(),
|
|
2733
|
-
},
|
|
2734
|
-
|
|
3444
|
+
}, retryDetail);
|
|
3445
|
+
await pauseLocalRuntimeRetry(chatAttempt);
|
|
2735
3446
|
}
|
|
2736
3447
|
}
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
activeRuntimeMode = apiKeyFallback.runtimeMode;
|
|
2744
|
-
activeRuntimeSource = apiKeyFallback.runtimeSource;
|
|
2745
|
-
activeIsCliProvider = false;
|
|
2746
|
-
console.warn(chalk_1.default.yellow(` [chat] Runtime failed (${primaryErr?.message || primaryErr}); switching to API key fallback (${activeProviderName})`));
|
|
2747
|
-
sendEvent('status', {
|
|
2748
|
-
phase: 'thinking',
|
|
2749
|
-
detail: 'Switching to API key fallback...',
|
|
2750
|
-
runtime: runtimePayload(),
|
|
2751
|
-
});
|
|
2752
|
-
recordActivity('status', {
|
|
2753
|
-
phase: 'thinking',
|
|
2754
|
-
detail: 'Switching to API key fallback...',
|
|
2755
|
-
runtime: runtimePayload(),
|
|
2756
|
-
}, 'Switching to API key fallback...');
|
|
3448
|
+
if (enableCliSessionEpoch && response?.session?.id) {
|
|
3449
|
+
activeCliSessionId = response.session.id;
|
|
3450
|
+
}
|
|
3451
|
+
throwIfChatJobCancelled();
|
|
3452
|
+
const authFailure = detectInteractiveAuthFailure(response?.content || '', activeProviderName, profile.provider);
|
|
3453
|
+
if (authFailure && (!response?.toolCalls || response.toolCalls.length === 0)) {
|
|
2757
3454
|
sendEvent('status', {
|
|
2758
|
-
phase: '
|
|
2759
|
-
detail:
|
|
3455
|
+
phase: 'auth_required',
|
|
3456
|
+
detail: authFailure.detail,
|
|
2760
3457
|
runtime: runtimePayload(),
|
|
3458
|
+
auth: authFailure,
|
|
2761
3459
|
});
|
|
2762
3460
|
recordActivity('status', {
|
|
2763
|
-
phase: '
|
|
2764
|
-
detail:
|
|
3461
|
+
phase: 'auth_required',
|
|
3462
|
+
detail: authFailure.detail,
|
|
2765
3463
|
runtime: runtimePayload(),
|
|
2766
|
-
|
|
2767
|
-
|
|
3464
|
+
auth: authFailure,
|
|
3465
|
+
}, authFailure.detail);
|
|
3466
|
+
const authErr = new Error(authFailure.message);
|
|
3467
|
+
authErr.authRequired = true;
|
|
3468
|
+
authErr.providerId = authFailure.providerId;
|
|
3469
|
+
authErr.cli = authFailure.cli;
|
|
3470
|
+
throw authErr;
|
|
2768
3471
|
}
|
|
2769
|
-
|
|
2770
|
-
|
|
3472
|
+
if (response.usage) {
|
|
3473
|
+
totalInputTokens += response.usage.inputTokens || 0;
|
|
3474
|
+
totalOutputTokens += response.usage.outputTokens || 0;
|
|
3475
|
+
hasExactUsage = true;
|
|
2771
3476
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
llmMessages.push({ role: 'tool', content:
|
|
2825
|
-
continue;
|
|
2826
|
-
}
|
|
2827
|
-
let result;
|
|
2828
|
-
try {
|
|
2829
|
-
const raw = await (0, index_2.executeToolWithMCP)({ id: tc.id, name: tc.name, arguments: tc.arguments }, toolCtx, mcpManager);
|
|
2830
|
-
const verified = await (0, index_2.verifyToolResult)(raw, tc.arguments, toolCtx);
|
|
2831
|
-
result = {
|
|
2832
|
-
success: verified.success,
|
|
2833
|
-
output: verified.output,
|
|
2834
|
-
error: verified.error,
|
|
2835
|
-
};
|
|
2836
|
-
}
|
|
2837
|
-
catch (toolErr) {
|
|
2838
|
-
result = { success: false, output: '', error: toolErr.message || 'Tool execution failed' };
|
|
3477
|
+
// Accumulate thinking/reasoning across multi-turn tool loops
|
|
3478
|
+
if (response.thinking) {
|
|
3479
|
+
accumulatedThinking += (accumulatedThinking ? '\n---\n' : '') + response.thinking;
|
|
3480
|
+
}
|
|
3481
|
+
if (response.toolCalls && response.toolCalls.length > 0) {
|
|
3482
|
+
llmMessages.push({
|
|
3483
|
+
role: 'assistant',
|
|
3484
|
+
content: response.content || '',
|
|
3485
|
+
toolCalls: response.toolCalls,
|
|
3486
|
+
});
|
|
3487
|
+
for (const tc of response.toolCalls) {
|
|
3488
|
+
throwIfChatJobCancelled();
|
|
3489
|
+
sendEvent('status', { phase: 'calling_tool', detail: `Running ${tc.name}...`, toolName: tc.name });
|
|
3490
|
+
sendEvent('tool_call', { id: tc.id, name: tc.name, arguments: tc.arguments });
|
|
3491
|
+
recordActivity('tool_call', { id: tc.id, name: tc.name, arguments: tc.arguments }, `Tool call: ${tc.name}`);
|
|
3492
|
+
if (!allowedToolNames.has(tc.name)) {
|
|
3493
|
+
const errMsg = `TOOL_DISABLED: ${tc.name} is not enabled for this bot.`;
|
|
3494
|
+
sendEvent('tool_result', { callId: tc.id, output: errMsg, isError: true });
|
|
3495
|
+
recordActivity('tool_result', { callId: tc.id, output: errMsg, isError: true }, `Tool failed: ${tc.name}`);
|
|
3496
|
+
llmMessages.push({ role: 'tool', content: errMsg, toolCallId: tc.id, toolName: tc.name });
|
|
3497
|
+
continue;
|
|
3498
|
+
}
|
|
3499
|
+
const approval = unrestrictedCliProfile
|
|
3500
|
+
? { approved: true }
|
|
3501
|
+
: (0, approval_1.checkPermission)(tc.name, (profile.permission_mode || 'autopilot'));
|
|
3502
|
+
if (!approval.approved) {
|
|
3503
|
+
const errMsg = `PERMISSION_DENIED: ${approval.reason}`;
|
|
3504
|
+
sendEvent('tool_result', { callId: tc.id, output: errMsg, isError: true });
|
|
3505
|
+
recordActivity('tool_result', { callId: tc.id, output: errMsg, isError: true }, `Tool denied: ${tc.name}`);
|
|
3506
|
+
llmMessages.push({ role: 'tool', content: errMsg, toolCallId: tc.id, toolName: tc.name });
|
|
3507
|
+
continue;
|
|
3508
|
+
}
|
|
3509
|
+
let result;
|
|
3510
|
+
try {
|
|
3511
|
+
const raw = await (0, index_2.executeToolWithMCP)({ id: tc.id, name: tc.name, arguments: tc.arguments }, toolCtx, mcpManager);
|
|
3512
|
+
const verified = await (0, index_2.verifyToolResult)(raw, tc.arguments, toolCtx);
|
|
3513
|
+
result = {
|
|
3514
|
+
success: verified.success,
|
|
3515
|
+
output: verified.output,
|
|
3516
|
+
error: verified.error,
|
|
3517
|
+
};
|
|
3518
|
+
}
|
|
3519
|
+
catch (toolErr) {
|
|
3520
|
+
result = { success: false, output: '', error: toolErr.message || 'Tool execution failed' };
|
|
3521
|
+
}
|
|
3522
|
+
const output = result.success ? result.output : `ERROR: ${result.error || 'Unknown error'}`;
|
|
3523
|
+
sendEvent('tool_result', { callId: tc.id, output, isError: !result.success });
|
|
3524
|
+
recordActivity('tool_result', {
|
|
3525
|
+
callId: tc.id,
|
|
3526
|
+
output,
|
|
3527
|
+
isError: !result.success,
|
|
3528
|
+
}, `${result.success ? 'Tool completed' : 'Tool failed'}: ${tc.name}`);
|
|
3529
|
+
llmMessages.push({ role: 'tool', content: output, toolCallId: tc.id, toolName: tc.name });
|
|
2839
3530
|
}
|
|
2840
|
-
|
|
2841
|
-
sendEvent('tool_result', { callId: tc.id, output, isError: !result.success });
|
|
2842
|
-
recordActivity('tool_result', {
|
|
2843
|
-
callId: tc.id,
|
|
2844
|
-
output,
|
|
2845
|
-
isError: !result.success,
|
|
2846
|
-
}, `${result.success ? 'Tool completed' : 'Tool failed'}: ${tc.name}`);
|
|
2847
|
-
llmMessages.push({ role: 'tool', content: output, toolCallId: tc.id, toolName: tc.name });
|
|
3531
|
+
continue;
|
|
2848
3532
|
}
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
3533
|
+
// Final response (guard against defer-only filler)
|
|
3534
|
+
const candidate = (response.content || '').trim();
|
|
3535
|
+
if (!forcedFinalizationPass && (0, response_guard_1.isLikelyDeferredReply)(candidate)) {
|
|
3536
|
+
forcedFinalizationPass = true;
|
|
3537
|
+
llmMessages.push({ role: 'assistant', content: candidate });
|
|
3538
|
+
llmMessages.push({
|
|
3539
|
+
role: 'user',
|
|
3540
|
+
content: 'Provide the final answer now. Do not say you will check later. Either provide concrete results or explicitly say what is unavailable.',
|
|
3541
|
+
});
|
|
3542
|
+
sendEvent('status', { phase: 'thinking', detail: 'Finalizing response...' });
|
|
3543
|
+
recordActivity('status', { phase: 'thinking', detail: 'Finalizing response...' }, 'Finalizing response...');
|
|
3544
|
+
continue;
|
|
3545
|
+
}
|
|
3546
|
+
fullContent = candidate;
|
|
3547
|
+
break;
|
|
2863
3548
|
}
|
|
2864
|
-
fullContent = candidate;
|
|
2865
|
-
break;
|
|
2866
|
-
}
|
|
2867
3549
|
const persistedContent = fullContent || streamedContent.trim();
|
|
3550
|
+
if (!persistedContent) {
|
|
3551
|
+
throw new Error('Assistant returned no final response');
|
|
3552
|
+
}
|
|
3553
|
+
persistAssistantPartial(true);
|
|
3554
|
+
if (enableCliSessionEpoch && activeCliSessionId) {
|
|
3555
|
+
const nextEpochTurnCount = cliSessionEpochPlan.resumeSessionId
|
|
3556
|
+
? ((cliSessionEpochPlan.existing?.epoch_turn_count || 0) + 1)
|
|
3557
|
+
: 1;
|
|
3558
|
+
data.upsertCliSessionEpoch({
|
|
3559
|
+
conversationId: convId,
|
|
3560
|
+
botId: profile.id,
|
|
3561
|
+
provider: activeProviderName,
|
|
3562
|
+
sessionId: activeCliSessionId,
|
|
3563
|
+
epochTurnCount: nextEpochTurnCount,
|
|
3564
|
+
lastInputTokens: hasExactUsage ? totalInputTokens : approxInputTokens,
|
|
3565
|
+
lastOutputTokens: hasExactUsage ? totalOutputTokens : 0,
|
|
3566
|
+
resetReason: cliSessionEpochPlan.resetReason,
|
|
3567
|
+
epochStartedAt: cliEpochStartedAt,
|
|
3568
|
+
lastUsedAt: localTimestamp(),
|
|
3569
|
+
});
|
|
3570
|
+
}
|
|
2868
3571
|
// Emit thinking_done event if we accumulated any thinking
|
|
2869
3572
|
if (accumulatedThinking) {
|
|
2870
3573
|
sendEvent('thinking_done', {
|
|
@@ -2885,7 +3588,14 @@ function startLocalServer(opts) {
|
|
|
2885
3588
|
activeModelName || profile.model || '',
|
|
2886
3589
|
runtimeModeLabel(activeRuntimeMode, activeRuntimeSource),
|
|
2887
3590
|
].filter(Boolean).join(' | ');
|
|
2888
|
-
const savedMessage =
|
|
3591
|
+
const savedMessage = assistantMessageId
|
|
3592
|
+
? (data.updateMessage(assistantMessageId, {
|
|
3593
|
+
content: persistedContent,
|
|
3594
|
+
model: modelWithRuntime || null,
|
|
3595
|
+
botId: profile.id,
|
|
3596
|
+
agentName: profile.name,
|
|
3597
|
+
}) || data.addMessage(convId, 'assistant', persistedContent, modelWithRuntime || undefined, undefined, profile.id, profile.name))
|
|
3598
|
+
: data.addMessage(convId, 'assistant', persistedContent, modelWithRuntime || undefined, undefined, profile.id, profile.name);
|
|
2889
3599
|
data.attachMessageActivitiesToMessage(activityStreamId, savedMessage.id);
|
|
2890
3600
|
data.createMessageActivity({
|
|
2891
3601
|
conversationId: convId,
|
|
@@ -2913,6 +3623,7 @@ function startLocalServer(opts) {
|
|
|
2913
3623
|
approxInputTokens,
|
|
2914
3624
|
},
|
|
2915
3625
|
});
|
|
3626
|
+
responseEnded = true;
|
|
2916
3627
|
res.end();
|
|
2917
3628
|
// Background processing: auto-title + funnel extraction
|
|
2918
3629
|
const conv = data.getConversation(convId);
|
|
@@ -2926,13 +3637,13 @@ function startLocalServer(opts) {
|
|
|
2926
3637
|
autoTitleConversation(convId, message, persistedContent, activeProviderName, activeModelName, activeApiKey);
|
|
2927
3638
|
}
|
|
2928
3639
|
}
|
|
2929
|
-
(0, local_funnel_1.scheduleFunnelProcessing)(convId);
|
|
2930
3640
|
// Summarization: trigger after every 5-turn boundary (fire-and-forget)
|
|
2931
3641
|
// Includes topic creation/linking + project verification
|
|
2932
3642
|
if ((0, context_window_1.shouldSummarize)(convId)) {
|
|
2933
3643
|
const catalog = (0, backfill_1.buildProjectTopicCatalog)();
|
|
2934
3644
|
(0, summarization_pipeline_1.runSummarization)(convId, catalog).catch(err => console.error(chalk_1.default.yellow(` [summarization] ${err.message}`)));
|
|
2935
3645
|
}
|
|
3646
|
+
(0, local_funnel_1.scheduleFunnelProcessing)(convId);
|
|
2936
3647
|
}
|
|
2937
3648
|
catch (err) {
|
|
2938
3649
|
console.error(chalk_1.default.red(`Chat error: ${err.message}`));
|
|
@@ -2940,6 +3651,7 @@ function startLocalServer(opts) {
|
|
|
2940
3651
|
if (activityErrorContext.conversationId) {
|
|
2941
3652
|
data.createMessageActivity({
|
|
2942
3653
|
conversationId: activityErrorContext.conversationId,
|
|
3654
|
+
messageId: activityErrorContext.messageId ?? null,
|
|
2943
3655
|
streamId: activityErrorContext.streamId ?? null,
|
|
2944
3656
|
botId: activityErrorContext.botId ?? null,
|
|
2945
3657
|
agentName: activityErrorContext.agentName ?? null,
|
|
@@ -2969,64 +3681,19 @@ function startLocalServer(opts) {
|
|
|
2969
3681
|
providerId: err?.providerId || null,
|
|
2970
3682
|
cli: err?.cli || null,
|
|
2971
3683
|
})}\n\n`);
|
|
3684
|
+
responseEnded = true;
|
|
2972
3685
|
res.end();
|
|
2973
3686
|
}
|
|
2974
3687
|
catch { /* connection already closed */ }
|
|
2975
3688
|
}
|
|
2976
3689
|
}
|
|
3690
|
+
finally {
|
|
3691
|
+
req.off?.('close', abortOnClientClose);
|
|
3692
|
+
}
|
|
2977
3693
|
});
|
|
2978
3694
|
// ─── 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
|
-
}
|
|
3695
|
+
app.get('/api/memory/facts', (_req, res) => {
|
|
3696
|
+
res.json([]);
|
|
3030
3697
|
});
|
|
3031
3698
|
app.get('/api/memory/search', (req, res) => {
|
|
3032
3699
|
try {
|
|
@@ -3054,120 +3721,23 @@ function startLocalServer(opts) {
|
|
|
3054
3721
|
res.status(500).json({ error: err.message });
|
|
3055
3722
|
}
|
|
3056
3723
|
});
|
|
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
|
-
}
|
|
3724
|
+
app.delete('/api/memory/facts/:id', (_req, res) => {
|
|
3725
|
+
res.json({ ok: true });
|
|
3067
3726
|
});
|
|
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
|
-
}
|
|
3727
|
+
app.get('/api/memory/entities', (_req, res) => {
|
|
3728
|
+
res.json({ nodes: [], edges: [] });
|
|
3079
3729
|
});
|
|
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
|
-
}
|
|
3730
|
+
app.get('/api/memory/decisions', (_req, res) => {
|
|
3731
|
+
res.json([]);
|
|
3112
3732
|
});
|
|
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
|
-
}
|
|
3733
|
+
app.delete('/api/memory/decisions/:id', (_req, res) => {
|
|
3734
|
+
res.json({ ok: true });
|
|
3123
3735
|
});
|
|
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
|
-
}
|
|
3736
|
+
app.get('/api/memory/action-items', (_req, res) => {
|
|
3737
|
+
res.json([]);
|
|
3160
3738
|
});
|
|
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
|
-
}
|
|
3739
|
+
app.delete('/api/memory/action-items/:id', (_req, res) => {
|
|
3740
|
+
res.json({ ok: true });
|
|
3171
3741
|
});
|
|
3172
3742
|
// ─── Settings ───────────────────────────────────────────────
|
|
3173
3743
|
app.get('/api/settings', (_req, res) => {
|
|
@@ -3272,7 +3842,7 @@ function startLocalServer(opts) {
|
|
|
3272
3842
|
const { prompt } = req.body;
|
|
3273
3843
|
if (!prompt)
|
|
3274
3844
|
return res.status(400).json({ error: 'prompt is required' });
|
|
3275
|
-
const clerk = (0, clerk_model_1.getClerk)();
|
|
3845
|
+
const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: 'local_desktop' });
|
|
3276
3846
|
if (!clerk)
|
|
3277
3847
|
return res.json({ routing: 'default', reason: 'No clerk configured' });
|
|
3278
3848
|
const agents = data.listAgentProfiles();
|
|
@@ -3289,7 +3859,7 @@ function startLocalServer(opts) {
|
|
|
3289
3859
|
const { prompt, conversationId, agentId, pinnedMessageIds } = req.body;
|
|
3290
3860
|
if (!prompt)
|
|
3291
3861
|
return res.status(400).json({ error: 'prompt is required' });
|
|
3292
|
-
const engine = (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir);
|
|
3862
|
+
const engine = (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir, 'local_desktop');
|
|
3293
3863
|
const profile = agentId
|
|
3294
3864
|
? data.getAgentProfile(agentId)
|
|
3295
3865
|
: data.getDefaultAgentProfile();
|
|
@@ -3307,6 +3877,7 @@ function startLocalServer(opts) {
|
|
|
3307
3877
|
const result = await engine.execute(prompt, conversationId || null, profile.id, {
|
|
3308
3878
|
onProgress: (p) => sendEvent('progress', p),
|
|
3309
3879
|
pinnedMessageIds: pinnedMessageIds || undefined,
|
|
3880
|
+
runtimeMode: 'local_desktop',
|
|
3310
3881
|
});
|
|
3311
3882
|
sendEvent('done', result);
|
|
3312
3883
|
res.end();
|
|
@@ -3334,7 +3905,7 @@ function startLocalServer(opts) {
|
|
|
3334
3905
|
return res.status(400).json({ error: 'template id is required' });
|
|
3335
3906
|
if (!Number.isFinite(taskId) || taskId <= 0)
|
|
3336
3907
|
return res.status(400).json({ error: 'taskId is required' });
|
|
3337
|
-
const engine = (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir);
|
|
3908
|
+
const engine = (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir, 'local_desktop');
|
|
3338
3909
|
res.writeHead(200, {
|
|
3339
3910
|
'Content-Type': 'text/event-stream',
|
|
3340
3911
|
'Cache-Control': 'no-cache',
|
|
@@ -3366,7 +3937,7 @@ function startLocalServer(opts) {
|
|
|
3366
3937
|
});
|
|
3367
3938
|
app.get('/api/workflow/active', (_req, res) => {
|
|
3368
3939
|
try {
|
|
3369
|
-
const engine = (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir);
|
|
3940
|
+
const engine = (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir, 'local_desktop');
|
|
3370
3941
|
const workflows = engine.getActiveWorkflows();
|
|
3371
3942
|
res.json(workflows);
|
|
3372
3943
|
}
|
|
@@ -3376,7 +3947,7 @@ function startLocalServer(opts) {
|
|
|
3376
3947
|
});
|
|
3377
3948
|
app.post('/api/workflow/:id/cancel', (req, res) => {
|
|
3378
3949
|
try {
|
|
3379
|
-
const engine = (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir);
|
|
3950
|
+
const engine = (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir, 'local_desktop');
|
|
3380
3951
|
const cancelled = engine.cancel(req.params.id);
|
|
3381
3952
|
res.json({ ok: cancelled });
|
|
3382
3953
|
}
|
|
@@ -3610,24 +4181,9 @@ function startLocalServer(opts) {
|
|
|
3610
4181
|
const ids = convIds.map(c => c.id);
|
|
3611
4182
|
const placeholders = ids.map(() => '?').join(',');
|
|
3612
4183
|
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
|
-
}
|
|
4184
|
+
result.decisions = [];
|
|
4185
|
+
result.actionItems = [];
|
|
4186
|
+
result.facts = [];
|
|
3631
4187
|
if (!type || type === 'summaries') {
|
|
3632
4188
|
result.summaries = db.prepare(`SELECT s.*, c.title as conversation_title FROM conversation_summary s
|
|
3633
4189
|
LEFT JOIN conversation c ON s.conversation_id = c.id
|
|
@@ -4032,6 +4588,7 @@ function startLocalServer(opts) {
|
|
|
4032
4588
|
mcpManager.autoLaunch().catch((err) => {
|
|
4033
4589
|
console.error(chalk_1.default.yellow(`[MCP] Auto-launch error: ${err.message}`));
|
|
4034
4590
|
});
|
|
4591
|
+
void runQueuedChatJobs();
|
|
4035
4592
|
/**
|
|
4036
4593
|
* GET /api/mcp/catalog — full marketplace catalog with install status
|
|
4037
4594
|
*/
|
|
@@ -4121,7 +4678,7 @@ function startLocalServer(opts) {
|
|
|
4121
4678
|
}
|
|
4122
4679
|
});
|
|
4123
4680
|
// Initialize workflow engine
|
|
4124
|
-
(0, workflow_engine_1.getWorkflowEngine)(opts.projectDir);
|
|
4681
|
+
(0, workflow_engine_1.getWorkflowEngine)(opts.projectDir, 'local_desktop');
|
|
4125
4682
|
// Start server
|
|
4126
4683
|
return new Promise((resolve, reject) => {
|
|
4127
4684
|
_server = app.listen(port, '127.0.0.1', () => {
|
|
@@ -4152,43 +4709,38 @@ function stopLocalServer() {
|
|
|
4152
4709
|
}
|
|
4153
4710
|
});
|
|
4154
4711
|
}
|
|
4155
|
-
function buildCliFallback(profile) {
|
|
4156
|
-
const providerName = profile.provider;
|
|
4157
|
-
const model = (profile.model || '').trim() || 'default';
|
|
4158
|
-
return {
|
|
4159
|
-
providerName,
|
|
4160
|
-
apiKey: 'cli-auth',
|
|
4161
|
-
model,
|
|
4162
|
-
llm: (0, index_1.createProvider)(providerName, { apiKey: 'cli-auth', model }),
|
|
4163
|
-
runtimeMode: 'subscription-cli',
|
|
4164
|
-
runtimeSource: 'cli-direct',
|
|
4165
|
-
};
|
|
4166
|
-
}
|
|
4167
4712
|
async function buildChatRuntime(profile) {
|
|
4168
4713
|
const providerName = profile.provider;
|
|
4169
|
-
const
|
|
4170
|
-
|
|
4171
|
-
if (providerName === 'claude-cli' || providerName === 'codex-cli') {
|
|
4714
|
+
const model = (profile.model || '').trim() || 'default';
|
|
4715
|
+
if (providerName === 'claude-cli') {
|
|
4172
4716
|
return {
|
|
4173
4717
|
providerName,
|
|
4174
4718
|
apiKey: 'cli-auth',
|
|
4175
|
-
model
|
|
4176
|
-
llm: (0, index_1.createProvider)(providerName, { apiKey: 'cli-auth', model:
|
|
4719
|
+
model,
|
|
4720
|
+
llm: (0, index_1.createProvider)(providerName, { apiKey: 'cli-auth', model, runtimeMode: 'local_desktop' }),
|
|
4721
|
+
runtimeMode: 'subscription-cli',
|
|
4722
|
+
runtimeSource: 'cli-direct',
|
|
4723
|
+
};
|
|
4724
|
+
}
|
|
4725
|
+
if (providerName === 'codex-cli') {
|
|
4726
|
+
return {
|
|
4727
|
+
providerName,
|
|
4728
|
+
apiKey: 'cli-auth',
|
|
4729
|
+
model,
|
|
4730
|
+
llm: (0, index_1.createProvider)(providerName, { apiKey: 'cli-auth', model, runtimeMode: 'local_desktop' }),
|
|
4177
4731
|
runtimeMode: 'subscription-cli',
|
|
4178
4732
|
runtimeSource: 'cli-direct',
|
|
4179
|
-
apiKeyFallback,
|
|
4180
4733
|
};
|
|
4181
4734
|
}
|
|
4182
4735
|
const apiKey = resolveApiKey(profile);
|
|
4183
4736
|
if (!apiKey) {
|
|
4184
4737
|
throw new Error(`No API key for provider ${profile.provider}. Configure one in Settings.`);
|
|
4185
4738
|
}
|
|
4186
|
-
const model = (profile.model || '').trim() || 'default';
|
|
4187
4739
|
return {
|
|
4188
4740
|
providerName,
|
|
4189
4741
|
apiKey,
|
|
4190
4742
|
model,
|
|
4191
|
-
llm: (0, index_1.createProvider)(providerName, { apiKey, model }),
|
|
4743
|
+
llm: (0, index_1.createProvider)(providerName, { apiKey, model, runtimeMode: 'local_desktop' }),
|
|
4192
4744
|
runtimeMode: 'api-key',
|
|
4193
4745
|
runtimeSource: 'api-key',
|
|
4194
4746
|
};
|
|
@@ -4196,16 +4748,17 @@ async function buildChatRuntime(profile) {
|
|
|
4196
4748
|
function runtimeModeLabel(mode, runtimeSource) {
|
|
4197
4749
|
if (mode === 'subscription-cli')
|
|
4198
4750
|
return 'Subscription CLI';
|
|
4751
|
+
if (mode === 'subscription-api')
|
|
4752
|
+
return (0, subscription_runtime_1.claudeSubscriptionRuntimeLabel)(runtimeSource);
|
|
4199
4753
|
return 'API Key';
|
|
4200
4754
|
}
|
|
4201
|
-
function runtimePayloadForDisplay(providerName, model, runtimeMode, runtimeSource
|
|
4755
|
+
function runtimePayloadForDisplay(providerName, model, runtimeMode, runtimeSource) {
|
|
4202
4756
|
return {
|
|
4203
4757
|
mode: runtimeMode,
|
|
4204
4758
|
modeLabel: runtimeModeLabel(runtimeMode, runtimeSource),
|
|
4205
4759
|
provider: providerName,
|
|
4206
4760
|
model: model || null,
|
|
4207
4761
|
source: runtimeSource || null,
|
|
4208
|
-
fallbackUsed,
|
|
4209
4762
|
};
|
|
4210
4763
|
}
|
|
4211
4764
|
function configuredRuntimeLabelForProfile(profile) {
|
|
@@ -4214,17 +4767,22 @@ function configuredRuntimeLabelForProfile(profile) {
|
|
|
4214
4767
|
const directConnection = profile.provider_connection_id
|
|
4215
4768
|
? data.listProviderConnections().find((row) => row.id === profile.provider_connection_id)
|
|
4216
4769
|
: undefined;
|
|
4770
|
+
if (profile.provider === 'claude-cli' || profile.provider === 'codex-cli') {
|
|
4771
|
+
return 'Subscription CLI';
|
|
4772
|
+
}
|
|
4217
4773
|
const connection = directConnection;
|
|
4218
4774
|
if (connection) {
|
|
4219
4775
|
if (connection.access_mode === 'cli')
|
|
4220
4776
|
return 'Subscription CLI';
|
|
4777
|
+
if (connection.access_mode === 'oauth')
|
|
4778
|
+
return 'Subscription API (Token)';
|
|
4221
4779
|
return 'API Key';
|
|
4222
4780
|
}
|
|
4223
|
-
if (profile.provider === 'claude-cli' || profile.provider === 'codex-cli') {
|
|
4224
|
-
return 'Subscription CLI';
|
|
4225
|
-
}
|
|
4226
4781
|
return 'API Key';
|
|
4227
4782
|
}
|
|
4783
|
+
async function buildChatRuntimeForTest(profile) {
|
|
4784
|
+
return buildChatRuntime(profile);
|
|
4785
|
+
}
|
|
4228
4786
|
function buildConfiguredMessageModel(profile) {
|
|
4229
4787
|
if (!profile)
|
|
4230
4788
|
return '';
|
|
@@ -4372,32 +4930,30 @@ function detectInteractiveAuthFailure(text, activeProviderName, configuredProvid
|
|
|
4372
4930
|
cli,
|
|
4373
4931
|
};
|
|
4374
4932
|
}
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
return {
|
|
4389
|
-
providerName,
|
|
4390
|
-
apiKey,
|
|
4391
|
-
model,
|
|
4392
|
-
llm: (0, index_1.createProvider)(providerName, { apiKey, model }),
|
|
4393
|
-
runtimeMode: 'api-key',
|
|
4394
|
-
runtimeSource: 'api-key-fallback',
|
|
4395
|
-
};
|
|
4933
|
+
const LOCAL_RUNTIME_RETRY_LIMIT = 2;
|
|
4934
|
+
function shouldRetrySelectedLocalRuntime(err) {
|
|
4935
|
+
const text = String(err?.message || err || '').toLowerCase();
|
|
4936
|
+
if (!text)
|
|
4937
|
+
return false;
|
|
4938
|
+
if (/\b(no api key|configure one in settings|not available on this machine|not installed|please run \/login|not logged in|invalid api key)\b/i.test(text)) {
|
|
4939
|
+
return false;
|
|
4940
|
+
}
|
|
4941
|
+
return /\b(429|rate limit|timeout|timed out|temporar|temporarily|econnreset|etimedout|enotfound|econnrefused|socket hang up|network|try again|overloaded|busy)\b/i.test(text);
|
|
4942
|
+
}
|
|
4943
|
+
async function pauseLocalRuntimeRetry(attempt) {
|
|
4944
|
+
const delayMs = attempt <= 1 ? 750 : 1500;
|
|
4945
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
4396
4946
|
}
|
|
4397
4947
|
function resolveApiKey(profile) {
|
|
4398
4948
|
// Check profile-stored key first
|
|
4399
4949
|
if (profile.api_key_enc)
|
|
4400
4950
|
return profile.api_key_enc;
|
|
4951
|
+
// Prefer the bot's explicitly assigned provider connection when present.
|
|
4952
|
+
if (profile.provider_connection_id) {
|
|
4953
|
+
const directConnection = data.getProviderConnection(profile.provider_connection_id);
|
|
4954
|
+
if (directConnection?.api_key_enc)
|
|
4955
|
+
return directConnection.api_key_enc;
|
|
4956
|
+
}
|
|
4401
4957
|
// Fall back to DB-backed provider connections
|
|
4402
4958
|
const providerConnection = data.findProviderConnection(profile.provider);
|
|
4403
4959
|
if (providerConnection?.api_key_enc)
|
|
@@ -4461,7 +5017,7 @@ function expandAllowedToolNames(allToolDefs, configuredBuiltinTools, configuredM
|
|
|
4461
5017
|
}
|
|
4462
5018
|
async function autoTitleConversation(convId, userMsg, assistantMsg, providerName, modelName, apiKey) {
|
|
4463
5019
|
try {
|
|
4464
|
-
const llm = (0, index_1.createProvider)(providerName, { apiKey, model: modelName || 'default' });
|
|
5020
|
+
const llm = (0, index_1.createProvider)(providerName, { apiKey, model: modelName || 'default', runtimeMode: 'local_desktop' });
|
|
4465
5021
|
const resp = await llm.chat({
|
|
4466
5022
|
messages: [{ role: 'user', content: `Generate a short title (max 6 words, no quotes) for this conversation:\n\nUser: ${userMsg.slice(0, 200)}\nAssistant: ${assistantMsg.slice(0, 200)}` }],
|
|
4467
5023
|
system: 'You generate short conversation titles. Return ONLY the title, nothing else.',
|