@love-moon/conductor-cli 0.2.12 → 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 +370 -0
- package/bin/conductor-fire.js +1065 -95
- package/bin/conductor.js +5 -1
- package/package.json +3 -3
- package/src/daemon.js +11 -2
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";
|
|
@@ -103,6 +104,95 @@ const DEFAULT_POLL_INTERVAL_MS = parseInt(
|
|
|
103
104
|
process.env.CONDUCTOR_CLI_POLL_INTERVAL_MS || process.env.CCODEX_POLL_INTERVAL_MS || "2000",
|
|
104
105
|
10,
|
|
105
106
|
);
|
|
107
|
+
const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
|
|
108
|
+
const MIN_TURN_DEADLINE_MS = 30 * 1000;
|
|
109
|
+
const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
|
|
110
|
+
const DEFAULT_ERROR_LOOP_WINDOW_MS = 2 * 60 * 1000;
|
|
111
|
+
const DEFAULT_ERROR_LOOP_BACKOFF_MS = 3 * 60 * 1000;
|
|
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
|
+
}
|
|
106
196
|
|
|
107
197
|
function appendFireLocalLog(line) {
|
|
108
198
|
if (!ENABLE_FIRE_LOCAL_LOG) {
|
|
@@ -118,6 +208,7 @@ function appendFireLocalLog(line) {
|
|
|
118
208
|
async function main() {
|
|
119
209
|
const cliArgs = parseCliArgs();
|
|
120
210
|
let runtimeProjectPath = process.cwd();
|
|
211
|
+
let backendSession = null;
|
|
121
212
|
|
|
122
213
|
if (cliArgs.showHelp) {
|
|
123
214
|
return;
|
|
@@ -155,16 +246,6 @@ async function main() {
|
|
|
155
246
|
log(`Switched working directory to ${runtimeProjectPath} before Conductor connect`);
|
|
156
247
|
}
|
|
157
248
|
|
|
158
|
-
// Create backend session using tui-driver
|
|
159
|
-
const backendSession = new TuiDriverSession(cliArgs.backend, {
|
|
160
|
-
initialImages: cliArgs.initialImages,
|
|
161
|
-
cwd: runtimeProjectPath,
|
|
162
|
-
resumeSessionId: cliArgs.resumeSessionId,
|
|
163
|
-
configFile: cliArgs.configFile,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
log(`Using backend: ${cliArgs.backend}`);
|
|
167
|
-
|
|
168
249
|
const env = buildEnv();
|
|
169
250
|
const launchedByDaemon = Boolean(process.env.CONDUCTOR_CLI_COMMAND);
|
|
170
251
|
let reconnectRunner = null;
|
|
@@ -224,6 +305,7 @@ async function main() {
|
|
|
224
305
|
if (cliArgs.configFile) {
|
|
225
306
|
env.CONDUCTOR_CONFIG = cliArgs.configFile;
|
|
226
307
|
}
|
|
308
|
+
let configuredDaemonName = "";
|
|
227
309
|
try {
|
|
228
310
|
const config = loadConfig(cliArgs.configFile);
|
|
229
311
|
if (config.backendUrl && !env.CONDUCTOR_BACKEND_URL) {
|
|
@@ -235,6 +317,9 @@ async function main() {
|
|
|
235
317
|
if (config.agentToken && !env.CONDUCTOR_AGENT_TOKEN) {
|
|
236
318
|
env.CONDUCTOR_AGENT_TOKEN = config.agentToken;
|
|
237
319
|
}
|
|
320
|
+
if (config.daemonName) {
|
|
321
|
+
configuredDaemonName = config.daemonName;
|
|
322
|
+
}
|
|
238
323
|
} catch (error) {
|
|
239
324
|
// Ignore config loading errors, rely on env vars or defaults
|
|
240
325
|
}
|
|
@@ -261,6 +346,7 @@ async function main() {
|
|
|
261
346
|
providedTaskId: process.env.CONDUCTOR_TASK_ID,
|
|
262
347
|
requestedTitle: requestedTaskTitle,
|
|
263
348
|
backend: cliArgs.backend,
|
|
349
|
+
daemonName: configuredDaemonName,
|
|
264
350
|
});
|
|
265
351
|
|
|
266
352
|
log(
|
|
@@ -270,6 +356,48 @@ async function main() {
|
|
|
270
356
|
);
|
|
271
357
|
reconnectTaskId = taskContext.taskId;
|
|
272
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
|
+
|
|
273
401
|
try {
|
|
274
402
|
await conductor.sendAgentResume({
|
|
275
403
|
active_tasks: [taskContext.taskId],
|
|
@@ -289,7 +417,8 @@ async function main() {
|
|
|
289
417
|
includeInitialImages: Boolean(cliArgs.initialPrompt && cliArgs.initialImages.length),
|
|
290
418
|
cliArgs: cliArgs.rawBackendArgs,
|
|
291
419
|
backendName: cliArgs.backend,
|
|
292
|
-
resumeMode: Boolean(
|
|
420
|
+
resumeMode: Boolean(resolvedResumeSessionId),
|
|
421
|
+
daemonName: configuredDaemonName,
|
|
293
422
|
});
|
|
294
423
|
reconnectRunner = runner;
|
|
295
424
|
if (pendingRemoteStopEvent) {
|
|
@@ -307,7 +436,9 @@ async function main() {
|
|
|
307
436
|
backendShutdownRequested = true;
|
|
308
437
|
void (async () => {
|
|
309
438
|
try {
|
|
310
|
-
|
|
439
|
+
if (backendSession && typeof backendSession.close === "function") {
|
|
440
|
+
await backendSession.close();
|
|
441
|
+
}
|
|
311
442
|
} catch (error) {
|
|
312
443
|
log(`Failed to close backend session after ${source}: ${error?.message || error}`);
|
|
313
444
|
}
|
|
@@ -338,6 +469,11 @@ async function main() {
|
|
|
338
469
|
|
|
339
470
|
let runnerError = null;
|
|
340
471
|
try {
|
|
472
|
+
if (!resolvedResumeSessionId && String(cliArgs.backend).trim().toLowerCase() === "codex") {
|
|
473
|
+
await withFreshSessionBootstrapLock(cliArgs.backend, runtimeProjectPath, async () => {
|
|
474
|
+
await runner.announceBackendSession();
|
|
475
|
+
});
|
|
476
|
+
}
|
|
341
477
|
await runner.start(signals.signal);
|
|
342
478
|
} catch (error) {
|
|
343
479
|
runnerError = error;
|
|
@@ -379,7 +515,7 @@ async function main() {
|
|
|
379
515
|
}
|
|
380
516
|
}
|
|
381
517
|
} finally {
|
|
382
|
-
if (typeof backendSession.close === "function") {
|
|
518
|
+
if (backendSession && typeof backendSession.close === "function") {
|
|
383
519
|
try {
|
|
384
520
|
await backendSession.close();
|
|
385
521
|
} catch (error) {
|
|
@@ -667,6 +803,9 @@ async function ensureTaskContext(conductor, opts) {
|
|
|
667
803
|
task_title: deriveTaskTitle(opts.initialPrompt, opts.requestedTitle, opts.backend),
|
|
668
804
|
backend_type: opts.backend,
|
|
669
805
|
};
|
|
806
|
+
if (opts.daemonName) {
|
|
807
|
+
payload.daemon_name = opts.daemonName;
|
|
808
|
+
}
|
|
670
809
|
if (opts.initialPrompt) {
|
|
671
810
|
payload.prefill = opts.initialPrompt;
|
|
672
811
|
}
|
|
@@ -1027,6 +1166,39 @@ function sanitizeForLog(value, maxLen = 180) {
|
|
|
1027
1166
|
return truncateText(String(value).replace(/\s+/g, " ").trim(), maxLen);
|
|
1028
1167
|
}
|
|
1029
1168
|
|
|
1169
|
+
function getBoundedEnvInt(envName, fallback, min, max) {
|
|
1170
|
+
const fallbackNumber = Number(fallback);
|
|
1171
|
+
const normalizedFallback = Number.isFinite(fallbackNumber)
|
|
1172
|
+
? Math.min(Math.max(Math.round(fallbackNumber), min), max)
|
|
1173
|
+
: min;
|
|
1174
|
+
const raw = process.env[envName];
|
|
1175
|
+
const parsed = Number.parseInt(String(raw ?? ""), 10);
|
|
1176
|
+
if (!Number.isFinite(parsed)) {
|
|
1177
|
+
return normalizedFallback;
|
|
1178
|
+
}
|
|
1179
|
+
return Math.min(Math.max(parsed, min), max);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function normalizeExecutionErrorKey(errorMessage) {
|
|
1183
|
+
const normalized = sanitizeForLog(errorMessage, 280).toLowerCase();
|
|
1184
|
+
if (!normalized) {
|
|
1185
|
+
return "unknown_error";
|
|
1186
|
+
}
|
|
1187
|
+
if (normalized.includes("pty session already spawned")) {
|
|
1188
|
+
return "pty_session_already_spawned";
|
|
1189
|
+
}
|
|
1190
|
+
if (normalized.includes("tui process has exited")) {
|
|
1191
|
+
return "tui_process_exited";
|
|
1192
|
+
}
|
|
1193
|
+
if (normalized.includes("cannot proceed: tui process has exited")) {
|
|
1194
|
+
return "cannot_proceed_tui_exited";
|
|
1195
|
+
}
|
|
1196
|
+
if (normalized.includes("turn exceeded hard deadline")) {
|
|
1197
|
+
return "turn_timeout";
|
|
1198
|
+
}
|
|
1199
|
+
return normalized;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1030
1202
|
function tailLines(value, count = 6) {
|
|
1031
1203
|
if (!value) return "";
|
|
1032
1204
|
const lines = String(value).split(/\r?\n/);
|
|
@@ -1045,6 +1217,7 @@ class TuiDriverSession {
|
|
|
1045
1217
|
this.sessionId = resumeSessionId || `${backend}-${Date.now()}`;
|
|
1046
1218
|
this.history = Array.isArray(options.initialHistory) ? [...options.initialHistory] : [];
|
|
1047
1219
|
this.pendingHistorySeed = this.history.length > 0;
|
|
1220
|
+
this.sessionInfo = null;
|
|
1048
1221
|
|
|
1049
1222
|
const allowCliList = options.configFile ? loadAllowCliList(options.configFile) : DEFAULT_ALLOW_CLI_LIST;
|
|
1050
1223
|
const cliCommand = CUSTOM_CLI_COMMAND || allowCliList[backend] || backend;
|
|
@@ -1066,11 +1239,51 @@ class TuiDriverSession {
|
|
|
1066
1239
|
this.closeRequested = false;
|
|
1067
1240
|
this.closed = false;
|
|
1068
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 = "";
|
|
1256
|
+
this.turnDeadlineMs = getBoundedEnvInt(
|
|
1257
|
+
"CONDUCTOR_TURN_DEADLINE_MS",
|
|
1258
|
+
DEFAULT_TURN_DEADLINE_MS,
|
|
1259
|
+
MIN_TURN_DEADLINE_MS,
|
|
1260
|
+
MAX_TURN_DEADLINE_MS,
|
|
1261
|
+
);
|
|
1069
1262
|
|
|
1070
1263
|
const profileName = profileNameForBackend(backend);
|
|
1071
1264
|
if (!profileName) {
|
|
1072
1265
|
throw new Error(`Backend "${backend}" is not supported by tui-driver`);
|
|
1073
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
|
+
);
|
|
1074
1287
|
|
|
1075
1288
|
const profileMap = {
|
|
1076
1289
|
codex: codexProfile,
|
|
@@ -1078,6 +1291,7 @@ class TuiDriverSession {
|
|
|
1078
1291
|
copilot: copilotProfile,
|
|
1079
1292
|
};
|
|
1080
1293
|
const baseProfile = profileMap[profileName];
|
|
1294
|
+
const effectiveDriverArgs = [...(baseProfile.args || []), ...this.args];
|
|
1081
1295
|
const envConfig = loadEnvConfig(options.configFile);
|
|
1082
1296
|
const proxyEnv = proxyToEnv(envConfig);
|
|
1083
1297
|
const cliEnv = envConfig && typeof envConfig === "object" ? { ...envConfig, ...proxyEnv } : proxyEnv;
|
|
@@ -1086,10 +1300,10 @@ class TuiDriverSession {
|
|
|
1086
1300
|
log(`Using proxy: ${proxyEnv.http_proxy || proxyEnv.https_proxy || proxyEnv.all_proxy}`);
|
|
1087
1301
|
}
|
|
1088
1302
|
|
|
1089
|
-
log(`Using TUI command for ${backend}: ${[this.command, ...
|
|
1303
|
+
log(`Using TUI command for ${backend}: ${[this.command, ...effectiveDriverArgs].join(" ")} (cwd: ${this.cwd})`);
|
|
1090
1304
|
if (this.tuiTrace) {
|
|
1091
1305
|
log(
|
|
1092
|
-
`[${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)}`,
|
|
1093
1307
|
);
|
|
1094
1308
|
log(
|
|
1095
1309
|
`[${this.backend}] [tui-trace] timeouts=${JSON.stringify(baseProfile.timeouts || {})} size=${baseProfile.cols || 120}x${baseProfile.rows || 40}`,
|
|
@@ -1100,7 +1314,7 @@ class TuiDriverSession {
|
|
|
1100
1314
|
profile: {
|
|
1101
1315
|
...baseProfile,
|
|
1102
1316
|
command: this.command,
|
|
1103
|
-
args:
|
|
1317
|
+
args: effectiveDriverArgs,
|
|
1104
1318
|
env: {
|
|
1105
1319
|
...process.env,
|
|
1106
1320
|
...(baseProfile.env || {}),
|
|
@@ -1108,6 +1322,7 @@ class TuiDriverSession {
|
|
|
1108
1322
|
},
|
|
1109
1323
|
},
|
|
1110
1324
|
cwd: this.cwd,
|
|
1325
|
+
expectedSessionId: resumeSessionId || undefined,
|
|
1111
1326
|
debug: this.tuiDebug,
|
|
1112
1327
|
onSnapshot: this.tuiTrace
|
|
1113
1328
|
? (snapshot, state) => {
|
|
@@ -1129,6 +1344,10 @@ class TuiDriverSession {
|
|
|
1129
1344
|
}
|
|
1130
1345
|
log(`[${this.backend}] [WARN] Please run "${this.command} login" or authenticate manually.`);
|
|
1131
1346
|
});
|
|
1347
|
+
|
|
1348
|
+
this.driver.on("session", (session) => {
|
|
1349
|
+
this.applySessionInfo(session);
|
|
1350
|
+
});
|
|
1132
1351
|
}
|
|
1133
1352
|
|
|
1134
1353
|
get threadId() {
|
|
@@ -1139,12 +1358,316 @@ class TuiDriverSession {
|
|
|
1139
1358
|
return { model: this.backend };
|
|
1140
1359
|
}
|
|
1141
1360
|
|
|
1361
|
+
applySessionInfo(session) {
|
|
1362
|
+
if (!session || typeof session !== "object") {
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const sessionId = typeof session.sessionId === "string" ? session.sessionId.trim() : "";
|
|
1366
|
+
const sessionFilePath =
|
|
1367
|
+
typeof session.sessionFilePath === "string" ? session.sessionFilePath.trim() : "";
|
|
1368
|
+
if (!sessionId) {
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
this.sessionId = sessionId;
|
|
1372
|
+
this.sessionInfo = {
|
|
1373
|
+
backend: this.backend,
|
|
1374
|
+
sessionId,
|
|
1375
|
+
sessionFilePath: sessionFilePath || undefined,
|
|
1376
|
+
};
|
|
1377
|
+
this.trace(
|
|
1378
|
+
`session id=${sessionId} file="${sanitizeForLog(sessionFilePath, 180)}"`,
|
|
1379
|
+
);
|
|
1380
|
+
if (this.useSessionFileReplyStream) {
|
|
1381
|
+
void this.ensureSessionFileMonitor();
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
getSessionInfo() {
|
|
1386
|
+
if (this.sessionInfo) {
|
|
1387
|
+
return { ...this.sessionInfo };
|
|
1388
|
+
}
|
|
1389
|
+
return null;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
async ensureSessionInfo() {
|
|
1393
|
+
if (!this.driver) {
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
1396
|
+
try {
|
|
1397
|
+
await this.driver.boot();
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
this.trace(`session boot failed: ${sanitizeForLog(error?.message || error, 180)}`);
|
|
1400
|
+
return this.getSessionInfo();
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
try {
|
|
1404
|
+
if (typeof this.driver.ensureSessionInfo === "function") {
|
|
1405
|
+
const detected = await this.driver.ensureSessionInfo();
|
|
1406
|
+
this.applySessionInfo(detected);
|
|
1407
|
+
} else if (typeof this.driver.getSessionInfo === "function") {
|
|
1408
|
+
this.applySessionInfo(this.driver.getSessionInfo());
|
|
1409
|
+
}
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
this.trace(`session detect failed: ${sanitizeForLog(error?.message || error, 180)}`);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
return this.getSessionInfo();
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
async getSessionUsageSummary() {
|
|
1418
|
+
if (!this.driver || typeof this.driver.getSessionUsageSummary !== "function") {
|
|
1419
|
+
return null;
|
|
1420
|
+
}
|
|
1421
|
+
try {
|
|
1422
|
+
const summary = await this.driver.getSessionUsageSummary();
|
|
1423
|
+
return summary && typeof summary === "object" ? summary : null;
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
this.trace(`session usage detect failed: ${sanitizeForLog(error?.message || error, 180)}`);
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
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
|
+
|
|
1142
1657
|
createSessionClosedError() {
|
|
1143
1658
|
const error = new Error("TUI session closed");
|
|
1144
1659
|
error.reason = "session_closed";
|
|
1145
1660
|
return error;
|
|
1146
1661
|
}
|
|
1147
1662
|
|
|
1663
|
+
createTurnTimeoutError(timeoutMs) {
|
|
1664
|
+
const seconds = Math.max(1, Math.round(timeoutMs / 1000));
|
|
1665
|
+
const error = new Error(`Turn exceeded hard deadline (${seconds}s)`);
|
|
1666
|
+
error.reason = "turn_timeout";
|
|
1667
|
+
error.timeoutMs = timeoutMs;
|
|
1668
|
+
return error;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1148
1671
|
createCloseGuard() {
|
|
1149
1672
|
if (this.closeRequested) {
|
|
1150
1673
|
return {
|
|
@@ -1169,6 +1692,32 @@ class TuiDriverSession {
|
|
|
1169
1692
|
};
|
|
1170
1693
|
}
|
|
1171
1694
|
|
|
1695
|
+
createTurnTimeoutGuard() {
|
|
1696
|
+
if (!Number.isFinite(this.turnDeadlineMs) || this.turnDeadlineMs <= 0) {
|
|
1697
|
+
return {
|
|
1698
|
+
promise: new Promise(() => {}),
|
|
1699
|
+
cleanup: () => {},
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
let timer = null;
|
|
1703
|
+
const promise = new Promise((_, reject) => {
|
|
1704
|
+
timer = setTimeout(() => {
|
|
1705
|
+
reject(this.createTurnTimeoutError(this.turnDeadlineMs));
|
|
1706
|
+
}, this.turnDeadlineMs);
|
|
1707
|
+
if (typeof timer.unref === "function") {
|
|
1708
|
+
timer.unref();
|
|
1709
|
+
}
|
|
1710
|
+
});
|
|
1711
|
+
return {
|
|
1712
|
+
promise,
|
|
1713
|
+
cleanup: () => {
|
|
1714
|
+
if (timer) {
|
|
1715
|
+
clearTimeout(timer);
|
|
1716
|
+
}
|
|
1717
|
+
},
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1172
1721
|
flushCloseWaiters() {
|
|
1173
1722
|
if (!this.closeWaiters || this.closeWaiters.size === 0) {
|
|
1174
1723
|
return;
|
|
@@ -1189,6 +1738,8 @@ class TuiDriverSession {
|
|
|
1189
1738
|
}
|
|
1190
1739
|
this.closed = true;
|
|
1191
1740
|
this.closeRequested = true;
|
|
1741
|
+
this.sessionMonitorStopRequested = true;
|
|
1742
|
+
this.workingStatusMonitorStopRequested = true;
|
|
1192
1743
|
this.flushCloseWaiters();
|
|
1193
1744
|
if (this.driver) {
|
|
1194
1745
|
this.driver.kill();
|
|
@@ -1310,9 +1861,13 @@ class TuiDriverSession {
|
|
|
1310
1861
|
|
|
1311
1862
|
log(`[${this.backend}] Running prompt: ${truncateText(effectivePrompt, 100)}...`);
|
|
1312
1863
|
this.trace(`runTurn start promptLen=${effectivePrompt.length} useInitialImages=${Boolean(useInitialImages)}`);
|
|
1864
|
+
const useSessionFileReplyStream = this.usesSessionFileReplyStream();
|
|
1313
1865
|
|
|
1314
1866
|
const handleStateChange = (transition) => {
|
|
1315
1867
|
this.trace(`state ${transition.from} -> ${transition.to}`);
|
|
1868
|
+
if (useSessionFileReplyStream) {
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1316
1871
|
this.emitProgress(onProgress, {
|
|
1317
1872
|
state: transition.to,
|
|
1318
1873
|
phase: "state_change",
|
|
@@ -1340,6 +1895,9 @@ class TuiDriverSession {
|
|
|
1340
1895
|
);
|
|
1341
1896
|
}
|
|
1342
1897
|
}
|
|
1898
|
+
if (useSessionFileReplyStream) {
|
|
1899
|
+
return;
|
|
1900
|
+
}
|
|
1343
1901
|
this.emitProgress(onProgress, {
|
|
1344
1902
|
state: this.driver.state,
|
|
1345
1903
|
phase: "signal_poll",
|
|
@@ -1364,28 +1922,38 @@ class TuiDriverSession {
|
|
|
1364
1922
|
}
|
|
1365
1923
|
}
|
|
1366
1924
|
const closeGuard = this.createCloseGuard();
|
|
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
|
+
}
|
|
1367
1931
|
|
|
1368
1932
|
try {
|
|
1369
|
-
const result = await Promise.race([this.driver.ask(effectivePrompt), closeGuard.promise]);
|
|
1933
|
+
const result = await Promise.race([this.driver.ask(effectivePrompt), closeGuard.promise, turnTimeoutGuard.promise]);
|
|
1370
1934
|
const answer = String(result.answer || result.replyText || "").trim();
|
|
1371
1935
|
this.trace(
|
|
1372
1936
|
`runTurn finished success=${Boolean(result.success)} elapsedMs=${result.elapsedMs} answerLen=${answer.length} state=${this.driver.state}`,
|
|
1373
1937
|
);
|
|
1374
1938
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1939
|
+
if (!useSessionFileReplyStream) {
|
|
1940
|
+
this.history.push({ role: "user", content: promptText });
|
|
1941
|
+
}
|
|
1942
|
+
if (answer && !useSessionFileReplyStream) {
|
|
1377
1943
|
this.history.push({ role: "assistant", content: answer });
|
|
1378
1944
|
}
|
|
1379
1945
|
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
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
|
+
}
|
|
1389
1957
|
|
|
1390
1958
|
if (!result.success) {
|
|
1391
1959
|
const error = result.error || new Error("tui-driver failed to complete this turn");
|
|
@@ -1415,8 +1983,23 @@ class TuiDriverSession {
|
|
|
1415
1983
|
throw error instanceof Error ? error : new Error(errorMessage);
|
|
1416
1984
|
}
|
|
1417
1985
|
|
|
1418
|
-
|
|
1419
|
-
|
|
1986
|
+
if (errorReason === "turn_timeout") {
|
|
1987
|
+
this.emitProgress(onProgress, {
|
|
1988
|
+
state: "ERROR",
|
|
1989
|
+
phase: "timeout_recovered",
|
|
1990
|
+
source: "tui-driver",
|
|
1991
|
+
error: errorMessage,
|
|
1992
|
+
reason: errorReason,
|
|
1993
|
+
timeout_ms: error?.timeoutMs,
|
|
1994
|
+
});
|
|
1995
|
+
log(`[${this.backend}] Turn timed out (${error?.timeoutMs || this.turnDeadlineMs}ms), restarting TUI session`);
|
|
1996
|
+
try {
|
|
1997
|
+
await this.driver.forceRestart();
|
|
1998
|
+
} catch (restartError) {
|
|
1999
|
+
log(`[${this.backend}] Failed to restart TUI after timeout: ${restartError?.message || restartError}`);
|
|
2000
|
+
}
|
|
2001
|
+
log(`[${this.backend}] Error: ${errorMessage}`);
|
|
2002
|
+
} else if (errorReason === "login_required") {
|
|
1420
2003
|
this.emitProgress(onProgress, {
|
|
1421
2004
|
state: "ERROR",
|
|
1422
2005
|
phase: "login_required",
|
|
@@ -1469,6 +2052,7 @@ class TuiDriverSession {
|
|
|
1469
2052
|
}
|
|
1470
2053
|
clearInterval(signalTimer);
|
|
1471
2054
|
this.driver.off("stateChange", handleStateChange);
|
|
2055
|
+
turnTimeoutGuard.cleanup();
|
|
1472
2056
|
closeGuard.cleanup();
|
|
1473
2057
|
this.trace(`runTurn cleanup state=${this.driver.state}`);
|
|
1474
2058
|
}
|
|
@@ -1486,6 +2070,7 @@ export class BridgeRunner {
|
|
|
1486
2070
|
cliArgs,
|
|
1487
2071
|
backendName,
|
|
1488
2072
|
resumeMode,
|
|
2073
|
+
daemonName,
|
|
1489
2074
|
}) {
|
|
1490
2075
|
this.backendSession = backendSession;
|
|
1491
2076
|
this.conductor = conductor;
|
|
@@ -1503,10 +2088,66 @@ export class BridgeRunner {
|
|
|
1503
2088
|
this.stopped = false;
|
|
1504
2089
|
this.runningTurn = false;
|
|
1505
2090
|
this.processedMessageIds = new Set();
|
|
2091
|
+
this.inFlightMessageIds = new Set();
|
|
1506
2092
|
this.lastRuntimeStatusSignature = null;
|
|
1507
2093
|
this.lastRuntimeStatusPayload = null;
|
|
2094
|
+
this.runtimeContextSnapshot = null;
|
|
2095
|
+
this.runtimeContextSnapshotAt = 0;
|
|
2096
|
+
this.runtimeContextInFlight = null;
|
|
2097
|
+
this.runtimeContextRefreshMs = getBoundedEnvInt(
|
|
2098
|
+
"CONDUCTOR_RUNTIME_CONTEXT_REFRESH_MS",
|
|
2099
|
+
2000,
|
|
2100
|
+
500,
|
|
2101
|
+
60 * 1000,
|
|
2102
|
+
);
|
|
2103
|
+
this.useSessionFileReplyStream =
|
|
2104
|
+
Boolean(this.backendSession) &&
|
|
2105
|
+
typeof this.backendSession.usesSessionFileReplyStream === "function" &&
|
|
2106
|
+
this.backendSession.usesSessionFileReplyStream();
|
|
2107
|
+
this.daemonName =
|
|
2108
|
+
(typeof daemonName === "string" && daemonName.trim()) ||
|
|
2109
|
+
(typeof process.env.CONDUCTOR_AGENT_NAME === "string" && process.env.CONDUCTOR_AGENT_NAME.trim()) ||
|
|
2110
|
+
(typeof process.env.CONDUCTOR_DAEMON_NAME === "string" && process.env.CONDUCTOR_DAEMON_NAME.trim()) ||
|
|
2111
|
+
(typeof process.env.HOSTNAME === "string" && process.env.HOSTNAME.trim()) ||
|
|
2112
|
+
os.hostname();
|
|
1508
2113
|
this.needsReconnectRecovery = false;
|
|
1509
2114
|
this.remoteStopInfo = null;
|
|
2115
|
+
this.sessionAnnouncementSent = false;
|
|
2116
|
+
this.errorLoop = null;
|
|
2117
|
+
this.errorLoopWindowMs = getBoundedEnvInt(
|
|
2118
|
+
"CONDUCTOR_ERROR_LOOP_WINDOW_MS",
|
|
2119
|
+
DEFAULT_ERROR_LOOP_WINDOW_MS,
|
|
2120
|
+
15 * 1000,
|
|
2121
|
+
30 * 60 * 1000,
|
|
2122
|
+
);
|
|
2123
|
+
this.errorLoopBackoffMs = getBoundedEnvInt(
|
|
2124
|
+
"CONDUCTOR_ERROR_LOOP_BACKOFF_MS",
|
|
2125
|
+
DEFAULT_ERROR_LOOP_BACKOFF_MS,
|
|
2126
|
+
15 * 1000,
|
|
2127
|
+
60 * 60 * 1000,
|
|
2128
|
+
);
|
|
2129
|
+
this.errorLoopThreshold = getBoundedEnvInt(
|
|
2130
|
+
"CONDUCTOR_ERROR_LOOP_THRESHOLD",
|
|
2131
|
+
DEFAULT_ERROR_LOOP_THRESHOLD,
|
|
2132
|
+
2,
|
|
2133
|
+
20,
|
|
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
|
+
}
|
|
1510
2151
|
}
|
|
1511
2152
|
|
|
1512
2153
|
copilotLog(message) {
|
|
@@ -1516,6 +2157,71 @@ export class BridgeRunner {
|
|
|
1516
2157
|
log(`[copilot-debug] task=${this.taskId} ${message}`);
|
|
1517
2158
|
}
|
|
1518
2159
|
|
|
2160
|
+
async announceBackendSession() {
|
|
2161
|
+
if (this.sessionAnnouncementSent) {
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
if (!this.backendSession || typeof this.backendSession.ensureSessionInfo !== "function") {
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
let sessionInfo = null;
|
|
2168
|
+
try {
|
|
2169
|
+
sessionInfo = await this.backendSession.ensureSessionInfo();
|
|
2170
|
+
} catch (error) {
|
|
2171
|
+
this.copilotLog(`session announce skipped: ${sanitizeForLog(error?.message || error, 160)}`);
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
const discoveredSessionId = String(sessionInfo?.sessionId || "").trim();
|
|
2175
|
+
const fallbackSessionId = this.resumeMode
|
|
2176
|
+
? String(this.backendSession.threadId || "").trim()
|
|
2177
|
+
: "";
|
|
2178
|
+
const sessionId = discoveredSessionId || fallbackSessionId;
|
|
2179
|
+
const sessionFilePath = sessionInfo?.sessionFilePath ? String(sessionInfo.sessionFilePath).trim() : "";
|
|
2180
|
+
const hasRealSessionId = Boolean(sessionId);
|
|
2181
|
+
const message = hasRealSessionId
|
|
2182
|
+
? `${this.backendName} session started: ${sessionId}`
|
|
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
|
+
}
|
|
2195
|
+
try {
|
|
2196
|
+
await this.conductor.sendMessage(this.taskId, message, {
|
|
2197
|
+
backend: this.backendName,
|
|
2198
|
+
thread_id: hasRealSessionId ? sessionId : undefined,
|
|
2199
|
+
session_id: hasRealSessionId ? sessionId : undefined,
|
|
2200
|
+
session_file_path: sessionFilePath || undefined,
|
|
2201
|
+
cli_args: this.cliArgs,
|
|
2202
|
+
synthetic: true,
|
|
2203
|
+
});
|
|
2204
|
+
this.sessionAnnouncementSent = true;
|
|
2205
|
+
this.copilotLog(hasRealSessionId ? `session announced id=${sessionId}` : "session announced without id");
|
|
2206
|
+
await this.reportRuntimeStatus(
|
|
2207
|
+
{
|
|
2208
|
+
state: this.useSessionFileReplyStream ? undefined : "WAIT_READY",
|
|
2209
|
+
phase: "session_started",
|
|
2210
|
+
source: "tui-driver",
|
|
2211
|
+
reply_in_progress: false,
|
|
2212
|
+
status_done_line: this.useSessionFileReplyStream
|
|
2213
|
+
? undefined
|
|
2214
|
+
: `${this.backendName} session started`,
|
|
2215
|
+
backend: this.backendName,
|
|
2216
|
+
thread_id: hasRealSessionId ? sessionId : undefined,
|
|
2217
|
+
},
|
|
2218
|
+
undefined,
|
|
2219
|
+
);
|
|
2220
|
+
} catch (error) {
|
|
2221
|
+
log(`Failed to send session announcement: ${error?.message || error}`);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
|
|
1519
2225
|
async start(abortSignal) {
|
|
1520
2226
|
abortSignal?.addEventListener("abort", () => {
|
|
1521
2227
|
this.stopped = true;
|
|
@@ -1524,6 +2230,11 @@ export class BridgeRunner {
|
|
|
1524
2230
|
return;
|
|
1525
2231
|
}
|
|
1526
2232
|
|
|
2233
|
+
await this.announceBackendSession();
|
|
2234
|
+
if (this.stopped) {
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
|
|
1527
2238
|
if (this.initialPrompt) {
|
|
1528
2239
|
this.copilotLog("processing initial prompt");
|
|
1529
2240
|
await this.handleSyntheticMessage(this.initialPrompt, {
|
|
@@ -1539,11 +2250,6 @@ export class BridgeRunner {
|
|
|
1539
2250
|
if (this.stopped) {
|
|
1540
2251
|
return;
|
|
1541
2252
|
}
|
|
1542
|
-
this.copilotLog("running startup backfill");
|
|
1543
|
-
await this.backfillPendingUserMessages();
|
|
1544
|
-
if (this.stopped) {
|
|
1545
|
-
return;
|
|
1546
|
-
}
|
|
1547
2253
|
|
|
1548
2254
|
while (!this.stopped) {
|
|
1549
2255
|
if (this.needsReconnectRecovery && !this.runningTurn) {
|
|
@@ -1611,10 +2317,8 @@ export class BridgeRunner {
|
|
|
1611
2317
|
}
|
|
1612
2318
|
this.needsReconnectRecovery = false;
|
|
1613
2319
|
log(`Recovering task ${this.taskId} after reconnect`);
|
|
1614
|
-
//
|
|
1615
|
-
//
|
|
1616
|
-
// the local TUI session after reconnect. Keep startup backfill, but disable
|
|
1617
|
-
// 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.
|
|
1618
2322
|
if (process.env.CONDUCTOR_FIRE_RECONNECT_BACKFILL === "1") {
|
|
1619
2323
|
await this.backfillPendingUserMessages();
|
|
1620
2324
|
}
|
|
@@ -1779,7 +2483,80 @@ export class BridgeRunner {
|
|
|
1779
2483
|
}
|
|
1780
2484
|
}
|
|
1781
2485
|
|
|
1782
|
-
|
|
2486
|
+
normalizePercent(value) {
|
|
2487
|
+
if (!Number.isFinite(value)) {
|
|
2488
|
+
return undefined;
|
|
2489
|
+
}
|
|
2490
|
+
if (value < 0) {
|
|
2491
|
+
return 0;
|
|
2492
|
+
}
|
|
2493
|
+
if (value > 100) {
|
|
2494
|
+
return 100;
|
|
2495
|
+
}
|
|
2496
|
+
return value;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
async resolveRuntimeContext() {
|
|
2500
|
+
const now = Date.now();
|
|
2501
|
+
if (
|
|
2502
|
+
this.runtimeContextSnapshot &&
|
|
2503
|
+
now - this.runtimeContextSnapshotAt < this.runtimeContextRefreshMs
|
|
2504
|
+
) {
|
|
2505
|
+
return this.runtimeContextSnapshot;
|
|
2506
|
+
}
|
|
2507
|
+
if (this.runtimeContextInFlight) {
|
|
2508
|
+
return this.runtimeContextInFlight;
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
this.runtimeContextInFlight = (async () => {
|
|
2512
|
+
let sessionInfo = null;
|
|
2513
|
+
try {
|
|
2514
|
+
if (typeof this.backendSession?.getSessionInfo === "function") {
|
|
2515
|
+
sessionInfo = this.backendSession.getSessionInfo();
|
|
2516
|
+
}
|
|
2517
|
+
} catch {
|
|
2518
|
+
sessionInfo = null;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
let usage = null;
|
|
2522
|
+
try {
|
|
2523
|
+
if (typeof this.backendSession?.getSessionUsageSummary === "function") {
|
|
2524
|
+
usage = await this.backendSession.getSessionUsageSummary();
|
|
2525
|
+
}
|
|
2526
|
+
} catch {
|
|
2527
|
+
usage = null;
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
const resolvedSessionId = String(
|
|
2531
|
+
usage?.sessionId || sessionInfo?.sessionId || "",
|
|
2532
|
+
).trim();
|
|
2533
|
+
const resolvedSessionFilePath = String(
|
|
2534
|
+
usage?.sessionFilePath || sessionInfo?.sessionFilePath || "",
|
|
2535
|
+
).trim();
|
|
2536
|
+
const tokenUsagePercent = this.normalizePercent(Number(usage?.tokenUsagePercent));
|
|
2537
|
+
const contextUsagePercent = this.normalizePercent(Number(usage?.contextUsagePercent));
|
|
2538
|
+
|
|
2539
|
+
const snapshot = {
|
|
2540
|
+
daemon: this.daemonName || undefined,
|
|
2541
|
+
pid: process.pid,
|
|
2542
|
+
session_id: resolvedSessionId || undefined,
|
|
2543
|
+
session_file_path: resolvedSessionFilePath || undefined,
|
|
2544
|
+
token_usage_percent: tokenUsagePercent,
|
|
2545
|
+
context_usage_percent: contextUsagePercent,
|
|
2546
|
+
};
|
|
2547
|
+
this.runtimeContextSnapshot = snapshot;
|
|
2548
|
+
this.runtimeContextSnapshotAt = Date.now();
|
|
2549
|
+
return snapshot;
|
|
2550
|
+
})();
|
|
2551
|
+
|
|
2552
|
+
try {
|
|
2553
|
+
return await this.runtimeContextInFlight;
|
|
2554
|
+
} finally {
|
|
2555
|
+
this.runtimeContextInFlight = null;
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
createRuntimeStatus(payload, replyTo, runtimeContext = null) {
|
|
1783
2560
|
if (!payload || typeof payload !== "object") {
|
|
1784
2561
|
return null;
|
|
1785
2562
|
}
|
|
@@ -1804,12 +2581,22 @@ export class BridgeRunner {
|
|
|
1804
2581
|
reply_preview: truncateText(replyPreview, 240) || undefined,
|
|
1805
2582
|
reply_to: replyTo,
|
|
1806
2583
|
backend: this.backendName,
|
|
1807
|
-
thread_id:
|
|
2584
|
+
thread_id:
|
|
2585
|
+
String(
|
|
2586
|
+
payload.thread_id || runtimeContext?.session_id || "",
|
|
2587
|
+
).trim() || undefined,
|
|
2588
|
+
daemon: runtimeContext?.daemon,
|
|
2589
|
+
pid: runtimeContext?.pid,
|
|
2590
|
+
session_id: runtimeContext?.session_id,
|
|
2591
|
+
session_file_path: runtimeContext?.session_file_path,
|
|
2592
|
+
token_usage_percent: runtimeContext?.token_usage_percent,
|
|
2593
|
+
context_usage_percent: runtimeContext?.context_usage_percent,
|
|
1808
2594
|
};
|
|
1809
2595
|
}
|
|
1810
2596
|
|
|
1811
2597
|
async reportRuntimeStatus(payload, replyTo) {
|
|
1812
|
-
const
|
|
2598
|
+
const runtimeContext = await this.resolveRuntimeContext();
|
|
2599
|
+
const runtime = this.createRuntimeStatus(payload, replyTo, runtimeContext);
|
|
1813
2600
|
if (!runtime) {
|
|
1814
2601
|
return;
|
|
1815
2602
|
}
|
|
@@ -1852,6 +2639,105 @@ export class BridgeRunner {
|
|
|
1852
2639
|
}
|
|
1853
2640
|
}
|
|
1854
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
|
+
|
|
2680
|
+
resetErrorLoop() {
|
|
2681
|
+
this.errorLoop = null;
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
evaluateErrorLoop(errorMessage) {
|
|
2685
|
+
const normalizedKey = normalizeExecutionErrorKey(errorMessage);
|
|
2686
|
+
const now = Date.now();
|
|
2687
|
+
const current = this.errorLoop;
|
|
2688
|
+
|
|
2689
|
+
if (
|
|
2690
|
+
!current ||
|
|
2691
|
+
current.key !== normalizedKey ||
|
|
2692
|
+
now - current.lastAt > this.errorLoopWindowMs
|
|
2693
|
+
) {
|
|
2694
|
+
this.errorLoop = {
|
|
2695
|
+
key: normalizedKey,
|
|
2696
|
+
count: 1,
|
|
2697
|
+
lastAt: now,
|
|
2698
|
+
cooldownUntil: 0,
|
|
2699
|
+
};
|
|
2700
|
+
return {
|
|
2701
|
+
key: normalizedKey,
|
|
2702
|
+
count: 1,
|
|
2703
|
+
open: false,
|
|
2704
|
+
suppressReport: false,
|
|
2705
|
+
cooldownMs: 0,
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
current.count += 1;
|
|
2710
|
+
current.lastAt = now;
|
|
2711
|
+
const open = current.count >= this.errorLoopThreshold;
|
|
2712
|
+
let suppressReport = false;
|
|
2713
|
+
|
|
2714
|
+
if (open) {
|
|
2715
|
+
if (current.cooldownUntil > now) {
|
|
2716
|
+
suppressReport = true;
|
|
2717
|
+
} else {
|
|
2718
|
+
current.cooldownUntil = now + this.errorLoopBackoffMs;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
this.errorLoop = current;
|
|
2723
|
+
return {
|
|
2724
|
+
key: normalizedKey,
|
|
2725
|
+
count: current.count,
|
|
2726
|
+
open,
|
|
2727
|
+
suppressReport,
|
|
2728
|
+
cooldownMs: suppressReport ? current.cooldownUntil - now : 0,
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
isExecutionFailureLoopError(errorMessage) {
|
|
2733
|
+
const normalized = String(errorMessage || "").toLowerCase();
|
|
2734
|
+
return (
|
|
2735
|
+
normalized.includes("pty session already spawned") ||
|
|
2736
|
+
normalized.includes("tui process has exited") ||
|
|
2737
|
+
normalized.includes("cannot proceed: tui process has exited")
|
|
2738
|
+
);
|
|
2739
|
+
}
|
|
2740
|
+
|
|
1855
2741
|
async respondToMessage(message) {
|
|
1856
2742
|
const content = String(message.content || "").trim();
|
|
1857
2743
|
if (!content) {
|
|
@@ -1863,6 +2749,19 @@ export class BridgeRunner {
|
|
|
1863
2749
|
this.copilotLog(`skip duplicated message replyTo=${replyTo}`);
|
|
1864
2750
|
return;
|
|
1865
2751
|
}
|
|
2752
|
+
if (replyTo && this.inFlightMessageIds.has(replyTo)) {
|
|
2753
|
+
this.copilotLog(`skip in-flight duplicated message replyTo=${replyTo}`);
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
if (replyTo) {
|
|
2757
|
+
this.inFlightMessageIds.add(replyTo);
|
|
2758
|
+
}
|
|
2759
|
+
if (
|
|
2760
|
+
this.useSessionFileReplyStream &&
|
|
2761
|
+
typeof this.backendSession?.setSessionReplyTarget === "function"
|
|
2762
|
+
) {
|
|
2763
|
+
this.backendSession.setSessionReplyTarget(replyTo);
|
|
2764
|
+
}
|
|
1866
2765
|
this.lastRuntimeStatusSignature = null;
|
|
1867
2766
|
this.runningTurn = true;
|
|
1868
2767
|
const turnStartedAt = Date.now();
|
|
@@ -1883,15 +2782,25 @@ export class BridgeRunner {
|
|
|
1883
2782
|
);
|
|
1884
2783
|
log(`Processing message ${replyTo} (${message.role})`);
|
|
1885
2784
|
try {
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
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
|
+
}
|
|
1895
2804
|
|
|
1896
2805
|
const result = await this.backendSession.runTurn(content, {
|
|
1897
2806
|
onProgress: (payload) => {
|
|
@@ -1904,35 +2813,49 @@ export class BridgeRunner {
|
|
|
1904
2813
|
).trim().length} items=${Array.isArray(result.items) ? result.items.length : 0}`,
|
|
1905
2814
|
);
|
|
1906
2815
|
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
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
|
+
}
|
|
1916
2827
|
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
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
|
+
}
|
|
1932
2845
|
if (replyTo) {
|
|
1933
2846
|
this.processedMessageIds.add(replyTo);
|
|
1934
2847
|
}
|
|
1935
|
-
this.
|
|
2848
|
+
this.resetErrorLoop();
|
|
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
|
+
}
|
|
1936
2859
|
} catch (error) {
|
|
1937
2860
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1938
2861
|
if (this.stopped && (this.remoteStopInfo || isSessionClosedError(error))) {
|
|
@@ -1953,11 +2876,48 @@ export class BridgeRunner {
|
|
|
1953
2876
|
},
|
|
1954
2877
|
replyTo,
|
|
1955
2878
|
);
|
|
1956
|
-
|
|
2879
|
+
const isExecutionFailure = this.isExecutionFailureLoopError(errorMessage);
|
|
2880
|
+
const loopState = isExecutionFailure
|
|
2881
|
+
? this.evaluateErrorLoop(errorMessage)
|
|
2882
|
+
: {
|
|
2883
|
+
key: "non_execution_error",
|
|
2884
|
+
count: 1,
|
|
2885
|
+
open: false,
|
|
2886
|
+
suppressReport: false,
|
|
2887
|
+
cooldownMs: 0,
|
|
2888
|
+
};
|
|
2889
|
+
if (!isExecutionFailure) {
|
|
2890
|
+
this.resetErrorLoop();
|
|
2891
|
+
}
|
|
2892
|
+
const executionFailureLoop = isExecutionFailure && loopState.open;
|
|
2893
|
+
if (executionFailureLoop) {
|
|
2894
|
+
await this.reportRuntimeStatus(
|
|
2895
|
+
{
|
|
2896
|
+
state: "ERROR",
|
|
2897
|
+
phase: "execution_failure_loop",
|
|
2898
|
+
reply_in_progress: false,
|
|
2899
|
+
status_done_line: `${this.backendName} execution_failure_loop`,
|
|
2900
|
+
},
|
|
2901
|
+
replyTo,
|
|
2902
|
+
);
|
|
2903
|
+
}
|
|
2904
|
+
if (isExecutionFailure && loopState.suppressReport) {
|
|
2905
|
+
this.copilotLog(
|
|
2906
|
+
`suppress repeated error report key=${loopState.key} count=${loopState.count} cooldownMs=${loopState.cooldownMs}`,
|
|
2907
|
+
);
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
const reportMessage = executionFailureLoop
|
|
2911
|
+
? `${this.backendName} 执行层失败循环(${loopState.key}, 连续${loopState.count}次): ${errorMessage}`
|
|
2912
|
+
: `${this.backendName} 处理失败: ${errorMessage}`;
|
|
2913
|
+
await this.reportError(reportMessage, replyTo);
|
|
1957
2914
|
} finally {
|
|
1958
2915
|
if (turnWatchdog) {
|
|
1959
2916
|
clearInterval(turnWatchdog);
|
|
1960
2917
|
}
|
|
2918
|
+
if (replyTo) {
|
|
2919
|
+
this.inFlightMessageIds.delete(replyTo);
|
|
2920
|
+
}
|
|
1961
2921
|
this.copilotLog(
|
|
1962
2922
|
`turn end replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} processedIds=${this.processedMessageIds.size}`,
|
|
1963
2923
|
);
|
|
@@ -1969,6 +2929,12 @@ export class BridgeRunner {
|
|
|
1969
2929
|
this.lastRuntimeStatusSignature = null;
|
|
1970
2930
|
this.runningTurn = true;
|
|
1971
2931
|
const startedAt = Date.now();
|
|
2932
|
+
if (
|
|
2933
|
+
this.useSessionFileReplyStream &&
|
|
2934
|
+
typeof this.backendSession?.setSessionReplyTarget === "function"
|
|
2935
|
+
) {
|
|
2936
|
+
this.backendSession.setSessionReplyTarget("initial");
|
|
2937
|
+
}
|
|
1972
2938
|
this.copilotLog(`synthetic turn start includeImages=${Boolean(includeImages)} contentLen=${String(content || "").length}`);
|
|
1973
2939
|
try {
|
|
1974
2940
|
const result = await this.backendSession.runTurn(content, {
|
|
@@ -1980,24 +2946,28 @@ export class BridgeRunner {
|
|
|
1980
2946
|
this.copilotLog(
|
|
1981
2947
|
`synthetic runTurn completed elapsedMs=${Date.now() - startedAt} answerLen=${String(result.text || "").trim().length}`,
|
|
1982
2948
|
);
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
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
|
+
}
|
|
2001
2971
|
} catch (error) {
|
|
2002
2972
|
if (this.stopped && (this.remoteStopInfo || isSessionClosedError(error))) {
|
|
2003
2973
|
this.copilotLog(`synthetic turn interrupted by stop_task elapsedMs=${Date.now() - startedAt}`);
|