funolio-agent 1.0.75 → 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/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.map +1 -1
- package/dist/bot-manager.js +23 -14
- 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/codex-app-server-manager.d.ts +64 -4
- package/dist/codex-app-server-manager.d.ts.map +1 -1
- package/dist/codex-app-server-manager.js +755 -55
- package/dist/codex-app-server-manager.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/start.d.ts +21 -0
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +484 -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/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +170 -58
- 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 +202 -16
- package/dist/context-window.js.map +1 -1
- package/dist/live-activity.d.ts +3 -1
- package/dist/live-activity.d.ts.map +1 -1
- package/dist/live-activity.js.map +1 -1
- 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 +138 -3
- package/dist/local-cli-pty-manager.d.ts.map +1 -1
- package/dist/local-cli-pty-manager.js +1415 -111
- 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 +235 -5
- package/dist/local-data.d.ts.map +1 -1
- package/dist/local-data.js +1066 -87
- 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 +376 -4
- 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 +30 -0
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +2898 -319
- 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.map +1 -1
- package/dist/message-loop.js +43 -1
- package/dist/message-loop.js.map +1 -1
- package/dist/mqtt-client.d.ts +34 -0
- package/dist/mqtt-client.d.ts.map +1 -1
- package/dist/mqtt-client.js +270 -45
- 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/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 +14 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.js +157 -31
- 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/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 +195 -3
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +1970 -432
- 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.d.ts.map +1 -1
- package/dist/providers/claude-cli.js +28 -3
- 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 +190 -17
- 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/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 +13 -4
- package/dist/tools/admin-tools.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.map +1 -1
- package/dist/tools/search-conversation-history.js +12 -2
- package/dist/tools/search-conversation-history.js.map +1 -1
- 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 +13 -0
- package/dist/wizard-state.d.ts.map +1 -1
- package/dist/wizard-state.js +61 -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 +40 -1
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +753 -93
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +2 -2
package/dist/local-server.js
CHANGED
|
@@ -41,6 +41,10 @@ exports.stopLocalServer = stopLocalServer;
|
|
|
41
41
|
exports.buildChatRuntimeForTest = buildChatRuntimeForTest;
|
|
42
42
|
exports.resolveDirectCliSessionTransportForTest = resolveDirectCliSessionTransportForTest;
|
|
43
43
|
exports.buildLocalDesktopDirectPromptForTest = buildLocalDesktopDirectPromptForTest;
|
|
44
|
+
exports.resolveLocalDesktopPromptContextForTest = resolveLocalDesktopPromptContextForTest;
|
|
45
|
+
exports.shouldSkipFreshCliBootstrapForTest = shouldSkipFreshCliBootstrapForTest;
|
|
46
|
+
exports.buildLocalRuntimeUserFacingErrorForTest = buildLocalRuntimeUserFacingErrorForTest;
|
|
47
|
+
exports.persistCliOauthSessionForTest = persistCliOauthSessionForTest;
|
|
44
48
|
/**
|
|
45
49
|
* Local HTTP server for desktop-first operation.
|
|
46
50
|
*
|
|
@@ -53,11 +57,14 @@ const completion_marker_1 = require("./completion-marker");
|
|
|
53
57
|
const index_2 = require("./index");
|
|
54
58
|
const approval_1 = require("./approval");
|
|
55
59
|
const config_1 = require("./config");
|
|
60
|
+
const bench_prefix_1 = require("./bench-prefix");
|
|
61
|
+
const usage_log_1 = require("./usage-log");
|
|
56
62
|
const data = __importStar(require("./local-data"));
|
|
57
63
|
const local_import_worker_1 = require("./local-import-worker");
|
|
58
64
|
const clerk_model_1 = require("./clerk-model");
|
|
59
65
|
const workflow_engine_1 = require("./workflow-engine");
|
|
60
66
|
const context_window_1 = require("./context-window");
|
|
67
|
+
const cli_bootstrap_history_1 = require("./cli-bootstrap-history");
|
|
61
68
|
const summarization_pipeline_1 = require("./summarization-pipeline");
|
|
62
69
|
const backfill_1 = require("./backfill");
|
|
63
70
|
const config_cleanup_1 = require("./config-cleanup");
|
|
@@ -73,15 +80,23 @@ const registry_1 = require("./mcp/registry");
|
|
|
73
80
|
const marketplace_1 = require("./mcp/marketplace");
|
|
74
81
|
const claude_config_writer_1 = require("./mcp/claude-config-writer");
|
|
75
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");
|
|
76
85
|
const local_memory_search_1 = require("./local-memory-search");
|
|
77
86
|
const local_funnel_1 = require("./local-funnel");
|
|
78
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");
|
|
79
90
|
const policy_detection_1 = require("./policy-detection");
|
|
80
91
|
const server_runtime_1 = require("./server-runtime");
|
|
81
92
|
const storage_mode_1 = require("./storage-mode");
|
|
82
93
|
const local_cli_pty_manager_1 = require("./local-cli-pty-manager");
|
|
83
94
|
const codex_app_server_manager_1 = require("./codex-app-server-manager");
|
|
95
|
+
const managed_process_registry_1 = require("./managed-process-registry");
|
|
84
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");
|
|
85
100
|
const server_adapter_1 = require("./server-adapter");
|
|
86
101
|
const wizard_support_1 = require("./wizard-support");
|
|
87
102
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -99,6 +114,351 @@ function requireExpress() {
|
|
|
99
114
|
throw new Error('express is not installed. Run: npm install express');
|
|
100
115
|
}
|
|
101
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
|
+
}
|
|
102
462
|
function startLocalServer(opts) {
|
|
103
463
|
const express = requireExpress();
|
|
104
464
|
const app = express();
|
|
@@ -106,6 +466,16 @@ function startLocalServer(opts) {
|
|
|
106
466
|
if ((0, storage_mode_1.isLocalStorageMode)()) {
|
|
107
467
|
data.purgeLegacyExtractionDataOnce();
|
|
108
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
|
+
}
|
|
109
479
|
const cliNormalization = data.normalizeCliProviderConnections();
|
|
110
480
|
if (cliNormalization.updatedIds.length > 0) {
|
|
111
481
|
console.info(`[local-server] normalized ${cliNormalization.updatedIds.length} CLI provider connection(s): ${cliNormalization.updatedIds.join(', ')}`);
|
|
@@ -138,6 +508,553 @@ function startLocalServer(opts) {
|
|
|
138
508
|
}
|
|
139
509
|
return res.status(500).json({ error: message });
|
|
140
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
|
+
}
|
|
141
1058
|
// Auto-seed agent profiles from DB-backed provider connections after legacy migration.
|
|
142
1059
|
try {
|
|
143
1060
|
const migration = (0, wizard_state_1.migrateLegacyConfigToDb)();
|
|
@@ -194,14 +1111,70 @@ function startLocalServer(opts) {
|
|
|
194
1111
|
console.error(chalk_1.default.yellow(` Failed to reconcile conversation/topic project consistency: ${err}`));
|
|
195
1112
|
}
|
|
196
1113
|
try {
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
199
|
-
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
|
+
}));
|
|
200
1132
|
}
|
|
201
1133
|
}
|
|
202
1134
|
catch (err) {
|
|
203
1135
|
console.error(chalk_1.default.yellow(` Failed to recover interrupted chat jobs: ${err}`));
|
|
204
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
|
+
}
|
|
205
1178
|
// ─── Health ──────────────────────────────────────────────────
|
|
206
1179
|
app.get('/api/health', (_req, res) => {
|
|
207
1180
|
try {
|
|
@@ -212,12 +1185,37 @@ function startLocalServer(opts) {
|
|
|
212
1185
|
db: stats,
|
|
213
1186
|
version: require('../package.json').version,
|
|
214
1187
|
runtime,
|
|
1188
|
+
systemInfo: {
|
|
1189
|
+
hostname: os.hostname(),
|
|
1190
|
+
platform: os.platform(),
|
|
1191
|
+
arch: os.arch(),
|
|
1192
|
+
},
|
|
215
1193
|
});
|
|
216
1194
|
}
|
|
217
1195
|
catch (err) {
|
|
218
1196
|
res.status(500).json({ status: 'error', error: err.message });
|
|
219
1197
|
}
|
|
220
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
|
+
});
|
|
221
1219
|
app.get('/api/runtime/config', async (_req, res) => {
|
|
222
1220
|
try {
|
|
223
1221
|
const runtime = (0, server_runtime_1.getRuntimeConnectionConfig)();
|
|
@@ -234,6 +1232,49 @@ function startLocalServer(opts) {
|
|
|
234
1232
|
res.status(500).json({ error: err.message });
|
|
235
1233
|
}
|
|
236
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
|
+
});
|
|
237
1278
|
app.get('/api/runtime/agents', async (_req, res) => {
|
|
238
1279
|
try {
|
|
239
1280
|
const runtime = (0, server_runtime_1.getRuntimeConnectionConfig)();
|
|
@@ -555,6 +1596,36 @@ function startLocalServer(opts) {
|
|
|
555
1596
|
function localTimestamp() {
|
|
556
1597
|
return new Date().toISOString().replace('T', ' ').replace('Z', '');
|
|
557
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
|
+
}
|
|
558
1629
|
const MAX_LOCAL_CHAT_JOBS = 3;
|
|
559
1630
|
const runningChatJobControllers = new Map();
|
|
560
1631
|
const finalizeCancelledChatJobMessage = (job) => {
|
|
@@ -610,8 +1681,8 @@ function startLocalServer(opts) {
|
|
|
610
1681
|
await handlers.onDone?.({});
|
|
611
1682
|
}
|
|
612
1683
|
};
|
|
613
|
-
const runQueuedChatJobs = async () => {
|
|
614
|
-
if (isConnectedMode())
|
|
1684
|
+
const runQueuedChatJobs = async (opts) => {
|
|
1685
|
+
if (isConnectedMode() && !opts?.force)
|
|
615
1686
|
return;
|
|
616
1687
|
while (runningChatJobControllers.size < MAX_LOCAL_CHAT_JOBS) {
|
|
617
1688
|
const runningKeys = new Set(data.listRunningChatJobs(MAX_LOCAL_CHAT_JOBS + 20).map((job) => `${job.conversation_id}::${job.bot_id}`));
|
|
@@ -624,11 +1695,15 @@ function startLocalServer(opts) {
|
|
|
624
1695
|
return;
|
|
625
1696
|
const controller = new AbortController();
|
|
626
1697
|
runningChatJobControllers.set(next.id, controller);
|
|
1698
|
+
const runningAt = localTimestamp();
|
|
627
1699
|
data.updateChatJob(next.id, {
|
|
628
1700
|
status: 'running',
|
|
629
|
-
startedAt:
|
|
1701
|
+
startedAt: runningAt,
|
|
630
1702
|
error: null,
|
|
631
1703
|
});
|
|
1704
|
+
emitConversationSyncEvents(next.conversation_id, [
|
|
1705
|
+
{ change: 'job.running', jobId: next.id, jobStatus: 'running' },
|
|
1706
|
+
], { updatedAt: runningAt });
|
|
632
1707
|
void (async () => {
|
|
633
1708
|
try {
|
|
634
1709
|
const job = data.getChatJob(next.id);
|
|
@@ -652,6 +1727,7 @@ function startLocalServer(opts) {
|
|
|
652
1727
|
message: userMessage?.content || '',
|
|
653
1728
|
botId: job.bot_id,
|
|
654
1729
|
skipUserMessage: true,
|
|
1730
|
+
attachments: Array.isArray(requestPayload?.attachments) ? requestPayload.attachments : undefined,
|
|
655
1731
|
pinnedMessageIds: Array.isArray(requestPayload?.pinnedMessageIds) ? requestPayload.pinnedMessageIds : undefined,
|
|
656
1732
|
topicId: requestPayload?.topicId || undefined,
|
|
657
1733
|
projectId: requestPayload?.projectId || undefined,
|
|
@@ -669,12 +1745,17 @@ function startLocalServer(opts) {
|
|
|
669
1745
|
const latest = data.getChatJob(next.id);
|
|
670
1746
|
if (!latest || latest.status === 'cancelled')
|
|
671
1747
|
return;
|
|
1748
|
+
const completedAt = localTimestamp();
|
|
672
1749
|
data.updateChatJob(next.id, {
|
|
673
1750
|
status: 'completed',
|
|
674
|
-
completedAt
|
|
1751
|
+
completedAt,
|
|
675
1752
|
error: null,
|
|
676
1753
|
});
|
|
677
|
-
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 });
|
|
678
1759
|
},
|
|
679
1760
|
onError: async (payload) => {
|
|
680
1761
|
const latest = data.getChatJob(next.id);
|
|
@@ -687,12 +1768,17 @@ function startLocalServer(opts) {
|
|
|
687
1768
|
content: existingContent ? `${existingContent}\n\n**Error:** ${errorText}` : `**Error:** ${errorText}`,
|
|
688
1769
|
botId: next.bot_id,
|
|
689
1770
|
});
|
|
1771
|
+
const failedAt = localTimestamp();
|
|
690
1772
|
data.updateChatJob(next.id, {
|
|
691
1773
|
status: 'failed',
|
|
692
1774
|
error: errorText,
|
|
693
|
-
completedAt:
|
|
1775
|
+
completedAt: failedAt,
|
|
694
1776
|
});
|
|
695
|
-
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 });
|
|
696
1782
|
},
|
|
697
1783
|
});
|
|
698
1784
|
}
|
|
@@ -702,11 +1788,16 @@ function startLocalServer(opts) {
|
|
|
702
1788
|
return;
|
|
703
1789
|
if (latest.status === 'cancelled' || err?.name === 'AbortError') {
|
|
704
1790
|
finalizeCancelledChatJobMessage(latest);
|
|
1791
|
+
const cancelledAt = latest.cancelled_at || localTimestamp();
|
|
705
1792
|
data.updateChatJob(next.id, {
|
|
706
1793
|
status: 'cancelled',
|
|
707
|
-
cancelledAt
|
|
708
|
-
completedAt: latest.completed_at ||
|
|
1794
|
+
cancelledAt,
|
|
1795
|
+
completedAt: latest.completed_at || cancelledAt,
|
|
709
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 });
|
|
710
1801
|
return;
|
|
711
1802
|
}
|
|
712
1803
|
const errorText = err?.message || 'Background chat failed';
|
|
@@ -716,12 +1807,17 @@ function startLocalServer(opts) {
|
|
|
716
1807
|
content: existingContent ? `${existingContent}\n\n**Error:** ${errorText}` : `**Error:** ${errorText}`,
|
|
717
1808
|
botId: next.bot_id,
|
|
718
1809
|
});
|
|
1810
|
+
const failedAt = localTimestamp();
|
|
719
1811
|
data.updateChatJob(next.id, {
|
|
720
1812
|
status: 'failed',
|
|
721
1813
|
error: errorText,
|
|
722
|
-
completedAt:
|
|
1814
|
+
completedAt: failedAt,
|
|
723
1815
|
});
|
|
724
|
-
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 });
|
|
725
1821
|
}
|
|
726
1822
|
finally {
|
|
727
1823
|
runningChatJobControllers.delete(next.id);
|
|
@@ -751,6 +1847,7 @@ function startLocalServer(opts) {
|
|
|
751
1847
|
conversationId: req.body?.conversationId || undefined,
|
|
752
1848
|
projectId: req.body?.projectId || undefined,
|
|
753
1849
|
botId: req.body?.botId || undefined,
|
|
1850
|
+
attachments: Array.isArray(req.body?.attachments) ? req.body.attachments : undefined,
|
|
754
1851
|
targetAgentId,
|
|
755
1852
|
stream: true,
|
|
756
1853
|
skipUserMessage: !!req.body?.skipUserMessage,
|
|
@@ -1078,6 +2175,10 @@ function startLocalServer(opts) {
|
|
|
1078
2175
|
if (!isCliProvider) {
|
|
1079
2176
|
return res.status(400).json({ error: 'Desktop auth refresh only supports Claude CLI or Codex CLI sessions.' });
|
|
1080
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
|
+
}
|
|
1081
2182
|
const metadataJson = JSON.stringify({
|
|
1082
2183
|
source: cli ? `desktop-cli:${cli}` : 'desktop-cli',
|
|
1083
2184
|
});
|
|
@@ -1107,6 +2208,12 @@ function startLocalServer(opts) {
|
|
|
1107
2208
|
metadataJson,
|
|
1108
2209
|
});
|
|
1109
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
|
+
}
|
|
1110
2217
|
res.json({ ok: true, updated });
|
|
1111
2218
|
}
|
|
1112
2219
|
catch (err) {
|
|
@@ -1236,10 +2343,22 @@ function startLocalServer(opts) {
|
|
|
1236
2343
|
}
|
|
1237
2344
|
})();
|
|
1238
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
|
+
});
|
|
1239
2358
|
app.post('/api/bots', (req, res) => {
|
|
1240
2359
|
(async () => {
|
|
1241
2360
|
try {
|
|
1242
|
-
const { provider, model, name, soulMd, memoryMd, toolsMd, skillsMd, apiKeyEnc, permissionMode, isDefault, roleLabel, roleClass, isActive, priority, isOrchestrator, is_orchestrator, codexReasoningEffort, codex_reasoning_effort, codexReasoningSummary, codex_reasoning_summary, codexPersonality, codex_personality, codexServiceTier, codex_service_tier, codexSandboxPolicy, codex_sandbox_policy, codexApprovalPolicy, codex_approval_policy, } = req.body;
|
|
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;
|
|
1243
2362
|
if (!provider || !model || !name) {
|
|
1244
2363
|
return res.status(400).json({ error: 'provider, model, and name are required' });
|
|
1245
2364
|
}
|
|
@@ -1266,8 +2385,12 @@ function startLocalServer(opts) {
|
|
|
1266
2385
|
isDefault,
|
|
1267
2386
|
roleLabel,
|
|
1268
2387
|
roleClass,
|
|
2388
|
+
orchestrationRoleLabel: orchestrationRoleLabel ?? orchestration_role_label,
|
|
2389
|
+
orchestrationRoleClass: orchestrationRoleClass ?? orchestration_role_class,
|
|
2390
|
+
orchestrationIncludeUserPrompt: orchestrationIncludeUserPrompt ?? orchestration_include_user_prompt,
|
|
1269
2391
|
isActive,
|
|
1270
2392
|
priority,
|
|
2393
|
+
color,
|
|
1271
2394
|
isOrchestrator: isOrchestrator ?? is_orchestrator,
|
|
1272
2395
|
codexReasoningEffort: codexReasoningEffort ?? codex_reasoning_effort,
|
|
1273
2396
|
codexReasoningSummary: codexReasoningSummary ?? codex_reasoning_summary,
|
|
@@ -1275,7 +2398,15 @@ function startLocalServer(opts) {
|
|
|
1275
2398
|
codexServiceTier: codexServiceTier ?? codex_service_tier,
|
|
1276
2399
|
codexSandboxPolicy: codexSandboxPolicy ?? codex_sandbox_policy,
|
|
1277
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),
|
|
1278
2406
|
});
|
|
2407
|
+
syncCliBotConfigFiles(profile);
|
|
2408
|
+
reapCliBotSessions(profile.id);
|
|
2409
|
+
watchCliBotConfig(profile);
|
|
1279
2410
|
res.status(201).json(profile);
|
|
1280
2411
|
}
|
|
1281
2412
|
catch (err) {
|
|
@@ -1313,10 +2444,20 @@ function startLocalServer(opts) {
|
|
|
1313
2444
|
fields.roleLabel = b.roleLabel ?? b.role_label;
|
|
1314
2445
|
if (b.roleClass !== undefined || b.role_class !== undefined)
|
|
1315
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;
|
|
1316
2453
|
if (b.isActive !== undefined || b.is_active !== undefined)
|
|
1317
2454
|
fields.isActive = b.isActive ?? b.is_active;
|
|
1318
2455
|
if (b.priority !== undefined)
|
|
1319
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;
|
|
1320
2461
|
if (b.showThinking !== undefined || b.show_thinking !== undefined)
|
|
1321
2462
|
fields.showThinking = b.showThinking ?? b.show_thinking;
|
|
1322
2463
|
if (b.isOrchestrator !== undefined || b.is_orchestrator !== undefined)
|
|
@@ -1333,14 +2474,33 @@ function startLocalServer(opts) {
|
|
|
1333
2474
|
fields.codexSandboxPolicy = b.codexSandboxPolicy ?? b.codex_sandbox_policy;
|
|
1334
2475
|
if (b.codexApprovalPolicy !== undefined || b.codex_approval_policy !== undefined)
|
|
1335
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
|
+
}
|
|
1336
2488
|
if (isConnectedMode()) {
|
|
1337
2489
|
const runtime = (0, server_runtime_1.getRuntimeConnectionConfig)();
|
|
1338
2490
|
const auth = await getHydratedDesktopAuth();
|
|
1339
2491
|
return res.json(await (0, server_adapter_1.updateServerBot)(auth, runtime, req.params.id, fields));
|
|
1340
2492
|
}
|
|
2493
|
+
const previous = data.getAgentProfile(req.params.id);
|
|
1341
2494
|
const updated = data.updateAgentProfile(req.params.id, fields);
|
|
1342
2495
|
if (!updated)
|
|
1343
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
|
+
}
|
|
1344
2504
|
res.json(updated);
|
|
1345
2505
|
}
|
|
1346
2506
|
catch (err) {
|
|
@@ -1376,6 +2536,8 @@ function startLocalServer(opts) {
|
|
|
1376
2536
|
const deleted = data.deleteAgentProfile(req.params.id);
|
|
1377
2537
|
if (!deleted)
|
|
1378
2538
|
return res.status(404).json({ error: 'Not found' });
|
|
2539
|
+
closeCliBotConfigWatcher(req.params.id);
|
|
2540
|
+
reapCliBotSessions(req.params.id);
|
|
1379
2541
|
res.json({ ok: true });
|
|
1380
2542
|
}
|
|
1381
2543
|
catch (err) {
|
|
@@ -1397,18 +2559,52 @@ function startLocalServer(opts) {
|
|
|
1397
2559
|
return res.status(404).json({ error: 'Not found' });
|
|
1398
2560
|
const convCount = data.countConversations(req.params.id);
|
|
1399
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;
|
|
1400
2579
|
const recentConversations = data.listBotConversationActivity(req.params.id, 8).map((conversation) => ({
|
|
1401
2580
|
id: conversation.id,
|
|
1402
|
-
agent_id: conversation.agent_id,
|
|
1403
2581
|
title: conversation.title,
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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,
|
|
1410
2588
|
}));
|
|
1411
|
-
|
|
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
|
+
});
|
|
1412
2608
|
}
|
|
1413
2609
|
catch (err) {
|
|
1414
2610
|
res.status(500).json({ error: err.message });
|
|
@@ -1447,7 +2643,7 @@ function startLocalServer(opts) {
|
|
|
1447
2643
|
app.post('/api/conversations', (req, res) => {
|
|
1448
2644
|
(async () => {
|
|
1449
2645
|
try {
|
|
1450
|
-
const { agentId, title, source, projectId, projectName } = req.body;
|
|
2646
|
+
const { agentId, botIds, initialBotId, title, source, projectId, projectName, topicId } = req.body;
|
|
1451
2647
|
if (isConnectedMode()) {
|
|
1452
2648
|
const runtime = (0, server_runtime_1.getRuntimeConnectionConfig)();
|
|
1453
2649
|
const auth = await getHydratedDesktopAuth();
|
|
@@ -1462,12 +2658,27 @@ function startLocalServer(opts) {
|
|
|
1462
2658
|
...(projectName ? { project_name: projectName } : {}),
|
|
1463
2659
|
});
|
|
1464
2660
|
}
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
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, {
|
|
1468
2668
|
projectId: projectId ?? null,
|
|
1469
2669
|
projectName: projectName ?? null,
|
|
2670
|
+
botIds: normalizedBotIds,
|
|
2671
|
+
initialBotId: resolvedInitialBotId,
|
|
1470
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
|
+
]);
|
|
1471
2682
|
res.status(201).json(conv);
|
|
1472
2683
|
}
|
|
1473
2684
|
catch (err) {
|
|
@@ -1512,9 +2723,14 @@ function startLocalServer(opts) {
|
|
|
1512
2723
|
const auth = await getHydratedDesktopAuth();
|
|
1513
2724
|
return res.json(await (0, server_adapter_1.deleteServerConversation)(auth, runtime, req.params.id));
|
|
1514
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');
|
|
1515
2730
|
const deleted = data.deleteConversation(req.params.id);
|
|
1516
2731
|
if (!deleted)
|
|
1517
2732
|
return res.status(404).json({ error: 'Not found' });
|
|
2733
|
+
emitDeletedConversationSyncEvent(existing);
|
|
1518
2734
|
res.json({ ok: true });
|
|
1519
2735
|
}
|
|
1520
2736
|
catch (err) {
|
|
@@ -1534,6 +2750,9 @@ function startLocalServer(opts) {
|
|
|
1534
2750
|
if (!conv)
|
|
1535
2751
|
return res.status(404).json({ error: 'Not found' });
|
|
1536
2752
|
data.updateConversation(req.params.id, req.body);
|
|
2753
|
+
emitConversationSyncEvents(req.params.id, [
|
|
2754
|
+
{ change: 'conversation.updated' },
|
|
2755
|
+
]);
|
|
1537
2756
|
res.json(data.getConversation(req.params.id));
|
|
1538
2757
|
}
|
|
1539
2758
|
catch (err) {
|
|
@@ -1638,6 +2857,12 @@ function startLocalServer(opts) {
|
|
|
1638
2857
|
const auth = await getHydratedDesktopAuth();
|
|
1639
2858
|
return res.json(await (0, server_adapter_1.deleteServerProject)(auth, runtime, req.params.id));
|
|
1640
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
|
+
}
|
|
1641
2866
|
const deleted = data.deleteProject(req.params.id);
|
|
1642
2867
|
if (!deleted)
|
|
1643
2868
|
return res.status(404).json({ error: 'Not found' });
|
|
@@ -1801,6 +3026,10 @@ function startLocalServer(opts) {
|
|
|
1801
3026
|
const auth = await getHydratedDesktopAuth();
|
|
1802
3027
|
return res.json(await (0, server_adapter_1.deleteServerTopic)(auth, runtime, req.params.id));
|
|
1803
3028
|
}
|
|
3029
|
+
const topic = data.getTopic(req.params.id);
|
|
3030
|
+
for (const segment of topic?.segments || []) {
|
|
3031
|
+
closeCliSessionsForConversation(segment.conversation_id, 'topic_deleted');
|
|
3032
|
+
}
|
|
1804
3033
|
const deleted = data.deleteTopic(req.params.id);
|
|
1805
3034
|
if (!deleted)
|
|
1806
3035
|
return res.status(404).json({ error: 'Not found' });
|
|
@@ -2022,6 +3251,9 @@ function startLocalServer(opts) {
|
|
|
2022
3251
|
const botId = req.body?.botId ? String(req.body.botId) : undefined;
|
|
2023
3252
|
const agentName = req.body?.agentName ? String(req.body.agentName) : undefined;
|
|
2024
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
|
+
]);
|
|
2025
3257
|
res.status(201).json(msg);
|
|
2026
3258
|
}
|
|
2027
3259
|
catch (err) {
|
|
@@ -2079,6 +3311,9 @@ function startLocalServer(opts) {
|
|
|
2079
3311
|
});
|
|
2080
3312
|
if (!updated)
|
|
2081
3313
|
return res.status(404).json({ error: 'Message not found' });
|
|
3314
|
+
emitConversationSyncEvents(updated.conversation_id, [
|
|
3315
|
+
{ change: 'message.updated', messageId: updated.id },
|
|
3316
|
+
]);
|
|
2082
3317
|
res.json(updated);
|
|
2083
3318
|
}
|
|
2084
3319
|
catch (err) {
|
|
@@ -2225,11 +3460,18 @@ function startLocalServer(opts) {
|
|
|
2225
3460
|
app.post('/api/todo/:id/worker-complete', (req, res) => {
|
|
2226
3461
|
try {
|
|
2227
3462
|
const taskId = Number(req.params.id);
|
|
2228
|
-
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;
|
|
2229
3468
|
const result = data.completeTodoTaskByWorker(taskId, {
|
|
2230
3469
|
outputSummary: outputSummary === undefined ? undefined : String(outputSummary),
|
|
2231
3470
|
artifactRefs: Array.isArray(artifactRefs) ? artifactRefs : undefined,
|
|
2232
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),
|
|
2233
3475
|
insertTask: insertTask ? {
|
|
2234
3476
|
title: String(insertTask.title || ''),
|
|
2235
3477
|
prompt: String(insertTask.prompt || ''),
|
|
@@ -2391,19 +3633,19 @@ function startLocalServer(opts) {
|
|
|
2391
3633
|
return res.json(result.messages);
|
|
2392
3634
|
}
|
|
2393
3635
|
if (hasDirectRange) {
|
|
2394
|
-
return res.json(data.getMessagesInRange(req.params.id, startSeq, endSeq));
|
|
3636
|
+
return res.json(serializeMessageHistoryForRequest(req, data.getMessagesInRange(req.params.id, startSeq, endSeq)));
|
|
2395
3637
|
}
|
|
2396
3638
|
if (beforeSeq > 0) {
|
|
2397
3639
|
// Backward paging: get N rounds or messages before given seq, returned in ASC order
|
|
2398
3640
|
const msgs = rounds > 0
|
|
2399
3641
|
? data.getMessageRoundsBefore(req.params.id, beforeSeq, rounds)
|
|
2400
3642
|
: data.getMessagesBefore(req.params.id, beforeSeq, limit);
|
|
2401
|
-
res.json(msgs);
|
|
3643
|
+
res.json(serializeMessageHistoryForRequest(req, msgs));
|
|
2402
3644
|
}
|
|
2403
3645
|
else {
|
|
2404
3646
|
const offset = parseInt(req.query.offset, 10) || 0;
|
|
2405
3647
|
const msgs = data.getMessages(req.params.id, { limit, offset });
|
|
2406
|
-
res.json(msgs);
|
|
3648
|
+
res.json(serializeMessageHistoryForRequest(req, msgs));
|
|
2407
3649
|
}
|
|
2408
3650
|
}
|
|
2409
3651
|
catch (err) {
|
|
@@ -2461,90 +3703,51 @@ function startLocalServer(opts) {
|
|
|
2461
3703
|
res.status(500).json({ error: err.message });
|
|
2462
3704
|
}
|
|
2463
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
|
+
});
|
|
2464
3713
|
app.post('/api/chat/jobs', async (req, res) => {
|
|
2465
3714
|
try {
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
return res.status(
|
|
2472
|
-
}
|
|
2473
|
-
let profile = botId ? data.getAgentProfile(String(botId)) : data.getDefaultAgentProfile();
|
|
2474
|
-
if (!profile) {
|
|
2475
|
-
return res.status(400).json({ error: 'No bot configured. Create one first.' });
|
|
2476
|
-
}
|
|
2477
|
-
const shouldUseOrchestratorMode = orchestrationEnabled !== false && (data.isClerkOrchestratorEnabled() || (0, orchestrator_profile_1.isOrchestratorProfile)(profile));
|
|
2478
|
-
if (shouldUseOrchestratorMode) {
|
|
2479
|
-
return res.status(400).json({ error: 'Background chat jobs do not support orchestrator mode yet.' });
|
|
2480
|
-
}
|
|
2481
|
-
let convId = conversationId ? String(conversationId) : '';
|
|
2482
|
-
if (convId) {
|
|
2483
|
-
const latestJob = data.getLatestConversationBotChatJob(convId, profile.id);
|
|
2484
|
-
if (latestJob && (latestJob.status === 'queued' || latestJob.status === 'running')) {
|
|
2485
|
-
return res.status(409).json({ error: 'This bot already has a pending response in this conversation.' });
|
|
2486
|
-
}
|
|
2487
|
-
}
|
|
2488
|
-
if (!convId) {
|
|
2489
|
-
let topicProjectId = null;
|
|
2490
|
-
if (topicId) {
|
|
2491
|
-
const topic = data.getTopic(String(topicId));
|
|
2492
|
-
topicProjectId = topic?.project_id ?? null;
|
|
2493
|
-
}
|
|
2494
|
-
const requestedProjectId = (projectId ? String(projectId) : null) || topicProjectId;
|
|
2495
|
-
const conv = data.createConversation(profile.id, '', 'local', {
|
|
2496
|
-
projectId: requestedProjectId,
|
|
2497
|
-
});
|
|
2498
|
-
convId = conv.id;
|
|
2499
|
-
}
|
|
2500
|
-
if (topicId && convId) {
|
|
2501
|
-
try {
|
|
2502
|
-
data.upsertConversationTopicSegment(convId, String(topicId));
|
|
2503
|
-
}
|
|
2504
|
-
catch { /* best effort */ }
|
|
2505
|
-
}
|
|
2506
|
-
if (!topicId && projectId && convId) {
|
|
2507
|
-
const selectedProject = data.getProject(String(projectId));
|
|
2508
|
-
if (selectedProject) {
|
|
2509
|
-
data.updateConversation(convId, {
|
|
2510
|
-
projectId: selectedProject.id,
|
|
2511
|
-
projectName: selectedProject.name,
|
|
2512
|
-
});
|
|
2513
|
-
}
|
|
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);
|
|
2514
3721
|
}
|
|
2515
|
-
const
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
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
|
+
}));
|
|
2522
3739
|
}
|
|
2523
|
-
|
|
2524
|
-
conversationId: convId,
|
|
2525
|
-
userMessageId: savedUserMessage.id,
|
|
2526
|
-
assistantMessageId: assistantMessage.id,
|
|
2527
|
-
botId: profile.id,
|
|
2528
|
-
status: 'queued',
|
|
2529
|
-
requestJson: JSON.stringify({
|
|
2530
|
-
pinnedMessageIds: Array.isArray(pinnedMessageIds) ? pinnedMessageIds : [],
|
|
2531
|
-
topicId: topicId ? String(topicId) : null,
|
|
2532
|
-
projectId: projectId ? String(projectId) : null,
|
|
2533
|
-
orchestrationEnabled: orchestrationEnabled !== false,
|
|
2534
|
-
}),
|
|
2535
|
-
});
|
|
2536
|
-
void runQueuedChatJobs();
|
|
2537
|
-
res.status(201).json({
|
|
2538
|
-
ok: true,
|
|
2539
|
-
conversationId: convId,
|
|
2540
|
-
userMessageId: savedUserMessage.id,
|
|
2541
|
-
assistantMessageId: assistantMessage.id,
|
|
2542
|
-
jobId: job.id,
|
|
2543
|
-
status: job.status,
|
|
2544
|
-
});
|
|
3740
|
+
res.status(201).json(canonicalResponse);
|
|
2545
3741
|
}
|
|
2546
3742
|
catch (err) {
|
|
2547
|
-
|
|
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 });
|
|
2548
3751
|
}
|
|
2549
3752
|
});
|
|
2550
3753
|
app.post('/api/chat/jobs/:id/cancel', async (req, res) => {
|
|
@@ -2567,7 +3770,11 @@ function startLocalServer(opts) {
|
|
|
2567
3770
|
}
|
|
2568
3771
|
else {
|
|
2569
3772
|
finalizeCancelledChatJobMessage(job);
|
|
2570
|
-
data.touchConversationActivity(job.conversation_id);
|
|
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 });
|
|
2571
3778
|
}
|
|
2572
3779
|
res.json({ ok: true, status: 'cancelled' });
|
|
2573
3780
|
}
|
|
@@ -2575,7 +3782,95 @@ function startLocalServer(opts) {
|
|
|
2575
3782
|
res.status(500).json({ error: err.message });
|
|
2576
3783
|
}
|
|
2577
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();
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
});
|
|
2578
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
|
+
});
|
|
2579
3874
|
app.post('/api/conversations/:id/chat-job/cancel', async (req, res) => {
|
|
2580
3875
|
try {
|
|
2581
3876
|
const requestedBotId = String(req.body?.botId || '').trim();
|
|
@@ -2599,7 +3894,11 @@ function startLocalServer(opts) {
|
|
|
2599
3894
|
}
|
|
2600
3895
|
else {
|
|
2601
3896
|
finalizeCancelledChatJobMessage(latestJob);
|
|
2602
|
-
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 });
|
|
2603
3902
|
void runQueuedChatJobs();
|
|
2604
3903
|
}
|
|
2605
3904
|
res.json({ ok: true, status: 'cancelled', jobId: latestJob.id });
|
|
@@ -2619,14 +3918,42 @@ function startLocalServer(opts) {
|
|
|
2619
3918
|
req.on('close', abortOnClientClose);
|
|
2620
3919
|
res.on('close', abortOnClientClose);
|
|
2621
3920
|
try {
|
|
2622
|
-
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;
|
|
2623
3922
|
if (!message)
|
|
2624
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 ─────────────────────────────────────
|
|
2625
3952
|
if (await relayConnectedChat(req, res)) {
|
|
2626
3953
|
return;
|
|
2627
3954
|
}
|
|
2628
3955
|
// Resolve bot
|
|
2629
|
-
let profile =
|
|
3956
|
+
let profile = resolvedBotId ? data.getAgentProfile(resolvedBotId) : data.getDefaultAgentProfile();
|
|
2630
3957
|
if (!profile) {
|
|
2631
3958
|
// Auto-create a default profile from the DB-backed provider connection if available.
|
|
2632
3959
|
const providerConnection = data.listProviderConnections().find((conn) => conn.access_mode === 'cli' || !!conn.api_key_enc);
|
|
@@ -2642,7 +3969,11 @@ function startLocalServer(opts) {
|
|
|
2642
3969
|
if (!profile)
|
|
2643
3970
|
return res.status(400).json({ error: 'No bot configured. Create one first.' });
|
|
2644
3971
|
}
|
|
3972
|
+
const incomingAttachments = parseChatAttachmentInputs(attachments);
|
|
3973
|
+
const shouldUseOrchestratorMode = normalizedOrchestrationEnabled !== false && (data.isClerkOrchestratorEnabled() || (0, orchestrator_profile_1.isOrchestratorProfile)(profile));
|
|
2645
3974
|
// Resolve or create conversation
|
|
3975
|
+
let conversationCreated = false;
|
|
3976
|
+
let turnStartSyncRevision = null;
|
|
2646
3977
|
let convId = conversationId;
|
|
2647
3978
|
if (!convId) {
|
|
2648
3979
|
// Create with empty title so auto-title can fill it in after first response
|
|
@@ -2654,8 +3985,14 @@ function startLocalServer(opts) {
|
|
|
2654
3985
|
const requestedProjectId = (projectId ? String(projectId) : null) || topicProjectId;
|
|
2655
3986
|
const conv = data.createConversation(profile.id, '', 'local', {
|
|
2656
3987
|
projectId: requestedProjectId,
|
|
3988
|
+
botIds: requestedBotIds.length > 0 ? requestedBotIds : [profile.id],
|
|
3989
|
+
initialBotId: profile.id,
|
|
2657
3990
|
});
|
|
2658
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 });
|
|
2659
3996
|
}
|
|
2660
3997
|
// Link conversation to topic if topicId provided
|
|
2661
3998
|
if (topicId && convId) {
|
|
@@ -2731,8 +4068,12 @@ function startLocalServer(opts) {
|
|
|
2731
4068
|
};
|
|
2732
4069
|
// Save user message (skip if multi-bot call where first bot already saved it)
|
|
2733
4070
|
let savedUserMessage = null;
|
|
4071
|
+
let persistedAttachments = [];
|
|
2734
4072
|
if (!skipUserMessage) {
|
|
2735
|
-
|
|
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);
|
|
2736
4077
|
(0, context_window_1.incrementTurnCount)(convId);
|
|
2737
4078
|
const convForPolicy = data.getConversation(convId);
|
|
2738
4079
|
const effectiveProjectId = projectId ? String(projectId) : (convForPolicy?.project_id || undefined);
|
|
@@ -2757,24 +4098,229 @@ function startLocalServer(opts) {
|
|
|
2757
4098
|
});
|
|
2758
4099
|
}
|
|
2759
4100
|
}
|
|
2760
|
-
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) {
|
|
2761
4294
|
const placeholder = data.addMessage(convId, 'assistant', '', buildConfiguredMessageModel(profile), undefined, profile?.id || undefined, profile?.name || undefined);
|
|
2762
4295
|
assistantMessageId = placeholder.id;
|
|
4296
|
+
turnStartSyncRevision = emitConversationSyncEvents(convId, [
|
|
4297
|
+
{ change: 'message.created', messageId: placeholder.id },
|
|
4298
|
+
], turnStartSyncRevision ? { revision: turnStartSyncRevision } : undefined);
|
|
2763
4299
|
}
|
|
2764
4300
|
// ─── Orchestrator Mode Branch ─────────────────────────
|
|
2765
|
-
const shouldUseOrchestratorMode = orchestrationEnabled !== false && (data.isClerkOrchestratorEnabled() || (0, orchestrator_profile_1.isOrchestratorProfile)(profile));
|
|
2766
4301
|
if (shouldUseOrchestratorMode) {
|
|
2767
4302
|
const { OrchestratorAgent } = require('./orchestrator');
|
|
2768
4303
|
const { buildLocalDesktopOrchestratorRuntime } = require('./orchestrator');
|
|
2769
4304
|
const { getWorkflowEngine } = require('./workflow-engine');
|
|
2770
4305
|
const workflowEngine = getWorkflowEngine(opts.projectDir, 'local_desktop');
|
|
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;
|
|
2771
4317
|
let orchestratorRuntime;
|
|
2772
4318
|
try {
|
|
2773
|
-
orchestratorRuntime = buildLocalDesktopOrchestratorRuntime(
|
|
4319
|
+
orchestratorRuntime = buildLocalDesktopOrchestratorRuntime(explicitOrchestratorBot);
|
|
2774
4320
|
}
|
|
2775
4321
|
catch (runtimeErr) {
|
|
2776
4322
|
return res.status(400).json({
|
|
2777
|
-
error: runtimeErr?.message || 'Orchestrator mode is not configured
|
|
4323
|
+
error: runtimeErr?.message || 'Orchestrator mode is not configured. Mark a bot with is_orchestrator=1 or enable Clerk orchestration.',
|
|
2778
4324
|
});
|
|
2779
4325
|
}
|
|
2780
4326
|
const orchestrator = new OrchestratorAgent(orchestratorRuntime, workflowEngine);
|
|
@@ -2786,6 +4332,44 @@ function startLocalServer(opts) {
|
|
|
2786
4332
|
const shortTitle = message.slice(0, 60).replace(/\n/g, ' ').trim();
|
|
2787
4333
|
data.updateConversation(convId, { title: shortTitle || 'New Chat' });
|
|
2788
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
|
+
}
|
|
2789
4373
|
// SSE setup — same contract as normal chat path
|
|
2790
4374
|
res.writeHead(200, {
|
|
2791
4375
|
'Content-Type': 'text/event-stream',
|
|
@@ -2803,9 +4387,12 @@ function startLocalServer(opts) {
|
|
|
2803
4387
|
const clerkSelectedAsOrchestrator = data.isClerkOrchestratorEnabled();
|
|
2804
4388
|
try {
|
|
2805
4389
|
if (clerkSelectedAsOrchestrator) {
|
|
2806
|
-
const
|
|
2807
|
-
const
|
|
2808
|
-
const
|
|
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);
|
|
2809
4396
|
const clerkRuntimeMode = clerkProvider === 'claude-cli' || clerkProvider === 'codex-cli'
|
|
2810
4397
|
? 'subscription-cli'
|
|
2811
4398
|
: 'api-key';
|
|
@@ -2834,22 +4421,24 @@ function startLocalServer(opts) {
|
|
|
2834
4421
|
}
|
|
2835
4422
|
sendEvent('meta', {
|
|
2836
4423
|
conversationId: convId,
|
|
4424
|
+
assistantMessageId: assistantMessageId || null,
|
|
2837
4425
|
...(orchestratorRuntimePayload ? { runtime: orchestratorRuntimePayload } : {}),
|
|
2838
4426
|
});
|
|
2839
4427
|
let lastProgressChat = '';
|
|
2840
4428
|
let lastProgressActivity = '';
|
|
2841
4429
|
let selfExecuteStreamed = false;
|
|
2842
|
-
let hasWorkerActivity = false;
|
|
2843
4430
|
try {
|
|
2844
4431
|
let lastInterimMessage = '';
|
|
2845
|
-
const response = await orchestrator.handleUserMessage(
|
|
4432
|
+
const response = await orchestrator.handleUserMessage(runtimeUserPrompt, convId, {
|
|
2846
4433
|
projectDir: opts.projectDir,
|
|
2847
4434
|
projectId: effectiveProjectId,
|
|
4435
|
+
orchestratorBotIdHint,
|
|
4436
|
+
storedAttachments: storedAttachmentsForTurn.length > 0 ? storedAttachmentsForTurn : undefined,
|
|
4437
|
+
abortSignal: routeAbortController.signal,
|
|
2848
4438
|
commandId: `local-${Date.now()}`,
|
|
2849
4439
|
workflowTemplateId: workflowTemplateId || undefined,
|
|
2850
4440
|
onWorkerChunk: (event) => {
|
|
2851
4441
|
if (event.type === 'step_start') {
|
|
2852
|
-
hasWorkerActivity = true;
|
|
2853
4442
|
recordWorkerActivity('worker_step_start', event, {
|
|
2854
4443
|
stepId: event.stepId,
|
|
2855
4444
|
agentName: event.agentName,
|
|
@@ -2868,7 +4457,6 @@ function startLocalServer(opts) {
|
|
|
2868
4457
|
});
|
|
2869
4458
|
}
|
|
2870
4459
|
else if (event.type === 'worker_chunk') {
|
|
2871
|
-
hasWorkerActivity = true;
|
|
2872
4460
|
recordWorkerActivity('worker_chunk', event, {
|
|
2873
4461
|
stepId: event.stepId,
|
|
2874
4462
|
agentName: event.agentName,
|
|
@@ -2889,7 +4477,6 @@ function startLocalServer(opts) {
|
|
|
2889
4477
|
});
|
|
2890
4478
|
}
|
|
2891
4479
|
else if (event.type === 'worker_terminal_chunk') {
|
|
2892
|
-
hasWorkerActivity = true;
|
|
2893
4480
|
sendEvent('worker_terminal_chunk', {
|
|
2894
4481
|
stepId: event.stepId,
|
|
2895
4482
|
botId: resolveWorkerBotId(event.agentName),
|
|
@@ -2901,7 +4488,6 @@ function startLocalServer(opts) {
|
|
|
2901
4488
|
});
|
|
2902
4489
|
}
|
|
2903
4490
|
else if (event.type === 'worker_tool_call') {
|
|
2904
|
-
hasWorkerActivity = true;
|
|
2905
4491
|
recordWorkerActivity('worker_tool_call', event, {
|
|
2906
4492
|
stepId: event.stepId,
|
|
2907
4493
|
agentName: event.agentName,
|
|
@@ -2926,7 +4512,6 @@ function startLocalServer(opts) {
|
|
|
2926
4512
|
});
|
|
2927
4513
|
}
|
|
2928
4514
|
else if (event.type === 'worker_tool_result') {
|
|
2929
|
-
hasWorkerActivity = true;
|
|
2930
4515
|
recordWorkerActivity('worker_tool_result', event, {
|
|
2931
4516
|
stepId: event.stepId,
|
|
2932
4517
|
agentName: event.agentName,
|
|
@@ -2965,7 +4550,6 @@ function startLocalServer(opts) {
|
|
|
2965
4550
|
sendEvent('chunk', { text: `> [${icon}] ${event.toolName} completed\n` });
|
|
2966
4551
|
}
|
|
2967
4552
|
else if (event.type === 'step_done') {
|
|
2968
|
-
hasWorkerActivity = true;
|
|
2969
4553
|
recordWorkerActivity('worker_step_done', event, {
|
|
2970
4554
|
stepId: event.stepId,
|
|
2971
4555
|
agentName: event.agentName,
|
|
@@ -3002,7 +4586,7 @@ function startLocalServer(opts) {
|
|
|
3002
4586
|
// Progress chatText suppressed — worker card handles all streaming display.
|
|
3003
4587
|
// Main bubble only gets final content via 'done' event.
|
|
3004
4588
|
// Emit interim messages for key orchestrator transitions
|
|
3005
|
-
const interimMessage =
|
|
4589
|
+
const interimMessage = deriveVisibleOrchestratorMessage(status);
|
|
3006
4590
|
if (interimMessage && interimMessage !== lastInterimMessage) {
|
|
3007
4591
|
lastInterimMessage = interimMessage;
|
|
3008
4592
|
recordActivity('orchestrator_interim', { text: interimMessage }, interimMessage);
|
|
@@ -3022,41 +4606,44 @@ function startLocalServer(opts) {
|
|
|
3022
4606
|
// Save O's response (no incrementTurnCount — Fix #1: user message already incremented it)
|
|
3023
4607
|
const responseMeta = orchestrator.getLastResponseMeta();
|
|
3024
4608
|
const finalAgentName = responseMeta?.agentName || 'Orchestrator';
|
|
3025
|
-
const finalBotId = responseMeta?.botId
|
|
4609
|
+
const finalBotId = responseMeta?.botId
|
|
4610
|
+
|| (finalAgentName === 'Orchestrator' && !clerkSelectedAsOrchestrator ? profile.id : undefined);
|
|
3026
4611
|
const finalModelLabel = responseMeta?.modelLabel || orchestratorRuntimeLabel || undefined;
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
data.
|
|
3033
|
-
|
|
3034
|
-
|
|
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,
|
|
3035
4620
|
botId: finalBotId,
|
|
3036
4621
|
agentName: finalAgentName,
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
});
|
|
4622
|
+
})
|
|
4623
|
+
: null;
|
|
4624
|
+
if (!savedMessage) {
|
|
4625
|
+
savedMessage = data.addMessage(convId, 'assistant', response, finalModelLabel, undefined, finalBotId, finalAgentName);
|
|
3042
4626
|
}
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
data.createMessageActivity({
|
|
3047
|
-
conversationId: convId,
|
|
4627
|
+
emitConversationSyncEvents(convId, [
|
|
4628
|
+
{
|
|
4629
|
+
change: savedMessage.id === assistantMessageId ? 'message.updated' : 'message.created',
|
|
3048
4630
|
messageId: savedMessage.id,
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
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
|
+
});
|
|
3057
4644
|
// Emit chunk + done events using the same SSE contract as normal chat
|
|
3058
4645
|
// Skip the final bulk chunk if we already streamed via worker_chunk (execute_self)
|
|
3059
|
-
if (!selfExecuteStreamed
|
|
4646
|
+
if (!selfExecuteStreamed) {
|
|
3060
4647
|
sendEvent('chunk', { text: response });
|
|
3061
4648
|
}
|
|
3062
4649
|
sendEvent('done', {
|
|
@@ -3064,7 +4651,6 @@ function startLocalServer(opts) {
|
|
|
3064
4651
|
content: response,
|
|
3065
4652
|
agentName: finalAgentName,
|
|
3066
4653
|
botId: finalBotId,
|
|
3067
|
-
separateFinalMessage: splitFinalMessage,
|
|
3068
4654
|
...((responseMeta?.modelLabel || orchestratorRuntimePayload)
|
|
3069
4655
|
? {
|
|
3070
4656
|
runtime: {
|
|
@@ -3084,7 +4670,41 @@ function startLocalServer(opts) {
|
|
|
3084
4670
|
(0, local_funnel_1.scheduleFunnelProcessing)(convId);
|
|
3085
4671
|
}
|
|
3086
4672
|
catch (orchErr) {
|
|
3087
|
-
|
|
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
|
+
}
|
|
3088
4708
|
sendEvent('error', { type: 'error', error: orchErr.message });
|
|
3089
4709
|
responseEnded = true;
|
|
3090
4710
|
res.end();
|
|
@@ -3104,13 +4724,6 @@ function startLocalServer(opts) {
|
|
|
3104
4724
|
: expandAllowedToolNames(allToolDefs, configuredBuiltinTools, configuredMcpTools);
|
|
3105
4725
|
const toolDefs = allToolDefs.filter((tool) => allowedToolNames.has(tool.name));
|
|
3106
4726
|
const conversation = data.getConversation(convId);
|
|
3107
|
-
const topicTitle = topicId ? data.getTopic(topicId)?.title : undefined;
|
|
3108
|
-
const project = conversation?.project_id ? data.getProject(conversation.project_id) : undefined;
|
|
3109
|
-
const workspacePath = project?.folder?.trim() || undefined;
|
|
3110
|
-
let llmSpawnCwd = opts.projectDir;
|
|
3111
|
-
if (workspacePath && fs.existsSync(workspacePath)) {
|
|
3112
|
-
llmSpawnCwd = workspacePath;
|
|
3113
|
-
}
|
|
3114
4727
|
// Resolve LLM runtime early so the local desktop prompt contract can differ
|
|
3115
4728
|
// between API/fresh CLI and recurring CLI sessions without affecting server paths.
|
|
3116
4729
|
const runtime = await buildChatRuntime(profile);
|
|
@@ -3140,33 +4753,133 @@ function startLocalServer(opts) {
|
|
|
3140
4753
|
const cliEpochStartedAt = cliSessionEpochPlan.resumeSessionId
|
|
3141
4754
|
? (cliSessionEpochPlan.existing?.epoch_started_at || localTimestamp())
|
|
3142
4755
|
: localTimestamp();
|
|
3143
|
-
const
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
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({
|
|
3153
4803
|
conversationId: convId,
|
|
3154
4804
|
currentBotId: profile.id,
|
|
3155
4805
|
currentBotName: profile.name,
|
|
3156
4806
|
currentProvider: activeProviderName,
|
|
3157
|
-
userPrompt: message,
|
|
4807
|
+
userPrompt: supportsNativeImageInput(activeProviderName) ? message : runtimeUserPrompt,
|
|
3158
4808
|
soulMd: profile.soul_md || 'You are an AI assistant running locally. You have access to project files and can execute code.',
|
|
3159
|
-
projectName:
|
|
3160
|
-
topicTitle: topicTitle
|
|
3161
|
-
workspacePath,
|
|
4809
|
+
projectName: promptContext.projectName,
|
|
4810
|
+
topicTitle: promptContext.topicTitle,
|
|
4811
|
+
workspacePath: promptContext.workspacePath,
|
|
3162
4812
|
timezone: effectiveTimezone,
|
|
3163
4813
|
availableTools: toolDefs.map((tool) => ({ name: tool.name, description: tool.description })),
|
|
3164
|
-
isCliRecurring: !!cliSessionEpochPlan.resumeSessionId,
|
|
3165
|
-
cliHistoryFilePath,
|
|
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),
|
|
3166
4820
|
useCompletionSentinel: resolveDirectCliSessionTransport(activeProviderName, enableCliSessionEpoch, (0, storage_mode_1.isLocalStorageMode)()) === 'pty',
|
|
3167
4821
|
});
|
|
3168
|
-
|
|
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
|
+
}];
|
|
3169
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
|
+
};
|
|
3170
4883
|
if (!activeApiKey) {
|
|
3171
4884
|
return res.status(400).json({ error: `No API key for provider ${profile.provider}. Configure one in Settings.` });
|
|
3172
4885
|
}
|
|
@@ -3194,10 +4907,17 @@ function startLocalServer(opts) {
|
|
|
3194
4907
|
// CLI providers only see the last user message — prepend pinned context there
|
|
3195
4908
|
const lastUserIdx = llmMessages.map(m => m.role).lastIndexOf('user');
|
|
3196
4909
|
if (lastUserIdx >= 0) {
|
|
3197
|
-
llmMessages[lastUserIdx]
|
|
3198
|
-
|
|
3199
|
-
content: `${pinnedBlock}\n---\n${
|
|
3200
|
-
}
|
|
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
|
+
}
|
|
3201
4921
|
}
|
|
3202
4922
|
}
|
|
3203
4923
|
else {
|
|
@@ -3298,7 +5018,7 @@ function startLocalServer(opts) {
|
|
|
3298
5018
|
while (true) {
|
|
3299
5019
|
appServerAttempt++;
|
|
3300
5020
|
try {
|
|
3301
|
-
const isFreshSession = forceFreshInteractiveCliSession || !cliSessionEpochPlan.resumeSessionId;
|
|
5021
|
+
const isFreshSession = forceFreshInteractiveCliSession || (!codexAppServerManager.hasActiveSession(convId, profile.id) && !cliSessionEpochPlan.resumeSessionId);
|
|
3302
5022
|
const result = await codexAppServerManager.runTurn({
|
|
3303
5023
|
runtimeMode: 'local_desktop',
|
|
3304
5024
|
conversationId: convId,
|
|
@@ -3308,7 +5028,7 @@ function startLocalServer(opts) {
|
|
|
3308
5028
|
systemPrompt,
|
|
3309
5029
|
messages: llmMessages,
|
|
3310
5030
|
forceFreshSession: isFreshSession,
|
|
3311
|
-
resumeSessionId: cliSessionEpochPlan.resumeSessionId || undefined,
|
|
5031
|
+
resumeSessionId: isFreshSession ? undefined : (cliSessionEpochPlan.resumeSessionId || undefined),
|
|
3312
5032
|
model: activeModelName || profile.model || null,
|
|
3313
5033
|
projectId: conversation?.project_id ?? null,
|
|
3314
5034
|
codexSettings: {
|
|
@@ -3326,6 +5046,12 @@ function startLocalServer(opts) {
|
|
|
3326
5046
|
persistAssistantPartial(false);
|
|
3327
5047
|
sendEvent('chunk', { text: chunk });
|
|
3328
5048
|
},
|
|
5049
|
+
onCommentary: async (commentary) => {
|
|
5050
|
+
const text = String(commentary || '').trim();
|
|
5051
|
+
if (!text)
|
|
5052
|
+
return;
|
|
5053
|
+
sendEvent('status', { phase: 'commentary', detail: text });
|
|
5054
|
+
},
|
|
3329
5055
|
onDetail: async (detail) => {
|
|
3330
5056
|
const text = String(detail || '').trim();
|
|
3331
5057
|
if (!text)
|
|
@@ -3333,6 +5059,35 @@ function startLocalServer(opts) {
|
|
|
3333
5059
|
sendEvent('status', { phase: 'thinking', detail: text });
|
|
3334
5060
|
recordActivity('status', { phase: 'thinking', detail: text }, text);
|
|
3335
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
|
+
},
|
|
3336
5091
|
});
|
|
3337
5092
|
if (result.sessionId) {
|
|
3338
5093
|
activeCliSessionId = result.sessionId;
|
|
@@ -3342,10 +5097,40 @@ function startLocalServer(opts) {
|
|
|
3342
5097
|
totalOutputTokens += result.usage.outputTokens || 0;
|
|
3343
5098
|
hasExactUsage = true;
|
|
3344
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 ──────────────────────────────────────────
|
|
3345
5127
|
rawCliTranscript = result.rawOutput || '';
|
|
3346
5128
|
fullContent = (0, completion_marker_1.stripCompletionSentinel)((result.content || '').trim()).text.trim();
|
|
3347
5129
|
if (!fullContent && appServerAttempt < LOCAL_RUNTIME_RETRY_LIMIT) {
|
|
3348
5130
|
forceFreshInteractiveCliSession = true;
|
|
5131
|
+
if (cliSessionEpochPlan.resumeSessionId) {
|
|
5132
|
+
applyFreshCliBootstrapFallback('resume_failed');
|
|
5133
|
+
}
|
|
3349
5134
|
codexAppServerManager.closeSessionByConversation(convId, profile.id);
|
|
3350
5135
|
const retryDetail = `Selected runtime returned an empty response; retrying with a fresh ${activeProviderName} session (${appServerAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`;
|
|
3351
5136
|
console.warn(chalk_1.default.yellow(` [chat] ${retryDetail}`));
|
|
@@ -3358,10 +5143,14 @@ function startLocalServer(opts) {
|
|
|
3358
5143
|
if (routeAbortController.signal.aborted || codexErr?.name === 'AbortError') {
|
|
3359
5144
|
throw codexErr;
|
|
3360
5145
|
}
|
|
5146
|
+
clearFailedLocalCliSessionEpoch(convId, profile.id, activeCliSessionId || cliSessionEpochPlan.resumeSessionId);
|
|
3361
5147
|
if (appServerAttempt >= LOCAL_RUNTIME_RETRY_LIMIT || !shouldRetrySelectedLocalRuntime(codexErr)) {
|
|
3362
5148
|
throw codexErr;
|
|
3363
5149
|
}
|
|
3364
5150
|
forceFreshInteractiveCliSession = true;
|
|
5151
|
+
if (cliSessionEpochPlan.resumeSessionId) {
|
|
5152
|
+
applyFreshCliBootstrapFallback('resume_failed');
|
|
5153
|
+
}
|
|
3365
5154
|
codexAppServerManager.closeSessionByConversation(convId, profile.id);
|
|
3366
5155
|
const retryDetail = `Selected runtime failed (${codexErr?.message || codexErr}); retrying with a fresh ${activeProviderName} session (${appServerAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`;
|
|
3367
5156
|
console.warn(chalk_1.default.yellow(` [chat] ${retryDetail}`));
|
|
@@ -3381,10 +5170,9 @@ function startLocalServer(opts) {
|
|
|
3381
5170
|
while (true) {
|
|
3382
5171
|
ptyAttempt++;
|
|
3383
5172
|
try {
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
const
|
|
3387
|
-
const newSessionId = isFreshSession && activeProviderName === 'claude-cli'
|
|
5173
|
+
const hasLivePtySession = ptyManager.hasActiveSession(convId, profile.id);
|
|
5174
|
+
const isFreshSession = forceFreshInteractiveCliSession || (!hasLivePtySession && !cliSessionEpochPlan.resumeSessionId);
|
|
5175
|
+
const newSessionId = activeProviderName === 'claude-cli' && isFreshSession
|
|
3388
5176
|
? data.generateNextSessionId()
|
|
3389
5177
|
: undefined;
|
|
3390
5178
|
currentAttemptWasFreshSession = isFreshSession;
|
|
@@ -3393,11 +5181,20 @@ function startLocalServer(opts) {
|
|
|
3393
5181
|
conversationId: convId,
|
|
3394
5182
|
botId: profile.id,
|
|
3395
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
|
+
},
|
|
3396
5193
|
cwd: llmSpawnCwd,
|
|
3397
5194
|
systemPrompt,
|
|
3398
5195
|
messages: llmMessages,
|
|
3399
5196
|
forceFreshSession: isFreshSession,
|
|
3400
|
-
resumeSessionId: cliSessionEpochPlan.resumeSessionId || undefined,
|
|
5197
|
+
resumeSessionId: isFreshSession ? undefined : (cliSessionEpochPlan.resumeSessionId || undefined),
|
|
3401
5198
|
newSessionId,
|
|
3402
5199
|
abortSignal: routeAbortController.signal,
|
|
3403
5200
|
onRawChunk: async (chunk) => {
|
|
@@ -3430,10 +5227,40 @@ function startLocalServer(opts) {
|
|
|
3430
5227
|
totalOutputTokens += result.usage.outputTokens || 0;
|
|
3431
5228
|
hasExactUsage = true;
|
|
3432
5229
|
}
|
|
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 ──────────────────────────────────────────
|
|
3433
5257
|
rawCliTranscript = result.rawOutput || '';
|
|
3434
5258
|
fullContent = (0, completion_marker_1.stripCompletionSentinel)((result.content || '').trim()).text.trim();
|
|
3435
5259
|
if (!fullContent && ptyAttempt < LOCAL_RUNTIME_RETRY_LIMIT) {
|
|
3436
5260
|
forceFreshInteractiveCliSession = true;
|
|
5261
|
+
if (cliSessionEpochPlan.resumeSessionId) {
|
|
5262
|
+
applyFreshCliBootstrapFallback('resume_failed');
|
|
5263
|
+
}
|
|
3437
5264
|
ptyManager.closeSessionByConversation(convId, profile.id);
|
|
3438
5265
|
const retryDetail = `Selected runtime returned an empty response; retrying with a fresh ${activeProviderName} session (${ptyAttempt + 1}/${LOCAL_RUNTIME_RETRY_LIMIT})...`;
|
|
3439
5266
|
console.warn(chalk_1.default.yellow(` [chat] ${retryDetail}`));
|
|
@@ -3446,6 +5273,8 @@ function startLocalServer(opts) {
|
|
|
3446
5273
|
if (routeAbortController.signal.aborted || ptyErr?.name === 'AbortError') {
|
|
3447
5274
|
throw ptyErr;
|
|
3448
5275
|
}
|
|
5276
|
+
ptyManager.logSessionFailureByConversation(convId, profile.id, 'chat_runtime_failure_before_kill', ptyErr);
|
|
5277
|
+
clearFailedLocalCliSessionEpoch(convId, profile.id, currentAttemptSessionId || activeCliSessionId || cliSessionEpochPlan.resumeSessionId);
|
|
3449
5278
|
if (ptyAttempt >= LOCAL_RUNTIME_RETRY_LIMIT || !shouldRetrySelectedLocalRuntime(ptyErr)) {
|
|
3450
5279
|
throw ptyErr;
|
|
3451
5280
|
}
|
|
@@ -3454,11 +5283,19 @@ function startLocalServer(opts) {
|
|
|
3454
5283
|
forceFreshInteractiveCliSession = true;
|
|
3455
5284
|
ptyManager.closeSessionByConversation(convId, profile.id);
|
|
3456
5285
|
}
|
|
5286
|
+
const resumeFailureFallback = !!cliSessionEpochPlan.resumeSessionId && !currentAttemptWasFreshSession;
|
|
5287
|
+
if (resumeFailureFallback) {
|
|
5288
|
+
forceFreshInteractiveCliSession = true;
|
|
5289
|
+
applyFreshCliBootstrapFallback('resume_failed');
|
|
5290
|
+
ptyManager.closeSessionByConversation(convId, profile.id);
|
|
5291
|
+
}
|
|
3457
5292
|
const retryDetail = startupRetry
|
|
3458
|
-
? `Fresh ${activeProviderName} session ${currentAttemptSessionId || '(unknown)'} did not
|
|
3459
|
-
:
|
|
3460
|
-
? `
|
|
3461
|
-
:
|
|
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})...`;
|
|
3462
5299
|
console.warn(chalk_1.default.yellow(` [chat] ${retryDetail}`));
|
|
3463
5300
|
await pauseLocalRuntimeRetry(ptyAttempt);
|
|
3464
5301
|
}
|
|
@@ -3634,6 +5471,17 @@ function startLocalServer(opts) {
|
|
|
3634
5471
|
}
|
|
3635
5472
|
persistAssistantPartial(true);
|
|
3636
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)'}`);
|
|
3637
5485
|
const nextEpochTurnCount = cliSessionEpochPlan.resumeSessionId
|
|
3638
5486
|
? ((cliSessionEpochPlan.existing?.epoch_turn_count || 0) + 1)
|
|
3639
5487
|
: 1;
|
|
@@ -3645,7 +5493,7 @@ function startLocalServer(opts) {
|
|
|
3645
5493
|
epochTurnCount: nextEpochTurnCount,
|
|
3646
5494
|
lastInputTokens: hasExactUsage ? totalInputTokens : approxInputTokens,
|
|
3647
5495
|
lastOutputTokens: hasExactUsage ? totalOutputTokens : 0,
|
|
3648
|
-
resetReason:
|
|
5496
|
+
resetReason: cliEpochResetReason,
|
|
3649
5497
|
epochStartedAt: cliEpochStartedAt,
|
|
3650
5498
|
lastUsedAt: localTimestamp(),
|
|
3651
5499
|
});
|
|
@@ -3679,6 +5527,12 @@ function startLocalServer(opts) {
|
|
|
3679
5527
|
resultArtifact: useInteractiveCliSession ? (rawCliTranscript || null) : undefined,
|
|
3680
5528
|
}) || data.addMessage(convId, 'assistant', persistedContent, modelWithRuntime || undefined, undefined, profile.id, profile.name))
|
|
3681
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
|
+
]);
|
|
3682
5536
|
data.attachMessageActivitiesToMessage(activityStreamId, savedMessage.id);
|
|
3683
5537
|
data.createMessageActivity({
|
|
3684
5538
|
conversationId: convId,
|
|
@@ -3716,6 +5570,9 @@ function startLocalServer(opts) {
|
|
|
3716
5570
|
// For CLI providers, set a simple title without burning a CLI call
|
|
3717
5571
|
const shortTitle = message.slice(0, 60).replace(/\n/g, ' ').trim();
|
|
3718
5572
|
data.updateConversation(convId, { title: shortTitle || 'New Chat' });
|
|
5573
|
+
emitConversationSyncEvents(convId, [
|
|
5574
|
+
{ change: 'conversation.updated' },
|
|
5575
|
+
]);
|
|
3719
5576
|
}
|
|
3720
5577
|
else {
|
|
3721
5578
|
autoTitleConversation(convId, message, persistedContent, activeProviderName, activeModelName, activeApiKey);
|
|
@@ -3730,8 +5587,17 @@ function startLocalServer(opts) {
|
|
|
3730
5587
|
(0, local_funnel_1.scheduleFunnelProcessing)(convId);
|
|
3731
5588
|
}
|
|
3732
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.)
|
|
3733
5595
|
console.error(chalk_1.default.red(`Chat error: ${err.message}`));
|
|
5596
|
+
if (err?.stack) {
|
|
5597
|
+
console.error(String(err.stack));
|
|
5598
|
+
}
|
|
3734
5599
|
try {
|
|
5600
|
+
const userMessage = buildLocalRuntimeUserFacingError(err);
|
|
3735
5601
|
if (activityErrorContext.conversationId) {
|
|
3736
5602
|
data.createMessageActivity({
|
|
3737
5603
|
conversationId: activityErrorContext.conversationId,
|
|
@@ -3743,6 +5609,8 @@ function startLocalServer(opts) {
|
|
|
3743
5609
|
summary: err.message,
|
|
3744
5610
|
payload: {
|
|
3745
5611
|
error: err.message,
|
|
5612
|
+
userMessage,
|
|
5613
|
+
stack: err?.stack || null,
|
|
3746
5614
|
authRequired: err?.authRequired === true,
|
|
3747
5615
|
providerId: err?.providerId || null,
|
|
3748
5616
|
cli: err?.cli || null,
|
|
@@ -3755,12 +5623,13 @@ function startLocalServer(opts) {
|
|
|
3755
5623
|
// best effort only
|
|
3756
5624
|
}
|
|
3757
5625
|
if (!res.headersSent) {
|
|
3758
|
-
res.status(500).json({ error: err.message });
|
|
5626
|
+
res.status(500).json({ error: err.message, userMessage: buildLocalRuntimeUserFacingError(err) });
|
|
3759
5627
|
}
|
|
3760
5628
|
else {
|
|
3761
5629
|
try {
|
|
3762
5630
|
res.write(`event: error\ndata: ${JSON.stringify({
|
|
3763
5631
|
error: err.message,
|
|
5632
|
+
userMessage: buildLocalRuntimeUserFacingError(err),
|
|
3764
5633
|
authRequired: err?.authRequired === true,
|
|
3765
5634
|
providerId: err?.providerId || null,
|
|
3766
5635
|
cli: err?.cli || null,
|
|
@@ -3848,6 +5717,650 @@ function startLocalServer(opts) {
|
|
|
3848
5717
|
res.status(500).json({ error: err.message });
|
|
3849
5718
|
}
|
|
3850
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
|
+
});
|
|
3851
6364
|
app.get('/api/admin/audit', (req, res) => {
|
|
3852
6365
|
try {
|
|
3853
6366
|
const rows = data.listAdminAudit({
|
|
@@ -3889,16 +6402,14 @@ function startLocalServer(opts) {
|
|
|
3889
6402
|
// ─── Clerk Config ──────────────────────────────────────────
|
|
3890
6403
|
app.get('/api/clerk/config', (_req, res) => {
|
|
3891
6404
|
try {
|
|
3892
|
-
const
|
|
3893
|
-
const clerkModel = data.getSetting('clerk_model');
|
|
3894
|
-
const hasKey = !!data.getSetting('clerk_api_key');
|
|
6405
|
+
const clerkConfig = data.getResolvedClerkConfigInfo();
|
|
3895
6406
|
const currentOrchestrator = data.getCurrentOrchestratorSelection();
|
|
3896
6407
|
const currentDefaultBot = data.getDefaultAgentProfile();
|
|
3897
6408
|
res.json({
|
|
3898
|
-
provider:
|
|
3899
|
-
model:
|
|
3900
|
-
hasApiKey:
|
|
3901
|
-
configured:
|
|
6409
|
+
provider: clerkConfig.provider,
|
|
6410
|
+
model: clerkConfig.model,
|
|
6411
|
+
hasApiKey: clerkConfig.hasSecret,
|
|
6412
|
+
configured: clerkConfig.configured,
|
|
3902
6413
|
isOrchestrator: data.isClerkOrchestratorEnabled(),
|
|
3903
6414
|
currentOrchestrator,
|
|
3904
6415
|
currentDefaultBot: currentDefaultBot
|
|
@@ -4794,6 +7305,8 @@ function startLocalServer(opts) {
|
|
|
4794
7305
|
});
|
|
4795
7306
|
// Initialize workflow engine
|
|
4796
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);
|
|
4797
7310
|
// Start server
|
|
4798
7311
|
return new Promise((resolve, reject) => {
|
|
4799
7312
|
_server = app.listen(port, '127.0.0.1', () => {
|
|
@@ -4801,6 +7314,12 @@ function startLocalServer(opts) {
|
|
|
4801
7314
|
console.log(chalk_1.default.green(`\n Local server: http://127.0.0.1:${port}`));
|
|
4802
7315
|
resolve(_server);
|
|
4803
7316
|
});
|
|
7317
|
+
_server.on('close', () => {
|
|
7318
|
+
(0, managed_process_registry_1.stopIdleSweep)();
|
|
7319
|
+
for (const botId of [...cliBotConfigWatchers.keys()]) {
|
|
7320
|
+
closeCliBotConfigWatcher(botId);
|
|
7321
|
+
}
|
|
7322
|
+
});
|
|
4804
7323
|
_server.on('error', (err) => {
|
|
4805
7324
|
if (err.code === 'EADDRINUSE') {
|
|
4806
7325
|
console.error(chalk_1.default.red(`Port ${port} already in use`));
|
|
@@ -4810,6 +7329,7 @@ function startLocalServer(opts) {
|
|
|
4810
7329
|
});
|
|
4811
7330
|
}
|
|
4812
7331
|
function stopLocalServer() {
|
|
7332
|
+
(0, managed_process_registry_1.stopIdleSweep)();
|
|
4813
7333
|
return new Promise((resolve) => {
|
|
4814
7334
|
if (_server) {
|
|
4815
7335
|
_server.close(() => {
|
|
@@ -4933,18 +7453,46 @@ function hydrateMessageDisplayMetadata(message) {
|
|
|
4933
7453
|
model: [modelBase, runtimeLabel].filter(Boolean).join(' | '),
|
|
4934
7454
|
};
|
|
4935
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
|
+
}
|
|
4936
7487
|
function buildLocalDesktopDirectPrompt(input) {
|
|
4937
7488
|
const isCliProvider = index_1.CLI_PROVIDERS.has(input.currentProvider);
|
|
4938
7489
|
const useCompletionSentinel = !!input.useCompletionSentinel;
|
|
4939
|
-
const
|
|
4940
|
-
const
|
|
4941
|
-
|
|
4942
|
-
: [];
|
|
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;
|
|
4943
7493
|
const contract = !isCliProvider || !input.isCliRecurring
|
|
4944
7494
|
? 'api_or_fresh_cli'
|
|
4945
|
-
:
|
|
4946
|
-
? 'cli_recurring_multibot'
|
|
4947
|
-
: 'cli_recurring_single';
|
|
7495
|
+
: 'cli_recurring_single';
|
|
4948
7496
|
const lines = [
|
|
4949
7497
|
'[Bot Identity]',
|
|
4950
7498
|
input.soulMd.trim(),
|
|
@@ -4978,16 +7526,51 @@ function buildLocalDesktopDirectPrompt(input) {
|
|
|
4978
7526
|
lines.push('If the user refers to prior project or conversation context that is not included here, search with these tools first.');
|
|
4979
7527
|
}
|
|
4980
7528
|
}
|
|
4981
|
-
|
|
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) {
|
|
4982
7558
|
const summaryWindow = (0, context_window_1.getPromptContextWindow)(input.conversationId, safeguards_1.SAFEGUARDS.CONTEXT_WINDOW_TURNS);
|
|
4983
7559
|
if (summaryWindow.summary?.summary_text?.trim()) {
|
|
4984
|
-
lines.push('',
|
|
7560
|
+
lines.push('', summaryWindow.carriedForward
|
|
7561
|
+
? '[Context Summary (Carried Forward from Previous Conversation in This Topic)]'
|
|
7562
|
+
: '[Context Summary]', summaryWindow.summary.summary_text.trim());
|
|
4985
7563
|
}
|
|
4986
7564
|
const recentTurnsWindow = (0, context_window_1.getPromptContextWindow)(input.conversationId, summaryWindow.summary?.summary_text?.trim()
|
|
4987
7565
|
? safeguards_1.SAFEGUARDS.CONTEXT_WINDOW_TURNS
|
|
4988
7566
|
: safeguards_1.SAFEGUARDS.NO_SUMMARY_CONTEXT_WINDOW_TURNS);
|
|
4989
|
-
|
|
4990
|
-
|
|
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));
|
|
4991
7574
|
}
|
|
4992
7575
|
}
|
|
4993
7576
|
let effectiveUserPrompt = input.userPrompt;
|
|
@@ -5002,15 +7585,6 @@ function buildLocalDesktopDirectPrompt(input) {
|
|
|
5002
7585
|
input.userPrompt,
|
|
5003
7586
|
].join('\n');
|
|
5004
7587
|
}
|
|
5005
|
-
if (contract === 'cli_recurring_multibot' && crossBotReplies.length > 0) {
|
|
5006
|
-
effectiveUserPrompt = [
|
|
5007
|
-
'[Cross-Bot Context]',
|
|
5008
|
-
...crossBotReplies.map((line) => `- ${line}`),
|
|
5009
|
-
'',
|
|
5010
|
-
'Current user request:',
|
|
5011
|
-
input.userPrompt,
|
|
5012
|
-
].join('\n');
|
|
5013
|
-
}
|
|
5014
7588
|
if (useCompletionSentinel) {
|
|
5015
7589
|
effectiveUserPrompt = [
|
|
5016
7590
|
effectiveUserPrompt,
|
|
@@ -5027,70 +7601,17 @@ function buildLocalDesktopDirectPrompt(input) {
|
|
|
5027
7601
|
function buildLocalDesktopDirectPromptForTest(input) {
|
|
5028
7602
|
return buildLocalDesktopDirectPrompt(input);
|
|
5029
7603
|
}
|
|
5030
|
-
function
|
|
5031
|
-
|
|
5032
|
-
const filename = `${input.conversationId}--${input.botId}.txt`;
|
|
5033
|
-
const historyFilePath = path.join(historyDir, filename);
|
|
5034
|
-
const historyContent = buildCliBootstrapHistoryContent(input.conversationId, input.topicId);
|
|
5035
|
-
try {
|
|
5036
|
-
fs.mkdirSync(historyDir, { recursive: true });
|
|
5037
|
-
cleanupStaleCliHistoryFiles(historyDir);
|
|
5038
|
-
fs.writeFileSync(historyFilePath, historyContent, 'utf8');
|
|
5039
|
-
return historyFilePath;
|
|
5040
|
-
}
|
|
5041
|
-
catch (err) {
|
|
5042
|
-
console.warn(chalk_1.default.yellow(` [cli-history] Failed to write bootstrap history file: ${err instanceof Error ? err.message : String(err)}`));
|
|
5043
|
-
return null;
|
|
5044
|
-
}
|
|
7604
|
+
function resolveLocalDesktopPromptContextForTest(input) {
|
|
7605
|
+
return resolveLocalDesktopPromptContext(input);
|
|
5045
7606
|
}
|
|
5046
|
-
function
|
|
5047
|
-
|
|
5048
|
-
const previousConversationId = topicId ? data.getPreviousConversationInTopic(conversationId)?.id || null : null;
|
|
5049
|
-
lines.push('[Bootstrap History]');
|
|
5050
|
-
if (!topicId) {
|
|
5051
|
-
lines.push('No prior topic history is available for this conversation.');
|
|
5052
|
-
return lines.join('\n').trim();
|
|
5053
|
-
}
|
|
5054
|
-
if (!previousConversationId) {
|
|
5055
|
-
lines.push('No prior topic history is available for this conversation.');
|
|
5056
|
-
return lines.join('\n').trim();
|
|
5057
|
-
}
|
|
5058
|
-
const rollingSummary = (0, context_window_1.getLatestRollingSummary)(previousConversationId);
|
|
5059
|
-
const recentTurns = (0, context_window_1.getRecentTurns)(previousConversationId, 5);
|
|
5060
|
-
if (rollingSummary?.summary_text?.trim()) {
|
|
5061
|
-
lines.push('', '[Running Summary]', rollingSummary.summary_text.trim());
|
|
5062
|
-
}
|
|
5063
|
-
if (recentTurns.length > 0) {
|
|
5064
|
-
lines.push('', '[Last 5 Turns]', (0, context_window_1.formatTurnsForPrompt)(recentTurns));
|
|
5065
|
-
}
|
|
5066
|
-
else {
|
|
5067
|
-
lines.push('', '[Last 5 Turns]', '(no recent topic turns)');
|
|
5068
|
-
}
|
|
5069
|
-
return lines.join('\n').trim();
|
|
7607
|
+
function shouldSkipFreshCliBootstrapForTest(input) {
|
|
7608
|
+
return shouldSkipFreshCliBootstrap(input);
|
|
5070
7609
|
}
|
|
5071
|
-
function
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
for (const file of files) {
|
|
5077
|
-
if (!file.toLowerCase().endsWith('.txt'))
|
|
5078
|
-
continue;
|
|
5079
|
-
const fullPath = path.join(historyDir, file);
|
|
5080
|
-
try {
|
|
5081
|
-
const stat = fs.statSync(fullPath);
|
|
5082
|
-
if (now - stat.mtimeMs > maxAgeMs) {
|
|
5083
|
-
fs.unlinkSync(fullPath);
|
|
5084
|
-
}
|
|
5085
|
-
}
|
|
5086
|
-
catch {
|
|
5087
|
-
// best effort cleanup only
|
|
5088
|
-
}
|
|
5089
|
-
}
|
|
5090
|
-
}
|
|
5091
|
-
catch {
|
|
5092
|
-
// best effort cleanup only
|
|
5093
|
-
}
|
|
7610
|
+
function buildLocalRuntimeUserFacingErrorForTest(err) {
|
|
7611
|
+
return buildLocalRuntimeUserFacingError(err);
|
|
7612
|
+
}
|
|
7613
|
+
function persistCliOauthSessionForTest(input) {
|
|
7614
|
+
return persistCliOauthSession(input.providerId, input.accessToken, input.refreshToken, input.expiresAt);
|
|
5094
7615
|
}
|
|
5095
7616
|
function buildLocalDesktopToolManifest(availableTools, contract) {
|
|
5096
7617
|
const selectedTools = contract === 'api_or_fresh_cli'
|
|
@@ -5101,32 +7622,6 @@ function buildLocalDesktopToolManifest(availableTools, contract) {
|
|
|
5101
7622
|
.join('\n')
|
|
5102
7623
|
.trim();
|
|
5103
7624
|
}
|
|
5104
|
-
function getLatestOtherBotReplies(conversationId, currentBotId, currentBotName) {
|
|
5105
|
-
const turns = (0, context_window_1.getPromptContextWindow)(conversationId, safeguards_1.SAFEGUARDS.NO_SUMMARY_CONTEXT_WINDOW_TURNS).turns;
|
|
5106
|
-
const seen = new Set();
|
|
5107
|
-
const replies = [];
|
|
5108
|
-
for (let turnIndex = turns.length - 1; turnIndex >= 0; turnIndex -= 1) {
|
|
5109
|
-
const turn = turns[turnIndex];
|
|
5110
|
-
for (let responseIndex = turn.responses.length - 1; responseIndex >= 0; responseIndex -= 1) {
|
|
5111
|
-
const response = turn.responses[responseIndex];
|
|
5112
|
-
if (response.role !== 'assistant')
|
|
5113
|
-
continue;
|
|
5114
|
-
const identity = String(response.bot_id || response.agent_name || '').trim();
|
|
5115
|
-
if (!identity)
|
|
5116
|
-
continue;
|
|
5117
|
-
const sameBot = (response.bot_id && response.bot_id === currentBotId)
|
|
5118
|
-
|| (response.agent_name && response.agent_name === currentBotName);
|
|
5119
|
-
if (sameBot || seen.has(identity))
|
|
5120
|
-
continue;
|
|
5121
|
-
const content = String(response.content || '').trim();
|
|
5122
|
-
if (!content)
|
|
5123
|
-
continue;
|
|
5124
|
-
seen.add(identity);
|
|
5125
|
-
replies.unshift(`${response.agent_name || 'Assistant'} (${response.created_at}): ${content}`);
|
|
5126
|
-
}
|
|
5127
|
-
}
|
|
5128
|
-
return replies;
|
|
5129
|
-
}
|
|
5130
7625
|
function classifyOrchestratorProgress(status) {
|
|
5131
7626
|
const trimmed = String(status || '').trim();
|
|
5132
7627
|
if (!trimmed)
|
|
@@ -5168,8 +7663,45 @@ function resolveCliNameForProvider(providerName) {
|
|
|
5168
7663
|
return 'codex';
|
|
5169
7664
|
return null;
|
|
5170
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
|
+
}
|
|
5171
7678
|
function isInteractiveAuthFailure(text) {
|
|
5172
|
-
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;
|
|
5173
7705
|
}
|
|
5174
7706
|
function detectInteractiveAuthFailure(text, activeProviderName, configuredProviderName) {
|
|
5175
7707
|
if (!isInteractiveAuthFailure(text))
|
|
@@ -5187,19 +7719,60 @@ function detectInteractiveAuthFailure(text, activeProviderName, configuredProvid
|
|
|
5187
7719
|
};
|
|
5188
7720
|
}
|
|
5189
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
|
+
}
|
|
5190
7737
|
function isClaudeFreshSessionStartupFailure(err) {
|
|
5191
7738
|
return err?.code === 'CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT'
|
|
5192
7739
|
|| err?.name === 'ClaudeFreshSessionStartupTimeoutError'
|
|
5193
7740
|
|| /fresh session startup timed out/i.test(String(err?.message || err || ''));
|
|
5194
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
|
+
}
|
|
5195
7768
|
function shouldRetrySelectedLocalRuntime(err) {
|
|
5196
7769
|
const text = String(err?.message || err || '').toLowerCase();
|
|
5197
7770
|
if (!text)
|
|
5198
7771
|
return false;
|
|
5199
|
-
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)) {
|
|
5200
7773
|
return false;
|
|
5201
7774
|
}
|
|
5202
|
-
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);
|
|
5203
7776
|
}
|
|
5204
7777
|
async function pauseLocalRuntimeRetry(attempt) {
|
|
5205
7778
|
const delayMs = attempt <= 1 ? 750 : 1500;
|
|
@@ -5285,6 +7858,12 @@ async function autoTitleConversation(convId, userMsg, assistantMsg, providerName
|
|
|
5285
7858
|
});
|
|
5286
7859
|
if (resp.content) {
|
|
5287
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
|
+
});
|
|
5288
7867
|
}
|
|
5289
7868
|
}
|
|
5290
7869
|
catch { /* best-effort */ }
|