@love-moon/conductor-cli 0.2.13 → 0.2.14
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 +2 -0
- package/bin/conductor-daemon.js +2 -12
- package/bin/conductor-diagnose.js +5 -1
- package/bin/conductor-fire.js +617 -91
- package/package.json +3 -3
- package/src/daemon.js +11 -2
package/bin/conductor-config.js
CHANGED
|
@@ -51,6 +51,7 @@ const backendUrl =
|
|
|
51
51
|
process.env.CONDUCTOR_BACKEND_URL ||
|
|
52
52
|
process.env.BACKEND_URL ||
|
|
53
53
|
"https://conductor-ai.top";
|
|
54
|
+
const defaultDaemonName = os.hostname() || "my-daemon";
|
|
54
55
|
|
|
55
56
|
// ANSI 颜色代码
|
|
56
57
|
const COLORS = {
|
|
@@ -157,6 +158,7 @@ async function main() {
|
|
|
157
158
|
const lines = [
|
|
158
159
|
`agent_token: ${yamlQuote(token)}`,
|
|
159
160
|
`backend_url: ${yamlQuote(backendUrl)}`,
|
|
161
|
+
`daemon_name: ${yamlQuote(defaultDaemonName)}`,
|
|
160
162
|
"log_level: debug",
|
|
161
163
|
"workspace: '~/ws/fires'",
|
|
162
164
|
"",
|
package/bin/conductor-daemon.js
CHANGED
|
@@ -92,13 +92,7 @@ function pidFromLockFile(lockFile) {
|
|
|
92
92
|
|
|
93
93
|
const args = yargs(argv)
|
|
94
94
|
.scriptName(CLI_NAME)
|
|
95
|
-
.usage("Usage: $0 [--
|
|
96
|
-
.option("name", {
|
|
97
|
-
alias: "n",
|
|
98
|
-
type: "string",
|
|
99
|
-
demandOption: false,
|
|
100
|
-
describe: "Unique daemon name (used as agent host)",
|
|
101
|
-
})
|
|
95
|
+
.usage("Usage: $0 [--clean-all] [--config-file <path>] [--nohup] [--force]")
|
|
102
96
|
.option("nohup", {
|
|
103
97
|
type: "boolean",
|
|
104
98
|
default: false,
|
|
@@ -118,10 +112,7 @@ const args = yargs(argv)
|
|
|
118
112
|
type: "string",
|
|
119
113
|
describe: "Path to Conductor config file",
|
|
120
114
|
})
|
|
121
|
-
.example(
|
|
122
|
-
"$0 --config-file ~/.conductor/config.yaml --name agent-1",
|
|
123
|
-
"Use custom config file and daemon name",
|
|
124
|
-
)
|
|
115
|
+
.example("$0 --config-file ~/.conductor/config.yaml", "Run with daemon_name from config")
|
|
125
116
|
.example("$0 --nohup", "Run daemon in background with logfile")
|
|
126
117
|
.example("$0 --nohup --force", "Restart daemon in background by stopping the existing one")
|
|
127
118
|
.help()
|
|
@@ -155,7 +146,6 @@ if (args.nohup) {
|
|
|
155
146
|
}
|
|
156
147
|
|
|
157
148
|
startDaemon({
|
|
158
|
-
NAME: args.name,
|
|
159
149
|
CLEAN_ALL: args.cleanAll,
|
|
160
150
|
CONFIG_FILE: args.configFile,
|
|
161
151
|
FORCE: args.force,
|
|
@@ -120,7 +120,9 @@ async function runFallbackDiagnosis(baseUrl, token, taskId, timeoutMs) {
|
|
|
120
120
|
const pendingAgeMs = hasPendingUser && Number.isFinite(latestUserMs) ? Date.now() - latestUserMs : null;
|
|
121
121
|
const latestSdkFailureKey = detectExecutionFailureLoopKey(latestSdk?.content);
|
|
122
122
|
|
|
123
|
-
const assignedHost = cleanText(
|
|
123
|
+
const assignedHost = cleanText(
|
|
124
|
+
task?.execution_host || task?.executionHost || task?.agent_host || task?.agentHost,
|
|
125
|
+
);
|
|
124
126
|
const assignedConnected = Boolean(
|
|
125
127
|
assignedHost && agents.some((agent) => cleanText(agent?.host) === assignedHost),
|
|
126
128
|
);
|
|
@@ -139,6 +141,7 @@ async function runFallbackDiagnosis(baseUrl, token, taskId, timeoutMs) {
|
|
|
139
141
|
id: task?.id || taskId,
|
|
140
142
|
status: normalizeTaskStatus(task?.status),
|
|
141
143
|
agent_host: assignedHost || null,
|
|
144
|
+
execution_host: cleanText(task?.execution_host || task?.executionHost) || null,
|
|
142
145
|
},
|
|
143
146
|
messages: {
|
|
144
147
|
total: messages.length,
|
|
@@ -251,6 +254,7 @@ function printReport(taskId, report) {
|
|
|
251
254
|
process.stdout.write("Signals:\n");
|
|
252
255
|
process.stdout.write(`- task.status: ${String(task?.status || "unknown")}\n`);
|
|
253
256
|
process.stdout.write(`- task.agent_host: ${String(task?.agent_host || task?.agentHost || "n/a")}\n`);
|
|
257
|
+
process.stdout.write(`- task.execution_host: ${String(task?.execution_host || task?.executionHost || "n/a")}\n`);
|
|
254
258
|
process.stdout.write(`- bound_agent_host: ${String(realtime?.bound_agent_host || realtime?.boundAgentHost || "n/a")}\n`);
|
|
255
259
|
process.stdout.write(
|
|
256
260
|
`- pending user: ${Boolean(messages?.has_pending_user ?? messages?.hasPendingUser)}${
|
package/bin/conductor-fire.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import fs from "node:fs";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
11
12
|
import os from "node:os";
|
|
12
13
|
import path from "node:path";
|
|
13
14
|
import process from "node:process";
|
|
@@ -109,6 +110,89 @@ const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
|
|
|
109
110
|
const DEFAULT_ERROR_LOOP_WINDOW_MS = 2 * 60 * 1000;
|
|
110
111
|
const DEFAULT_ERROR_LOOP_BACKOFF_MS = 3 * 60 * 1000;
|
|
111
112
|
const DEFAULT_ERROR_LOOP_THRESHOLD = 3;
|
|
113
|
+
const SESSION_BOOTSTRAP_LOCK_TIMEOUT_MS = 15_000;
|
|
114
|
+
const SESSION_BOOTSTRAP_LOCK_RETRY_MS = 50;
|
|
115
|
+
|
|
116
|
+
function sleepSync(ms) {
|
|
117
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const buffer = new SharedArrayBuffer(4);
|
|
122
|
+
const arr = new Int32Array(buffer);
|
|
123
|
+
Atomics.wait(arr, 0, 0, ms);
|
|
124
|
+
} catch {
|
|
125
|
+
const startedAt = Date.now();
|
|
126
|
+
while (Date.now() - startedAt < ms) {
|
|
127
|
+
// busy wait fallback
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolveLockWorkingDirectory(workingDirectory) {
|
|
133
|
+
const normalizedPath =
|
|
134
|
+
typeof workingDirectory === "string" && workingDirectory.trim()
|
|
135
|
+
? path.resolve(workingDirectory.trim())
|
|
136
|
+
: process.cwd();
|
|
137
|
+
try {
|
|
138
|
+
return fs.realpathSync(normalizedPath);
|
|
139
|
+
} catch {
|
|
140
|
+
return normalizedPath;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function resolveFreshSessionBootstrapLockPath(backendName, workingDirectory) {
|
|
145
|
+
const normalizedBackend = String(backendName || "").trim().toLowerCase();
|
|
146
|
+
if (normalizedBackend !== "codex") {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const lockKey = `${normalizedBackend}:${resolveLockWorkingDirectory(workingDirectory)}`;
|
|
150
|
+
const digest = createHash("sha1").update(lockKey).digest("hex");
|
|
151
|
+
return path.join(os.homedir(), ".conductor", "locks", `session-bootstrap-${digest}.lock`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function acquireFileLock(lockPath) {
|
|
155
|
+
const startedAt = Date.now();
|
|
156
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
157
|
+
while (true) {
|
|
158
|
+
try {
|
|
159
|
+
const fd = fs.openSync(lockPath, "wx");
|
|
160
|
+
return () => {
|
|
161
|
+
try {
|
|
162
|
+
fs.closeSync(fd);
|
|
163
|
+
} catch {
|
|
164
|
+
// ignore close failure
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
fs.unlinkSync(lockPath);
|
|
168
|
+
} catch {
|
|
169
|
+
// ignore unlink failure
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (error?.code !== "EEXIST") {
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
if (Date.now() - startedAt > SESSION_BOOTSTRAP_LOCK_TIMEOUT_MS) {
|
|
177
|
+
throw new Error(`Timed out waiting for fresh session lock: ${lockPath}`);
|
|
178
|
+
}
|
|
179
|
+
sleepSync(SESSION_BOOTSTRAP_LOCK_RETRY_MS);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function withFreshSessionBootstrapLock(backendName, workingDirectory, fn) {
|
|
185
|
+
const lockPath = resolveFreshSessionBootstrapLockPath(backendName, workingDirectory);
|
|
186
|
+
if (!lockPath) {
|
|
187
|
+
return await fn();
|
|
188
|
+
}
|
|
189
|
+
const release = acquireFileLock(lockPath);
|
|
190
|
+
try {
|
|
191
|
+
return await fn();
|
|
192
|
+
} finally {
|
|
193
|
+
release();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
112
196
|
|
|
113
197
|
function appendFireLocalLog(line) {
|
|
114
198
|
if (!ENABLE_FIRE_LOCAL_LOG) {
|
|
@@ -124,6 +208,7 @@ function appendFireLocalLog(line) {
|
|
|
124
208
|
async function main() {
|
|
125
209
|
const cliArgs = parseCliArgs();
|
|
126
210
|
let runtimeProjectPath = process.cwd();
|
|
211
|
+
let backendSession = null;
|
|
127
212
|
|
|
128
213
|
if (cliArgs.showHelp) {
|
|
129
214
|
return;
|
|
@@ -161,16 +246,6 @@ async function main() {
|
|
|
161
246
|
log(`Switched working directory to ${runtimeProjectPath} before Conductor connect`);
|
|
162
247
|
}
|
|
163
248
|
|
|
164
|
-
// Create backend session using tui-driver
|
|
165
|
-
const backendSession = new TuiDriverSession(cliArgs.backend, {
|
|
166
|
-
initialImages: cliArgs.initialImages,
|
|
167
|
-
cwd: runtimeProjectPath,
|
|
168
|
-
resumeSessionId: cliArgs.resumeSessionId,
|
|
169
|
-
configFile: cliArgs.configFile,
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
log(`Using backend: ${cliArgs.backend}`);
|
|
173
|
-
|
|
174
249
|
const env = buildEnv();
|
|
175
250
|
const launchedByDaemon = Boolean(process.env.CONDUCTOR_CLI_COMMAND);
|
|
176
251
|
let reconnectRunner = null;
|
|
@@ -230,6 +305,7 @@ async function main() {
|
|
|
230
305
|
if (cliArgs.configFile) {
|
|
231
306
|
env.CONDUCTOR_CONFIG = cliArgs.configFile;
|
|
232
307
|
}
|
|
308
|
+
let configuredDaemonName = "";
|
|
233
309
|
try {
|
|
234
310
|
const config = loadConfig(cliArgs.configFile);
|
|
235
311
|
if (config.backendUrl && !env.CONDUCTOR_BACKEND_URL) {
|
|
@@ -241,6 +317,9 @@ async function main() {
|
|
|
241
317
|
if (config.agentToken && !env.CONDUCTOR_AGENT_TOKEN) {
|
|
242
318
|
env.CONDUCTOR_AGENT_TOKEN = config.agentToken;
|
|
243
319
|
}
|
|
320
|
+
if (config.daemonName) {
|
|
321
|
+
configuredDaemonName = config.daemonName;
|
|
322
|
+
}
|
|
244
323
|
} catch (error) {
|
|
245
324
|
// Ignore config loading errors, rely on env vars or defaults
|
|
246
325
|
}
|
|
@@ -267,6 +346,7 @@ async function main() {
|
|
|
267
346
|
providedTaskId: process.env.CONDUCTOR_TASK_ID,
|
|
268
347
|
requestedTitle: requestedTaskTitle,
|
|
269
348
|
backend: cliArgs.backend,
|
|
349
|
+
daemonName: configuredDaemonName,
|
|
270
350
|
});
|
|
271
351
|
|
|
272
352
|
log(
|
|
@@ -276,6 +356,48 @@ async function main() {
|
|
|
276
356
|
);
|
|
277
357
|
reconnectTaskId = taskContext.taskId;
|
|
278
358
|
|
|
359
|
+
if (typeof conductor.bindTaskSession === "function") {
|
|
360
|
+
try {
|
|
361
|
+
await conductor.bindTaskSession(taskContext.taskId, {
|
|
362
|
+
project_id: process.env.CONDUCTOR_PROJECT_ID,
|
|
363
|
+
project_path: runtimeProjectPath,
|
|
364
|
+
backend_type: cliArgs.backend,
|
|
365
|
+
});
|
|
366
|
+
} catch {
|
|
367
|
+
// best effort only
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let resolvedResumeSessionId = cliArgs.resumeSessionId;
|
|
372
|
+
if (!resolvedResumeSessionId) {
|
|
373
|
+
try {
|
|
374
|
+
const localTaskRecord = await conductor.getLocalTaskRecord({
|
|
375
|
+
task_id: taskContext.taskId,
|
|
376
|
+
});
|
|
377
|
+
const taskSessionFilePath = typeof localTaskRecord?.session_file_path === "string"
|
|
378
|
+
? localTaskRecord.session_file_path.trim()
|
|
379
|
+
: "";
|
|
380
|
+
const taskSessionId = typeof localTaskRecord?.session_id === "string"
|
|
381
|
+
? localTaskRecord.session_id.trim()
|
|
382
|
+
: "";
|
|
383
|
+
if (taskSessionId && taskSessionFilePath) {
|
|
384
|
+
resolvedResumeSessionId = taskSessionId;
|
|
385
|
+
log(`Using bound session ${taskSessionId} for task ${taskContext.taskId}`);
|
|
386
|
+
}
|
|
387
|
+
} catch {
|
|
388
|
+
// no local task binding yet; start a fresh backend session
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
backendSession = new TuiDriverSession(cliArgs.backend, {
|
|
393
|
+
initialImages: cliArgs.initialImages,
|
|
394
|
+
cwd: runtimeProjectPath,
|
|
395
|
+
resumeSessionId: resolvedResumeSessionId,
|
|
396
|
+
configFile: cliArgs.configFile,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
log(`Using backend: ${cliArgs.backend}`);
|
|
400
|
+
|
|
279
401
|
try {
|
|
280
402
|
await conductor.sendAgentResume({
|
|
281
403
|
active_tasks: [taskContext.taskId],
|
|
@@ -295,7 +417,8 @@ async function main() {
|
|
|
295
417
|
includeInitialImages: Boolean(cliArgs.initialPrompt && cliArgs.initialImages.length),
|
|
296
418
|
cliArgs: cliArgs.rawBackendArgs,
|
|
297
419
|
backendName: cliArgs.backend,
|
|
298
|
-
resumeMode: Boolean(
|
|
420
|
+
resumeMode: Boolean(resolvedResumeSessionId),
|
|
421
|
+
daemonName: configuredDaemonName,
|
|
299
422
|
});
|
|
300
423
|
reconnectRunner = runner;
|
|
301
424
|
if (pendingRemoteStopEvent) {
|
|
@@ -313,7 +436,9 @@ async function main() {
|
|
|
313
436
|
backendShutdownRequested = true;
|
|
314
437
|
void (async () => {
|
|
315
438
|
try {
|
|
316
|
-
|
|
439
|
+
if (backendSession && typeof backendSession.close === "function") {
|
|
440
|
+
await backendSession.close();
|
|
441
|
+
}
|
|
317
442
|
} catch (error) {
|
|
318
443
|
log(`Failed to close backend session after ${source}: ${error?.message || error}`);
|
|
319
444
|
}
|
|
@@ -344,6 +469,11 @@ async function main() {
|
|
|
344
469
|
|
|
345
470
|
let runnerError = null;
|
|
346
471
|
try {
|
|
472
|
+
if (!resolvedResumeSessionId && String(cliArgs.backend).trim().toLowerCase() === "codex") {
|
|
473
|
+
await withFreshSessionBootstrapLock(cliArgs.backend, runtimeProjectPath, async () => {
|
|
474
|
+
await runner.announceBackendSession();
|
|
475
|
+
});
|
|
476
|
+
}
|
|
347
477
|
await runner.start(signals.signal);
|
|
348
478
|
} catch (error) {
|
|
349
479
|
runnerError = error;
|
|
@@ -385,7 +515,7 @@ async function main() {
|
|
|
385
515
|
}
|
|
386
516
|
}
|
|
387
517
|
} finally {
|
|
388
|
-
if (typeof backendSession.close === "function") {
|
|
518
|
+
if (backendSession && typeof backendSession.close === "function") {
|
|
389
519
|
try {
|
|
390
520
|
await backendSession.close();
|
|
391
521
|
} catch (error) {
|
|
@@ -673,6 +803,9 @@ async function ensureTaskContext(conductor, opts) {
|
|
|
673
803
|
task_title: deriveTaskTitle(opts.initialPrompt, opts.requestedTitle, opts.backend),
|
|
674
804
|
backend_type: opts.backend,
|
|
675
805
|
};
|
|
806
|
+
if (opts.daemonName) {
|
|
807
|
+
payload.daemon_name = opts.daemonName;
|
|
808
|
+
}
|
|
676
809
|
if (opts.initialPrompt) {
|
|
677
810
|
payload.prefill = opts.initialPrompt;
|
|
678
811
|
}
|
|
@@ -1106,6 +1239,20 @@ class TuiDriverSession {
|
|
|
1106
1239
|
this.closeRequested = false;
|
|
1107
1240
|
this.closed = false;
|
|
1108
1241
|
this.closeWaiters = new Set();
|
|
1242
|
+
this.sessionMessageHandler = null;
|
|
1243
|
+
this.sessionMonitorPromise = null;
|
|
1244
|
+
this.sessionMonitorStopRequested = false;
|
|
1245
|
+
this.sessionMonitorCursor = 0;
|
|
1246
|
+
this.sessionMonitorSessionId = "";
|
|
1247
|
+
this.sessionMonitorSessionFilePath = "";
|
|
1248
|
+
this.sessionMonitorActiveReplyTo = "";
|
|
1249
|
+
this.sessionMonitorHasActiveReplyTarget = false;
|
|
1250
|
+
this.sessionMonitorLastReplyTo = "";
|
|
1251
|
+
this.sessionMonitorAwaitingFirstReply = false;
|
|
1252
|
+
this.workingStatusHandler = null;
|
|
1253
|
+
this.workingStatusMonitorPromise = null;
|
|
1254
|
+
this.workingStatusMonitorStopRequested = false;
|
|
1255
|
+
this.lastReportedWorkingStatusLine = "";
|
|
1109
1256
|
this.turnDeadlineMs = getBoundedEnvInt(
|
|
1110
1257
|
"CONDUCTOR_TURN_DEADLINE_MS",
|
|
1111
1258
|
DEFAULT_TURN_DEADLINE_MS,
|
|
@@ -1117,6 +1264,26 @@ class TuiDriverSession {
|
|
|
1117
1264
|
if (!profileName) {
|
|
1118
1265
|
throw new Error(`Backend "${backend}" is not supported by tui-driver`);
|
|
1119
1266
|
}
|
|
1267
|
+
this.useSessionFileReplyStream =
|
|
1268
|
+
profileName === "codex" || profileName === "copilot";
|
|
1269
|
+
this.sessionMonitorFastPollMs = getBoundedEnvInt(
|
|
1270
|
+
"CONDUCTOR_CODEX_SESSION_FAST_POLL_MS",
|
|
1271
|
+
700,
|
|
1272
|
+
100,
|
|
1273
|
+
10 * 1000,
|
|
1274
|
+
);
|
|
1275
|
+
this.sessionMonitorSlowPollMs = getBoundedEnvInt(
|
|
1276
|
+
"CONDUCTOR_CODEX_SESSION_SLOW_POLL_MS",
|
|
1277
|
+
2500,
|
|
1278
|
+
300,
|
|
1279
|
+
60 * 1000,
|
|
1280
|
+
);
|
|
1281
|
+
this.workingStatusPollMs = getBoundedEnvInt(
|
|
1282
|
+
"CONDUCTOR_CODEX_TUI_STATUS_POLL_MS",
|
|
1283
|
+
700,
|
|
1284
|
+
100,
|
|
1285
|
+
10 * 1000,
|
|
1286
|
+
);
|
|
1120
1287
|
|
|
1121
1288
|
const profileMap = {
|
|
1122
1289
|
codex: codexProfile,
|
|
@@ -1124,6 +1291,7 @@ class TuiDriverSession {
|
|
|
1124
1291
|
copilot: copilotProfile,
|
|
1125
1292
|
};
|
|
1126
1293
|
const baseProfile = profileMap[profileName];
|
|
1294
|
+
const effectiveDriverArgs = [...(baseProfile.args || []), ...this.args];
|
|
1127
1295
|
const envConfig = loadEnvConfig(options.configFile);
|
|
1128
1296
|
const proxyEnv = proxyToEnv(envConfig);
|
|
1129
1297
|
const cliEnv = envConfig && typeof envConfig === "object" ? { ...envConfig, ...proxyEnv } : proxyEnv;
|
|
@@ -1132,10 +1300,10 @@ class TuiDriverSession {
|
|
|
1132
1300
|
log(`Using proxy: ${proxyEnv.http_proxy || proxyEnv.https_proxy || proxyEnv.all_proxy}`);
|
|
1133
1301
|
}
|
|
1134
1302
|
|
|
1135
|
-
log(`Using TUI command for ${backend}: ${[this.command, ...
|
|
1303
|
+
log(`Using TUI command for ${backend}: ${[this.command, ...effectiveDriverArgs].join(" ")} (cwd: ${this.cwd})`);
|
|
1136
1304
|
if (this.tuiTrace) {
|
|
1137
1305
|
log(
|
|
1138
|
-
`[${this.backend}] [tui-trace] profile=${baseProfile.name} command=${this.command} args=${JSON.stringify(
|
|
1306
|
+
`[${this.backend}] [tui-trace] profile=${baseProfile.name} command=${this.command} args=${JSON.stringify(effectiveDriverArgs)}`,
|
|
1139
1307
|
);
|
|
1140
1308
|
log(
|
|
1141
1309
|
`[${this.backend}] [tui-trace] timeouts=${JSON.stringify(baseProfile.timeouts || {})} size=${baseProfile.cols || 120}x${baseProfile.rows || 40}`,
|
|
@@ -1146,7 +1314,7 @@ class TuiDriverSession {
|
|
|
1146
1314
|
profile: {
|
|
1147
1315
|
...baseProfile,
|
|
1148
1316
|
command: this.command,
|
|
1149
|
-
args:
|
|
1317
|
+
args: effectiveDriverArgs,
|
|
1150
1318
|
env: {
|
|
1151
1319
|
...process.env,
|
|
1152
1320
|
...(baseProfile.env || {}),
|
|
@@ -1154,6 +1322,7 @@ class TuiDriverSession {
|
|
|
1154
1322
|
},
|
|
1155
1323
|
},
|
|
1156
1324
|
cwd: this.cwd,
|
|
1325
|
+
expectedSessionId: resumeSessionId || undefined,
|
|
1157
1326
|
debug: this.tuiDebug,
|
|
1158
1327
|
onSnapshot: this.tuiTrace
|
|
1159
1328
|
? (snapshot, state) => {
|
|
@@ -1208,6 +1377,9 @@ class TuiDriverSession {
|
|
|
1208
1377
|
this.trace(
|
|
1209
1378
|
`session id=${sessionId} file="${sanitizeForLog(sessionFilePath, 180)}"`,
|
|
1210
1379
|
);
|
|
1380
|
+
if (this.useSessionFileReplyStream) {
|
|
1381
|
+
void this.ensureSessionFileMonitor();
|
|
1382
|
+
}
|
|
1211
1383
|
}
|
|
1212
1384
|
|
|
1213
1385
|
getSessionInfo() {
|
|
@@ -1255,6 +1427,233 @@ class TuiDriverSession {
|
|
|
1255
1427
|
}
|
|
1256
1428
|
}
|
|
1257
1429
|
|
|
1430
|
+
usesSessionFileReplyStream() {
|
|
1431
|
+
return Boolean(this.useSessionFileReplyStream);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
setSessionMessageHandler(handler) {
|
|
1435
|
+
this.sessionMessageHandler = typeof handler === "function" ? handler : null;
|
|
1436
|
+
if (this.useSessionFileReplyStream) {
|
|
1437
|
+
void this.ensureSessionFileMonitor();
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
setWorkingStatusHandler(handler) {
|
|
1442
|
+
this.workingStatusHandler = typeof handler === "function" ? handler : null;
|
|
1443
|
+
if (this.useSessionFileReplyStream) {
|
|
1444
|
+
void this.ensureWorkingStatusMonitor();
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
setSessionReplyTarget(replyTo) {
|
|
1449
|
+
if (!this.useSessionFileReplyStream) {
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
const normalizedReplyTo = typeof replyTo === "string" ? replyTo.trim() : "";
|
|
1453
|
+
this.sessionMonitorActiveReplyTo = normalizedReplyTo;
|
|
1454
|
+
this.sessionMonitorHasActiveReplyTarget = true;
|
|
1455
|
+
if (normalizedReplyTo) {
|
|
1456
|
+
this.sessionMonitorLastReplyTo = normalizedReplyTo;
|
|
1457
|
+
}
|
|
1458
|
+
this.sessionMonitorAwaitingFirstReply = true;
|
|
1459
|
+
void this.ensureSessionFileMonitor();
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
async ensureSessionFileMonitor() {
|
|
1463
|
+
if (!this.useSessionFileReplyStream || this.sessionMonitorPromise) {
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
this.sessionMonitorStopRequested = false;
|
|
1467
|
+
const monitorPromise = this.runSessionFileMonitor();
|
|
1468
|
+
this.sessionMonitorPromise = monitorPromise;
|
|
1469
|
+
monitorPromise.catch((error) => {
|
|
1470
|
+
this.trace(`session monitor exited: ${sanitizeForLog(error?.message || error, 180)}`);
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
async ensureWorkingStatusMonitor() {
|
|
1475
|
+
if (!this.useSessionFileReplyStream || this.workingStatusMonitorPromise) {
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
this.workingStatusMonitorStopRequested = false;
|
|
1479
|
+
const monitorPromise = this.runWorkingStatusMonitor();
|
|
1480
|
+
this.workingStatusMonitorPromise = monitorPromise;
|
|
1481
|
+
monitorPromise.catch((error) => {
|
|
1482
|
+
this.trace(`working status monitor exited: ${sanitizeForLog(error?.message || error, 180)}`);
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
async runSessionFileMonitor() {
|
|
1487
|
+
try {
|
|
1488
|
+
while (!this.closeRequested && !this.sessionMonitorStopRequested) {
|
|
1489
|
+
try {
|
|
1490
|
+
await this.pollSessionFileMessages();
|
|
1491
|
+
} catch (error) {
|
|
1492
|
+
this.trace(`session monitor poll failed: ${sanitizeForLog(error?.message || error, 180)}`);
|
|
1493
|
+
}
|
|
1494
|
+
await delay(this.resolveSessionMonitorPollMs());
|
|
1495
|
+
}
|
|
1496
|
+
} finally {
|
|
1497
|
+
this.sessionMonitorPromise = null;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
async runWorkingStatusMonitor() {
|
|
1502
|
+
try {
|
|
1503
|
+
while (!this.closeRequested && !this.workingStatusMonitorStopRequested) {
|
|
1504
|
+
try {
|
|
1505
|
+
await this.pollWorkingStatus();
|
|
1506
|
+
} catch (error) {
|
|
1507
|
+
this.trace(`working status monitor poll failed: ${sanitizeForLog(error?.message || error, 180)}`);
|
|
1508
|
+
}
|
|
1509
|
+
await delay(this.workingStatusPollMs);
|
|
1510
|
+
}
|
|
1511
|
+
} finally {
|
|
1512
|
+
this.workingStatusMonitorPromise = null;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
resolveSessionMonitorPollMs() {
|
|
1517
|
+
return this.sessionMonitorAwaitingFirstReply
|
|
1518
|
+
? this.sessionMonitorFastPollMs
|
|
1519
|
+
: this.sessionMonitorSlowPollMs;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
normalizeCodexWorkingStatusLine(statusLine) {
|
|
1523
|
+
const normalized = typeof statusLine === "string" ? statusLine.trim() : "";
|
|
1524
|
+
if (!normalized) {
|
|
1525
|
+
return "";
|
|
1526
|
+
}
|
|
1527
|
+
if (/^\s*[•◦]\s*Working\b/i.test(normalized)) {
|
|
1528
|
+
return normalized;
|
|
1529
|
+
}
|
|
1530
|
+
if (/\bWorking\s*\([^)]*\)/i.test(normalized)) {
|
|
1531
|
+
return normalized;
|
|
1532
|
+
}
|
|
1533
|
+
return "";
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
normalizeCopilotWorkingStatusLine(statusLine) {
|
|
1537
|
+
const normalized = typeof statusLine === "string" ? statusLine.trim() : "";
|
|
1538
|
+
if (!normalized) {
|
|
1539
|
+
return "";
|
|
1540
|
+
}
|
|
1541
|
+
// Copilot busy lines are prefixed with spinner-like bullets and include "(Esc to cancel ...)".
|
|
1542
|
+
if (/^\s*[∙◉◎◐◑◒◓◔◕●○•◦]\s*.+Esc to cancel/i.test(normalized)) {
|
|
1543
|
+
return normalized;
|
|
1544
|
+
}
|
|
1545
|
+
if (/^\s*.+Esc to cancel/i.test(normalized)) {
|
|
1546
|
+
return normalized;
|
|
1547
|
+
}
|
|
1548
|
+
return "";
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
normalizeWorkingStatusLine(statusLine) {
|
|
1552
|
+
if (this.backend === "copilot") {
|
|
1553
|
+
return this.normalizeCopilotWorkingStatusLine(statusLine);
|
|
1554
|
+
}
|
|
1555
|
+
return this.normalizeCodexWorkingStatusLine(statusLine);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
getCurrentReplyTarget() {
|
|
1559
|
+
if (this.sessionMonitorHasActiveReplyTarget) {
|
|
1560
|
+
return this.sessionMonitorActiveReplyTo || undefined;
|
|
1561
|
+
}
|
|
1562
|
+
return this.sessionMonitorLastReplyTo || undefined;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
async pollWorkingStatus() {
|
|
1566
|
+
if (!this.useSessionFileReplyStream || !this.driver || !this.driver.running) {
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
const signals = this.driver.getSignals();
|
|
1570
|
+
const workingStatusLine = this.normalizeWorkingStatusLine(signals.statusLine);
|
|
1571
|
+
if (workingStatusLine === this.lastReportedWorkingStatusLine) {
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
this.lastReportedWorkingStatusLine = workingStatusLine;
|
|
1575
|
+
if (typeof this.workingStatusHandler !== "function") {
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
if (workingStatusLine) {
|
|
1579
|
+
await this.workingStatusHandler({
|
|
1580
|
+
phase: "working_status_monitor",
|
|
1581
|
+
source: "tui-driver",
|
|
1582
|
+
reply_in_progress: true,
|
|
1583
|
+
status_line: workingStatusLine,
|
|
1584
|
+
replyTo: this.getCurrentReplyTarget(),
|
|
1585
|
+
});
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
await this.workingStatusHandler({
|
|
1589
|
+
phase: "working_status_clear",
|
|
1590
|
+
source: "tui-driver",
|
|
1591
|
+
reply_in_progress: false,
|
|
1592
|
+
replyTo: this.getCurrentReplyTarget(),
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
async pollSessionFileMessages() {
|
|
1597
|
+
if (!this.useSessionFileReplyStream || !this.driver) {
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
const sessionInfo = this.getSessionInfo();
|
|
1601
|
+
if (!sessionInfo?.sessionId || !sessionInfo?.sessionFilePath) {
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
const sessionId = String(sessionInfo.sessionId).trim();
|
|
1606
|
+
const sessionFilePath = String(sessionInfo.sessionFilePath).trim();
|
|
1607
|
+
if (!sessionId || !sessionFilePath) {
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
const sessionChanged =
|
|
1612
|
+
sessionId !== this.sessionMonitorSessionId ||
|
|
1613
|
+
sessionFilePath !== this.sessionMonitorSessionFilePath;
|
|
1614
|
+
if (sessionChanged) {
|
|
1615
|
+
this.sessionMonitorSessionId = sessionId;
|
|
1616
|
+
this.sessionMonitorSessionFilePath = sessionFilePath;
|
|
1617
|
+
this.sessionMonitorCursor = this.sessionMonitorAwaitingFirstReply
|
|
1618
|
+
? 0
|
|
1619
|
+
: await this.driver.getSessionFileSize(sessionInfo);
|
|
1620
|
+
this.trace(
|
|
1621
|
+
`session monitor bound id=${sessionId} file="${sanitizeForLog(sessionFilePath, 180)}" cursor=${this.sessionMonitorCursor}`,
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
const batch = await this.driver.readSessionAssistantMessagesSince(
|
|
1626
|
+
sessionInfo,
|
|
1627
|
+
this.sessionMonitorCursor,
|
|
1628
|
+
);
|
|
1629
|
+
this.sessionMonitorCursor = Number.isFinite(batch?.nextOffset)
|
|
1630
|
+
? batch.nextOffset
|
|
1631
|
+
: this.sessionMonitorCursor;
|
|
1632
|
+
|
|
1633
|
+
const messages = Array.isArray(batch?.messages) ? batch.messages : [];
|
|
1634
|
+
if (messages.length === 0) {
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
for (const message of messages) {
|
|
1639
|
+
const text = typeof message?.text === "string" ? message.text.trim() : "";
|
|
1640
|
+
if (!text) {
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
this.history.push({ role: "assistant", content: text });
|
|
1644
|
+
this.sessionMonitorAwaitingFirstReply = false;
|
|
1645
|
+
if (typeof this.sessionMessageHandler !== "function") {
|
|
1646
|
+
continue;
|
|
1647
|
+
}
|
|
1648
|
+
await this.sessionMessageHandler({
|
|
1649
|
+
...message,
|
|
1650
|
+
replyTo: this.sessionMonitorHasActiveReplyTarget
|
|
1651
|
+
? this.sessionMonitorActiveReplyTo || undefined
|
|
1652
|
+
: this.sessionMonitorLastReplyTo || undefined,
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1258
1657
|
createSessionClosedError() {
|
|
1259
1658
|
const error = new Error("TUI session closed");
|
|
1260
1659
|
error.reason = "session_closed";
|
|
@@ -1339,6 +1738,8 @@ class TuiDriverSession {
|
|
|
1339
1738
|
}
|
|
1340
1739
|
this.closed = true;
|
|
1341
1740
|
this.closeRequested = true;
|
|
1741
|
+
this.sessionMonitorStopRequested = true;
|
|
1742
|
+
this.workingStatusMonitorStopRequested = true;
|
|
1342
1743
|
this.flushCloseWaiters();
|
|
1343
1744
|
if (this.driver) {
|
|
1344
1745
|
this.driver.kill();
|
|
@@ -1460,9 +1861,13 @@ class TuiDriverSession {
|
|
|
1460
1861
|
|
|
1461
1862
|
log(`[${this.backend}] Running prompt: ${truncateText(effectivePrompt, 100)}...`);
|
|
1462
1863
|
this.trace(`runTurn start promptLen=${effectivePrompt.length} useInitialImages=${Boolean(useInitialImages)}`);
|
|
1864
|
+
const useSessionFileReplyStream = this.usesSessionFileReplyStream();
|
|
1463
1865
|
|
|
1464
1866
|
const handleStateChange = (transition) => {
|
|
1465
1867
|
this.trace(`state ${transition.from} -> ${transition.to}`);
|
|
1868
|
+
if (useSessionFileReplyStream) {
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1466
1871
|
this.emitProgress(onProgress, {
|
|
1467
1872
|
state: transition.to,
|
|
1468
1873
|
phase: "state_change",
|
|
@@ -1490,6 +1895,9 @@ class TuiDriverSession {
|
|
|
1490
1895
|
);
|
|
1491
1896
|
}
|
|
1492
1897
|
}
|
|
1898
|
+
if (useSessionFileReplyStream) {
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1493
1901
|
this.emitProgress(onProgress, {
|
|
1494
1902
|
state: this.driver.state,
|
|
1495
1903
|
phase: "signal_poll",
|
|
@@ -1515,6 +1923,11 @@ class TuiDriverSession {
|
|
|
1515
1923
|
}
|
|
1516
1924
|
const closeGuard = this.createCloseGuard();
|
|
1517
1925
|
const turnTimeoutGuard = this.createTurnTimeoutGuard();
|
|
1926
|
+
if (useSessionFileReplyStream) {
|
|
1927
|
+
this.history.push({ role: "user", content: promptText });
|
|
1928
|
+
void this.ensureSessionFileMonitor();
|
|
1929
|
+
void this.ensureWorkingStatusMonitor();
|
|
1930
|
+
}
|
|
1518
1931
|
|
|
1519
1932
|
try {
|
|
1520
1933
|
const result = await Promise.race([this.driver.ask(effectivePrompt), closeGuard.promise, turnTimeoutGuard.promise]);
|
|
@@ -1523,20 +1936,24 @@ class TuiDriverSession {
|
|
|
1523
1936
|
`runTurn finished success=${Boolean(result.success)} elapsedMs=${result.elapsedMs} answerLen=${answer.length} state=${this.driver.state}`,
|
|
1524
1937
|
);
|
|
1525
1938
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1939
|
+
if (!useSessionFileReplyStream) {
|
|
1940
|
+
this.history.push({ role: "user", content: promptText });
|
|
1941
|
+
}
|
|
1942
|
+
if (answer && !useSessionFileReplyStream) {
|
|
1528
1943
|
this.history.push({ role: "assistant", content: answer });
|
|
1529
1944
|
}
|
|
1530
1945
|
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1946
|
+
if (!useSessionFileReplyStream) {
|
|
1947
|
+
this.emitProgress(onProgress, {
|
|
1948
|
+
state: result.success ? "DONE" : "ERROR",
|
|
1949
|
+
phase: "turn_result",
|
|
1950
|
+
source: "tui-driver",
|
|
1951
|
+
reply_in_progress: false,
|
|
1952
|
+
status_line: result.statusLine || undefined,
|
|
1953
|
+
status_done_line: result.statusDoneLine || undefined,
|
|
1954
|
+
reply_preview: truncateText(result.replyText || answer, 240) || undefined,
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1540
1957
|
|
|
1541
1958
|
if (!result.success) {
|
|
1542
1959
|
const error = result.error || new Error("tui-driver failed to complete this turn");
|
|
@@ -1653,6 +2070,7 @@ export class BridgeRunner {
|
|
|
1653
2070
|
cliArgs,
|
|
1654
2071
|
backendName,
|
|
1655
2072
|
resumeMode,
|
|
2073
|
+
daemonName,
|
|
1656
2074
|
}) {
|
|
1657
2075
|
this.backendSession = backendSession;
|
|
1658
2076
|
this.conductor = conductor;
|
|
@@ -1682,7 +2100,12 @@ export class BridgeRunner {
|
|
|
1682
2100
|
500,
|
|
1683
2101
|
60 * 1000,
|
|
1684
2102
|
);
|
|
2103
|
+
this.useSessionFileReplyStream =
|
|
2104
|
+
Boolean(this.backendSession) &&
|
|
2105
|
+
typeof this.backendSession.usesSessionFileReplyStream === "function" &&
|
|
2106
|
+
this.backendSession.usesSessionFileReplyStream();
|
|
1685
2107
|
this.daemonName =
|
|
2108
|
+
(typeof daemonName === "string" && daemonName.trim()) ||
|
|
1686
2109
|
(typeof process.env.CONDUCTOR_AGENT_NAME === "string" && process.env.CONDUCTOR_AGENT_NAME.trim()) ||
|
|
1687
2110
|
(typeof process.env.CONDUCTOR_DAEMON_NAME === "string" && process.env.CONDUCTOR_DAEMON_NAME.trim()) ||
|
|
1688
2111
|
(typeof process.env.HOSTNAME === "string" && process.env.HOSTNAME.trim()) ||
|
|
@@ -1709,6 +2132,22 @@ export class BridgeRunner {
|
|
|
1709
2132
|
2,
|
|
1710
2133
|
20,
|
|
1711
2134
|
);
|
|
2135
|
+
if (
|
|
2136
|
+
this.useSessionFileReplyStream &&
|
|
2137
|
+
typeof this.backendSession?.setSessionMessageHandler === "function"
|
|
2138
|
+
) {
|
|
2139
|
+
this.backendSession.setSessionMessageHandler((payload) =>
|
|
2140
|
+
this.handleSessionFileAssistantMessage(payload),
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
if (
|
|
2144
|
+
this.useSessionFileReplyStream &&
|
|
2145
|
+
typeof this.backendSession?.setWorkingStatusHandler === "function"
|
|
2146
|
+
) {
|
|
2147
|
+
this.backendSession.setWorkingStatusHandler((payload) =>
|
|
2148
|
+
this.handleContinuousWorkingStatus(payload),
|
|
2149
|
+
);
|
|
2150
|
+
}
|
|
1712
2151
|
}
|
|
1713
2152
|
|
|
1714
2153
|
copilotLog(message) {
|
|
@@ -1732,12 +2171,27 @@ export class BridgeRunner {
|
|
|
1732
2171
|
this.copilotLog(`session announce skipped: ${sanitizeForLog(error?.message || error, 160)}`);
|
|
1733
2172
|
return;
|
|
1734
2173
|
}
|
|
1735
|
-
const
|
|
2174
|
+
const discoveredSessionId = String(sessionInfo?.sessionId || "").trim();
|
|
2175
|
+
const fallbackSessionId = this.resumeMode
|
|
2176
|
+
? String(this.backendSession.threadId || "").trim()
|
|
2177
|
+
: "";
|
|
2178
|
+
const sessionId = discoveredSessionId || fallbackSessionId;
|
|
1736
2179
|
const sessionFilePath = sessionInfo?.sessionFilePath ? String(sessionInfo.sessionFilePath).trim() : "";
|
|
1737
2180
|
const hasRealSessionId = Boolean(sessionId);
|
|
1738
2181
|
const message = hasRealSessionId
|
|
1739
2182
|
? `${this.backendName} session started: ${sessionId}`
|
|
1740
2183
|
: `${this.backendName} session started`;
|
|
2184
|
+
if (hasRealSessionId && typeof this.conductor?.bindTaskSession === "function") {
|
|
2185
|
+
try {
|
|
2186
|
+
await this.conductor.bindTaskSession(this.taskId, {
|
|
2187
|
+
session_id: sessionId,
|
|
2188
|
+
session_file_path: sessionFilePath || undefined,
|
|
2189
|
+
backend_type: this.backendName,
|
|
2190
|
+
});
|
|
2191
|
+
} catch (error) {
|
|
2192
|
+
log(`Failed to persist task session binding for ${this.taskId}: ${error?.message || error}`);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
1741
2195
|
try {
|
|
1742
2196
|
await this.conductor.sendMessage(this.taskId, message, {
|
|
1743
2197
|
backend: this.backendName,
|
|
@@ -1751,11 +2205,13 @@ export class BridgeRunner {
|
|
|
1751
2205
|
this.copilotLog(hasRealSessionId ? `session announced id=${sessionId}` : "session announced without id");
|
|
1752
2206
|
await this.reportRuntimeStatus(
|
|
1753
2207
|
{
|
|
1754
|
-
state: "WAIT_READY",
|
|
2208
|
+
state: this.useSessionFileReplyStream ? undefined : "WAIT_READY",
|
|
1755
2209
|
phase: "session_started",
|
|
1756
2210
|
source: "tui-driver",
|
|
1757
2211
|
reply_in_progress: false,
|
|
1758
|
-
status_done_line:
|
|
2212
|
+
status_done_line: this.useSessionFileReplyStream
|
|
2213
|
+
? undefined
|
|
2214
|
+
: `${this.backendName} session started`,
|
|
1759
2215
|
backend: this.backendName,
|
|
1760
2216
|
thread_id: hasRealSessionId ? sessionId : undefined,
|
|
1761
2217
|
},
|
|
@@ -1794,11 +2250,6 @@ export class BridgeRunner {
|
|
|
1794
2250
|
if (this.stopped) {
|
|
1795
2251
|
return;
|
|
1796
2252
|
}
|
|
1797
|
-
this.copilotLog("running startup backfill");
|
|
1798
|
-
await this.backfillPendingUserMessages();
|
|
1799
|
-
if (this.stopped) {
|
|
1800
|
-
return;
|
|
1801
|
-
}
|
|
1802
2253
|
|
|
1803
2254
|
while (!this.stopped) {
|
|
1804
2255
|
if (this.needsReconnectRecovery && !this.runningTurn) {
|
|
@@ -1866,10 +2317,8 @@ export class BridgeRunner {
|
|
|
1866
2317
|
}
|
|
1867
2318
|
this.needsReconnectRecovery = false;
|
|
1868
2319
|
log(`Recovering task ${this.taskId} after reconnect`);
|
|
1869
|
-
//
|
|
1870
|
-
//
|
|
1871
|
-
// the local TUI session after reconnect. Keep startup backfill, but disable
|
|
1872
|
-
// reconnect backfill by default (opt-in for debugging/legacy fallback).
|
|
2320
|
+
// User-message recovery relies on the durable server outbox. DB-history replay
|
|
2321
|
+
// stays opt-in here only as a legacy/debugging fallback after reconnect.
|
|
1873
2322
|
if (process.env.CONDUCTOR_FIRE_RECONNECT_BACKFILL === "1") {
|
|
1874
2323
|
await this.backfillPendingUserMessages();
|
|
1875
2324
|
}
|
|
@@ -2190,6 +2639,44 @@ export class BridgeRunner {
|
|
|
2190
2639
|
}
|
|
2191
2640
|
}
|
|
2192
2641
|
|
|
2642
|
+
async handleSessionFileAssistantMessage(payload) {
|
|
2643
|
+
if (this.stopped) {
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
const text = typeof payload?.text === "string" ? payload.text.trim() : "";
|
|
2647
|
+
if (!text) {
|
|
2648
|
+
return;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
const replyTo = typeof payload?.replyTo === "string" ? payload.replyTo.trim() : "";
|
|
2652
|
+
const sessionId = typeof payload?.sessionId === "string" ? payload.sessionId.trim() : "";
|
|
2653
|
+
const sessionFilePath =
|
|
2654
|
+
typeof payload?.sessionFilePath === "string" ? payload.sessionFilePath.trim() : "";
|
|
2655
|
+
|
|
2656
|
+
logBackendReply(this.backendName, text, {
|
|
2657
|
+
usage: null,
|
|
2658
|
+
replyTo: replyTo || "latest",
|
|
2659
|
+
});
|
|
2660
|
+
await this.conductor.sendMessage(this.taskId, text, {
|
|
2661
|
+
model: this.backendSession.threadOptions?.model || this.backendName,
|
|
2662
|
+
backend: this.backendName,
|
|
2663
|
+
thread_id: sessionId || this.backendSession.threadId,
|
|
2664
|
+
session_id: sessionId || undefined,
|
|
2665
|
+
session_file_path: sessionFilePath || undefined,
|
|
2666
|
+
reply_to: replyTo || undefined,
|
|
2667
|
+
cli_args: this.cliArgs,
|
|
2668
|
+
session_stream: true,
|
|
2669
|
+
timestamp: payload?.timestamp || undefined,
|
|
2670
|
+
});
|
|
2671
|
+
this.copilotLog(
|
|
2672
|
+
`session_file sdk_message sent replyTo=${replyTo || "latest"} responseLen=${text.length}`,
|
|
2673
|
+
);
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
async handleContinuousWorkingStatus(payload) {
|
|
2677
|
+
await this.reportRuntimeStatus(payload, payload?.replyTo);
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2193
2680
|
resetErrorLoop() {
|
|
2194
2681
|
this.errorLoop = null;
|
|
2195
2682
|
}
|
|
@@ -2269,6 +2756,12 @@ export class BridgeRunner {
|
|
|
2269
2756
|
if (replyTo) {
|
|
2270
2757
|
this.inFlightMessageIds.add(replyTo);
|
|
2271
2758
|
}
|
|
2759
|
+
if (
|
|
2760
|
+
this.useSessionFileReplyStream &&
|
|
2761
|
+
typeof this.backendSession?.setSessionReplyTarget === "function"
|
|
2762
|
+
) {
|
|
2763
|
+
this.backendSession.setSessionReplyTarget(replyTo);
|
|
2764
|
+
}
|
|
2272
2765
|
this.lastRuntimeStatusSignature = null;
|
|
2273
2766
|
this.runningTurn = true;
|
|
2274
2767
|
const turnStartedAt = Date.now();
|
|
@@ -2289,15 +2782,25 @@ export class BridgeRunner {
|
|
|
2289
2782
|
);
|
|
2290
2783
|
log(`Processing message ${replyTo} (${message.role})`);
|
|
2291
2784
|
try {
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2785
|
+
if (this.useSessionFileReplyStream) {
|
|
2786
|
+
await this.reportRuntimeStatus(
|
|
2787
|
+
{
|
|
2788
|
+
phase: "start_turn",
|
|
2789
|
+
reply_in_progress: true,
|
|
2790
|
+
},
|
|
2791
|
+
replyTo,
|
|
2792
|
+
);
|
|
2793
|
+
} else {
|
|
2794
|
+
await this.reportRuntimeStatus(
|
|
2795
|
+
{
|
|
2796
|
+
state: "PREPARE_TURN",
|
|
2797
|
+
phase: "start_turn",
|
|
2798
|
+
reply_in_progress: true,
|
|
2799
|
+
status_line: `${this.backendName} is preparing the response`,
|
|
2800
|
+
},
|
|
2801
|
+
replyTo,
|
|
2802
|
+
);
|
|
2803
|
+
}
|
|
2301
2804
|
|
|
2302
2805
|
const result = await this.backendSession.runTurn(content, {
|
|
2303
2806
|
onProgress: (payload) => {
|
|
@@ -2310,36 +2813,49 @@ export class BridgeRunner {
|
|
|
2310
2813
|
).trim().length} items=${Array.isArray(result.items) ? result.items.length : 0}`,
|
|
2311
2814
|
);
|
|
2312
2815
|
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2816
|
+
if (!this.useSessionFileReplyStream) {
|
|
2817
|
+
await this.reportRuntimeStatus(
|
|
2818
|
+
{
|
|
2819
|
+
state: "DONE",
|
|
2820
|
+
phase: "reply_ready",
|
|
2821
|
+
reply_in_progress: false,
|
|
2822
|
+
status_done_line: `${this.backendName} finished`,
|
|
2823
|
+
},
|
|
2824
|
+
replyTo,
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2322
2827
|
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2828
|
+
if (!this.useSessionFileReplyStream) {
|
|
2829
|
+
const responseText =
|
|
2830
|
+
result.text ||
|
|
2831
|
+
extractAgentTextFromItems(result.items) ||
|
|
2832
|
+
extractAgentTextFromMetadata(result.metadata) ||
|
|
2833
|
+
`(${this.backendName} 未返回任何文本)`;
|
|
2834
|
+
logBackendReply(this.backendName, responseText, { usage: result.usage, replyTo: replyTo || "latest" });
|
|
2835
|
+
await this.conductor.sendMessage(this.taskId, responseText, {
|
|
2836
|
+
model: this.backendSession.threadOptions?.model || this.backendName,
|
|
2837
|
+
backend: this.backendName,
|
|
2838
|
+
usage: result.usage || null,
|
|
2839
|
+
thread_id: this.backendSession.threadId,
|
|
2840
|
+
items: result.items,
|
|
2841
|
+
reply_to: replyTo,
|
|
2842
|
+
cli_args: this.cliArgs,
|
|
2843
|
+
});
|
|
2844
|
+
}
|
|
2338
2845
|
if (replyTo) {
|
|
2339
2846
|
this.processedMessageIds.add(replyTo);
|
|
2340
2847
|
}
|
|
2341
2848
|
this.resetErrorLoop();
|
|
2342
|
-
this.
|
|
2849
|
+
if (this.useSessionFileReplyStream) {
|
|
2850
|
+
this.copilotLog(`session_file turn settled replyTo=${replyTo || "latest"}`);
|
|
2851
|
+
} else {
|
|
2852
|
+
const responseText =
|
|
2853
|
+
result.text ||
|
|
2854
|
+
extractAgentTextFromItems(result.items) ||
|
|
2855
|
+
extractAgentTextFromMetadata(result.metadata) ||
|
|
2856
|
+
`(${this.backendName} 未返回任何文本)`;
|
|
2857
|
+
this.copilotLog(`sdk_message sent replyTo=${replyTo || "latest"} responseLen=${responseText.length}`);
|
|
2858
|
+
}
|
|
2343
2859
|
} catch (error) {
|
|
2344
2860
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2345
2861
|
if (this.stopped && (this.remoteStopInfo || isSessionClosedError(error))) {
|
|
@@ -2413,6 +2929,12 @@ export class BridgeRunner {
|
|
|
2413
2929
|
this.lastRuntimeStatusSignature = null;
|
|
2414
2930
|
this.runningTurn = true;
|
|
2415
2931
|
const startedAt = Date.now();
|
|
2932
|
+
if (
|
|
2933
|
+
this.useSessionFileReplyStream &&
|
|
2934
|
+
typeof this.backendSession?.setSessionReplyTarget === "function"
|
|
2935
|
+
) {
|
|
2936
|
+
this.backendSession.setSessionReplyTarget("initial");
|
|
2937
|
+
}
|
|
2416
2938
|
this.copilotLog(`synthetic turn start includeImages=${Boolean(includeImages)} contentLen=${String(content || "").length}`);
|
|
2417
2939
|
try {
|
|
2418
2940
|
const result = await this.backendSession.runTurn(content, {
|
|
@@ -2424,24 +2946,28 @@ export class BridgeRunner {
|
|
|
2424
2946
|
this.copilotLog(
|
|
2425
2947
|
`synthetic runTurn completed elapsedMs=${Date.now() - startedAt} answerLen=${String(result.text || "").trim().length}`,
|
|
2426
2948
|
);
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2949
|
+
if (!this.useSessionFileReplyStream) {
|
|
2950
|
+
const backendLabel = this.backendName.charAt(0).toUpperCase() + this.backendName.slice(1);
|
|
2951
|
+
const intro = `${backendLabel} 已根据初始提示给出回复:`;
|
|
2952
|
+
const replyText =
|
|
2953
|
+
result.text || extractAgentTextFromItems(result.items) || extractAgentTextFromMetadata(result.metadata);
|
|
2954
|
+
const text = replyText ? `${intro}\n\n${replyText}` : intro;
|
|
2955
|
+
logBackendReply(this.backendName, replyText || "(无文本输出)", {
|
|
2956
|
+
usage: result.usage,
|
|
2957
|
+
replyTo: "initial",
|
|
2958
|
+
});
|
|
2959
|
+
await this.conductor.sendMessage(this.taskId, text, {
|
|
2960
|
+
model: this.backendSession.threadOptions?.model || this.backendName,
|
|
2961
|
+
backend: this.backendName,
|
|
2962
|
+
usage: result.usage || null,
|
|
2963
|
+
thread_id: this.backendSession.threadId,
|
|
2964
|
+
cli_args: this.cliArgs,
|
|
2965
|
+
synthetic: true,
|
|
2966
|
+
});
|
|
2967
|
+
this.copilotLog(`synthetic sdk_message sent responseLen=${text.length}`);
|
|
2968
|
+
} else {
|
|
2969
|
+
this.copilotLog("synthetic session_file turn settled");
|
|
2970
|
+
}
|
|
2445
2971
|
} catch (error) {
|
|
2446
2972
|
if (this.stopped && (this.remoteStopInfo || isSessionClosedError(error))) {
|
|
2447
2973
|
this.copilotLog(`synthetic turn interrupted by stop_task elapsedMs=${Date.now() - startedAt}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"conductor": "bin/conductor.js"
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"test": "node --test"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@love-moon/tui-driver": "0.2.
|
|
20
|
-
"@love-moon/conductor-sdk": "0.2.
|
|
19
|
+
"@love-moon/tui-driver": "0.2.14",
|
|
20
|
+
"@love-moon/conductor-sdk": "0.2.14",
|
|
21
21
|
"dotenv": "^16.4.5",
|
|
22
22
|
"enquirer": "^2.4.1",
|
|
23
23
|
"js-yaml": "^4.1.1",
|
package/src/daemon.js
CHANGED
|
@@ -173,9 +173,18 @@ export function startDaemon(config = {}, deps = {}) {
|
|
|
173
173
|
deriveWebsocketUrlFromHttp(BACKEND_HTTP);
|
|
174
174
|
const AGENT_TOKEN =
|
|
175
175
|
config.AGENT_TOKEN || process.env.CONDUCTOR_AGENT_TOKEN || fileConfig?.agentToken || "default-agent-token";
|
|
176
|
-
const
|
|
176
|
+
const configuredDaemonName =
|
|
177
|
+
(typeof userConfig.daemon_name === "string" && userConfig.daemon_name.trim()) ||
|
|
178
|
+
(typeof fileConfig?.daemonName === "string" && fileConfig.daemonName.trim()) ||
|
|
179
|
+
"";
|
|
180
|
+
const AGENT_NAME = (
|
|
181
|
+
config.DAEMON_NAME ||
|
|
182
|
+
configuredDaemonName ||
|
|
183
|
+
process.env.CONDUCTOR_DAEMON_NAME ||
|
|
184
|
+
os.hostname()
|
|
185
|
+
).trim();
|
|
177
186
|
if (!AGENT_NAME) {
|
|
178
|
-
logError("Daemon name is required. Set
|
|
187
|
+
logError("Daemon name is required. Set daemon_name in ~/.conductor/config.yaml or CONDUCTOR_DAEMON_NAME.");
|
|
179
188
|
return exitAndReturn(1);
|
|
180
189
|
}
|
|
181
190
|
const homeDir = process.env.HOME || os.homedir() || "/tmp";
|