@love-moon/conductor-cli 0.2.11 → 0.2.13
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-diagnose.js +366 -0
- package/bin/conductor-fire.js +1185 -168
- package/bin/conductor.js +5 -1
- package/package.json +3 -3
- package/src/daemon.js +66 -0
- 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,18 +20,13 @@ 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
|
|
33
|
-
const FIRE_LOG_PATH = path.join(
|
|
28
|
+
const INITIAL_CLI_PROJECT_PATH = process.cwd();
|
|
29
|
+
const FIRE_LOG_PATH = path.join(INITIAL_CLI_PROJECT_PATH, "conductor.log");
|
|
34
30
|
const ENABLE_FIRE_LOCAL_LOG = !process.env.CONDUCTOR_CLI_COMMAND;
|
|
35
31
|
|
|
36
32
|
const pkgJson = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"));
|
|
@@ -107,6 +103,12 @@ const DEFAULT_POLL_INTERVAL_MS = parseInt(
|
|
|
107
103
|
process.env.CONDUCTOR_CLI_POLL_INTERVAL_MS || process.env.CCODEX_POLL_INTERVAL_MS || "2000",
|
|
108
104
|
10,
|
|
109
105
|
);
|
|
106
|
+
const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
|
|
107
|
+
const MIN_TURN_DEADLINE_MS = 30 * 1000;
|
|
108
|
+
const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
|
|
109
|
+
const DEFAULT_ERROR_LOOP_WINDOW_MS = 2 * 60 * 1000;
|
|
110
|
+
const DEFAULT_ERROR_LOOP_BACKOFF_MS = 3 * 60 * 1000;
|
|
111
|
+
const DEFAULT_ERROR_LOOP_THRESHOLD = 3;
|
|
110
112
|
|
|
111
113
|
function appendFireLocalLog(line) {
|
|
112
114
|
if (!ENABLE_FIRE_LOCAL_LOG) {
|
|
@@ -121,6 +123,7 @@ function appendFireLocalLog(line) {
|
|
|
121
123
|
|
|
122
124
|
async function main() {
|
|
123
125
|
const cliArgs = parseCliArgs();
|
|
126
|
+
let runtimeProjectPath = process.cwd();
|
|
124
127
|
|
|
125
128
|
if (cliArgs.showHelp) {
|
|
126
129
|
return;
|
|
@@ -147,48 +150,22 @@ async function main() {
|
|
|
147
150
|
return;
|
|
148
151
|
}
|
|
149
152
|
|
|
150
|
-
let
|
|
151
|
-
if (cliArgs.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const selected = await selectHistorySession(provider);
|
|
160
|
-
if (!selected) {
|
|
161
|
-
log("No session selected. Starting a new conversation.");
|
|
162
|
-
resolvedSpec = null;
|
|
163
|
-
} else {
|
|
164
|
-
resolvedSpec = parseFromSpec(`${provider}:${selected.sessionId}`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (resolvedSpec) {
|
|
169
|
-
if (cliArgs.backend !== resolvedSpec.provider) {
|
|
170
|
-
log(
|
|
171
|
-
`Ignoring --from ${resolvedSpec.provider}:${resolvedSpec.sessionId} because backend is ${cliArgs.backend}`,
|
|
172
|
-
);
|
|
173
|
-
} else {
|
|
174
|
-
fromHistory = await loadHistoryFromSpec(resolvedSpec);
|
|
175
|
-
if (fromHistory.warning) {
|
|
176
|
-
log(fromHistory.warning);
|
|
177
|
-
} else {
|
|
178
|
-
log(
|
|
179
|
-
`Loaded ${fromHistory.history.length} history messages from ${fromHistory.provider} session ${fromHistory.sessionId}`,
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
153
|
+
let resumeContext = null;
|
|
154
|
+
if (cliArgs.resumeSessionId) {
|
|
155
|
+
resumeContext = await resolveResumeContext(cliArgs.backend, cliArgs.resumeSessionId);
|
|
156
|
+
log(
|
|
157
|
+
`Validated --resume ${resumeContext.sessionId} (${resumeContext.provider}) at ${resumeContext.sessionPath}`,
|
|
158
|
+
);
|
|
159
|
+
log(`Resume will run backend from ${resumeContext.cwd}`);
|
|
160
|
+
runtimeProjectPath = await applyWorkingDirectory(resumeContext.cwd);
|
|
161
|
+
log(`Switched working directory to ${runtimeProjectPath} before Conductor connect`);
|
|
184
162
|
}
|
|
185
163
|
|
|
186
164
|
// Create backend session using tui-driver
|
|
187
165
|
const backendSession = new TuiDriverSession(cliArgs.backend, {
|
|
188
166
|
initialImages: cliArgs.initialImages,
|
|
189
|
-
cwd:
|
|
190
|
-
|
|
191
|
-
resumeSessionId: fromHistory.sessionId,
|
|
167
|
+
cwd: runtimeProjectPath,
|
|
168
|
+
resumeSessionId: cliArgs.resumeSessionId,
|
|
192
169
|
configFile: cliArgs.configFile,
|
|
193
170
|
});
|
|
194
171
|
|
|
@@ -198,6 +175,7 @@ async function main() {
|
|
|
198
175
|
const launchedByDaemon = Boolean(process.env.CONDUCTOR_CLI_COMMAND);
|
|
199
176
|
let reconnectRunner = null;
|
|
200
177
|
let reconnectTaskId = null;
|
|
178
|
+
let pendingRemoteStopEvent = null;
|
|
201
179
|
let conductor = null;
|
|
202
180
|
let reconnectResumeInFlight = false;
|
|
203
181
|
|
|
@@ -234,6 +212,21 @@ async function main() {
|
|
|
234
212
|
})();
|
|
235
213
|
};
|
|
236
214
|
|
|
215
|
+
const handleStopTaskCommand = async (event) => {
|
|
216
|
+
if (!event || typeof event !== "object") {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const taskId = typeof event.taskId === "string" ? event.taskId : "";
|
|
220
|
+
if (reconnectTaskId && taskId && taskId !== reconnectTaskId) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (reconnectRunner && typeof reconnectRunner.requestStopFromRemote === "function") {
|
|
224
|
+
await reconnectRunner.requestStopFromRemote(event);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
pendingRemoteStopEvent = event;
|
|
228
|
+
};
|
|
229
|
+
|
|
237
230
|
if (cliArgs.configFile) {
|
|
238
231
|
env.CONDUCTOR_CONFIG = cliArgs.configFile;
|
|
239
232
|
}
|
|
@@ -252,111 +245,159 @@ async function main() {
|
|
|
252
245
|
// Ignore config loading errors, rely on env vars or defaults
|
|
253
246
|
}
|
|
254
247
|
|
|
255
|
-
conductor = await ConductorClient.connect({
|
|
256
|
-
projectPath: CLI_PROJECT_PATH,
|
|
257
|
-
extraEnv: env,
|
|
258
|
-
configFile: cliArgs.configFile,
|
|
259
|
-
onConnected: scheduleReconnectRecovery,
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
const taskContext = await ensureTaskContext(conductor, {
|
|
263
|
-
initialPrompt: cliArgs.initialPrompt,
|
|
264
|
-
requestedProjectId: process.env.CONDUCTOR_PROJECT_ID,
|
|
265
|
-
providedTaskId: process.env.CONDUCTOR_TASK_ID,
|
|
266
|
-
requestedTitle: cliArgs.taskTitle || process.env.CONDUCTOR_TASK_TITLE,
|
|
267
|
-
backend: cliArgs.backend,
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
log(
|
|
271
|
-
`Attached to Conductor task ${taskContext.taskId}${
|
|
272
|
-
taskContext.appUrl ? ` (${taskContext.appUrl})` : ""
|
|
273
|
-
}`,
|
|
274
|
-
);
|
|
275
|
-
reconnectTaskId = taskContext.taskId;
|
|
276
|
-
|
|
277
248
|
try {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
249
|
+
const requestedTaskTitle = resolveRequestedTaskTitle({
|
|
250
|
+
cliTaskTitle: cliArgs.taskTitle,
|
|
251
|
+
hasExplicitTaskTitle: cliArgs.hasExplicitTaskTitle,
|
|
252
|
+
envTaskTitle: process.env.CONDUCTOR_TASK_TITLE,
|
|
253
|
+
runtimeProjectPath,
|
|
282
254
|
});
|
|
283
|
-
} catch (error) {
|
|
284
|
-
log(`Failed to report agent resume: ${error?.message || error}`);
|
|
285
|
-
}
|
|
286
255
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
cliArgs: cliArgs.rawBackendArgs,
|
|
295
|
-
backendName: cliArgs.backend,
|
|
296
|
-
});
|
|
297
|
-
reconnectRunner = runner;
|
|
256
|
+
conductor = await ConductorClient.connect({
|
|
257
|
+
projectPath: runtimeProjectPath,
|
|
258
|
+
extraEnv: env,
|
|
259
|
+
configFile: cliArgs.configFile,
|
|
260
|
+
onConnected: scheduleReconnectRecovery,
|
|
261
|
+
onStopTask: handleStopTaskCommand,
|
|
262
|
+
});
|
|
298
263
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
264
|
+
const taskContext = await ensureTaskContext(conductor, {
|
|
265
|
+
initialPrompt: cliArgs.initialPrompt,
|
|
266
|
+
requestedProjectId: process.env.CONDUCTOR_PROJECT_ID,
|
|
267
|
+
providedTaskId: process.env.CONDUCTOR_TASK_ID,
|
|
268
|
+
requestedTitle: requestedTaskTitle,
|
|
269
|
+
backend: cliArgs.backend,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
log(
|
|
273
|
+
`Attached to Conductor task ${taskContext.taskId}${
|
|
274
|
+
taskContext.appUrl ? ` (${taskContext.appUrl})` : ""
|
|
275
|
+
}`,
|
|
276
|
+
);
|
|
277
|
+
reconnectTaskId = taskContext.taskId;
|
|
311
278
|
|
|
312
|
-
if (!launchedByDaemon) {
|
|
313
279
|
try {
|
|
314
|
-
await conductor.
|
|
315
|
-
|
|
280
|
+
await conductor.sendAgentResume({
|
|
281
|
+
active_tasks: [taskContext.taskId],
|
|
282
|
+
source: "conductor-fire",
|
|
283
|
+
metadata: { reconnect: false },
|
|
316
284
|
});
|
|
317
285
|
} catch (error) {
|
|
318
|
-
log(`Failed to report
|
|
286
|
+
log(`Failed to report agent resume: ${error?.message || error}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const runner = new BridgeRunner({
|
|
290
|
+
backendSession,
|
|
291
|
+
conductor,
|
|
292
|
+
taskId: taskContext.taskId,
|
|
293
|
+
pollIntervalMs: Math.max(cliArgs.pollIntervalMs, 500),
|
|
294
|
+
initialPrompt: taskContext.shouldProcessInitialPrompt ? cliArgs.initialPrompt : "",
|
|
295
|
+
includeInitialImages: Boolean(cliArgs.initialPrompt && cliArgs.initialImages.length),
|
|
296
|
+
cliArgs: cliArgs.rawBackendArgs,
|
|
297
|
+
backendName: cliArgs.backend,
|
|
298
|
+
resumeMode: Boolean(cliArgs.resumeSessionId),
|
|
299
|
+
});
|
|
300
|
+
reconnectRunner = runner;
|
|
301
|
+
if (pendingRemoteStopEvent) {
|
|
302
|
+
await runner.requestStopFromRemote(pendingRemoteStopEvent);
|
|
303
|
+
pendingRemoteStopEvent = null;
|
|
319
304
|
}
|
|
320
|
-
}
|
|
321
305
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
306
|
+
const signals = new AbortController();
|
|
307
|
+
let shutdownSignal = null;
|
|
308
|
+
let backendShutdownRequested = false;
|
|
309
|
+
const requestBackendShutdown = (source) => {
|
|
310
|
+
if (backendShutdownRequested) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
backendShutdownRequested = true;
|
|
314
|
+
void (async () => {
|
|
315
|
+
try {
|
|
316
|
+
await backendSession.close();
|
|
317
|
+
} catch (error) {
|
|
318
|
+
log(`Failed to close backend session after ${source}: ${error?.message || error}`);
|
|
319
|
+
}
|
|
320
|
+
})();
|
|
321
|
+
};
|
|
322
|
+
const onSigint = () => {
|
|
323
|
+
shutdownSignal = shutdownSignal || "SIGINT";
|
|
324
|
+
signals.abort();
|
|
325
|
+
requestBackendShutdown("SIGINT");
|
|
326
|
+
};
|
|
327
|
+
const onSigterm = () => {
|
|
328
|
+
shutdownSignal = shutdownSignal || "SIGTERM";
|
|
329
|
+
signals.abort();
|
|
330
|
+
requestBackendShutdown("SIGTERM");
|
|
331
|
+
};
|
|
332
|
+
process.on("SIGINT", onSigint);
|
|
333
|
+
process.on("SIGTERM", onSigterm);
|
|
334
|
+
|
|
331
335
|
if (!launchedByDaemon) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
:
|
|
336
|
+
try {
|
|
337
|
+
await conductor.sendTaskStatus(taskContext.taskId, {
|
|
338
|
+
status: "RUNNING",
|
|
339
|
+
});
|
|
340
|
+
} catch (error) {
|
|
341
|
+
log(`Failed to report task status (RUNNING): ${error?.message || error}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let runnerError = null;
|
|
346
|
+
try {
|
|
347
|
+
await runner.start(signals.signal);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
runnerError = error;
|
|
350
|
+
throw error;
|
|
351
|
+
} finally {
|
|
352
|
+
process.off("SIGINT", onSigint);
|
|
353
|
+
process.off("SIGTERM", onSigterm);
|
|
354
|
+
if (!launchedByDaemon) {
|
|
355
|
+
const remoteStopSummary = typeof runner.getRemoteStopSummary === "function" ? runner.getRemoteStopSummary() : null;
|
|
356
|
+
const finalStatus = shutdownSignal
|
|
338
357
|
? {
|
|
339
358
|
status: "KILLED",
|
|
340
|
-
summary: `
|
|
359
|
+
summary: `terminated by ${shutdownSignal}`,
|
|
341
360
|
}
|
|
342
|
-
:
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
361
|
+
: runnerError
|
|
362
|
+
? {
|
|
363
|
+
status: "KILLED",
|
|
364
|
+
summary: `conductor fire failed: ${runnerError?.message || runnerError}`,
|
|
365
|
+
}
|
|
366
|
+
: remoteStopSummary
|
|
367
|
+
? {
|
|
368
|
+
status: "KILLED",
|
|
369
|
+
summary: remoteStopSummary,
|
|
370
|
+
}
|
|
371
|
+
: {
|
|
372
|
+
status: "COMPLETED",
|
|
373
|
+
summary: "conductor fire exited",
|
|
374
|
+
};
|
|
375
|
+
try {
|
|
376
|
+
await conductor.sendTaskStatus(taskContext.taskId, finalStatus);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
log(`Failed to report task status (${finalStatus.status}): ${error?.message || error}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (shutdownSignal === "SIGINT") {
|
|
382
|
+
process.exitCode = 130;
|
|
383
|
+
} else if (shutdownSignal === "SIGTERM") {
|
|
384
|
+
process.exitCode = 143;
|
|
350
385
|
}
|
|
351
386
|
}
|
|
387
|
+
} finally {
|
|
352
388
|
if (typeof backendSession.close === "function") {
|
|
353
|
-
|
|
389
|
+
try {
|
|
390
|
+
await backendSession.close();
|
|
391
|
+
} catch (error) {
|
|
392
|
+
log(`Failed to close backend session: ${error?.message || error}`);
|
|
393
|
+
}
|
|
354
394
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
395
|
+
if (conductor && typeof conductor.close === "function") {
|
|
396
|
+
try {
|
|
397
|
+
await conductor.close();
|
|
398
|
+
} catch (error) {
|
|
399
|
+
log(`Failed to close conductor connection: ${error?.message || error}`);
|
|
400
|
+
}
|
|
360
401
|
}
|
|
361
402
|
}
|
|
362
403
|
}
|
|
@@ -374,6 +415,65 @@ function extractConfigFileFromArgv(argv) {
|
|
|
374
415
|
return undefined;
|
|
375
416
|
}
|
|
376
417
|
|
|
418
|
+
function hasLegacyFromFlags(argv = []) {
|
|
419
|
+
return argv.some(
|
|
420
|
+
(arg) =>
|
|
421
|
+
arg === "--from" ||
|
|
422
|
+
arg.startsWith("--from=") ||
|
|
423
|
+
arg === "--from-provider" ||
|
|
424
|
+
arg.startsWith("--from-provider="),
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const CONDUCTOR_BOOLEAN_FLAGS = new Set([
|
|
429
|
+
"--list-backends",
|
|
430
|
+
"--version",
|
|
431
|
+
"-v",
|
|
432
|
+
"--help",
|
|
433
|
+
"-h",
|
|
434
|
+
]);
|
|
435
|
+
|
|
436
|
+
const CONDUCTOR_VALUE_FLAGS = new Set([
|
|
437
|
+
"--backend",
|
|
438
|
+
"-b",
|
|
439
|
+
"--config-file",
|
|
440
|
+
"--poll-interval",
|
|
441
|
+
"--title",
|
|
442
|
+
"-t",
|
|
443
|
+
"--resume",
|
|
444
|
+
"--prefill",
|
|
445
|
+
]);
|
|
446
|
+
|
|
447
|
+
function stripConductorArgsFromArgv(argv = []) {
|
|
448
|
+
const backendArgs = [];
|
|
449
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
450
|
+
const raw = String(argv[i] ?? "");
|
|
451
|
+
if (!raw) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (raw === "--") {
|
|
455
|
+
backendArgs.push(...argv.slice(i + 1));
|
|
456
|
+
break;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const eqIndex = raw.indexOf("=");
|
|
460
|
+
const flag = eqIndex > 0 ? raw.slice(0, eqIndex) : raw;
|
|
461
|
+
|
|
462
|
+
if (CONDUCTOR_BOOLEAN_FLAGS.has(flag)) {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (CONDUCTOR_VALUE_FLAGS.has(flag)) {
|
|
466
|
+
if (eqIndex < 0) {
|
|
467
|
+
i += 1;
|
|
468
|
+
}
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
backendArgs.push(raw);
|
|
473
|
+
}
|
|
474
|
+
return backendArgs;
|
|
475
|
+
}
|
|
476
|
+
|
|
377
477
|
export function parseCliArgs(argvInput = process.argv) {
|
|
378
478
|
const rawArgv = Array.isArray(argvInput) ? argvInput : process.argv;
|
|
379
479
|
const argv = hideBin(rawArgv);
|
|
@@ -381,12 +481,15 @@ export function parseCliArgs(argvInput = process.argv) {
|
|
|
381
481
|
|
|
382
482
|
// When no separator, parse all args first to check for conductor-specific options
|
|
383
483
|
const conductorArgv = separatorIndex === -1 ? argv : argv.slice(0, separatorIndex);
|
|
384
|
-
const backendArgv = separatorIndex === -1 ? argv : argv.slice(separatorIndex + 1);
|
|
484
|
+
const backendArgv = separatorIndex === -1 ? stripConductorArgsFromArgv(argv) : argv.slice(separatorIndex + 1);
|
|
385
485
|
|
|
386
486
|
// Check for version/list-backends/help without separator
|
|
387
487
|
const versionWithoutSeparator = separatorIndex === -1 && (argv.includes("--version") || argv.includes("-v"));
|
|
388
488
|
const listBackendsWithoutSeparator = separatorIndex === -1 && argv.includes("--list-backends");
|
|
389
489
|
const helpWithoutSeparator = separatorIndex === -1 && (argv.includes("--help") || argv.includes("-h"));
|
|
490
|
+
if (hasLegacyFromFlags(conductorArgv)) {
|
|
491
|
+
throw new Error("--from and --from-provider were removed. Use --resume <session-id>.");
|
|
492
|
+
}
|
|
390
493
|
|
|
391
494
|
const configFileFromArgs = extractConfigFileFromArgv(argv);
|
|
392
495
|
const allowCliList = loadAllowCliList(configFileFromArgs);
|
|
@@ -419,15 +522,9 @@ export function parseCliArgs(argvInput = process.argv) {
|
|
|
419
522
|
type: "string",
|
|
420
523
|
describe: "Optional task title shown in the app task list",
|
|
421
524
|
})
|
|
422
|
-
.option("
|
|
423
|
-
alias: "f",
|
|
525
|
+
.option("resume", {
|
|
424
526
|
type: "string",
|
|
425
|
-
describe: "Resume from
|
|
426
|
-
})
|
|
427
|
-
.option("from-provider", {
|
|
428
|
-
type: "string",
|
|
429
|
-
describe: "Provider for --from picker (defaults to --backend)",
|
|
430
|
-
choices: SUPPORTED_FROM_PROVIDERS,
|
|
527
|
+
describe: "Resume from a backend session ID",
|
|
431
528
|
})
|
|
432
529
|
.option("prefill", {
|
|
433
530
|
type: "string",
|
|
@@ -460,8 +557,7 @@ Options:
|
|
|
460
557
|
--config-file <path> Path to Conductor config file
|
|
461
558
|
--poll-interval <ms> Polling interval when waiting for Conductor messages
|
|
462
559
|
-t, --title <text> Optional task title shown in the app task list
|
|
463
|
-
|
|
464
|
-
--from-provider <p> Provider for --from picker (codex or claude)
|
|
560
|
+
--resume <id> Resume from an existing backend session
|
|
465
561
|
-v, --version Show Conductor CLI version and exit
|
|
466
562
|
-h, --help Show this help message
|
|
467
563
|
|
|
@@ -475,6 +571,7 @@ Examples:
|
|
|
475
571
|
${CLI_NAME} -- "fix the bug" # Use default backend
|
|
476
572
|
${CLI_NAME} --backend claude -- "fix the bug" # Use Claude CLI backend
|
|
477
573
|
${CLI_NAME} --backend copilot -- "fix the bug" # Use GitHub Copilot CLI backend
|
|
574
|
+
${CLI_NAME} --backend codex --resume <id> # Resume Codex session
|
|
478
575
|
${CLI_NAME} --list-backends # Show configured backends
|
|
479
576
|
${CLI_NAME} --config-file ~/.conductor/config.yaml -- "fix the bug"
|
|
480
577
|
|
|
@@ -504,11 +601,13 @@ Environment:
|
|
|
504
601
|
? Number(conductorArgs.pollInterval)
|
|
505
602
|
: DEFAULT_POLL_INTERVAL_MS;
|
|
506
603
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
|
|
604
|
+
const resumeRaw = conductorArgs.resume;
|
|
605
|
+
if (resumeRaw === true) {
|
|
606
|
+
throw new Error("--resume requires a session id");
|
|
607
|
+
}
|
|
608
|
+
const resumeSessionId = typeof resumeRaw === "string" ? resumeRaw.trim() : "";
|
|
609
|
+
if (resumeRaw !== undefined && !resumeSessionId) {
|
|
610
|
+
throw new Error("--resume requires a session id");
|
|
512
611
|
}
|
|
513
612
|
|
|
514
613
|
return {
|
|
@@ -517,22 +616,34 @@ Environment:
|
|
|
517
616
|
initialImages,
|
|
518
617
|
pollIntervalMs,
|
|
519
618
|
rawBackendArgs: backendArgv,
|
|
520
|
-
taskTitle:
|
|
619
|
+
taskTitle: typeof conductorArgs.title === "string" ? conductorArgs.title.trim() : "",
|
|
620
|
+
hasExplicitTaskTitle: typeof conductorArgs.title === "string" && Boolean(conductorArgs.title.trim()),
|
|
521
621
|
configFile: conductorArgs.configFile,
|
|
522
|
-
|
|
622
|
+
resumeSessionId,
|
|
523
623
|
showVersion: Boolean(conductorArgs.version) || versionWithoutSeparator,
|
|
524
624
|
listBackends: Boolean(conductorArgs.listBackends) || listBackendsWithoutSeparator,
|
|
525
625
|
};
|
|
526
626
|
}
|
|
527
627
|
|
|
528
|
-
function resolveTaskTitle(titleFlag) {
|
|
628
|
+
export function resolveTaskTitle(titleFlag, defaultPath = process.cwd()) {
|
|
529
629
|
if (typeof titleFlag === "string" && titleFlag.trim()) {
|
|
530
630
|
return titleFlag.trim();
|
|
531
631
|
}
|
|
532
|
-
const
|
|
632
|
+
const targetPath = typeof defaultPath === "string" ? defaultPath.trim() : "";
|
|
633
|
+
const cwdName = path.basename(targetPath || process.cwd());
|
|
533
634
|
return cwdName || "";
|
|
534
635
|
}
|
|
535
636
|
|
|
637
|
+
export function resolveRequestedTaskTitle({
|
|
638
|
+
cliTaskTitle,
|
|
639
|
+
hasExplicitTaskTitle,
|
|
640
|
+
envTaskTitle,
|
|
641
|
+
runtimeProjectPath,
|
|
642
|
+
}) {
|
|
643
|
+
const explicit = hasExplicitTaskTitle ? cliTaskTitle : envTaskTitle;
|
|
644
|
+
return resolveTaskTitle(explicit, runtimeProjectPath);
|
|
645
|
+
}
|
|
646
|
+
|
|
536
647
|
function normalizeArray(value) {
|
|
537
648
|
if (!value) {
|
|
538
649
|
return [];
|
|
@@ -655,6 +766,7 @@ function deriveTaskTitle(prompt, explicit, backend = "codex") {
|
|
|
655
766
|
|
|
656
767
|
const BACKEND_PROFILE_MAP = {
|
|
657
768
|
codex: "codex",
|
|
769
|
+
code: "codex",
|
|
658
770
|
claude: "claude-code",
|
|
659
771
|
"claude-code": "claude-code",
|
|
660
772
|
copilot: "copilot",
|
|
@@ -677,6 +789,228 @@ function parseCommandParts(commandLine) {
|
|
|
677
789
|
};
|
|
678
790
|
}
|
|
679
791
|
|
|
792
|
+
export function buildResumeArgsForBackend(backend, sessionId) {
|
|
793
|
+
const resumeSessionId = typeof sessionId === "string" ? sessionId.trim() : "";
|
|
794
|
+
if (!resumeSessionId) {
|
|
795
|
+
return [];
|
|
796
|
+
}
|
|
797
|
+
const normalizedBackend = String(backend || "").trim().toLowerCase();
|
|
798
|
+
if (normalizedBackend === "codex" || normalizedBackend === "code") {
|
|
799
|
+
return ["resume", resumeSessionId];
|
|
800
|
+
}
|
|
801
|
+
if (normalizedBackend === "claude" || normalizedBackend === "claude-code") {
|
|
802
|
+
return ["--resume", resumeSessionId];
|
|
803
|
+
}
|
|
804
|
+
if (normalizedBackend === "copilot") {
|
|
805
|
+
return [`--resume=${resumeSessionId}`];
|
|
806
|
+
}
|
|
807
|
+
throw new Error(`--resume is not supported for backend "${backend}"`);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
export function resumeProviderForBackend(backend) {
|
|
811
|
+
const normalizedBackend = String(backend || "").trim().toLowerCase();
|
|
812
|
+
if (normalizedBackend === "codex" || normalizedBackend === "code") {
|
|
813
|
+
return "codex";
|
|
814
|
+
}
|
|
815
|
+
if (normalizedBackend === "claude" || normalizedBackend === "claude-code") {
|
|
816
|
+
return "claude";
|
|
817
|
+
}
|
|
818
|
+
if (normalizedBackend === "copilot") {
|
|
819
|
+
return "copilot";
|
|
820
|
+
}
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
export async function resolveSessionRunDirectory(sessionPath) {
|
|
825
|
+
const normalizedPath = typeof sessionPath === "string" ? sessionPath.trim() : "";
|
|
826
|
+
if (!normalizedPath) {
|
|
827
|
+
throw new Error("Invalid session path");
|
|
828
|
+
}
|
|
829
|
+
let stats;
|
|
830
|
+
try {
|
|
831
|
+
stats = await fs.promises.stat(normalizedPath);
|
|
832
|
+
} catch {
|
|
833
|
+
throw new Error(`Session path does not exist: ${normalizedPath}`);
|
|
834
|
+
}
|
|
835
|
+
return stats.isDirectory() ? normalizedPath : path.dirname(normalizedPath);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async function isExistingDirectory(targetPath) {
|
|
839
|
+
const normalizedPath = typeof targetPath === "string" ? targetPath.trim() : "";
|
|
840
|
+
if (!normalizedPath) {
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
try {
|
|
844
|
+
const stats = await fs.promises.stat(normalizedPath);
|
|
845
|
+
return stats.isDirectory();
|
|
846
|
+
} catch {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async function extractCodexResumeCwd(sessionPath) {
|
|
852
|
+
if (!sessionPath.endsWith(".jsonl")) {
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
const rl = readline.createInterface({
|
|
856
|
+
input: fs.createReadStream(sessionPath),
|
|
857
|
+
crlfDelay: Infinity,
|
|
858
|
+
});
|
|
859
|
+
for await (const line of rl) {
|
|
860
|
+
const trimmed = line.trim();
|
|
861
|
+
if (!trimmed) {
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
let entry;
|
|
865
|
+
try {
|
|
866
|
+
entry = JSON.parse(trimmed);
|
|
867
|
+
} catch {
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
const maybeCwd = entry?.type === "session_meta" ? entry?.payload?.cwd : null;
|
|
871
|
+
if (typeof maybeCwd === "string" && maybeCwd.trim()) {
|
|
872
|
+
return maybeCwd.trim();
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
async function extractClaudeResumeCwd(sessionPath, sessionId) {
|
|
879
|
+
if (!sessionPath.endsWith(".jsonl")) {
|
|
880
|
+
return null;
|
|
881
|
+
}
|
|
882
|
+
const rl = readline.createInterface({
|
|
883
|
+
input: fs.createReadStream(sessionPath),
|
|
884
|
+
crlfDelay: Infinity,
|
|
885
|
+
});
|
|
886
|
+
for await (const line of rl) {
|
|
887
|
+
const trimmed = line.trim();
|
|
888
|
+
if (!trimmed) {
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
let entry;
|
|
892
|
+
try {
|
|
893
|
+
entry = JSON.parse(trimmed);
|
|
894
|
+
} catch {
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
const idMatches = String(entry?.sessionId || "").trim() === sessionId;
|
|
898
|
+
const maybeCwd = entry?.cwd;
|
|
899
|
+
if (idMatches && typeof maybeCwd === "string" && maybeCwd.trim()) {
|
|
900
|
+
return maybeCwd.trim();
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
async function extractCopilotResumeCwd(sessionPath) {
|
|
907
|
+
let stats;
|
|
908
|
+
try {
|
|
909
|
+
stats = await fs.promises.stat(sessionPath);
|
|
910
|
+
} catch {
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (stats.isDirectory()) {
|
|
915
|
+
const workspaceYamlPath = path.join(sessionPath, "workspace.yaml");
|
|
916
|
+
try {
|
|
917
|
+
const yamlContent = await fs.promises.readFile(workspaceYamlPath, "utf8");
|
|
918
|
+
const parsed = yaml.load(yamlContent);
|
|
919
|
+
const maybeCwd = parsed && typeof parsed === "object" ? parsed.cwd : null;
|
|
920
|
+
if (typeof maybeCwd === "string" && maybeCwd.trim()) {
|
|
921
|
+
return maybeCwd.trim();
|
|
922
|
+
}
|
|
923
|
+
} catch {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
return null;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (!sessionPath.endsWith(".jsonl")) {
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const rl = readline.createInterface({
|
|
934
|
+
input: fs.createReadStream(sessionPath),
|
|
935
|
+
crlfDelay: Infinity,
|
|
936
|
+
});
|
|
937
|
+
for await (const line of rl) {
|
|
938
|
+
const trimmed = line.trim();
|
|
939
|
+
if (!trimmed) {
|
|
940
|
+
continue;
|
|
941
|
+
}
|
|
942
|
+
let entry;
|
|
943
|
+
try {
|
|
944
|
+
entry = JSON.parse(trimmed);
|
|
945
|
+
} catch {
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
const maybeCwd = entry?.data?.context?.cwd || entry?.data?.cwd;
|
|
949
|
+
if (typeof maybeCwd === "string" && maybeCwd.trim()) {
|
|
950
|
+
return maybeCwd.trim();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
async function extractResumeCwdFromSession(provider, sessionPath, sessionId) {
|
|
957
|
+
if (provider === "codex") {
|
|
958
|
+
return extractCodexResumeCwd(sessionPath);
|
|
959
|
+
}
|
|
960
|
+
if (provider === "claude") {
|
|
961
|
+
return extractClaudeResumeCwd(sessionPath, sessionId);
|
|
962
|
+
}
|
|
963
|
+
if (provider === "copilot") {
|
|
964
|
+
return extractCopilotResumeCwd(sessionPath);
|
|
965
|
+
}
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
export async function resolveResumeContext(backend, sessionId, options = {}) {
|
|
970
|
+
const normalizedSessionId = typeof sessionId === "string" ? sessionId.trim() : "";
|
|
971
|
+
if (!normalizedSessionId) {
|
|
972
|
+
throw new Error("--resume requires a session id");
|
|
973
|
+
}
|
|
974
|
+
const provider = resumeProviderForBackend(backend);
|
|
975
|
+
if (!provider) {
|
|
976
|
+
throw new Error(`--resume is not supported for backend "${backend}"`);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const sessionPath = await findSessionPath(provider, normalizedSessionId, options);
|
|
980
|
+
if (!sessionPath) {
|
|
981
|
+
throw new Error(`Invalid --resume session id for ${provider}: ${normalizedSessionId}`);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const cwdFromSession = await extractResumeCwdFromSession(provider, sessionPath, normalizedSessionId);
|
|
985
|
+
const fallbackCwd = await resolveSessionRunDirectory(sessionPath);
|
|
986
|
+
const cwd = cwdFromSession || fallbackCwd;
|
|
987
|
+
if (!(await isExistingDirectory(cwd))) {
|
|
988
|
+
throw new Error(`Resume workspace path does not exist: ${cwd}`);
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
provider,
|
|
992
|
+
sessionId: normalizedSessionId,
|
|
993
|
+
sessionPath,
|
|
994
|
+
cwd,
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
export async function applyWorkingDirectory(targetPath) {
|
|
999
|
+
const normalizedPath = typeof targetPath === "string" ? targetPath.trim() : "";
|
|
1000
|
+
if (!normalizedPath) {
|
|
1001
|
+
throw new Error("Cannot switch working directory: empty path");
|
|
1002
|
+
}
|
|
1003
|
+
if (!(await isExistingDirectory(normalizedPath))) {
|
|
1004
|
+
throw new Error(`Cannot switch working directory: ${normalizedPath}`);
|
|
1005
|
+
}
|
|
1006
|
+
try {
|
|
1007
|
+
process.chdir(normalizedPath);
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
throw new Error(`Cannot switch working directory to ${normalizedPath}: ${error?.message || error}`);
|
|
1010
|
+
}
|
|
1011
|
+
return process.cwd();
|
|
1012
|
+
}
|
|
1013
|
+
|
|
680
1014
|
function truncateText(value, maxLen = 240) {
|
|
681
1015
|
if (!value) return "";
|
|
682
1016
|
const text = String(value).trim();
|
|
@@ -690,11 +1024,48 @@ function isTruthyEnv(value) {
|
|
|
690
1024
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
691
1025
|
}
|
|
692
1026
|
|
|
1027
|
+
function isSessionClosedError(error) {
|
|
1028
|
+
return Boolean(error && typeof error === "object" && error.reason === "session_closed");
|
|
1029
|
+
}
|
|
1030
|
+
|
|
693
1031
|
function sanitizeForLog(value, maxLen = 180) {
|
|
694
1032
|
if (!value) return "";
|
|
695
1033
|
return truncateText(String(value).replace(/\s+/g, " ").trim(), maxLen);
|
|
696
1034
|
}
|
|
697
1035
|
|
|
1036
|
+
function getBoundedEnvInt(envName, fallback, min, max) {
|
|
1037
|
+
const fallbackNumber = Number(fallback);
|
|
1038
|
+
const normalizedFallback = Number.isFinite(fallbackNumber)
|
|
1039
|
+
? Math.min(Math.max(Math.round(fallbackNumber), min), max)
|
|
1040
|
+
: min;
|
|
1041
|
+
const raw = process.env[envName];
|
|
1042
|
+
const parsed = Number.parseInt(String(raw ?? ""), 10);
|
|
1043
|
+
if (!Number.isFinite(parsed)) {
|
|
1044
|
+
return normalizedFallback;
|
|
1045
|
+
}
|
|
1046
|
+
return Math.min(Math.max(parsed, min), max);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function normalizeExecutionErrorKey(errorMessage) {
|
|
1050
|
+
const normalized = sanitizeForLog(errorMessage, 280).toLowerCase();
|
|
1051
|
+
if (!normalized) {
|
|
1052
|
+
return "unknown_error";
|
|
1053
|
+
}
|
|
1054
|
+
if (normalized.includes("pty session already spawned")) {
|
|
1055
|
+
return "pty_session_already_spawned";
|
|
1056
|
+
}
|
|
1057
|
+
if (normalized.includes("tui process has exited")) {
|
|
1058
|
+
return "tui_process_exited";
|
|
1059
|
+
}
|
|
1060
|
+
if (normalized.includes("cannot proceed: tui process has exited")) {
|
|
1061
|
+
return "cannot_proceed_tui_exited";
|
|
1062
|
+
}
|
|
1063
|
+
if (normalized.includes("turn exceeded hard deadline")) {
|
|
1064
|
+
return "turn_timeout";
|
|
1065
|
+
}
|
|
1066
|
+
return normalized;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
698
1069
|
function tailLines(value, count = 6) {
|
|
699
1070
|
if (!value) return "";
|
|
700
1071
|
const lines = String(value).split(/\r?\n/);
|
|
@@ -705,9 +1076,15 @@ class TuiDriverSession {
|
|
|
705
1076
|
constructor(backend, options = {}) {
|
|
706
1077
|
this.backend = backend;
|
|
707
1078
|
this.options = options;
|
|
708
|
-
this.
|
|
1079
|
+
this.cwd =
|
|
1080
|
+
typeof options.cwd === "string" && options.cwd.trim()
|
|
1081
|
+
? options.cwd.trim()
|
|
1082
|
+
: INITIAL_CLI_PROJECT_PATH;
|
|
1083
|
+
const resumeSessionId = typeof options.resumeSessionId === "string" ? options.resumeSessionId.trim() : "";
|
|
1084
|
+
this.sessionId = resumeSessionId || `${backend}-${Date.now()}`;
|
|
709
1085
|
this.history = Array.isArray(options.initialHistory) ? [...options.initialHistory] : [];
|
|
710
1086
|
this.pendingHistorySeed = this.history.length > 0;
|
|
1087
|
+
this.sessionInfo = null;
|
|
711
1088
|
|
|
712
1089
|
const allowCliList = options.configFile ? loadAllowCliList(options.configFile) : DEFAULT_ALLOW_CLI_LIST;
|
|
713
1090
|
const cliCommand = CUSTOM_CLI_COMMAND || allowCliList[backend] || backend;
|
|
@@ -715,8 +1092,9 @@ class TuiDriverSession {
|
|
|
715
1092
|
if (!command) {
|
|
716
1093
|
throw new Error(`Invalid command for backend "${backend}"`);
|
|
717
1094
|
}
|
|
1095
|
+
const resumeArgs = buildResumeArgsForBackend(backend, resumeSessionId);
|
|
718
1096
|
this.command = command;
|
|
719
|
-
this.args = args;
|
|
1097
|
+
this.args = [...args, ...resumeArgs];
|
|
720
1098
|
this.tuiDebug = isTruthyEnv(process.env.CONDUCTOR_TUI_DEBUG);
|
|
721
1099
|
this.tuiTrace = this.tuiDebug || isTruthyEnv(process.env.CONDUCTOR_TUI_TRACE);
|
|
722
1100
|
this.tuiTraceLines = Number.isFinite(Number.parseInt(process.env.CONDUCTOR_TUI_TRACE_LINES || "", 10))
|
|
@@ -725,6 +1103,15 @@ class TuiDriverSession {
|
|
|
725
1103
|
this.lastSignalSignature = "";
|
|
726
1104
|
this.lastPollSignature = "";
|
|
727
1105
|
this.lastSnapshotHash = "";
|
|
1106
|
+
this.closeRequested = false;
|
|
1107
|
+
this.closed = false;
|
|
1108
|
+
this.closeWaiters = new Set();
|
|
1109
|
+
this.turnDeadlineMs = getBoundedEnvInt(
|
|
1110
|
+
"CONDUCTOR_TURN_DEADLINE_MS",
|
|
1111
|
+
DEFAULT_TURN_DEADLINE_MS,
|
|
1112
|
+
MIN_TURN_DEADLINE_MS,
|
|
1113
|
+
MAX_TURN_DEADLINE_MS,
|
|
1114
|
+
);
|
|
728
1115
|
|
|
729
1116
|
const profileName = profileNameForBackend(backend);
|
|
730
1117
|
if (!profileName) {
|
|
@@ -745,7 +1132,7 @@ class TuiDriverSession {
|
|
|
745
1132
|
log(`Using proxy: ${proxyEnv.http_proxy || proxyEnv.https_proxy || proxyEnv.all_proxy}`);
|
|
746
1133
|
}
|
|
747
1134
|
|
|
748
|
-
log(`Using TUI command for ${backend}: ${
|
|
1135
|
+
log(`Using TUI command for ${backend}: ${[this.command, ...this.args].join(" ")} (cwd: ${this.cwd})`);
|
|
749
1136
|
if (this.tuiTrace) {
|
|
750
1137
|
log(
|
|
751
1138
|
`[${this.backend}] [tui-trace] profile=${baseProfile.name} command=${this.command} args=${JSON.stringify(this.args)}`,
|
|
@@ -766,6 +1153,7 @@ class TuiDriverSession {
|
|
|
766
1153
|
...cliEnv,
|
|
767
1154
|
},
|
|
768
1155
|
},
|
|
1156
|
+
cwd: this.cwd,
|
|
769
1157
|
debug: this.tuiDebug,
|
|
770
1158
|
onSnapshot: this.tuiTrace
|
|
771
1159
|
? (snapshot, state) => {
|
|
@@ -787,6 +1175,10 @@ class TuiDriverSession {
|
|
|
787
1175
|
}
|
|
788
1176
|
log(`[${this.backend}] [WARN] Please run "${this.command} login" or authenticate manually.`);
|
|
789
1177
|
});
|
|
1178
|
+
|
|
1179
|
+
this.driver.on("session", (session) => {
|
|
1180
|
+
this.applySessionInfo(session);
|
|
1181
|
+
});
|
|
790
1182
|
}
|
|
791
1183
|
|
|
792
1184
|
get threadId() {
|
|
@@ -797,7 +1189,157 @@ class TuiDriverSession {
|
|
|
797
1189
|
return { model: this.backend };
|
|
798
1190
|
}
|
|
799
1191
|
|
|
1192
|
+
applySessionInfo(session) {
|
|
1193
|
+
if (!session || typeof session !== "object") {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const sessionId = typeof session.sessionId === "string" ? session.sessionId.trim() : "";
|
|
1197
|
+
const sessionFilePath =
|
|
1198
|
+
typeof session.sessionFilePath === "string" ? session.sessionFilePath.trim() : "";
|
|
1199
|
+
if (!sessionId) {
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
this.sessionId = sessionId;
|
|
1203
|
+
this.sessionInfo = {
|
|
1204
|
+
backend: this.backend,
|
|
1205
|
+
sessionId,
|
|
1206
|
+
sessionFilePath: sessionFilePath || undefined,
|
|
1207
|
+
};
|
|
1208
|
+
this.trace(
|
|
1209
|
+
`session id=${sessionId} file="${sanitizeForLog(sessionFilePath, 180)}"`,
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
getSessionInfo() {
|
|
1214
|
+
if (this.sessionInfo) {
|
|
1215
|
+
return { ...this.sessionInfo };
|
|
1216
|
+
}
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
async ensureSessionInfo() {
|
|
1221
|
+
if (!this.driver) {
|
|
1222
|
+
return null;
|
|
1223
|
+
}
|
|
1224
|
+
try {
|
|
1225
|
+
await this.driver.boot();
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
this.trace(`session boot failed: ${sanitizeForLog(error?.message || error, 180)}`);
|
|
1228
|
+
return this.getSessionInfo();
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
try {
|
|
1232
|
+
if (typeof this.driver.ensureSessionInfo === "function") {
|
|
1233
|
+
const detected = await this.driver.ensureSessionInfo();
|
|
1234
|
+
this.applySessionInfo(detected);
|
|
1235
|
+
} else if (typeof this.driver.getSessionInfo === "function") {
|
|
1236
|
+
this.applySessionInfo(this.driver.getSessionInfo());
|
|
1237
|
+
}
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
this.trace(`session detect failed: ${sanitizeForLog(error?.message || error, 180)}`);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
return this.getSessionInfo();
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
async getSessionUsageSummary() {
|
|
1246
|
+
if (!this.driver || typeof this.driver.getSessionUsageSummary !== "function") {
|
|
1247
|
+
return null;
|
|
1248
|
+
}
|
|
1249
|
+
try {
|
|
1250
|
+
const summary = await this.driver.getSessionUsageSummary();
|
|
1251
|
+
return summary && typeof summary === "object" ? summary : null;
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
this.trace(`session usage detect failed: ${sanitizeForLog(error?.message || error, 180)}`);
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
createSessionClosedError() {
|
|
1259
|
+
const error = new Error("TUI session closed");
|
|
1260
|
+
error.reason = "session_closed";
|
|
1261
|
+
return error;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
createTurnTimeoutError(timeoutMs) {
|
|
1265
|
+
const seconds = Math.max(1, Math.round(timeoutMs / 1000));
|
|
1266
|
+
const error = new Error(`Turn exceeded hard deadline (${seconds}s)`);
|
|
1267
|
+
error.reason = "turn_timeout";
|
|
1268
|
+
error.timeoutMs = timeoutMs;
|
|
1269
|
+
return error;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
createCloseGuard() {
|
|
1273
|
+
if (this.closeRequested) {
|
|
1274
|
+
return {
|
|
1275
|
+
promise: Promise.reject(this.createSessionClosedError()),
|
|
1276
|
+
cleanup: () => {},
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
let waiter = null;
|
|
1280
|
+
const promise = new Promise((_, reject) => {
|
|
1281
|
+
waiter = () => {
|
|
1282
|
+
reject(this.createSessionClosedError());
|
|
1283
|
+
};
|
|
1284
|
+
this.closeWaiters.add(waiter);
|
|
1285
|
+
});
|
|
1286
|
+
return {
|
|
1287
|
+
promise,
|
|
1288
|
+
cleanup: () => {
|
|
1289
|
+
if (waiter) {
|
|
1290
|
+
this.closeWaiters.delete(waiter);
|
|
1291
|
+
}
|
|
1292
|
+
},
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
createTurnTimeoutGuard() {
|
|
1297
|
+
if (!Number.isFinite(this.turnDeadlineMs) || this.turnDeadlineMs <= 0) {
|
|
1298
|
+
return {
|
|
1299
|
+
promise: new Promise(() => {}),
|
|
1300
|
+
cleanup: () => {},
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
let timer = null;
|
|
1304
|
+
const promise = new Promise((_, reject) => {
|
|
1305
|
+
timer = setTimeout(() => {
|
|
1306
|
+
reject(this.createTurnTimeoutError(this.turnDeadlineMs));
|
|
1307
|
+
}, this.turnDeadlineMs);
|
|
1308
|
+
if (typeof timer.unref === "function") {
|
|
1309
|
+
timer.unref();
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
return {
|
|
1313
|
+
promise,
|
|
1314
|
+
cleanup: () => {
|
|
1315
|
+
if (timer) {
|
|
1316
|
+
clearTimeout(timer);
|
|
1317
|
+
}
|
|
1318
|
+
},
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
flushCloseWaiters() {
|
|
1323
|
+
if (!this.closeWaiters || this.closeWaiters.size === 0) {
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
for (const waiter of this.closeWaiters) {
|
|
1327
|
+
try {
|
|
1328
|
+
waiter();
|
|
1329
|
+
} catch {
|
|
1330
|
+
// best effort
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
this.closeWaiters.clear();
|
|
1334
|
+
}
|
|
1335
|
+
|
|
800
1336
|
async close() {
|
|
1337
|
+
if (this.closed) {
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
this.closed = true;
|
|
1341
|
+
this.closeRequested = true;
|
|
1342
|
+
this.flushCloseWaiters();
|
|
801
1343
|
if (this.driver) {
|
|
802
1344
|
this.driver.kill();
|
|
803
1345
|
}
|
|
@@ -902,6 +1444,10 @@ class TuiDriverSession {
|
|
|
902
1444
|
}
|
|
903
1445
|
|
|
904
1446
|
async runTurn(promptText, { useInitialImages = false, onProgress } = {}) {
|
|
1447
|
+
if (this.closeRequested) {
|
|
1448
|
+
throw this.createSessionClosedError();
|
|
1449
|
+
}
|
|
1450
|
+
|
|
905
1451
|
const effectivePrompt = this.buildPrompt(promptText, { useInitialImages });
|
|
906
1452
|
if (!effectivePrompt) {
|
|
907
1453
|
return {
|
|
@@ -958,8 +1504,20 @@ class TuiDriverSession {
|
|
|
958
1504
|
signalTimer.unref();
|
|
959
1505
|
}
|
|
960
1506
|
|
|
1507
|
+
const previousCwd = process.cwd();
|
|
1508
|
+
const shouldSwitchCwd = this.cwd && this.cwd !== previousCwd;
|
|
1509
|
+
if (shouldSwitchCwd) {
|
|
1510
|
+
try {
|
|
1511
|
+
process.chdir(this.cwd);
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
throw new Error(`Failed to switch backend cwd to ${this.cwd}: ${error?.message || error}`);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
const closeGuard = this.createCloseGuard();
|
|
1517
|
+
const turnTimeoutGuard = this.createTurnTimeoutGuard();
|
|
1518
|
+
|
|
961
1519
|
try {
|
|
962
|
-
const result = await this.driver.ask(effectivePrompt);
|
|
1520
|
+
const result = await Promise.race([this.driver.ask(effectivePrompt), closeGuard.promise, turnTimeoutGuard.promise]);
|
|
963
1521
|
const answer = String(result.answer || result.replyText || "").trim();
|
|
964
1522
|
this.trace(
|
|
965
1523
|
`runTurn finished success=${Boolean(result.success)} elapsedMs=${result.elapsedMs} answerLen=${answer.length} state=${this.driver.state}`,
|
|
@@ -1003,9 +1561,28 @@ class TuiDriverSession {
|
|
|
1003
1561
|
} catch (error) {
|
|
1004
1562
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1005
1563
|
const errorReason = error?.reason || "unknown";
|
|
1564
|
+
if (errorReason === "session_closed") {
|
|
1565
|
+
this.trace("runTurn interrupted because backend session is closing");
|
|
1566
|
+
throw error instanceof Error ? error : new Error(errorMessage);
|
|
1567
|
+
}
|
|
1006
1568
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1569
|
+
if (errorReason === "turn_timeout") {
|
|
1570
|
+
this.emitProgress(onProgress, {
|
|
1571
|
+
state: "ERROR",
|
|
1572
|
+
phase: "timeout_recovered",
|
|
1573
|
+
source: "tui-driver",
|
|
1574
|
+
error: errorMessage,
|
|
1575
|
+
reason: errorReason,
|
|
1576
|
+
timeout_ms: error?.timeoutMs,
|
|
1577
|
+
});
|
|
1578
|
+
log(`[${this.backend}] Turn timed out (${error?.timeoutMs || this.turnDeadlineMs}ms), restarting TUI session`);
|
|
1579
|
+
try {
|
|
1580
|
+
await this.driver.forceRestart();
|
|
1581
|
+
} catch (restartError) {
|
|
1582
|
+
log(`[${this.backend}] Failed to restart TUI after timeout: ${restartError?.message || restartError}`);
|
|
1583
|
+
}
|
|
1584
|
+
log(`[${this.backend}] Error: ${errorMessage}`);
|
|
1585
|
+
} else if (errorReason === "login_required") {
|
|
1009
1586
|
this.emitProgress(onProgress, {
|
|
1010
1587
|
state: "ERROR",
|
|
1011
1588
|
phase: "login_required",
|
|
@@ -1037,21 +1614,35 @@ class TuiDriverSession {
|
|
|
1037
1614
|
log(`[${this.backend}] Error: ${errorMessage}`);
|
|
1038
1615
|
}
|
|
1039
1616
|
|
|
1040
|
-
|
|
1617
|
+
let latestSignals = {};
|
|
1618
|
+
try {
|
|
1619
|
+
latestSignals = this.driver.getSignals();
|
|
1620
|
+
} catch {
|
|
1621
|
+
// driver may already be disposed while closing
|
|
1622
|
+
}
|
|
1041
1623
|
const summary = this.formatSignalSummary(latestSignals);
|
|
1042
1624
|
this.trace(
|
|
1043
1625
|
`runTurn exception state=${this.driver.state} error="${sanitizeForLog(errorMessage, 220)}" status="${summary.status || ""}" done="${summary.done || ""}" preview="${summary.replyPreview || ""}"`,
|
|
1044
1626
|
);
|
|
1045
1627
|
throw error instanceof Error ? error : new Error(errorMessage);
|
|
1046
1628
|
} finally {
|
|
1629
|
+
if (shouldSwitchCwd) {
|
|
1630
|
+
try {
|
|
1631
|
+
process.chdir(previousCwd);
|
|
1632
|
+
} catch (error) {
|
|
1633
|
+
log(`Failed to restore cwd to ${previousCwd}: ${error?.message || error}`);
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1047
1636
|
clearInterval(signalTimer);
|
|
1048
1637
|
this.driver.off("stateChange", handleStateChange);
|
|
1638
|
+
turnTimeoutGuard.cleanup();
|
|
1639
|
+
closeGuard.cleanup();
|
|
1049
1640
|
this.trace(`runTurn cleanup state=${this.driver.state}`);
|
|
1050
1641
|
}
|
|
1051
1642
|
}
|
|
1052
1643
|
}
|
|
1053
1644
|
|
|
1054
|
-
class BridgeRunner {
|
|
1645
|
+
export class BridgeRunner {
|
|
1055
1646
|
constructor({
|
|
1056
1647
|
backendSession,
|
|
1057
1648
|
conductor,
|
|
@@ -1061,6 +1652,7 @@ class BridgeRunner {
|
|
|
1061
1652
|
includeInitialImages,
|
|
1062
1653
|
cliArgs,
|
|
1063
1654
|
backendName,
|
|
1655
|
+
resumeMode,
|
|
1064
1656
|
}) {
|
|
1065
1657
|
this.backendSession = backendSession;
|
|
1066
1658
|
this.conductor = conductor;
|
|
@@ -1070,26 +1662,122 @@ class BridgeRunner {
|
|
|
1070
1662
|
this.includeInitialImages = includeInitialImages;
|
|
1071
1663
|
this.cliArgs = cliArgs;
|
|
1072
1664
|
this.backendName = backendName || "codex";
|
|
1665
|
+
this.resumeMode = Boolean(resumeMode);
|
|
1073
1666
|
this.isCopilotBackend = String(this.backendName).toLowerCase() === "copilot";
|
|
1667
|
+
this.copilotDebug =
|
|
1668
|
+
this.isCopilotBackend &&
|
|
1669
|
+
(isTruthyEnv(process.env.CONDUCTOR_COPILOT_DEBUG) || isTruthyEnv(process.env.CONDUCTOR_DEBUG));
|
|
1074
1670
|
this.stopped = false;
|
|
1075
1671
|
this.runningTurn = false;
|
|
1076
1672
|
this.processedMessageIds = new Set();
|
|
1673
|
+
this.inFlightMessageIds = new Set();
|
|
1077
1674
|
this.lastRuntimeStatusSignature = null;
|
|
1078
1675
|
this.lastRuntimeStatusPayload = null;
|
|
1676
|
+
this.runtimeContextSnapshot = null;
|
|
1677
|
+
this.runtimeContextSnapshotAt = 0;
|
|
1678
|
+
this.runtimeContextInFlight = null;
|
|
1679
|
+
this.runtimeContextRefreshMs = getBoundedEnvInt(
|
|
1680
|
+
"CONDUCTOR_RUNTIME_CONTEXT_REFRESH_MS",
|
|
1681
|
+
2000,
|
|
1682
|
+
500,
|
|
1683
|
+
60 * 1000,
|
|
1684
|
+
);
|
|
1685
|
+
this.daemonName =
|
|
1686
|
+
(typeof process.env.CONDUCTOR_AGENT_NAME === "string" && process.env.CONDUCTOR_AGENT_NAME.trim()) ||
|
|
1687
|
+
(typeof process.env.CONDUCTOR_DAEMON_NAME === "string" && process.env.CONDUCTOR_DAEMON_NAME.trim()) ||
|
|
1688
|
+
(typeof process.env.HOSTNAME === "string" && process.env.HOSTNAME.trim()) ||
|
|
1689
|
+
os.hostname();
|
|
1079
1690
|
this.needsReconnectRecovery = false;
|
|
1691
|
+
this.remoteStopInfo = null;
|
|
1692
|
+
this.sessionAnnouncementSent = false;
|
|
1693
|
+
this.errorLoop = null;
|
|
1694
|
+
this.errorLoopWindowMs = getBoundedEnvInt(
|
|
1695
|
+
"CONDUCTOR_ERROR_LOOP_WINDOW_MS",
|
|
1696
|
+
DEFAULT_ERROR_LOOP_WINDOW_MS,
|
|
1697
|
+
15 * 1000,
|
|
1698
|
+
30 * 60 * 1000,
|
|
1699
|
+
);
|
|
1700
|
+
this.errorLoopBackoffMs = getBoundedEnvInt(
|
|
1701
|
+
"CONDUCTOR_ERROR_LOOP_BACKOFF_MS",
|
|
1702
|
+
DEFAULT_ERROR_LOOP_BACKOFF_MS,
|
|
1703
|
+
15 * 1000,
|
|
1704
|
+
60 * 60 * 1000,
|
|
1705
|
+
);
|
|
1706
|
+
this.errorLoopThreshold = getBoundedEnvInt(
|
|
1707
|
+
"CONDUCTOR_ERROR_LOOP_THRESHOLD",
|
|
1708
|
+
DEFAULT_ERROR_LOOP_THRESHOLD,
|
|
1709
|
+
2,
|
|
1710
|
+
20,
|
|
1711
|
+
);
|
|
1080
1712
|
}
|
|
1081
1713
|
|
|
1082
1714
|
copilotLog(message) {
|
|
1083
|
-
if (!this.
|
|
1715
|
+
if (!this.copilotDebug) {
|
|
1084
1716
|
return;
|
|
1085
1717
|
}
|
|
1086
1718
|
log(`[copilot-debug] task=${this.taskId} ${message}`);
|
|
1087
1719
|
}
|
|
1088
1720
|
|
|
1721
|
+
async announceBackendSession() {
|
|
1722
|
+
if (this.sessionAnnouncementSent) {
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
if (!this.backendSession || typeof this.backendSession.ensureSessionInfo !== "function") {
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
let sessionInfo = null;
|
|
1729
|
+
try {
|
|
1730
|
+
sessionInfo = await this.backendSession.ensureSessionInfo();
|
|
1731
|
+
} catch (error) {
|
|
1732
|
+
this.copilotLog(`session announce skipped: ${sanitizeForLog(error?.message || error, 160)}`);
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
const sessionId = String(sessionInfo?.sessionId || "").trim();
|
|
1736
|
+
const sessionFilePath = sessionInfo?.sessionFilePath ? String(sessionInfo.sessionFilePath).trim() : "";
|
|
1737
|
+
const hasRealSessionId = Boolean(sessionId);
|
|
1738
|
+
const message = hasRealSessionId
|
|
1739
|
+
? `${this.backendName} session started: ${sessionId}`
|
|
1740
|
+
: `${this.backendName} session started`;
|
|
1741
|
+
try {
|
|
1742
|
+
await this.conductor.sendMessage(this.taskId, message, {
|
|
1743
|
+
backend: this.backendName,
|
|
1744
|
+
thread_id: hasRealSessionId ? sessionId : undefined,
|
|
1745
|
+
session_id: hasRealSessionId ? sessionId : undefined,
|
|
1746
|
+
session_file_path: sessionFilePath || undefined,
|
|
1747
|
+
cli_args: this.cliArgs,
|
|
1748
|
+
synthetic: true,
|
|
1749
|
+
});
|
|
1750
|
+
this.sessionAnnouncementSent = true;
|
|
1751
|
+
this.copilotLog(hasRealSessionId ? `session announced id=${sessionId}` : "session announced without id");
|
|
1752
|
+
await this.reportRuntimeStatus(
|
|
1753
|
+
{
|
|
1754
|
+
state: "WAIT_READY",
|
|
1755
|
+
phase: "session_started",
|
|
1756
|
+
source: "tui-driver",
|
|
1757
|
+
reply_in_progress: false,
|
|
1758
|
+
status_done_line: `${this.backendName} session started`,
|
|
1759
|
+
backend: this.backendName,
|
|
1760
|
+
thread_id: hasRealSessionId ? sessionId : undefined,
|
|
1761
|
+
},
|
|
1762
|
+
undefined,
|
|
1763
|
+
);
|
|
1764
|
+
} catch (error) {
|
|
1765
|
+
log(`Failed to send session announcement: ${error?.message || error}`);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1089
1769
|
async start(abortSignal) {
|
|
1090
1770
|
abortSignal?.addEventListener("abort", () => {
|
|
1091
1771
|
this.stopped = true;
|
|
1092
1772
|
});
|
|
1773
|
+
if (this.stopped) {
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
await this.announceBackendSession();
|
|
1778
|
+
if (this.stopped) {
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1093
1781
|
|
|
1094
1782
|
if (this.initialPrompt) {
|
|
1095
1783
|
this.copilotLog("processing initial prompt");
|
|
@@ -1097,8 +1785,20 @@ class BridgeRunner {
|
|
|
1097
1785
|
includeImages: this.includeInitialImages,
|
|
1098
1786
|
});
|
|
1099
1787
|
}
|
|
1788
|
+
if (this.stopped) {
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
if (this.resumeMode) {
|
|
1792
|
+
await this.drainBufferedMessagesForResume();
|
|
1793
|
+
}
|
|
1794
|
+
if (this.stopped) {
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1100
1797
|
this.copilotLog("running startup backfill");
|
|
1101
1798
|
await this.backfillPendingUserMessages();
|
|
1799
|
+
if (this.stopped) {
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1102
1802
|
|
|
1103
1803
|
while (!this.stopped) {
|
|
1104
1804
|
if (this.needsReconnectRecovery && !this.runningTurn) {
|
|
@@ -1108,10 +1808,13 @@ class BridgeRunner {
|
|
|
1108
1808
|
try {
|
|
1109
1809
|
processed = await this.processIncomingBatch();
|
|
1110
1810
|
} catch (error) {
|
|
1811
|
+
if (this.stopped && (this.remoteStopInfo || isSessionClosedError(error))) {
|
|
1812
|
+
break;
|
|
1813
|
+
}
|
|
1111
1814
|
log(`Error while processing messages: ${error.message}`);
|
|
1112
1815
|
await this.reportError(`处理任务失败: ${error.message}`);
|
|
1113
1816
|
}
|
|
1114
|
-
if (!processed) {
|
|
1817
|
+
if (!processed && !this.stopped) {
|
|
1115
1818
|
await delay(this.pollIntervalMs);
|
|
1116
1819
|
}
|
|
1117
1820
|
}
|
|
@@ -1121,6 +1824,42 @@ class BridgeRunner {
|
|
|
1121
1824
|
this.needsReconnectRecovery = true;
|
|
1122
1825
|
}
|
|
1123
1826
|
|
|
1827
|
+
getRemoteStopSummary() {
|
|
1828
|
+
if (!this.remoteStopInfo) {
|
|
1829
|
+
return null;
|
|
1830
|
+
}
|
|
1831
|
+
const reason = this.remoteStopInfo.reason || "killed by app";
|
|
1832
|
+
return `task stopped by app: ${reason}`;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
async requestStopFromRemote(event = {}) {
|
|
1836
|
+
const taskId = typeof event.taskId === "string" ? event.taskId.trim() : "";
|
|
1837
|
+
if (taskId && taskId !== this.taskId) {
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
const requestId = typeof event.requestId === "string" ? event.requestId.trim() : "";
|
|
1841
|
+
const reason = typeof event.reason === "string" ? event.reason.trim() : "";
|
|
1842
|
+
if (!this.remoteStopInfo) {
|
|
1843
|
+
this.remoteStopInfo = {
|
|
1844
|
+
requestId: requestId || null,
|
|
1845
|
+
reason: reason || null,
|
|
1846
|
+
};
|
|
1847
|
+
log(
|
|
1848
|
+
`Received stop_task for ${this.taskId}${
|
|
1849
|
+
reason ? ` (${reason})` : ""
|
|
1850
|
+
}; stopping conductor fire`,
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
this.stopped = true;
|
|
1854
|
+
if (typeof this.backendSession?.close === "function") {
|
|
1855
|
+
try {
|
|
1856
|
+
await this.backendSession.close();
|
|
1857
|
+
} catch (error) {
|
|
1858
|
+
log(`Failed to stop backend session for ${this.taskId}: ${error?.message || error}`);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1124
1863
|
async recoverAfterReconnect() {
|
|
1125
1864
|
if (!this.needsReconnectRecovery) {
|
|
1126
1865
|
return;
|
|
@@ -1137,6 +1876,41 @@ class BridgeRunner {
|
|
|
1137
1876
|
await this.replayLastRuntimeStatus();
|
|
1138
1877
|
}
|
|
1139
1878
|
|
|
1879
|
+
async drainBufferedMessagesForResume() {
|
|
1880
|
+
let drainedCount = 0;
|
|
1881
|
+
let drainedBatches = 0;
|
|
1882
|
+
|
|
1883
|
+
while (!this.stopped) {
|
|
1884
|
+
const result = await this.conductor.receiveMessages(this.taskId, 50);
|
|
1885
|
+
const messages = Array.isArray(result?.messages) ? result.messages : [];
|
|
1886
|
+
if (messages.length === 0) {
|
|
1887
|
+
break;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
drainedBatches += 1;
|
|
1891
|
+
for (const message of messages) {
|
|
1892
|
+
const replyTo = message?.message_id ? String(message.message_id) : "";
|
|
1893
|
+
if (replyTo) {
|
|
1894
|
+
this.processedMessageIds.add(replyTo);
|
|
1895
|
+
}
|
|
1896
|
+
drainedCount += 1;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
const ackToken = result.next_ack_token || result.nextAckToken;
|
|
1900
|
+
if (ackToken) {
|
|
1901
|
+
await this.conductor.ackMessages(this.taskId, ackToken);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
if (drainedCount > 0) {
|
|
1906
|
+
log(
|
|
1907
|
+
`Resume startup skipped ${drainedCount} buffered message(s) in ${drainedBatches} batch(es) for task ${this.taskId}`,
|
|
1908
|
+
);
|
|
1909
|
+
} else {
|
|
1910
|
+
this.copilotLog("resume startup found no buffered messages to skip");
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1140
1914
|
async processIncomingBatch() {
|
|
1141
1915
|
const result = await this.conductor.receiveMessages(this.taskId, 20);
|
|
1142
1916
|
const messages = Array.isArray(result?.messages) ? result.messages : [];
|
|
@@ -1151,6 +1925,9 @@ class BridgeRunner {
|
|
|
1151
1925
|
);
|
|
1152
1926
|
|
|
1153
1927
|
for (const message of messages) {
|
|
1928
|
+
if (this.stopped) {
|
|
1929
|
+
break;
|
|
1930
|
+
}
|
|
1154
1931
|
if (!this.shouldRespond(message)) {
|
|
1155
1932
|
this.copilotLog(`skip message role=${String(message?.role || "unknown").toLowerCase()}`);
|
|
1156
1933
|
continue;
|
|
@@ -1211,12 +1988,37 @@ class BridgeRunner {
|
|
|
1211
1988
|
}
|
|
1212
1989
|
}
|
|
1213
1990
|
|
|
1991
|
+
const historyUserIds = history
|
|
1992
|
+
.filter((item) => String(item?.role || "").toLowerCase() === "user")
|
|
1993
|
+
.map((item) => (item?.id ? String(item.id) : ""))
|
|
1994
|
+
.filter(Boolean);
|
|
1995
|
+
|
|
1996
|
+
const handledUserIds = history
|
|
1997
|
+
.slice(0, lastSdkIndex + 1)
|
|
1998
|
+
.filter((item) => String(item?.role || "").toLowerCase() === "user")
|
|
1999
|
+
.map((item) => (item?.id ? String(item.id) : ""))
|
|
2000
|
+
.filter(Boolean);
|
|
2001
|
+
|
|
2002
|
+
for (const handledId of handledUserIds) {
|
|
2003
|
+
this.processedMessageIds.add(handledId);
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1214
2006
|
const pendingUserMessages = history
|
|
1215
2007
|
.slice(lastSdkIndex + 1)
|
|
1216
2008
|
.filter((item) => String(item?.role || "").toLowerCase() === "user")
|
|
1217
2009
|
.filter((item) => typeof item?.content === "string" && item.content.trim());
|
|
2010
|
+
|
|
2011
|
+
if (this.resumeMode) {
|
|
2012
|
+
for (const historyId of historyUserIds) {
|
|
2013
|
+
this.processedMessageIds.add(historyId);
|
|
2014
|
+
}
|
|
2015
|
+
this.copilotLog(
|
|
2016
|
+
`resume mode: seeded processed ids=${historyUserIds.length}, skip startup backfill replay`,
|
|
2017
|
+
);
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
1218
2020
|
this.copilotLog(
|
|
1219
|
-
`backfill loaded history=${history.length} pendingUsers=${pendingUserMessages.length} lastSdkIndex=${lastSdkIndex}`,
|
|
2021
|
+
`backfill loaded history=${history.length} handledUsers=${handledUserIds.length} pendingUsers=${pendingUserMessages.length} lastSdkIndex=${lastSdkIndex}`,
|
|
1220
2022
|
);
|
|
1221
2023
|
|
|
1222
2024
|
for (const pending of pendingUserMessages) {
|
|
@@ -1232,7 +2034,80 @@ class BridgeRunner {
|
|
|
1232
2034
|
}
|
|
1233
2035
|
}
|
|
1234
2036
|
|
|
1235
|
-
|
|
2037
|
+
normalizePercent(value) {
|
|
2038
|
+
if (!Number.isFinite(value)) {
|
|
2039
|
+
return undefined;
|
|
2040
|
+
}
|
|
2041
|
+
if (value < 0) {
|
|
2042
|
+
return 0;
|
|
2043
|
+
}
|
|
2044
|
+
if (value > 100) {
|
|
2045
|
+
return 100;
|
|
2046
|
+
}
|
|
2047
|
+
return value;
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
async resolveRuntimeContext() {
|
|
2051
|
+
const now = Date.now();
|
|
2052
|
+
if (
|
|
2053
|
+
this.runtimeContextSnapshot &&
|
|
2054
|
+
now - this.runtimeContextSnapshotAt < this.runtimeContextRefreshMs
|
|
2055
|
+
) {
|
|
2056
|
+
return this.runtimeContextSnapshot;
|
|
2057
|
+
}
|
|
2058
|
+
if (this.runtimeContextInFlight) {
|
|
2059
|
+
return this.runtimeContextInFlight;
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
this.runtimeContextInFlight = (async () => {
|
|
2063
|
+
let sessionInfo = null;
|
|
2064
|
+
try {
|
|
2065
|
+
if (typeof this.backendSession?.getSessionInfo === "function") {
|
|
2066
|
+
sessionInfo = this.backendSession.getSessionInfo();
|
|
2067
|
+
}
|
|
2068
|
+
} catch {
|
|
2069
|
+
sessionInfo = null;
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
let usage = null;
|
|
2073
|
+
try {
|
|
2074
|
+
if (typeof this.backendSession?.getSessionUsageSummary === "function") {
|
|
2075
|
+
usage = await this.backendSession.getSessionUsageSummary();
|
|
2076
|
+
}
|
|
2077
|
+
} catch {
|
|
2078
|
+
usage = null;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
const resolvedSessionId = String(
|
|
2082
|
+
usage?.sessionId || sessionInfo?.sessionId || "",
|
|
2083
|
+
).trim();
|
|
2084
|
+
const resolvedSessionFilePath = String(
|
|
2085
|
+
usage?.sessionFilePath || sessionInfo?.sessionFilePath || "",
|
|
2086
|
+
).trim();
|
|
2087
|
+
const tokenUsagePercent = this.normalizePercent(Number(usage?.tokenUsagePercent));
|
|
2088
|
+
const contextUsagePercent = this.normalizePercent(Number(usage?.contextUsagePercent));
|
|
2089
|
+
|
|
2090
|
+
const snapshot = {
|
|
2091
|
+
daemon: this.daemonName || undefined,
|
|
2092
|
+
pid: process.pid,
|
|
2093
|
+
session_id: resolvedSessionId || undefined,
|
|
2094
|
+
session_file_path: resolvedSessionFilePath || undefined,
|
|
2095
|
+
token_usage_percent: tokenUsagePercent,
|
|
2096
|
+
context_usage_percent: contextUsagePercent,
|
|
2097
|
+
};
|
|
2098
|
+
this.runtimeContextSnapshot = snapshot;
|
|
2099
|
+
this.runtimeContextSnapshotAt = Date.now();
|
|
2100
|
+
return snapshot;
|
|
2101
|
+
})();
|
|
2102
|
+
|
|
2103
|
+
try {
|
|
2104
|
+
return await this.runtimeContextInFlight;
|
|
2105
|
+
} finally {
|
|
2106
|
+
this.runtimeContextInFlight = null;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
createRuntimeStatus(payload, replyTo, runtimeContext = null) {
|
|
1236
2111
|
if (!payload || typeof payload !== "object") {
|
|
1237
2112
|
return null;
|
|
1238
2113
|
}
|
|
@@ -1257,12 +2132,22 @@ class BridgeRunner {
|
|
|
1257
2132
|
reply_preview: truncateText(replyPreview, 240) || undefined,
|
|
1258
2133
|
reply_to: replyTo,
|
|
1259
2134
|
backend: this.backendName,
|
|
1260
|
-
thread_id:
|
|
2135
|
+
thread_id:
|
|
2136
|
+
String(
|
|
2137
|
+
payload.thread_id || runtimeContext?.session_id || "",
|
|
2138
|
+
).trim() || undefined,
|
|
2139
|
+
daemon: runtimeContext?.daemon,
|
|
2140
|
+
pid: runtimeContext?.pid,
|
|
2141
|
+
session_id: runtimeContext?.session_id,
|
|
2142
|
+
session_file_path: runtimeContext?.session_file_path,
|
|
2143
|
+
token_usage_percent: runtimeContext?.token_usage_percent,
|
|
2144
|
+
context_usage_percent: runtimeContext?.context_usage_percent,
|
|
1261
2145
|
};
|
|
1262
2146
|
}
|
|
1263
2147
|
|
|
1264
2148
|
async reportRuntimeStatus(payload, replyTo) {
|
|
1265
|
-
const
|
|
2149
|
+
const runtimeContext = await this.resolveRuntimeContext();
|
|
2150
|
+
const runtime = this.createRuntimeStatus(payload, replyTo, runtimeContext);
|
|
1266
2151
|
if (!runtime) {
|
|
1267
2152
|
return;
|
|
1268
2153
|
}
|
|
@@ -1305,6 +2190,67 @@ class BridgeRunner {
|
|
|
1305
2190
|
}
|
|
1306
2191
|
}
|
|
1307
2192
|
|
|
2193
|
+
resetErrorLoop() {
|
|
2194
|
+
this.errorLoop = null;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
evaluateErrorLoop(errorMessage) {
|
|
2198
|
+
const normalizedKey = normalizeExecutionErrorKey(errorMessage);
|
|
2199
|
+
const now = Date.now();
|
|
2200
|
+
const current = this.errorLoop;
|
|
2201
|
+
|
|
2202
|
+
if (
|
|
2203
|
+
!current ||
|
|
2204
|
+
current.key !== normalizedKey ||
|
|
2205
|
+
now - current.lastAt > this.errorLoopWindowMs
|
|
2206
|
+
) {
|
|
2207
|
+
this.errorLoop = {
|
|
2208
|
+
key: normalizedKey,
|
|
2209
|
+
count: 1,
|
|
2210
|
+
lastAt: now,
|
|
2211
|
+
cooldownUntil: 0,
|
|
2212
|
+
};
|
|
2213
|
+
return {
|
|
2214
|
+
key: normalizedKey,
|
|
2215
|
+
count: 1,
|
|
2216
|
+
open: false,
|
|
2217
|
+
suppressReport: false,
|
|
2218
|
+
cooldownMs: 0,
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
current.count += 1;
|
|
2223
|
+
current.lastAt = now;
|
|
2224
|
+
const open = current.count >= this.errorLoopThreshold;
|
|
2225
|
+
let suppressReport = false;
|
|
2226
|
+
|
|
2227
|
+
if (open) {
|
|
2228
|
+
if (current.cooldownUntil > now) {
|
|
2229
|
+
suppressReport = true;
|
|
2230
|
+
} else {
|
|
2231
|
+
current.cooldownUntil = now + this.errorLoopBackoffMs;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
this.errorLoop = current;
|
|
2236
|
+
return {
|
|
2237
|
+
key: normalizedKey,
|
|
2238
|
+
count: current.count,
|
|
2239
|
+
open,
|
|
2240
|
+
suppressReport,
|
|
2241
|
+
cooldownMs: suppressReport ? current.cooldownUntil - now : 0,
|
|
2242
|
+
};
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
isExecutionFailureLoopError(errorMessage) {
|
|
2246
|
+
const normalized = String(errorMessage || "").toLowerCase();
|
|
2247
|
+
return (
|
|
2248
|
+
normalized.includes("pty session already spawned") ||
|
|
2249
|
+
normalized.includes("tui process has exited") ||
|
|
2250
|
+
normalized.includes("cannot proceed: tui process has exited")
|
|
2251
|
+
);
|
|
2252
|
+
}
|
|
2253
|
+
|
|
1308
2254
|
async respondToMessage(message) {
|
|
1309
2255
|
const content = String(message.content || "").trim();
|
|
1310
2256
|
if (!content) {
|
|
@@ -1316,6 +2262,13 @@ class BridgeRunner {
|
|
|
1316
2262
|
this.copilotLog(`skip duplicated message replyTo=${replyTo}`);
|
|
1317
2263
|
return;
|
|
1318
2264
|
}
|
|
2265
|
+
if (replyTo && this.inFlightMessageIds.has(replyTo)) {
|
|
2266
|
+
this.copilotLog(`skip in-flight duplicated message replyTo=${replyTo}`);
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
if (replyTo) {
|
|
2270
|
+
this.inFlightMessageIds.add(replyTo);
|
|
2271
|
+
}
|
|
1319
2272
|
this.lastRuntimeStatusSignature = null;
|
|
1320
2273
|
this.runningTurn = true;
|
|
1321
2274
|
const turnStartedAt = Date.now();
|
|
@@ -1385,9 +2338,16 @@ class BridgeRunner {
|
|
|
1385
2338
|
if (replyTo) {
|
|
1386
2339
|
this.processedMessageIds.add(replyTo);
|
|
1387
2340
|
}
|
|
2341
|
+
this.resetErrorLoop();
|
|
1388
2342
|
this.copilotLog(`sdk_message sent replyTo=${replyTo || "latest"} responseLen=${responseText.length}`);
|
|
1389
2343
|
} catch (error) {
|
|
1390
2344
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2345
|
+
if (this.stopped && (this.remoteStopInfo || isSessionClosedError(error))) {
|
|
2346
|
+
this.copilotLog(
|
|
2347
|
+
`turn interrupted by stop_task replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt}`,
|
|
2348
|
+
);
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
1391
2351
|
this.copilotLog(
|
|
1392
2352
|
`turn failed replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} error="${sanitizeForLog(errorMessage, 200)}"`,
|
|
1393
2353
|
);
|
|
@@ -1400,11 +2360,48 @@ class BridgeRunner {
|
|
|
1400
2360
|
},
|
|
1401
2361
|
replyTo,
|
|
1402
2362
|
);
|
|
1403
|
-
|
|
2363
|
+
const isExecutionFailure = this.isExecutionFailureLoopError(errorMessage);
|
|
2364
|
+
const loopState = isExecutionFailure
|
|
2365
|
+
? this.evaluateErrorLoop(errorMessage)
|
|
2366
|
+
: {
|
|
2367
|
+
key: "non_execution_error",
|
|
2368
|
+
count: 1,
|
|
2369
|
+
open: false,
|
|
2370
|
+
suppressReport: false,
|
|
2371
|
+
cooldownMs: 0,
|
|
2372
|
+
};
|
|
2373
|
+
if (!isExecutionFailure) {
|
|
2374
|
+
this.resetErrorLoop();
|
|
2375
|
+
}
|
|
2376
|
+
const executionFailureLoop = isExecutionFailure && loopState.open;
|
|
2377
|
+
if (executionFailureLoop) {
|
|
2378
|
+
await this.reportRuntimeStatus(
|
|
2379
|
+
{
|
|
2380
|
+
state: "ERROR",
|
|
2381
|
+
phase: "execution_failure_loop",
|
|
2382
|
+
reply_in_progress: false,
|
|
2383
|
+
status_done_line: `${this.backendName} execution_failure_loop`,
|
|
2384
|
+
},
|
|
2385
|
+
replyTo,
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
if (isExecutionFailure && loopState.suppressReport) {
|
|
2389
|
+
this.copilotLog(
|
|
2390
|
+
`suppress repeated error report key=${loopState.key} count=${loopState.count} cooldownMs=${loopState.cooldownMs}`,
|
|
2391
|
+
);
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
const reportMessage = executionFailureLoop
|
|
2395
|
+
? `${this.backendName} 执行层失败循环(${loopState.key}, 连续${loopState.count}次): ${errorMessage}`
|
|
2396
|
+
: `${this.backendName} 处理失败: ${errorMessage}`;
|
|
2397
|
+
await this.reportError(reportMessage, replyTo);
|
|
1404
2398
|
} finally {
|
|
1405
2399
|
if (turnWatchdog) {
|
|
1406
2400
|
clearInterval(turnWatchdog);
|
|
1407
2401
|
}
|
|
2402
|
+
if (replyTo) {
|
|
2403
|
+
this.inFlightMessageIds.delete(replyTo);
|
|
2404
|
+
}
|
|
1408
2405
|
this.copilotLog(
|
|
1409
2406
|
`turn end replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} processedIds=${this.processedMessageIds.size}`,
|
|
1410
2407
|
);
|
|
@@ -1446,6 +2443,10 @@ class BridgeRunner {
|
|
|
1446
2443
|
});
|
|
1447
2444
|
this.copilotLog(`synthetic sdk_message sent responseLen=${text.length}`);
|
|
1448
2445
|
} catch (error) {
|
|
2446
|
+
if (this.stopped && (this.remoteStopInfo || isSessionClosedError(error))) {
|
|
2447
|
+
this.copilotLog(`synthetic turn interrupted by stop_task elapsedMs=${Date.now() - startedAt}`);
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
1449
2450
|
this.copilotLog(
|
|
1450
2451
|
`synthetic turn failed elapsedMs=${Date.now() - startedAt} error="${sanitizeForLog(error?.message || error, 200)}"`,
|
|
1451
2452
|
);
|
|
@@ -1519,6 +2520,22 @@ function logBackendReply(backend, text, { usage, replyTo }) {
|
|
|
1519
2520
|
log(`${backend} reply (${replyTo}): ${text}${usageSuffix}`);
|
|
1520
2521
|
}
|
|
1521
2522
|
|
|
2523
|
+
export function formatFatalError(error, opts = {}) {
|
|
2524
|
+
const showStack =
|
|
2525
|
+
typeof opts.showStack === "boolean"
|
|
2526
|
+
? opts.showStack
|
|
2527
|
+
: isTruthyEnv(process.env.CONDUCTOR_CLI_SHOW_STACK) ||
|
|
2528
|
+
isTruthyEnv(process.env.CONDUCTOR_DEBUG);
|
|
2529
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2530
|
+
if (!showStack) {
|
|
2531
|
+
return message;
|
|
2532
|
+
}
|
|
2533
|
+
if (error instanceof Error && typeof error.stack === "string" && error.stack.trim()) {
|
|
2534
|
+
return error.stack;
|
|
2535
|
+
}
|
|
2536
|
+
return message;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
1522
2539
|
function isDirectRun() {
|
|
1523
2540
|
try {
|
|
1524
2541
|
if (!process.argv[1]) {
|
|
@@ -1535,7 +2552,7 @@ function isDirectRun() {
|
|
|
1535
2552
|
if (isDirectRun()) {
|
|
1536
2553
|
main().catch((error) => {
|
|
1537
2554
|
const ts = new Date().toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" }).replace(" ", "T");
|
|
1538
|
-
const line = `[${CLI_NAME} ${ts}] failed: ${error
|
|
2555
|
+
const line = `[${CLI_NAME} ${ts}] failed: ${formatFatalError(error)}\n`;
|
|
1539
2556
|
process.stderr.write(line);
|
|
1540
2557
|
appendFireLocalLog(line);
|
|
1541
2558
|
process.exitCode = 1;
|