funolio-agent 1.0.53 → 1.1.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approval.d.ts +1 -6
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js +2 -7
- package/dist/approval.js.map +1 -1
- package/dist/auth/credential-reader.d.ts.map +1 -1
- package/dist/auth/credential-reader.js +4 -3
- package/dist/auth/credential-reader.js.map +1 -1
- package/dist/auth/token-refresh.d.ts +8 -0
- package/dist/auth/token-refresh.d.ts.map +1 -1
- package/dist/auth/token-refresh.js +82 -52
- package/dist/auth/token-refresh.js.map +1 -1
- package/dist/auto-organizer.d.ts.map +1 -1
- package/dist/auto-organizer.js +6 -7
- package/dist/auto-organizer.js.map +1 -1
- package/dist/bench-prefix.d.ts +16 -0
- package/dist/bench-prefix.d.ts.map +1 -0
- package/dist/bench-prefix.js +25 -0
- package/dist/bench-prefix.js.map +1 -0
- package/dist/bot-manager.d.ts +5 -1
- package/dist/bot-manager.d.ts.map +1 -1
- package/dist/bot-manager.js +46 -27
- package/dist/bot-manager.js.map +1 -1
- package/dist/chat-sync.d.ts +42 -0
- package/dist/chat-sync.d.ts.map +1 -0
- package/dist/chat-sync.js +95 -0
- package/dist/chat-sync.js.map +1 -0
- package/dist/clerk-model.d.ts +7 -0
- package/dist/clerk-model.d.ts.map +1 -1
- package/dist/clerk-model.js +42 -8
- package/dist/clerk-model.js.map +1 -1
- package/dist/cli-bootstrap-history.d.ts +10 -0
- package/dist/cli-bootstrap-history.d.ts.map +1 -0
- package/dist/cli-bootstrap-history.js +112 -0
- package/dist/cli-bootstrap-history.js.map +1 -0
- package/dist/cli-models.d.ts +8 -0
- package/dist/cli-models.d.ts.map +1 -0
- package/dist/cli-models.js +91 -0
- package/dist/cli-models.js.map +1 -0
- package/dist/cli-session-epoch.d.ts +13 -3
- package/dist/cli-session-epoch.d.ts.map +1 -1
- package/dist/cli-session-epoch.js +53 -4
- package/dist/cli-session-epoch.js.map +1 -1
- package/dist/cli-session-registry.d.ts +35 -0
- package/dist/cli-session-registry.d.ts.map +1 -0
- package/dist/cli-session-registry.js +177 -0
- package/dist/cli-session-registry.js.map +1 -0
- package/dist/cli.js +62 -0
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-server-manager.d.ts +189 -0
- package/dist/codex-app-server-manager.d.ts.map +1 -0
- package/dist/codex-app-server-manager.js +1468 -0
- package/dist/codex-app-server-manager.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +8 -30
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/pool.d.ts +32 -0
- package/dist/commands/pool.d.ts.map +1 -1
- package/dist/commands/pool.js +145 -66
- package/dist/commands/pool.js.map +1 -1
- package/dist/commands/setup.d.ts +4 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +9 -25
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/start.d.ts +21 -0
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +559 -63
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +5 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/completion-marker.d.ts +7 -0
- package/dist/completion-marker.d.ts.map +1 -0
- package/dist/completion-marker.js +28 -0
- package/dist/completion-marker.js.map +1 -0
- package/dist/config.d.ts +7 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +184 -60
- package/dist/config.js.map +1 -1
- package/dist/context-window.d.ts +37 -1
- package/dist/context-window.d.ts.map +1 -1
- package/dist/context-window.js +210 -17
- package/dist/context-window.js.map +1 -1
- package/dist/live-activity.d.ts +31 -0
- package/dist/live-activity.d.ts.map +1 -0
- package/dist/live-activity.js +36 -0
- package/dist/live-activity.js.map +1 -0
- package/dist/local-chat-execution.d.ts +114 -0
- package/dist/local-chat-execution.d.ts.map +1 -0
- package/dist/local-chat-execution.js +349 -0
- package/dist/local-chat-execution.js.map +1 -0
- package/dist/local-cli-pty-manager.d.ts +186 -0
- package/dist/local-cli-pty-manager.d.ts.map +1 -1
- package/dist/local-cli-pty-manager.js +2581 -164
- package/dist/local-cli-pty-manager.js.map +1 -1
- package/dist/local-conversation-gateway.d.ts +110 -0
- package/dist/local-conversation-gateway.d.ts.map +1 -0
- package/dist/local-conversation-gateway.js +175 -0
- package/dist/local-conversation-gateway.js.map +1 -0
- package/dist/local-data.d.ts +276 -5
- package/dist/local-data.d.ts.map +1 -1
- package/dist/local-data.js +1201 -86
- package/dist/local-data.js.map +1 -1
- package/dist/local-db.d.ts +6 -0
- package/dist/local-db.d.ts.map +1 -1
- package/dist/local-db.js +428 -2
- package/dist/local-db.js.map +1 -1
- package/dist/local-funnel.d.ts.map +1 -1
- package/dist/local-funnel.js +6 -5
- package/dist/local-funnel.js.map +1 -1
- package/dist/local-server.d.ts +55 -0
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +3281 -441
- package/dist/local-server.js.map +1 -1
- package/dist/managed-process-registry.d.ts +59 -0
- package/dist/managed-process-registry.d.ts.map +1 -0
- package/dist/managed-process-registry.js +390 -0
- package/dist/managed-process-registry.js.map +1 -0
- package/dist/mcp/claude-config-writer.d.ts +5 -5
- package/dist/mcp/claude-config-writer.d.ts.map +1 -1
- package/dist/mcp/claude-config-writer.js +19 -11
- package/dist/mcp/claude-config-writer.js.map +1 -1
- package/dist/mcp/index.d.ts +4 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/sync-cli-config.d.ts +42 -4
- package/dist/mcp/sync-cli-config.d.ts.map +1 -1
- package/dist/mcp/sync-cli-config.js +497 -17
- package/dist/mcp/sync-cli-config.js.map +1 -1
- package/dist/message-loop.d.ts +6 -0
- package/dist/message-loop.d.ts.map +1 -1
- package/dist/message-loop.js +281 -89
- package/dist/message-loop.js.map +1 -1
- package/dist/mqtt-client.d.ts +44 -1
- package/dist/mqtt-client.d.ts.map +1 -1
- package/dist/mqtt-client.js +284 -46
- package/dist/mqtt-client.js.map +1 -1
- package/dist/mqtt-data-relay.d.ts +44 -0
- package/dist/mqtt-data-relay.d.ts.map +1 -0
- package/dist/mqtt-data-relay.js +106 -0
- package/dist/mqtt-data-relay.js.map +1 -0
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +69 -29
- package/dist/oauth.js.map +1 -1
- package/dist/orchestration/capabilities.d.ts +13 -0
- package/dist/orchestration/capabilities.d.ts.map +1 -0
- package/dist/orchestration/capabilities.js +152 -0
- package/dist/orchestration/capabilities.js.map +1 -0
- package/dist/orchestration/dispatch-executor.d.ts +83 -0
- package/dist/orchestration/dispatch-executor.d.ts.map +1 -0
- package/dist/orchestration/dispatch-executor.js +266 -0
- package/dist/orchestration/dispatch-executor.js.map +1 -0
- package/dist/orchestration/dispatch-hint.d.ts +134 -0
- package/dist/orchestration/dispatch-hint.d.ts.map +1 -0
- package/dist/orchestration/dispatch-hint.js +247 -0
- package/dist/orchestration/dispatch-hint.js.map +1 -0
- package/dist/orchestration/dispatch-runner.d.ts +106 -0
- package/dist/orchestration/dispatch-runner.d.ts.map +1 -0
- package/dist/orchestration/dispatch-runner.js +604 -0
- package/dist/orchestration/dispatch-runner.js.map +1 -0
- package/dist/orchestration/dispatch-tools.d.ts +167 -0
- package/dist/orchestration/dispatch-tools.d.ts.map +1 -0
- package/dist/orchestration/dispatch-tools.js +328 -0
- package/dist/orchestration/dispatch-tools.js.map +1 -0
- package/dist/orchestration/front-door-policy.d.ts +35 -10
- package/dist/orchestration/front-door-policy.d.ts.map +1 -1
- package/dist/orchestration/front-door-policy.js +30 -267
- package/dist/orchestration/front-door-policy.js.map +1 -1
- package/dist/orchestration/orchestrator-dispatch-prompt.d.ts +43 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.d.ts.map +1 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.js +267 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.js.map +1 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts +15 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.js +206 -20
- package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
- package/dist/orchestration/plan-import.d.ts +39 -0
- package/dist/orchestration/plan-import.d.ts.map +1 -0
- package/dist/orchestration/plan-import.js +547 -0
- package/dist/orchestration/plan-import.js.map +1 -0
- package/dist/orchestration/validation.d.ts +40 -0
- package/dist/orchestration/validation.d.ts.map +1 -0
- package/dist/orchestration/validation.js +203 -0
- package/dist/orchestration/validation.js.map +1 -0
- 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 +36 -46
- package/dist/orchestration/worker-operating-prompt.js.map +1 -1
- package/dist/orchestrator.d.ts +214 -33
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +2200 -1100
- package/dist/orchestrator.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +8 -4
- 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 +49 -5
- 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 +81 -5
- package/dist/providers/claude-cli.js.map +1 -1
- package/dist/providers/codex-cli.d.ts +10 -6
- package/dist/providers/codex-cli.d.ts.map +1 -1
- package/dist/providers/codex-cli.js +204 -26
- package/dist/providers/codex-cli.js.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +15 -5
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/index.d.ts +15 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +13 -5
- package/dist/providers/openai.js.map +1 -1
- package/dist/response-guard.js +1 -1
- package/dist/response-guard.js.map +1 -1
- package/dist/server-adapter.d.ts +8 -0
- package/dist/server-adapter.d.ts.map +1 -1
- package/dist/server-adapter.js +7 -0
- package/dist/server-adapter.js.map +1 -1
- package/dist/service-mode.d.ts +1 -1
- package/dist/service-mode.d.ts.map +1 -1
- package/dist/service-mode.js +64 -1
- package/dist/service-mode.js.map +1 -1
- package/dist/service-setup-only.d.ts +8 -0
- package/dist/service-setup-only.d.ts.map +1 -0
- package/dist/service-setup-only.js +37 -0
- package/dist/service-setup-only.js.map +1 -0
- package/dist/slash-commands.d.ts +21 -0
- package/dist/slash-commands.d.ts.map +1 -0
- package/dist/slash-commands.js +99 -0
- package/dist/slash-commands.js.map +1 -0
- package/dist/subagent/index.d.ts +4 -2
- package/dist/subagent/index.d.ts.map +1 -1
- package/dist/subagent/index.js.map +1 -1
- package/dist/summarization-pipeline.d.ts.map +1 -1
- package/dist/summarization-pipeline.js +1 -9
- package/dist/summarization-pipeline.js.map +1 -1
- package/dist/token-counter.d.ts.map +1 -1
- package/dist/token-counter.js +11 -4
- package/dist/token-counter.js.map +1 -1
- package/dist/tool-filter.d.ts.map +1 -1
- package/dist/tool-filter.js +10 -6
- package/dist/tool-filter.js.map +1 -1
- package/dist/tools/admin-tools.d.ts.map +1 -1
- package/dist/tools/admin-tools.js +20 -5
- package/dist/tools/admin-tools.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/run-command.d.ts.map +1 -1
- package/dist/tools/run-command.js +5 -1
- package/dist/tools/run-command.js.map +1 -1
- package/dist/tools/search-conversation-history.d.ts +16 -0
- package/dist/tools/search-conversation-history.d.ts.map +1 -0
- package/dist/tools/search-conversation-history.js +334 -0
- package/dist/tools/search-conversation-history.js.map +1 -0
- package/dist/tools/todo-tasks.d.ts.map +1 -1
- package/dist/tools/todo-tasks.js +77 -5
- package/dist/tools/todo-tasks.js.map +1 -1
- package/dist/usage-log.d.ts +62 -0
- package/dist/usage-log.d.ts.map +1 -0
- package/dist/usage-log.js +98 -0
- package/dist/usage-log.js.map +1 -0
- package/dist/wizard-state.d.ts +20 -0
- package/dist/wizard-state.d.ts.map +1 -1
- package/dist/wizard-state.js +90 -3
- package/dist/wizard-state.js.map +1 -1
- package/dist/wizard-support.d.ts.map +1 -1
- package/dist/wizard-support.js +27 -1
- package/dist/wizard-support.js.map +1 -1
- package/dist/workflow-engine.d.ts +44 -2
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +932 -111
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +2 -2
package/dist/local-server.js
CHANGED
|
@@ -39,6 +39,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.startLocalServer = startLocalServer;
|
|
40
40
|
exports.stopLocalServer = stopLocalServer;
|
|
41
41
|
exports.buildChatRuntimeForTest = buildChatRuntimeForTest;
|
|
42
|
+
exports.resolveDirectCliSessionTransportForTest = resolveDirectCliSessionTransportForTest;
|
|
43
|
+
exports.buildLocalDesktopDirectPromptForTest = buildLocalDesktopDirectPromptForTest;
|
|
44
|
+
exports.resolveLocalDesktopPromptContextForTest = resolveLocalDesktopPromptContextForTest;
|
|
45
|
+
exports.shouldSkipFreshCliBootstrapForTest = shouldSkipFreshCliBootstrapForTest;
|
|
46
|
+
exports.buildLocalRuntimeUserFacingErrorForTest = buildLocalRuntimeUserFacingErrorForTest;
|
|
47
|
+
exports.persistCliOauthSessionForTest = persistCliOauthSessionForTest;
|
|
42
48
|
/**
|
|
43
49
|
* Local HTTP server for desktop-first operation.
|
|
44
50
|
*
|
|
@@ -47,21 +53,24 @@ exports.buildChatRuntimeForTest = buildChatRuntimeForTest;
|
|
|
47
53
|
* Reuses existing LLM providers and tool execution from message-loop/providers.
|
|
48
54
|
*/
|
|
49
55
|
const index_1 = require("./providers/index");
|
|
56
|
+
const completion_marker_1 = require("./completion-marker");
|
|
50
57
|
const index_2 = require("./index");
|
|
51
58
|
const approval_1 = require("./approval");
|
|
52
59
|
const config_1 = require("./config");
|
|
60
|
+
const bench_prefix_1 = require("./bench-prefix");
|
|
61
|
+
const usage_log_1 = require("./usage-log");
|
|
53
62
|
const data = __importStar(require("./local-data"));
|
|
54
63
|
const local_import_worker_1 = require("./local-import-worker");
|
|
55
64
|
const clerk_model_1 = require("./clerk-model");
|
|
56
65
|
const workflow_engine_1 = require("./workflow-engine");
|
|
57
66
|
const context_window_1 = require("./context-window");
|
|
67
|
+
const cli_bootstrap_history_1 = require("./cli-bootstrap-history");
|
|
58
68
|
const summarization_pipeline_1 = require("./summarization-pipeline");
|
|
59
69
|
const backfill_1 = require("./backfill");
|
|
60
70
|
const config_cleanup_1 = require("./config-cleanup");
|
|
61
71
|
const status_parser_1 = require("./orchestration/status-parser");
|
|
62
72
|
const guided_actions_1 = require("./orchestration/guided-actions");
|
|
63
73
|
const topic_normalizer_1 = require("./orchestration/topic-normalizer");
|
|
64
|
-
const policy_prompt_1 = require("./orchestration/policy-prompt");
|
|
65
74
|
const safeguards_1 = require("./orchestration/safeguards");
|
|
66
75
|
const token_counter_1 = require("./token-counter");
|
|
67
76
|
const response_guard_1 = require("./response-guard");
|
|
@@ -71,14 +80,23 @@ const registry_1 = require("./mcp/registry");
|
|
|
71
80
|
const marketplace_1 = require("./mcp/marketplace");
|
|
72
81
|
const claude_config_writer_1 = require("./mcp/claude-config-writer");
|
|
73
82
|
const subscription_runtime_1 = require("./auth/subscription-runtime");
|
|
83
|
+
const credential_reader_1 = require("./auth/credential-reader");
|
|
84
|
+
const token_refresh_1 = require("./auth/token-refresh");
|
|
74
85
|
const local_memory_search_1 = require("./local-memory-search");
|
|
75
86
|
const local_funnel_1 = require("./local-funnel");
|
|
76
87
|
const orchestrator_profile_1 = require("./orchestrator-profile");
|
|
88
|
+
const local_chat_execution_1 = require("./local-chat-execution");
|
|
89
|
+
const chat_sync_1 = require("./chat-sync");
|
|
77
90
|
const policy_detection_1 = require("./policy-detection");
|
|
78
91
|
const server_runtime_1 = require("./server-runtime");
|
|
79
92
|
const storage_mode_1 = require("./storage-mode");
|
|
80
93
|
const local_cli_pty_manager_1 = require("./local-cli-pty-manager");
|
|
94
|
+
const codex_app_server_manager_1 = require("./codex-app-server-manager");
|
|
95
|
+
const managed_process_registry_1 = require("./managed-process-registry");
|
|
81
96
|
const cli_session_epoch_1 = require("./cli-session-epoch");
|
|
97
|
+
const cli_models_1 = require("./cli-models");
|
|
98
|
+
const slash_commands_1 = require("./slash-commands");
|
|
99
|
+
const sync_cli_config_1 = require("./mcp/sync-cli-config");
|
|
82
100
|
const server_adapter_1 = require("./server-adapter");
|
|
83
101
|
const wizard_support_1 = require("./wizard-support");
|
|
84
102
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -96,6 +114,351 @@ function requireExpress() {
|
|
|
96
114
|
throw new Error('express is not installed. Run: npm install express');
|
|
97
115
|
}
|
|
98
116
|
}
|
|
117
|
+
const MAX_INLINE_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
118
|
+
const MAX_PLAN_IMPORT_BYTES = 512 * 1024;
|
|
119
|
+
/**
|
|
120
|
+
* Normalize an incoming `orchestration_roles_json` payload into a clean
|
|
121
|
+
* string[] for createAgentProfile / updateAgentProfile. Accepts:
|
|
122
|
+
* - an array of strings (already in the target shape)
|
|
123
|
+
* - a JSON-string of an array (wizard/UI sometimes serializes early)
|
|
124
|
+
* - null / undefined → null (signals "clear the field")
|
|
125
|
+
* Anything else → null (defensive: never pass malformed data to the DB layer).
|
|
126
|
+
*/
|
|
127
|
+
function normalizeRolesArray(input) {
|
|
128
|
+
if (input == null)
|
|
129
|
+
return null;
|
|
130
|
+
if (Array.isArray(input)) {
|
|
131
|
+
const cleaned = input.map((v) => String(v ?? '').trim()).filter(Boolean);
|
|
132
|
+
return cleaned.length > 0 ? cleaned : null;
|
|
133
|
+
}
|
|
134
|
+
if (typeof input === 'string') {
|
|
135
|
+
const trimmed = input.trim();
|
|
136
|
+
if (!trimmed)
|
|
137
|
+
return null;
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(trimmed);
|
|
140
|
+
if (Array.isArray(parsed)) {
|
|
141
|
+
const cleaned = parsed.map((v) => String(v ?? '').trim()).filter(Boolean);
|
|
142
|
+
return cleaned.length > 0 ? cleaned : null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Not JSON — treat as a single role string for convenience.
|
|
147
|
+
return [trimmed];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
function parseChatAttachmentInputs(raw) {
|
|
153
|
+
if (!Array.isArray(raw))
|
|
154
|
+
return [];
|
|
155
|
+
return raw.flatMap((entry) => {
|
|
156
|
+
const filename = String(entry?.filename || '').trim();
|
|
157
|
+
const mimeType = String(entry?.mimeType || entry?.mime_type || '').trim();
|
|
158
|
+
const dataUrl = typeof entry?.dataUrl === 'string'
|
|
159
|
+
? entry.dataUrl
|
|
160
|
+
: typeof entry?.data_url === 'string'
|
|
161
|
+
? entry.data_url
|
|
162
|
+
: undefined;
|
|
163
|
+
const sizeBytes = Number(entry?.sizeBytes ?? entry?.size_bytes ?? 0);
|
|
164
|
+
if (!filename || !mimeType || !Number.isFinite(sizeBytes) || sizeBytes < 0)
|
|
165
|
+
return [];
|
|
166
|
+
return [{
|
|
167
|
+
id: typeof entry?.id === 'string' ? entry.id : undefined,
|
|
168
|
+
filename,
|
|
169
|
+
mimeType,
|
|
170
|
+
sizeBytes,
|
|
171
|
+
dataUrl,
|
|
172
|
+
}];
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function parseInlineDataUrl(dataUrl) {
|
|
176
|
+
const match = String(dataUrl || '').match(/^data:([^;]+);base64,(.+)$/);
|
|
177
|
+
if (!match)
|
|
178
|
+
return null;
|
|
179
|
+
return { mimeType: match[1], base64: match[2] };
|
|
180
|
+
}
|
|
181
|
+
function extensionForMimeType(mimeType, filename) {
|
|
182
|
+
const existing = path.extname(filename || '').trim();
|
|
183
|
+
if (existing)
|
|
184
|
+
return existing;
|
|
185
|
+
const normalized = String(mimeType || '').toLowerCase();
|
|
186
|
+
if (normalized === 'image/png')
|
|
187
|
+
return '.png';
|
|
188
|
+
if (normalized === 'image/jpeg')
|
|
189
|
+
return '.jpg';
|
|
190
|
+
if (normalized === 'image/webp')
|
|
191
|
+
return '.webp';
|
|
192
|
+
if (normalized === 'image/gif')
|
|
193
|
+
return '.gif';
|
|
194
|
+
return '';
|
|
195
|
+
}
|
|
196
|
+
function localFilesDir() {
|
|
197
|
+
const filesDir = path.join(os.homedir(), '.funolio', 'files');
|
|
198
|
+
if (!fs.existsSync(filesDir))
|
|
199
|
+
fs.mkdirSync(filesDir, { recursive: true });
|
|
200
|
+
return filesDir;
|
|
201
|
+
}
|
|
202
|
+
function persistInlineAttachments(conversationId, attachments) {
|
|
203
|
+
const filesDir = localFilesDir();
|
|
204
|
+
return attachments.flatMap((attachment) => {
|
|
205
|
+
if (!attachment.mimeType.startsWith('image/'))
|
|
206
|
+
return [];
|
|
207
|
+
if (!attachment.dataUrl)
|
|
208
|
+
return [];
|
|
209
|
+
const parsed = parseInlineDataUrl(attachment.dataUrl);
|
|
210
|
+
if (!parsed)
|
|
211
|
+
return [];
|
|
212
|
+
const buffer = Buffer.from(parsed.base64, 'base64');
|
|
213
|
+
if (buffer.length > MAX_INLINE_ATTACHMENT_BYTES) {
|
|
214
|
+
throw new Error(`Attachment "${attachment.filename}" exceeds max size of 10MB.`);
|
|
215
|
+
}
|
|
216
|
+
const fileId = require('crypto').randomUUID();
|
|
217
|
+
const ext = extensionForMimeType(parsed.mimeType, attachment.filename);
|
|
218
|
+
const storagePath = path.join(filesDir, `${fileId}${ext}`);
|
|
219
|
+
fs.writeFileSync(storagePath, buffer);
|
|
220
|
+
const chatFile = data.createChatFile({
|
|
221
|
+
conversationId,
|
|
222
|
+
filename: attachment.filename,
|
|
223
|
+
fileType: 'image',
|
|
224
|
+
mimeType: parsed.mimeType,
|
|
225
|
+
sizeBytes: buffer.length,
|
|
226
|
+
storagePath,
|
|
227
|
+
preview: attachment.dataUrl,
|
|
228
|
+
});
|
|
229
|
+
return [{
|
|
230
|
+
id: chatFile.id,
|
|
231
|
+
filename: chatFile.filename,
|
|
232
|
+
mimeType: chatFile.mime_type || parsed.mimeType,
|
|
233
|
+
sizeBytes: chatFile.size_bytes || buffer.length,
|
|
234
|
+
fileType: chatFile.file_type || 'image',
|
|
235
|
+
storagePath: chatFile.storage_path,
|
|
236
|
+
}];
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
function buildMessageContentWithAttachments(text, attachments) {
|
|
240
|
+
const parts = [];
|
|
241
|
+
for (const attachment of attachments) {
|
|
242
|
+
if (!attachment.mimeType.startsWith('image/') || !attachment.dataUrl)
|
|
243
|
+
continue;
|
|
244
|
+
const parsed = parseInlineDataUrl(attachment.dataUrl);
|
|
245
|
+
if (!parsed)
|
|
246
|
+
continue;
|
|
247
|
+
parts.push({ type: 'image', mimeType: parsed.mimeType, data: parsed.base64 });
|
|
248
|
+
}
|
|
249
|
+
if (parts.length === 0)
|
|
250
|
+
return text;
|
|
251
|
+
parts.push({ type: 'text', text });
|
|
252
|
+
return parts;
|
|
253
|
+
}
|
|
254
|
+
function buildMessageContentWithStoredAttachments(text, attachments) {
|
|
255
|
+
const parts = [];
|
|
256
|
+
for (const attachment of attachments) {
|
|
257
|
+
if (!attachment.mimeType.startsWith('image/') || !attachment.storagePath || !fs.existsSync(attachment.storagePath))
|
|
258
|
+
continue;
|
|
259
|
+
const data = fs.readFileSync(attachment.storagePath).toString('base64');
|
|
260
|
+
parts.push({ type: 'image', mimeType: attachment.mimeType, data });
|
|
261
|
+
}
|
|
262
|
+
if (parts.length === 0)
|
|
263
|
+
return text;
|
|
264
|
+
parts.push({ type: 'text', text });
|
|
265
|
+
return parts;
|
|
266
|
+
}
|
|
267
|
+
function parseStoredMessageAttachments(raw) {
|
|
268
|
+
if (!raw)
|
|
269
|
+
return [];
|
|
270
|
+
try {
|
|
271
|
+
const parsed = JSON.parse(raw);
|
|
272
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function hydrateMessageAttachmentForClient(attachment, opts = {}) {
|
|
279
|
+
const chatFile = attachment.id ? data.getChatFile(attachment.id) : undefined;
|
|
280
|
+
const includeAttachmentData = opts.includeAttachmentData !== false;
|
|
281
|
+
let dataUrl;
|
|
282
|
+
if (includeAttachmentData) {
|
|
283
|
+
dataUrl = chatFile?.preview || undefined;
|
|
284
|
+
if (!dataUrl && attachment.mimeType.startsWith('image/') && chatFile?.storage_path && fs.existsSync(chatFile.storage_path)) {
|
|
285
|
+
const encoded = fs.readFileSync(chatFile.storage_path).toString('base64');
|
|
286
|
+
dataUrl = `data:${attachment.mimeType};base64,${encoded}`;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const hasPreview = !!chatFile?.preview || !!chatFile?.storage_path;
|
|
290
|
+
return {
|
|
291
|
+
id: attachment.id,
|
|
292
|
+
filename: attachment.filename,
|
|
293
|
+
fileType: attachment.fileType,
|
|
294
|
+
file_type: attachment.fileType,
|
|
295
|
+
mimeType: attachment.mimeType,
|
|
296
|
+
mime_type: attachment.mimeType,
|
|
297
|
+
sizeBytes: attachment.sizeBytes,
|
|
298
|
+
size_bytes: attachment.sizeBytes,
|
|
299
|
+
hasPreview,
|
|
300
|
+
has_preview: hasPreview,
|
|
301
|
+
dataUrl,
|
|
302
|
+
data_url: dataUrl,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function serializeMessageForClient(row, opts = {}) {
|
|
306
|
+
const attachments = parseStoredMessageAttachments(row.attachments_json);
|
|
307
|
+
const includeResultArtifact = opts.includeResultArtifact !== false;
|
|
308
|
+
const includeAttachmentData = opts.includeAttachmentData !== false;
|
|
309
|
+
const resultArtifact = includeResultArtifact ? row.result_artifact : undefined;
|
|
310
|
+
return {
|
|
311
|
+
...row,
|
|
312
|
+
result_artifact: resultArtifact,
|
|
313
|
+
resultArtifact,
|
|
314
|
+
has_result_artifact: !includeResultArtifact && !!row.result_artifact,
|
|
315
|
+
hasResultArtifact: !includeResultArtifact && !!row.result_artifact,
|
|
316
|
+
result_artifact_bytes: !includeResultArtifact && row.result_artifact ? row.result_artifact.length : undefined,
|
|
317
|
+
resultArtifactBytes: !includeResultArtifact && row.result_artifact ? row.result_artifact.length : undefined,
|
|
318
|
+
attachments: attachments.length > 0
|
|
319
|
+
? attachments.map((attachment) => hydrateMessageAttachmentForClient(attachment, { includeAttachmentData }))
|
|
320
|
+
: undefined,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function shouldUseLightweightMessageHistory(req) {
|
|
324
|
+
const relayHeader = String(req.headers?.['x-funolio-relay'] || '').trim();
|
|
325
|
+
const lightQuery = String(req.query?.light || '').trim().toLowerCase();
|
|
326
|
+
const includeDetailsQuery = String(req.query?.includeDetails || req.query?.include_details || '').trim().toLowerCase();
|
|
327
|
+
if (includeDetailsQuery === '1' || includeDetailsQuery === 'true')
|
|
328
|
+
return false;
|
|
329
|
+
return relayHeader === '1' || lightQuery === '1' || lightQuery === 'true';
|
|
330
|
+
}
|
|
331
|
+
function serializeMessageHistoryForRequest(req, rows) {
|
|
332
|
+
const light = shouldUseLightweightMessageHistory(req);
|
|
333
|
+
return rows.map((row) => serializeMessageForClient(row, {
|
|
334
|
+
includeResultArtifact: !light,
|
|
335
|
+
includeAttachmentData: !light,
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
function findLatestUserMessageWithAttachments(conversationId, expectedText) {
|
|
339
|
+
const recent = data.getLastMessages(conversationId, 20);
|
|
340
|
+
const normalizedExpected = String(expectedText || '').trim();
|
|
341
|
+
for (let idx = recent.length - 1; idx >= 0; idx -= 1) {
|
|
342
|
+
const row = recent[idx];
|
|
343
|
+
if (row.role !== 'user' || !row.attachments_json)
|
|
344
|
+
continue;
|
|
345
|
+
if (!normalizedExpected || String(row.content || '').trim() === normalizedExpected) {
|
|
346
|
+
return row;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return recent.slice().reverse().find((row) => row.role === 'user' && !!row.attachments_json);
|
|
350
|
+
}
|
|
351
|
+
function resolveStoredAttachmentsForTurn(opts) {
|
|
352
|
+
if (opts.persistedAttachments && opts.persistedAttachments.length > 0) {
|
|
353
|
+
return opts.persistedAttachments;
|
|
354
|
+
}
|
|
355
|
+
if (!opts.skipUserMessage)
|
|
356
|
+
return [];
|
|
357
|
+
const job = opts.chatJobId ? data.getChatJob(String(opts.chatJobId)) : undefined;
|
|
358
|
+
const jobUserMessage = job ? data.getMessage(job.user_message_id) : undefined;
|
|
359
|
+
const attachedViaJob = parseStoredMessageAttachments(jobUserMessage?.attachments_json);
|
|
360
|
+
if (attachedViaJob.length > 0)
|
|
361
|
+
return attachedViaJob;
|
|
362
|
+
const latestUser = findLatestUserMessageWithAttachments(opts.conversationId, opts.message);
|
|
363
|
+
return parseStoredMessageAttachments(latestUser?.attachments_json);
|
|
364
|
+
}
|
|
365
|
+
function supportsNativeImageInput(providerName) {
|
|
366
|
+
const normalized = String(providerName || '').trim().toLowerCase();
|
|
367
|
+
return normalized === 'anthropic' || normalized === 'openai' || normalized === 'google' || normalized === 'codex-cli' || normalized === 'claude-cli';
|
|
368
|
+
}
|
|
369
|
+
function buildAttachmentContextBlock(summaries) {
|
|
370
|
+
if (summaries.length === 0)
|
|
371
|
+
return '';
|
|
372
|
+
return [
|
|
373
|
+
'',
|
|
374
|
+
'[Attached Images]',
|
|
375
|
+
...summaries.map((summary, index) => `Image ${index + 1}:\n${summary}`),
|
|
376
|
+
'',
|
|
377
|
+
].join('\n');
|
|
378
|
+
}
|
|
379
|
+
function resolveImageAnalysisConfig(preferredProfile) {
|
|
380
|
+
if (preferredProfile) {
|
|
381
|
+
const provider = String(preferredProfile.provider || '').trim().toLowerCase();
|
|
382
|
+
const apiKey = resolveApiKey(preferredProfile);
|
|
383
|
+
if ((provider === 'openai' || provider === 'anthropic') && apiKey) {
|
|
384
|
+
return { provider: provider, apiKey };
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const clerkConfig = data.getResolvedClerkConfigInfo();
|
|
388
|
+
const clerkProvider = String(clerkConfig.provider || '').trim().toLowerCase();
|
|
389
|
+
if (clerkProvider === 'openai' || clerkProvider === 'anthropic') {
|
|
390
|
+
const clerkConn = clerkConfig.providerConnectionId
|
|
391
|
+
? data.getProviderConnection(clerkConfig.providerConnectionId)
|
|
392
|
+
: data.findProviderConnection(clerkProvider);
|
|
393
|
+
if (clerkConn?.api_key_enc) {
|
|
394
|
+
return {
|
|
395
|
+
provider: clerkProvider,
|
|
396
|
+
apiKey: clerkConn.api_key_enc,
|
|
397
|
+
authMode: clerkConn.access_mode === 'oauth' && clerkProvider === 'anthropic' ? 'oauth-bearer' : 'api-key',
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const openaiConn = data.findProviderConnection('openai');
|
|
402
|
+
if (openaiConn?.api_key_enc)
|
|
403
|
+
return { provider: 'openai', apiKey: openaiConn.api_key_enc };
|
|
404
|
+
if (process.env.OPENAI_API_KEY)
|
|
405
|
+
return { provider: 'openai', apiKey: process.env.OPENAI_API_KEY };
|
|
406
|
+
const anthropicConn = data.findProviderConnection('anthropic');
|
|
407
|
+
if (anthropicConn?.api_key_enc) {
|
|
408
|
+
return {
|
|
409
|
+
provider: 'anthropic',
|
|
410
|
+
apiKey: anthropicConn.api_key_enc,
|
|
411
|
+
authMode: anthropicConn.access_mode === 'oauth' ? 'oauth-bearer' : 'api-key',
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
if (process.env.ANTHROPIC_API_KEY)
|
|
415
|
+
return { provider: 'anthropic', apiKey: process.env.ANTHROPIC_API_KEY };
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
async function summarizeStoredAttachmentsForTextRuntime(opts) {
|
|
419
|
+
if (!opts.attachments.length)
|
|
420
|
+
return '';
|
|
421
|
+
const analysisConfig = resolveImageAnalysisConfig(opts.preferredProfile);
|
|
422
|
+
const summaries = [];
|
|
423
|
+
for (const attachment of opts.attachments) {
|
|
424
|
+
if (!attachment.mimeType.startsWith('image/'))
|
|
425
|
+
continue;
|
|
426
|
+
if (!attachment.storagePath || !fs.existsSync(attachment.storagePath)) {
|
|
427
|
+
summaries.push(`[${attachment.filename}] Attached image is missing from local storage.`);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
if (!analysisConfig) {
|
|
431
|
+
summaries.push(`[${attachment.filename}] Attached image is available locally at: ${attachment.storagePath}`);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const toolCtx = (0, index_2.createToolContext)(opts.workspacePath, {
|
|
435
|
+
runtimeMode: 'local_desktop',
|
|
436
|
+
actorType: 'llm',
|
|
437
|
+
actorId: opts.preferredProfile?.name || opts.preferredProfile?.id || 'ImageAnalyzer',
|
|
438
|
+
llmProvider: analysisConfig.provider,
|
|
439
|
+
llmApiKey: analysisConfig.apiKey,
|
|
440
|
+
llmAuthMode: analysisConfig.authMode,
|
|
441
|
+
});
|
|
442
|
+
const result = await (0, index_2.executeToolWithMCP)({
|
|
443
|
+
id: `image-analysis-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
444
|
+
name: 'analyze_image',
|
|
445
|
+
arguments: {
|
|
446
|
+
image_path: attachment.storagePath,
|
|
447
|
+
prompt: `Describe this image for a text-only assistant. Focus on details relevant to the user's request.\n\nUser request:\n${opts.userPrompt}`,
|
|
448
|
+
},
|
|
449
|
+
}, toolCtx);
|
|
450
|
+
if (result.success && result.output.trim()) {
|
|
451
|
+
summaries.push(`[${attachment.filename}] ${result.output.trim()}`);
|
|
452
|
+
}
|
|
453
|
+
else if (result.error?.trim()) {
|
|
454
|
+
summaries.push(`[${attachment.filename}] Image analysis failed: ${result.error.trim()}`);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
summaries.push(`[${attachment.filename}] Image attached at local path: ${attachment.storagePath}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return buildAttachmentContextBlock(summaries);
|
|
461
|
+
}
|
|
99
462
|
function startLocalServer(opts) {
|
|
100
463
|
const express = requireExpress();
|
|
101
464
|
const app = express();
|
|
@@ -103,6 +466,16 @@ function startLocalServer(opts) {
|
|
|
103
466
|
if ((0, storage_mode_1.isLocalStorageMode)()) {
|
|
104
467
|
data.purgeLegacyExtractionDataOnce();
|
|
105
468
|
}
|
|
469
|
+
// Sweep orphan managed processes from previous crashes
|
|
470
|
+
try {
|
|
471
|
+
const orphanResult = (0, managed_process_registry_1.sweepOrphans)();
|
|
472
|
+
if (orphanResult.reaped > 0 || orphanResult.skipped > 0) {
|
|
473
|
+
console.info(`[managed-process] startup orphan sweep: reaped=${orphanResult.reaped} skipped=${orphanResult.skipped}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch (err) {
|
|
477
|
+
console.warn(`[managed-process] startup orphan sweep failed: ${err.message}`);
|
|
478
|
+
}
|
|
106
479
|
const cliNormalization = data.normalizeCliProviderConnections();
|
|
107
480
|
if (cliNormalization.updatedIds.length > 0) {
|
|
108
481
|
console.info(`[local-server] normalized ${cliNormalization.updatedIds.length} CLI provider connection(s): ${cliNormalization.updatedIds.join(', ')}`);
|
|
@@ -135,6 +508,553 @@ function startLocalServer(opts) {
|
|
|
135
508
|
}
|
|
136
509
|
return res.status(500).json({ error: message });
|
|
137
510
|
}
|
|
511
|
+
function syncCliBotConfigFiles(profile) {
|
|
512
|
+
if (!profile)
|
|
513
|
+
return;
|
|
514
|
+
noteCliBotConfigWrite(profile.id);
|
|
515
|
+
if (profile.provider === 'claude-cli') {
|
|
516
|
+
let parsedPermissions = null;
|
|
517
|
+
if (profile.claude_permissions_json) {
|
|
518
|
+
try {
|
|
519
|
+
parsedPermissions = JSON.parse(profile.claude_permissions_json);
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
parsedPermissions = null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
(0, sync_cli_config_1.writeClaudeBotSettings)(profile.id, {
|
|
526
|
+
model: profile.model,
|
|
527
|
+
effortLevel: profile.claude_effort_level,
|
|
528
|
+
outputStyle: profile.claude_output_style,
|
|
529
|
+
fastMode: profile.claude_fast_mode === 1,
|
|
530
|
+
permissions: parsedPermissions,
|
|
531
|
+
});
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
if (profile.provider === 'codex-cli') {
|
|
535
|
+
(0, sync_cli_config_1.writeCodexBotConfig)(profile.id, {
|
|
536
|
+
model: profile.model,
|
|
537
|
+
modelReasoningEffort: profile.codex_reasoning_effort,
|
|
538
|
+
personality: profile.codex_personality,
|
|
539
|
+
serviceTier: profile.codex_service_tier,
|
|
540
|
+
sandbox: profile.codex_sandbox_policy,
|
|
541
|
+
approvalPolicy: profile.codex_approval_policy,
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
async function refreshCliOAuthTokensIfNeeded(reason) {
|
|
546
|
+
const candidates = [
|
|
547
|
+
{ provider: 'anthropic', credential: (0, credential_reader_1.readClaudeCredentials)() },
|
|
548
|
+
{ provider: 'openai', credential: (0, credential_reader_1.readCodexCredentials)() },
|
|
549
|
+
];
|
|
550
|
+
for (const candidate of candidates) {
|
|
551
|
+
const credential = candidate.credential;
|
|
552
|
+
if (!credential?.accessToken || !credential.refreshToken || !credential.expiresAt)
|
|
553
|
+
continue;
|
|
554
|
+
if (!(0, token_refresh_1.needsRefresh)(credential))
|
|
555
|
+
continue;
|
|
556
|
+
try {
|
|
557
|
+
const refreshed = await (0, token_refresh_1.refreshToken)(credential);
|
|
558
|
+
console.info(`[token-refresh] proactive_refresh_success provider=${candidate.provider} reason=${reason} expiresAt=${new Date(refreshed.credential.expiresAt).toISOString()}`);
|
|
559
|
+
(0, credential_reader_1.clearCache)();
|
|
560
|
+
if (candidate.provider === 'anthropic') {
|
|
561
|
+
for (const profile of data.listAgentProfiles().filter((agent) => agent.provider === 'claude-cli')) {
|
|
562
|
+
syncCliBotConfigFiles(profile);
|
|
563
|
+
}
|
|
564
|
+
const closedIdleSessions = (0, local_cli_pty_manager_1.getLocalCliPtySessionManager)().closeIdleClaudeSessions();
|
|
565
|
+
if (closedIdleSessions > 0) {
|
|
566
|
+
console.info(`[claude-auth] evicted ${closedIdleSessions} idle Claude PTY session(s) after proactive refresh`);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
catch (err) {
|
|
571
|
+
console.warn(`[token-refresh] proactive_refresh_failed provider=${candidate.provider} reason=${reason} message=${JSON.stringify(err?.message || String(err))}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
void refreshCliOAuthTokensIfNeeded('startup');
|
|
576
|
+
const proactiveTokenRefreshTimer = setInterval(() => {
|
|
577
|
+
void refreshCliOAuthTokensIfNeeded('interval');
|
|
578
|
+
}, 60 * 60_000);
|
|
579
|
+
proactiveTokenRefreshTimer.unref?.();
|
|
580
|
+
function reapCliBotSessions(botId) {
|
|
581
|
+
(0, local_cli_pty_manager_1.getLocalCliPtySessionManager)().closeSessionsByBotId(botId);
|
|
582
|
+
(0, codex_app_server_manager_1.getCodexAppServerManager)().closeSessionsByBotId(botId);
|
|
583
|
+
}
|
|
584
|
+
function closeIdleManagedSession(sessionKey) {
|
|
585
|
+
(0, local_cli_pty_manager_1.getLocalCliPtySessionManager)().closeSessionByKey(sessionKey);
|
|
586
|
+
(0, codex_app_server_manager_1.getCodexAppServerManager)().closeSessionByKey(sessionKey);
|
|
587
|
+
}
|
|
588
|
+
function handleIdleSweepClose(sessionKey, provider) {
|
|
589
|
+
if (provider === 'codex-app-server') {
|
|
590
|
+
(0, codex_app_server_manager_1.getCodexAppServerManager)().closeSessionByKey(sessionKey);
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
(0, local_cli_pty_manager_1.getLocalCliPtySessionManager)().closeSessionByKey(sessionKey);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function logCliWarmEvent(event, fields) {
|
|
597
|
+
const suffix = Object.entries(fields)
|
|
598
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
599
|
+
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
|
600
|
+
.join(' ');
|
|
601
|
+
console.info(`[cli-warm] ${event}${suffix ? ` ${suffix}` : ''}`);
|
|
602
|
+
}
|
|
603
|
+
function normalizeCliWarmTrigger(value) {
|
|
604
|
+
return value === 'bot_selection' ? 'bot_selection' : 'first_keystroke';
|
|
605
|
+
}
|
|
606
|
+
function normalizeCliWarmRequestRuntimeMode(value) {
|
|
607
|
+
return value === 'server' ? 'server' : 'local_desktop';
|
|
608
|
+
}
|
|
609
|
+
function buildCliWarmFailureReason(err) {
|
|
610
|
+
const error = err;
|
|
611
|
+
if (error?.code === 'CLI_WARM_TIMEOUT' || error?.code === 'CODEX_APP_SERVER_WARM_TIMEOUT') {
|
|
612
|
+
return 'warm_timeout';
|
|
613
|
+
}
|
|
614
|
+
if (error?.authRequired === true)
|
|
615
|
+
return 'auth_required';
|
|
616
|
+
if (error?.code === 'CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT')
|
|
617
|
+
return 'warm_startup_timeout';
|
|
618
|
+
if (error?.name === 'AbortError')
|
|
619
|
+
return 'warm_aborted';
|
|
620
|
+
const message = String(error?.message || '').toLowerCase();
|
|
621
|
+
if (message.includes('closed before it was ready'))
|
|
622
|
+
return 'session_closed_before_ready';
|
|
623
|
+
return 'warm_failed';
|
|
624
|
+
}
|
|
625
|
+
function recordCliWarmSkip(skipped, context, entry) {
|
|
626
|
+
skipped.push({ botId: entry.botId, reason: entry.reason });
|
|
627
|
+
logCliWarmEvent('warm_skipped', {
|
|
628
|
+
conversationId: context.conversationId,
|
|
629
|
+
topicId: context.topicId,
|
|
630
|
+
trigger: context.trigger,
|
|
631
|
+
runtimeMode: context.runtimeMode,
|
|
632
|
+
botId: entry.botId,
|
|
633
|
+
provider: entry.provider,
|
|
634
|
+
skipReason: entry.reason,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
const CODEX_HIDDEN_WARM_PRIMER = 'This is a hidden background warm-up turn for a new conversation. Internalize the current project, topic, workspace, and bot instructions. There is no user-visible request yet. Reply with exactly READY and nothing else.';
|
|
638
|
+
function closeCliSessionsForConversation(conversationId, reason) {
|
|
639
|
+
const ptyClosed = (0, local_cli_pty_manager_1.getLocalCliPtySessionManager)().closeSessionsByConversation(conversationId);
|
|
640
|
+
const appServerClosed = (0, codex_app_server_manager_1.getCodexAppServerManager)().closeSessionsByConversation(conversationId);
|
|
641
|
+
if (ptyClosed > 0 || appServerClosed > 0) {
|
|
642
|
+
logCliWarmEvent('warm_evicted', { conversationId, reason, ptyClosed, appServerClosed });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function closeWarmCliSessionsForConversation(conversationId, reason) {
|
|
646
|
+
const ptyClosed = (0, local_cli_pty_manager_1.getLocalCliPtySessionManager)().closeWarmSessionsByConversation(conversationId);
|
|
647
|
+
const appServerClosed = (0, codex_app_server_manager_1.getCodexAppServerManager)().closeWarmSessionsByConversation(conversationId);
|
|
648
|
+
if (ptyClosed > 0 || appServerClosed > 0) {
|
|
649
|
+
logCliWarmEvent(reason === 'expired' ? 'warm_expired' : 'warm_evicted', {
|
|
650
|
+
conversationId,
|
|
651
|
+
reason,
|
|
652
|
+
ptyClosed,
|
|
653
|
+
appServerClosed,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function resolveConversationCwd(conversation) {
|
|
658
|
+
const project = conversation?.project_id ? data.getProject(conversation.project_id) : undefined;
|
|
659
|
+
const workspacePath = project?.folder?.trim() || undefined;
|
|
660
|
+
return workspacePath && fs.existsSync(workspacePath) ? workspacePath : opts.projectDir;
|
|
661
|
+
}
|
|
662
|
+
async function warmCliSessionsForConversation(input) {
|
|
663
|
+
const conversation = data.getConversation(input.conversationId);
|
|
664
|
+
if (!conversation) {
|
|
665
|
+
return {
|
|
666
|
+
ok: false,
|
|
667
|
+
warmed: [],
|
|
668
|
+
skipped: [],
|
|
669
|
+
failed: [{ botId: '*', error: 'Conversation not found' }],
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
const cwd = resolveConversationCwd(conversation);
|
|
673
|
+
const uniqueBotIds = [...new Set((input.botIds || []).map(String).filter(Boolean))];
|
|
674
|
+
const warmed = [];
|
|
675
|
+
const skipped = [];
|
|
676
|
+
const failed = [];
|
|
677
|
+
const trigger = normalizeCliWarmTrigger(input.trigger);
|
|
678
|
+
const runtimeMode = normalizeCliWarmRequestRuntimeMode(input.runtimeMode);
|
|
679
|
+
await Promise.all(uniqueBotIds.map(async (botId) => {
|
|
680
|
+
const profile = data.getAgentProfile(botId);
|
|
681
|
+
if (!profile) {
|
|
682
|
+
recordCliWarmSkip(skipped, {
|
|
683
|
+
conversationId: input.conversationId,
|
|
684
|
+
topicId: input.topicId,
|
|
685
|
+
trigger,
|
|
686
|
+
runtimeMode,
|
|
687
|
+
}, { botId, reason: 'bot_not_found' });
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (profile.is_active === 0) {
|
|
691
|
+
recordCliWarmSkip(skipped, {
|
|
692
|
+
conversationId: input.conversationId,
|
|
693
|
+
topicId: input.topicId,
|
|
694
|
+
trigger,
|
|
695
|
+
runtimeMode,
|
|
696
|
+
}, { botId, provider: profile.provider, reason: 'bot_inactive' });
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
let runtime;
|
|
700
|
+
let createdEpochSessionId;
|
|
701
|
+
try {
|
|
702
|
+
runtime = await buildChatRuntime(profile);
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
recordCliWarmSkip(skipped, {
|
|
706
|
+
conversationId: input.conversationId,
|
|
707
|
+
topicId: input.topicId,
|
|
708
|
+
trigger,
|
|
709
|
+
runtimeMode,
|
|
710
|
+
}, { botId, provider: profile.provider, reason: 'runtime_build_failed' });
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (!index_1.CLI_PROVIDERS.has(runtime.providerName)) {
|
|
714
|
+
recordCliWarmSkip(skipped, {
|
|
715
|
+
conversationId: input.conversationId,
|
|
716
|
+
topicId: input.topicId,
|
|
717
|
+
trigger,
|
|
718
|
+
runtimeMode,
|
|
719
|
+
}, { botId, provider: runtime.providerName, reason: 'not_cli_provider' });
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const transport = resolveDirectCliSessionTransport(runtime.providerName, true, (0, storage_mode_1.isLocalStorageMode)());
|
|
723
|
+
const cliSessionEpochPlan = (0, cli_session_epoch_1.selectCliSessionEpoch)(input.conversationId, profile.id, runtime.providerName);
|
|
724
|
+
logCliWarmEvent('warm_requested', {
|
|
725
|
+
conversationId: input.conversationId,
|
|
726
|
+
topicId: input.topicId,
|
|
727
|
+
botId,
|
|
728
|
+
provider: runtime.providerName,
|
|
729
|
+
trigger,
|
|
730
|
+
runtimeMode,
|
|
731
|
+
});
|
|
732
|
+
try {
|
|
733
|
+
if (transport === 'codex-app-server') {
|
|
734
|
+
// Build the same systemPrompt the send path will use so warm can
|
|
735
|
+
// pre-create the Codex thread under the correct fingerprint. If
|
|
736
|
+
// anything drifts between warm and Send (e.g. user edits soul_md
|
|
737
|
+
// mid-typing), the fingerprint check in codex-app-server-manager
|
|
738
|
+
// invalidates the warm thread and Send falls back to a fresh
|
|
739
|
+
// thread/start — same as today's uncarmed behavior.
|
|
740
|
+
const allToolDefs = (0, index_2.getAllToolDefinitions)('local_desktop', mcpManager);
|
|
741
|
+
const unrestrictedCliProfile = index_1.CLI_PROVIDERS.has(profile.provider);
|
|
742
|
+
const configuredBuiltinTools = parseToolSelectionJson(profile.enabled_builtin_tools_json);
|
|
743
|
+
const configuredMcpTools = parseToolSelectionJson(profile.enabled_mcp_tools_json);
|
|
744
|
+
const allowedToolNames = unrestrictedCliProfile
|
|
745
|
+
? new Set(allToolDefs.map((tool) => tool.name))
|
|
746
|
+
: expandAllowedToolNames(allToolDefs, configuredBuiltinTools, configuredMcpTools);
|
|
747
|
+
const toolDefs = allToolDefs.filter((tool) => allowedToolNames.has(tool.name));
|
|
748
|
+
const configuredTz = (data.getSetting('timezone') || '').trim();
|
|
749
|
+
const effectiveTimezone = configuredTz && configuredTz.toLowerCase() !== 'system'
|
|
750
|
+
? configuredTz
|
|
751
|
+
: Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
752
|
+
const promptContextWindow = (0, context_window_1.getPromptContextWindow)(input.conversationId, safeguards_1.SAFEGUARDS.CONTEXT_WINDOW_TURNS, {
|
|
753
|
+
targetBotId: profile.id,
|
|
754
|
+
targetBotName: profile.name,
|
|
755
|
+
});
|
|
756
|
+
const promptContext = resolveLocalDesktopPromptContext({
|
|
757
|
+
conversationId: input.conversationId,
|
|
758
|
+
conversation,
|
|
759
|
+
explicitTopicId: input.topicId,
|
|
760
|
+
fallbackCwd: cwd,
|
|
761
|
+
});
|
|
762
|
+
const directPrompt = buildLocalDesktopDirectPrompt({
|
|
763
|
+
conversationId: input.conversationId,
|
|
764
|
+
currentBotId: profile.id,
|
|
765
|
+
currentBotName: profile.name,
|
|
766
|
+
currentProvider: runtime.providerName,
|
|
767
|
+
userPrompt: '',
|
|
768
|
+
soulMd: profile.soul_md
|
|
769
|
+
|| 'You are an AI assistant running locally. You have access to project files and can execute code.',
|
|
770
|
+
projectName: promptContext.projectName,
|
|
771
|
+
topicTitle: promptContext.topicTitle,
|
|
772
|
+
workspacePath: promptContext.workspacePath,
|
|
773
|
+
timezone: effectiveTimezone,
|
|
774
|
+
availableTools: toolDefs.map((tool) => ({ name: tool.name, description: tool.description })),
|
|
775
|
+
isCliRecurring: !!cliSessionEpochPlan.resumeSessionId,
|
|
776
|
+
cliHistoryFilePath: null,
|
|
777
|
+
crossBotTurn: promptContextWindow.lastCrossBotTurn,
|
|
778
|
+
forceInlinePromptContext: transport === 'codex-app-server' && !cliSessionEpochPlan.resumeSessionId,
|
|
779
|
+
useCompletionSentinel: false,
|
|
780
|
+
});
|
|
781
|
+
const shouldPrimeFreshCodex = runtime.providerName === 'codex-cli'
|
|
782
|
+
&& !cliSessionEpochPlan.resumeSessionId
|
|
783
|
+
&& (conversation.turn_count || 0) === 0;
|
|
784
|
+
const result = await (0, codex_app_server_manager_1.getCodexAppServerManager)().warmSession({
|
|
785
|
+
runtimeMode: 'local_desktop',
|
|
786
|
+
conversationId: input.conversationId,
|
|
787
|
+
botId: profile.id,
|
|
788
|
+
botName: profile.name,
|
|
789
|
+
cwd,
|
|
790
|
+
projectId: conversation.project_id ?? null,
|
|
791
|
+
timeoutMs: 30_000,
|
|
792
|
+
systemPrompt: directPrompt.systemPrompt,
|
|
793
|
+
model: runtime.model || profile.model || null,
|
|
794
|
+
codexSettings: {
|
|
795
|
+
reasoningEffort: profile.codex_reasoning_effort,
|
|
796
|
+
reasoningSummary: profile.codex_reasoning_summary,
|
|
797
|
+
personality: profile.codex_personality,
|
|
798
|
+
serviceTier: profile.codex_service_tier,
|
|
799
|
+
sandboxPolicy: profile.codex_sandbox_policy,
|
|
800
|
+
approvalPolicy: profile.codex_approval_policy,
|
|
801
|
+
},
|
|
802
|
+
resumeSessionId: cliSessionEpochPlan.resumeSessionId || undefined,
|
|
803
|
+
primerMessages: shouldPrimeFreshCodex
|
|
804
|
+
? [{ role: 'user', content: CODEX_HIDDEN_WARM_PRIMER }]
|
|
805
|
+
: undefined,
|
|
806
|
+
});
|
|
807
|
+
if (shouldPrimeFreshCodex && result.hiddenPrimerCompleted && result.sessionId) {
|
|
808
|
+
data.upsertCliSessionEpoch({
|
|
809
|
+
conversationId: input.conversationId,
|
|
810
|
+
botId: profile.id,
|
|
811
|
+
provider: runtime.providerName,
|
|
812
|
+
sessionId: result.sessionId,
|
|
813
|
+
epochTurnCount: 1,
|
|
814
|
+
epochStartedAt: localTimestamp(),
|
|
815
|
+
lastUsedAt: localTimestamp(),
|
|
816
|
+
resetReason: null,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
warmed.push({
|
|
820
|
+
botId,
|
|
821
|
+
provider: runtime.providerName,
|
|
822
|
+
reusedExistingSession: result.reusedExistingSession,
|
|
823
|
+
readyAgeMs: result.readyAgeMs,
|
|
824
|
+
});
|
|
825
|
+
logCliWarmEvent('warm_ready', {
|
|
826
|
+
conversationId: input.conversationId,
|
|
827
|
+
topicId: input.topicId,
|
|
828
|
+
botId,
|
|
829
|
+
provider: runtime.providerName,
|
|
830
|
+
trigger,
|
|
831
|
+
runtimeMode,
|
|
832
|
+
reusedExistingSession: result.reusedExistingSession,
|
|
833
|
+
readyAgeMs: result.readyAgeMs,
|
|
834
|
+
});
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (transport !== 'pty') {
|
|
838
|
+
recordCliWarmSkip(skipped, {
|
|
839
|
+
conversationId: input.conversationId,
|
|
840
|
+
topicId: input.topicId,
|
|
841
|
+
trigger,
|
|
842
|
+
runtimeMode,
|
|
843
|
+
}, { botId, provider: runtime.providerName, reason: 'transport_not_warmable' });
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
const isFreshClaude = runtime.providerName === 'claude-cli' && !cliSessionEpochPlan.resumeSessionId;
|
|
847
|
+
const newSessionId = isFreshClaude ? data.generateNextSessionId() : undefined;
|
|
848
|
+
const result = await (0, local_cli_pty_manager_1.getLocalCliPtySessionManager)().warmSession({
|
|
849
|
+
conversationId: input.conversationId,
|
|
850
|
+
botId: profile.id,
|
|
851
|
+
provider: runtime.providerName,
|
|
852
|
+
botSettings: {
|
|
853
|
+
claude: runtime.providerName === 'claude-cli'
|
|
854
|
+
? {
|
|
855
|
+
model: runtime.model || profile.model,
|
|
856
|
+
effortLevel: profile.claude_effort_level,
|
|
857
|
+
outputStyle: profile.claude_output_style,
|
|
858
|
+
fastMode: profile.claude_fast_mode === 1,
|
|
859
|
+
permissionsJson: profile.claude_permissions_json,
|
|
860
|
+
}
|
|
861
|
+
: undefined,
|
|
862
|
+
codex: runtime.providerName === 'codex-cli'
|
|
863
|
+
? {
|
|
864
|
+
model: runtime.model || profile.model,
|
|
865
|
+
reasoningEffort: profile.codex_reasoning_effort,
|
|
866
|
+
personality: profile.codex_personality,
|
|
867
|
+
serviceTier: profile.codex_service_tier,
|
|
868
|
+
sandboxPolicy: profile.codex_sandbox_policy,
|
|
869
|
+
approvalPolicy: profile.codex_approval_policy,
|
|
870
|
+
}
|
|
871
|
+
: undefined,
|
|
872
|
+
},
|
|
873
|
+
cwd,
|
|
874
|
+
toolActorId: profile.name,
|
|
875
|
+
toolProjectId: conversation.project_id ?? null,
|
|
876
|
+
topicId: input.topicId,
|
|
877
|
+
runtimeMode,
|
|
878
|
+
resumeSessionId: cliSessionEpochPlan.resumeSessionId || undefined,
|
|
879
|
+
newSessionId,
|
|
880
|
+
useConpty: input.orchestrationMode ? process.platform !== 'win32' : undefined,
|
|
881
|
+
timeoutMs: 10_000,
|
|
882
|
+
});
|
|
883
|
+
if (!result.reusedExistingSession && newSessionId) {
|
|
884
|
+
createdEpochSessionId = newSessionId;
|
|
885
|
+
data.upsertCliSessionEpoch({
|
|
886
|
+
conversationId: input.conversationId,
|
|
887
|
+
botId: profile.id,
|
|
888
|
+
provider: runtime.providerName,
|
|
889
|
+
sessionId: newSessionId,
|
|
890
|
+
epochTurnCount: 0,
|
|
891
|
+
lastInputTokens: 0,
|
|
892
|
+
lastOutputTokens: 0,
|
|
893
|
+
resetReason: cliSessionEpochPlan.resetReason || 'prewarm',
|
|
894
|
+
epochStartedAt: localTimestamp(),
|
|
895
|
+
lastUsedAt: localTimestamp(),
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
warmed.push({
|
|
899
|
+
botId,
|
|
900
|
+
provider: runtime.providerName,
|
|
901
|
+
reusedExistingSession: result.reusedExistingSession,
|
|
902
|
+
readyAgeMs: result.readyAgeMs,
|
|
903
|
+
});
|
|
904
|
+
logCliWarmEvent('warm_ready', {
|
|
905
|
+
conversationId: input.conversationId,
|
|
906
|
+
topicId: input.topicId,
|
|
907
|
+
botId,
|
|
908
|
+
provider: runtime.providerName,
|
|
909
|
+
trigger,
|
|
910
|
+
runtimeMode,
|
|
911
|
+
reusedExistingSession: result.reusedExistingSession,
|
|
912
|
+
readyAgeMs: result.readyAgeMs,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
catch (err) {
|
|
916
|
+
if (createdEpochSessionId) {
|
|
917
|
+
data.deleteCliSessionEpoch(input.conversationId, profile.id);
|
|
918
|
+
}
|
|
919
|
+
const message = err?.message || String(err);
|
|
920
|
+
const failureReason = buildCliWarmFailureReason(err);
|
|
921
|
+
const timeout = failureReason === 'warm_timeout';
|
|
922
|
+
const timeoutMs = err?.code === 'CODEX_APP_SERVER_WARM_TIMEOUT'
|
|
923
|
+
? 30_000
|
|
924
|
+
: err?.code === 'CLI_WARM_TIMEOUT'
|
|
925
|
+
? 10_000
|
|
926
|
+
: undefined;
|
|
927
|
+
failed.push({ botId, provider: runtime.providerName, error: message, timeout });
|
|
928
|
+
logCliWarmEvent(timeout ? 'warm_timeout' : 'warm_failed', {
|
|
929
|
+
conversationId: input.conversationId,
|
|
930
|
+
topicId: input.topicId,
|
|
931
|
+
botId,
|
|
932
|
+
provider: runtime.providerName,
|
|
933
|
+
trigger,
|
|
934
|
+
runtimeMode,
|
|
935
|
+
timeoutMs,
|
|
936
|
+
failureReason,
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
}));
|
|
940
|
+
return {
|
|
941
|
+
ok: failed.length === 0,
|
|
942
|
+
warmed,
|
|
943
|
+
skipped,
|
|
944
|
+
failed,
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
const cliBotConfigWatchers = new Map();
|
|
948
|
+
function noteCliBotConfigWrite(botId) {
|
|
949
|
+
const existing = cliBotConfigWatchers.get(botId);
|
|
950
|
+
if (!existing)
|
|
951
|
+
return;
|
|
952
|
+
existing.suppressUntilMs = Date.now() + 1_500;
|
|
953
|
+
}
|
|
954
|
+
function closeCliBotConfigWatcher(botId) {
|
|
955
|
+
const existing = cliBotConfigWatchers.get(botId);
|
|
956
|
+
if (!existing)
|
|
957
|
+
return;
|
|
958
|
+
if (existing.debounceTimer)
|
|
959
|
+
clearTimeout(existing.debounceTimer);
|
|
960
|
+
try {
|
|
961
|
+
existing.watcher.close();
|
|
962
|
+
}
|
|
963
|
+
catch {
|
|
964
|
+
// best effort
|
|
965
|
+
}
|
|
966
|
+
cliBotConfigWatchers.delete(botId);
|
|
967
|
+
}
|
|
968
|
+
function buildCliBotConfigUpdateFromDisk(profile) {
|
|
969
|
+
if (profile.provider === 'claude-cli') {
|
|
970
|
+
const disk = (0, sync_cli_config_1.readClaudeBotSettings)(profile.id);
|
|
971
|
+
const nextPermissionsJson = disk.permissions ? JSON.stringify(disk.permissions) : null;
|
|
972
|
+
const fields = {};
|
|
973
|
+
const nextModel = (disk.model || '').trim();
|
|
974
|
+
if (nextModel && nextModel !== profile.model)
|
|
975
|
+
fields.model = nextModel;
|
|
976
|
+
const nextEffort = (disk.effortLevel || 'auto').trim() || 'auto';
|
|
977
|
+
if (nextEffort !== profile.claude_effort_level)
|
|
978
|
+
fields.claudeEffortLevel = nextEffort;
|
|
979
|
+
const nextOutputStyle = (disk.outputStyle || 'default').trim() || 'default';
|
|
980
|
+
if (nextOutputStyle !== profile.claude_output_style)
|
|
981
|
+
fields.claudeOutputStyle = nextOutputStyle;
|
|
982
|
+
const nextFastMode = disk.fastMode === true;
|
|
983
|
+
if ((nextFastMode ? 1 : 0) !== profile.claude_fast_mode)
|
|
984
|
+
fields.claudeFastMode = nextFastMode;
|
|
985
|
+
if ((nextPermissionsJson || null) !== (profile.claude_permissions_json || null)) {
|
|
986
|
+
fields.claudePermissionsJson = nextPermissionsJson;
|
|
987
|
+
}
|
|
988
|
+
return Object.keys(fields).length > 0 ? fields : null;
|
|
989
|
+
}
|
|
990
|
+
if (profile.provider === 'codex-cli') {
|
|
991
|
+
const disk = (0, sync_cli_config_1.readCodexBotConfig)(profile.id);
|
|
992
|
+
const fields = {};
|
|
993
|
+
const nextModel = (disk.model || '').trim();
|
|
994
|
+
if (nextModel && nextModel !== profile.model)
|
|
995
|
+
fields.model = nextModel;
|
|
996
|
+
const nextReasoning = (disk.modelReasoningEffort || 'high').trim() || 'high';
|
|
997
|
+
if (nextReasoning !== profile.codex_reasoning_effort)
|
|
998
|
+
fields.codexReasoningEffort = nextReasoning;
|
|
999
|
+
const nextPersonality = (disk.personality || 'friendly').trim() || 'friendly';
|
|
1000
|
+
if (nextPersonality !== profile.codex_personality)
|
|
1001
|
+
fields.codexPersonality = nextPersonality;
|
|
1002
|
+
const nextServiceTier = (disk.serviceTier || 'fast').trim() || 'fast';
|
|
1003
|
+
if (nextServiceTier !== profile.codex_service_tier)
|
|
1004
|
+
fields.codexServiceTier = nextServiceTier;
|
|
1005
|
+
const nextSandbox = (disk.sandbox || 'danger-full-access').trim() || 'danger-full-access';
|
|
1006
|
+
if (nextSandbox !== profile.codex_sandbox_policy)
|
|
1007
|
+
fields.codexSandboxPolicy = nextSandbox;
|
|
1008
|
+
const nextApproval = (disk.approvalPolicy || 'never').trim() || 'never';
|
|
1009
|
+
if (nextApproval !== profile.codex_approval_policy)
|
|
1010
|
+
fields.codexApprovalPolicy = nextApproval;
|
|
1011
|
+
return Object.keys(fields).length > 0 ? fields : null;
|
|
1012
|
+
}
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
function applyCliBotConfigUpdateFromDisk(botId) {
|
|
1016
|
+
const profile = data.getAgentProfile(botId);
|
|
1017
|
+
if (!profile || (profile.provider !== 'claude-cli' && profile.provider !== 'codex-cli'))
|
|
1018
|
+
return;
|
|
1019
|
+
const fields = buildCliBotConfigUpdateFromDisk(profile);
|
|
1020
|
+
if (!fields)
|
|
1021
|
+
return;
|
|
1022
|
+
data.updateAgentProfile(botId, fields);
|
|
1023
|
+
reapCliBotSessions(botId);
|
|
1024
|
+
}
|
|
1025
|
+
function watchCliBotConfig(profile) {
|
|
1026
|
+
if (!profile || (profile.provider !== 'claude-cli' && profile.provider !== 'codex-cli'))
|
|
1027
|
+
return;
|
|
1028
|
+
closeCliBotConfigWatcher(profile.id);
|
|
1029
|
+
const configPath = profile.provider === 'claude-cli'
|
|
1030
|
+
? (0, sync_cli_config_1.claudeBotSettingsPath)(profile.id)
|
|
1031
|
+
: (0, sync_cli_config_1.codexConfigPath)((0, sync_cli_config_1.getCliBotSessionHome)('codex-cli', profile.id));
|
|
1032
|
+
const watchDir = path.dirname(configPath);
|
|
1033
|
+
if (!fs.existsSync(watchDir))
|
|
1034
|
+
return;
|
|
1035
|
+
try {
|
|
1036
|
+
const state = {
|
|
1037
|
+
watcher: fs.watch(watchDir, () => {
|
|
1038
|
+
const current = cliBotConfigWatchers.get(profile.id);
|
|
1039
|
+
if (!current)
|
|
1040
|
+
return;
|
|
1041
|
+
if (current.debounceTimer)
|
|
1042
|
+
clearTimeout(current.debounceTimer);
|
|
1043
|
+
current.debounceTimer = setTimeout(() => {
|
|
1044
|
+
if (Date.now() < current.suppressUntilMs)
|
|
1045
|
+
return;
|
|
1046
|
+
applyCliBotConfigUpdateFromDisk(profile.id);
|
|
1047
|
+
}, 200);
|
|
1048
|
+
}),
|
|
1049
|
+
debounceTimer: null,
|
|
1050
|
+
suppressUntilMs: 0,
|
|
1051
|
+
};
|
|
1052
|
+
cliBotConfigWatchers.set(profile.id, state);
|
|
1053
|
+
}
|
|
1054
|
+
catch {
|
|
1055
|
+
// best effort only
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
138
1058
|
// Auto-seed agent profiles from DB-backed provider connections after legacy migration.
|
|
139
1059
|
try {
|
|
140
1060
|
const migration = (0, wizard_state_1.migrateLegacyConfigToDb)();
|
|
@@ -191,14 +1111,70 @@ function startLocalServer(opts) {
|
|
|
191
1111
|
console.error(chalk_1.default.yellow(` Failed to reconcile conversation/topic project consistency: ${err}`));
|
|
192
1112
|
}
|
|
193
1113
|
try {
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
196
|
-
console.log(chalk_1.default.gray(`
|
|
1114
|
+
const recovery = data.recoverInterruptedChatJobs();
|
|
1115
|
+
if (recovery.totalRecovered > 0) {
|
|
1116
|
+
console.log(chalk_1.default.gray(` Recovered ${recovery.totalRecovered} interrupted chat job(s)`));
|
|
1117
|
+
}
|
|
1118
|
+
console.log(JSON.stringify({
|
|
1119
|
+
ts: new Date().toISOString(),
|
|
1120
|
+
level: 'info',
|
|
1121
|
+
event: 'CRASH_RECOVERY',
|
|
1122
|
+
recoveredCount: recovery.totalRecovered,
|
|
1123
|
+
}));
|
|
1124
|
+
for (const job of recovery.recoveredJobs) {
|
|
1125
|
+
console.log(JSON.stringify({
|
|
1126
|
+
ts: new Date().toISOString(),
|
|
1127
|
+
level: 'info',
|
|
1128
|
+
event: 'CRASH_RECOVERY_JOB',
|
|
1129
|
+
jobId: job.jobId,
|
|
1130
|
+
previousStatus: job.previousStatus,
|
|
1131
|
+
}));
|
|
197
1132
|
}
|
|
198
1133
|
}
|
|
199
1134
|
catch (err) {
|
|
200
1135
|
console.error(chalk_1.default.yellow(` Failed to recover interrupted chat jobs: ${err}`));
|
|
201
1136
|
}
|
|
1137
|
+
try {
|
|
1138
|
+
const workflowRecovery = data.recoverInterruptedWorkflowRuns();
|
|
1139
|
+
if (workflowRecovery.totalRecovered > 0) {
|
|
1140
|
+
console.log(chalk_1.default.gray(` Recovered ${workflowRecovery.totalRecovered} interrupted workflow item(s)`));
|
|
1141
|
+
}
|
|
1142
|
+
console.log(JSON.stringify({
|
|
1143
|
+
ts: new Date().toISOString(),
|
|
1144
|
+
level: 'info',
|
|
1145
|
+
event: 'WORKFLOW_CRASH_RECOVERY',
|
|
1146
|
+
recoveredCount: workflowRecovery.totalRecovered,
|
|
1147
|
+
recoveredWorkflowExecutions: workflowRecovery.recoveredWorkflowExecutions.length,
|
|
1148
|
+
recoveredImportedPlanRuns: workflowRecovery.recoveredImportedPlanRuns.length,
|
|
1149
|
+
}));
|
|
1150
|
+
for (const workflow of workflowRecovery.recoveredWorkflowExecutions) {
|
|
1151
|
+
console.log(JSON.stringify({
|
|
1152
|
+
ts: new Date().toISOString(),
|
|
1153
|
+
level: 'info',
|
|
1154
|
+
event: 'WORKFLOW_CRASH_RECOVERY_EXECUTION',
|
|
1155
|
+
workflowId: workflow.workflowId,
|
|
1156
|
+
conversationId: workflow.conversationId,
|
|
1157
|
+
previousStatus: workflow.previousStatus,
|
|
1158
|
+
}));
|
|
1159
|
+
}
|
|
1160
|
+
for (const run of workflowRecovery.recoveredImportedPlanRuns) {
|
|
1161
|
+
console.log(JSON.stringify({
|
|
1162
|
+
ts: new Date().toISOString(),
|
|
1163
|
+
level: 'info',
|
|
1164
|
+
event: 'WORKFLOW_CRASH_RECOVERY_IMPORTED_PLAN',
|
|
1165
|
+
runId: run.runId,
|
|
1166
|
+
conversationId: run.conversationId,
|
|
1167
|
+
previousStatus: run.previousStatus,
|
|
1168
|
+
remainingTaskCount: run.remainingTaskCount,
|
|
1169
|
+
}));
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
catch (err) {
|
|
1173
|
+
console.error(chalk_1.default.yellow(` Failed to recover interrupted workflow runs: ${err}`));
|
|
1174
|
+
}
|
|
1175
|
+
for (const profile of data.listAgentProfiles()) {
|
|
1176
|
+
watchCliBotConfig(profile);
|
|
1177
|
+
}
|
|
202
1178
|
// ─── Health ──────────────────────────────────────────────────
|
|
203
1179
|
app.get('/api/health', (_req, res) => {
|
|
204
1180
|
try {
|
|
@@ -209,12 +1185,37 @@ function startLocalServer(opts) {
|
|
|
209
1185
|
db: stats,
|
|
210
1186
|
version: require('../package.json').version,
|
|
211
1187
|
runtime,
|
|
1188
|
+
systemInfo: {
|
|
1189
|
+
hostname: os.hostname(),
|
|
1190
|
+
platform: os.platform(),
|
|
1191
|
+
arch: os.arch(),
|
|
1192
|
+
},
|
|
212
1193
|
});
|
|
213
1194
|
}
|
|
214
1195
|
catch (err) {
|
|
215
1196
|
res.status(500).json({ status: 'error', error: err.message });
|
|
216
1197
|
}
|
|
217
1198
|
});
|
|
1199
|
+
// ─── Diagnostics: Managed Sessions ─────────────────────────
|
|
1200
|
+
app.get('/api/diagnostics/sessions', (_req, res) => {
|
|
1201
|
+
try {
|
|
1202
|
+
const sessions = (0, managed_process_registry_1.getDiagnostics)();
|
|
1203
|
+
res.json({ sessions });
|
|
1204
|
+
}
|
|
1205
|
+
catch (err) {
|
|
1206
|
+
res.status(500).json({ error: err.message });
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
app.post('/api/diagnostics/sessions/:sessionKey/close', (req, res) => {
|
|
1210
|
+
try {
|
|
1211
|
+
const sessionKey = decodeURIComponent(req.params.sessionKey);
|
|
1212
|
+
closeIdleManagedSession(sessionKey);
|
|
1213
|
+
res.json({ ok: true, sessionKey });
|
|
1214
|
+
}
|
|
1215
|
+
catch (err) {
|
|
1216
|
+
res.status(500).json({ error: err.message });
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
218
1219
|
app.get('/api/runtime/config', async (_req, res) => {
|
|
219
1220
|
try {
|
|
220
1221
|
const runtime = (0, server_runtime_1.getRuntimeConnectionConfig)();
|
|
@@ -231,6 +1232,49 @@ function startLocalServer(opts) {
|
|
|
231
1232
|
res.status(500).json({ error: err.message });
|
|
232
1233
|
}
|
|
233
1234
|
});
|
|
1235
|
+
app.post('/api/cli-sessions/warm', async (req, res) => {
|
|
1236
|
+
try {
|
|
1237
|
+
if (isConnectedMode()) {
|
|
1238
|
+
return res.json({ ok: true, warmed: [], skipped: [{ botId: '*', reason: 'connected_mode' }], failed: [] });
|
|
1239
|
+
}
|
|
1240
|
+
const conversationId = String(req.body?.conversationId || '').trim();
|
|
1241
|
+
const botIds = Array.isArray(req.body?.botIds) ? req.body.botIds.map(String) : [];
|
|
1242
|
+
if (!conversationId)
|
|
1243
|
+
return res.status(400).json({ error: 'conversationId is required' });
|
|
1244
|
+
if (botIds.length === 0)
|
|
1245
|
+
return res.json({ ok: true, warmed: [], skipped: [], failed: [] });
|
|
1246
|
+
const result = await warmCliSessionsForConversation({
|
|
1247
|
+
conversationId,
|
|
1248
|
+
botIds,
|
|
1249
|
+
topicId: typeof req.body?.topicId === 'string' ? req.body.topicId : undefined,
|
|
1250
|
+
trigger: typeof req.body?.trigger === 'string' ? req.body.trigger : undefined,
|
|
1251
|
+
runtimeMode: typeof req.body?.runtimeMode === 'string' ? req.body.runtimeMode : undefined,
|
|
1252
|
+
orchestrationMode: req.body?.orchestrationMode === true,
|
|
1253
|
+
});
|
|
1254
|
+
if (!result.ok && result.failed.some((entry) => entry.error === 'Conversation not found')) {
|
|
1255
|
+
return res.status(404).json(result);
|
|
1256
|
+
}
|
|
1257
|
+
res.json(result);
|
|
1258
|
+
}
|
|
1259
|
+
catch (err) {
|
|
1260
|
+
res.status(500).json({ error: err.message });
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
app.post('/api/cli-sessions/warm/close', async (req, res) => {
|
|
1264
|
+
try {
|
|
1265
|
+
if (isConnectedMode()) {
|
|
1266
|
+
return res.json({ ok: true });
|
|
1267
|
+
}
|
|
1268
|
+
const conversationId = String(req.body?.conversationId || '').trim();
|
|
1269
|
+
if (!conversationId)
|
|
1270
|
+
return res.status(400).json({ error: 'conversationId is required' });
|
|
1271
|
+
closeWarmCliSessionsForConversation(conversationId, String(req.body?.reason || 'expired'));
|
|
1272
|
+
res.json({ ok: true });
|
|
1273
|
+
}
|
|
1274
|
+
catch (err) {
|
|
1275
|
+
res.status(500).json({ error: err.message });
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
234
1278
|
app.get('/api/runtime/agents', async (_req, res) => {
|
|
235
1279
|
try {
|
|
236
1280
|
const runtime = (0, server_runtime_1.getRuntimeConnectionConfig)();
|
|
@@ -552,6 +1596,36 @@ function startLocalServer(opts) {
|
|
|
552
1596
|
function localTimestamp() {
|
|
553
1597
|
return new Date().toISOString().replace('T', ' ').replace('Z', '');
|
|
554
1598
|
}
|
|
1599
|
+
function emitConversationSyncEvents(conversationId, events, opts) {
|
|
1600
|
+
const revision = typeof opts?.revision === 'number'
|
|
1601
|
+
? opts.revision
|
|
1602
|
+
: data.bumpConversationSyncRevision(conversationId, opts?.updatedAt || undefined);
|
|
1603
|
+
const updatedAt = opts?.updatedAt || localTimestamp();
|
|
1604
|
+
for (const event of events) {
|
|
1605
|
+
(0, chat_sync_1.emitChatSyncEvent)({
|
|
1606
|
+
change: event.change,
|
|
1607
|
+
conversationId,
|
|
1608
|
+
messageId: event.messageId ?? null,
|
|
1609
|
+
jobId: event.jobId ?? null,
|
|
1610
|
+
jobStatus: event.jobStatus ?? null,
|
|
1611
|
+
revision,
|
|
1612
|
+
updatedAt,
|
|
1613
|
+
source: event.source ?? 'agent.local',
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
return revision;
|
|
1617
|
+
}
|
|
1618
|
+
function emitDeletedConversationSyncEvent(conversation) {
|
|
1619
|
+
(0, chat_sync_1.emitChatSyncEvent)({
|
|
1620
|
+
change: 'conversation.deleted',
|
|
1621
|
+
conversationId: conversation.id,
|
|
1622
|
+
revision: Number(conversation.sync_revision || 0) + 1,
|
|
1623
|
+
projectId: conversation.project_id ?? null,
|
|
1624
|
+
topicId: data.getPrimaryTopicIdForConversation(conversation.id),
|
|
1625
|
+
updatedAt: localTimestamp(),
|
|
1626
|
+
source: 'agent.local',
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
555
1629
|
const MAX_LOCAL_CHAT_JOBS = 3;
|
|
556
1630
|
const runningChatJobControllers = new Map();
|
|
557
1631
|
const finalizeCancelledChatJobMessage = (job) => {
|
|
@@ -607,22 +1681,29 @@ function startLocalServer(opts) {
|
|
|
607
1681
|
await handlers.onDone?.({});
|
|
608
1682
|
}
|
|
609
1683
|
};
|
|
610
|
-
const runQueuedChatJobs = async () => {
|
|
611
|
-
if (isConnectedMode())
|
|
1684
|
+
const runQueuedChatJobs = async (opts) => {
|
|
1685
|
+
if (isConnectedMode() && !opts?.force)
|
|
612
1686
|
return;
|
|
613
1687
|
while (runningChatJobControllers.size < MAX_LOCAL_CHAT_JOBS) {
|
|
614
|
-
const
|
|
1688
|
+
const runningKeys = new Set(data.listRunningChatJobs(MAX_LOCAL_CHAT_JOBS + 20).map((job) => `${job.conversation_id}::${job.bot_id}`));
|
|
1689
|
+
const next = data
|
|
1690
|
+
.listQueuedChatJobs(50)
|
|
1691
|
+
.find((job) => !runningKeys.has(`${job.conversation_id}::${job.bot_id}`));
|
|
615
1692
|
if (!next)
|
|
616
1693
|
return;
|
|
617
1694
|
if (runningChatJobControllers.has(next.id))
|
|
618
1695
|
return;
|
|
619
1696
|
const controller = new AbortController();
|
|
620
1697
|
runningChatJobControllers.set(next.id, controller);
|
|
1698
|
+
const runningAt = localTimestamp();
|
|
621
1699
|
data.updateChatJob(next.id, {
|
|
622
1700
|
status: 'running',
|
|
623
|
-
startedAt:
|
|
1701
|
+
startedAt: runningAt,
|
|
624
1702
|
error: null,
|
|
625
1703
|
});
|
|
1704
|
+
emitConversationSyncEvents(next.conversation_id, [
|
|
1705
|
+
{ change: 'job.running', jobId: next.id, jobStatus: 'running' },
|
|
1706
|
+
], { updatedAt: runningAt });
|
|
626
1707
|
void (async () => {
|
|
627
1708
|
try {
|
|
628
1709
|
const job = data.getChatJob(next.id);
|
|
@@ -646,6 +1727,7 @@ function startLocalServer(opts) {
|
|
|
646
1727
|
message: userMessage?.content || '',
|
|
647
1728
|
botId: job.bot_id,
|
|
648
1729
|
skipUserMessage: true,
|
|
1730
|
+
attachments: Array.isArray(requestPayload?.attachments) ? requestPayload.attachments : undefined,
|
|
649
1731
|
pinnedMessageIds: Array.isArray(requestPayload?.pinnedMessageIds) ? requestPayload.pinnedMessageIds : undefined,
|
|
650
1732
|
topicId: requestPayload?.topicId || undefined,
|
|
651
1733
|
projectId: requestPayload?.projectId || undefined,
|
|
@@ -663,12 +1745,17 @@ function startLocalServer(opts) {
|
|
|
663
1745
|
const latest = data.getChatJob(next.id);
|
|
664
1746
|
if (!latest || latest.status === 'cancelled')
|
|
665
1747
|
return;
|
|
1748
|
+
const completedAt = localTimestamp();
|
|
666
1749
|
data.updateChatJob(next.id, {
|
|
667
1750
|
status: 'completed',
|
|
668
|
-
completedAt
|
|
1751
|
+
completedAt,
|
|
669
1752
|
error: null,
|
|
670
1753
|
});
|
|
671
|
-
data.touchConversationActivity(next.conversation_id);
|
|
1754
|
+
data.touchConversationActivity(next.conversation_id, completedAt);
|
|
1755
|
+
emitConversationSyncEvents(next.conversation_id, [
|
|
1756
|
+
{ change: 'message.updated', messageId: next.assistant_message_id },
|
|
1757
|
+
{ change: 'job.completed', jobId: next.id, jobStatus: 'completed' },
|
|
1758
|
+
], { updatedAt: completedAt });
|
|
672
1759
|
},
|
|
673
1760
|
onError: async (payload) => {
|
|
674
1761
|
const latest = data.getChatJob(next.id);
|
|
@@ -681,12 +1768,17 @@ function startLocalServer(opts) {
|
|
|
681
1768
|
content: existingContent ? `${existingContent}\n\n**Error:** ${errorText}` : `**Error:** ${errorText}`,
|
|
682
1769
|
botId: next.bot_id,
|
|
683
1770
|
});
|
|
1771
|
+
const failedAt = localTimestamp();
|
|
684
1772
|
data.updateChatJob(next.id, {
|
|
685
1773
|
status: 'failed',
|
|
686
1774
|
error: errorText,
|
|
687
|
-
completedAt:
|
|
1775
|
+
completedAt: failedAt,
|
|
688
1776
|
});
|
|
689
|
-
data.touchConversationActivity(next.conversation_id);
|
|
1777
|
+
data.touchConversationActivity(next.conversation_id, failedAt);
|
|
1778
|
+
emitConversationSyncEvents(next.conversation_id, [
|
|
1779
|
+
{ change: 'message.updated', messageId: next.assistant_message_id },
|
|
1780
|
+
{ change: 'job.failed', jobId: next.id, jobStatus: 'failed' },
|
|
1781
|
+
], { updatedAt: failedAt });
|
|
690
1782
|
},
|
|
691
1783
|
});
|
|
692
1784
|
}
|
|
@@ -696,11 +1788,16 @@ function startLocalServer(opts) {
|
|
|
696
1788
|
return;
|
|
697
1789
|
if (latest.status === 'cancelled' || err?.name === 'AbortError') {
|
|
698
1790
|
finalizeCancelledChatJobMessage(latest);
|
|
1791
|
+
const cancelledAt = latest.cancelled_at || localTimestamp();
|
|
699
1792
|
data.updateChatJob(next.id, {
|
|
700
1793
|
status: 'cancelled',
|
|
701
|
-
cancelledAt
|
|
702
|
-
completedAt: latest.completed_at ||
|
|
1794
|
+
cancelledAt,
|
|
1795
|
+
completedAt: latest.completed_at || cancelledAt,
|
|
703
1796
|
});
|
|
1797
|
+
emitConversationSyncEvents(next.conversation_id, [
|
|
1798
|
+
{ change: 'message.updated', messageId: next.assistant_message_id },
|
|
1799
|
+
{ change: 'job.cancelled', jobId: next.id, jobStatus: 'cancelled' },
|
|
1800
|
+
], { updatedAt: cancelledAt });
|
|
704
1801
|
return;
|
|
705
1802
|
}
|
|
706
1803
|
const errorText = err?.message || 'Background chat failed';
|
|
@@ -710,12 +1807,17 @@ function startLocalServer(opts) {
|
|
|
710
1807
|
content: existingContent ? `${existingContent}\n\n**Error:** ${errorText}` : `**Error:** ${errorText}`,
|
|
711
1808
|
botId: next.bot_id,
|
|
712
1809
|
});
|
|
1810
|
+
const failedAt = localTimestamp();
|
|
713
1811
|
data.updateChatJob(next.id, {
|
|
714
1812
|
status: 'failed',
|
|
715
1813
|
error: errorText,
|
|
716
|
-
completedAt:
|
|
1814
|
+
completedAt: failedAt,
|
|
717
1815
|
});
|
|
718
|
-
data.touchConversationActivity(next.conversation_id);
|
|
1816
|
+
data.touchConversationActivity(next.conversation_id, failedAt);
|
|
1817
|
+
emitConversationSyncEvents(next.conversation_id, [
|
|
1818
|
+
{ change: 'message.updated', messageId: next.assistant_message_id },
|
|
1819
|
+
{ change: 'job.failed', jobId: next.id, jobStatus: 'failed' },
|
|
1820
|
+
], { updatedAt: failedAt });
|
|
719
1821
|
}
|
|
720
1822
|
finally {
|
|
721
1823
|
runningChatJobControllers.delete(next.id);
|
|
@@ -745,6 +1847,7 @@ function startLocalServer(opts) {
|
|
|
745
1847
|
conversationId: req.body?.conversationId || undefined,
|
|
746
1848
|
projectId: req.body?.projectId || undefined,
|
|
747
1849
|
botId: req.body?.botId || undefined,
|
|
1850
|
+
attachments: Array.isArray(req.body?.attachments) ? req.body.attachments : undefined,
|
|
748
1851
|
targetAgentId,
|
|
749
1852
|
stream: true,
|
|
750
1853
|
skipUserMessage: !!req.body?.skipUserMessage,
|
|
@@ -1072,6 +2175,10 @@ function startLocalServer(opts) {
|
|
|
1072
2175
|
if (!isCliProvider) {
|
|
1073
2176
|
return res.status(400).json({ error: 'Desktop auth refresh only supports Claude CLI or Codex CLI sessions.' });
|
|
1074
2177
|
}
|
|
2178
|
+
const persisted = persistCliOauthSession(effectiveProviderId, accessToken, refreshToken, expiresAt);
|
|
2179
|
+
if (!persisted) {
|
|
2180
|
+
return res.status(500).json({ error: `Failed to persist ${effectiveProviderId} OAuth credentials locally.` });
|
|
2181
|
+
}
|
|
1075
2182
|
const metadataJson = JSON.stringify({
|
|
1076
2183
|
source: cli ? `desktop-cli:${cli}` : 'desktop-cli',
|
|
1077
2184
|
});
|
|
@@ -1101,6 +2208,12 @@ function startLocalServer(opts) {
|
|
|
1101
2208
|
metadataJson,
|
|
1102
2209
|
});
|
|
1103
2210
|
});
|
|
2211
|
+
if (effectiveProviderId === 'claude-cli') {
|
|
2212
|
+
const closedIdleSessions = (0, local_cli_pty_manager_1.getLocalCliPtySessionManager)().closeIdleClaudeSessions();
|
|
2213
|
+
if (closedIdleSessions > 0) {
|
|
2214
|
+
console.info(`[claude-auth] evicted ${closedIdleSessions} idle Claude PTY session(s) after reauth`);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
1104
2217
|
res.json({ ok: true, updated });
|
|
1105
2218
|
}
|
|
1106
2219
|
catch (err) {
|
|
@@ -1230,10 +2343,22 @@ function startLocalServer(opts) {
|
|
|
1230
2343
|
}
|
|
1231
2344
|
})();
|
|
1232
2345
|
});
|
|
2346
|
+
app.get('/api/cli/model-options', (req, res) => {
|
|
2347
|
+
try {
|
|
2348
|
+
const provider = String(req.query.provider || '').trim();
|
|
2349
|
+
if (provider !== 'claude-cli' && provider !== 'codex-cli') {
|
|
2350
|
+
return res.status(400).json({ error: 'provider must be claude-cli or codex-cli' });
|
|
2351
|
+
}
|
|
2352
|
+
res.json({ provider, models: (0, cli_models_1.getCliModelOptions)(provider) });
|
|
2353
|
+
}
|
|
2354
|
+
catch (err) {
|
|
2355
|
+
res.status(500).json({ error: err.message });
|
|
2356
|
+
}
|
|
2357
|
+
});
|
|
1233
2358
|
app.post('/api/bots', (req, res) => {
|
|
1234
2359
|
(async () => {
|
|
1235
2360
|
try {
|
|
1236
|
-
const { provider, model, name, soulMd, memoryMd, toolsMd, skillsMd, apiKeyEnc, permissionMode, isDefault, roleLabel, roleClass, isActive, priority, isOrchestrator, is_orchestrator } = req.body;
|
|
2361
|
+
const { provider, model, name, soulMd, memoryMd, toolsMd, skillsMd, apiKeyEnc, permissionMode, isDefault, roleLabel, roleClass, orchestrationRoleLabel, orchestration_role_label, orchestrationRoleClass, orchestration_role_class, orchestrationIncludeUserPrompt, orchestration_include_user_prompt, isActive, priority, isOrchestrator, is_orchestrator, color, codexReasoningEffort, codex_reasoning_effort, codexReasoningSummary, codex_reasoning_summary, codexPersonality, codex_personality, codexServiceTier, codex_service_tier, codexSandboxPolicy, codex_sandbox_policy, codexApprovalPolicy, codex_approval_policy, claudeEffortLevel, claude_effort_level, claudeOutputStyle, claude_output_style, claudeFastMode, claude_fast_mode, claudePermissionsJson, claude_permissions_json, orchestrationRolesJson, orchestration_roles_json, } = req.body;
|
|
1237
2362
|
if (!provider || !model || !name) {
|
|
1238
2363
|
return res.status(400).json({ error: 'provider, model, and name are required' });
|
|
1239
2364
|
}
|
|
@@ -1260,10 +2385,28 @@ function startLocalServer(opts) {
|
|
|
1260
2385
|
isDefault,
|
|
1261
2386
|
roleLabel,
|
|
1262
2387
|
roleClass,
|
|
2388
|
+
orchestrationRoleLabel: orchestrationRoleLabel ?? orchestration_role_label,
|
|
2389
|
+
orchestrationRoleClass: orchestrationRoleClass ?? orchestration_role_class,
|
|
2390
|
+
orchestrationIncludeUserPrompt: orchestrationIncludeUserPrompt ?? orchestration_include_user_prompt,
|
|
1263
2391
|
isActive,
|
|
1264
2392
|
priority,
|
|
2393
|
+
color,
|
|
1265
2394
|
isOrchestrator: isOrchestrator ?? is_orchestrator,
|
|
2395
|
+
codexReasoningEffort: codexReasoningEffort ?? codex_reasoning_effort,
|
|
2396
|
+
codexReasoningSummary: codexReasoningSummary ?? codex_reasoning_summary,
|
|
2397
|
+
codexPersonality: codexPersonality ?? codex_personality,
|
|
2398
|
+
codexServiceTier: codexServiceTier ?? codex_service_tier,
|
|
2399
|
+
codexSandboxPolicy: codexSandboxPolicy ?? codex_sandbox_policy,
|
|
2400
|
+
codexApprovalPolicy: codexApprovalPolicy ?? codex_approval_policy,
|
|
2401
|
+
claudeEffortLevel: claudeEffortLevel ?? claude_effort_level,
|
|
2402
|
+
claudeOutputStyle: claudeOutputStyle ?? claude_output_style,
|
|
2403
|
+
claudeFastMode: claudeFastMode ?? claude_fast_mode,
|
|
2404
|
+
claudePermissionsJson: claudePermissionsJson ?? claude_permissions_json,
|
|
2405
|
+
orchestrationRolesJson: normalizeRolesArray(orchestrationRolesJson ?? orchestration_roles_json),
|
|
1266
2406
|
});
|
|
2407
|
+
syncCliBotConfigFiles(profile);
|
|
2408
|
+
reapCliBotSessions(profile.id);
|
|
2409
|
+
watchCliBotConfig(profile);
|
|
1267
2410
|
res.status(201).json(profile);
|
|
1268
2411
|
}
|
|
1269
2412
|
catch (err) {
|
|
@@ -1301,22 +2444,63 @@ function startLocalServer(opts) {
|
|
|
1301
2444
|
fields.roleLabel = b.roleLabel ?? b.role_label;
|
|
1302
2445
|
if (b.roleClass !== undefined || b.role_class !== undefined)
|
|
1303
2446
|
fields.roleClass = b.roleClass ?? b.role_class;
|
|
2447
|
+
if (b.orchestrationRoleLabel !== undefined || b.orchestration_role_label !== undefined)
|
|
2448
|
+
fields.orchestrationRoleLabel = b.orchestrationRoleLabel ?? b.orchestration_role_label;
|
|
2449
|
+
if (b.orchestrationRoleClass !== undefined || b.orchestration_role_class !== undefined)
|
|
2450
|
+
fields.orchestrationRoleClass = b.orchestrationRoleClass ?? b.orchestration_role_class;
|
|
2451
|
+
if (b.orchestrationIncludeUserPrompt !== undefined || b.orchestration_include_user_prompt !== undefined)
|
|
2452
|
+
fields.orchestrationIncludeUserPrompt = b.orchestrationIncludeUserPrompt ?? b.orchestration_include_user_prompt;
|
|
1304
2453
|
if (b.isActive !== undefined || b.is_active !== undefined)
|
|
1305
2454
|
fields.isActive = b.isActive ?? b.is_active;
|
|
1306
2455
|
if (b.priority !== undefined)
|
|
1307
2456
|
fields.priority = b.priority;
|
|
2457
|
+
if (b.color !== undefined)
|
|
2458
|
+
fields.color = b.color;
|
|
2459
|
+
if (b.purposeMd !== undefined || b.purpose_md !== undefined)
|
|
2460
|
+
fields.purposeMd = b.purposeMd ?? b.purpose_md;
|
|
1308
2461
|
if (b.showThinking !== undefined || b.show_thinking !== undefined)
|
|
1309
2462
|
fields.showThinking = b.showThinking ?? b.show_thinking;
|
|
1310
2463
|
if (b.isOrchestrator !== undefined || b.is_orchestrator !== undefined)
|
|
1311
2464
|
fields.isOrchestrator = b.isOrchestrator ?? b.is_orchestrator;
|
|
2465
|
+
if (b.codexReasoningEffort !== undefined || b.codex_reasoning_effort !== undefined)
|
|
2466
|
+
fields.codexReasoningEffort = b.codexReasoningEffort ?? b.codex_reasoning_effort;
|
|
2467
|
+
if (b.codexReasoningSummary !== undefined || b.codex_reasoning_summary !== undefined)
|
|
2468
|
+
fields.codexReasoningSummary = b.codexReasoningSummary ?? b.codex_reasoning_summary;
|
|
2469
|
+
if (b.codexPersonality !== undefined || b.codex_personality !== undefined)
|
|
2470
|
+
fields.codexPersonality = b.codexPersonality ?? b.codex_personality;
|
|
2471
|
+
if (b.codexServiceTier !== undefined || b.codex_service_tier !== undefined)
|
|
2472
|
+
fields.codexServiceTier = b.codexServiceTier ?? b.codex_service_tier;
|
|
2473
|
+
if (b.codexSandboxPolicy !== undefined || b.codex_sandbox_policy !== undefined)
|
|
2474
|
+
fields.codexSandboxPolicy = b.codexSandboxPolicy ?? b.codex_sandbox_policy;
|
|
2475
|
+
if (b.codexApprovalPolicy !== undefined || b.codex_approval_policy !== undefined)
|
|
2476
|
+
fields.codexApprovalPolicy = b.codexApprovalPolicy ?? b.codex_approval_policy;
|
|
2477
|
+
if (b.claudeEffortLevel !== undefined || b.claude_effort_level !== undefined)
|
|
2478
|
+
fields.claudeEffortLevel = b.claudeEffortLevel ?? b.claude_effort_level;
|
|
2479
|
+
if (b.claudeOutputStyle !== undefined || b.claude_output_style !== undefined)
|
|
2480
|
+
fields.claudeOutputStyle = b.claudeOutputStyle ?? b.claude_output_style;
|
|
2481
|
+
if (b.claudeFastMode !== undefined || b.claude_fast_mode !== undefined)
|
|
2482
|
+
fields.claudeFastMode = b.claudeFastMode ?? b.claude_fast_mode;
|
|
2483
|
+
if (b.claudePermissionsJson !== undefined || b.claude_permissions_json !== undefined)
|
|
2484
|
+
fields.claudePermissionsJson = b.claudePermissionsJson ?? b.claude_permissions_json;
|
|
2485
|
+
if (b.orchestrationRolesJson !== undefined || b.orchestration_roles_json !== undefined) {
|
|
2486
|
+
fields.orchestrationRolesJson = normalizeRolesArray(b.orchestrationRolesJson ?? b.orchestration_roles_json);
|
|
2487
|
+
}
|
|
1312
2488
|
if (isConnectedMode()) {
|
|
1313
2489
|
const runtime = (0, server_runtime_1.getRuntimeConnectionConfig)();
|
|
1314
2490
|
const auth = await getHydratedDesktopAuth();
|
|
1315
2491
|
return res.json(await (0, server_adapter_1.updateServerBot)(auth, runtime, req.params.id, fields));
|
|
1316
2492
|
}
|
|
2493
|
+
const previous = data.getAgentProfile(req.params.id);
|
|
1317
2494
|
const updated = data.updateAgentProfile(req.params.id, fields);
|
|
1318
2495
|
if (!updated)
|
|
1319
2496
|
return res.status(404).json({ error: 'Not found' });
|
|
2497
|
+
syncCliBotConfigFiles(updated);
|
|
2498
|
+
reapCliBotSessions(updated.id);
|
|
2499
|
+
watchCliBotConfig(updated);
|
|
2500
|
+
if (previous && previous.id !== updated.id) {
|
|
2501
|
+
reapCliBotSessions(previous.id);
|
|
2502
|
+
closeCliBotConfigWatcher(previous.id);
|
|
2503
|
+
}
|
|
1320
2504
|
res.json(updated);
|
|
1321
2505
|
}
|
|
1322
2506
|
catch (err) {
|
|
@@ -1352,6 +2536,8 @@ function startLocalServer(opts) {
|
|
|
1352
2536
|
const deleted = data.deleteAgentProfile(req.params.id);
|
|
1353
2537
|
if (!deleted)
|
|
1354
2538
|
return res.status(404).json({ error: 'Not found' });
|
|
2539
|
+
closeCliBotConfigWatcher(req.params.id);
|
|
2540
|
+
reapCliBotSessions(req.params.id);
|
|
1355
2541
|
res.json({ ok: true });
|
|
1356
2542
|
}
|
|
1357
2543
|
catch (err) {
|
|
@@ -1373,18 +2559,52 @@ function startLocalServer(opts) {
|
|
|
1373
2559
|
return res.status(404).json({ error: 'Not found' });
|
|
1374
2560
|
const convCount = data.countConversations(req.params.id);
|
|
1375
2561
|
const messageCount = data.countMessagesForBot(req.params.id);
|
|
2562
|
+
const now = new Date();
|
|
2563
|
+
const todayStart = new Date(now);
|
|
2564
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
2565
|
+
const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
2566
|
+
const twoWeeksAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000);
|
|
2567
|
+
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
2568
|
+
const decisionCount = data.countDecisionsForBot(req.params.id);
|
|
2569
|
+
const messagesToday = data.countMessagesForBotSince(req.params.id, todayStart.toISOString());
|
|
2570
|
+
const convsThisWeek = data.countBotConversationsUpdatedBetween(req.params.id, oneWeekAgo.toISOString());
|
|
2571
|
+
const convsPrevWeek = data.countBotConversationsUpdatedBetween(req.params.id, twoWeeksAgo.toISOString(), oneWeekAgo.toISOString());
|
|
2572
|
+
const tokenUsage = data.sumBotTokenUsageSince(req.params.id, monthStart.toISOString());
|
|
2573
|
+
const activeDays = data.countBotActiveDays(req.params.id);
|
|
2574
|
+
const createdAt = profile.created_at || now.toISOString();
|
|
2575
|
+
const daysSinceCreation = Math.max(1, Math.ceil((now.getTime() - new Date(createdAt).getTime()) / (24 * 60 * 60 * 1000)));
|
|
2576
|
+
const uptimePercent = Math.min(100, Math.round((activeDays / daysSinceCreation) * 100));
|
|
2577
|
+
const convTrend = convsThisWeek > convsPrevWeek ? 'up' : convsThisWeek < convsPrevWeek ? 'down' : 'flat';
|
|
2578
|
+
const estimatedCost = Math.round(tokenUsage * 0.000003 * 100) / 100;
|
|
1376
2579
|
const recentConversations = data.listBotConversationActivity(req.params.id, 8).map((conversation) => ({
|
|
1377
2580
|
id: conversation.id,
|
|
1378
|
-
agent_id: conversation.agent_id,
|
|
1379
2581
|
title: conversation.title,
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
2582
|
+
updatedAt: conversation.updated_at,
|
|
2583
|
+
createdAt: conversation.created_at,
|
|
2584
|
+
messageCount: conversation.message_count,
|
|
2585
|
+
botMessageCount: conversation.bot_message_count,
|
|
2586
|
+
botLastMessageAt: conversation.bot_last_message_at,
|
|
2587
|
+
projectName: conversation.project_name,
|
|
1386
2588
|
}));
|
|
1387
|
-
|
|
2589
|
+
const lastActivityAt = recentConversations[0]?.botLastMessageAt || recentConversations[0]?.updatedAt || null;
|
|
2590
|
+
res.json({
|
|
2591
|
+
botId: req.params.id,
|
|
2592
|
+
botName: profile.name || profile.id,
|
|
2593
|
+
status: profile.is_active === 0 ? 'inactive' : 'active',
|
|
2594
|
+
createdAt,
|
|
2595
|
+
totalConversations: convCount,
|
|
2596
|
+
totalMessages: messageCount,
|
|
2597
|
+
totalDecisions: decisionCount,
|
|
2598
|
+
messagesToday,
|
|
2599
|
+
lastActivityAt,
|
|
2600
|
+
uptimePercent,
|
|
2601
|
+
avgResponseTime: null,
|
|
2602
|
+
convTrend,
|
|
2603
|
+
tokenUsage,
|
|
2604
|
+
estimatedCost,
|
|
2605
|
+
recentConversations,
|
|
2606
|
+
profile,
|
|
2607
|
+
});
|
|
1388
2608
|
}
|
|
1389
2609
|
catch (err) {
|
|
1390
2610
|
res.status(500).json({ error: err.message });
|
|
@@ -1423,7 +2643,7 @@ function startLocalServer(opts) {
|
|
|
1423
2643
|
app.post('/api/conversations', (req, res) => {
|
|
1424
2644
|
(async () => {
|
|
1425
2645
|
try {
|
|
1426
|
-
const { agentId, title, source, projectId, projectName } = req.body;
|
|
2646
|
+
const { agentId, botIds, initialBotId, title, source, projectId, projectName, topicId } = req.body;
|
|
1427
2647
|
if (isConnectedMode()) {
|
|
1428
2648
|
const runtime = (0, server_runtime_1.getRuntimeConnectionConfig)();
|
|
1429
2649
|
const auth = await getHydratedDesktopAuth();
|
|
@@ -1438,12 +2658,27 @@ function startLocalServer(opts) {
|
|
|
1438
2658
|
...(projectName ? { project_name: projectName } : {}),
|
|
1439
2659
|
});
|
|
1440
2660
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
2661
|
+
const normalizedBotIds = Array.isArray(botIds)
|
|
2662
|
+
? botIds.map((value) => String(value || '').trim()).filter(Boolean)
|
|
2663
|
+
: [];
|
|
2664
|
+
const resolvedInitialBotId = String(initialBotId || agentId || normalizedBotIds[0] || '').trim();
|
|
2665
|
+
if (!resolvedInitialBotId)
|
|
2666
|
+
return res.status(400).json({ error: 'agentId or botIds is required' });
|
|
2667
|
+
const conv = data.createConversation(resolvedInitialBotId, title, source, {
|
|
1444
2668
|
projectId: projectId ?? null,
|
|
1445
2669
|
projectName: projectName ?? null,
|
|
2670
|
+
botIds: normalizedBotIds,
|
|
2671
|
+
initialBotId: resolvedInitialBotId,
|
|
1446
2672
|
});
|
|
2673
|
+
if (topicId) {
|
|
2674
|
+
try {
|
|
2675
|
+
data.upsertConversationTopicSegment(conv.id, String(topicId));
|
|
2676
|
+
}
|
|
2677
|
+
catch { /* best-effort */ }
|
|
2678
|
+
}
|
|
2679
|
+
emitConversationSyncEvents(conv.id, [
|
|
2680
|
+
{ change: 'conversation.created' },
|
|
2681
|
+
]);
|
|
1447
2682
|
res.status(201).json(conv);
|
|
1448
2683
|
}
|
|
1449
2684
|
catch (err) {
|
|
@@ -1488,9 +2723,14 @@ function startLocalServer(opts) {
|
|
|
1488
2723
|
const auth = await getHydratedDesktopAuth();
|
|
1489
2724
|
return res.json(await (0, server_adapter_1.deleteServerConversation)(auth, runtime, req.params.id));
|
|
1490
2725
|
}
|
|
2726
|
+
const existing = data.getConversation(req.params.id);
|
|
2727
|
+
if (!existing)
|
|
2728
|
+
return res.status(404).json({ error: 'Not found' });
|
|
2729
|
+
closeCliSessionsForConversation(req.params.id, 'conversation_deleted');
|
|
1491
2730
|
const deleted = data.deleteConversation(req.params.id);
|
|
1492
2731
|
if (!deleted)
|
|
1493
2732
|
return res.status(404).json({ error: 'Not found' });
|
|
2733
|
+
emitDeletedConversationSyncEvent(existing);
|
|
1494
2734
|
res.json({ ok: true });
|
|
1495
2735
|
}
|
|
1496
2736
|
catch (err) {
|
|
@@ -1510,6 +2750,9 @@ function startLocalServer(opts) {
|
|
|
1510
2750
|
if (!conv)
|
|
1511
2751
|
return res.status(404).json({ error: 'Not found' });
|
|
1512
2752
|
data.updateConversation(req.params.id, req.body);
|
|
2753
|
+
emitConversationSyncEvents(req.params.id, [
|
|
2754
|
+
{ change: 'conversation.updated' },
|
|
2755
|
+
]);
|
|
1513
2756
|
res.json(data.getConversation(req.params.id));
|
|
1514
2757
|
}
|
|
1515
2758
|
catch (err) {
|
|
@@ -1614,6 +2857,12 @@ function startLocalServer(opts) {
|
|
|
1614
2857
|
const auth = await getHydratedDesktopAuth();
|
|
1615
2858
|
return res.json(await (0, server_adapter_1.deleteServerProject)(auth, runtime, req.params.id));
|
|
1616
2859
|
}
|
|
2860
|
+
const projectConversationIds = data.listConversations({ limit: 100000 })
|
|
2861
|
+
.filter((conv) => conv.project_id === req.params.id)
|
|
2862
|
+
.map((conv) => conv.id);
|
|
2863
|
+
for (const conversationId of projectConversationIds) {
|
|
2864
|
+
closeCliSessionsForConversation(conversationId, 'project_deleted');
|
|
2865
|
+
}
|
|
1617
2866
|
const deleted = data.deleteProject(req.params.id);
|
|
1618
2867
|
if (!deleted)
|
|
1619
2868
|
return res.status(404).json({ error: 'Not found' });
|
|
@@ -1777,6 +3026,10 @@ function startLocalServer(opts) {
|
|
|
1777
3026
|
const auth = await getHydratedDesktopAuth();
|
|
1778
3027
|
return res.json(await (0, server_adapter_1.deleteServerTopic)(auth, runtime, req.params.id));
|
|
1779
3028
|
}
|
|
3029
|
+
const topic = data.getTopic(req.params.id);
|
|
3030
|
+
for (const segment of topic?.segments || []) {
|
|
3031
|
+
closeCliSessionsForConversation(segment.conversation_id, 'topic_deleted');
|
|
3032
|
+
}
|
|
1780
3033
|
const deleted = data.deleteTopic(req.params.id);
|
|
1781
3034
|
if (!deleted)
|
|
1782
3035
|
return res.status(404).json({ error: 'Not found' });
|
|
@@ -1998,6 +3251,9 @@ function startLocalServer(opts) {
|
|
|
1998
3251
|
const botId = req.body?.botId ? String(req.body.botId) : undefined;
|
|
1999
3252
|
const agentName = req.body?.agentName ? String(req.body.agentName) : undefined;
|
|
2000
3253
|
const msg = data.addMessage(req.params.id, role, content, model, undefined, botId, agentName);
|
|
3254
|
+
emitConversationSyncEvents(req.params.id, [
|
|
3255
|
+
{ change: 'message.created', messageId: msg.id },
|
|
3256
|
+
]);
|
|
2001
3257
|
res.status(201).json(msg);
|
|
2002
3258
|
}
|
|
2003
3259
|
catch (err) {
|
|
@@ -2055,6 +3311,9 @@ function startLocalServer(opts) {
|
|
|
2055
3311
|
});
|
|
2056
3312
|
if (!updated)
|
|
2057
3313
|
return res.status(404).json({ error: 'Message not found' });
|
|
3314
|
+
emitConversationSyncEvents(updated.conversation_id, [
|
|
3315
|
+
{ change: 'message.updated', messageId: updated.id },
|
|
3316
|
+
]);
|
|
2058
3317
|
res.json(updated);
|
|
2059
3318
|
}
|
|
2060
3319
|
catch (err) {
|
|
@@ -2201,11 +3460,18 @@ function startLocalServer(opts) {
|
|
|
2201
3460
|
app.post('/api/todo/:id/worker-complete', (req, res) => {
|
|
2202
3461
|
try {
|
|
2203
3462
|
const taskId = Number(req.params.id);
|
|
2204
|
-
const { outputSummary, artifactRefs, handoffPrompt, insertTask, expectedVersion, expectedLastUpdated, } = req.body || {};
|
|
3463
|
+
const { outputSummary, artifactRefs, handoffPrompt, qaResult, findings, requiresFixTask, insertTask, expectedVersion, expectedLastUpdated, } = req.body || {};
|
|
3464
|
+
const rawQaResult = qaResult === undefined ? undefined : String(qaResult).trim().toLowerCase();
|
|
3465
|
+
const normalizedQaResult = rawQaResult === 'pass' || rawQaResult === 'fail' || rawQaResult === 'not_applicable'
|
|
3466
|
+
? rawQaResult
|
|
3467
|
+
: undefined;
|
|
2205
3468
|
const result = data.completeTodoTaskByWorker(taskId, {
|
|
2206
3469
|
outputSummary: outputSummary === undefined ? undefined : String(outputSummary),
|
|
2207
3470
|
artifactRefs: Array.isArray(artifactRefs) ? artifactRefs : undefined,
|
|
2208
3471
|
handoffPrompt: handoffPrompt === undefined ? undefined : String(handoffPrompt),
|
|
3472
|
+
qaResult: normalizedQaResult,
|
|
3473
|
+
findings: Array.isArray(findings) ? findings.map((v) => String(v || '').trim()).filter(Boolean) : undefined,
|
|
3474
|
+
requiresFixTask: requiresFixTask === undefined ? undefined : Boolean(requiresFixTask),
|
|
2209
3475
|
insertTask: insertTask ? {
|
|
2210
3476
|
title: String(insertTask.title || ''),
|
|
2211
3477
|
prompt: String(insertTask.prompt || ''),
|
|
@@ -2367,19 +3633,19 @@ function startLocalServer(opts) {
|
|
|
2367
3633
|
return res.json(result.messages);
|
|
2368
3634
|
}
|
|
2369
3635
|
if (hasDirectRange) {
|
|
2370
|
-
return res.json(data.getMessagesInRange(req.params.id, startSeq, endSeq));
|
|
3636
|
+
return res.json(serializeMessageHistoryForRequest(req, data.getMessagesInRange(req.params.id, startSeq, endSeq)));
|
|
2371
3637
|
}
|
|
2372
3638
|
if (beforeSeq > 0) {
|
|
2373
3639
|
// Backward paging: get N rounds or messages before given seq, returned in ASC order
|
|
2374
3640
|
const msgs = rounds > 0
|
|
2375
3641
|
? data.getMessageRoundsBefore(req.params.id, beforeSeq, rounds)
|
|
2376
3642
|
: data.getMessagesBefore(req.params.id, beforeSeq, limit);
|
|
2377
|
-
res.json(msgs);
|
|
3643
|
+
res.json(serializeMessageHistoryForRequest(req, msgs));
|
|
2378
3644
|
}
|
|
2379
3645
|
else {
|
|
2380
3646
|
const offset = parseInt(req.query.offset, 10) || 0;
|
|
2381
3647
|
const msgs = data.getMessages(req.params.id, { limit, offset });
|
|
2382
|
-
res.json(msgs);
|
|
3648
|
+
res.json(serializeMessageHistoryForRequest(req, msgs));
|
|
2383
3649
|
}
|
|
2384
3650
|
}
|
|
2385
3651
|
catch (err) {
|
|
@@ -2437,90 +3703,51 @@ function startLocalServer(opts) {
|
|
|
2437
3703
|
res.status(500).json({ error: err.message });
|
|
2438
3704
|
}
|
|
2439
3705
|
});
|
|
3706
|
+
// ─── Chat job gateway (shared by POST + GET endpoints) ────
|
|
3707
|
+
const chatGateway = new local_chat_execution_1.LocalChatExecution({
|
|
3708
|
+
runQueuedChatJobs,
|
|
3709
|
+
runningChatJobControllers,
|
|
3710
|
+
finalizeCancelledChatJobMessage,
|
|
3711
|
+
persistInlineAttachments,
|
|
3712
|
+
});
|
|
2440
3713
|
app.post('/api/chat/jobs', async (req, res) => {
|
|
2441
3714
|
try {
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
return res.status(
|
|
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
|
-
}
|
|
3715
|
+
const isRelay = req.headers?.['x-funolio-relay'] === '1';
|
|
3716
|
+
if (isConnectedMode() && !isRelay) {
|
|
3717
|
+
const runtime = (0, server_runtime_1.getRuntimeConnectionConfig)();
|
|
3718
|
+
const auth = await getHydratedDesktopAuth();
|
|
3719
|
+
const result = await (0, server_adapter_1.createServerChatJob)(auth, runtime, req.body || {});
|
|
3720
|
+
return res.status(201).json(result);
|
|
2490
3721
|
}
|
|
2491
|
-
const
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
3722
|
+
const result = await chatGateway.createJob(req.body || {});
|
|
3723
|
+
const canonicalResponse = {
|
|
3724
|
+
conversationId: result.conversationId,
|
|
3725
|
+
jobs: result.jobs.map((j) => ({ id: j.id, botId: j.botId, status: j.status })),
|
|
3726
|
+
};
|
|
3727
|
+
if (isRelay) {
|
|
3728
|
+
// In connected mode, the dep call inside chatGateway.createJob() is
|
|
3729
|
+
// a no-op (isConnectedMode() guard). Force-run the queue so the
|
|
3730
|
+
// relay-created job actually starts local execution.
|
|
3731
|
+
runQueuedChatJobs({ force: true });
|
|
3732
|
+
console.log(JSON.stringify({
|
|
3733
|
+
ts: new Date().toISOString(),
|
|
3734
|
+
level: 'info',
|
|
3735
|
+
event: 'JOB_CREATED_VIA_RELAY',
|
|
3736
|
+
jobId: result.jobs[0]?.id,
|
|
3737
|
+
conversationId: result.conversationId,
|
|
3738
|
+
}));
|
|
2498
3739
|
}
|
|
2499
|
-
|
|
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
|
-
});
|
|
3740
|
+
res.status(201).json(canonicalResponse);
|
|
2521
3741
|
}
|
|
2522
3742
|
catch (err) {
|
|
2523
|
-
|
|
3743
|
+
const msg = err.message || '';
|
|
3744
|
+
if (msg === 'message is required' || msg.startsWith('No bot configured')) {
|
|
3745
|
+
return res.status(400).json({ error: msg });
|
|
3746
|
+
}
|
|
3747
|
+
if (msg.includes('already has a pending response')) {
|
|
3748
|
+
return res.status(409).json({ error: msg });
|
|
3749
|
+
}
|
|
3750
|
+
res.status(500).json({ error: msg });
|
|
2524
3751
|
}
|
|
2525
3752
|
});
|
|
2526
3753
|
app.post('/api/chat/jobs/:id/cancel', async (req, res) => {
|
|
@@ -2542,19 +3769,114 @@ function startLocalServer(opts) {
|
|
|
2542
3769
|
controller.abort();
|
|
2543
3770
|
}
|
|
2544
3771
|
else {
|
|
2545
|
-
finalizeCancelledChatJobMessage(job);
|
|
2546
|
-
data.touchConversationActivity(job.conversation_id);
|
|
3772
|
+
finalizeCancelledChatJobMessage(job);
|
|
3773
|
+
data.touchConversationActivity(job.conversation_id, ts);
|
|
3774
|
+
emitConversationSyncEvents(job.conversation_id, [
|
|
3775
|
+
{ change: 'message.updated', messageId: job.assistant_message_id },
|
|
3776
|
+
{ change: 'job.cancelled', jobId: job.id, jobStatus: 'cancelled' },
|
|
3777
|
+
], { updatedAt: ts });
|
|
3778
|
+
}
|
|
3779
|
+
res.json({ ok: true, status: 'cancelled' });
|
|
3780
|
+
}
|
|
3781
|
+
catch (err) {
|
|
3782
|
+
res.status(500).json({ error: err.message });
|
|
3783
|
+
}
|
|
3784
|
+
});
|
|
3785
|
+
// ─── Chat job GET endpoints ────────────────────────────────
|
|
3786
|
+
app.get('/api/chat/jobs', async (req, res) => {
|
|
3787
|
+
try {
|
|
3788
|
+
const conversationId = req.query?.conversationId;
|
|
3789
|
+
if (!conversationId || typeof conversationId !== 'string') {
|
|
3790
|
+
return res.status(400).json({ error: 'conversationId query parameter is required' });
|
|
3791
|
+
}
|
|
3792
|
+
const result = await chatGateway.listJobs(conversationId);
|
|
3793
|
+
res.json(result);
|
|
3794
|
+
}
|
|
3795
|
+
catch (err) {
|
|
3796
|
+
res.status(500).json({ error: err.message });
|
|
3797
|
+
}
|
|
3798
|
+
});
|
|
3799
|
+
app.get('/api/chat/jobs/:id', async (req, res) => {
|
|
3800
|
+
try {
|
|
3801
|
+
const result = await chatGateway.getJobStatus(req.params.id);
|
|
3802
|
+
res.json(result);
|
|
3803
|
+
}
|
|
3804
|
+
catch (err) {
|
|
3805
|
+
if (err.message === 'Job not found') {
|
|
3806
|
+
return res.status(404).json({ error: 'Job not found' });
|
|
3807
|
+
}
|
|
3808
|
+
res.status(500).json({ error: err.message });
|
|
3809
|
+
}
|
|
3810
|
+
});
|
|
3811
|
+
app.get('/api/chat/jobs/:id/stream', async (req, res) => {
|
|
3812
|
+
try {
|
|
3813
|
+
res.writeHead(200, {
|
|
3814
|
+
'Content-Type': 'text/event-stream',
|
|
3815
|
+
'Cache-Control': 'no-cache',
|
|
3816
|
+
'Connection': 'keep-alive',
|
|
3817
|
+
});
|
|
3818
|
+
let closed = false;
|
|
3819
|
+
req.on('close', () => { closed = true; });
|
|
3820
|
+
for await (const event of chatGateway.streamJob(req.params.id)) {
|
|
3821
|
+
if (closed)
|
|
3822
|
+
break;
|
|
3823
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
3824
|
+
}
|
|
3825
|
+
if (!closed) {
|
|
3826
|
+
res.write('data: [DONE]\n\n');
|
|
3827
|
+
res.end();
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
catch (err) {
|
|
3831
|
+
if (!res.headersSent) {
|
|
3832
|
+
if (err.message === 'Job not found') {
|
|
3833
|
+
return res.status(404).json({ error: 'Job not found' });
|
|
3834
|
+
}
|
|
3835
|
+
res.status(500).json({ error: err.message });
|
|
3836
|
+
}
|
|
3837
|
+
else {
|
|
3838
|
+
res.write(`data: ${JSON.stringify({ type: 'error', error: err.message })}\n\n`);
|
|
3839
|
+
res.write('data: [DONE]\n\n');
|
|
3840
|
+
res.end();
|
|
2547
3841
|
}
|
|
2548
|
-
res.json({ ok: true, status: 'cancelled' });
|
|
2549
|
-
}
|
|
2550
|
-
catch (err) {
|
|
2551
|
-
res.status(500).json({ error: err.message });
|
|
2552
3842
|
}
|
|
2553
3843
|
});
|
|
2554
3844
|
// ─── Chat (SSE streaming) ──────────────────────────────────
|
|
3845
|
+
app.get('/api/chat/sync/stream', async (req, res) => {
|
|
3846
|
+
res.writeHead(200, {
|
|
3847
|
+
'Content-Type': 'text/event-stream',
|
|
3848
|
+
'Cache-Control': 'no-cache',
|
|
3849
|
+
'Connection': 'keep-alive',
|
|
3850
|
+
});
|
|
3851
|
+
res.write(`event: ready\ndata: ${JSON.stringify({ ok: true })}\n\n`);
|
|
3852
|
+
const heartbeat = setInterval(() => {
|
|
3853
|
+
try {
|
|
3854
|
+
res.write(`event: ping\ndata: ${JSON.stringify({ ts: Date.now() })}\n\n`);
|
|
3855
|
+
}
|
|
3856
|
+
catch {
|
|
3857
|
+
// Connection cleanup handled below.
|
|
3858
|
+
}
|
|
3859
|
+
}, 15000);
|
|
3860
|
+
const unsubscribe = (0, chat_sync_1.subscribeChatSync)((event) => {
|
|
3861
|
+
try {
|
|
3862
|
+
res.write(`event: sync\ndata: ${JSON.stringify(event)}\n\n`);
|
|
3863
|
+
}
|
|
3864
|
+
catch {
|
|
3865
|
+
// Connection cleanup handled below.
|
|
3866
|
+
}
|
|
3867
|
+
});
|
|
3868
|
+
req.on('close', () => {
|
|
3869
|
+
clearInterval(heartbeat);
|
|
3870
|
+
unsubscribe();
|
|
3871
|
+
res.end();
|
|
3872
|
+
});
|
|
3873
|
+
});
|
|
2555
3874
|
app.post('/api/conversations/:id/chat-job/cancel', async (req, res) => {
|
|
2556
3875
|
try {
|
|
2557
|
-
const
|
|
3876
|
+
const requestedBotId = String(req.body?.botId || '').trim();
|
|
3877
|
+
const latestJob = requestedBotId
|
|
3878
|
+
? data.getLatestConversationBotChatJob(req.params.id, requestedBotId)
|
|
3879
|
+
: data.getLatestConversationChatJob(req.params.id);
|
|
2558
3880
|
if (!latestJob)
|
|
2559
3881
|
return res.status(404).json({ error: 'Chat job not found' });
|
|
2560
3882
|
if (latestJob.status === 'completed' || latestJob.status === 'failed' || latestJob.status === 'cancelled') {
|
|
@@ -2572,7 +3894,11 @@ function startLocalServer(opts) {
|
|
|
2572
3894
|
}
|
|
2573
3895
|
else {
|
|
2574
3896
|
finalizeCancelledChatJobMessage(latestJob);
|
|
2575
|
-
data.touchConversationActivity(latestJob.conversation_id);
|
|
3897
|
+
data.touchConversationActivity(latestJob.conversation_id, ts);
|
|
3898
|
+
emitConversationSyncEvents(latestJob.conversation_id, [
|
|
3899
|
+
{ change: 'message.updated', messageId: latestJob.assistant_message_id },
|
|
3900
|
+
{ change: 'job.cancelled', jobId: latestJob.id, jobStatus: 'cancelled' },
|
|
3901
|
+
], { updatedAt: ts });
|
|
2576
3902
|
void runQueuedChatJobs();
|
|
2577
3903
|
}
|
|
2578
3904
|
res.json({ ok: true, status: 'cancelled', jobId: latestJob.id });
|
|
@@ -2590,15 +3916,44 @@ function startLocalServer(opts) {
|
|
|
2590
3916
|
routeAbortController.abort();
|
|
2591
3917
|
};
|
|
2592
3918
|
req.on('close', abortOnClientClose);
|
|
3919
|
+
res.on('close', abortOnClientClose);
|
|
2593
3920
|
try {
|
|
2594
|
-
let { conversationId, message, botId, skipUserMessage, pinnedMessageIds, topicId, projectId, workflowTemplateId, orchestrationEnabled, chatJobId, assistantMessageId, persistAssistantPlaceholder, } = req.body;
|
|
3921
|
+
let { conversationId, message, botId, skipUserMessage, pinnedMessageIds, topicId, projectId, workflowTemplateId, orchestrationEnabled, chatJobId, assistantMessageId, persistAssistantPlaceholder, botIds, attachments, } = req.body;
|
|
2595
3922
|
if (!message)
|
|
2596
3923
|
return res.status(400).json({ error: 'message is required' });
|
|
3924
|
+
const requestedBotIds = [
|
|
3925
|
+
typeof botId === 'string' ? botId : '',
|
|
3926
|
+
...(Array.isArray(botIds) ? botIds : []),
|
|
3927
|
+
]
|
|
3928
|
+
.map((value) => String(value || '').trim())
|
|
3929
|
+
.filter((value, index, list) => !!value && list.indexOf(value) === index);
|
|
3930
|
+
const resolvedBotId = requestedBotIds[0] || null;
|
|
3931
|
+
const normalizedOrchestrationEnabled = typeof orchestrationEnabled === 'boolean'
|
|
3932
|
+
? orchestrationEnabled
|
|
3933
|
+
: typeof req.body?.orchestrate === 'boolean'
|
|
3934
|
+
? req.body.orchestrate
|
|
3935
|
+
: true;
|
|
3936
|
+
// ─── token.txt §6: bench-prefix tagging ─────────────────────────
|
|
3937
|
+
// Detect [bench:<id>] at the start of the user message OR
|
|
3938
|
+
// X-Test-Run-Id header. If present, strip the prefix from the
|
|
3939
|
+
// message (so the LLM never sees it / token counts aren't
|
|
3940
|
+
// inflated) and remember testRunId + turnIndex for the
|
|
3941
|
+
// LlmUsageLog row written after the LLM call returns.
|
|
3942
|
+
const __benchHeader = String(req.headers?.['x-test-run-id'] || '').trim();
|
|
3943
|
+
const __turnIndexHeader = String(req.headers?.['x-test-turn-index'] || '').trim();
|
|
3944
|
+
const __turnIndexParsed = __turnIndexHeader ? parseInt(__turnIndexHeader, 10) : null;
|
|
3945
|
+
const __benchResult = (0, bench_prefix_1.extractBenchPrefix)(typeof message === 'string' ? message : '');
|
|
3946
|
+
const __testRunId = __benchHeader || __benchResult.testRunId || null;
|
|
3947
|
+
if (__benchResult.testRunId && typeof message === 'string') {
|
|
3948
|
+
message = __benchResult.cleanMessage;
|
|
3949
|
+
}
|
|
3950
|
+
const __benchUserMessage = typeof message === 'string' ? message : null;
|
|
3951
|
+
// ─── end bench-prefix block ─────────────────────────────────────
|
|
2597
3952
|
if (await relayConnectedChat(req, res)) {
|
|
2598
3953
|
return;
|
|
2599
3954
|
}
|
|
2600
3955
|
// Resolve bot
|
|
2601
|
-
let profile =
|
|
3956
|
+
let profile = resolvedBotId ? data.getAgentProfile(resolvedBotId) : data.getDefaultAgentProfile();
|
|
2602
3957
|
if (!profile) {
|
|
2603
3958
|
// Auto-create a default profile from the DB-backed provider connection if available.
|
|
2604
3959
|
const providerConnection = data.listProviderConnections().find((conn) => conn.access_mode === 'cli' || !!conn.api_key_enc);
|
|
@@ -2614,7 +3969,11 @@ function startLocalServer(opts) {
|
|
|
2614
3969
|
if (!profile)
|
|
2615
3970
|
return res.status(400).json({ error: 'No bot configured. Create one first.' });
|
|
2616
3971
|
}
|
|
3972
|
+
const incomingAttachments = parseChatAttachmentInputs(attachments);
|
|
3973
|
+
const shouldUseOrchestratorMode = normalizedOrchestrationEnabled !== false && (data.isClerkOrchestratorEnabled() || (0, orchestrator_profile_1.isOrchestratorProfile)(profile));
|
|
2617
3974
|
// Resolve or create conversation
|
|
3975
|
+
let conversationCreated = false;
|
|
3976
|
+
let turnStartSyncRevision = null;
|
|
2618
3977
|
let convId = conversationId;
|
|
2619
3978
|
if (!convId) {
|
|
2620
3979
|
// Create with empty title so auto-title can fill it in after first response
|
|
@@ -2626,8 +3985,14 @@ function startLocalServer(opts) {
|
|
|
2626
3985
|
const requestedProjectId = (projectId ? String(projectId) : null) || topicProjectId;
|
|
2627
3986
|
const conv = data.createConversation(profile.id, '', 'local', {
|
|
2628
3987
|
projectId: requestedProjectId,
|
|
3988
|
+
botIds: requestedBotIds.length > 0 ? requestedBotIds : [profile.id],
|
|
3989
|
+
initialBotId: profile.id,
|
|
2629
3990
|
});
|
|
2630
3991
|
convId = conv.id;
|
|
3992
|
+
conversationCreated = true;
|
|
3993
|
+
}
|
|
3994
|
+
if (convId) {
|
|
3995
|
+
data.syncConversationParticipants(convId, requestedBotIds.length > 0 ? requestedBotIds : [profile.id], { initialBotId: profile.id, replace: true });
|
|
2631
3996
|
}
|
|
2632
3997
|
// Link conversation to topic if topicId provided
|
|
2633
3998
|
if (topicId && convId) {
|
|
@@ -2703,8 +4068,12 @@ function startLocalServer(opts) {
|
|
|
2703
4068
|
};
|
|
2704
4069
|
// Save user message (skip if multi-bot call where first bot already saved it)
|
|
2705
4070
|
let savedUserMessage = null;
|
|
4071
|
+
let persistedAttachments = [];
|
|
2706
4072
|
if (!skipUserMessage) {
|
|
2707
|
-
|
|
4073
|
+
persistedAttachments = incomingAttachments.length > 0
|
|
4074
|
+
? persistInlineAttachments(convId, incomingAttachments)
|
|
4075
|
+
: [];
|
|
4076
|
+
savedUserMessage = data.addMessage(convId, 'user', message, undefined, undefined, undefined, undefined, undefined, persistedAttachments.length > 0 ? JSON.stringify(persistedAttachments) : null);
|
|
2708
4077
|
(0, context_window_1.incrementTurnCount)(convId);
|
|
2709
4078
|
const convForPolicy = data.getConversation(convId);
|
|
2710
4079
|
const effectiveProjectId = projectId ? String(projectId) : (convForPolicy?.project_id || undefined);
|
|
@@ -2729,24 +4098,232 @@ function startLocalServer(opts) {
|
|
|
2729
4098
|
});
|
|
2730
4099
|
}
|
|
2731
4100
|
}
|
|
2732
|
-
if (
|
|
4101
|
+
if (savedUserMessage) {
|
|
4102
|
+
turnStartSyncRevision = emitConversationSyncEvents(convId, [
|
|
4103
|
+
...(conversationCreated ? [{ change: 'conversation.created' }] : []),
|
|
4104
|
+
{ change: 'message.created', messageId: savedUserMessage.id },
|
|
4105
|
+
]);
|
|
4106
|
+
}
|
|
4107
|
+
const interceptedSlashCommand = (0, slash_commands_1.classifySlashCommand)(message, profile?.provider);
|
|
4108
|
+
if (interceptedSlashCommand) {
|
|
4109
|
+
if (!assistantMessageId && persistAssistantPlaceholder === true) {
|
|
4110
|
+
const placeholder = data.addMessage(convId, 'assistant', '', buildConfiguredMessageModel(profile), undefined, profile?.id || undefined, profile?.name || undefined);
|
|
4111
|
+
assistantMessageId = placeholder.id;
|
|
4112
|
+
turnStartSyncRevision = emitConversationSyncEvents(convId, [
|
|
4113
|
+
{ change: 'message.created', messageId: placeholder.id },
|
|
4114
|
+
], turnStartSyncRevision ? { revision: turnStartSyncRevision } : undefined);
|
|
4115
|
+
}
|
|
4116
|
+
res.writeHead(200, {
|
|
4117
|
+
'Content-Type': 'text/event-stream',
|
|
4118
|
+
'Cache-Control': 'no-cache',
|
|
4119
|
+
'Connection': 'keep-alive',
|
|
4120
|
+
'X-Conversation-Id': convId,
|
|
4121
|
+
});
|
|
4122
|
+
const sendSlashEvent = (event, payload) => {
|
|
4123
|
+
if (responseEnded)
|
|
4124
|
+
return;
|
|
4125
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
4126
|
+
};
|
|
4127
|
+
sendSlashEvent('meta', {
|
|
4128
|
+
conversationId: convId,
|
|
4129
|
+
botId: profile.id,
|
|
4130
|
+
assistantMessageId: assistantMessageId || null,
|
|
4131
|
+
});
|
|
4132
|
+
if (interceptedSlashCommand.kind === 'rotate') {
|
|
4133
|
+
data.upsertCliSessionEpoch({
|
|
4134
|
+
conversationId: convId,
|
|
4135
|
+
botId: profile.id,
|
|
4136
|
+
provider: profile.provider,
|
|
4137
|
+
sessionId: '',
|
|
4138
|
+
epochTurnCount: 0,
|
|
4139
|
+
lastInputTokens: 0,
|
|
4140
|
+
lastOutputTokens: 0,
|
|
4141
|
+
resetReason: interceptedSlashCommand.resetReason,
|
|
4142
|
+
epochStartedAt: localTimestamp(),
|
|
4143
|
+
lastUsedAt: localTimestamp(),
|
|
4144
|
+
});
|
|
4145
|
+
reapCliBotSessions(profile.id);
|
|
4146
|
+
if (assistantMessageId) {
|
|
4147
|
+
data.updateMessage(assistantMessageId, {
|
|
4148
|
+
content: interceptedSlashCommand.summary,
|
|
4149
|
+
model: buildConfiguredMessageModel(profile),
|
|
4150
|
+
botId: profile.id,
|
|
4151
|
+
agentName: profile.name,
|
|
4152
|
+
});
|
|
4153
|
+
emitConversationSyncEvents(convId, [
|
|
4154
|
+
{ change: 'message.updated', messageId: assistantMessageId },
|
|
4155
|
+
]);
|
|
4156
|
+
}
|
|
4157
|
+
sendSlashEvent('done', {
|
|
4158
|
+
content: interceptedSlashCommand.summary,
|
|
4159
|
+
});
|
|
4160
|
+
responseEnded = true;
|
|
4161
|
+
res.end();
|
|
4162
|
+
return;
|
|
4163
|
+
}
|
|
4164
|
+
if (interceptedSlashCommand.kind === 'settings-ui') {
|
|
4165
|
+
sendSlashEvent('slash_command', {
|
|
4166
|
+
kind: interceptedSlashCommand.kind,
|
|
4167
|
+
command: interceptedSlashCommand.command,
|
|
4168
|
+
provider: interceptedSlashCommand.provider,
|
|
4169
|
+
botId: profile.id,
|
|
4170
|
+
conversationId: convId,
|
|
4171
|
+
assistantMessageId: assistantMessageId || null,
|
|
4172
|
+
summary: interceptedSlashCommand.summary,
|
|
4173
|
+
});
|
|
4174
|
+
if (assistantMessageId) {
|
|
4175
|
+
data.updateMessage(assistantMessageId, {
|
|
4176
|
+
content: interceptedSlashCommand.summary,
|
|
4177
|
+
model: buildConfiguredMessageModel(profile),
|
|
4178
|
+
botId: profile.id,
|
|
4179
|
+
agentName: profile.name,
|
|
4180
|
+
});
|
|
4181
|
+
emitConversationSyncEvents(convId, [
|
|
4182
|
+
{ change: 'message.updated', messageId: assistantMessageId },
|
|
4183
|
+
]);
|
|
4184
|
+
}
|
|
4185
|
+
sendSlashEvent('done', {
|
|
4186
|
+
content: interceptedSlashCommand.summary,
|
|
4187
|
+
});
|
|
4188
|
+
responseEnded = true;
|
|
4189
|
+
res.end();
|
|
4190
|
+
return;
|
|
4191
|
+
}
|
|
4192
|
+
const workspaceForSlashCommand = (() => {
|
|
4193
|
+
const conversation = data.getConversation(convId);
|
|
4194
|
+
const project = conversation?.project_id ? data.getProject(conversation.project_id) : undefined;
|
|
4195
|
+
const workspacePath = project?.folder?.trim() || undefined;
|
|
4196
|
+
return workspacePath && fs.existsSync(workspacePath) ? workspacePath : opts.projectDir;
|
|
4197
|
+
})();
|
|
4198
|
+
try {
|
|
4199
|
+
const passthroughResult = await (0, local_cli_pty_manager_1.getLocalCliPtySessionManager)().runPassthroughCommand({
|
|
4200
|
+
conversationId: convId,
|
|
4201
|
+
botId: profile.id,
|
|
4202
|
+
provider: interceptedSlashCommand.provider,
|
|
4203
|
+
botSettings: {
|
|
4204
|
+
claude: interceptedSlashCommand.provider === 'claude-cli'
|
|
4205
|
+
? {
|
|
4206
|
+
model: profile.model,
|
|
4207
|
+
effortLevel: profile.claude_effort_level,
|
|
4208
|
+
outputStyle: profile.claude_output_style,
|
|
4209
|
+
fastMode: profile.claude_fast_mode === 1,
|
|
4210
|
+
permissionsJson: profile.claude_permissions_json,
|
|
4211
|
+
}
|
|
4212
|
+
: null,
|
|
4213
|
+
codex: interceptedSlashCommand.provider === 'codex-cli'
|
|
4214
|
+
? {
|
|
4215
|
+
model: profile.model,
|
|
4216
|
+
reasoningEffort: profile.codex_reasoning_effort,
|
|
4217
|
+
personality: profile.codex_personality,
|
|
4218
|
+
serviceTier: profile.codex_service_tier,
|
|
4219
|
+
sandboxPolicy: profile.codex_sandbox_policy,
|
|
4220
|
+
approvalPolicy: profile.codex_approval_policy,
|
|
4221
|
+
}
|
|
4222
|
+
: null,
|
|
4223
|
+
},
|
|
4224
|
+
cwd: workspaceForSlashCommand,
|
|
4225
|
+
command: interceptedSlashCommand.command,
|
|
4226
|
+
abortSignal: routeAbortController.signal,
|
|
4227
|
+
onRawChunk: async (chunk) => {
|
|
4228
|
+
sendSlashEvent('terminal_chunk', {
|
|
4229
|
+
text: chunk,
|
|
4230
|
+
provider: interceptedSlashCommand.provider,
|
|
4231
|
+
botId: profile.id,
|
|
4232
|
+
agentName: profile.name,
|
|
4233
|
+
});
|
|
4234
|
+
},
|
|
4235
|
+
});
|
|
4236
|
+
const finalContent = passthroughResult.content.trim() || interceptedSlashCommand.summary;
|
|
4237
|
+
if (assistantMessageId) {
|
|
4238
|
+
data.updateMessage(assistantMessageId, {
|
|
4239
|
+
content: finalContent,
|
|
4240
|
+
model: buildConfiguredMessageModel(profile),
|
|
4241
|
+
botId: profile.id,
|
|
4242
|
+
agentName: profile.name,
|
|
4243
|
+
});
|
|
4244
|
+
emitConversationSyncEvents(convId, [
|
|
4245
|
+
{ change: 'message.updated', messageId: assistantMessageId },
|
|
4246
|
+
]);
|
|
4247
|
+
}
|
|
4248
|
+
sendSlashEvent('done', {
|
|
4249
|
+
content: finalContent,
|
|
4250
|
+
});
|
|
4251
|
+
responseEnded = true;
|
|
4252
|
+
res.end();
|
|
4253
|
+
return;
|
|
4254
|
+
}
|
|
4255
|
+
catch (slashErr) {
|
|
4256
|
+
const errorMessage = slashErr?.message || `Failed to run ${interceptedSlashCommand.command}`;
|
|
4257
|
+
if (assistantMessageId) {
|
|
4258
|
+
data.updateMessage(assistantMessageId, {
|
|
4259
|
+
content: `Error: ${errorMessage}`,
|
|
4260
|
+
model: buildConfiguredMessageModel(profile),
|
|
4261
|
+
botId: profile.id,
|
|
4262
|
+
agentName: profile.name,
|
|
4263
|
+
});
|
|
4264
|
+
emitConversationSyncEvents(convId, [
|
|
4265
|
+
{ change: 'message.updated', messageId: assistantMessageId },
|
|
4266
|
+
]);
|
|
4267
|
+
}
|
|
4268
|
+
sendSlashEvent('error', { error: errorMessage });
|
|
4269
|
+
responseEnded = true;
|
|
4270
|
+
res.end();
|
|
4271
|
+
return;
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
const storedAttachmentsForTurn = resolveStoredAttachmentsForTurn({
|
|
4275
|
+
conversationId: convId,
|
|
4276
|
+
message,
|
|
4277
|
+
skipUserMessage: !!skipUserMessage,
|
|
4278
|
+
chatJobId: chatJobId ? String(chatJobId) : null,
|
|
4279
|
+
persistedAttachments,
|
|
4280
|
+
});
|
|
4281
|
+
const conversationForAttachments = data.getConversation(convId);
|
|
4282
|
+
const projectForAttachments = conversationForAttachments?.project_id ? data.getProject(conversationForAttachments.project_id) : undefined;
|
|
4283
|
+
const attachmentWorkspacePath = projectForAttachments?.folder?.trim() || opts.projectDir;
|
|
4284
|
+
const attachmentSummaryBlock = storedAttachmentsForTurn.length > 0
|
|
4285
|
+
? await summarizeStoredAttachmentsForTextRuntime({
|
|
4286
|
+
attachments: storedAttachmentsForTurn,
|
|
4287
|
+
workspacePath: attachmentWorkspacePath,
|
|
4288
|
+
userPrompt: message,
|
|
4289
|
+
preferredProfile: profile,
|
|
4290
|
+
})
|
|
4291
|
+
: '';
|
|
4292
|
+
const runtimeUserPrompt = attachmentSummaryBlock ? `${message}${attachmentSummaryBlock}` : message;
|
|
4293
|
+
if (!assistantMessageId && persistAssistantPlaceholder === true && !shouldUseOrchestratorMode) {
|
|
2733
4294
|
const placeholder = data.addMessage(convId, 'assistant', '', buildConfiguredMessageModel(profile), undefined, profile?.id || undefined, profile?.name || undefined);
|
|
2734
4295
|
assistantMessageId = placeholder.id;
|
|
4296
|
+
turnStartSyncRevision = emitConversationSyncEvents(convId, [
|
|
4297
|
+
{ change: 'message.created', messageId: placeholder.id },
|
|
4298
|
+
], turnStartSyncRevision ? { revision: turnStartSyncRevision } : undefined);
|
|
2735
4299
|
}
|
|
2736
4300
|
// ─── Orchestrator Mode Branch ─────────────────────────
|
|
2737
|
-
const shouldUseOrchestratorMode = orchestrationEnabled !== false && (0, orchestrator_profile_1.isOrchestratorProfile)(profile);
|
|
2738
4301
|
if (shouldUseOrchestratorMode) {
|
|
2739
|
-
const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: 'local_desktop' });
|
|
2740
|
-
if (!clerk) {
|
|
2741
|
-
// Fix #2: Do not silently fall through to direct chat — return a clear error
|
|
2742
|
-
return res.status(400).json({
|
|
2743
|
-
error: 'Orchestrator mode requires a clerk model to be configured. Please add a provider connection in Settings.',
|
|
2744
|
-
});
|
|
2745
|
-
}
|
|
2746
4302
|
const { OrchestratorAgent } = require('./orchestrator');
|
|
4303
|
+
const { buildLocalDesktopOrchestratorRuntime } = require('./orchestrator');
|
|
2747
4304
|
const { getWorkflowEngine } = require('./workflow-engine');
|
|
2748
4305
|
const workflowEngine = getWorkflowEngine(opts.projectDir, 'local_desktop');
|
|
2749
|
-
|
|
4306
|
+
// Resolve orchestrator runtime independently of request/conversation bot identity.
|
|
4307
|
+
// The request botId is a participant hint, never runtime authority.
|
|
4308
|
+
// Orchestrator must be explicitly designated: clerk → is_orchestrator=1 bot.
|
|
4309
|
+
// No fallback to is_default=1 (that's the hardcoded-Ben trap — made the
|
|
4310
|
+
// default worker bot double as the orchestrator and caused dispatch bugs).
|
|
4311
|
+
const orchestratorBotIdHint = data.isClerkOrchestratorEnabled()
|
|
4312
|
+
? null
|
|
4313
|
+
: (data.getOrchestratorBot()?.id || null);
|
|
4314
|
+
const explicitOrchestratorBot = orchestratorBotIdHint
|
|
4315
|
+
? data.getAgentProfile(orchestratorBotIdHint)
|
|
4316
|
+
: null;
|
|
4317
|
+
let orchestratorRuntime;
|
|
4318
|
+
try {
|
|
4319
|
+
orchestratorRuntime = buildLocalDesktopOrchestratorRuntime(explicitOrchestratorBot);
|
|
4320
|
+
}
|
|
4321
|
+
catch (runtimeErr) {
|
|
4322
|
+
return res.status(400).json({
|
|
4323
|
+
error: runtimeErr?.message || 'Orchestrator mode is not configured. Mark a bot with is_orchestrator=1 or enable Clerk orchestration.',
|
|
4324
|
+
});
|
|
4325
|
+
}
|
|
4326
|
+
const orchestrator = new OrchestratorAgent(orchestratorRuntime, workflowEngine);
|
|
2750
4327
|
// Resolve effective project ID from request or existing conversation
|
|
2751
4328
|
const conv = data.getConversation(convId);
|
|
2752
4329
|
const effectiveProjectId = projectId ? String(projectId) : (conv?.project_id || undefined);
|
|
@@ -2755,6 +4332,44 @@ function startLocalServer(opts) {
|
|
|
2755
4332
|
const shortTitle = message.slice(0, 60).replace(/\n/g, ' ').trim();
|
|
2756
4333
|
data.updateConversation(convId, { title: shortTitle || 'New Chat' });
|
|
2757
4334
|
}
|
|
4335
|
+
// Create an empty assistant placeholder message BEFORE the
|
|
4336
|
+
// orchestrator runs. All worker progress activities recorded via
|
|
4337
|
+
// recordWorkerActivity (below) attach to this message_id, so the
|
|
4338
|
+
// UI can render the in-flight orchestrator card immediately — no
|
|
4339
|
+
// more blank chat pane when a user navigates away mid-run and
|
|
4340
|
+
// comes back. (april19fixes.txt item 6a.) At turn end we UPDATE
|
|
4341
|
+
// the placeholder with the final orchestrator response instead
|
|
4342
|
+
// of addMessage'ing a new row.
|
|
4343
|
+
//
|
|
4344
|
+
// Identity note (Codex QA 2026-04-19 on 308cbcfd): when Clerk
|
|
4345
|
+
// is the orchestrator, the placeholder must be attributed to
|
|
4346
|
+
// the Orchestrator, not to the selected request bot (profile).
|
|
4347
|
+
// Otherwise the in-flight card renders under the wrong bot
|
|
4348
|
+
// identity mid-run, and an errored turn permanently persists
|
|
4349
|
+
// under the wrong bot. At turn end we update agentName/botId/
|
|
4350
|
+
// model with the authoritative final values from
|
|
4351
|
+
// orchestrator.getLastResponseMeta().
|
|
4352
|
+
const placeholderClerkMode = data.isClerkOrchestratorEnabled();
|
|
4353
|
+
const placeholderAgentName = placeholderClerkMode
|
|
4354
|
+
? 'Orchestrator'
|
|
4355
|
+
: (profile?.name || undefined);
|
|
4356
|
+
const placeholderBotId = placeholderClerkMode
|
|
4357
|
+
? undefined
|
|
4358
|
+
: (profile?.id || undefined);
|
|
4359
|
+
const placeholderClerkConfig = placeholderClerkMode
|
|
4360
|
+
? data.getResolvedClerkConfigInfo()
|
|
4361
|
+
: null;
|
|
4362
|
+
const placeholderModel = placeholderClerkMode
|
|
4363
|
+
? ((placeholderClerkConfig?.model || '').trim() || 'Orchestrator')
|
|
4364
|
+
: buildConfiguredMessageModel(profile);
|
|
4365
|
+
if (!assistantMessageId) {
|
|
4366
|
+
const placeholder = data.addMessage(convId, 'assistant', '', placeholderModel, undefined, placeholderBotId, placeholderAgentName);
|
|
4367
|
+
assistantMessageId = placeholder.id;
|
|
4368
|
+
activityErrorContext.messageId = String(assistantMessageId);
|
|
4369
|
+
turnStartSyncRevision = emitConversationSyncEvents(convId, [
|
|
4370
|
+
{ change: 'message.created', messageId: placeholder.id },
|
|
4371
|
+
], turnStartSyncRevision ? { revision: turnStartSyncRevision } : undefined);
|
|
4372
|
+
}
|
|
2758
4373
|
// SSE setup — same contract as normal chat path
|
|
2759
4374
|
res.writeHead(200, {
|
|
2760
4375
|
'Content-Type': 'text/event-stream',
|
|
@@ -2769,35 +4384,61 @@ function startLocalServer(opts) {
|
|
|
2769
4384
|
};
|
|
2770
4385
|
let orchestratorRuntimeLabel = '';
|
|
2771
4386
|
let orchestratorRuntimePayload;
|
|
4387
|
+
const clerkSelectedAsOrchestrator = data.isClerkOrchestratorEnabled();
|
|
2772
4388
|
try {
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
4389
|
+
if (clerkSelectedAsOrchestrator) {
|
|
4390
|
+
const clerkConfig = data.getResolvedClerkConfigInfo();
|
|
4391
|
+
const clerkProvider = clerkConfig.provider || profile.provider;
|
|
4392
|
+
const clerkModel = clerkConfig.model || profile.model || null;
|
|
4393
|
+
const clerkConnection = clerkConfig.providerConnectionId
|
|
4394
|
+
? data.getProviderConnection(clerkConfig.providerConnectionId)
|
|
4395
|
+
: data.findProviderConnection(clerkProvider);
|
|
4396
|
+
const clerkRuntimeMode = clerkProvider === 'claude-cli' || clerkProvider === 'codex-cli'
|
|
4397
|
+
? 'subscription-cli'
|
|
4398
|
+
: 'api-key';
|
|
4399
|
+
const clerkRuntimeSource = clerkConnection?.access_mode === 'oauth'
|
|
4400
|
+
? 'oauth-token'
|
|
4401
|
+
: clerkConnection?.access_mode === 'cli'
|
|
4402
|
+
? 'cli-direct'
|
|
4403
|
+
: 'api-key';
|
|
4404
|
+
orchestratorRuntimeLabel = [
|
|
4405
|
+
clerkModel || '',
|
|
4406
|
+
runtimeModeLabel(clerkRuntimeMode, clerkRuntimeSource),
|
|
4407
|
+
].filter(Boolean).join(' | ');
|
|
4408
|
+
orchestratorRuntimePayload = runtimePayloadForDisplay(clerkProvider, clerkModel, clerkRuntimeMode, clerkRuntimeSource);
|
|
4409
|
+
}
|
|
4410
|
+
else {
|
|
4411
|
+
const orchestratorRuntime = await buildChatRuntime(profile);
|
|
4412
|
+
orchestratorRuntimeLabel = [
|
|
4413
|
+
orchestratorRuntime.model || profile.model || '',
|
|
4414
|
+
runtimeModeLabel(orchestratorRuntime.runtimeMode, orchestratorRuntime.runtimeSource),
|
|
4415
|
+
].filter(Boolean).join(' | ');
|
|
4416
|
+
orchestratorRuntimePayload = runtimePayloadForDisplay(profile.provider, orchestratorRuntime.model || profile.model || null, orchestratorRuntime.runtimeMode, orchestratorRuntime.runtimeSource || null);
|
|
4417
|
+
}
|
|
2779
4418
|
}
|
|
2780
4419
|
catch {
|
|
2781
4420
|
orchestratorRuntimeLabel = buildConfiguredMessageModel(profile);
|
|
2782
4421
|
}
|
|
2783
4422
|
sendEvent('meta', {
|
|
2784
4423
|
conversationId: convId,
|
|
4424
|
+
assistantMessageId: assistantMessageId || null,
|
|
2785
4425
|
...(orchestratorRuntimePayload ? { runtime: orchestratorRuntimePayload } : {}),
|
|
2786
4426
|
});
|
|
2787
4427
|
let lastProgressChat = '';
|
|
2788
4428
|
let lastProgressActivity = '';
|
|
2789
4429
|
let selfExecuteStreamed = false;
|
|
2790
|
-
let hasWorkerActivity = false;
|
|
2791
4430
|
try {
|
|
2792
4431
|
let lastInterimMessage = '';
|
|
2793
|
-
const response = await orchestrator.handleUserMessage(
|
|
4432
|
+
const response = await orchestrator.handleUserMessage(runtimeUserPrompt, convId, {
|
|
2794
4433
|
projectDir: opts.projectDir,
|
|
2795
4434
|
projectId: effectiveProjectId,
|
|
4435
|
+
orchestratorBotIdHint,
|
|
4436
|
+
storedAttachments: storedAttachmentsForTurn.length > 0 ? storedAttachmentsForTurn : undefined,
|
|
4437
|
+
abortSignal: routeAbortController.signal,
|
|
2796
4438
|
commandId: `local-${Date.now()}`,
|
|
2797
4439
|
workflowTemplateId: workflowTemplateId || undefined,
|
|
2798
4440
|
onWorkerChunk: (event) => {
|
|
2799
4441
|
if (event.type === 'step_start') {
|
|
2800
|
-
hasWorkerActivity = true;
|
|
2801
4442
|
recordWorkerActivity('worker_step_start', event, {
|
|
2802
4443
|
stepId: event.stepId,
|
|
2803
4444
|
agentName: event.agentName,
|
|
@@ -2816,7 +4457,6 @@ function startLocalServer(opts) {
|
|
|
2816
4457
|
});
|
|
2817
4458
|
}
|
|
2818
4459
|
else if (event.type === 'worker_chunk') {
|
|
2819
|
-
hasWorkerActivity = true;
|
|
2820
4460
|
recordWorkerActivity('worker_chunk', event, {
|
|
2821
4461
|
stepId: event.stepId,
|
|
2822
4462
|
agentName: event.agentName,
|
|
@@ -2836,8 +4476,18 @@ function startLocalServer(opts) {
|
|
|
2836
4476
|
text: event.text,
|
|
2837
4477
|
});
|
|
2838
4478
|
}
|
|
4479
|
+
else if (event.type === 'worker_terminal_chunk') {
|
|
4480
|
+
sendEvent('worker_terminal_chunk', {
|
|
4481
|
+
stepId: event.stepId,
|
|
4482
|
+
botId: resolveWorkerBotId(event.agentName),
|
|
4483
|
+
agentName: event.agentName,
|
|
4484
|
+
description: event.description,
|
|
4485
|
+
stepIndex: event.stepIndex,
|
|
4486
|
+
totalSteps: event.totalSteps,
|
|
4487
|
+
text: event.rawText,
|
|
4488
|
+
});
|
|
4489
|
+
}
|
|
2839
4490
|
else if (event.type === 'worker_tool_call') {
|
|
2840
|
-
hasWorkerActivity = true;
|
|
2841
4491
|
recordWorkerActivity('worker_tool_call', event, {
|
|
2842
4492
|
stepId: event.stepId,
|
|
2843
4493
|
agentName: event.agentName,
|
|
@@ -2862,7 +4512,6 @@ function startLocalServer(opts) {
|
|
|
2862
4512
|
});
|
|
2863
4513
|
}
|
|
2864
4514
|
else if (event.type === 'worker_tool_result') {
|
|
2865
|
-
hasWorkerActivity = true;
|
|
2866
4515
|
recordWorkerActivity('worker_tool_result', event, {
|
|
2867
4516
|
stepId: event.stepId,
|
|
2868
4517
|
agentName: event.agentName,
|
|
@@ -2901,7 +4550,6 @@ function startLocalServer(opts) {
|
|
|
2901
4550
|
sendEvent('chunk', { text: `> [${icon}] ${event.toolName} completed\n` });
|
|
2902
4551
|
}
|
|
2903
4552
|
else if (event.type === 'step_done') {
|
|
2904
|
-
hasWorkerActivity = true;
|
|
2905
4553
|
recordWorkerActivity('worker_step_done', event, {
|
|
2906
4554
|
stepId: event.stepId,
|
|
2907
4555
|
agentName: event.agentName,
|
|
@@ -2938,7 +4586,7 @@ function startLocalServer(opts) {
|
|
|
2938
4586
|
// Progress chatText suppressed — worker card handles all streaming display.
|
|
2939
4587
|
// Main bubble only gets final content via 'done' event.
|
|
2940
4588
|
// Emit interim messages for key orchestrator transitions
|
|
2941
|
-
const interimMessage =
|
|
4589
|
+
const interimMessage = deriveVisibleOrchestratorMessage(status);
|
|
2942
4590
|
if (interimMessage && interimMessage !== lastInterimMessage) {
|
|
2943
4591
|
lastInterimMessage = interimMessage;
|
|
2944
4592
|
recordActivity('orchestrator_interim', { text: interimMessage }, interimMessage);
|
|
@@ -2958,41 +4606,44 @@ function startLocalServer(opts) {
|
|
|
2958
4606
|
// Save O's response (no incrementTurnCount — Fix #1: user message already incremented it)
|
|
2959
4607
|
const responseMeta = orchestrator.getLastResponseMeta();
|
|
2960
4608
|
const finalAgentName = responseMeta?.agentName || 'Orchestrator';
|
|
2961
|
-
const finalBotId = responseMeta?.botId
|
|
4609
|
+
const finalBotId = responseMeta?.botId
|
|
4610
|
+
|| (finalAgentName === 'Orchestrator' && !clerkSelectedAsOrchestrator ? profile.id : undefined);
|
|
2962
4611
|
const finalModelLabel = responseMeta?.modelLabel || orchestratorRuntimeLabel || undefined;
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
data.
|
|
2969
|
-
|
|
2970
|
-
|
|
4612
|
+
// UPDATE the placeholder we created at turn start (april19fixes.txt
|
|
4613
|
+
// item 6a). Activities are already attached via assistantMessageId,
|
|
4614
|
+
// so the final message row completes the in-flight card rather than
|
|
4615
|
+
// creating a second message row.
|
|
4616
|
+
let savedMessage = assistantMessageId
|
|
4617
|
+
? data.updateMessage(assistantMessageId, {
|
|
4618
|
+
content: response,
|
|
4619
|
+
model: finalModelLabel,
|
|
2971
4620
|
botId: finalBotId,
|
|
2972
4621
|
agentName: finalAgentName,
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
});
|
|
4622
|
+
})
|
|
4623
|
+
: null;
|
|
4624
|
+
if (!savedMessage) {
|
|
4625
|
+
savedMessage = data.addMessage(convId, 'assistant', response, finalModelLabel, undefined, finalBotId, finalAgentName);
|
|
2978
4626
|
}
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
data.createMessageActivity({
|
|
2983
|
-
conversationId: convId,
|
|
4627
|
+
emitConversationSyncEvents(convId, [
|
|
4628
|
+
{
|
|
4629
|
+
change: savedMessage.id === assistantMessageId ? 'message.updated' : 'message.created',
|
|
2984
4630
|
messageId: savedMessage.id,
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
4631
|
+
},
|
|
4632
|
+
]);
|
|
4633
|
+
data.attachMessageActivitiesToMessage(activityStreamId, savedMessage.id);
|
|
4634
|
+
data.createMessageActivity({
|
|
4635
|
+
conversationId: convId,
|
|
4636
|
+
messageId: savedMessage.id,
|
|
4637
|
+
botId: finalBotId,
|
|
4638
|
+
agentName: finalAgentName,
|
|
4639
|
+
activityType: 'message',
|
|
4640
|
+
summary: 'Final orchestrator response',
|
|
4641
|
+
payload: { content: response },
|
|
4642
|
+
expiresAt: activityExpiresAt,
|
|
4643
|
+
});
|
|
2993
4644
|
// Emit chunk + done events using the same SSE contract as normal chat
|
|
2994
4645
|
// Skip the final bulk chunk if we already streamed via worker_chunk (execute_self)
|
|
2995
|
-
if (!selfExecuteStreamed
|
|
4646
|
+
if (!selfExecuteStreamed) {
|
|
2996
4647
|
sendEvent('chunk', { text: response });
|
|
2997
4648
|
}
|
|
2998
4649
|
sendEvent('done', {
|
|
@@ -3000,7 +4651,6 @@ function startLocalServer(opts) {
|
|
|
3000
4651
|
content: response,
|
|
3001
4652
|
agentName: finalAgentName,
|
|
3002
4653
|
botId: finalBotId,
|
|
3003
|
-
separateFinalMessage: splitFinalMessage,
|
|
3004
4654
|
...((responseMeta?.modelLabel || orchestratorRuntimePayload)
|
|
3005
4655
|
? {
|
|
3006
4656
|
runtime: {
|
|
@@ -3020,16 +4670,47 @@ function startLocalServer(opts) {
|
|
|
3020
4670
|
(0, local_funnel_1.scheduleFunnelProcessing)(convId);
|
|
3021
4671
|
}
|
|
3022
4672
|
catch (orchErr) {
|
|
3023
|
-
|
|
4673
|
+
if (routeAbortController.signal.aborted || orchErr?.name === 'AbortError') {
|
|
4674
|
+
responseEnded = true;
|
|
4675
|
+
res.end();
|
|
4676
|
+
return;
|
|
4677
|
+
}
|
|
4678
|
+
// Log the full stack so recursion crashes like "Maximum
|
|
4679
|
+
// call stack size exceeded" land in agent.log with the
|
|
4680
|
+
// failing frame. Previously only the message was captured.
|
|
4681
|
+
console.error(chalk_1.default.red(`Orchestrator error: ${orchErr.message}`));
|
|
4682
|
+
if (orchErr?.stack) {
|
|
4683
|
+
console.error(String(orchErr.stack));
|
|
4684
|
+
}
|
|
4685
|
+
recordActivity('error', { error: orchErr.message, stack: orchErr?.stack || null }, orchErr.message);
|
|
4686
|
+
// Populate the placeholder with the error text AND correct
|
|
4687
|
+
// the identity fields so an errored turn doesn't remain
|
|
4688
|
+
// persisted under the wrong bot. Under Clerk orchestration
|
|
4689
|
+
// the placeholder was created with agentName='Orchestrator',
|
|
4690
|
+
// botId=null — those are re-applied here explicitly because
|
|
4691
|
+
// updateMessage with no overrides preserves existing values
|
|
4692
|
+
// but we want to guarantee nothing else mutated them.
|
|
4693
|
+
// (Codex QA 2026-04-19 on 308cbcfd: error-path identity bug.)
|
|
4694
|
+
if (assistantMessageId) {
|
|
4695
|
+
try {
|
|
4696
|
+
data.updateMessage(assistantMessageId, {
|
|
4697
|
+
content: `Error: ${orchErr.message}`,
|
|
4698
|
+
agentName: placeholderAgentName,
|
|
4699
|
+
botId: placeholderBotId,
|
|
4700
|
+
model: placeholderModel,
|
|
4701
|
+
});
|
|
4702
|
+
emitConversationSyncEvents(convId, [
|
|
4703
|
+
{ change: 'message.updated', messageId: assistantMessageId },
|
|
4704
|
+
]);
|
|
4705
|
+
}
|
|
4706
|
+
catch { /* best effort */ }
|
|
4707
|
+
}
|
|
3024
4708
|
sendEvent('error', { type: 'error', error: orchErr.message });
|
|
3025
4709
|
responseEnded = true;
|
|
3026
4710
|
res.end();
|
|
3027
4711
|
}
|
|
3028
4712
|
return;
|
|
3029
4713
|
}
|
|
3030
|
-
// Prompt Contract v1: system carries summary + last 5 turns.
|
|
3031
|
-
// Send only the current user request as the primary user message.
|
|
3032
|
-
const llmMessages = [{ role: 'user', content: message }];
|
|
3033
4714
|
const configuredTz = (data.getSetting('timezone') || '').trim();
|
|
3034
4715
|
const effectiveTimezone = configuredTz && configuredTz.toLowerCase() !== 'system'
|
|
3035
4716
|
? configuredTz
|
|
@@ -3042,101 +4723,9 @@ function startLocalServer(opts) {
|
|
|
3042
4723
|
? new Set(allToolDefs.map((tool) => tool.name))
|
|
3043
4724
|
: expandAllowedToolNames(allToolDefs, configuredBuiltinTools, configuredMcpTools);
|
|
3044
4725
|
const toolDefs = allToolDefs.filter((tool) => allowedToolNames.has(tool.name));
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: 'local_desktop' });
|
|
3049
|
-
if (clerk) {
|
|
3050
|
-
const conv = data.getConversation(convId);
|
|
3051
|
-
const topicTitle = topicId ? data.getTopic(topicId)?.title : undefined;
|
|
3052
|
-
const project = conv?.project_id ? data.getProject(conv.project_id) : undefined;
|
|
3053
|
-
const workspacePath = project?.folder?.trim() || undefined;
|
|
3054
|
-
if (workspacePath && fs.existsSync(workspacePath)) {
|
|
3055
|
-
llmSpawnCwd = workspacePath;
|
|
3056
|
-
}
|
|
3057
|
-
const built = clerk.buildPrompt(message, profile.id, profile, {
|
|
3058
|
-
targetModel: profile.model,
|
|
3059
|
-
conversationId: convId,
|
|
3060
|
-
projectName: conv?.project_name || undefined,
|
|
3061
|
-
projectId: conv?.project_id || undefined,
|
|
3062
|
-
topicTitle: topicTitle || undefined,
|
|
3063
|
-
workspacePath,
|
|
3064
|
-
timezone: effectiveTimezone,
|
|
3065
|
-
includeKeyDecisions: false,
|
|
3066
|
-
availableTools: toolDefs.map((tool) => ({ name: tool.name, description: tool.description })),
|
|
3067
|
-
});
|
|
3068
|
-
systemPrompt = built.systemPrompt;
|
|
3069
|
-
console.log(chalk_1.default.gray(` [clerk] Context: ${built.injectedSummaries} summaries (${built.contextTokensUsed} tokens)`));
|
|
3070
|
-
}
|
|
3071
|
-
else {
|
|
3072
|
-
// Fallback: manual prompt building
|
|
3073
|
-
systemPrompt = '[Bot Identity]\n' + (profile.soul_md
|
|
3074
|
-
|| 'You are an AI assistant running locally. You have access to project files and can execute code.');
|
|
3075
|
-
systemPrompt += '\n\nDo not end with a deferred promise (for example: "Let me check..."). Return a final answer in this turn, or state exactly what is unavailable.';
|
|
3076
|
-
systemPrompt += '\n\nWhen [Project Overview] is present, treat Project/Topic/Workspace values there as authoritative for the current turn and override stale prior-chat claims.';
|
|
3077
|
-
systemPrompt += '\n\n[Response Style]\nWrite in short readable paragraphs. Put a blank line between distinct ideas. Use bullets when listing findings, steps, or issues. Do not return one dense wall of text. For progress updates, keep them compact and clearly separate what you checked, what you found, and what you are doing next.';
|
|
3078
|
-
const convForFallback = data.getConversation(convId);
|
|
3079
|
-
const projectForFallback = convForFallback?.project_id ? data.getProject(convForFallback.project_id) : undefined;
|
|
3080
|
-
const workspaceForFallback = projectForFallback?.folder?.trim();
|
|
3081
|
-
if (workspaceForFallback && fs.existsSync(workspaceForFallback)) {
|
|
3082
|
-
llmSpawnCwd = workspaceForFallback;
|
|
3083
|
-
}
|
|
3084
|
-
const fallbackMeta = [];
|
|
3085
|
-
if (convForFallback?.project_name)
|
|
3086
|
-
fallbackMeta.push(`Project: ${convForFallback.project_name}`);
|
|
3087
|
-
if (topicId) {
|
|
3088
|
-
const fallbackTopic = data.getTopic(topicId);
|
|
3089
|
-
if (fallbackTopic?.title)
|
|
3090
|
-
fallbackMeta.push(`Topic: ${fallbackTopic.title}`);
|
|
3091
|
-
}
|
|
3092
|
-
if (convForFallback?.project_id) {
|
|
3093
|
-
fallbackMeta.push(`Workspace: ${workspaceForFallback || '(project folder not configured)'}`);
|
|
3094
|
-
}
|
|
3095
|
-
if (fallbackMeta.length > 0) {
|
|
3096
|
-
systemPrompt += '\n\n[Project Overview]\n' + fallbackMeta.join('\n');
|
|
3097
|
-
}
|
|
3098
|
-
const effectivePolicy = data.getEffectiveOrchestrationPolicy(convForFallback?.project_id || undefined);
|
|
3099
|
-
systemPrompt += '\n\n' + (0, policy_prompt_1.buildEffectivePolicyPromptSection)(effectivePolicy, {
|
|
3100
|
-
heading: '[Effective Policy]',
|
|
3101
|
-
defaultLine: 'No confirmed special policy is set.',
|
|
3102
|
-
});
|
|
3103
|
-
try {
|
|
3104
|
-
const todoStatus = data.getTodoStatusMarker(convForFallback?.project_id ?? undefined);
|
|
3105
|
-
systemPrompt += `\n\n[TODO Coordination]\nTODO STATUS: ${todoStatus}`;
|
|
3106
|
-
}
|
|
3107
|
-
catch { /* best-effort */ }
|
|
3108
|
-
systemPrompt += '\n\n' + (0, clerk_model_1.buildTodoInstructions)(profile?.name || profile?.id || 'LLM');
|
|
3109
|
-
let hasSummary = false;
|
|
3110
|
-
try {
|
|
3111
|
-
const summaryContext = (0, context_window_1.getPromptContextWindow)(convId, safeguards_1.SAFEGUARDS.CONTEXT_WINDOW_TURNS);
|
|
3112
|
-
if (summaryContext.summary?.summary_text) {
|
|
3113
|
-
const summaryHeader = summaryContext.carriedForward
|
|
3114
|
-
? '[Context Summary (Carried Forward from Previous Conversation in This Topic)]'
|
|
3115
|
-
: '[Context Summary]';
|
|
3116
|
-
systemPrompt += `\n\n${summaryHeader}\n` + summaryContext.summary.summary_text;
|
|
3117
|
-
hasSummary = true;
|
|
3118
|
-
}
|
|
3119
|
-
}
|
|
3120
|
-
catch { /* best-effort */ }
|
|
3121
|
-
try {
|
|
3122
|
-
const turnWindow = hasSummary
|
|
3123
|
-
? safeguards_1.SAFEGUARDS.CONTEXT_WINDOW_TURNS
|
|
3124
|
-
: safeguards_1.SAFEGUARDS.NO_SUMMARY_CONTEXT_WINDOW_TURNS;
|
|
3125
|
-
const promptContext = (0, context_window_1.getPromptContextWindow)(convId, turnWindow);
|
|
3126
|
-
if (promptContext.turns.length > 0) {
|
|
3127
|
-
const turnsHeader = promptContext.carriedForward
|
|
3128
|
-
? `[Recent Messages (Last ${promptContext.turns.length} Turns from Previous Conversation in This Topic)]`
|
|
3129
|
-
: `[Recent Messages (Last ${promptContext.turns.length} Turns)]`;
|
|
3130
|
-
systemPrompt += `\n\n${turnsHeader}\n` + (0, context_window_1.formatTurnsForPrompt)(promptContext.turns);
|
|
3131
|
-
}
|
|
3132
|
-
}
|
|
3133
|
-
catch { /* best-effort */ }
|
|
3134
|
-
}
|
|
3135
|
-
// Resolve LLM runtime.
|
|
3136
|
-
// Desktop local mode intentionally supports only:
|
|
3137
|
-
// - Subscription CLI
|
|
3138
|
-
// - API Key
|
|
3139
|
-
// We do not use subscription-token API routing for local CLI bots.
|
|
4726
|
+
const conversation = data.getConversation(convId);
|
|
4727
|
+
// Resolve LLM runtime early so the local desktop prompt contract can differ
|
|
4728
|
+
// between API/fresh CLI and recurring CLI sessions without affecting server paths.
|
|
3140
4729
|
const runtime = await buildChatRuntime(profile);
|
|
3141
4730
|
let activeProviderName = runtime.providerName;
|
|
3142
4731
|
let activeModelName = runtime.model;
|
|
@@ -3164,6 +4753,133 @@ function startLocalServer(opts) {
|
|
|
3164
4753
|
const cliEpochStartedAt = cliSessionEpochPlan.resumeSessionId
|
|
3165
4754
|
? (cliSessionEpochPlan.existing?.epoch_started_at || localTimestamp())
|
|
3166
4755
|
: localTimestamp();
|
|
4756
|
+
const promptContextWindow = (0, context_window_1.getPromptContextWindow)(convId, safeguards_1.SAFEGUARDS.CONTEXT_WINDOW_TURNS, {
|
|
4757
|
+
targetBotId: profile.id,
|
|
4758
|
+
targetBotName: profile.name,
|
|
4759
|
+
});
|
|
4760
|
+
const skipFreshCliBootstrap = shouldSkipFreshCliBootstrap({
|
|
4761
|
+
providerName: activeProviderName,
|
|
4762
|
+
resumeSessionId: cliSessionEpochPlan.resumeSessionId,
|
|
4763
|
+
promptContextMode: promptContextWindow.mode,
|
|
4764
|
+
});
|
|
4765
|
+
// Session lifecycle logging — tracks invariant between prompt-building
|
|
4766
|
+
// decisions and session runtime reality plus structured telemetry events.
|
|
4767
|
+
if (enableCliSessionEpoch) {
|
|
4768
|
+
const sessionLaunchReason = cliSessionEpochPlan.resumeSessionId
|
|
4769
|
+
? 'resumed'
|
|
4770
|
+
: promptContextWindow.mode === 'new_topic'
|
|
4771
|
+
? 'new_topic/no_context'
|
|
4772
|
+
: cliSessionEpochPlan.resetReason
|
|
4773
|
+
? cliSessionEpochPlan.resetReason
|
|
4774
|
+
: 'fresh_with_bootstrap';
|
|
4775
|
+
console.info(`[chat] session_launch_reason=${sessionLaunchReason} botId=${profile.id} provider=${activeProviderName} mode=${promptContextWindow.mode || 'unknown'} bootstrap=${!!promptContextWindow.allowBootstrap && !skipFreshCliBootstrap}`);
|
|
4776
|
+
console.info(`[chat] cli_session_selected conversationId=${convId} botId=${profile.id} provider=${activeProviderName} selectedSessionId=${cliSessionEpochPlan.resumeSessionId || '(none)'} resetReason=${cliSessionEpochPlan.resetReason || '(none)'}`);
|
|
4777
|
+
if (cliSessionEpochPlan.resumeSessionId) {
|
|
4778
|
+
console.info(`[chat] cli_session_resuming conversationId=${convId} botId=${profile.id} provider=${activeProviderName} sessionId=${cliSessionEpochPlan.resumeSessionId}`);
|
|
4779
|
+
}
|
|
4780
|
+
else if (promptContextWindow.mode === 'new_topic') {
|
|
4781
|
+
console.info(`[chat] cli_session_fresh_without_bootstrap_new_topic conversationId=${convId} botId=${profile.id} provider=${activeProviderName}`);
|
|
4782
|
+
}
|
|
4783
|
+
else if (promptContextWindow.allowBootstrap && !skipFreshCliBootstrap) {
|
|
4784
|
+
console.info(`[chat] cli_session_fresh_bootstrap_applied conversationId=${convId} botId=${profile.id} provider=${activeProviderName}`);
|
|
4785
|
+
}
|
|
4786
|
+
else {
|
|
4787
|
+
console.warn(`[chat] cli_session_fresh_without_bootstrap_unexpected conversationId=${convId} botId=${profile.id} provider=${activeProviderName} mode=${promptContextWindow.mode || 'unknown'}`);
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4790
|
+
const promptContext = resolveLocalDesktopPromptContext({
|
|
4791
|
+
conversationId: convId,
|
|
4792
|
+
conversation,
|
|
4793
|
+
explicitTopicId: topicId,
|
|
4794
|
+
fallbackCwd: opts.projectDir,
|
|
4795
|
+
writeCliHistoryBootstrap: activeIsCliProvider
|
|
4796
|
+
&& !cliSessionEpochPlan.resumeSessionId
|
|
4797
|
+
&& !!promptContextWindow.allowBootstrap
|
|
4798
|
+
&& !skipFreshCliBootstrap,
|
|
4799
|
+
});
|
|
4800
|
+
const llmSpawnCwd = promptContext.llmSpawnCwd;
|
|
4801
|
+
let cliEpochResetReason = cliSessionEpochPlan.resetReason;
|
|
4802
|
+
let directPrompt = buildLocalDesktopDirectPrompt({
|
|
4803
|
+
conversationId: convId,
|
|
4804
|
+
currentBotId: profile.id,
|
|
4805
|
+
currentBotName: profile.name,
|
|
4806
|
+
currentProvider: activeProviderName,
|
|
4807
|
+
userPrompt: supportsNativeImageInput(activeProviderName) ? message : runtimeUserPrompt,
|
|
4808
|
+
soulMd: profile.soul_md || 'You are an AI assistant running locally. You have access to project files and can execute code.',
|
|
4809
|
+
projectName: promptContext.projectName,
|
|
4810
|
+
topicTitle: promptContext.topicTitle,
|
|
4811
|
+
workspacePath: promptContext.workspacePath,
|
|
4812
|
+
timezone: effectiveTimezone,
|
|
4813
|
+
availableTools: toolDefs.map((tool) => ({ name: tool.name, description: tool.description })),
|
|
4814
|
+
isCliRecurring: !!cliSessionEpochPlan.resumeSessionId || skipFreshCliBootstrap,
|
|
4815
|
+
cliHistoryFilePath: promptContext.cliHistoryFilePath,
|
|
4816
|
+
crossBotTurn: promptContextWindow.lastCrossBotTurn,
|
|
4817
|
+
forceInlinePromptContext: activeProviderName === 'codex-cli'
|
|
4818
|
+
&& !!cliSessionEpochPlan.resumeSessionId
|
|
4819
|
+
&& ((conversation?.turn_count || 0) === 0),
|
|
4820
|
+
useCompletionSentinel: resolveDirectCliSessionTransport(activeProviderName, enableCliSessionEpoch, (0, storage_mode_1.isLocalStorageMode)()) === 'pty',
|
|
4821
|
+
});
|
|
4822
|
+
// Recurring CLI sessions rely on session memory for same-bot context, so
|
|
4823
|
+
// inject the last completed cross-bot handoff turn explicitly when the
|
|
4824
|
+
// shared prompt-context policy says it is required.
|
|
4825
|
+
if (activeIsCliProvider && !!cliSessionEpochPlan.resumeSessionId && promptContextWindow.lastCrossBotTurn) {
|
|
4826
|
+
const crossBotBlock = (0, context_window_1.formatCrossBotPreviousTurn)(promptContextWindow.lastCrossBotTurn);
|
|
4827
|
+
directPrompt.userPrompt = `${crossBotBlock}\n\n${directPrompt.userPrompt}`;
|
|
4828
|
+
}
|
|
4829
|
+
let llmMessages = [{
|
|
4830
|
+
role: 'user',
|
|
4831
|
+
content: supportsNativeImageInput(activeProviderName)
|
|
4832
|
+
? buildMessageContentWithStoredAttachments(directPrompt.userPrompt, storedAttachmentsForTurn)
|
|
4833
|
+
: directPrompt.userPrompt,
|
|
4834
|
+
}];
|
|
4835
|
+
let systemPrompt = directPrompt.systemPrompt;
|
|
4836
|
+
let freshCliBootstrapFallbackApplied = false;
|
|
4837
|
+
let freshCliBootstrapFileWritten = false;
|
|
4838
|
+
const applyFreshCliBootstrapFallback = (reason) => {
|
|
4839
|
+
if (freshCliBootstrapFallbackApplied)
|
|
4840
|
+
return;
|
|
4841
|
+
freshCliBootstrapFallbackApplied = true;
|
|
4842
|
+
cliEpochResetReason = reason;
|
|
4843
|
+
const bootstrapHistoryFilePath = promptContextWindow.allowBootstrap
|
|
4844
|
+
? (0, cli_bootstrap_history_1.writeConversationBootstrapHistoryFile)({
|
|
4845
|
+
conversationId: convId,
|
|
4846
|
+
topicId: promptContext.topicId,
|
|
4847
|
+
})
|
|
4848
|
+
: null;
|
|
4849
|
+
freshCliBootstrapFileWritten = !!bootstrapHistoryFilePath;
|
|
4850
|
+
directPrompt = buildLocalDesktopDirectPrompt({
|
|
4851
|
+
conversationId: convId,
|
|
4852
|
+
currentBotId: profile.id,
|
|
4853
|
+
currentBotName: profile.name,
|
|
4854
|
+
currentProvider: activeProviderName,
|
|
4855
|
+
userPrompt: supportsNativeImageInput(activeProviderName) ? message : runtimeUserPrompt,
|
|
4856
|
+
soulMd: profile.soul_md || 'You are an AI assistant running locally. You have access to project files and can execute code.',
|
|
4857
|
+
projectName: promptContext.projectName,
|
|
4858
|
+
topicTitle: promptContext.topicTitle,
|
|
4859
|
+
workspacePath: promptContext.workspacePath,
|
|
4860
|
+
timezone: effectiveTimezone,
|
|
4861
|
+
availableTools: toolDefs.map((tool) => ({ name: tool.name, description: tool.description })),
|
|
4862
|
+
isCliRecurring: false,
|
|
4863
|
+
cliHistoryFilePath: bootstrapHistoryFilePath,
|
|
4864
|
+
crossBotTurn: promptContextWindow.lastCrossBotTurn,
|
|
4865
|
+
useCompletionSentinel: resolveDirectCliSessionTransport(activeProviderName, enableCliSessionEpoch, (0, storage_mode_1.isLocalStorageMode)()) === 'pty',
|
|
4866
|
+
});
|
|
4867
|
+
llmMessages = [{
|
|
4868
|
+
role: 'user',
|
|
4869
|
+
content: supportsNativeImageInput(activeProviderName)
|
|
4870
|
+
? buildMessageContentWithStoredAttachments(directPrompt.userPrompt, storedAttachmentsForTurn)
|
|
4871
|
+
: directPrompt.userPrompt,
|
|
4872
|
+
}];
|
|
4873
|
+
systemPrompt = directPrompt.systemPrompt;
|
|
4874
|
+
console.info(`[chat] session_launch_reason=resume_failed botId=${profile.id} provider=${activeProviderName} bootstrap=${!!bootstrapHistoryFilePath}`);
|
|
4875
|
+
console.warn(`[chat] cli_session_resume_failed conversationId=${convId} botId=${profile.id} provider=${activeProviderName} bootstrap=${!!bootstrapHistoryFilePath}`);
|
|
4876
|
+
if (bootstrapHistoryFilePath) {
|
|
4877
|
+
console.info(`[chat] cli_session_fresh_bootstrap_applied conversationId=${convId} botId=${profile.id} provider=${activeProviderName} bootstrapReason=resume_failed`);
|
|
4878
|
+
}
|
|
4879
|
+
else {
|
|
4880
|
+
console.warn(`[chat] cli_session_fresh_without_bootstrap_unexpected conversationId=${convId} botId=${profile.id} provider=${activeProviderName} bootstrapReason=resume_failed`);
|
|
4881
|
+
}
|
|
4882
|
+
};
|
|
3167
4883
|
if (!activeApiKey) {
|
|
3168
4884
|
return res.status(400).json({ error: `No API key for provider ${profile.provider}. Configure one in Settings.` });
|
|
3169
4885
|
}
|
|
@@ -3175,14 +4891,6 @@ function startLocalServer(opts) {
|
|
|
3175
4891
|
restrictFileAccessToProject: unrestrictedCliProfile ? false : undefined,
|
|
3176
4892
|
abortSignal: routeAbortController.signal,
|
|
3177
4893
|
});
|
|
3178
|
-
const toolManifest = toolDefs
|
|
3179
|
-
.map(t => `- ${t.name}: ${t.description}`)
|
|
3180
|
-
.join('\n');
|
|
3181
|
-
if (toolManifest.trim()) {
|
|
3182
|
-
systemPrompt += unrestrictedCliProfile
|
|
3183
|
-
? '\n\n[Available Tools]\nThe following tools are available in the current runtime:\n' + toolManifest
|
|
3184
|
-
: '\n\n[Available Tools]\nOnly the following tools are enabled for this bot in the current runtime:\n' + toolManifest;
|
|
3185
|
-
}
|
|
3186
4894
|
// Inject pinned messages as context (user-selected cross-bot references)
|
|
3187
4895
|
if (pinnedMessageIds && Array.isArray(pinnedMessageIds) && pinnedMessageIds.length > 0) {
|
|
3188
4896
|
const pinnedLines = [];
|
|
@@ -3199,10 +4907,17 @@ function startLocalServer(opts) {
|
|
|
3199
4907
|
// CLI providers only see the last user message — prepend pinned context there
|
|
3200
4908
|
const lastUserIdx = llmMessages.map(m => m.role).lastIndexOf('user');
|
|
3201
4909
|
if (lastUserIdx >= 0) {
|
|
3202
|
-
llmMessages[lastUserIdx]
|
|
3203
|
-
|
|
3204
|
-
content: `${pinnedBlock}\n---\n${
|
|
3205
|
-
}
|
|
4910
|
+
const existing = llmMessages[lastUserIdx].content;
|
|
4911
|
+
if (typeof existing === 'string') {
|
|
4912
|
+
llmMessages[lastUserIdx] = { ...llmMessages[lastUserIdx], content: `${pinnedBlock}\n---\n${existing}` };
|
|
4913
|
+
}
|
|
4914
|
+
else {
|
|
4915
|
+
// Multimodal content — prepend pinned text as a new text block, keep images intact
|
|
4916
|
+
llmMessages[lastUserIdx] = {
|
|
4917
|
+
...llmMessages[lastUserIdx],
|
|
4918
|
+
content: [{ type: 'text', text: `${pinnedBlock}\n---\n` }, ...existing],
|
|
4919
|
+
};
|
|
4920
|
+
}
|
|
3206
4921
|
}
|
|
3207
4922
|
}
|
|
3208
4923
|
else {
|
|
@@ -3240,33 +4955,6 @@ function startLocalServer(opts) {
|
|
|
3240
4955
|
isApproximate: true,
|
|
3241
4956
|
},
|
|
3242
4957
|
});
|
|
3243
|
-
sendEvent('status', {
|
|
3244
|
-
phase: 'thinking',
|
|
3245
|
-
detail: `Sending request to ${activeProviderName}...`,
|
|
3246
|
-
runtime: runtimePayload(),
|
|
3247
|
-
});
|
|
3248
|
-
recordActivity('status', {
|
|
3249
|
-
phase: 'thinking',
|
|
3250
|
-
detail: `Sending request to ${activeProviderName}...`,
|
|
3251
|
-
runtime: runtimePayload(),
|
|
3252
|
-
}, `Sending request to ${activeProviderName}...`);
|
|
3253
|
-
if (cliSessionEpochPlan.resetReason && enableCliSessionEpoch) {
|
|
3254
|
-
const resetDetail = cliSessionEpochPlan.resetReason === 'turn_limit'
|
|
3255
|
-
? 'Resetting CLI session after reaching the turn limit.'
|
|
3256
|
-
: cliSessionEpochPlan.resetReason === 'token_limit'
|
|
3257
|
-
? 'Resetting CLI session after reaching the context budget.'
|
|
3258
|
-
: 'Resetting CLI session because the runtime changed.';
|
|
3259
|
-
sendEvent('status', {
|
|
3260
|
-
phase: 'thinking',
|
|
3261
|
-
detail: resetDetail,
|
|
3262
|
-
runtime: runtimePayload(),
|
|
3263
|
-
});
|
|
3264
|
-
recordActivity('status', {
|
|
3265
|
-
phase: 'thinking',
|
|
3266
|
-
detail: resetDetail,
|
|
3267
|
-
runtime: runtimePayload(),
|
|
3268
|
-
}, resetDetail);
|
|
3269
|
-
}
|
|
3270
4958
|
let partialPersistedContent = '';
|
|
3271
4959
|
let partialPersistedAt = 0;
|
|
3272
4960
|
const throwIfChatJobCancelled = () => {
|
|
@@ -3306,6 +4994,7 @@ function startLocalServer(opts) {
|
|
|
3306
4994
|
let fullContent = '';
|
|
3307
4995
|
let streamedContent = '';
|
|
3308
4996
|
let streamedAnyChunk = false;
|
|
4997
|
+
let rawCliTranscript = '';
|
|
3309
4998
|
let iteration = 0;
|
|
3310
4999
|
const MAX_ITERATIONS = 10; // Phase 1d: reduced from 20
|
|
3311
5000
|
let totalInputTokens = 0;
|
|
@@ -3316,31 +5005,218 @@ function startLocalServer(opts) {
|
|
|
3316
5005
|
let accumulatedThinking = '';
|
|
3317
5006
|
const thinkingEnabled = !!profile?.show_thinking;
|
|
3318
5007
|
let useInteractiveCliSession = enableCliSessionEpoch;
|
|
3319
|
-
|
|
5008
|
+
const directCliSessionTransport = resolveDirectCliSessionTransport(activeProviderName, useInteractiveCliSession, (0, storage_mode_1.isLocalStorageMode)());
|
|
5009
|
+
const useCodexAppServerInteractive = directCliSessionTransport === 'codex-app-server';
|
|
5010
|
+
const usePtyInteractiveCliSession = directCliSessionTransport === 'pty';
|
|
5011
|
+
const startDetail = `Started response via ${runtimeModeLabel(activeRuntimeMode, activeRuntimeSource) || activeProviderName}`;
|
|
5012
|
+
sendEvent('status', { phase: 'thinking', detail: startDetail });
|
|
5013
|
+
recordActivity('status', { phase: 'thinking', detail: startDetail }, startDetail);
|
|
5014
|
+
if (useCodexAppServerInteractive) {
|
|
5015
|
+
const codexAppServerManager = (0, codex_app_server_manager_1.getCodexAppServerManager)();
|
|
5016
|
+
let appServerAttempt = 0;
|
|
5017
|
+
let forceFreshInteractiveCliSession = false;
|
|
5018
|
+
while (true) {
|
|
5019
|
+
appServerAttempt++;
|
|
5020
|
+
try {
|
|
5021
|
+
const isFreshSession = forceFreshInteractiveCliSession || (!codexAppServerManager.hasActiveSession(convId, profile.id) && !cliSessionEpochPlan.resumeSessionId);
|
|
5022
|
+
const result = await codexAppServerManager.runTurn({
|
|
5023
|
+
runtimeMode: 'local_desktop',
|
|
5024
|
+
conversationId: convId,
|
|
5025
|
+
botId: profile.id,
|
|
5026
|
+
botName: profile.name,
|
|
5027
|
+
cwd: llmSpawnCwd,
|
|
5028
|
+
systemPrompt,
|
|
5029
|
+
messages: llmMessages,
|
|
5030
|
+
forceFreshSession: isFreshSession,
|
|
5031
|
+
resumeSessionId: isFreshSession ? undefined : (cliSessionEpochPlan.resumeSessionId || undefined),
|
|
5032
|
+
model: activeModelName || profile.model || null,
|
|
5033
|
+
projectId: conversation?.project_id ?? null,
|
|
5034
|
+
codexSettings: {
|
|
5035
|
+
reasoningEffort: profile.codex_reasoning_effort,
|
|
5036
|
+
reasoningSummary: profile.codex_reasoning_summary,
|
|
5037
|
+
personality: profile.codex_personality,
|
|
5038
|
+
serviceTier: profile.codex_service_tier,
|
|
5039
|
+
sandboxPolicy: profile.codex_sandbox_policy,
|
|
5040
|
+
approvalPolicy: profile.codex_approval_policy,
|
|
5041
|
+
},
|
|
5042
|
+
abortSignal: routeAbortController.signal,
|
|
5043
|
+
onChunk: async (chunk) => {
|
|
5044
|
+
streamedAnyChunk = true;
|
|
5045
|
+
streamedContent += chunk;
|
|
5046
|
+
persistAssistantPartial(false);
|
|
5047
|
+
sendEvent('chunk', { text: chunk });
|
|
5048
|
+
},
|
|
5049
|
+
onCommentary: async (commentary) => {
|
|
5050
|
+
const text = String(commentary || '').trim();
|
|
5051
|
+
if (!text)
|
|
5052
|
+
return;
|
|
5053
|
+
sendEvent('status', { phase: 'commentary', detail: text });
|
|
5054
|
+
},
|
|
5055
|
+
onDetail: async (detail) => {
|
|
5056
|
+
const text = String(detail || '').trim();
|
|
5057
|
+
if (!text)
|
|
5058
|
+
return;
|
|
5059
|
+
sendEvent('status', { phase: 'thinking', detail: text });
|
|
5060
|
+
recordActivity('status', { phase: 'thinking', detail: text }, text);
|
|
5061
|
+
},
|
|
5062
|
+
onToolEvent: async (toolEvent) => {
|
|
5063
|
+
if (toolEvent.kind === 'call') {
|
|
5064
|
+
sendEvent('tool_call', {
|
|
5065
|
+
name: toolEvent.toolName,
|
|
5066
|
+
callId: toolEvent.toolCallId,
|
|
5067
|
+
arguments: toolEvent.arguments || null,
|
|
5068
|
+
});
|
|
5069
|
+
recordActivity('tool_call', {
|
|
5070
|
+
name: toolEvent.toolName,
|
|
5071
|
+
callId: toolEvent.toolCallId,
|
|
5072
|
+
arguments: toolEvent.arguments || null,
|
|
5073
|
+
}, `Running ${toolEvent.toolName}`);
|
|
5074
|
+
return;
|
|
5075
|
+
}
|
|
5076
|
+
sendEvent('tool_result', {
|
|
5077
|
+
name: toolEvent.toolName,
|
|
5078
|
+
callId: toolEvent.toolCallId,
|
|
5079
|
+
output: toolEvent.output || '',
|
|
5080
|
+
isError: !!toolEvent.isError,
|
|
5081
|
+
});
|
|
5082
|
+
recordActivity('tool_result', {
|
|
5083
|
+
callId: toolEvent.toolCallId,
|
|
5084
|
+
toolName: toolEvent.toolName,
|
|
5085
|
+
output: toolEvent.output || '',
|
|
5086
|
+
isError: !!toolEvent.isError,
|
|
5087
|
+
}, toolEvent.isError
|
|
5088
|
+
? `${toolEvent.toolName} returned an error`
|
|
5089
|
+
: `${toolEvent.toolName} completed`);
|
|
5090
|
+
},
|
|
5091
|
+
});
|
|
5092
|
+
if (result.sessionId) {
|
|
5093
|
+
activeCliSessionId = result.sessionId;
|
|
5094
|
+
}
|
|
5095
|
+
if (result.usage) {
|
|
5096
|
+
totalInputTokens += result.usage.inputTokens || 0;
|
|
5097
|
+
totalOutputTokens += result.usage.outputTokens || 0;
|
|
5098
|
+
hasExactUsage = true;
|
|
5099
|
+
}
|
|
5100
|
+
// ─── token.txt: agent-side usage log (Codex App Server path) ─
|
|
5101
|
+
// Fire-and-forget. Never blocks the chat reply.
|
|
5102
|
+
try {
|
|
5103
|
+
const __auth = await getHydratedDesktopAuth().catch(() => null);
|
|
5104
|
+
if (__auth?.token && result.usage) {
|
|
5105
|
+
void (0, usage_log_1.writeAgentUsageLog)({
|
|
5106
|
+
apiToken: __auth.token,
|
|
5107
|
+
testRunId: __testRunId,
|
|
5108
|
+
mode: 'local-db-codex-app',
|
|
5109
|
+
conversationId: convId,
|
|
5110
|
+
botId: profile?.id || null,
|
|
5111
|
+
botName: profile?.name || null,
|
|
5112
|
+
turnIndex: __turnIndexParsed,
|
|
5113
|
+
provider: 'openai',
|
|
5114
|
+
model: profile?.model || 'unknown',
|
|
5115
|
+
inputTokensFresh: result.usage.inputTokensFresh ?? 0,
|
|
5116
|
+
inputTokensCacheCreation: result.usage.inputTokensCacheCreation ?? 0,
|
|
5117
|
+
inputTokensCacheRead: result.usage.inputTokensCacheRead ?? 0,
|
|
5118
|
+
outputTokens: result.usage.outputTokens ?? 0,
|
|
5119
|
+
userPromptText: __benchUserMessage,
|
|
5120
|
+
systemPromptText: systemPrompt,
|
|
5121
|
+
responseText: result.content || null,
|
|
5122
|
+
});
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5125
|
+
catch { /* best effort; never break chat */ }
|
|
5126
|
+
// ─── end usage log ──────────────────────────────────────────
|
|
5127
|
+
rawCliTranscript = result.rawOutput || '';
|
|
5128
|
+
fullContent = (0, completion_marker_1.stripCompletionSentinel)((result.content || '').trim()).text.trim();
|
|
5129
|
+
if (!fullContent && appServerAttempt < LOCAL_RUNTIME_RETRY_LIMIT) {
|
|
5130
|
+
forceFreshInteractiveCliSession = true;
|
|
5131
|
+
if (cliSessionEpochPlan.resumeSessionId) {
|
|
5132
|
+
applyFreshCliBootstrapFallback('resume_failed');
|
|
5133
|
+
}
|
|
5134
|
+
codexAppServerManager.closeSessionByConversation(convId, profile.id);
|
|
5135
|
+
const retryDetail = `Selected runtime returned an empty response; retrying with a fresh ${activeProviderName} session (${appServerAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`;
|
|
5136
|
+
console.warn(chalk_1.default.yellow(` [chat] ${retryDetail}`));
|
|
5137
|
+
await pauseLocalRuntimeRetry(appServerAttempt);
|
|
5138
|
+
continue;
|
|
5139
|
+
}
|
|
5140
|
+
break;
|
|
5141
|
+
}
|
|
5142
|
+
catch (codexErr) {
|
|
5143
|
+
if (routeAbortController.signal.aborted || codexErr?.name === 'AbortError') {
|
|
5144
|
+
throw codexErr;
|
|
5145
|
+
}
|
|
5146
|
+
clearFailedLocalCliSessionEpoch(convId, profile.id, activeCliSessionId || cliSessionEpochPlan.resumeSessionId);
|
|
5147
|
+
if (appServerAttempt >= LOCAL_RUNTIME_RETRY_LIMIT || !shouldRetrySelectedLocalRuntime(codexErr)) {
|
|
5148
|
+
throw codexErr;
|
|
5149
|
+
}
|
|
5150
|
+
forceFreshInteractiveCliSession = true;
|
|
5151
|
+
if (cliSessionEpochPlan.resumeSessionId) {
|
|
5152
|
+
applyFreshCliBootstrapFallback('resume_failed');
|
|
5153
|
+
}
|
|
5154
|
+
codexAppServerManager.closeSessionByConversation(convId, profile.id);
|
|
5155
|
+
const retryDetail = `Selected runtime failed (${codexErr?.message || codexErr}); retrying with a fresh ${activeProviderName} session (${appServerAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`;
|
|
5156
|
+
console.warn(chalk_1.default.yellow(` [chat] ${retryDetail}`));
|
|
5157
|
+
await pauseLocalRuntimeRetry(appServerAttempt);
|
|
5158
|
+
}
|
|
5159
|
+
}
|
|
5160
|
+
}
|
|
5161
|
+
if (usePtyInteractiveCliSession) {
|
|
5162
|
+
if (activeProviderName !== 'claude-cli') {
|
|
5163
|
+
throw new Error(`Legacy PTY interactive sessions are Claude-only in local desktop mode; expected ${activeProviderName} to use Codex app-server.`);
|
|
5164
|
+
}
|
|
3320
5165
|
const ptyManager = (0, local_cli_pty_manager_1.getLocalCliPtySessionManager)();
|
|
3321
5166
|
let ptyAttempt = 0;
|
|
5167
|
+
let forceFreshInteractiveCliSession = false;
|
|
5168
|
+
let currentAttemptSessionId = null;
|
|
5169
|
+
let currentAttemptWasFreshSession = false;
|
|
3322
5170
|
while (true) {
|
|
3323
5171
|
ptyAttempt++;
|
|
3324
5172
|
try {
|
|
5173
|
+
const hasLivePtySession = ptyManager.hasActiveSession(convId, profile.id);
|
|
5174
|
+
const isFreshSession = forceFreshInteractiveCliSession || (!hasLivePtySession && !cliSessionEpochPlan.resumeSessionId);
|
|
5175
|
+
const newSessionId = activeProviderName === 'claude-cli' && isFreshSession
|
|
5176
|
+
? data.generateNextSessionId()
|
|
5177
|
+
: undefined;
|
|
5178
|
+
currentAttemptWasFreshSession = isFreshSession;
|
|
5179
|
+
currentAttemptSessionId = newSessionId || cliSessionEpochPlan.resumeSessionId || activeCliSessionId || null;
|
|
3325
5180
|
const result = await ptyManager.runTurn({
|
|
3326
5181
|
conversationId: convId,
|
|
3327
5182
|
botId: profile.id,
|
|
3328
|
-
provider:
|
|
5183
|
+
provider: 'claude-cli',
|
|
5184
|
+
botSettings: {
|
|
5185
|
+
claude: {
|
|
5186
|
+
model: profile.model,
|
|
5187
|
+
effortLevel: profile.claude_effort_level,
|
|
5188
|
+
outputStyle: profile.claude_output_style,
|
|
5189
|
+
fastMode: profile.claude_fast_mode === 1,
|
|
5190
|
+
permissionsJson: profile.claude_permissions_json,
|
|
5191
|
+
},
|
|
5192
|
+
},
|
|
3329
5193
|
cwd: llmSpawnCwd,
|
|
3330
5194
|
systemPrompt,
|
|
3331
5195
|
messages: llmMessages,
|
|
3332
|
-
forceFreshSession:
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
5196
|
+
forceFreshSession: isFreshSession,
|
|
5197
|
+
resumeSessionId: isFreshSession ? undefined : (cliSessionEpochPlan.resumeSessionId || undefined),
|
|
5198
|
+
newSessionId,
|
|
5199
|
+
abortSignal: routeAbortController.signal,
|
|
5200
|
+
onRawChunk: async (chunk) => {
|
|
5201
|
+
sendEvent('terminal_chunk', {
|
|
5202
|
+
text: chunk,
|
|
5203
|
+
provider: activeProviderName,
|
|
5204
|
+
botId: profile?.id || null,
|
|
5205
|
+
agentName: profile?.name || null,
|
|
3338
5206
|
});
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
5207
|
+
},
|
|
5208
|
+
onChunk: async (chunk) => {
|
|
5209
|
+
streamedAnyChunk = true;
|
|
5210
|
+
streamedContent += chunk;
|
|
5211
|
+
persistAssistantPartial(false);
|
|
5212
|
+
sendEvent('chunk', { text: chunk });
|
|
5213
|
+
},
|
|
5214
|
+
onDetail: async (detail) => {
|
|
5215
|
+
const text = String(detail || '').trim();
|
|
5216
|
+
if (!text)
|
|
5217
|
+
return;
|
|
5218
|
+
sendEvent('status', { phase: 'thinking', detail: text });
|
|
5219
|
+
recordActivity('status', { phase: 'thinking', detail: text }, text);
|
|
3344
5220
|
},
|
|
3345
5221
|
});
|
|
3346
5222
|
if (result.sessionId) {
|
|
@@ -3351,25 +5227,76 @@ function startLocalServer(opts) {
|
|
|
3351
5227
|
totalOutputTokens += result.usage.outputTokens || 0;
|
|
3352
5228
|
hasExactUsage = true;
|
|
3353
5229
|
}
|
|
3354
|
-
|
|
5230
|
+
// ─── token.txt: agent-side usage log (Claude PTY path) ──────
|
|
5231
|
+
// Fire-and-forget. Never blocks the chat reply.
|
|
5232
|
+
try {
|
|
5233
|
+
const __auth = await getHydratedDesktopAuth().catch(() => null);
|
|
5234
|
+
if (__auth?.token && result.usage) {
|
|
5235
|
+
void (0, usage_log_1.writeAgentUsageLog)({
|
|
5236
|
+
apiToken: __auth.token,
|
|
5237
|
+
testRunId: __testRunId,
|
|
5238
|
+
mode: 'local-db-claude-pty',
|
|
5239
|
+
conversationId: convId,
|
|
5240
|
+
botId: profile?.id || null,
|
|
5241
|
+
botName: profile?.name || null,
|
|
5242
|
+
turnIndex: __turnIndexParsed,
|
|
5243
|
+
provider: 'anthropic',
|
|
5244
|
+
model: profile?.model || 'unknown',
|
|
5245
|
+
inputTokensFresh: result.usage.inputTokensFresh ?? 0,
|
|
5246
|
+
inputTokensCacheCreation: result.usage.inputTokensCacheCreation ?? 0,
|
|
5247
|
+
inputTokensCacheRead: result.usage.inputTokensCacheRead ?? 0,
|
|
5248
|
+
outputTokens: result.usage.outputTokens ?? 0,
|
|
5249
|
+
userPromptText: __benchUserMessage,
|
|
5250
|
+
systemPromptText: systemPrompt,
|
|
5251
|
+
responseText: result.content || null,
|
|
5252
|
+
});
|
|
5253
|
+
}
|
|
5254
|
+
}
|
|
5255
|
+
catch { /* best effort; never break chat */ }
|
|
5256
|
+
// ─── end usage log ──────────────────────────────────────────
|
|
5257
|
+
rawCliTranscript = result.rawOutput || '';
|
|
5258
|
+
fullContent = (0, completion_marker_1.stripCompletionSentinel)((result.content || '').trim()).text.trim();
|
|
5259
|
+
if (!fullContent && ptyAttempt < LOCAL_RUNTIME_RETRY_LIMIT) {
|
|
5260
|
+
forceFreshInteractiveCliSession = true;
|
|
5261
|
+
if (cliSessionEpochPlan.resumeSessionId) {
|
|
5262
|
+
applyFreshCliBootstrapFallback('resume_failed');
|
|
5263
|
+
}
|
|
5264
|
+
ptyManager.closeSessionByConversation(convId, profile.id);
|
|
5265
|
+
const retryDetail = `Selected runtime returned an empty response; retrying with a fresh ${activeProviderName} session (${ptyAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`;
|
|
5266
|
+
console.warn(chalk_1.default.yellow(` [chat] ${retryDetail}`));
|
|
5267
|
+
await pauseLocalRuntimeRetry(ptyAttempt);
|
|
5268
|
+
continue;
|
|
5269
|
+
}
|
|
3355
5270
|
break;
|
|
3356
5271
|
}
|
|
3357
5272
|
catch (ptyErr) {
|
|
5273
|
+
if (routeAbortController.signal.aborted || ptyErr?.name === 'AbortError') {
|
|
5274
|
+
throw ptyErr;
|
|
5275
|
+
}
|
|
5276
|
+
ptyManager.logSessionFailureByConversation(convId, profile.id, 'chat_runtime_failure_before_kill', ptyErr);
|
|
5277
|
+
clearFailedLocalCliSessionEpoch(convId, profile.id, currentAttemptSessionId || activeCliSessionId || cliSessionEpochPlan.resumeSessionId);
|
|
3358
5278
|
if (ptyAttempt >= LOCAL_RUNTIME_RETRY_LIMIT || !shouldRetrySelectedLocalRuntime(ptyErr)) {
|
|
3359
5279
|
throw ptyErr;
|
|
3360
5280
|
}
|
|
3361
|
-
const
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
5281
|
+
const startupRetry = isClaudeFreshSessionStartupFailure(ptyErr);
|
|
5282
|
+
if (startupRetry || currentAttemptWasFreshSession) {
|
|
5283
|
+
forceFreshInteractiveCliSession = true;
|
|
5284
|
+
ptyManager.closeSessionByConversation(convId, profile.id);
|
|
5285
|
+
}
|
|
5286
|
+
const resumeFailureFallback = !!cliSessionEpochPlan.resumeSessionId && !currentAttemptWasFreshSession;
|
|
5287
|
+
if (resumeFailureFallback) {
|
|
5288
|
+
forceFreshInteractiveCliSession = true;
|
|
5289
|
+
applyFreshCliBootstrapFallback('resume_failed');
|
|
5290
|
+
ptyManager.closeSessionByConversation(convId, profile.id);
|
|
5291
|
+
}
|
|
5292
|
+
const retryDetail = startupRetry
|
|
5293
|
+
? `Fresh ${activeProviderName} session ${currentAttemptSessionId || '(unknown)'} did not finish startup before the transcript became available; killing it and retrying with a new session (${ptyAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`
|
|
5294
|
+
: resumeFailureFallback
|
|
5295
|
+
? `Stored ${activeProviderName} session ${currentAttemptSessionId || '(unknown)'} failed to resume (${ptyErr?.message || ptyErr}); retrying with a fresh bootstrapped session (${ptyAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`
|
|
5296
|
+
: currentAttemptWasFreshSession && activeProviderName === 'claude-cli'
|
|
5297
|
+
? `Fresh ${activeProviderName} session ${currentAttemptSessionId || '(unknown)'} failed (${ptyErr?.message || ptyErr}); killing it and retrying with a new session (${ptyAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`
|
|
5298
|
+
: `Selected runtime failed (${ptyErr?.message || ptyErr}); retrying the same connection (${ptyAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`;
|
|
5299
|
+
console.warn(chalk_1.default.yellow(` [chat] ${retryDetail}`));
|
|
3373
5300
|
await pauseLocalRuntimeRetry(ptyAttempt);
|
|
3374
5301
|
}
|
|
3375
5302
|
}
|
|
@@ -3379,10 +5306,6 @@ function startLocalServer(opts) {
|
|
|
3379
5306
|
iteration++;
|
|
3380
5307
|
let iterationFirstChunk = true;
|
|
3381
5308
|
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
5309
|
let response;
|
|
3387
5310
|
const chatOptions = {
|
|
3388
5311
|
messages: llmMessages,
|
|
@@ -3398,8 +5321,6 @@ function startLocalServer(opts) {
|
|
|
3398
5321
|
throwIfChatJobCancelled();
|
|
3399
5322
|
if (iterationFirstChunk) {
|
|
3400
5323
|
iterationFirstChunk = false;
|
|
3401
|
-
sendEvent('status', { phase: 'generating' });
|
|
3402
|
-
recordActivity('status', { phase: 'generating' }, 'Generating response...');
|
|
3403
5324
|
}
|
|
3404
5325
|
streamedAnyChunk = true;
|
|
3405
5326
|
streamedContent += chunk;
|
|
@@ -3531,7 +5452,7 @@ function startLocalServer(opts) {
|
|
|
3531
5452
|
continue;
|
|
3532
5453
|
}
|
|
3533
5454
|
// Final response (guard against defer-only filler)
|
|
3534
|
-
const candidate = (response.content || '').trim();
|
|
5455
|
+
const candidate = (0, completion_marker_1.stripCompletionSentinel)((response.content || '').trim()).text.trim();
|
|
3535
5456
|
if (!forcedFinalizationPass && (0, response_guard_1.isLikelyDeferredReply)(candidate)) {
|
|
3536
5457
|
forcedFinalizationPass = true;
|
|
3537
5458
|
llmMessages.push({ role: 'assistant', content: candidate });
|
|
@@ -3539,19 +5460,28 @@ function startLocalServer(opts) {
|
|
|
3539
5460
|
role: 'user',
|
|
3540
5461
|
content: 'Provide the final answer now. Do not say you will check later. Either provide concrete results or explicitly say what is unavailable.',
|
|
3541
5462
|
});
|
|
3542
|
-
sendEvent('status', { phase: 'thinking', detail: 'Finalizing response...' });
|
|
3543
|
-
recordActivity('status', { phase: 'thinking', detail: 'Finalizing response...' }, 'Finalizing response...');
|
|
3544
5463
|
continue;
|
|
3545
5464
|
}
|
|
3546
5465
|
fullContent = candidate;
|
|
3547
5466
|
break;
|
|
3548
5467
|
}
|
|
3549
|
-
const persistedContent = fullContent || streamedContent.trim();
|
|
5468
|
+
const persistedContent = (0, completion_marker_1.stripCompletionSentinel)(fullContent || streamedContent.trim()).text.trim();
|
|
3550
5469
|
if (!persistedContent) {
|
|
3551
5470
|
throw new Error('Assistant returned no final response');
|
|
3552
5471
|
}
|
|
3553
5472
|
persistAssistantPartial(true);
|
|
3554
5473
|
if (enableCliSessionEpoch && activeCliSessionId) {
|
|
5474
|
+
if (cliSessionEpochPlan.resumeSessionId && !freshCliBootstrapFallbackApplied) {
|
|
5475
|
+
console.info(`[chat] cli_session_resume_succeeded conversationId=${convId} botId=${profile.id} provider=${activeProviderName} sessionId=${activeCliSessionId}`);
|
|
5476
|
+
}
|
|
5477
|
+
const usedBootstrap = freshCliBootstrapFileWritten
|
|
5478
|
+
|| (!cliSessionEpochPlan.resumeSessionId && !!promptContextWindow.allowBootstrap && !skipFreshCliBootstrap);
|
|
5479
|
+
const bootstrapReason = freshCliBootstrapFileWritten
|
|
5480
|
+
? 'resume_failed'
|
|
5481
|
+
: (!cliSessionEpochPlan.resumeSessionId && !!promptContextWindow.allowBootstrap && !skipFreshCliBootstrap)
|
|
5482
|
+
? 'fresh_with_context'
|
|
5483
|
+
: null;
|
|
5484
|
+
console.info(`[chat] cli_turn_telemetry conversationId=${convId} botId=${profile.id} provider=${activeProviderName} selectedSessionId=${cliSessionEpochPlan.resumeSessionId || '(none)'} actualSessionId=${activeCliSessionId} promptContextMode=${promptContextWindow.mode || 'unknown'} usedBootstrap=${usedBootstrap} bootstrapReason=${bootstrapReason || '(none)'} resetReason=${cliEpochResetReason || '(none)'}`);
|
|
3555
5485
|
const nextEpochTurnCount = cliSessionEpochPlan.resumeSessionId
|
|
3556
5486
|
? ((cliSessionEpochPlan.existing?.epoch_turn_count || 0) + 1)
|
|
3557
5487
|
: 1;
|
|
@@ -3563,7 +5493,7 @@ function startLocalServer(opts) {
|
|
|
3563
5493
|
epochTurnCount: nextEpochTurnCount,
|
|
3564
5494
|
lastInputTokens: hasExactUsage ? totalInputTokens : approxInputTokens,
|
|
3565
5495
|
lastOutputTokens: hasExactUsage ? totalOutputTokens : 0,
|
|
3566
|
-
resetReason:
|
|
5496
|
+
resetReason: cliEpochResetReason,
|
|
3567
5497
|
epochStartedAt: cliEpochStartedAt,
|
|
3568
5498
|
lastUsedAt: localTimestamp(),
|
|
3569
5499
|
});
|
|
@@ -3594,8 +5524,15 @@ function startLocalServer(opts) {
|
|
|
3594
5524
|
model: modelWithRuntime || null,
|
|
3595
5525
|
botId: profile.id,
|
|
3596
5526
|
agentName: profile.name,
|
|
5527
|
+
resultArtifact: useInteractiveCliSession ? (rawCliTranscript || null) : undefined,
|
|
3597
5528
|
}) || data.addMessage(convId, 'assistant', persistedContent, modelWithRuntime || undefined, undefined, profile.id, profile.name))
|
|
3598
5529
|
: data.addMessage(convId, 'assistant', persistedContent, modelWithRuntime || undefined, undefined, profile.id, profile.name);
|
|
5530
|
+
emitConversationSyncEvents(convId, [
|
|
5531
|
+
{
|
|
5532
|
+
change: savedMessage.id === assistantMessageId ? 'message.updated' : 'message.created',
|
|
5533
|
+
messageId: savedMessage.id,
|
|
5534
|
+
},
|
|
5535
|
+
]);
|
|
3599
5536
|
data.attachMessageActivitiesToMessage(activityStreamId, savedMessage.id);
|
|
3600
5537
|
data.createMessageActivity({
|
|
3601
5538
|
conversationId: convId,
|
|
@@ -3606,6 +5543,7 @@ function startLocalServer(opts) {
|
|
|
3606
5543
|
summary: 'Final assistant response',
|
|
3607
5544
|
payload: {
|
|
3608
5545
|
content: persistedContent,
|
|
5546
|
+
...(useInteractiveCliSession && rawCliTranscript ? { rawOutput: rawCliTranscript } : {}),
|
|
3609
5547
|
runtime: runtimePayload(),
|
|
3610
5548
|
},
|
|
3611
5549
|
expiresAt: activityExpiresAt,
|
|
@@ -3632,6 +5570,9 @@ function startLocalServer(opts) {
|
|
|
3632
5570
|
// For CLI providers, set a simple title without burning a CLI call
|
|
3633
5571
|
const shortTitle = message.slice(0, 60).replace(/\n/g, ' ').trim();
|
|
3634
5572
|
data.updateConversation(convId, { title: shortTitle || 'New Chat' });
|
|
5573
|
+
emitConversationSyncEvents(convId, [
|
|
5574
|
+
{ change: 'conversation.updated' },
|
|
5575
|
+
]);
|
|
3635
5576
|
}
|
|
3636
5577
|
else {
|
|
3637
5578
|
autoTitleConversation(convId, message, persistedContent, activeProviderName, activeModelName, activeApiKey);
|
|
@@ -3646,8 +5587,17 @@ function startLocalServer(opts) {
|
|
|
3646
5587
|
(0, local_funnel_1.scheduleFunnelProcessing)(convId);
|
|
3647
5588
|
}
|
|
3648
5589
|
catch (err) {
|
|
5590
|
+
// Log the FULL stack to agent.log. Previously only err.message
|
|
5591
|
+
// made it into DB / console; for crashes like "Maximum call
|
|
5592
|
+
// stack size exceeded" we lost the one useful diagnostic (the
|
|
5593
|
+
// recursion site). Chalk-colored message + serialized stack.
|
|
5594
|
+
// (2026-04-19.)
|
|
3649
5595
|
console.error(chalk_1.default.red(`Chat error: ${err.message}`));
|
|
5596
|
+
if (err?.stack) {
|
|
5597
|
+
console.error(String(err.stack));
|
|
5598
|
+
}
|
|
3650
5599
|
try {
|
|
5600
|
+
const userMessage = buildLocalRuntimeUserFacingError(err);
|
|
3651
5601
|
if (activityErrorContext.conversationId) {
|
|
3652
5602
|
data.createMessageActivity({
|
|
3653
5603
|
conversationId: activityErrorContext.conversationId,
|
|
@@ -3659,6 +5609,8 @@ function startLocalServer(opts) {
|
|
|
3659
5609
|
summary: err.message,
|
|
3660
5610
|
payload: {
|
|
3661
5611
|
error: err.message,
|
|
5612
|
+
userMessage,
|
|
5613
|
+
stack: err?.stack || null,
|
|
3662
5614
|
authRequired: err?.authRequired === true,
|
|
3663
5615
|
providerId: err?.providerId || null,
|
|
3664
5616
|
cli: err?.cli || null,
|
|
@@ -3671,12 +5623,13 @@ function startLocalServer(opts) {
|
|
|
3671
5623
|
// best effort only
|
|
3672
5624
|
}
|
|
3673
5625
|
if (!res.headersSent) {
|
|
3674
|
-
res.status(500).json({ error: err.message });
|
|
5626
|
+
res.status(500).json({ error: err.message, userMessage: buildLocalRuntimeUserFacingError(err) });
|
|
3675
5627
|
}
|
|
3676
5628
|
else {
|
|
3677
5629
|
try {
|
|
3678
5630
|
res.write(`event: error\ndata: ${JSON.stringify({
|
|
3679
5631
|
error: err.message,
|
|
5632
|
+
userMessage: buildLocalRuntimeUserFacingError(err),
|
|
3680
5633
|
authRequired: err?.authRequired === true,
|
|
3681
5634
|
providerId: err?.providerId || null,
|
|
3682
5635
|
cli: err?.cli || null,
|
|
@@ -3689,6 +5642,7 @@ function startLocalServer(opts) {
|
|
|
3689
5642
|
}
|
|
3690
5643
|
finally {
|
|
3691
5644
|
req.off?.('close', abortOnClientClose);
|
|
5645
|
+
res.off?.('close', abortOnClientClose);
|
|
3692
5646
|
}
|
|
3693
5647
|
});
|
|
3694
5648
|
// ─── Memory Facts ───────────────────────────────────────────
|
|
@@ -3763,6 +5717,650 @@ function startLocalServer(opts) {
|
|
|
3763
5717
|
res.status(500).json({ error: err.message });
|
|
3764
5718
|
}
|
|
3765
5719
|
});
|
|
5720
|
+
// Phase B: tool-dispatch is always on for local_desktop; keep GET for
|
|
5721
|
+
// introspection so any external tooling knows the state. POST is a
|
|
5722
|
+
// no-op that returns the current (permanent) state.
|
|
5723
|
+
app.get('/api/orchestration/tool-dispatch', (_req, res) => {
|
|
5724
|
+
res.json({ enabled: true, permanent: true, phase: 'B' });
|
|
5725
|
+
});
|
|
5726
|
+
app.post('/api/orchestration/tool-dispatch', (_req, res) => {
|
|
5727
|
+
res.json({ enabled: true, permanent: true, phase: 'B' });
|
|
5728
|
+
});
|
|
5729
|
+
function buildPlanImportChatMessage(params) {
|
|
5730
|
+
const lines = [];
|
|
5731
|
+
lines.push(`**Plan Imported: ${params.sourceFileName}**`);
|
|
5732
|
+
lines.push('');
|
|
5733
|
+
lines.push(`**Plan File:** ${params.sourceFileName}`);
|
|
5734
|
+
const plannerLabel = params.planner.agentName || 'Planner';
|
|
5735
|
+
lines.push(`**Planner:** ${plannerLabel}${params.planner.model ? ` (${params.planner.model})` : ''}`);
|
|
5736
|
+
const stageLines = params.stages.map((stage, i) => {
|
|
5737
|
+
const bot = data.getAgentProfile(stage.botId);
|
|
5738
|
+
return `#${i + 1} ${stage.role} → ${bot?.name || stage.botId}`;
|
|
5739
|
+
});
|
|
5740
|
+
lines.push(`**Task Flow:** ${stageLines.join(', ')}`);
|
|
5741
|
+
if (params.plannerInstructions?.trim()) {
|
|
5742
|
+
lines.push(`**Planner Instructions:** ${params.plannerInstructions.trim()}`);
|
|
5743
|
+
}
|
|
5744
|
+
const orderLabel = params.executionOrder === 'batch_by_stage'
|
|
5745
|
+
? 'batch_by_stage (all coding first, then QA)'
|
|
5746
|
+
: 'per_item_pipeline (Code → QA per item)';
|
|
5747
|
+
lines.push(`**Execution Order:** ${orderLabel}`);
|
|
5748
|
+
lines.push(`**Status:** ${params.status}`);
|
|
5749
|
+
lines.push('');
|
|
5750
|
+
lines.push(`**Created ${params.taskCount} TODOs:**`);
|
|
5751
|
+
lines.push('');
|
|
5752
|
+
for (let i = 0; i < params.tasks.length; i++) {
|
|
5753
|
+
const task = params.tasks[i];
|
|
5754
|
+
const role = task.task_type || 'Task';
|
|
5755
|
+
const owner = task.owner_name || 'Unassigned';
|
|
5756
|
+
lines.push(`${i + 1}. [${role}] ${owner}: ${task.title}`);
|
|
5757
|
+
}
|
|
5758
|
+
lines.push('');
|
|
5759
|
+
lines.push('Review the stored tasks, delete any you do not want, then start the plan.');
|
|
5760
|
+
return lines.join('\n');
|
|
5761
|
+
}
|
|
5762
|
+
function buildPlanStartChatMessage(params) {
|
|
5763
|
+
const lines = [];
|
|
5764
|
+
const action = params.isResume ? 'Resumed' : 'Started';
|
|
5765
|
+
lines.push(`**Plan ${action}: ${params.sourceFileName}**`);
|
|
5766
|
+
lines.push('');
|
|
5767
|
+
if (params.firstTask) {
|
|
5768
|
+
const role = params.firstTask.task_type || 'Task';
|
|
5769
|
+
const owner = params.firstTask.owner_name || 'Unassigned';
|
|
5770
|
+
lines.push(`**Next TODO:** #${params.firstTask.id} "${params.firstTask.title}" (${owner}, ${role})`);
|
|
5771
|
+
}
|
|
5772
|
+
lines.push(`**Active TODOs remaining:** ${params.remainingCount} of ${params.totalCount}`);
|
|
5773
|
+
if (params.executionOrder) {
|
|
5774
|
+
const orderLabel = params.executionOrder === 'batch_by_stage'
|
|
5775
|
+
? 'batch_by_stage (all coding first, then QA)'
|
|
5776
|
+
: 'per_item_pipeline (Code → QA per item)';
|
|
5777
|
+
lines.push(`**Execution Order:** ${orderLabel}`);
|
|
5778
|
+
}
|
|
5779
|
+
lines.push(`**Status:** ${params.status}`);
|
|
5780
|
+
return lines.join('\n');
|
|
5781
|
+
}
|
|
5782
|
+
function buildPlanFailureChatMessage(params) {
|
|
5783
|
+
const lines = [];
|
|
5784
|
+
lines.push(`**Plan Resume Failed: ${params.sourceFileName}**`);
|
|
5785
|
+
lines.push('');
|
|
5786
|
+
lines.push(`**Error:** ${params.error}`);
|
|
5787
|
+
lines.push(`**Status:** ${params.status}`);
|
|
5788
|
+
lines.push('');
|
|
5789
|
+
lines.push('The plan has been paused. Check the error and retry.');
|
|
5790
|
+
return lines.join('\n');
|
|
5791
|
+
}
|
|
5792
|
+
const activeImportedPlanRunners = new Map();
|
|
5793
|
+
const buildImportedPlanSessionScopeKey = (runId, conversationId) => `${conversationId}::plan::${runId}`;
|
|
5794
|
+
const resolveImportedPlanWorkerBotIds = (run) => {
|
|
5795
|
+
const tasks = data.getImportedPlanRunTasks(run.id);
|
|
5796
|
+
const ids = new Set();
|
|
5797
|
+
for (const stage of run.requested_stages || []) {
|
|
5798
|
+
if (stage.botId)
|
|
5799
|
+
ids.add(stage.botId);
|
|
5800
|
+
}
|
|
5801
|
+
for (const task of tasks) {
|
|
5802
|
+
if (task.owner_bot_id) {
|
|
5803
|
+
ids.add(task.owner_bot_id);
|
|
5804
|
+
continue;
|
|
5805
|
+
}
|
|
5806
|
+
const ownerName = String(task.owner_name || '').trim().toLowerCase();
|
|
5807
|
+
if (!ownerName)
|
|
5808
|
+
continue;
|
|
5809
|
+
const match = data.listAgentProfiles().find((agent) => agent.name.trim().toLowerCase() === ownerName);
|
|
5810
|
+
if (match?.id)
|
|
5811
|
+
ids.add(match.id);
|
|
5812
|
+
}
|
|
5813
|
+
return Array.from(ids);
|
|
5814
|
+
};
|
|
5815
|
+
const teardownImportedPlanRunnerSessions = (run, sessionScopeKey) => {
|
|
5816
|
+
if (!run)
|
|
5817
|
+
return;
|
|
5818
|
+
const scopeKey = String(sessionScopeKey || '').trim() || buildImportedPlanSessionScopeKey(run.id, run.conversation_id);
|
|
5819
|
+
const workerBotIds = resolveImportedPlanWorkerBotIds(run);
|
|
5820
|
+
const ptyManager = (0, local_cli_pty_manager_1.getLocalCliPtySessionManager)();
|
|
5821
|
+
const codexAppServerManager = (0, codex_app_server_manager_1.getCodexAppServerManager)();
|
|
5822
|
+
for (const botId of workerBotIds) {
|
|
5823
|
+
ptyManager.closeSessionByConversation(scopeKey, botId);
|
|
5824
|
+
codexAppServerManager.closeSessionByConversation(scopeKey, botId);
|
|
5825
|
+
data.deleteCliSessionEpoch(run.conversation_id, botId, scopeKey);
|
|
5826
|
+
}
|
|
5827
|
+
};
|
|
5828
|
+
const importedPlanPauseSummary = (action) => {
|
|
5829
|
+
if (action === 'cancel')
|
|
5830
|
+
return 'Plan cancelled. Resume will continue from the current active TODO.';
|
|
5831
|
+
if (action === 'stop')
|
|
5832
|
+
return 'Plan stopped. Resume will continue from the current active TODO.';
|
|
5833
|
+
return 'Plan paused. Resume will continue from the current active TODO.';
|
|
5834
|
+
};
|
|
5835
|
+
const normalizeImportedPlanRunForUi = (run) => {
|
|
5836
|
+
if (!run)
|
|
5837
|
+
return undefined;
|
|
5838
|
+
if (run.status === 'running' && !activeImportedPlanRunners.has(run.id) && data.getImportedPlanRunRemainingTaskIds(run.id).length > 0) {
|
|
5839
|
+
return data.updateImportedPlanRun(run.id, {
|
|
5840
|
+
status: 'paused',
|
|
5841
|
+
lastError: run.last_error || 'Agent restarted or lost the in-memory Plan-TODO runner. Resume will continue from the next active TODO.',
|
|
5842
|
+
}) || run;
|
|
5843
|
+
}
|
|
5844
|
+
return run;
|
|
5845
|
+
};
|
|
5846
|
+
const serializeImportedPlanRun = (run) => {
|
|
5847
|
+
run = normalizeImportedPlanRunForUi(run);
|
|
5848
|
+
if (!run)
|
|
5849
|
+
return null;
|
|
5850
|
+
const tasks = data.getImportedPlanRunTasks(run.id);
|
|
5851
|
+
const remainingTaskIds = data.getImportedPlanRunRemainingTaskIds(run.id);
|
|
5852
|
+
const currentTask = data.getImportedPlanRunCurrentTask(run.id) || null;
|
|
5853
|
+
return {
|
|
5854
|
+
id: run.id,
|
|
5855
|
+
conversationId: run.conversation_id,
|
|
5856
|
+
projectId: run.project_id,
|
|
5857
|
+
sourceFilePath: run.source_file_path,
|
|
5858
|
+
sourceFileName: run.source_file_name,
|
|
5859
|
+
planner: {
|
|
5860
|
+
botId: run.planner_bot_id,
|
|
5861
|
+
agentName: run.planner_agent_name,
|
|
5862
|
+
provider: run.planner_provider,
|
|
5863
|
+
model: run.planner_model,
|
|
5864
|
+
},
|
|
5865
|
+
stages: run.requested_stages,
|
|
5866
|
+
options: run.requested_options,
|
|
5867
|
+
taskIds: run.task_ids,
|
|
5868
|
+
remainingTaskIds,
|
|
5869
|
+
taskCount: tasks.length,
|
|
5870
|
+
completedTaskCount: tasks.filter((task) => task.state === 'completed').length,
|
|
5871
|
+
remainingTaskCount: remainingTaskIds.length,
|
|
5872
|
+
currentTask,
|
|
5873
|
+
status: run.status,
|
|
5874
|
+
createdAt: run.created_at,
|
|
5875
|
+
updatedAt: run.updated_at,
|
|
5876
|
+
startedAt: run.started_at,
|
|
5877
|
+
completedAt: run.completed_at,
|
|
5878
|
+
discardedAt: run.discarded_at,
|
|
5879
|
+
lastError: run.last_error,
|
|
5880
|
+
tasks,
|
|
5881
|
+
};
|
|
5882
|
+
};
|
|
5883
|
+
app.get('/api/orchestration/import-plan/current', (req, res) => {
|
|
5884
|
+
try {
|
|
5885
|
+
const conversationId = String(req.query.conversationId || '').trim();
|
|
5886
|
+
if (!conversationId)
|
|
5887
|
+
return res.status(400).json({ error: 'conversationId is required' });
|
|
5888
|
+
const run = normalizeImportedPlanRunForUi(data.getLatestImportedPlanRunForConversation(conversationId));
|
|
5889
|
+
res.json({ ok: true, run: serializeImportedPlanRun(run) });
|
|
5890
|
+
}
|
|
5891
|
+
catch (err) {
|
|
5892
|
+
res.status(500).json({ error: err.message });
|
|
5893
|
+
}
|
|
5894
|
+
});
|
|
5895
|
+
app.post('/api/orchestration/import-plan/control', (req, res) => {
|
|
5896
|
+
try {
|
|
5897
|
+
if (isConnectedMode()) {
|
|
5898
|
+
return res.status(501).json({ error: 'Imported plan control is only implemented for local desktop storage right now.' });
|
|
5899
|
+
}
|
|
5900
|
+
const runId = String(req.body?.runId || '').trim();
|
|
5901
|
+
const action = String(req.body?.action || '').trim().toLowerCase();
|
|
5902
|
+
if (!runId)
|
|
5903
|
+
return res.status(400).json({ error: 'runId is required' });
|
|
5904
|
+
if (action !== 'pause' && action !== 'stop' && action !== 'cancel') {
|
|
5905
|
+
return res.status(400).json({ error: 'action must be pause, stop, or cancel' });
|
|
5906
|
+
}
|
|
5907
|
+
const run = normalizeImportedPlanRunForUi(data.getImportedPlanRun(runId));
|
|
5908
|
+
if (!run)
|
|
5909
|
+
return res.status(404).json({ error: 'Imported plan run not found.' });
|
|
5910
|
+
if (run.status === 'completed' || run.status === 'discarded') {
|
|
5911
|
+
return res.status(400).json({ error: `Imported plan is already ${run.status}.` });
|
|
5912
|
+
}
|
|
5913
|
+
const runner = activeImportedPlanRunners.get(run.id);
|
|
5914
|
+
const summary = importedPlanPauseSummary(action);
|
|
5915
|
+
const updatedRun = data.updateImportedPlanRun(run.id, {
|
|
5916
|
+
status: 'paused',
|
|
5917
|
+
completedAt: null,
|
|
5918
|
+
lastError: summary,
|
|
5919
|
+
}) || run;
|
|
5920
|
+
if (runner) {
|
|
5921
|
+
runner.stopRequested = true;
|
|
5922
|
+
runner.stopAction = action;
|
|
5923
|
+
runner.abortController.abort();
|
|
5924
|
+
}
|
|
5925
|
+
else {
|
|
5926
|
+
teardownImportedPlanRunnerSessions(updatedRun);
|
|
5927
|
+
}
|
|
5928
|
+
res.json({
|
|
5929
|
+
ok: true,
|
|
5930
|
+
action,
|
|
5931
|
+
summary,
|
|
5932
|
+
run: serializeImportedPlanRun(updatedRun),
|
|
5933
|
+
});
|
|
5934
|
+
}
|
|
5935
|
+
catch (err) {
|
|
5936
|
+
res.status(500).json({ error: err.message });
|
|
5937
|
+
}
|
|
5938
|
+
});
|
|
5939
|
+
app.post('/api/orchestration/import-plan', async (req, res) => {
|
|
5940
|
+
try {
|
|
5941
|
+
if (isConnectedMode()) {
|
|
5942
|
+
return res.status(501).json({ error: 'Plan import is only implemented for local desktop storage right now.' });
|
|
5943
|
+
}
|
|
5944
|
+
const { conversationId, topicId, projectId, filePath, importMode, stages, plannerBotId, plannerInstructions, executionOrder, } = req.body || {};
|
|
5945
|
+
const normalizedFilePath = String(filePath || '').trim();
|
|
5946
|
+
if (!normalizedFilePath)
|
|
5947
|
+
return res.status(400).json({ error: 'filePath is required' });
|
|
5948
|
+
if (String(importMode || '') !== 'custom_pipeline') {
|
|
5949
|
+
return res.status(400).json({ error: 'importMode must be custom_pipeline' });
|
|
5950
|
+
}
|
|
5951
|
+
const normalizedPlannerBotId = String(plannerBotId || '').trim();
|
|
5952
|
+
if (!normalizedPlannerBotId)
|
|
5953
|
+
return res.status(400).json({ error: 'plannerBotId is required' });
|
|
5954
|
+
const normalizedStages = Array.isArray(stages)
|
|
5955
|
+
? stages.map((stage) => ({
|
|
5956
|
+
role: String(stage?.role || '').trim(),
|
|
5957
|
+
botId: String(stage?.botId || '').trim(),
|
|
5958
|
+
})).filter((stage) => stage.role && stage.botId)
|
|
5959
|
+
: [];
|
|
5960
|
+
if (normalizedStages.length === 0) {
|
|
5961
|
+
return res.status(400).json({ error: 'Choose at least one stage and bot before importing a plan.' });
|
|
5962
|
+
}
|
|
5963
|
+
const normalizedExecutionOrder = String(executionOrder || '').trim();
|
|
5964
|
+
if (normalizedExecutionOrder
|
|
5965
|
+
&& normalizedExecutionOrder !== 'per_item_pipeline'
|
|
5966
|
+
&& normalizedExecutionOrder !== 'batch_by_stage') {
|
|
5967
|
+
return res.status(400).json({ error: 'executionOrder must be per_item_pipeline or batch_by_stage.' });
|
|
5968
|
+
}
|
|
5969
|
+
const resolvedFilePath = path.resolve(normalizedFilePath);
|
|
5970
|
+
if (!fs.existsSync(resolvedFilePath)) {
|
|
5971
|
+
return res.status(404).json({ error: `File not found: ${resolvedFilePath}` });
|
|
5972
|
+
}
|
|
5973
|
+
const stat = fs.statSync(resolvedFilePath);
|
|
5974
|
+
if (!stat.isFile()) {
|
|
5975
|
+
return res.status(400).json({ error: 'Selected plan path is not a file.' });
|
|
5976
|
+
}
|
|
5977
|
+
if (stat.size > MAX_PLAN_IMPORT_BYTES) {
|
|
5978
|
+
return res.status(400).json({ error: `Selected plan file is too large (${stat.size} bytes). Keep it under ${MAX_PLAN_IMPORT_BYTES} bytes.` });
|
|
5979
|
+
}
|
|
5980
|
+
const rawFileContent = fs.readFileSync(resolvedFilePath, 'utf8');
|
|
5981
|
+
if (!rawFileContent.trim()) {
|
|
5982
|
+
return res.status(400).json({ error: 'Selected plan file is empty.' });
|
|
5983
|
+
}
|
|
5984
|
+
const topic = topicId ? data.getTopic(String(topicId)) : undefined;
|
|
5985
|
+
let effectiveProjectId = projectId ? String(projectId) : (topic?.project_id || null);
|
|
5986
|
+
let convId = conversationId ? String(conversationId) : '';
|
|
5987
|
+
const existingConversation = convId ? data.getConversation(convId) : undefined;
|
|
5988
|
+
if (!effectiveProjectId) {
|
|
5989
|
+
effectiveProjectId = existingConversation?.project_id || null;
|
|
5990
|
+
}
|
|
5991
|
+
if (!effectiveProjectId) {
|
|
5992
|
+
return res.status(400).json({ error: 'Select a project before importing a plan.' });
|
|
5993
|
+
}
|
|
5994
|
+
if (!convId) {
|
|
5995
|
+
const project = data.getProject(effectiveProjectId);
|
|
5996
|
+
const projectBots = (project?.bot_ids || [])
|
|
5997
|
+
.map((botId) => data.getAgentProfile(botId))
|
|
5998
|
+
.filter((bot) => !!bot && bot.is_active === 1);
|
|
5999
|
+
const seedBot = projectBots[0] || data.getDefaultAgentProfile() || data.listAgentProfiles().find((bot) => bot.is_active === 1);
|
|
6000
|
+
if (!seedBot)
|
|
6001
|
+
return res.status(400).json({ error: 'No active bot is available to seed the plan-import conversation.' });
|
|
6002
|
+
const conv = data.createConversation(seedBot.id, `Plan Import: ${path.basename(resolvedFilePath)}`, 'local', {
|
|
6003
|
+
projectId: effectiveProjectId,
|
|
6004
|
+
projectName: project?.name || null,
|
|
6005
|
+
botIds: projectBots.length > 0 ? projectBots.map((bot) => bot.id) : [seedBot.id],
|
|
6006
|
+
initialBotId: seedBot.id,
|
|
6007
|
+
});
|
|
6008
|
+
convId = conv.id;
|
|
6009
|
+
}
|
|
6010
|
+
if (topicId) {
|
|
6011
|
+
try {
|
|
6012
|
+
data.upsertConversationTopicSegment(convId, String(topicId));
|
|
6013
|
+
}
|
|
6014
|
+
catch { /* best effort */ }
|
|
6015
|
+
}
|
|
6016
|
+
const existingRun = normalizeImportedPlanRunForUi(data.getLatestImportedPlanRunForConversation(convId));
|
|
6017
|
+
if (existingRun && (existingRun.status === 'paused' || existingRun.status === 'running')) {
|
|
6018
|
+
return res.status(409).json({
|
|
6019
|
+
error: 'This conversation already has an imported plan waiting for review or execution.',
|
|
6020
|
+
run: serializeImportedPlanRun(existingRun),
|
|
6021
|
+
});
|
|
6022
|
+
}
|
|
6023
|
+
const { OrchestratorAgent, buildLocalDesktopOrchestratorRuntime } = require('./orchestrator');
|
|
6024
|
+
const workflowEngine = (0, workflow_engine_1.getWorkflowEngine)(opts.projectDir, 'local_desktop');
|
|
6025
|
+
const explicitOrchestratorBot = data.isClerkOrchestratorEnabled()
|
|
6026
|
+
? null
|
|
6027
|
+
: (data.getOrchestratorBot() || null);
|
|
6028
|
+
let orchestratorRuntime;
|
|
6029
|
+
try {
|
|
6030
|
+
orchestratorRuntime = buildLocalDesktopOrchestratorRuntime(explicitOrchestratorBot);
|
|
6031
|
+
}
|
|
6032
|
+
catch (runtimeErr) {
|
|
6033
|
+
return res.status(400).json({
|
|
6034
|
+
error: runtimeErr?.message || 'Plan import needs an orchestrator or clerk runtime configured.',
|
|
6035
|
+
});
|
|
6036
|
+
}
|
|
6037
|
+
const orchestrator = new OrchestratorAgent(orchestratorRuntime, workflowEngine);
|
|
6038
|
+
const normalizedPlannerInstructions = typeof plannerInstructions === 'string' ? plannerInstructions.trim() : null;
|
|
6039
|
+
const result = await orchestrator.importPlanAsTodos({
|
|
6040
|
+
conversationId: convId,
|
|
6041
|
+
projectId: effectiveProjectId,
|
|
6042
|
+
filePath: resolvedFilePath,
|
|
6043
|
+
fileContent: rawFileContent,
|
|
6044
|
+
mode: importMode,
|
|
6045
|
+
stages: normalizedStages,
|
|
6046
|
+
plannerInstructions: normalizedPlannerInstructions || null,
|
|
6047
|
+
executionOrder: normalizedExecutionOrder === 'batch_by_stage' || normalizedExecutionOrder === 'per_item_pipeline'
|
|
6048
|
+
? normalizedExecutionOrder
|
|
6049
|
+
: null,
|
|
6050
|
+
plannerBotId: normalizedPlannerBotId,
|
|
6051
|
+
orchestratorBot: explicitOrchestratorBot,
|
|
6052
|
+
autoDispatchTodos: false,
|
|
6053
|
+
opts: {
|
|
6054
|
+
...opts,
|
|
6055
|
+
projectId: effectiveProjectId,
|
|
6056
|
+
onProgress: () => { },
|
|
6057
|
+
mqttPublish: async () => { },
|
|
6058
|
+
commandId: `plan-import-${Date.now()}`,
|
|
6059
|
+
autoDispatchTodos: false,
|
|
6060
|
+
},
|
|
6061
|
+
});
|
|
6062
|
+
const importedPlanRunForMsg = data.getImportedPlanRun(result.planRunId);
|
|
6063
|
+
const importedTasks = importedPlanRunForMsg ? data.getImportedPlanRunTasks(result.planRunId) : [];
|
|
6064
|
+
const richImportMessage = buildPlanImportChatMessage({
|
|
6065
|
+
sourceFileName: path.basename(resolvedFilePath),
|
|
6066
|
+
planner: result.planner,
|
|
6067
|
+
stages: normalizedStages,
|
|
6068
|
+
plannerInstructions: normalizedPlannerInstructions,
|
|
6069
|
+
executionOrder: importedPlanRunForMsg?.requested_options?.executionOrder || null,
|
|
6070
|
+
taskCount: result.taskIds.length,
|
|
6071
|
+
tasks: importedTasks.map((t) => ({
|
|
6072
|
+
id: t.id,
|
|
6073
|
+
owner_name: t.owner_name,
|
|
6074
|
+
task_type: t.task_type,
|
|
6075
|
+
title: t.title,
|
|
6076
|
+
state: t.state,
|
|
6077
|
+
})),
|
|
6078
|
+
status: importedPlanRunForMsg?.status || 'paused',
|
|
6079
|
+
});
|
|
6080
|
+
const assistant = data.addMessage(convId, 'assistant', richImportMessage, result.planner.model || 'Orchestrator', undefined, undefined, 'Orchestrator');
|
|
6081
|
+
res.json({
|
|
6082
|
+
ok: true,
|
|
6083
|
+
conversationId: convId,
|
|
6084
|
+
projectId: effectiveProjectId,
|
|
6085
|
+
messageId: assistant.id,
|
|
6086
|
+
planRun: serializeImportedPlanRun(importedPlanRunForMsg),
|
|
6087
|
+
planRunId: result.planRunId,
|
|
6088
|
+
importMode,
|
|
6089
|
+
planner: result.planner,
|
|
6090
|
+
taskCount: result.taskIds.length,
|
|
6091
|
+
taskIds: result.taskIds,
|
|
6092
|
+
ownerNames: result.ownerNames,
|
|
6093
|
+
summary: richImportMessage,
|
|
6094
|
+
});
|
|
6095
|
+
}
|
|
6096
|
+
catch (err) {
|
|
6097
|
+
res.status(500).json({ error: err.message });
|
|
6098
|
+
}
|
|
6099
|
+
});
|
|
6100
|
+
app.post('/api/orchestration/import-plan/start', async (req, res) => {
|
|
6101
|
+
try {
|
|
6102
|
+
if (isConnectedMode()) {
|
|
6103
|
+
return res.status(501).json({ error: 'Imported plan execution is only implemented for local desktop storage right now.' });
|
|
6104
|
+
}
|
|
6105
|
+
const runId = String(req.body?.runId || '').trim();
|
|
6106
|
+
if (!runId)
|
|
6107
|
+
return res.status(400).json({ error: 'runId is required' });
|
|
6108
|
+
const run = normalizeImportedPlanRunForUi(data.getImportedPlanRun(runId));
|
|
6109
|
+
if (!run)
|
|
6110
|
+
return res.status(404).json({ error: 'Imported plan run not found.' });
|
|
6111
|
+
if (run.status === 'discarded')
|
|
6112
|
+
return res.status(400).json({ error: 'This imported plan was discarded.' });
|
|
6113
|
+
if (activeImportedPlanRunners.has(run.id)) {
|
|
6114
|
+
return res.json({ ok: true, started: false, run: serializeImportedPlanRun(run), summary: 'Imported plan is already running.' });
|
|
6115
|
+
}
|
|
6116
|
+
const project = run.project_id ? data.getProject(run.project_id) : undefined;
|
|
6117
|
+
const { OrchestratorAgent, buildLocalDesktopOrchestratorRuntime } = require('./orchestrator');
|
|
6118
|
+
const workflowEngine = (0, workflow_engine_1.getWorkflowEngine)(project?.folder || opts.projectDir, 'local_desktop');
|
|
6119
|
+
const explicitOrchestratorBot = data.isClerkOrchestratorEnabled()
|
|
6120
|
+
? null
|
|
6121
|
+
: (data.getOrchestratorBot() || null);
|
|
6122
|
+
let orchestratorRuntime;
|
|
6123
|
+
try {
|
|
6124
|
+
orchestratorRuntime = buildLocalDesktopOrchestratorRuntime(explicitOrchestratorBot);
|
|
6125
|
+
}
|
|
6126
|
+
catch (runtimeErr) {
|
|
6127
|
+
return res.status(400).json({
|
|
6128
|
+
error: runtimeErr?.message || 'Imported plan execution needs an orchestrator or clerk runtime configured.',
|
|
6129
|
+
});
|
|
6130
|
+
}
|
|
6131
|
+
const orchestrator = new OrchestratorAgent(orchestratorRuntime, workflowEngine);
|
|
6132
|
+
const startAllTasks = data.getImportedPlanRunTasks(run.id);
|
|
6133
|
+
const startRemainingIds = data.getImportedPlanRunRemainingTaskIds(run.id);
|
|
6134
|
+
const startFirstTask = startRemainingIds.length > 0
|
|
6135
|
+
? data.getTodoTask(startRemainingIds[0], 'active')
|
|
6136
|
+
: null;
|
|
6137
|
+
const isResumingPlan = !!run.started_at;
|
|
6138
|
+
const startedSummary = buildPlanStartChatMessage({
|
|
6139
|
+
sourceFileName: run.source_file_name,
|
|
6140
|
+
isResume: isResumingPlan,
|
|
6141
|
+
remainingCount: startRemainingIds.length,
|
|
6142
|
+
totalCount: startAllTasks.length,
|
|
6143
|
+
firstTask: startFirstTask ? {
|
|
6144
|
+
id: startFirstTask.id,
|
|
6145
|
+
title: startFirstTask.title,
|
|
6146
|
+
owner_name: startFirstTask.owner_name,
|
|
6147
|
+
task_type: startFirstTask.task_type,
|
|
6148
|
+
} : null,
|
|
6149
|
+
executionOrder: run.requested_options?.executionOrder || null,
|
|
6150
|
+
status: 'running',
|
|
6151
|
+
});
|
|
6152
|
+
const assistant = data.addMessage(run.conversation_id, 'assistant', startedSummary, orchestratorRuntime.model || 'Orchestrator', undefined, undefined, 'Orchestrator');
|
|
6153
|
+
const activityStreamId = `activity-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
6154
|
+
const activityExpiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000)
|
|
6155
|
+
.toISOString()
|
|
6156
|
+
.replace('T', ' ')
|
|
6157
|
+
.replace('Z', '');
|
|
6158
|
+
const workerBotByName = new Map();
|
|
6159
|
+
const resolveWorkerBotId = (agentName) => {
|
|
6160
|
+
const key = String(agentName || '').trim().toLowerCase();
|
|
6161
|
+
if (!key)
|
|
6162
|
+
return null;
|
|
6163
|
+
if (workerBotByName.has(key))
|
|
6164
|
+
return workerBotByName.get(key) || null;
|
|
6165
|
+
const match = data.listAgentProfiles().find((agent) => agent.name.trim().toLowerCase() === key);
|
|
6166
|
+
const resolved = match?.id || null;
|
|
6167
|
+
workerBotByName.set(key, resolved || '');
|
|
6168
|
+
return resolved;
|
|
6169
|
+
};
|
|
6170
|
+
const recordActivity = (activityType, payload, summary) => {
|
|
6171
|
+
try {
|
|
6172
|
+
data.createMessageActivity({
|
|
6173
|
+
conversationId: run.conversation_id,
|
|
6174
|
+
messageId: String(assistant.id),
|
|
6175
|
+
streamId: activityStreamId,
|
|
6176
|
+
botId: null,
|
|
6177
|
+
agentName: 'Orchestrator',
|
|
6178
|
+
activityType,
|
|
6179
|
+
summary: summary || null,
|
|
6180
|
+
payload,
|
|
6181
|
+
expiresAt: activityExpiresAt,
|
|
6182
|
+
});
|
|
6183
|
+
}
|
|
6184
|
+
catch {
|
|
6185
|
+
// Best effort only
|
|
6186
|
+
}
|
|
6187
|
+
};
|
|
6188
|
+
const recordWorkerActivity = (activityType, event, payload, summary) => {
|
|
6189
|
+
try {
|
|
6190
|
+
data.createMessageActivity({
|
|
6191
|
+
conversationId: run.conversation_id,
|
|
6192
|
+
messageId: String(assistant.id),
|
|
6193
|
+
streamId: activityStreamId,
|
|
6194
|
+
botId: resolveWorkerBotId(event.agentName) || null,
|
|
6195
|
+
agentName: event.agentName || null,
|
|
6196
|
+
activityType,
|
|
6197
|
+
summary: summary || null,
|
|
6198
|
+
payload,
|
|
6199
|
+
expiresAt: activityExpiresAt,
|
|
6200
|
+
});
|
|
6201
|
+
}
|
|
6202
|
+
catch {
|
|
6203
|
+
// Best effort only
|
|
6204
|
+
}
|
|
6205
|
+
};
|
|
6206
|
+
recordActivity('status', { phase: 'starting', detail: startedSummary }, startedSummary);
|
|
6207
|
+
const activeRunner = {
|
|
6208
|
+
runId: run.id,
|
|
6209
|
+
conversationId: run.conversation_id,
|
|
6210
|
+
sessionScopeKey: buildImportedPlanSessionScopeKey(run.id, run.conversation_id),
|
|
6211
|
+
abortController: new AbortController(),
|
|
6212
|
+
stopRequested: false,
|
|
6213
|
+
stopAction: null,
|
|
6214
|
+
};
|
|
6215
|
+
activeImportedPlanRunners.set(run.id, activeRunner);
|
|
6216
|
+
void (async () => {
|
|
6217
|
+
try {
|
|
6218
|
+
let lastProgressActivity = '';
|
|
6219
|
+
let lastInterimMessage = '';
|
|
6220
|
+
const started = await orchestrator.startImportedPlanRun({
|
|
6221
|
+
runId: run.id,
|
|
6222
|
+
orchestratorBot: explicitOrchestratorBot,
|
|
6223
|
+
opts: {
|
|
6224
|
+
...opts,
|
|
6225
|
+
projectId: run.project_id || undefined,
|
|
6226
|
+
projectDir: project?.folder || opts.projectDir,
|
|
6227
|
+
abortSignal: activeRunner.abortController.signal,
|
|
6228
|
+
importedPlanRunId: run.id,
|
|
6229
|
+
cliSessionScopeKey: activeRunner.sessionScopeKey,
|
|
6230
|
+
onProgress: (status) => {
|
|
6231
|
+
const progressUpdate = classifyOrchestratorProgress(status);
|
|
6232
|
+
if (progressUpdate.activityText && progressUpdate.activityText !== lastProgressActivity) {
|
|
6233
|
+
lastProgressActivity = progressUpdate.activityText;
|
|
6234
|
+
recordActivity('status', { phase: 'running', detail: progressUpdate.activityText }, progressUpdate.activityText);
|
|
6235
|
+
}
|
|
6236
|
+
const interimMessage = deriveVisibleOrchestratorMessage(status);
|
|
6237
|
+
if (interimMessage && interimMessage !== lastInterimMessage) {
|
|
6238
|
+
lastInterimMessage = interimMessage;
|
|
6239
|
+
recordActivity('orchestrator_interim', { text: interimMessage }, interimMessage);
|
|
6240
|
+
}
|
|
6241
|
+
},
|
|
6242
|
+
onWorkerChunk: (event) => {
|
|
6243
|
+
if (event.type === 'step_start') {
|
|
6244
|
+
recordWorkerActivity('worker_step_start', event, {
|
|
6245
|
+
stepId: event.stepId,
|
|
6246
|
+
botId: resolveWorkerBotId(event.agentName),
|
|
6247
|
+
agentName: event.agentName,
|
|
6248
|
+
description: event.description,
|
|
6249
|
+
stepIndex: event.stepIndex,
|
|
6250
|
+
totalSteps: event.totalSteps,
|
|
6251
|
+
});
|
|
6252
|
+
}
|
|
6253
|
+
else if (event.type === 'worker_chunk') {
|
|
6254
|
+
recordWorkerActivity('worker_chunk', event, {
|
|
6255
|
+
stepId: event.stepId,
|
|
6256
|
+
botId: resolveWorkerBotId(event.agentName),
|
|
6257
|
+
agentName: event.agentName,
|
|
6258
|
+
description: event.description,
|
|
6259
|
+
stepIndex: event.stepIndex,
|
|
6260
|
+
totalSteps: event.totalSteps,
|
|
6261
|
+
text: event.text,
|
|
6262
|
+
});
|
|
6263
|
+
}
|
|
6264
|
+
else if (event.type === 'worker_tool_call') {
|
|
6265
|
+
recordWorkerActivity('worker_tool_call', event, {
|
|
6266
|
+
stepId: event.stepId,
|
|
6267
|
+
botId: resolveWorkerBotId(event.agentName),
|
|
6268
|
+
agentName: event.agentName,
|
|
6269
|
+
description: event.description,
|
|
6270
|
+
stepIndex: event.stepIndex,
|
|
6271
|
+
totalSteps: event.totalSteps,
|
|
6272
|
+
toolCallId: event.toolCallId,
|
|
6273
|
+
toolName: event.toolName,
|
|
6274
|
+
toolArguments: event.toolArguments,
|
|
6275
|
+
});
|
|
6276
|
+
}
|
|
6277
|
+
else if (event.type === 'worker_tool_result') {
|
|
6278
|
+
recordWorkerActivity('worker_tool_result', event, {
|
|
6279
|
+
stepId: event.stepId,
|
|
6280
|
+
botId: resolveWorkerBotId(event.agentName),
|
|
6281
|
+
agentName: event.agentName,
|
|
6282
|
+
description: event.description,
|
|
6283
|
+
stepIndex: event.stepIndex,
|
|
6284
|
+
totalSteps: event.totalSteps,
|
|
6285
|
+
toolCallId: event.toolCallId,
|
|
6286
|
+
toolName: event.toolName,
|
|
6287
|
+
toolOutput: event.toolOutput,
|
|
6288
|
+
toolIsError: event.toolIsError,
|
|
6289
|
+
});
|
|
6290
|
+
}
|
|
6291
|
+
else if (event.type === 'step_done') {
|
|
6292
|
+
recordWorkerActivity('worker_step_done', event, {
|
|
6293
|
+
stepId: event.stepId,
|
|
6294
|
+
botId: resolveWorkerBotId(event.agentName),
|
|
6295
|
+
agentName: event.agentName,
|
|
6296
|
+
description: event.description,
|
|
6297
|
+
stepIndex: event.stepIndex,
|
|
6298
|
+
totalSteps: event.totalSteps,
|
|
6299
|
+
status: event.status,
|
|
6300
|
+
summary: event.summary,
|
|
6301
|
+
});
|
|
6302
|
+
}
|
|
6303
|
+
},
|
|
6304
|
+
mqttPublish: async () => { },
|
|
6305
|
+
commandId: `plan-run-${run.id}-${Date.now()}`,
|
|
6306
|
+
},
|
|
6307
|
+
});
|
|
6308
|
+
const summary = (started.replyText || '').trim();
|
|
6309
|
+
if (summary && !activeRunner.stopRequested) {
|
|
6310
|
+
data.addMessage(run.conversation_id, 'assistant', summary, orchestratorRuntime.model || 'Orchestrator', undefined, undefined, 'Orchestrator');
|
|
6311
|
+
}
|
|
6312
|
+
}
|
|
6313
|
+
catch (startErr) {
|
|
6314
|
+
if (activeRunner.stopRequested || startErr?.name === 'AbortError') {
|
|
6315
|
+
const pausedRun = data.getImportedPlanRun(run.id) || run;
|
|
6316
|
+
data.updateImportedPlanRun(run.id, {
|
|
6317
|
+
status: 'paused',
|
|
6318
|
+
completedAt: null,
|
|
6319
|
+
lastError: pausedRun.last_error || importedPlanPauseSummary(activeRunner.stopAction || 'pause'),
|
|
6320
|
+
});
|
|
6321
|
+
return;
|
|
6322
|
+
}
|
|
6323
|
+
const errorDetail = startErr?.message || 'Imported plan execution failed.';
|
|
6324
|
+
data.updateImportedPlanRun(run.id, { status: 'paused', lastError: errorDetail });
|
|
6325
|
+
const failureMessage = buildPlanFailureChatMessage({
|
|
6326
|
+
sourceFileName: run.source_file_name,
|
|
6327
|
+
error: errorDetail,
|
|
6328
|
+
status: 'paused (was running)',
|
|
6329
|
+
});
|
|
6330
|
+
data.addMessage(run.conversation_id, 'assistant', failureMessage, orchestratorRuntime.model || 'Orchestrator', undefined, undefined, 'Orchestrator');
|
|
6331
|
+
}
|
|
6332
|
+
finally {
|
|
6333
|
+
activeImportedPlanRunners.delete(run.id);
|
|
6334
|
+
teardownImportedPlanRunnerSessions(data.getImportedPlanRun(run.id) || run, activeRunner.sessionScopeKey);
|
|
6335
|
+
}
|
|
6336
|
+
})();
|
|
6337
|
+
res.json({
|
|
6338
|
+
ok: true,
|
|
6339
|
+
started: true,
|
|
6340
|
+
messageId: assistant.id,
|
|
6341
|
+
run: serializeImportedPlanRun(data.getImportedPlanRun(run.id)),
|
|
6342
|
+
summary: startedSummary,
|
|
6343
|
+
});
|
|
6344
|
+
}
|
|
6345
|
+
catch (err) {
|
|
6346
|
+
res.status(500).json({ error: err.message });
|
|
6347
|
+
}
|
|
6348
|
+
});
|
|
6349
|
+
app.post('/api/orchestration/import-plan/delete-selected', (req, res) => {
|
|
6350
|
+
try {
|
|
6351
|
+
const runId = String(req.body?.runId || '').trim();
|
|
6352
|
+
const taskIds = Array.isArray(req.body?.taskIds) ? req.body.taskIds : [];
|
|
6353
|
+
if (!runId)
|
|
6354
|
+
return res.status(400).json({ error: 'runId is required' });
|
|
6355
|
+
const updatedRun = data.deleteImportedPlanRunSelectedTasks(runId, taskIds, { actorType: 'user', actorId: 'plan-import-review' });
|
|
6356
|
+
if (!updatedRun)
|
|
6357
|
+
return res.status(404).json({ error: 'Imported plan run not found.' });
|
|
6358
|
+
res.json({ ok: true, run: serializeImportedPlanRun(updatedRun) });
|
|
6359
|
+
}
|
|
6360
|
+
catch (err) {
|
|
6361
|
+
res.status(500).json({ error: err.message });
|
|
6362
|
+
}
|
|
6363
|
+
});
|
|
3766
6364
|
app.get('/api/admin/audit', (req, res) => {
|
|
3767
6365
|
try {
|
|
3768
6366
|
const rows = data.listAdminAudit({
|
|
@@ -3804,14 +6402,19 @@ function startLocalServer(opts) {
|
|
|
3804
6402
|
// ─── Clerk Config ──────────────────────────────────────────
|
|
3805
6403
|
app.get('/api/clerk/config', (_req, res) => {
|
|
3806
6404
|
try {
|
|
3807
|
-
const
|
|
3808
|
-
const
|
|
3809
|
-
const
|
|
6405
|
+
const clerkConfig = data.getResolvedClerkConfigInfo();
|
|
6406
|
+
const currentOrchestrator = data.getCurrentOrchestratorSelection();
|
|
6407
|
+
const currentDefaultBot = data.getDefaultAgentProfile();
|
|
3810
6408
|
res.json({
|
|
3811
|
-
provider:
|
|
3812
|
-
model:
|
|
3813
|
-
hasApiKey:
|
|
3814
|
-
configured:
|
|
6409
|
+
provider: clerkConfig.provider,
|
|
6410
|
+
model: clerkConfig.model,
|
|
6411
|
+
hasApiKey: clerkConfig.hasSecret,
|
|
6412
|
+
configured: clerkConfig.configured,
|
|
6413
|
+
isOrchestrator: data.isClerkOrchestratorEnabled(),
|
|
6414
|
+
currentOrchestrator,
|
|
6415
|
+
currentDefaultBot: currentDefaultBot
|
|
6416
|
+
? { botId: currentDefaultBot.id, botName: currentDefaultBot.name }
|
|
6417
|
+
: null,
|
|
3815
6418
|
});
|
|
3816
6419
|
}
|
|
3817
6420
|
catch (err) {
|
|
@@ -3820,13 +6423,16 @@ function startLocalServer(opts) {
|
|
|
3820
6423
|
});
|
|
3821
6424
|
app.put('/api/clerk/config', (req, res) => {
|
|
3822
6425
|
try {
|
|
3823
|
-
const { provider, model, apiKey } = req.body;
|
|
6426
|
+
const { provider, model, apiKey, isOrchestrator } = req.body;
|
|
3824
6427
|
if (!provider || !model)
|
|
3825
6428
|
return res.status(400).json({ error: 'provider and model are required' });
|
|
3826
6429
|
data.setSetting('clerk_provider', provider);
|
|
3827
6430
|
data.setSetting('clerk_model', model);
|
|
3828
6431
|
if (apiKey)
|
|
3829
6432
|
data.setSetting('clerk_api_key', apiKey);
|
|
6433
|
+
if (typeof isOrchestrator === 'boolean') {
|
|
6434
|
+
data.setClerkAsOrchestrator(isOrchestrator);
|
|
6435
|
+
}
|
|
3830
6436
|
// Reset clerk instance so it picks up new config
|
|
3831
6437
|
const { resetClerk } = require('./clerk-model');
|
|
3832
6438
|
resetClerk();
|
|
@@ -3842,11 +6448,31 @@ function startLocalServer(opts) {
|
|
|
3842
6448
|
const { prompt } = req.body;
|
|
3843
6449
|
if (!prompt)
|
|
3844
6450
|
return res.status(400).json({ error: 'prompt is required' });
|
|
3845
|
-
const clerk = (0, clerk_model_1.getClerk)({ runtimeMode: 'local_desktop' });
|
|
3846
|
-
if (!clerk)
|
|
3847
|
-
return res.json({ routing: 'default', reason: 'No clerk configured' });
|
|
3848
6451
|
const agents = data.listAgentProfiles();
|
|
3849
|
-
const
|
|
6452
|
+
const normalized = String(prompt || '').toLowerCase();
|
|
6453
|
+
const defaultAgent = data.getDefaultAgentProfile() || agents[0];
|
|
6454
|
+
const namedAgent = agents.find((agent) => normalized.includes(agent.name.toLowerCase()));
|
|
6455
|
+
const roleMatchedAgent = /\b(qa|review|verify|test)\b/i.test(normalized)
|
|
6456
|
+
? agents.find((agent) => /\bqa|review\b/i.test(String(agent.role_class || agent.role_label || '')))
|
|
6457
|
+
: /\b(research|brainstorm|analy[sz]e|evaluate|investigate)\b/i.test(normalized)
|
|
6458
|
+
? agents.find((agent) => /\bresearch|analyst\b/i.test(String(agent.role_class || agent.role_label || '')))
|
|
6459
|
+
: /\b(build|code|implement|create|fix|write|update)\b/i.test(normalized)
|
|
6460
|
+
? agents.find((agent) => /\bcode|coding|developer|builder\b/i.test(String(agent.role_class || agent.role_label || '')))
|
|
6461
|
+
: undefined;
|
|
6462
|
+
const routeAgent = namedAgent || roleMatchedAgent || defaultAgent;
|
|
6463
|
+
const route = routeAgent
|
|
6464
|
+
? {
|
|
6465
|
+
agentId: routeAgent.id,
|
|
6466
|
+
agentName: routeAgent.name,
|
|
6467
|
+
provider: routeAgent.provider,
|
|
6468
|
+
model: routeAgent.model,
|
|
6469
|
+
reasoning: namedAgent
|
|
6470
|
+
? 'Matched the explicitly named bot.'
|
|
6471
|
+
: roleMatchedAgent
|
|
6472
|
+
? 'Matched the prompt to the configured bot role.'
|
|
6473
|
+
: 'Fell back to the configured default bot.',
|
|
6474
|
+
}
|
|
6475
|
+
: { routing: 'default', reason: 'No bot configured' };
|
|
3850
6476
|
res.json(route);
|
|
3851
6477
|
}
|
|
3852
6478
|
catch (err) {
|
|
@@ -4679,6 +7305,8 @@ function startLocalServer(opts) {
|
|
|
4679
7305
|
});
|
|
4680
7306
|
// Initialize workflow engine
|
|
4681
7307
|
(0, workflow_engine_1.getWorkflowEngine)(opts.projectDir, 'local_desktop');
|
|
7308
|
+
// Start idle session sweep (checks every 60s, reaps after 30min idle)
|
|
7309
|
+
(0, managed_process_registry_1.startIdleSweep)(handleIdleSweepClose);
|
|
4682
7310
|
// Start server
|
|
4683
7311
|
return new Promise((resolve, reject) => {
|
|
4684
7312
|
_server = app.listen(port, '127.0.0.1', () => {
|
|
@@ -4686,6 +7314,12 @@ function startLocalServer(opts) {
|
|
|
4686
7314
|
console.log(chalk_1.default.green(`\n Local server: http://127.0.0.1:${port}`));
|
|
4687
7315
|
resolve(_server);
|
|
4688
7316
|
});
|
|
7317
|
+
_server.on('close', () => {
|
|
7318
|
+
(0, managed_process_registry_1.stopIdleSweep)();
|
|
7319
|
+
for (const botId of [...cliBotConfigWatchers.keys()]) {
|
|
7320
|
+
closeCliBotConfigWatcher(botId);
|
|
7321
|
+
}
|
|
7322
|
+
});
|
|
4689
7323
|
_server.on('error', (err) => {
|
|
4690
7324
|
if (err.code === 'EADDRINUSE') {
|
|
4691
7325
|
console.error(chalk_1.default.red(`Port ${port} already in use`));
|
|
@@ -4695,6 +7329,7 @@ function startLocalServer(opts) {
|
|
|
4695
7329
|
});
|
|
4696
7330
|
}
|
|
4697
7331
|
function stopLocalServer() {
|
|
7332
|
+
(0, managed_process_registry_1.stopIdleSweep)();
|
|
4698
7333
|
return new Promise((resolve) => {
|
|
4699
7334
|
if (_server) {
|
|
4700
7335
|
_server.close(() => {
|
|
@@ -4752,6 +7387,13 @@ function runtimeModeLabel(mode, runtimeSource) {
|
|
|
4752
7387
|
return (0, subscription_runtime_1.claudeSubscriptionRuntimeLabel)(runtimeSource);
|
|
4753
7388
|
return 'API Key';
|
|
4754
7389
|
}
|
|
7390
|
+
function resolveDirectCliSessionTransport(providerName, enableCliSessionEpoch, localStorageMode) {
|
|
7391
|
+
if (!enableCliSessionEpoch)
|
|
7392
|
+
return 'none';
|
|
7393
|
+
if (providerName === 'codex-cli' && localStorageMode)
|
|
7394
|
+
return 'codex-app-server';
|
|
7395
|
+
return 'pty';
|
|
7396
|
+
}
|
|
4755
7397
|
function runtimePayloadForDisplay(providerName, model, runtimeMode, runtimeSource) {
|
|
4756
7398
|
return {
|
|
4757
7399
|
mode: runtimeMode,
|
|
@@ -4783,6 +7425,9 @@ function configuredRuntimeLabelForProfile(profile) {
|
|
|
4783
7425
|
async function buildChatRuntimeForTest(profile) {
|
|
4784
7426
|
return buildChatRuntime(profile);
|
|
4785
7427
|
}
|
|
7428
|
+
function resolveDirectCliSessionTransportForTest(providerName, enableCliSessionEpoch, localStorageMode) {
|
|
7429
|
+
return resolveDirectCliSessionTransport(providerName, enableCliSessionEpoch, localStorageMode);
|
|
7430
|
+
}
|
|
4786
7431
|
function buildConfiguredMessageModel(profile) {
|
|
4787
7432
|
if (!profile)
|
|
4788
7433
|
return '';
|
|
@@ -4808,100 +7453,206 @@ function hydrateMessageDisplayMetadata(message) {
|
|
|
4808
7453
|
model: [modelBase, runtimeLabel].filter(Boolean).join(' | '),
|
|
4809
7454
|
};
|
|
4810
7455
|
}
|
|
7456
|
+
function isSameTurn(a, b) {
|
|
7457
|
+
return String(a.userMessage.id || '') === String(b.userMessage.id || '');
|
|
7458
|
+
}
|
|
7459
|
+
function shouldSkipFreshCliBootstrap(input) {
|
|
7460
|
+
return input.providerName === 'codex-cli'
|
|
7461
|
+
&& !input.resumeSessionId
|
|
7462
|
+
&& input.promptContextMode === 'new_topic';
|
|
7463
|
+
}
|
|
7464
|
+
function resolveLocalDesktopPromptContext(input) {
|
|
7465
|
+
const project = input.conversation?.project_id ? data.getProject(input.conversation.project_id) : undefined;
|
|
7466
|
+
const configuredWorkspacePath = String(project?.folder || '').trim();
|
|
7467
|
+
const llmSpawnCwd = configuredWorkspacePath && fs.existsSync(configuredWorkspacePath)
|
|
7468
|
+
? configuredWorkspacePath
|
|
7469
|
+
: input.fallbackCwd;
|
|
7470
|
+
const normalizedExplicitTopicId = String(input.explicitTopicId || '').trim();
|
|
7471
|
+
const topicId = normalizedExplicitTopicId || data.getPrimaryTopicIdForConversation(input.conversationId) || undefined;
|
|
7472
|
+
const topicTitle = topicId ? data.getTopic(topicId)?.title : undefined;
|
|
7473
|
+
return {
|
|
7474
|
+
llmSpawnCwd,
|
|
7475
|
+
workspacePath: llmSpawnCwd,
|
|
7476
|
+
projectName: project?.name || input.conversation?.project_name || undefined,
|
|
7477
|
+
topicId,
|
|
7478
|
+
topicTitle: topicTitle || undefined,
|
|
7479
|
+
cliHistoryFilePath: input.writeCliHistoryBootstrap
|
|
7480
|
+
? (0, cli_bootstrap_history_1.writeConversationBootstrapHistoryFile)({
|
|
7481
|
+
conversationId: input.conversationId,
|
|
7482
|
+
topicId,
|
|
7483
|
+
})
|
|
7484
|
+
: null,
|
|
7485
|
+
};
|
|
7486
|
+
}
|
|
7487
|
+
function buildLocalDesktopDirectPrompt(input) {
|
|
7488
|
+
const isCliProvider = index_1.CLI_PROVIDERS.has(input.currentProvider);
|
|
7489
|
+
const useCompletionSentinel = !!input.useCompletionSentinel;
|
|
7490
|
+
const forceInlinePromptContext = !!input.forceInlinePromptContext;
|
|
7491
|
+
const useCliHistoryBootstrap = isCliProvider && !input.isCliRecurring && !forceInlinePromptContext;
|
|
7492
|
+
const crossBotBlock = input.crossBotTurn ? (0, context_window_1.formatCrossBotPreviousTurn)(input.crossBotTurn) : null;
|
|
7493
|
+
const contract = !isCliProvider || !input.isCliRecurring
|
|
7494
|
+
? 'api_or_fresh_cli'
|
|
7495
|
+
: 'cli_recurring_single';
|
|
7496
|
+
const lines = [
|
|
7497
|
+
'[Bot Identity]',
|
|
7498
|
+
input.soulMd.trim(),
|
|
7499
|
+
'',
|
|
7500
|
+
'If the provided context is not enough, use available tools to get what you need.',
|
|
7501
|
+
'',
|
|
7502
|
+
'[Response Style]',
|
|
7503
|
+
'Write in short readable paragraphs.',
|
|
7504
|
+
'Put a blank line between distinct ideas.',
|
|
7505
|
+
'Use bullets when listing findings, steps, or issues.',
|
|
7506
|
+
'Do not return one dense wall of text.',
|
|
7507
|
+
'Keep progress updates compact and factual.',
|
|
7508
|
+
'Do not mention bootstrap files, history files, file paths, or that you loaded context unless the user explicitly asks about them.',
|
|
7509
|
+
];
|
|
7510
|
+
if (useCompletionSentinel) {
|
|
7511
|
+
lines.push(completion_marker_1.CLI_COMPLETION_INSTRUCTION);
|
|
7512
|
+
}
|
|
7513
|
+
const projectLines = [];
|
|
7514
|
+
if (input.projectName)
|
|
7515
|
+
projectLines.push(`Project: ${input.projectName}`);
|
|
7516
|
+
if (input.topicTitle)
|
|
7517
|
+
projectLines.push(`Topic: ${input.topicTitle}`);
|
|
7518
|
+
projectLines.push(`Workspace: ${input.workspacePath || '(project folder not configured)'}`);
|
|
7519
|
+
if (input.timezone)
|
|
7520
|
+
projectLines.push(`Timezone: ${input.timezone}`);
|
|
7521
|
+
lines.push('', '[Project Overview]', ...projectLines);
|
|
7522
|
+
const toolManifest = isCliProvider ? '' : buildLocalDesktopToolManifest(input.availableTools, contract);
|
|
7523
|
+
if (toolManifest) {
|
|
7524
|
+
lines.push('', '[Available Tools]', toolManifest);
|
|
7525
|
+
if (/\bsearch_local_memory\b|\bsearch_memory\b/i.test(toolManifest)) {
|
|
7526
|
+
lines.push('If the user refers to prior project or conversation context that is not included here, search with these tools first.');
|
|
7527
|
+
}
|
|
7528
|
+
}
|
|
7529
|
+
// Worker TODO mechanics paragraph (Decision 7 of orchestration-plan.txt).
|
|
7530
|
+
// Advertises ONLY the TODO tools actually available to this bot. Respects
|
|
7531
|
+
// Codex Guardrail #6: don't advertise tools the bot can't call.
|
|
7532
|
+
const toolNamesAvailable = new Set((input.availableTools || [])
|
|
7533
|
+
.map((tool) => String(tool?.name || '').trim().toLowerCase())
|
|
7534
|
+
.filter(Boolean));
|
|
7535
|
+
const todoToolMentions = [];
|
|
7536
|
+
if (toolNamesAvailable.has('list_tasks')) {
|
|
7537
|
+
todoToolMentions.push('Use `list_tasks` to query the TODO list (filter by `owner` for specific bots).');
|
|
7538
|
+
}
|
|
7539
|
+
if (toolNamesAvailable.has('check_task')) {
|
|
7540
|
+
todoToolMentions.push('Use `check_task` to mark an item complete.');
|
|
7541
|
+
}
|
|
7542
|
+
if (toolNamesAvailable.has('add_task')) {
|
|
7543
|
+
todoToolMentions.push('Use `add_task` to add new items.');
|
|
7544
|
+
}
|
|
7545
|
+
if (toolNamesAvailable.has('edit_task')) {
|
|
7546
|
+
todoToolMentions.push('Use `edit_task` to modify.');
|
|
7547
|
+
}
|
|
7548
|
+
if (toolNamesAvailable.has('delete_task')) {
|
|
7549
|
+
todoToolMentions.push('Use `delete_task` to remove.');
|
|
7550
|
+
}
|
|
7551
|
+
if (todoToolMentions.length > 0) {
|
|
7552
|
+
lines.push('', '[TODO List Tools]', ...todoToolMentions);
|
|
7553
|
+
}
|
|
7554
|
+
if (crossBotBlock && !input.isCliRecurring) {
|
|
7555
|
+
lines.push('', '[Immediate Cross-Bot Handoff]', crossBotBlock);
|
|
7556
|
+
}
|
|
7557
|
+
if ((contract === 'api_or_fresh_cli' && !useCliHistoryBootstrap) || forceInlinePromptContext) {
|
|
7558
|
+
const summaryWindow = (0, context_window_1.getPromptContextWindow)(input.conversationId, safeguards_1.SAFEGUARDS.CONTEXT_WINDOW_TURNS);
|
|
7559
|
+
if (summaryWindow.summary?.summary_text?.trim()) {
|
|
7560
|
+
lines.push('', summaryWindow.carriedForward
|
|
7561
|
+
? '[Context Summary (Carried Forward from Previous Conversation in This Topic)]'
|
|
7562
|
+
: '[Context Summary]', summaryWindow.summary.summary_text.trim());
|
|
7563
|
+
}
|
|
7564
|
+
const recentTurnsWindow = (0, context_window_1.getPromptContextWindow)(input.conversationId, summaryWindow.summary?.summary_text?.trim()
|
|
7565
|
+
? safeguards_1.SAFEGUARDS.CONTEXT_WINDOW_TURNS
|
|
7566
|
+
: safeguards_1.SAFEGUARDS.NO_SUMMARY_CONTEXT_WINDOW_TURNS);
|
|
7567
|
+
const recentTurns = crossBotBlock
|
|
7568
|
+
? recentTurnsWindow.turns.filter((turn) => !input.crossBotTurn || !isSameTurn(turn, input.crossBotTurn))
|
|
7569
|
+
: recentTurnsWindow.turns;
|
|
7570
|
+
if (recentTurns.length > 0) {
|
|
7571
|
+
lines.push('', recentTurnsWindow.carriedForward
|
|
7572
|
+
? '[Recent Messages (Carried Forward from Previous Conversation in This Topic)]'
|
|
7573
|
+
: '[Recent Messages]', (0, context_window_1.formatTurnsForPrompt)(recentTurns));
|
|
7574
|
+
}
|
|
7575
|
+
}
|
|
7576
|
+
let effectiveUserPrompt = input.userPrompt;
|
|
7577
|
+
if (useCliHistoryBootstrap && input.cliHistoryFilePath) {
|
|
7578
|
+
effectiveUserPrompt = [
|
|
7579
|
+
`Please read the history file at: ${input.cliHistoryFilePath}`,
|
|
7580
|
+
'Use it for context only.',
|
|
7581
|
+
'There is no need to mention the file, its path, or that you loaded context unless the user explicitly asks about it.',
|
|
7582
|
+
'Then respond to the user request below.',
|
|
7583
|
+
'',
|
|
7584
|
+
'Current user request:',
|
|
7585
|
+
input.userPrompt,
|
|
7586
|
+
].join('\n');
|
|
7587
|
+
}
|
|
7588
|
+
if (useCompletionSentinel) {
|
|
7589
|
+
effectiveUserPrompt = [
|
|
7590
|
+
effectiveUserPrompt,
|
|
7591
|
+
'',
|
|
7592
|
+
`Required final line when the response is fully complete: ${completion_marker_1.CLI_COMPLETION_SENTINEL}`,
|
|
7593
|
+
'Do not mention or explain the tag.',
|
|
7594
|
+
].join('\n');
|
|
7595
|
+
}
|
|
7596
|
+
return {
|
|
7597
|
+
systemPrompt: lines.join('\n').trim(),
|
|
7598
|
+
userPrompt: effectiveUserPrompt,
|
|
7599
|
+
};
|
|
7600
|
+
}
|
|
7601
|
+
function buildLocalDesktopDirectPromptForTest(input) {
|
|
7602
|
+
return buildLocalDesktopDirectPrompt(input);
|
|
7603
|
+
}
|
|
7604
|
+
function resolveLocalDesktopPromptContextForTest(input) {
|
|
7605
|
+
return resolveLocalDesktopPromptContext(input);
|
|
7606
|
+
}
|
|
7607
|
+
function shouldSkipFreshCliBootstrapForTest(input) {
|
|
7608
|
+
return shouldSkipFreshCliBootstrap(input);
|
|
7609
|
+
}
|
|
7610
|
+
function buildLocalRuntimeUserFacingErrorForTest(err) {
|
|
7611
|
+
return buildLocalRuntimeUserFacingError(err);
|
|
7612
|
+
}
|
|
7613
|
+
function persistCliOauthSessionForTest(input) {
|
|
7614
|
+
return persistCliOauthSession(input.providerId, input.accessToken, input.refreshToken, input.expiresAt);
|
|
7615
|
+
}
|
|
7616
|
+
function buildLocalDesktopToolManifest(availableTools, contract) {
|
|
7617
|
+
const selectedTools = contract === 'api_or_fresh_cli'
|
|
7618
|
+
? availableTools
|
|
7619
|
+
: availableTools.filter((tool) => ['search_local_memory', 'search_memory'].includes(tool.name));
|
|
7620
|
+
return selectedTools
|
|
7621
|
+
.map((tool) => `- ${tool.name}: ${tool.description}`)
|
|
7622
|
+
.join('\n')
|
|
7623
|
+
.trim();
|
|
7624
|
+
}
|
|
4811
7625
|
function classifyOrchestratorProgress(status) {
|
|
4812
7626
|
const trimmed = String(status || '').trim();
|
|
4813
7627
|
if (!trimmed)
|
|
4814
7628
|
return {};
|
|
4815
7629
|
const match = trimmed.match(/^([a-z][a-z0-9_ -]{1,48})::\s*(.+)$/i);
|
|
4816
|
-
if (!match)
|
|
4817
|
-
return {
|
|
4818
|
-
activityText: trimmed.replace(/\*\*/g, '').replace(/\s+/g, ' ').trim(),
|
|
4819
|
-
chatText: trimmed,
|
|
4820
|
-
};
|
|
4821
|
-
}
|
|
7630
|
+
if (!match)
|
|
7631
|
+
return {};
|
|
4822
7632
|
const roleName = match[1].trim().toLowerCase();
|
|
4823
7633
|
const detail = match[2].trim();
|
|
4824
|
-
if (
|
|
4825
|
-
if (/decomposing request|classifying request/i.test(detail)) {
|
|
4826
|
-
return { activityText: 'Classifying request', chatText: 'Classifying request..' };
|
|
4827
|
-
}
|
|
4828
|
-
if (/routed single intent as (.+)$/i.test(detail)) {
|
|
4829
|
-
const classifiedAs = detail.replace(/^routed single intent as\s+/i, '').trim();
|
|
4830
|
-
return { activityText: `Classified as ${classifiedAs}`, chatText: `Classified as ${classifiedAs}..` };
|
|
4831
|
-
}
|
|
4832
|
-
const orderedCount = detail.match(/decomposed request into (\d+) ordered intents/i);
|
|
4833
|
-
if (orderedCount) {
|
|
4834
|
-
return { activityText: `${orderedCount[1]} ordered steps established` };
|
|
4835
|
-
}
|
|
7634
|
+
if (/(intent_classifier|orchestration_planner|dispatch_controller|policy_interpreter|verifier)/i.test(roleName)) {
|
|
4836
7635
|
return {};
|
|
4837
7636
|
}
|
|
4838
|
-
if (roleName === '
|
|
4839
|
-
if (
|
|
4840
|
-
|
|
7637
|
+
if (roleName === 'orchestrator') {
|
|
7638
|
+
if (/^understanding request$/i.test(detail)
|
|
7639
|
+
|| /^still understanding the request$/i.test(detail)
|
|
7640
|
+
|| /is working on the request/i.test(detail)
|
|
7641
|
+
|| /completed the request/i.test(detail)) {
|
|
7642
|
+
return {};
|
|
4841
7643
|
}
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
return {
|
|
4845
|
-
activityText: `${prepared[1]} workflow steps established`,
|
|
4846
|
-
chatText: `${prepared[1]} workflow steps established`,
|
|
4847
|
-
};
|
|
7644
|
+
if (/hit an issue while working/i.test(detail)) {
|
|
7645
|
+
return { activityText: detail };
|
|
4848
7646
|
}
|
|
4849
|
-
return {};
|
|
4850
|
-
}
|
|
4851
|
-
if (roleName === 'dispatch_controller') {
|
|
4852
|
-
if (/still working:/i.test(detail))
|
|
4853
|
-
return {};
|
|
4854
|
-
if (/locked .* execution/i.test(detail))
|
|
4855
|
-
return {};
|
|
4856
|
-
return { activityText: detail };
|
|
4857
|
-
}
|
|
4858
|
-
if (roleName === 'orchestrator') {
|
|
4859
|
-
return {
|
|
4860
|
-
activityText: detail,
|
|
4861
|
-
};
|
|
4862
|
-
}
|
|
4863
|
-
if (roleName === 'policy_interpreter' || roleName === 'verifier') {
|
|
4864
|
-
return { activityText: detail };
|
|
4865
7647
|
}
|
|
4866
7648
|
return {};
|
|
4867
7649
|
}
|
|
4868
7650
|
/** Derive user-visible interim messages from orchestrator progress for key transitions */
|
|
4869
7651
|
function deriveOrchestratorInterimMessage(status) {
|
|
4870
|
-
|
|
4871
|
-
const match = trimmed.match(/^([a-z][a-z0-9_ -]{1,48})::\s*(.+)$/i);
|
|
4872
|
-
if (!match)
|
|
4873
|
-
return null;
|
|
4874
|
-
const roleName = match[1].trim().toLowerCase();
|
|
4875
|
-
const detail = match[2].trim();
|
|
7652
|
+
void status;
|
|
4876
7653
|
// Intent classification → "I'll analyze this request..."
|
|
4877
|
-
if (roleName === 'intent_classifier') {
|
|
4878
|
-
if (/classif/i.test(detail))
|
|
4879
|
-
return null; // skip classification (too early)
|
|
4880
|
-
if (/decomposed request into (\d+)/i.test(detail)) {
|
|
4881
|
-
const m = detail.match(/(\d+)/);
|
|
4882
|
-
return `I've broken this down into ${m?.[1] || 'multiple'} steps. Let me work through them...`;
|
|
4883
|
-
}
|
|
4884
|
-
if (/routed single intent/i.test(detail))
|
|
4885
|
-
return null; // handled by dispatch
|
|
4886
|
-
}
|
|
4887
7654
|
// Dispatch → "I'm sending this to [Bot Name]..."
|
|
4888
|
-
if (roleName === 'dispatch_controller') {
|
|
4889
|
-
const routingMatch = detail.match(/routing (?:direct )?request to (.+)/i);
|
|
4890
|
-
if (routingMatch) {
|
|
4891
|
-
return `Sending this to ${routingMatch[1]}...`;
|
|
4892
|
-
}
|
|
4893
|
-
const connectMatch = detail.match(/connecting request to (.+)/i);
|
|
4894
|
-
if (connectMatch) {
|
|
4895
|
-
return `Connecting to ${connectMatch[1]}...`;
|
|
4896
|
-
}
|
|
4897
|
-
}
|
|
4898
7655
|
// Orchestration planner → workflow planning
|
|
4899
|
-
if (roleName === 'orchestration_planner') {
|
|
4900
|
-
const stepsMatch = detail.match(/prepared (\d+) workflow steps/i);
|
|
4901
|
-
if (stepsMatch) {
|
|
4902
|
-
return `I've prepared a ${stepsMatch[1]}-step workflow. Starting execution...`;
|
|
4903
|
-
}
|
|
4904
|
-
}
|
|
4905
7656
|
return null;
|
|
4906
7657
|
}
|
|
4907
7658
|
function resolveCliNameForProvider(providerName) {
|
|
@@ -4912,8 +7663,45 @@ function resolveCliNameForProvider(providerName) {
|
|
|
4912
7663
|
return 'codex';
|
|
4913
7664
|
return null;
|
|
4914
7665
|
}
|
|
7666
|
+
function deriveVisibleOrchestratorMessage(status) {
|
|
7667
|
+
const trimmed = String(status || '').trim();
|
|
7668
|
+
const match = trimmed.match(/^Orchestrator::\s*(.+)$/i);
|
|
7669
|
+
const detail = match?.[1]?.trim() || '';
|
|
7670
|
+
if (/^Detected requested (workflow|bot sequence):/i.test(detail)) {
|
|
7671
|
+
return detail;
|
|
7672
|
+
}
|
|
7673
|
+
// Robotic system-generated orchestrator placeholders stay hidden. The
|
|
7674
|
+
// detected workflow candidate is the one exception: it is user-facing
|
|
7675
|
+
// confirmation that no workers start until the orchestrator approves.
|
|
7676
|
+
return null;
|
|
7677
|
+
}
|
|
4915
7678
|
function isInteractiveAuthFailure(text) {
|
|
4916
|
-
return /\b(not logged in|please run \/login|unauthorized|invalid api key|authentication required)\b/i.test(text);
|
|
7679
|
+
return /\b(not logged in|please run \/login|login required|unauthorized|invalid authorization|invalid api key|missing api key|authentication required|invalid authentication credentials|invalid bearer token|token expired|session expired|credentials expired|expired credentials|reauthenticate)\b/i.test(text);
|
|
7680
|
+
}
|
|
7681
|
+
function normalizeCliOauthExpiry(expiresAt) {
|
|
7682
|
+
return typeof expiresAt === 'number' && Number.isFinite(expiresAt) && expiresAt > 0
|
|
7683
|
+
? expiresAt
|
|
7684
|
+
: Date.now() + 60 * 60 * 1000;
|
|
7685
|
+
}
|
|
7686
|
+
function persistCliOauthSession(providerId, accessToken, refreshToken, expiresAt) {
|
|
7687
|
+
const normalizedExpiry = normalizeCliOauthExpiry(expiresAt);
|
|
7688
|
+
const written = providerId === 'claude-cli'
|
|
7689
|
+
? (0, token_refresh_1.writeClaudeCredentials)({
|
|
7690
|
+
provider: 'anthropic',
|
|
7691
|
+
accessToken,
|
|
7692
|
+
refreshToken,
|
|
7693
|
+
expiresAt: normalizedExpiry,
|
|
7694
|
+
})
|
|
7695
|
+
: (0, token_refresh_1.writeCodexCredentials)({
|
|
7696
|
+
provider: 'openai',
|
|
7697
|
+
accessToken,
|
|
7698
|
+
refreshToken,
|
|
7699
|
+
expiresAt: normalizedExpiry,
|
|
7700
|
+
});
|
|
7701
|
+
if (written) {
|
|
7702
|
+
(0, credential_reader_1.clearCache)();
|
|
7703
|
+
}
|
|
7704
|
+
return written;
|
|
4917
7705
|
}
|
|
4918
7706
|
function detectInteractiveAuthFailure(text, activeProviderName, configuredProviderName) {
|
|
4919
7707
|
if (!isInteractiveAuthFailure(text))
|
|
@@ -4931,14 +7719,60 @@ function detectInteractiveAuthFailure(text, activeProviderName, configuredProvid
|
|
|
4931
7719
|
};
|
|
4932
7720
|
}
|
|
4933
7721
|
const LOCAL_RUNTIME_RETRY_LIMIT = 2;
|
|
7722
|
+
function resolveProviderDisplayLabel(providerName) {
|
|
7723
|
+
const cli = resolveCliNameForProvider(String(providerName || ''));
|
|
7724
|
+
if (cli === 'claude')
|
|
7725
|
+
return 'Claude';
|
|
7726
|
+
if (cli === 'codex')
|
|
7727
|
+
return 'Codex';
|
|
7728
|
+
const normalized = String(providerName || '').trim().toLowerCase();
|
|
7729
|
+
if (normalized === 'anthropic')
|
|
7730
|
+
return 'Anthropic';
|
|
7731
|
+
if (normalized === 'openai')
|
|
7732
|
+
return 'OpenAI';
|
|
7733
|
+
if (normalized === 'google' || normalized === 'gemini')
|
|
7734
|
+
return 'Gemini';
|
|
7735
|
+
return 'the selected LLM';
|
|
7736
|
+
}
|
|
7737
|
+
function isClaudeFreshSessionStartupFailure(err) {
|
|
7738
|
+
return err?.code === 'CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT'
|
|
7739
|
+
|| err?.name === 'ClaudeFreshSessionStartupTimeoutError'
|
|
7740
|
+
|| /fresh session startup timed out/i.test(String(err?.message || err || ''));
|
|
7741
|
+
}
|
|
7742
|
+
function clearFailedLocalCliSessionEpoch(conversationId, botId, sessionId, sessionScopeKey) {
|
|
7743
|
+
const normalizedSessionId = String(sessionId || '').trim();
|
|
7744
|
+
if (!normalizedSessionId)
|
|
7745
|
+
return;
|
|
7746
|
+
try {
|
|
7747
|
+
const cleared = data.clearCliSessionEpochIfMatches(conversationId, botId, normalizedSessionId, sessionScopeKey);
|
|
7748
|
+
if (cleared) {
|
|
7749
|
+
console.warn(`[chat] cleared poisoned cli_session_epoch conversationId=${conversationId} botId=${botId} sessionId=${normalizedSessionId}`);
|
|
7750
|
+
}
|
|
7751
|
+
}
|
|
7752
|
+
catch (err) {
|
|
7753
|
+
console.warn(`[chat] failed to clear cli_session_epoch conversationId=${conversationId} botId=${botId} sessionId=${normalizedSessionId}: ${err?.message || err}`);
|
|
7754
|
+
}
|
|
7755
|
+
}
|
|
7756
|
+
function buildLocalRuntimeUserFacingError(err) {
|
|
7757
|
+
const exact = String(err?.message || err || '').trim();
|
|
7758
|
+
if (!exact)
|
|
7759
|
+
return null;
|
|
7760
|
+
const providerLabel = resolveProviderDisplayLabel(err?.providerId || err?.provider || 'claude-cli');
|
|
7761
|
+
if (isClaudeFreshSessionStartupFailure(err)
|
|
7762
|
+
|| err?.cli
|
|
7763
|
+
|| /\b(pty|app-server|resume|session|transcript)\b/i.test(exact)) {
|
|
7764
|
+
return `${providerLabel} local runtime failed: ${exact}`;
|
|
7765
|
+
}
|
|
7766
|
+
return null;
|
|
7767
|
+
}
|
|
4934
7768
|
function shouldRetrySelectedLocalRuntime(err) {
|
|
4935
7769
|
const text = String(err?.message || err || '').toLowerCase();
|
|
4936
7770
|
if (!text)
|
|
4937
7771
|
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)) {
|
|
7772
|
+
if (/\b(no api key|missing api key|configure one in settings|not available on this machine|not installed|please run \/login|login required|not logged in|unauthorized|invalid authorization|invalid api key|authentication required|invalid authentication credentials|invalid bearer token|token expired|session expired|credentials expired|expired credentials|reauthenticate)\b/i.test(text)) {
|
|
4939
7773
|
return false;
|
|
4940
7774
|
}
|
|
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);
|
|
7775
|
+
return /\b(429|rate limit|timeout|timed out|temporar|temporarily|econnreset|etimedout|enotfound|econnrefused|socket hang up|network|try again|overloaded|busy|no meaningful pty activity)\b/i.test(text);
|
|
4942
7776
|
}
|
|
4943
7777
|
async function pauseLocalRuntimeRetry(attempt) {
|
|
4944
7778
|
const delayMs = attempt <= 1 ? 750 : 1500;
|
|
@@ -5024,6 +7858,12 @@ async function autoTitleConversation(convId, userMsg, assistantMsg, providerName
|
|
|
5024
7858
|
});
|
|
5025
7859
|
if (resp.content) {
|
|
5026
7860
|
data.updateConversation(convId, { title: resp.content.trim().slice(0, 100) });
|
|
7861
|
+
const revision = data.bumpConversationSyncRevision(convId);
|
|
7862
|
+
(0, chat_sync_1.emitChatSyncEvent)({
|
|
7863
|
+
change: 'conversation.updated',
|
|
7864
|
+
conversationId: convId,
|
|
7865
|
+
revision,
|
|
7866
|
+
});
|
|
5027
7867
|
}
|
|
5028
7868
|
}
|
|
5029
7869
|
catch { /* best-effort */ }
|