@love-moon/conductor-cli 0.2.10 → 0.2.12
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/bin/conductor-config.js +17 -26
- package/bin/conductor-fire.js +823 -161
- package/bin/conductor-update.js +307 -0
- package/bin/conductor.js +8 -4
- package/package.json +3 -3
- package/src/daemon.js +83 -2
- package/src/fire/history.js +109 -0
package/bin/conductor-fire.js
CHANGED
|
@@ -11,6 +11,7 @@ import fs from "node:fs";
|
|
|
11
11
|
import os from "node:os";
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
import process from "node:process";
|
|
14
|
+
import readline from "node:readline";
|
|
14
15
|
import { setTimeout as delay } from "node:timers/promises";
|
|
15
16
|
import { fileURLToPath } from "node:url";
|
|
16
17
|
|
|
@@ -19,17 +20,14 @@ import { hideBin } from "yargs/helpers";
|
|
|
19
20
|
import yaml from "js-yaml";
|
|
20
21
|
import { TuiDriver, claudeCodeProfile, codexProfile, copilotProfile } from "@love-moon/tui-driver";
|
|
21
22
|
import { ConductorClient, loadConfig } from "@love-moon/conductor-sdk";
|
|
22
|
-
import {
|
|
23
|
-
loadHistoryFromSpec,
|
|
24
|
-
parseFromSpec,
|
|
25
|
-
selectHistorySession,
|
|
26
|
-
SUPPORTED_FROM_PROVIDERS,
|
|
27
|
-
} from "../src/fire/history.js";
|
|
23
|
+
import { findSessionPath } from "../src/fire/history.js";
|
|
28
24
|
|
|
29
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
30
26
|
const __dirname = path.dirname(__filename);
|
|
31
27
|
const PKG_ROOT = path.join(__dirname, "..");
|
|
32
|
-
const
|
|
28
|
+
const INITIAL_CLI_PROJECT_PATH = process.cwd();
|
|
29
|
+
const FIRE_LOG_PATH = path.join(INITIAL_CLI_PROJECT_PATH, "conductor.log");
|
|
30
|
+
const ENABLE_FIRE_LOCAL_LOG = !process.env.CONDUCTOR_CLI_COMMAND;
|
|
33
31
|
|
|
34
32
|
const pkgJson = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"));
|
|
35
33
|
const CLI_NAME = (process.env.CONDUCTOR_CLI_NAME || path.basename(process.argv[1] || "conductor-fire")).replace(
|
|
@@ -106,8 +104,20 @@ const DEFAULT_POLL_INTERVAL_MS = parseInt(
|
|
|
106
104
|
10,
|
|
107
105
|
);
|
|
108
106
|
|
|
107
|
+
function appendFireLocalLog(line) {
|
|
108
|
+
if (!ENABLE_FIRE_LOCAL_LOG) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
fs.appendFileSync(FIRE_LOG_PATH, line);
|
|
113
|
+
} catch {
|
|
114
|
+
// ignore file log errors
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
109
118
|
async function main() {
|
|
110
119
|
const cliArgs = parseCliArgs();
|
|
120
|
+
let runtimeProjectPath = process.cwd();
|
|
111
121
|
|
|
112
122
|
if (cliArgs.showHelp) {
|
|
113
123
|
return;
|
|
@@ -134,48 +144,22 @@ async function main() {
|
|
|
134
144
|
return;
|
|
135
145
|
}
|
|
136
146
|
|
|
137
|
-
let
|
|
138
|
-
if (cliArgs.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const selected = await selectHistorySession(provider);
|
|
147
|
-
if (!selected) {
|
|
148
|
-
log("No session selected. Starting a new conversation.");
|
|
149
|
-
resolvedSpec = null;
|
|
150
|
-
} else {
|
|
151
|
-
resolvedSpec = parseFromSpec(`${provider}:${selected.sessionId}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (resolvedSpec) {
|
|
156
|
-
if (cliArgs.backend !== resolvedSpec.provider) {
|
|
157
|
-
log(
|
|
158
|
-
`Ignoring --from ${resolvedSpec.provider}:${resolvedSpec.sessionId} because backend is ${cliArgs.backend}`,
|
|
159
|
-
);
|
|
160
|
-
} else {
|
|
161
|
-
fromHistory = await loadHistoryFromSpec(resolvedSpec);
|
|
162
|
-
if (fromHistory.warning) {
|
|
163
|
-
log(fromHistory.warning);
|
|
164
|
-
} else {
|
|
165
|
-
log(
|
|
166
|
-
`Loaded ${fromHistory.history.length} history messages from ${fromHistory.provider} session ${fromHistory.sessionId}`,
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
147
|
+
let resumeContext = null;
|
|
148
|
+
if (cliArgs.resumeSessionId) {
|
|
149
|
+
resumeContext = await resolveResumeContext(cliArgs.backend, cliArgs.resumeSessionId);
|
|
150
|
+
log(
|
|
151
|
+
`Validated --resume ${resumeContext.sessionId} (${resumeContext.provider}) at ${resumeContext.sessionPath}`,
|
|
152
|
+
);
|
|
153
|
+
log(`Resume will run backend from ${resumeContext.cwd}`);
|
|
154
|
+
runtimeProjectPath = await applyWorkingDirectory(resumeContext.cwd);
|
|
155
|
+
log(`Switched working directory to ${runtimeProjectPath} before Conductor connect`);
|
|
171
156
|
}
|
|
172
157
|
|
|
173
158
|
// Create backend session using tui-driver
|
|
174
159
|
const backendSession = new TuiDriverSession(cliArgs.backend, {
|
|
175
160
|
initialImages: cliArgs.initialImages,
|
|
176
|
-
cwd:
|
|
177
|
-
|
|
178
|
-
resumeSessionId: fromHistory.sessionId,
|
|
161
|
+
cwd: runtimeProjectPath,
|
|
162
|
+
resumeSessionId: cliArgs.resumeSessionId,
|
|
179
163
|
configFile: cliArgs.configFile,
|
|
180
164
|
});
|
|
181
165
|
|
|
@@ -185,6 +169,7 @@ async function main() {
|
|
|
185
169
|
const launchedByDaemon = Boolean(process.env.CONDUCTOR_CLI_COMMAND);
|
|
186
170
|
let reconnectRunner = null;
|
|
187
171
|
let reconnectTaskId = null;
|
|
172
|
+
let pendingRemoteStopEvent = null;
|
|
188
173
|
let conductor = null;
|
|
189
174
|
let reconnectResumeInFlight = false;
|
|
190
175
|
|
|
@@ -221,6 +206,21 @@ async function main() {
|
|
|
221
206
|
})();
|
|
222
207
|
};
|
|
223
208
|
|
|
209
|
+
const handleStopTaskCommand = async (event) => {
|
|
210
|
+
if (!event || typeof event !== "object") {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const taskId = typeof event.taskId === "string" ? event.taskId : "";
|
|
214
|
+
if (reconnectTaskId && taskId && taskId !== reconnectTaskId) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (reconnectRunner && typeof reconnectRunner.requestStopFromRemote === "function") {
|
|
218
|
+
await reconnectRunner.requestStopFromRemote(event);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
pendingRemoteStopEvent = event;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
224
|
if (cliArgs.configFile) {
|
|
225
225
|
env.CONDUCTOR_CONFIG = cliArgs.configFile;
|
|
226
226
|
}
|
|
@@ -239,111 +239,159 @@ async function main() {
|
|
|
239
239
|
// Ignore config loading errors, rely on env vars or defaults
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
conductor = await ConductorClient.connect({
|
|
243
|
-
projectPath: CLI_PROJECT_PATH,
|
|
244
|
-
extraEnv: env,
|
|
245
|
-
configFile: cliArgs.configFile,
|
|
246
|
-
onConnected: scheduleReconnectRecovery,
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
const taskContext = await ensureTaskContext(conductor, {
|
|
250
|
-
initialPrompt: cliArgs.initialPrompt,
|
|
251
|
-
requestedProjectId: process.env.CONDUCTOR_PROJECT_ID,
|
|
252
|
-
providedTaskId: process.env.CONDUCTOR_TASK_ID,
|
|
253
|
-
requestedTitle: cliArgs.taskTitle || process.env.CONDUCTOR_TASK_TITLE,
|
|
254
|
-
backend: cliArgs.backend,
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
log(
|
|
258
|
-
`Attached to Conductor task ${taskContext.taskId}${
|
|
259
|
-
taskContext.appUrl ? ` (${taskContext.appUrl})` : ""
|
|
260
|
-
}`,
|
|
261
|
-
);
|
|
262
|
-
reconnectTaskId = taskContext.taskId;
|
|
263
|
-
|
|
264
242
|
try {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
243
|
+
const requestedTaskTitle = resolveRequestedTaskTitle({
|
|
244
|
+
cliTaskTitle: cliArgs.taskTitle,
|
|
245
|
+
hasExplicitTaskTitle: cliArgs.hasExplicitTaskTitle,
|
|
246
|
+
envTaskTitle: process.env.CONDUCTOR_TASK_TITLE,
|
|
247
|
+
runtimeProjectPath,
|
|
269
248
|
});
|
|
270
|
-
} catch (error) {
|
|
271
|
-
log(`Failed to report agent resume: ${error?.message || error}`);
|
|
272
|
-
}
|
|
273
249
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
cliArgs: cliArgs.rawBackendArgs,
|
|
282
|
-
backendName: cliArgs.backend,
|
|
283
|
-
});
|
|
284
|
-
reconnectRunner = runner;
|
|
250
|
+
conductor = await ConductorClient.connect({
|
|
251
|
+
projectPath: runtimeProjectPath,
|
|
252
|
+
extraEnv: env,
|
|
253
|
+
configFile: cliArgs.configFile,
|
|
254
|
+
onConnected: scheduleReconnectRecovery,
|
|
255
|
+
onStopTask: handleStopTaskCommand,
|
|
256
|
+
});
|
|
285
257
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
258
|
+
const taskContext = await ensureTaskContext(conductor, {
|
|
259
|
+
initialPrompt: cliArgs.initialPrompt,
|
|
260
|
+
requestedProjectId: process.env.CONDUCTOR_PROJECT_ID,
|
|
261
|
+
providedTaskId: process.env.CONDUCTOR_TASK_ID,
|
|
262
|
+
requestedTitle: requestedTaskTitle,
|
|
263
|
+
backend: cliArgs.backend,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
log(
|
|
267
|
+
`Attached to Conductor task ${taskContext.taskId}${
|
|
268
|
+
taskContext.appUrl ? ` (${taskContext.appUrl})` : ""
|
|
269
|
+
}`,
|
|
270
|
+
);
|
|
271
|
+
reconnectTaskId = taskContext.taskId;
|
|
298
272
|
|
|
299
|
-
if (!launchedByDaemon) {
|
|
300
273
|
try {
|
|
301
|
-
await conductor.
|
|
302
|
-
|
|
274
|
+
await conductor.sendAgentResume({
|
|
275
|
+
active_tasks: [taskContext.taskId],
|
|
276
|
+
source: "conductor-fire",
|
|
277
|
+
metadata: { reconnect: false },
|
|
303
278
|
});
|
|
304
279
|
} catch (error) {
|
|
305
|
-
log(`Failed to report
|
|
280
|
+
log(`Failed to report agent resume: ${error?.message || error}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const runner = new BridgeRunner({
|
|
284
|
+
backendSession,
|
|
285
|
+
conductor,
|
|
286
|
+
taskId: taskContext.taskId,
|
|
287
|
+
pollIntervalMs: Math.max(cliArgs.pollIntervalMs, 500),
|
|
288
|
+
initialPrompt: taskContext.shouldProcessInitialPrompt ? cliArgs.initialPrompt : "",
|
|
289
|
+
includeInitialImages: Boolean(cliArgs.initialPrompt && cliArgs.initialImages.length),
|
|
290
|
+
cliArgs: cliArgs.rawBackendArgs,
|
|
291
|
+
backendName: cliArgs.backend,
|
|
292
|
+
resumeMode: Boolean(cliArgs.resumeSessionId),
|
|
293
|
+
});
|
|
294
|
+
reconnectRunner = runner;
|
|
295
|
+
if (pendingRemoteStopEvent) {
|
|
296
|
+
await runner.requestStopFromRemote(pendingRemoteStopEvent);
|
|
297
|
+
pendingRemoteStopEvent = null;
|
|
306
298
|
}
|
|
307
|
-
}
|
|
308
299
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
300
|
+
const signals = new AbortController();
|
|
301
|
+
let shutdownSignal = null;
|
|
302
|
+
let backendShutdownRequested = false;
|
|
303
|
+
const requestBackendShutdown = (source) => {
|
|
304
|
+
if (backendShutdownRequested) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
backendShutdownRequested = true;
|
|
308
|
+
void (async () => {
|
|
309
|
+
try {
|
|
310
|
+
await backendSession.close();
|
|
311
|
+
} catch (error) {
|
|
312
|
+
log(`Failed to close backend session after ${source}: ${error?.message || error}`);
|
|
313
|
+
}
|
|
314
|
+
})();
|
|
315
|
+
};
|
|
316
|
+
const onSigint = () => {
|
|
317
|
+
shutdownSignal = shutdownSignal || "SIGINT";
|
|
318
|
+
signals.abort();
|
|
319
|
+
requestBackendShutdown("SIGINT");
|
|
320
|
+
};
|
|
321
|
+
const onSigterm = () => {
|
|
322
|
+
shutdownSignal = shutdownSignal || "SIGTERM";
|
|
323
|
+
signals.abort();
|
|
324
|
+
requestBackendShutdown("SIGTERM");
|
|
325
|
+
};
|
|
326
|
+
process.on("SIGINT", onSigint);
|
|
327
|
+
process.on("SIGTERM", onSigterm);
|
|
328
|
+
|
|
318
329
|
if (!launchedByDaemon) {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
:
|
|
330
|
+
try {
|
|
331
|
+
await conductor.sendTaskStatus(taskContext.taskId, {
|
|
332
|
+
status: "RUNNING",
|
|
333
|
+
});
|
|
334
|
+
} catch (error) {
|
|
335
|
+
log(`Failed to report task status (RUNNING): ${error?.message || error}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
let runnerError = null;
|
|
340
|
+
try {
|
|
341
|
+
await runner.start(signals.signal);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
runnerError = error;
|
|
344
|
+
throw error;
|
|
345
|
+
} finally {
|
|
346
|
+
process.off("SIGINT", onSigint);
|
|
347
|
+
process.off("SIGTERM", onSigterm);
|
|
348
|
+
if (!launchedByDaemon) {
|
|
349
|
+
const remoteStopSummary = typeof runner.getRemoteStopSummary === "function" ? runner.getRemoteStopSummary() : null;
|
|
350
|
+
const finalStatus = shutdownSignal
|
|
325
351
|
? {
|
|
326
352
|
status: "KILLED",
|
|
327
|
-
summary: `
|
|
353
|
+
summary: `terminated by ${shutdownSignal}`,
|
|
328
354
|
}
|
|
329
|
-
:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
355
|
+
: runnerError
|
|
356
|
+
? {
|
|
357
|
+
status: "KILLED",
|
|
358
|
+
summary: `conductor fire failed: ${runnerError?.message || runnerError}`,
|
|
359
|
+
}
|
|
360
|
+
: remoteStopSummary
|
|
361
|
+
? {
|
|
362
|
+
status: "KILLED",
|
|
363
|
+
summary: remoteStopSummary,
|
|
364
|
+
}
|
|
365
|
+
: {
|
|
366
|
+
status: "COMPLETED",
|
|
367
|
+
summary: "conductor fire exited",
|
|
368
|
+
};
|
|
369
|
+
try {
|
|
370
|
+
await conductor.sendTaskStatus(taskContext.taskId, finalStatus);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
log(`Failed to report task status (${finalStatus.status}): ${error?.message || error}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (shutdownSignal === "SIGINT") {
|
|
376
|
+
process.exitCode = 130;
|
|
377
|
+
} else if (shutdownSignal === "SIGTERM") {
|
|
378
|
+
process.exitCode = 143;
|
|
337
379
|
}
|
|
338
380
|
}
|
|
381
|
+
} finally {
|
|
339
382
|
if (typeof backendSession.close === "function") {
|
|
340
|
-
|
|
383
|
+
try {
|
|
384
|
+
await backendSession.close();
|
|
385
|
+
} catch (error) {
|
|
386
|
+
log(`Failed to close backend session: ${error?.message || error}`);
|
|
387
|
+
}
|
|
341
388
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
389
|
+
if (conductor && typeof conductor.close === "function") {
|
|
390
|
+
try {
|
|
391
|
+
await conductor.close();
|
|
392
|
+
} catch (error) {
|
|
393
|
+
log(`Failed to close conductor connection: ${error?.message || error}`);
|
|
394
|
+
}
|
|
347
395
|
}
|
|
348
396
|
}
|
|
349
397
|
}
|
|
@@ -361,6 +409,65 @@ function extractConfigFileFromArgv(argv) {
|
|
|
361
409
|
return undefined;
|
|
362
410
|
}
|
|
363
411
|
|
|
412
|
+
function hasLegacyFromFlags(argv = []) {
|
|
413
|
+
return argv.some(
|
|
414
|
+
(arg) =>
|
|
415
|
+
arg === "--from" ||
|
|
416
|
+
arg.startsWith("--from=") ||
|
|
417
|
+
arg === "--from-provider" ||
|
|
418
|
+
arg.startsWith("--from-provider="),
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const CONDUCTOR_BOOLEAN_FLAGS = new Set([
|
|
423
|
+
"--list-backends",
|
|
424
|
+
"--version",
|
|
425
|
+
"-v",
|
|
426
|
+
"--help",
|
|
427
|
+
"-h",
|
|
428
|
+
]);
|
|
429
|
+
|
|
430
|
+
const CONDUCTOR_VALUE_FLAGS = new Set([
|
|
431
|
+
"--backend",
|
|
432
|
+
"-b",
|
|
433
|
+
"--config-file",
|
|
434
|
+
"--poll-interval",
|
|
435
|
+
"--title",
|
|
436
|
+
"-t",
|
|
437
|
+
"--resume",
|
|
438
|
+
"--prefill",
|
|
439
|
+
]);
|
|
440
|
+
|
|
441
|
+
function stripConductorArgsFromArgv(argv = []) {
|
|
442
|
+
const backendArgs = [];
|
|
443
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
444
|
+
const raw = String(argv[i] ?? "");
|
|
445
|
+
if (!raw) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (raw === "--") {
|
|
449
|
+
backendArgs.push(...argv.slice(i + 1));
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const eqIndex = raw.indexOf("=");
|
|
454
|
+
const flag = eqIndex > 0 ? raw.slice(0, eqIndex) : raw;
|
|
455
|
+
|
|
456
|
+
if (CONDUCTOR_BOOLEAN_FLAGS.has(flag)) {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (CONDUCTOR_VALUE_FLAGS.has(flag)) {
|
|
460
|
+
if (eqIndex < 0) {
|
|
461
|
+
i += 1;
|
|
462
|
+
}
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
backendArgs.push(raw);
|
|
467
|
+
}
|
|
468
|
+
return backendArgs;
|
|
469
|
+
}
|
|
470
|
+
|
|
364
471
|
export function parseCliArgs(argvInput = process.argv) {
|
|
365
472
|
const rawArgv = Array.isArray(argvInput) ? argvInput : process.argv;
|
|
366
473
|
const argv = hideBin(rawArgv);
|
|
@@ -368,12 +475,15 @@ export function parseCliArgs(argvInput = process.argv) {
|
|
|
368
475
|
|
|
369
476
|
// When no separator, parse all args first to check for conductor-specific options
|
|
370
477
|
const conductorArgv = separatorIndex === -1 ? argv : argv.slice(0, separatorIndex);
|
|
371
|
-
const backendArgv = separatorIndex === -1 ? argv : argv.slice(separatorIndex + 1);
|
|
478
|
+
const backendArgv = separatorIndex === -1 ? stripConductorArgsFromArgv(argv) : argv.slice(separatorIndex + 1);
|
|
372
479
|
|
|
373
480
|
// Check for version/list-backends/help without separator
|
|
374
481
|
const versionWithoutSeparator = separatorIndex === -1 && (argv.includes("--version") || argv.includes("-v"));
|
|
375
482
|
const listBackendsWithoutSeparator = separatorIndex === -1 && argv.includes("--list-backends");
|
|
376
483
|
const helpWithoutSeparator = separatorIndex === -1 && (argv.includes("--help") || argv.includes("-h"));
|
|
484
|
+
if (hasLegacyFromFlags(conductorArgv)) {
|
|
485
|
+
throw new Error("--from and --from-provider were removed. Use --resume <session-id>.");
|
|
486
|
+
}
|
|
377
487
|
|
|
378
488
|
const configFileFromArgs = extractConfigFileFromArgv(argv);
|
|
379
489
|
const allowCliList = loadAllowCliList(configFileFromArgs);
|
|
@@ -406,15 +516,9 @@ export function parseCliArgs(argvInput = process.argv) {
|
|
|
406
516
|
type: "string",
|
|
407
517
|
describe: "Optional task title shown in the app task list",
|
|
408
518
|
})
|
|
409
|
-
.option("
|
|
410
|
-
alias: "f",
|
|
411
|
-
type: "string",
|
|
412
|
-
describe: "Resume from local history (optional session id; otherwise pick interactively)",
|
|
413
|
-
})
|
|
414
|
-
.option("from-provider", {
|
|
519
|
+
.option("resume", {
|
|
415
520
|
type: "string",
|
|
416
|
-
describe: "
|
|
417
|
-
choices: SUPPORTED_FROM_PROVIDERS,
|
|
521
|
+
describe: "Resume from a backend session ID",
|
|
418
522
|
})
|
|
419
523
|
.option("prefill", {
|
|
420
524
|
type: "string",
|
|
@@ -447,8 +551,7 @@ Options:
|
|
|
447
551
|
--config-file <path> Path to Conductor config file
|
|
448
552
|
--poll-interval <ms> Polling interval when waiting for Conductor messages
|
|
449
553
|
-t, --title <text> Optional task title shown in the app task list
|
|
450
|
-
|
|
451
|
-
--from-provider <p> Provider for --from picker (codex or claude)
|
|
554
|
+
--resume <id> Resume from an existing backend session
|
|
452
555
|
-v, --version Show Conductor CLI version and exit
|
|
453
556
|
-h, --help Show this help message
|
|
454
557
|
|
|
@@ -462,6 +565,7 @@ Examples:
|
|
|
462
565
|
${CLI_NAME} -- "fix the bug" # Use default backend
|
|
463
566
|
${CLI_NAME} --backend claude -- "fix the bug" # Use Claude CLI backend
|
|
464
567
|
${CLI_NAME} --backend copilot -- "fix the bug" # Use GitHub Copilot CLI backend
|
|
568
|
+
${CLI_NAME} --backend codex --resume <id> # Resume Codex session
|
|
465
569
|
${CLI_NAME} --list-backends # Show configured backends
|
|
466
570
|
${CLI_NAME} --config-file ~/.conductor/config.yaml -- "fix the bug"
|
|
467
571
|
|
|
@@ -491,11 +595,13 @@ Environment:
|
|
|
491
595
|
? Number(conductorArgs.pollInterval)
|
|
492
596
|
: DEFAULT_POLL_INTERVAL_MS;
|
|
493
597
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
}
|
|
498
|
-
|
|
598
|
+
const resumeRaw = conductorArgs.resume;
|
|
599
|
+
if (resumeRaw === true) {
|
|
600
|
+
throw new Error("--resume requires a session id");
|
|
601
|
+
}
|
|
602
|
+
const resumeSessionId = typeof resumeRaw === "string" ? resumeRaw.trim() : "";
|
|
603
|
+
if (resumeRaw !== undefined && !resumeSessionId) {
|
|
604
|
+
throw new Error("--resume requires a session id");
|
|
499
605
|
}
|
|
500
606
|
|
|
501
607
|
return {
|
|
@@ -504,22 +610,34 @@ Environment:
|
|
|
504
610
|
initialImages,
|
|
505
611
|
pollIntervalMs,
|
|
506
612
|
rawBackendArgs: backendArgv,
|
|
507
|
-
taskTitle:
|
|
613
|
+
taskTitle: typeof conductorArgs.title === "string" ? conductorArgs.title.trim() : "",
|
|
614
|
+
hasExplicitTaskTitle: typeof conductorArgs.title === "string" && Boolean(conductorArgs.title.trim()),
|
|
508
615
|
configFile: conductorArgs.configFile,
|
|
509
|
-
|
|
616
|
+
resumeSessionId,
|
|
510
617
|
showVersion: Boolean(conductorArgs.version) || versionWithoutSeparator,
|
|
511
618
|
listBackends: Boolean(conductorArgs.listBackends) || listBackendsWithoutSeparator,
|
|
512
619
|
};
|
|
513
620
|
}
|
|
514
621
|
|
|
515
|
-
function resolveTaskTitle(titleFlag) {
|
|
622
|
+
export function resolveTaskTitle(titleFlag, defaultPath = process.cwd()) {
|
|
516
623
|
if (typeof titleFlag === "string" && titleFlag.trim()) {
|
|
517
624
|
return titleFlag.trim();
|
|
518
625
|
}
|
|
519
|
-
const
|
|
626
|
+
const targetPath = typeof defaultPath === "string" ? defaultPath.trim() : "";
|
|
627
|
+
const cwdName = path.basename(targetPath || process.cwd());
|
|
520
628
|
return cwdName || "";
|
|
521
629
|
}
|
|
522
630
|
|
|
631
|
+
export function resolveRequestedTaskTitle({
|
|
632
|
+
cliTaskTitle,
|
|
633
|
+
hasExplicitTaskTitle,
|
|
634
|
+
envTaskTitle,
|
|
635
|
+
runtimeProjectPath,
|
|
636
|
+
}) {
|
|
637
|
+
const explicit = hasExplicitTaskTitle ? cliTaskTitle : envTaskTitle;
|
|
638
|
+
return resolveTaskTitle(explicit, runtimeProjectPath);
|
|
639
|
+
}
|
|
640
|
+
|
|
523
641
|
function normalizeArray(value) {
|
|
524
642
|
if (!value) {
|
|
525
643
|
return [];
|
|
@@ -642,6 +760,7 @@ function deriveTaskTitle(prompt, explicit, backend = "codex") {
|
|
|
642
760
|
|
|
643
761
|
const BACKEND_PROFILE_MAP = {
|
|
644
762
|
codex: "codex",
|
|
763
|
+
code: "codex",
|
|
645
764
|
claude: "claude-code",
|
|
646
765
|
"claude-code": "claude-code",
|
|
647
766
|
copilot: "copilot",
|
|
@@ -664,6 +783,228 @@ function parseCommandParts(commandLine) {
|
|
|
664
783
|
};
|
|
665
784
|
}
|
|
666
785
|
|
|
786
|
+
export function buildResumeArgsForBackend(backend, sessionId) {
|
|
787
|
+
const resumeSessionId = typeof sessionId === "string" ? sessionId.trim() : "";
|
|
788
|
+
if (!resumeSessionId) {
|
|
789
|
+
return [];
|
|
790
|
+
}
|
|
791
|
+
const normalizedBackend = String(backend || "").trim().toLowerCase();
|
|
792
|
+
if (normalizedBackend === "codex" || normalizedBackend === "code") {
|
|
793
|
+
return ["resume", resumeSessionId];
|
|
794
|
+
}
|
|
795
|
+
if (normalizedBackend === "claude" || normalizedBackend === "claude-code") {
|
|
796
|
+
return ["--resume", resumeSessionId];
|
|
797
|
+
}
|
|
798
|
+
if (normalizedBackend === "copilot") {
|
|
799
|
+
return [`--resume=${resumeSessionId}`];
|
|
800
|
+
}
|
|
801
|
+
throw new Error(`--resume is not supported for backend "${backend}"`);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
export function resumeProviderForBackend(backend) {
|
|
805
|
+
const normalizedBackend = String(backend || "").trim().toLowerCase();
|
|
806
|
+
if (normalizedBackend === "codex" || normalizedBackend === "code") {
|
|
807
|
+
return "codex";
|
|
808
|
+
}
|
|
809
|
+
if (normalizedBackend === "claude" || normalizedBackend === "claude-code") {
|
|
810
|
+
return "claude";
|
|
811
|
+
}
|
|
812
|
+
if (normalizedBackend === "copilot") {
|
|
813
|
+
return "copilot";
|
|
814
|
+
}
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
export async function resolveSessionRunDirectory(sessionPath) {
|
|
819
|
+
const normalizedPath = typeof sessionPath === "string" ? sessionPath.trim() : "";
|
|
820
|
+
if (!normalizedPath) {
|
|
821
|
+
throw new Error("Invalid session path");
|
|
822
|
+
}
|
|
823
|
+
let stats;
|
|
824
|
+
try {
|
|
825
|
+
stats = await fs.promises.stat(normalizedPath);
|
|
826
|
+
} catch {
|
|
827
|
+
throw new Error(`Session path does not exist: ${normalizedPath}`);
|
|
828
|
+
}
|
|
829
|
+
return stats.isDirectory() ? normalizedPath : path.dirname(normalizedPath);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
async function isExistingDirectory(targetPath) {
|
|
833
|
+
const normalizedPath = typeof targetPath === "string" ? targetPath.trim() : "";
|
|
834
|
+
if (!normalizedPath) {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
try {
|
|
838
|
+
const stats = await fs.promises.stat(normalizedPath);
|
|
839
|
+
return stats.isDirectory();
|
|
840
|
+
} catch {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async function extractCodexResumeCwd(sessionPath) {
|
|
846
|
+
if (!sessionPath.endsWith(".jsonl")) {
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
const rl = readline.createInterface({
|
|
850
|
+
input: fs.createReadStream(sessionPath),
|
|
851
|
+
crlfDelay: Infinity,
|
|
852
|
+
});
|
|
853
|
+
for await (const line of rl) {
|
|
854
|
+
const trimmed = line.trim();
|
|
855
|
+
if (!trimmed) {
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
858
|
+
let entry;
|
|
859
|
+
try {
|
|
860
|
+
entry = JSON.parse(trimmed);
|
|
861
|
+
} catch {
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
const maybeCwd = entry?.type === "session_meta" ? entry?.payload?.cwd : null;
|
|
865
|
+
if (typeof maybeCwd === "string" && maybeCwd.trim()) {
|
|
866
|
+
return maybeCwd.trim();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
async function extractClaudeResumeCwd(sessionPath, sessionId) {
|
|
873
|
+
if (!sessionPath.endsWith(".jsonl")) {
|
|
874
|
+
return null;
|
|
875
|
+
}
|
|
876
|
+
const rl = readline.createInterface({
|
|
877
|
+
input: fs.createReadStream(sessionPath),
|
|
878
|
+
crlfDelay: Infinity,
|
|
879
|
+
});
|
|
880
|
+
for await (const line of rl) {
|
|
881
|
+
const trimmed = line.trim();
|
|
882
|
+
if (!trimmed) {
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
let entry;
|
|
886
|
+
try {
|
|
887
|
+
entry = JSON.parse(trimmed);
|
|
888
|
+
} catch {
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
const idMatches = String(entry?.sessionId || "").trim() === sessionId;
|
|
892
|
+
const maybeCwd = entry?.cwd;
|
|
893
|
+
if (idMatches && typeof maybeCwd === "string" && maybeCwd.trim()) {
|
|
894
|
+
return maybeCwd.trim();
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
async function extractCopilotResumeCwd(sessionPath) {
|
|
901
|
+
let stats;
|
|
902
|
+
try {
|
|
903
|
+
stats = await fs.promises.stat(sessionPath);
|
|
904
|
+
} catch {
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (stats.isDirectory()) {
|
|
909
|
+
const workspaceYamlPath = path.join(sessionPath, "workspace.yaml");
|
|
910
|
+
try {
|
|
911
|
+
const yamlContent = await fs.promises.readFile(workspaceYamlPath, "utf8");
|
|
912
|
+
const parsed = yaml.load(yamlContent);
|
|
913
|
+
const maybeCwd = parsed && typeof parsed === "object" ? parsed.cwd : null;
|
|
914
|
+
if (typeof maybeCwd === "string" && maybeCwd.trim()) {
|
|
915
|
+
return maybeCwd.trim();
|
|
916
|
+
}
|
|
917
|
+
} catch {
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (!sessionPath.endsWith(".jsonl")) {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const rl = readline.createInterface({
|
|
928
|
+
input: fs.createReadStream(sessionPath),
|
|
929
|
+
crlfDelay: Infinity,
|
|
930
|
+
});
|
|
931
|
+
for await (const line of rl) {
|
|
932
|
+
const trimmed = line.trim();
|
|
933
|
+
if (!trimmed) {
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
let entry;
|
|
937
|
+
try {
|
|
938
|
+
entry = JSON.parse(trimmed);
|
|
939
|
+
} catch {
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
const maybeCwd = entry?.data?.context?.cwd || entry?.data?.cwd;
|
|
943
|
+
if (typeof maybeCwd === "string" && maybeCwd.trim()) {
|
|
944
|
+
return maybeCwd.trim();
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
async function extractResumeCwdFromSession(provider, sessionPath, sessionId) {
|
|
951
|
+
if (provider === "codex") {
|
|
952
|
+
return extractCodexResumeCwd(sessionPath);
|
|
953
|
+
}
|
|
954
|
+
if (provider === "claude") {
|
|
955
|
+
return extractClaudeResumeCwd(sessionPath, sessionId);
|
|
956
|
+
}
|
|
957
|
+
if (provider === "copilot") {
|
|
958
|
+
return extractCopilotResumeCwd(sessionPath);
|
|
959
|
+
}
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
export async function resolveResumeContext(backend, sessionId, options = {}) {
|
|
964
|
+
const normalizedSessionId = typeof sessionId === "string" ? sessionId.trim() : "";
|
|
965
|
+
if (!normalizedSessionId) {
|
|
966
|
+
throw new Error("--resume requires a session id");
|
|
967
|
+
}
|
|
968
|
+
const provider = resumeProviderForBackend(backend);
|
|
969
|
+
if (!provider) {
|
|
970
|
+
throw new Error(`--resume is not supported for backend "${backend}"`);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const sessionPath = await findSessionPath(provider, normalizedSessionId, options);
|
|
974
|
+
if (!sessionPath) {
|
|
975
|
+
throw new Error(`Invalid --resume session id for ${provider}: ${normalizedSessionId}`);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const cwdFromSession = await extractResumeCwdFromSession(provider, sessionPath, normalizedSessionId);
|
|
979
|
+
const fallbackCwd = await resolveSessionRunDirectory(sessionPath);
|
|
980
|
+
const cwd = cwdFromSession || fallbackCwd;
|
|
981
|
+
if (!(await isExistingDirectory(cwd))) {
|
|
982
|
+
throw new Error(`Resume workspace path does not exist: ${cwd}`);
|
|
983
|
+
}
|
|
984
|
+
return {
|
|
985
|
+
provider,
|
|
986
|
+
sessionId: normalizedSessionId,
|
|
987
|
+
sessionPath,
|
|
988
|
+
cwd,
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
export async function applyWorkingDirectory(targetPath) {
|
|
993
|
+
const normalizedPath = typeof targetPath === "string" ? targetPath.trim() : "";
|
|
994
|
+
if (!normalizedPath) {
|
|
995
|
+
throw new Error("Cannot switch working directory: empty path");
|
|
996
|
+
}
|
|
997
|
+
if (!(await isExistingDirectory(normalizedPath))) {
|
|
998
|
+
throw new Error(`Cannot switch working directory: ${normalizedPath}`);
|
|
999
|
+
}
|
|
1000
|
+
try {
|
|
1001
|
+
process.chdir(normalizedPath);
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
throw new Error(`Cannot switch working directory to ${normalizedPath}: ${error?.message || error}`);
|
|
1004
|
+
}
|
|
1005
|
+
return process.cwd();
|
|
1006
|
+
}
|
|
1007
|
+
|
|
667
1008
|
function truncateText(value, maxLen = 240) {
|
|
668
1009
|
if (!value) return "";
|
|
669
1010
|
const text = String(value).trim();
|
|
@@ -677,6 +1018,10 @@ function isTruthyEnv(value) {
|
|
|
677
1018
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
678
1019
|
}
|
|
679
1020
|
|
|
1021
|
+
function isSessionClosedError(error) {
|
|
1022
|
+
return Boolean(error && typeof error === "object" && error.reason === "session_closed");
|
|
1023
|
+
}
|
|
1024
|
+
|
|
680
1025
|
function sanitizeForLog(value, maxLen = 180) {
|
|
681
1026
|
if (!value) return "";
|
|
682
1027
|
return truncateText(String(value).replace(/\s+/g, " ").trim(), maxLen);
|
|
@@ -692,7 +1037,12 @@ class TuiDriverSession {
|
|
|
692
1037
|
constructor(backend, options = {}) {
|
|
693
1038
|
this.backend = backend;
|
|
694
1039
|
this.options = options;
|
|
695
|
-
this.
|
|
1040
|
+
this.cwd =
|
|
1041
|
+
typeof options.cwd === "string" && options.cwd.trim()
|
|
1042
|
+
? options.cwd.trim()
|
|
1043
|
+
: INITIAL_CLI_PROJECT_PATH;
|
|
1044
|
+
const resumeSessionId = typeof options.resumeSessionId === "string" ? options.resumeSessionId.trim() : "";
|
|
1045
|
+
this.sessionId = resumeSessionId || `${backend}-${Date.now()}`;
|
|
696
1046
|
this.history = Array.isArray(options.initialHistory) ? [...options.initialHistory] : [];
|
|
697
1047
|
this.pendingHistorySeed = this.history.length > 0;
|
|
698
1048
|
|
|
@@ -702,8 +1052,9 @@ class TuiDriverSession {
|
|
|
702
1052
|
if (!command) {
|
|
703
1053
|
throw new Error(`Invalid command for backend "${backend}"`);
|
|
704
1054
|
}
|
|
1055
|
+
const resumeArgs = buildResumeArgsForBackend(backend, resumeSessionId);
|
|
705
1056
|
this.command = command;
|
|
706
|
-
this.args = args;
|
|
1057
|
+
this.args = [...args, ...resumeArgs];
|
|
707
1058
|
this.tuiDebug = isTruthyEnv(process.env.CONDUCTOR_TUI_DEBUG);
|
|
708
1059
|
this.tuiTrace = this.tuiDebug || isTruthyEnv(process.env.CONDUCTOR_TUI_TRACE);
|
|
709
1060
|
this.tuiTraceLines = Number.isFinite(Number.parseInt(process.env.CONDUCTOR_TUI_TRACE_LINES || "", 10))
|
|
@@ -712,6 +1063,9 @@ class TuiDriverSession {
|
|
|
712
1063
|
this.lastSignalSignature = "";
|
|
713
1064
|
this.lastPollSignature = "";
|
|
714
1065
|
this.lastSnapshotHash = "";
|
|
1066
|
+
this.closeRequested = false;
|
|
1067
|
+
this.closed = false;
|
|
1068
|
+
this.closeWaiters = new Set();
|
|
715
1069
|
|
|
716
1070
|
const profileName = profileNameForBackend(backend);
|
|
717
1071
|
if (!profileName) {
|
|
@@ -732,7 +1086,7 @@ class TuiDriverSession {
|
|
|
732
1086
|
log(`Using proxy: ${proxyEnv.http_proxy || proxyEnv.https_proxy || proxyEnv.all_proxy}`);
|
|
733
1087
|
}
|
|
734
1088
|
|
|
735
|
-
log(`Using TUI command for ${backend}: ${
|
|
1089
|
+
log(`Using TUI command for ${backend}: ${[this.command, ...this.args].join(" ")} (cwd: ${this.cwd})`);
|
|
736
1090
|
if (this.tuiTrace) {
|
|
737
1091
|
log(
|
|
738
1092
|
`[${this.backend}] [tui-trace] profile=${baseProfile.name} command=${this.command} args=${JSON.stringify(this.args)}`,
|
|
@@ -753,6 +1107,7 @@ class TuiDriverSession {
|
|
|
753
1107
|
...cliEnv,
|
|
754
1108
|
},
|
|
755
1109
|
},
|
|
1110
|
+
cwd: this.cwd,
|
|
756
1111
|
debug: this.tuiDebug,
|
|
757
1112
|
onSnapshot: this.tuiTrace
|
|
758
1113
|
? (snapshot, state) => {
|
|
@@ -784,7 +1139,57 @@ class TuiDriverSession {
|
|
|
784
1139
|
return { model: this.backend };
|
|
785
1140
|
}
|
|
786
1141
|
|
|
1142
|
+
createSessionClosedError() {
|
|
1143
|
+
const error = new Error("TUI session closed");
|
|
1144
|
+
error.reason = "session_closed";
|
|
1145
|
+
return error;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
createCloseGuard() {
|
|
1149
|
+
if (this.closeRequested) {
|
|
1150
|
+
return {
|
|
1151
|
+
promise: Promise.reject(this.createSessionClosedError()),
|
|
1152
|
+
cleanup: () => {},
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
let waiter = null;
|
|
1156
|
+
const promise = new Promise((_, reject) => {
|
|
1157
|
+
waiter = () => {
|
|
1158
|
+
reject(this.createSessionClosedError());
|
|
1159
|
+
};
|
|
1160
|
+
this.closeWaiters.add(waiter);
|
|
1161
|
+
});
|
|
1162
|
+
return {
|
|
1163
|
+
promise,
|
|
1164
|
+
cleanup: () => {
|
|
1165
|
+
if (waiter) {
|
|
1166
|
+
this.closeWaiters.delete(waiter);
|
|
1167
|
+
}
|
|
1168
|
+
},
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
flushCloseWaiters() {
|
|
1173
|
+
if (!this.closeWaiters || this.closeWaiters.size === 0) {
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
for (const waiter of this.closeWaiters) {
|
|
1177
|
+
try {
|
|
1178
|
+
waiter();
|
|
1179
|
+
} catch {
|
|
1180
|
+
// best effort
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
this.closeWaiters.clear();
|
|
1184
|
+
}
|
|
1185
|
+
|
|
787
1186
|
async close() {
|
|
1187
|
+
if (this.closed) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
this.closed = true;
|
|
1191
|
+
this.closeRequested = true;
|
|
1192
|
+
this.flushCloseWaiters();
|
|
788
1193
|
if (this.driver) {
|
|
789
1194
|
this.driver.kill();
|
|
790
1195
|
}
|
|
@@ -889,6 +1294,10 @@ class TuiDriverSession {
|
|
|
889
1294
|
}
|
|
890
1295
|
|
|
891
1296
|
async runTurn(promptText, { useInitialImages = false, onProgress } = {}) {
|
|
1297
|
+
if (this.closeRequested) {
|
|
1298
|
+
throw this.createSessionClosedError();
|
|
1299
|
+
}
|
|
1300
|
+
|
|
892
1301
|
const effectivePrompt = this.buildPrompt(promptText, { useInitialImages });
|
|
893
1302
|
if (!effectivePrompt) {
|
|
894
1303
|
return {
|
|
@@ -945,8 +1354,19 @@ class TuiDriverSession {
|
|
|
945
1354
|
signalTimer.unref();
|
|
946
1355
|
}
|
|
947
1356
|
|
|
1357
|
+
const previousCwd = process.cwd();
|
|
1358
|
+
const shouldSwitchCwd = this.cwd && this.cwd !== previousCwd;
|
|
1359
|
+
if (shouldSwitchCwd) {
|
|
1360
|
+
try {
|
|
1361
|
+
process.chdir(this.cwd);
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
throw new Error(`Failed to switch backend cwd to ${this.cwd}: ${error?.message || error}`);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
const closeGuard = this.createCloseGuard();
|
|
1367
|
+
|
|
948
1368
|
try {
|
|
949
|
-
const result = await this.driver.ask(effectivePrompt);
|
|
1369
|
+
const result = await Promise.race([this.driver.ask(effectivePrompt), closeGuard.promise]);
|
|
950
1370
|
const answer = String(result.answer || result.replyText || "").trim();
|
|
951
1371
|
this.trace(
|
|
952
1372
|
`runTurn finished success=${Boolean(result.success)} elapsedMs=${result.elapsedMs} answerLen=${answer.length} state=${this.driver.state}`,
|
|
@@ -990,6 +1410,10 @@ class TuiDriverSession {
|
|
|
990
1410
|
} catch (error) {
|
|
991
1411
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
992
1412
|
const errorReason = error?.reason || "unknown";
|
|
1413
|
+
if (errorReason === "session_closed") {
|
|
1414
|
+
this.trace("runTurn interrupted because backend session is closing");
|
|
1415
|
+
throw error instanceof Error ? error : new Error(errorMessage);
|
|
1416
|
+
}
|
|
993
1417
|
|
|
994
1418
|
// 特殊处理登录和权限错误
|
|
995
1419
|
if (errorReason === "login_required") {
|
|
@@ -1024,21 +1448,34 @@ class TuiDriverSession {
|
|
|
1024
1448
|
log(`[${this.backend}] Error: ${errorMessage}`);
|
|
1025
1449
|
}
|
|
1026
1450
|
|
|
1027
|
-
|
|
1451
|
+
let latestSignals = {};
|
|
1452
|
+
try {
|
|
1453
|
+
latestSignals = this.driver.getSignals();
|
|
1454
|
+
} catch {
|
|
1455
|
+
// driver may already be disposed while closing
|
|
1456
|
+
}
|
|
1028
1457
|
const summary = this.formatSignalSummary(latestSignals);
|
|
1029
1458
|
this.trace(
|
|
1030
1459
|
`runTurn exception state=${this.driver.state} error="${sanitizeForLog(errorMessage, 220)}" status="${summary.status || ""}" done="${summary.done || ""}" preview="${summary.replyPreview || ""}"`,
|
|
1031
1460
|
);
|
|
1032
1461
|
throw error instanceof Error ? error : new Error(errorMessage);
|
|
1033
1462
|
} finally {
|
|
1463
|
+
if (shouldSwitchCwd) {
|
|
1464
|
+
try {
|
|
1465
|
+
process.chdir(previousCwd);
|
|
1466
|
+
} catch (error) {
|
|
1467
|
+
log(`Failed to restore cwd to ${previousCwd}: ${error?.message || error}`);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1034
1470
|
clearInterval(signalTimer);
|
|
1035
1471
|
this.driver.off("stateChange", handleStateChange);
|
|
1472
|
+
closeGuard.cleanup();
|
|
1036
1473
|
this.trace(`runTurn cleanup state=${this.driver.state}`);
|
|
1037
1474
|
}
|
|
1038
1475
|
}
|
|
1039
1476
|
}
|
|
1040
1477
|
|
|
1041
|
-
class BridgeRunner {
|
|
1478
|
+
export class BridgeRunner {
|
|
1042
1479
|
constructor({
|
|
1043
1480
|
backendSession,
|
|
1044
1481
|
conductor,
|
|
@@ -1048,6 +1485,7 @@ class BridgeRunner {
|
|
|
1048
1485
|
includeInitialImages,
|
|
1049
1486
|
cliArgs,
|
|
1050
1487
|
backendName,
|
|
1488
|
+
resumeMode,
|
|
1051
1489
|
}) {
|
|
1052
1490
|
this.backendSession = backendSession;
|
|
1053
1491
|
this.conductor = conductor;
|
|
@@ -1057,25 +1495,55 @@ class BridgeRunner {
|
|
|
1057
1495
|
this.includeInitialImages = includeInitialImages;
|
|
1058
1496
|
this.cliArgs = cliArgs;
|
|
1059
1497
|
this.backendName = backendName || "codex";
|
|
1498
|
+
this.resumeMode = Boolean(resumeMode);
|
|
1499
|
+
this.isCopilotBackend = String(this.backendName).toLowerCase() === "copilot";
|
|
1500
|
+
this.copilotDebug =
|
|
1501
|
+
this.isCopilotBackend &&
|
|
1502
|
+
(isTruthyEnv(process.env.CONDUCTOR_COPILOT_DEBUG) || isTruthyEnv(process.env.CONDUCTOR_DEBUG));
|
|
1060
1503
|
this.stopped = false;
|
|
1061
1504
|
this.runningTurn = false;
|
|
1062
1505
|
this.processedMessageIds = new Set();
|
|
1063
1506
|
this.lastRuntimeStatusSignature = null;
|
|
1064
1507
|
this.lastRuntimeStatusPayload = null;
|
|
1065
1508
|
this.needsReconnectRecovery = false;
|
|
1509
|
+
this.remoteStopInfo = null;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
copilotLog(message) {
|
|
1513
|
+
if (!this.copilotDebug) {
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
log(`[copilot-debug] task=${this.taskId} ${message}`);
|
|
1066
1517
|
}
|
|
1067
1518
|
|
|
1068
1519
|
async start(abortSignal) {
|
|
1069
1520
|
abortSignal?.addEventListener("abort", () => {
|
|
1070
1521
|
this.stopped = true;
|
|
1071
1522
|
});
|
|
1523
|
+
if (this.stopped) {
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1072
1526
|
|
|
1073
1527
|
if (this.initialPrompt) {
|
|
1528
|
+
this.copilotLog("processing initial prompt");
|
|
1074
1529
|
await this.handleSyntheticMessage(this.initialPrompt, {
|
|
1075
1530
|
includeImages: this.includeInitialImages,
|
|
1076
1531
|
});
|
|
1077
1532
|
}
|
|
1533
|
+
if (this.stopped) {
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
if (this.resumeMode) {
|
|
1537
|
+
await this.drainBufferedMessagesForResume();
|
|
1538
|
+
}
|
|
1539
|
+
if (this.stopped) {
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
this.copilotLog("running startup backfill");
|
|
1078
1543
|
await this.backfillPendingUserMessages();
|
|
1544
|
+
if (this.stopped) {
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1079
1547
|
|
|
1080
1548
|
while (!this.stopped) {
|
|
1081
1549
|
if (this.needsReconnectRecovery && !this.runningTurn) {
|
|
@@ -1085,10 +1553,13 @@ class BridgeRunner {
|
|
|
1085
1553
|
try {
|
|
1086
1554
|
processed = await this.processIncomingBatch();
|
|
1087
1555
|
} catch (error) {
|
|
1556
|
+
if (this.stopped && (this.remoteStopInfo || isSessionClosedError(error))) {
|
|
1557
|
+
break;
|
|
1558
|
+
}
|
|
1088
1559
|
log(`Error while processing messages: ${error.message}`);
|
|
1089
1560
|
await this.reportError(`处理任务失败: ${error.message}`);
|
|
1090
1561
|
}
|
|
1091
|
-
if (!processed) {
|
|
1562
|
+
if (!processed && !this.stopped) {
|
|
1092
1563
|
await delay(this.pollIntervalMs);
|
|
1093
1564
|
}
|
|
1094
1565
|
}
|
|
@@ -1098,6 +1569,42 @@ class BridgeRunner {
|
|
|
1098
1569
|
this.needsReconnectRecovery = true;
|
|
1099
1570
|
}
|
|
1100
1571
|
|
|
1572
|
+
getRemoteStopSummary() {
|
|
1573
|
+
if (!this.remoteStopInfo) {
|
|
1574
|
+
return null;
|
|
1575
|
+
}
|
|
1576
|
+
const reason = this.remoteStopInfo.reason || "killed by app";
|
|
1577
|
+
return `task stopped by app: ${reason}`;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
async requestStopFromRemote(event = {}) {
|
|
1581
|
+
const taskId = typeof event.taskId === "string" ? event.taskId.trim() : "";
|
|
1582
|
+
if (taskId && taskId !== this.taskId) {
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
const requestId = typeof event.requestId === "string" ? event.requestId.trim() : "";
|
|
1586
|
+
const reason = typeof event.reason === "string" ? event.reason.trim() : "";
|
|
1587
|
+
if (!this.remoteStopInfo) {
|
|
1588
|
+
this.remoteStopInfo = {
|
|
1589
|
+
requestId: requestId || null,
|
|
1590
|
+
reason: reason || null,
|
|
1591
|
+
};
|
|
1592
|
+
log(
|
|
1593
|
+
`Received stop_task for ${this.taskId}${
|
|
1594
|
+
reason ? ` (${reason})` : ""
|
|
1595
|
+
}; stopping conductor fire`,
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
this.stopped = true;
|
|
1599
|
+
if (typeof this.backendSession?.close === "function") {
|
|
1600
|
+
try {
|
|
1601
|
+
await this.backendSession.close();
|
|
1602
|
+
} catch (error) {
|
|
1603
|
+
log(`Failed to stop backend session for ${this.taskId}: ${error?.message || error}`);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1101
1608
|
async recoverAfterReconnect() {
|
|
1102
1609
|
if (!this.needsReconnectRecovery) {
|
|
1103
1610
|
return;
|
|
@@ -1114,23 +1621,68 @@ class BridgeRunner {
|
|
|
1114
1621
|
await this.replayLastRuntimeStatus();
|
|
1115
1622
|
}
|
|
1116
1623
|
|
|
1624
|
+
async drainBufferedMessagesForResume() {
|
|
1625
|
+
let drainedCount = 0;
|
|
1626
|
+
let drainedBatches = 0;
|
|
1627
|
+
|
|
1628
|
+
while (!this.stopped) {
|
|
1629
|
+
const result = await this.conductor.receiveMessages(this.taskId, 50);
|
|
1630
|
+
const messages = Array.isArray(result?.messages) ? result.messages : [];
|
|
1631
|
+
if (messages.length === 0) {
|
|
1632
|
+
break;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
drainedBatches += 1;
|
|
1636
|
+
for (const message of messages) {
|
|
1637
|
+
const replyTo = message?.message_id ? String(message.message_id) : "";
|
|
1638
|
+
if (replyTo) {
|
|
1639
|
+
this.processedMessageIds.add(replyTo);
|
|
1640
|
+
}
|
|
1641
|
+
drainedCount += 1;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
const ackToken = result.next_ack_token || result.nextAckToken;
|
|
1645
|
+
if (ackToken) {
|
|
1646
|
+
await this.conductor.ackMessages(this.taskId, ackToken);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
if (drainedCount > 0) {
|
|
1651
|
+
log(
|
|
1652
|
+
`Resume startup skipped ${drainedCount} buffered message(s) in ${drainedBatches} batch(es) for task ${this.taskId}`,
|
|
1653
|
+
);
|
|
1654
|
+
} else {
|
|
1655
|
+
this.copilotLog("resume startup found no buffered messages to skip");
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1117
1659
|
async processIncomingBatch() {
|
|
1118
1660
|
const result = await this.conductor.receiveMessages(this.taskId, 20);
|
|
1119
1661
|
const messages = Array.isArray(result?.messages) ? result.messages : [];
|
|
1120
1662
|
if (messages.length === 0) {
|
|
1121
1663
|
return false;
|
|
1122
1664
|
}
|
|
1665
|
+
const ackToken = result.next_ack_token || result.nextAckToken;
|
|
1666
|
+
this.copilotLog(
|
|
1667
|
+
`received batch size=${messages.length} hasMore=${Boolean(result?.has_more)} ackToken=${ackToken ? "yes" : "no"} roles=${messages
|
|
1668
|
+
.map((item) => String(item?.role || "unknown").toLowerCase())
|
|
1669
|
+
.join(",")}`,
|
|
1670
|
+
);
|
|
1123
1671
|
|
|
1124
1672
|
for (const message of messages) {
|
|
1673
|
+
if (this.stopped) {
|
|
1674
|
+
break;
|
|
1675
|
+
}
|
|
1125
1676
|
if (!this.shouldRespond(message)) {
|
|
1677
|
+
this.copilotLog(`skip message role=${String(message?.role || "unknown").toLowerCase()}`);
|
|
1126
1678
|
continue;
|
|
1127
1679
|
}
|
|
1128
1680
|
await this.respondToMessage(message);
|
|
1129
1681
|
}
|
|
1130
1682
|
|
|
1131
|
-
const ackToken = result.next_ack_token || result.nextAckToken;
|
|
1132
1683
|
if (ackToken) {
|
|
1133
1684
|
await this.conductor.ackMessages(this.taskId, ackToken);
|
|
1685
|
+
this.copilotLog(`acked batch ackToken=${truncateText(String(ackToken), 40)}`);
|
|
1134
1686
|
}
|
|
1135
1687
|
const hasMore = Boolean(result?.has_more);
|
|
1136
1688
|
return hasMore;
|
|
@@ -1148,6 +1700,7 @@ class BridgeRunner {
|
|
|
1148
1700
|
const backendUrl = process.env.CONDUCTOR_BACKEND_URL;
|
|
1149
1701
|
const token = process.env.CONDUCTOR_AGENT_TOKEN;
|
|
1150
1702
|
if (!backendUrl || !token) {
|
|
1703
|
+
this.copilotLog("skip backfill: missing backend url or token");
|
|
1151
1704
|
return;
|
|
1152
1705
|
}
|
|
1153
1706
|
|
|
@@ -1163,10 +1716,12 @@ class BridgeRunner {
|
|
|
1163
1716
|
},
|
|
1164
1717
|
);
|
|
1165
1718
|
if (!response.ok) {
|
|
1719
|
+
this.copilotLog(`backfill request failed status=${response.status}`);
|
|
1166
1720
|
return;
|
|
1167
1721
|
}
|
|
1168
1722
|
const history = await response.json();
|
|
1169
1723
|
if (!Array.isArray(history) || history.length === 0) {
|
|
1724
|
+
this.copilotLog("backfill: no history messages");
|
|
1170
1725
|
return;
|
|
1171
1726
|
}
|
|
1172
1727
|
|
|
@@ -1178,12 +1733,41 @@ class BridgeRunner {
|
|
|
1178
1733
|
}
|
|
1179
1734
|
}
|
|
1180
1735
|
|
|
1736
|
+
const historyUserIds = history
|
|
1737
|
+
.filter((item) => String(item?.role || "").toLowerCase() === "user")
|
|
1738
|
+
.map((item) => (item?.id ? String(item.id) : ""))
|
|
1739
|
+
.filter(Boolean);
|
|
1740
|
+
|
|
1741
|
+
const handledUserIds = history
|
|
1742
|
+
.slice(0, lastSdkIndex + 1)
|
|
1743
|
+
.filter((item) => String(item?.role || "").toLowerCase() === "user")
|
|
1744
|
+
.map((item) => (item?.id ? String(item.id) : ""))
|
|
1745
|
+
.filter(Boolean);
|
|
1746
|
+
|
|
1747
|
+
for (const handledId of handledUserIds) {
|
|
1748
|
+
this.processedMessageIds.add(handledId);
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1181
1751
|
const pendingUserMessages = history
|
|
1182
1752
|
.slice(lastSdkIndex + 1)
|
|
1183
1753
|
.filter((item) => String(item?.role || "").toLowerCase() === "user")
|
|
1184
1754
|
.filter((item) => typeof item?.content === "string" && item.content.trim());
|
|
1185
1755
|
|
|
1756
|
+
if (this.resumeMode) {
|
|
1757
|
+
for (const historyId of historyUserIds) {
|
|
1758
|
+
this.processedMessageIds.add(historyId);
|
|
1759
|
+
}
|
|
1760
|
+
this.copilotLog(
|
|
1761
|
+
`resume mode: seeded processed ids=${historyUserIds.length}, skip startup backfill replay`,
|
|
1762
|
+
);
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
this.copilotLog(
|
|
1766
|
+
`backfill loaded history=${history.length} handledUsers=${handledUserIds.length} pendingUsers=${pendingUserMessages.length} lastSdkIndex=${lastSdkIndex}`,
|
|
1767
|
+
);
|
|
1768
|
+
|
|
1186
1769
|
for (const pending of pendingUserMessages) {
|
|
1770
|
+
this.copilotLog(`backfill replay message id=${pending.id ? String(pending.id) : "unknown"}`);
|
|
1187
1771
|
await this.respondToMessage({
|
|
1188
1772
|
message_id: pending.id ? String(pending.id) : undefined,
|
|
1189
1773
|
role: "user",
|
|
@@ -1238,6 +1822,11 @@ class BridgeRunner {
|
|
|
1238
1822
|
this.lastRuntimeStatusPayload = {
|
|
1239
1823
|
...runtime,
|
|
1240
1824
|
};
|
|
1825
|
+
this.copilotLog(
|
|
1826
|
+
`runtime replyTo=${replyTo || "latest"} state=${runtime.state || ""} phase=${runtime.phase || ""} inProgress=${Boolean(
|
|
1827
|
+
runtime.reply_in_progress,
|
|
1828
|
+
)} status="${sanitizeForLog(runtime.status_line || "", 120)}" done="${sanitizeForLog(runtime.status_done_line || "", 120)}" preview="${sanitizeForLog(runtime.reply_preview || "", 120)}"`,
|
|
1829
|
+
);
|
|
1241
1830
|
|
|
1242
1831
|
try {
|
|
1243
1832
|
await this.conductor.sendRuntimeStatus(this.taskId, {
|
|
@@ -1266,14 +1855,32 @@ class BridgeRunner {
|
|
|
1266
1855
|
async respondToMessage(message) {
|
|
1267
1856
|
const content = String(message.content || "").trim();
|
|
1268
1857
|
if (!content) {
|
|
1858
|
+
this.copilotLog(`skip empty message replyTo=${message?.message_id || "latest"}`);
|
|
1269
1859
|
return;
|
|
1270
1860
|
}
|
|
1271
1861
|
const replyTo = message.message_id;
|
|
1272
1862
|
if (replyTo && this.processedMessageIds.has(replyTo)) {
|
|
1863
|
+
this.copilotLog(`skip duplicated message replyTo=${replyTo}`);
|
|
1273
1864
|
return;
|
|
1274
1865
|
}
|
|
1275
1866
|
this.lastRuntimeStatusSignature = null;
|
|
1276
1867
|
this.runningTurn = true;
|
|
1868
|
+
const turnStartedAt = Date.now();
|
|
1869
|
+
let turnWatchdog = null;
|
|
1870
|
+
if (this.isCopilotBackend) {
|
|
1871
|
+
turnWatchdog = setInterval(() => {
|
|
1872
|
+
const runtime = this.lastRuntimeStatusPayload || {};
|
|
1873
|
+
this.copilotLog(
|
|
1874
|
+
`turn waiting replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} state=${runtime.state || ""} phase=${runtime.phase || ""} status="${sanitizeForLog(runtime.status_line || runtime.status_done_line || "", 120)}"`,
|
|
1875
|
+
);
|
|
1876
|
+
}, 30000);
|
|
1877
|
+
if (typeof turnWatchdog.unref === "function") {
|
|
1878
|
+
turnWatchdog.unref();
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
this.copilotLog(
|
|
1882
|
+
`turn start replyTo=${replyTo || "latest"} role=${String(message.role || "").toLowerCase()} contentLen=${content.length} preview="${sanitizeForLog(content, 120)}"`,
|
|
1883
|
+
);
|
|
1277
1884
|
log(`Processing message ${replyTo} (${message.role})`);
|
|
1278
1885
|
try {
|
|
1279
1886
|
await this.reportRuntimeStatus(
|
|
@@ -1291,6 +1898,11 @@ class BridgeRunner {
|
|
|
1291
1898
|
void this.reportRuntimeStatus(payload, replyTo);
|
|
1292
1899
|
},
|
|
1293
1900
|
});
|
|
1901
|
+
this.copilotLog(
|
|
1902
|
+
`runTurn completed replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} answerLen=${String(
|
|
1903
|
+
result.text || "",
|
|
1904
|
+
).trim().length} items=${Array.isArray(result.items) ? result.items.length : 0}`,
|
|
1905
|
+
);
|
|
1294
1906
|
|
|
1295
1907
|
await this.reportRuntimeStatus(
|
|
1296
1908
|
{
|
|
@@ -1320,8 +1932,18 @@ class BridgeRunner {
|
|
|
1320
1932
|
if (replyTo) {
|
|
1321
1933
|
this.processedMessageIds.add(replyTo);
|
|
1322
1934
|
}
|
|
1935
|
+
this.copilotLog(`sdk_message sent replyTo=${replyTo || "latest"} responseLen=${responseText.length}`);
|
|
1323
1936
|
} catch (error) {
|
|
1324
1937
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1938
|
+
if (this.stopped && (this.remoteStopInfo || isSessionClosedError(error))) {
|
|
1939
|
+
this.copilotLog(
|
|
1940
|
+
`turn interrupted by stop_task replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt}`,
|
|
1941
|
+
);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
this.copilotLog(
|
|
1945
|
+
`turn failed replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} error="${sanitizeForLog(errorMessage, 200)}"`,
|
|
1946
|
+
);
|
|
1325
1947
|
await this.reportRuntimeStatus(
|
|
1326
1948
|
{
|
|
1327
1949
|
state: "ERROR",
|
|
@@ -1333,6 +1955,12 @@ class BridgeRunner {
|
|
|
1333
1955
|
);
|
|
1334
1956
|
await this.reportError(`${this.backendName} 处理失败: ${errorMessage}`, replyTo);
|
|
1335
1957
|
} finally {
|
|
1958
|
+
if (turnWatchdog) {
|
|
1959
|
+
clearInterval(turnWatchdog);
|
|
1960
|
+
}
|
|
1961
|
+
this.copilotLog(
|
|
1962
|
+
`turn end replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} processedIds=${this.processedMessageIds.size}`,
|
|
1963
|
+
);
|
|
1336
1964
|
this.runningTurn = false;
|
|
1337
1965
|
}
|
|
1338
1966
|
}
|
|
@@ -1340,6 +1968,8 @@ class BridgeRunner {
|
|
|
1340
1968
|
async handleSyntheticMessage(content, { includeImages }) {
|
|
1341
1969
|
this.lastRuntimeStatusSignature = null;
|
|
1342
1970
|
this.runningTurn = true;
|
|
1971
|
+
const startedAt = Date.now();
|
|
1972
|
+
this.copilotLog(`synthetic turn start includeImages=${Boolean(includeImages)} contentLen=${String(content || "").length}`);
|
|
1343
1973
|
try {
|
|
1344
1974
|
const result = await this.backendSession.runTurn(content, {
|
|
1345
1975
|
useInitialImages: includeImages,
|
|
@@ -1347,6 +1977,9 @@ class BridgeRunner {
|
|
|
1347
1977
|
void this.reportRuntimeStatus(payload, "initial");
|
|
1348
1978
|
},
|
|
1349
1979
|
});
|
|
1980
|
+
this.copilotLog(
|
|
1981
|
+
`synthetic runTurn completed elapsedMs=${Date.now() - startedAt} answerLen=${String(result.text || "").trim().length}`,
|
|
1982
|
+
);
|
|
1350
1983
|
const backendLabel = this.backendName.charAt(0).toUpperCase() + this.backendName.slice(1);
|
|
1351
1984
|
const intro = `${backendLabel} 已根据初始提示给出回复:`;
|
|
1352
1985
|
const replyText =
|
|
@@ -1364,9 +1997,18 @@ class BridgeRunner {
|
|
|
1364
1997
|
cli_args: this.cliArgs,
|
|
1365
1998
|
synthetic: true,
|
|
1366
1999
|
});
|
|
2000
|
+
this.copilotLog(`synthetic sdk_message sent responseLen=${text.length}`);
|
|
1367
2001
|
} catch (error) {
|
|
2002
|
+
if (this.stopped && (this.remoteStopInfo || isSessionClosedError(error))) {
|
|
2003
|
+
this.copilotLog(`synthetic turn interrupted by stop_task elapsedMs=${Date.now() - startedAt}`);
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
this.copilotLog(
|
|
2007
|
+
`synthetic turn failed elapsedMs=${Date.now() - startedAt} error="${sanitizeForLog(error?.message || error, 200)}"`,
|
|
2008
|
+
);
|
|
1368
2009
|
await this.reportError(`初始提示执行失败: ${error.message}`);
|
|
1369
2010
|
} finally {
|
|
2011
|
+
this.copilotLog(`synthetic turn end elapsedMs=${Date.now() - startedAt}`);
|
|
1370
2012
|
this.runningTurn = false;
|
|
1371
2013
|
}
|
|
1372
2014
|
}
|
|
@@ -1424,7 +2066,9 @@ function extractAgentTextFromMetadata(metadata) {
|
|
|
1424
2066
|
|
|
1425
2067
|
function log(message) {
|
|
1426
2068
|
const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
|
|
1427
|
-
|
|
2069
|
+
const line = `[${CLI_NAME} ${ts}] ${message}\n`;
|
|
2070
|
+
process.stdout.write(line);
|
|
2071
|
+
appendFireLocalLog(line);
|
|
1428
2072
|
}
|
|
1429
2073
|
|
|
1430
2074
|
function logBackendReply(backend, text, { usage, replyTo }) {
|
|
@@ -1432,6 +2076,22 @@ function logBackendReply(backend, text, { usage, replyTo }) {
|
|
|
1432
2076
|
log(`${backend} reply (${replyTo}): ${text}${usageSuffix}`);
|
|
1433
2077
|
}
|
|
1434
2078
|
|
|
2079
|
+
export function formatFatalError(error, opts = {}) {
|
|
2080
|
+
const showStack =
|
|
2081
|
+
typeof opts.showStack === "boolean"
|
|
2082
|
+
? opts.showStack
|
|
2083
|
+
: isTruthyEnv(process.env.CONDUCTOR_CLI_SHOW_STACK) ||
|
|
2084
|
+
isTruthyEnv(process.env.CONDUCTOR_DEBUG);
|
|
2085
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2086
|
+
if (!showStack) {
|
|
2087
|
+
return message;
|
|
2088
|
+
}
|
|
2089
|
+
if (error instanceof Error && typeof error.stack === "string" && error.stack.trim()) {
|
|
2090
|
+
return error.stack;
|
|
2091
|
+
}
|
|
2092
|
+
return message;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
1435
2095
|
function isDirectRun() {
|
|
1436
2096
|
try {
|
|
1437
2097
|
if (!process.argv[1]) {
|
|
@@ -1448,7 +2108,9 @@ function isDirectRun() {
|
|
|
1448
2108
|
if (isDirectRun()) {
|
|
1449
2109
|
main().catch((error) => {
|
|
1450
2110
|
const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
|
|
1451
|
-
|
|
2111
|
+
const line = `[${CLI_NAME} ${ts}] failed: ${formatFatalError(error)}\n`;
|
|
2112
|
+
process.stderr.write(line);
|
|
2113
|
+
appendFireLocalLog(line);
|
|
1452
2114
|
process.exitCode = 1;
|
|
1453
2115
|
});
|
|
1454
2116
|
}
|