@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.
@@ -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
  "",
@@ -92,13 +92,7 @@ function pidFromLockFile(lockFile) {
92
92
 
93
93
  const args = yargs(argv)
94
94
  .scriptName(CLI_NAME)
95
- .usage("Usage: $0 [--name <daemon-name>] [--clean-all] [--config-file <path>] [--nohup] [--force]")
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(task?.agent_host || task?.agentHost);
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)}${
@@ -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(cliArgs.resumeSessionId),
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
- await backendSession.close();
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, ...this.args].join(" ")} (cwd: ${this.cwd})`);
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(this.args)}`,
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: this.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
- this.history.push({ role: "user", content: promptText });
1527
- if (answer) {
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
- this.emitProgress(onProgress, {
1532
- state: result.success ? "DONE" : "ERROR",
1533
- phase: "turn_result",
1534
- source: "tui-driver",
1535
- reply_in_progress: false,
1536
- status_line: result.statusLine || undefined,
1537
- status_done_line: result.statusDoneLine || undefined,
1538
- reply_preview: truncateText(result.replyText || answer, 240) || undefined,
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 sessionId = String(sessionInfo?.sessionId || "").trim();
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: `${this.backendName} session started`,
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
- // With durable web outbox enabled, user messages are replayed by the server.
1870
- // Re-running DB-history backfill here can re-drive stale prompts and confuse
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
- await this.reportRuntimeStatus(
2293
- {
2294
- state: "PREPARE_TURN",
2295
- phase: "start_turn",
2296
- reply_in_progress: true,
2297
- status_line: `${this.backendName} is preparing the response`,
2298
- },
2299
- replyTo,
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
- await this.reportRuntimeStatus(
2314
- {
2315
- state: "DONE",
2316
- phase: "reply_ready",
2317
- reply_in_progress: false,
2318
- status_done_line: `${this.backendName} finished`,
2319
- },
2320
- replyTo,
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
- const responseText =
2324
- result.text ||
2325
- extractAgentTextFromItems(result.items) ||
2326
- extractAgentTextFromMetadata(result.metadata) ||
2327
- `(${this.backendName} 未返回任何文本)`;
2328
- logBackendReply(this.backendName, responseText, { usage: result.usage, replyTo: replyTo || "latest" });
2329
- await this.conductor.sendMessage(this.taskId, responseText, {
2330
- model: this.backendSession.threadOptions?.model || this.backendName,
2331
- backend: this.backendName,
2332
- usage: result.usage || null,
2333
- thread_id: this.backendSession.threadId,
2334
- items: result.items,
2335
- reply_to: replyTo,
2336
- cli_args: this.cliArgs,
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.copilotLog(`sdk_message sent replyTo=${replyTo || "latest"} responseLen=${responseText.length}`);
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
- const backendLabel = this.backendName.charAt(0).toUpperCase() + this.backendName.slice(1);
2428
- const intro = `${backendLabel} 已根据初始提示给出回复:`;
2429
- const replyText =
2430
- result.text || extractAgentTextFromItems(result.items) || extractAgentTextFromMetadata(result.metadata);
2431
- const text = replyText ? `${intro}\n\n${replyText}` : intro;
2432
- logBackendReply(this.backendName, replyText || "(无文本输出)", {
2433
- usage: result.usage,
2434
- replyTo: "initial",
2435
- });
2436
- await this.conductor.sendMessage(this.taskId, text, {
2437
- model: this.backendSession.threadOptions?.model || this.backendName,
2438
- backend: this.backendName,
2439
- usage: result.usage || null,
2440
- thread_id: this.backendSession.threadId,
2441
- cli_args: this.cliArgs,
2442
- synthetic: true,
2443
- });
2444
- this.copilotLog(`synthetic sdk_message sent responseLen=${text.length}`);
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.13",
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.13",
20
- "@love-moon/conductor-sdk": "0.2.13",
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 AGENT_NAME = (config.NAME || process.env.CONDUCTOR_DAEMON_NAME || os.hostname()).trim();
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 --name or CONDUCTOR_DAEMON_NAME.");
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";