@love-moon/conductor-cli 0.2.14 → 0.2.16
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-fire.js +122 -1209
- package/bin/conductor.js +3 -2
- package/package.json +6 -5
- package/src/fire/history.js +7 -109
- package/src/fire/resume.js +420 -0
package/bin/conductor-fire.js
CHANGED
|
@@ -12,16 +12,20 @@ import { createHash } from "node:crypto";
|
|
|
12
12
|
import os from "node:os";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import process from "node:process";
|
|
15
|
-
import readline from "node:readline";
|
|
16
15
|
import { setTimeout as delay } from "node:timers/promises";
|
|
17
16
|
import { fileURLToPath } from "node:url";
|
|
18
17
|
|
|
19
18
|
import yargs from "yargs/yargs";
|
|
20
19
|
import { hideBin } from "yargs/helpers";
|
|
21
20
|
import yaml from "js-yaml";
|
|
22
|
-
import {
|
|
21
|
+
import { createAiSession } from "@love-moon/ai-sdk";
|
|
23
22
|
import { ConductorClient, loadConfig } from "@love-moon/conductor-sdk";
|
|
24
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
buildResumeArgsForBackend as buildCliResumeArgsForBackend,
|
|
25
|
+
resolveResumeContext as resolveCliResumeContext,
|
|
26
|
+
resolveSessionRunDirectory as resolveCliSessionRunDirectory,
|
|
27
|
+
resumeProviderForBackend as resumeProviderForCliBackend,
|
|
28
|
+
} from "../src/fire/resume.js";
|
|
25
29
|
|
|
26
30
|
const __filename = fileURLToPath(import.meta.url);
|
|
27
31
|
const __dirname = path.dirname(__filename);
|
|
@@ -54,59 +58,10 @@ function loadAllowCliList(configFilePath) {
|
|
|
54
58
|
return {};
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
// Load envs config from config file
|
|
58
|
-
function loadEnvConfig(configFilePath) {
|
|
59
|
-
try {
|
|
60
|
-
const home = os.homedir();
|
|
61
|
-
const configPath = configFilePath || process.env.CONDUCTOR_CONFIG || path.join(home, ".conductor", "config.yaml");
|
|
62
|
-
if (fs.existsSync(configPath)) {
|
|
63
|
-
const content = fs.readFileSync(configPath, "utf8");
|
|
64
|
-
const parsed = yaml.load(content);
|
|
65
|
-
if (parsed && typeof parsed === "object" && parsed.envs) {
|
|
66
|
-
return parsed.envs;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
} catch (error) {
|
|
70
|
-
// ignore error
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Convert proxy env values to standard proxy environment variables
|
|
76
|
-
function proxyToEnv(envConfig) {
|
|
77
|
-
if (!envConfig || typeof envConfig !== "object") {
|
|
78
|
-
return {};
|
|
79
|
-
}
|
|
80
|
-
const env = {};
|
|
81
|
-
// Support both snake_case and lowercase keys
|
|
82
|
-
const mappings = {
|
|
83
|
-
http_proxy: ["HTTP_PROXY", "http_proxy"],
|
|
84
|
-
https_proxy: ["HTTPS_PROXY", "https_proxy"],
|
|
85
|
-
all_proxy: ["ALL_PROXY", "all_proxy"],
|
|
86
|
-
no_proxy: ["NO_PROXY", "no_proxy"],
|
|
87
|
-
};
|
|
88
|
-
for (const [key, envKeys] of Object.entries(mappings)) {
|
|
89
|
-
const value = envConfig[key] || envConfig[key.toUpperCase()];
|
|
90
|
-
if (value) {
|
|
91
|
-
for (const envKey of envKeys) {
|
|
92
|
-
env[envKey] = value;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return env;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Get CLI command from environment variable (set by daemon) or from config
|
|
100
|
-
const CUSTOM_CLI_COMMAND = process.env.CONDUCTOR_CLI_COMMAND;
|
|
101
|
-
const DEFAULT_ALLOW_CLI_LIST = loadAllowCliList();
|
|
102
|
-
|
|
103
61
|
const DEFAULT_POLL_INTERVAL_MS = parseInt(
|
|
104
62
|
process.env.CONDUCTOR_CLI_POLL_INTERVAL_MS || process.env.CCODEX_POLL_INTERVAL_MS || "2000",
|
|
105
63
|
10,
|
|
106
64
|
);
|
|
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
65
|
const DEFAULT_ERROR_LOOP_WINDOW_MS = 2 * 60 * 1000;
|
|
111
66
|
const DEFAULT_ERROR_LOOP_BACKOFF_MS = 3 * 60 * 1000;
|
|
112
67
|
const DEFAULT_ERROR_LOOP_THRESHOLD = 3;
|
|
@@ -368,32 +323,14 @@ async function main() {
|
|
|
368
323
|
}
|
|
369
324
|
}
|
|
370
325
|
|
|
371
|
-
|
|
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
|
-
}
|
|
326
|
+
const resolvedResumeSessionId = cliArgs.resumeSessionId;
|
|
391
327
|
|
|
392
|
-
backendSession =
|
|
328
|
+
backendSession = createAiSession(cliArgs.backend, {
|
|
393
329
|
initialImages: cliArgs.initialImages,
|
|
394
330
|
cwd: runtimeProjectPath,
|
|
395
331
|
resumeSessionId: resolvedResumeSessionId,
|
|
396
332
|
configFile: cliArgs.configFile,
|
|
333
|
+
logger: { log },
|
|
397
334
|
});
|
|
398
335
|
|
|
399
336
|
log(`Using backend: ${cliArgs.backend}`);
|
|
@@ -417,7 +354,7 @@ async function main() {
|
|
|
417
354
|
includeInitialImages: Boolean(cliArgs.initialPrompt && cliArgs.initialImages.length),
|
|
418
355
|
cliArgs: cliArgs.rawBackendArgs,
|
|
419
356
|
backendName: cliArgs.backend,
|
|
420
|
-
|
|
357
|
+
resumeSessionId: resolvedResumeSessionId,
|
|
421
358
|
daemonName: configuredDaemonName,
|
|
422
359
|
});
|
|
423
360
|
reconnectRunner = runner;
|
|
@@ -897,75 +834,16 @@ function deriveTaskTitle(prompt, explicit, backend = "codex") {
|
|
|
897
834
|
return `${backendName} Task`;
|
|
898
835
|
}
|
|
899
836
|
|
|
900
|
-
const BACKEND_PROFILE_MAP = {
|
|
901
|
-
codex: "codex",
|
|
902
|
-
code: "codex",
|
|
903
|
-
claude: "claude-code",
|
|
904
|
-
"claude-code": "claude-code",
|
|
905
|
-
copilot: "copilot",
|
|
906
|
-
};
|
|
907
|
-
|
|
908
|
-
function profileNameForBackend(backend) {
|
|
909
|
-
const normalized = String(backend || "").trim().toLowerCase();
|
|
910
|
-
return BACKEND_PROFILE_MAP[normalized] || null;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
function parseCommandParts(commandLine) {
|
|
914
|
-
const normalized = String(commandLine || "").trim();
|
|
915
|
-
if (!normalized) {
|
|
916
|
-
return { command: "", args: [] };
|
|
917
|
-
}
|
|
918
|
-
const parts = normalized.split(/\s+/);
|
|
919
|
-
return {
|
|
920
|
-
command: parts[0],
|
|
921
|
-
args: parts.slice(1),
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
|
|
925
837
|
export function buildResumeArgsForBackend(backend, sessionId) {
|
|
926
|
-
|
|
927
|
-
if (!resumeSessionId) {
|
|
928
|
-
return [];
|
|
929
|
-
}
|
|
930
|
-
const normalizedBackend = String(backend || "").trim().toLowerCase();
|
|
931
|
-
if (normalizedBackend === "codex" || normalizedBackend === "code") {
|
|
932
|
-
return ["resume", resumeSessionId];
|
|
933
|
-
}
|
|
934
|
-
if (normalizedBackend === "claude" || normalizedBackend === "claude-code") {
|
|
935
|
-
return ["--resume", resumeSessionId];
|
|
936
|
-
}
|
|
937
|
-
if (normalizedBackend === "copilot") {
|
|
938
|
-
return [`--resume=${resumeSessionId}`];
|
|
939
|
-
}
|
|
940
|
-
throw new Error(`--resume is not supported for backend "${backend}"`);
|
|
838
|
+
return buildCliResumeArgsForBackend(backend, sessionId);
|
|
941
839
|
}
|
|
942
840
|
|
|
943
841
|
export function resumeProviderForBackend(backend) {
|
|
944
|
-
|
|
945
|
-
if (normalizedBackend === "codex" || normalizedBackend === "code") {
|
|
946
|
-
return "codex";
|
|
947
|
-
}
|
|
948
|
-
if (normalizedBackend === "claude" || normalizedBackend === "claude-code") {
|
|
949
|
-
return "claude";
|
|
950
|
-
}
|
|
951
|
-
if (normalizedBackend === "copilot") {
|
|
952
|
-
return "copilot";
|
|
953
|
-
}
|
|
954
|
-
return null;
|
|
842
|
+
return resumeProviderForCliBackend(backend);
|
|
955
843
|
}
|
|
956
844
|
|
|
957
845
|
export async function resolveSessionRunDirectory(sessionPath) {
|
|
958
|
-
|
|
959
|
-
if (!normalizedPath) {
|
|
960
|
-
throw new Error("Invalid session path");
|
|
961
|
-
}
|
|
962
|
-
let stats;
|
|
963
|
-
try {
|
|
964
|
-
stats = await fs.promises.stat(normalizedPath);
|
|
965
|
-
} catch {
|
|
966
|
-
throw new Error(`Session path does not exist: ${normalizedPath}`);
|
|
967
|
-
}
|
|
968
|
-
return stats.isDirectory() ? normalizedPath : path.dirname(normalizedPath);
|
|
846
|
+
return resolveCliSessionRunDirectory(sessionPath);
|
|
969
847
|
}
|
|
970
848
|
|
|
971
849
|
async function isExistingDirectory(targetPath) {
|
|
@@ -981,151 +859,8 @@ async function isExistingDirectory(targetPath) {
|
|
|
981
859
|
}
|
|
982
860
|
}
|
|
983
861
|
|
|
984
|
-
async function extractCodexResumeCwd(sessionPath) {
|
|
985
|
-
if (!sessionPath.endsWith(".jsonl")) {
|
|
986
|
-
return null;
|
|
987
|
-
}
|
|
988
|
-
const rl = readline.createInterface({
|
|
989
|
-
input: fs.createReadStream(sessionPath),
|
|
990
|
-
crlfDelay: Infinity,
|
|
991
|
-
});
|
|
992
|
-
for await (const line of rl) {
|
|
993
|
-
const trimmed = line.trim();
|
|
994
|
-
if (!trimmed) {
|
|
995
|
-
continue;
|
|
996
|
-
}
|
|
997
|
-
let entry;
|
|
998
|
-
try {
|
|
999
|
-
entry = JSON.parse(trimmed);
|
|
1000
|
-
} catch {
|
|
1001
|
-
continue;
|
|
1002
|
-
}
|
|
1003
|
-
const maybeCwd = entry?.type === "session_meta" ? entry?.payload?.cwd : null;
|
|
1004
|
-
if (typeof maybeCwd === "string" && maybeCwd.trim()) {
|
|
1005
|
-
return maybeCwd.trim();
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
return null;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
async function extractClaudeResumeCwd(sessionPath, sessionId) {
|
|
1012
|
-
if (!sessionPath.endsWith(".jsonl")) {
|
|
1013
|
-
return null;
|
|
1014
|
-
}
|
|
1015
|
-
const rl = readline.createInterface({
|
|
1016
|
-
input: fs.createReadStream(sessionPath),
|
|
1017
|
-
crlfDelay: Infinity,
|
|
1018
|
-
});
|
|
1019
|
-
for await (const line of rl) {
|
|
1020
|
-
const trimmed = line.trim();
|
|
1021
|
-
if (!trimmed) {
|
|
1022
|
-
continue;
|
|
1023
|
-
}
|
|
1024
|
-
let entry;
|
|
1025
|
-
try {
|
|
1026
|
-
entry = JSON.parse(trimmed);
|
|
1027
|
-
} catch {
|
|
1028
|
-
continue;
|
|
1029
|
-
}
|
|
1030
|
-
const idMatches = String(entry?.sessionId || "").trim() === sessionId;
|
|
1031
|
-
const maybeCwd = entry?.cwd;
|
|
1032
|
-
if (idMatches && typeof maybeCwd === "string" && maybeCwd.trim()) {
|
|
1033
|
-
return maybeCwd.trim();
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
return null;
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
async function extractCopilotResumeCwd(sessionPath) {
|
|
1040
|
-
let stats;
|
|
1041
|
-
try {
|
|
1042
|
-
stats = await fs.promises.stat(sessionPath);
|
|
1043
|
-
} catch {
|
|
1044
|
-
return null;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
if (stats.isDirectory()) {
|
|
1048
|
-
const workspaceYamlPath = path.join(sessionPath, "workspace.yaml");
|
|
1049
|
-
try {
|
|
1050
|
-
const yamlContent = await fs.promises.readFile(workspaceYamlPath, "utf8");
|
|
1051
|
-
const parsed = yaml.load(yamlContent);
|
|
1052
|
-
const maybeCwd = parsed && typeof parsed === "object" ? parsed.cwd : null;
|
|
1053
|
-
if (typeof maybeCwd === "string" && maybeCwd.trim()) {
|
|
1054
|
-
return maybeCwd.trim();
|
|
1055
|
-
}
|
|
1056
|
-
} catch {
|
|
1057
|
-
return null;
|
|
1058
|
-
}
|
|
1059
|
-
return null;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
if (!sessionPath.endsWith(".jsonl")) {
|
|
1063
|
-
return null;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
const rl = readline.createInterface({
|
|
1067
|
-
input: fs.createReadStream(sessionPath),
|
|
1068
|
-
crlfDelay: Infinity,
|
|
1069
|
-
});
|
|
1070
|
-
for await (const line of rl) {
|
|
1071
|
-
const trimmed = line.trim();
|
|
1072
|
-
if (!trimmed) {
|
|
1073
|
-
continue;
|
|
1074
|
-
}
|
|
1075
|
-
let entry;
|
|
1076
|
-
try {
|
|
1077
|
-
entry = JSON.parse(trimmed);
|
|
1078
|
-
} catch {
|
|
1079
|
-
continue;
|
|
1080
|
-
}
|
|
1081
|
-
const maybeCwd = entry?.data?.context?.cwd || entry?.data?.cwd;
|
|
1082
|
-
if (typeof maybeCwd === "string" && maybeCwd.trim()) {
|
|
1083
|
-
return maybeCwd.trim();
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
return null;
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
async function extractResumeCwdFromSession(provider, sessionPath, sessionId) {
|
|
1090
|
-
if (provider === "codex") {
|
|
1091
|
-
return extractCodexResumeCwd(sessionPath);
|
|
1092
|
-
}
|
|
1093
|
-
if (provider === "claude") {
|
|
1094
|
-
return extractClaudeResumeCwd(sessionPath, sessionId);
|
|
1095
|
-
}
|
|
1096
|
-
if (provider === "copilot") {
|
|
1097
|
-
return extractCopilotResumeCwd(sessionPath);
|
|
1098
|
-
}
|
|
1099
|
-
return null;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
862
|
export async function resolveResumeContext(backend, sessionId, options = {}) {
|
|
1103
|
-
|
|
1104
|
-
if (!normalizedSessionId) {
|
|
1105
|
-
throw new Error("--resume requires a session id");
|
|
1106
|
-
}
|
|
1107
|
-
const provider = resumeProviderForBackend(backend);
|
|
1108
|
-
if (!provider) {
|
|
1109
|
-
throw new Error(`--resume is not supported for backend "${backend}"`);
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
const sessionPath = await findSessionPath(provider, normalizedSessionId, options);
|
|
1113
|
-
if (!sessionPath) {
|
|
1114
|
-
throw new Error(`Invalid --resume session id for ${provider}: ${normalizedSessionId}`);
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
const cwdFromSession = await extractResumeCwdFromSession(provider, sessionPath, normalizedSessionId);
|
|
1118
|
-
const fallbackCwd = await resolveSessionRunDirectory(sessionPath);
|
|
1119
|
-
const cwd = cwdFromSession || fallbackCwd;
|
|
1120
|
-
if (!(await isExistingDirectory(cwd))) {
|
|
1121
|
-
throw new Error(`Resume workspace path does not exist: ${cwd}`);
|
|
1122
|
-
}
|
|
1123
|
-
return {
|
|
1124
|
-
provider,
|
|
1125
|
-
sessionId: normalizedSessionId,
|
|
1126
|
-
sessionPath,
|
|
1127
|
-
cwd,
|
|
1128
|
-
};
|
|
863
|
+
return resolveCliResumeContext(backend, sessionId, options);
|
|
1129
864
|
}
|
|
1130
865
|
|
|
1131
866
|
export async function applyWorkingDirectory(targetPath) {
|
|
@@ -1199,866 +934,6 @@ function normalizeExecutionErrorKey(errorMessage) {
|
|
|
1199
934
|
return normalized;
|
|
1200
935
|
}
|
|
1201
936
|
|
|
1202
|
-
function tailLines(value, count = 6) {
|
|
1203
|
-
if (!value) return "";
|
|
1204
|
-
const lines = String(value).split(/\r?\n/);
|
|
1205
|
-
return lines.slice(Math.max(0, lines.length - Math.max(1, count))).join("\n");
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
class TuiDriverSession {
|
|
1209
|
-
constructor(backend, options = {}) {
|
|
1210
|
-
this.backend = backend;
|
|
1211
|
-
this.options = options;
|
|
1212
|
-
this.cwd =
|
|
1213
|
-
typeof options.cwd === "string" && options.cwd.trim()
|
|
1214
|
-
? options.cwd.trim()
|
|
1215
|
-
: INITIAL_CLI_PROJECT_PATH;
|
|
1216
|
-
const resumeSessionId = typeof options.resumeSessionId === "string" ? options.resumeSessionId.trim() : "";
|
|
1217
|
-
this.sessionId = resumeSessionId || `${backend}-${Date.now()}`;
|
|
1218
|
-
this.history = Array.isArray(options.initialHistory) ? [...options.initialHistory] : [];
|
|
1219
|
-
this.pendingHistorySeed = this.history.length > 0;
|
|
1220
|
-
this.sessionInfo = null;
|
|
1221
|
-
|
|
1222
|
-
const allowCliList = options.configFile ? loadAllowCliList(options.configFile) : DEFAULT_ALLOW_CLI_LIST;
|
|
1223
|
-
const cliCommand = CUSTOM_CLI_COMMAND || allowCliList[backend] || backend;
|
|
1224
|
-
const { command, args } = parseCommandParts(cliCommand);
|
|
1225
|
-
if (!command) {
|
|
1226
|
-
throw new Error(`Invalid command for backend "${backend}"`);
|
|
1227
|
-
}
|
|
1228
|
-
const resumeArgs = buildResumeArgsForBackend(backend, resumeSessionId);
|
|
1229
|
-
this.command = command;
|
|
1230
|
-
this.args = [...args, ...resumeArgs];
|
|
1231
|
-
this.tuiDebug = isTruthyEnv(process.env.CONDUCTOR_TUI_DEBUG);
|
|
1232
|
-
this.tuiTrace = this.tuiDebug || isTruthyEnv(process.env.CONDUCTOR_TUI_TRACE);
|
|
1233
|
-
this.tuiTraceLines = Number.isFinite(Number.parseInt(process.env.CONDUCTOR_TUI_TRACE_LINES || "", 10))
|
|
1234
|
-
? Math.max(2, Number.parseInt(process.env.CONDUCTOR_TUI_TRACE_LINES, 10))
|
|
1235
|
-
: 8;
|
|
1236
|
-
this.lastSignalSignature = "";
|
|
1237
|
-
this.lastPollSignature = "";
|
|
1238
|
-
this.lastSnapshotHash = "";
|
|
1239
|
-
this.closeRequested = false;
|
|
1240
|
-
this.closed = false;
|
|
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
|
-
);
|
|
1262
|
-
|
|
1263
|
-
const profileName = profileNameForBackend(backend);
|
|
1264
|
-
if (!profileName) {
|
|
1265
|
-
throw new Error(`Backend "${backend}" is not supported by tui-driver`);
|
|
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
|
-
);
|
|
1287
|
-
|
|
1288
|
-
const profileMap = {
|
|
1289
|
-
codex: codexProfile,
|
|
1290
|
-
"claude-code": claudeCodeProfile,
|
|
1291
|
-
copilot: copilotProfile,
|
|
1292
|
-
};
|
|
1293
|
-
const baseProfile = profileMap[profileName];
|
|
1294
|
-
const effectiveDriverArgs = [...(baseProfile.args || []), ...this.args];
|
|
1295
|
-
const envConfig = loadEnvConfig(options.configFile);
|
|
1296
|
-
const proxyEnv = proxyToEnv(envConfig);
|
|
1297
|
-
const cliEnv = envConfig && typeof envConfig === "object" ? { ...envConfig, ...proxyEnv } : proxyEnv;
|
|
1298
|
-
|
|
1299
|
-
if (Object.keys(proxyEnv).length > 0) {
|
|
1300
|
-
log(`Using proxy: ${proxyEnv.http_proxy || proxyEnv.https_proxy || proxyEnv.all_proxy}`);
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
log(`Using TUI command for ${backend}: ${[this.command, ...effectiveDriverArgs].join(" ")} (cwd: ${this.cwd})`);
|
|
1304
|
-
if (this.tuiTrace) {
|
|
1305
|
-
log(
|
|
1306
|
-
`[${this.backend}] [tui-trace] profile=${baseProfile.name} command=${this.command} args=${JSON.stringify(effectiveDriverArgs)}`,
|
|
1307
|
-
);
|
|
1308
|
-
log(
|
|
1309
|
-
`[${this.backend}] [tui-trace] timeouts=${JSON.stringify(baseProfile.timeouts || {})} size=${baseProfile.cols || 120}x${baseProfile.rows || 40}`,
|
|
1310
|
-
);
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
this.driver = new TuiDriver({
|
|
1314
|
-
profile: {
|
|
1315
|
-
...baseProfile,
|
|
1316
|
-
command: this.command,
|
|
1317
|
-
args: effectiveDriverArgs,
|
|
1318
|
-
env: {
|
|
1319
|
-
...process.env,
|
|
1320
|
-
...(baseProfile.env || {}),
|
|
1321
|
-
...cliEnv,
|
|
1322
|
-
},
|
|
1323
|
-
},
|
|
1324
|
-
cwd: this.cwd,
|
|
1325
|
-
expectedSessionId: resumeSessionId || undefined,
|
|
1326
|
-
debug: this.tuiDebug,
|
|
1327
|
-
onSnapshot: this.tuiTrace
|
|
1328
|
-
? (snapshot, state) => {
|
|
1329
|
-
this.logSnapshot(state, snapshot);
|
|
1330
|
-
}
|
|
1331
|
-
: undefined,
|
|
1332
|
-
onSignals: this.tuiTrace
|
|
1333
|
-
? (signals, snapshot, state) => {
|
|
1334
|
-
this.logSignals(state, signals, snapshot);
|
|
1335
|
-
}
|
|
1336
|
-
: undefined,
|
|
1337
|
-
});
|
|
1338
|
-
|
|
1339
|
-
// 监听登录需求事件
|
|
1340
|
-
this.driver.on("login_required", (health) => {
|
|
1341
|
-
log(`[${this.backend}] [WARN] Login required detected: ${health.message || health.reason}`);
|
|
1342
|
-
if (health.matchedPattern) {
|
|
1343
|
-
log(`[${this.backend}] [WARN] Matched pattern: "${health.matchedPattern}"`);
|
|
1344
|
-
}
|
|
1345
|
-
log(`[${this.backend}] [WARN] Please run "${this.command} login" or authenticate manually.`);
|
|
1346
|
-
});
|
|
1347
|
-
|
|
1348
|
-
this.driver.on("session", (session) => {
|
|
1349
|
-
this.applySessionInfo(session);
|
|
1350
|
-
});
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
get threadId() {
|
|
1354
|
-
return this.sessionId;
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
get threadOptions() {
|
|
1358
|
-
return { model: this.backend };
|
|
1359
|
-
}
|
|
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
|
-
|
|
1657
|
-
createSessionClosedError() {
|
|
1658
|
-
const error = new Error("TUI session closed");
|
|
1659
|
-
error.reason = "session_closed";
|
|
1660
|
-
return error;
|
|
1661
|
-
}
|
|
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
|
-
|
|
1671
|
-
createCloseGuard() {
|
|
1672
|
-
if (this.closeRequested) {
|
|
1673
|
-
return {
|
|
1674
|
-
promise: Promise.reject(this.createSessionClosedError()),
|
|
1675
|
-
cleanup: () => {},
|
|
1676
|
-
};
|
|
1677
|
-
}
|
|
1678
|
-
let waiter = null;
|
|
1679
|
-
const promise = new Promise((_, reject) => {
|
|
1680
|
-
waiter = () => {
|
|
1681
|
-
reject(this.createSessionClosedError());
|
|
1682
|
-
};
|
|
1683
|
-
this.closeWaiters.add(waiter);
|
|
1684
|
-
});
|
|
1685
|
-
return {
|
|
1686
|
-
promise,
|
|
1687
|
-
cleanup: () => {
|
|
1688
|
-
if (waiter) {
|
|
1689
|
-
this.closeWaiters.delete(waiter);
|
|
1690
|
-
}
|
|
1691
|
-
},
|
|
1692
|
-
};
|
|
1693
|
-
}
|
|
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
|
-
|
|
1721
|
-
flushCloseWaiters() {
|
|
1722
|
-
if (!this.closeWaiters || this.closeWaiters.size === 0) {
|
|
1723
|
-
return;
|
|
1724
|
-
}
|
|
1725
|
-
for (const waiter of this.closeWaiters) {
|
|
1726
|
-
try {
|
|
1727
|
-
waiter();
|
|
1728
|
-
} catch {
|
|
1729
|
-
// best effort
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
this.closeWaiters.clear();
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
async close() {
|
|
1736
|
-
if (this.closed) {
|
|
1737
|
-
return;
|
|
1738
|
-
}
|
|
1739
|
-
this.closed = true;
|
|
1740
|
-
this.closeRequested = true;
|
|
1741
|
-
this.sessionMonitorStopRequested = true;
|
|
1742
|
-
this.workingStatusMonitorStopRequested = true;
|
|
1743
|
-
this.flushCloseWaiters();
|
|
1744
|
-
if (this.driver) {
|
|
1745
|
-
this.driver.kill();
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
/**
|
|
1750
|
-
* 获取当前健康状态
|
|
1751
|
-
* @returns {Object} 健康状态对象 { healthy, reason, message, matchedPattern }
|
|
1752
|
-
*/
|
|
1753
|
-
getHealthStatus() {
|
|
1754
|
-
if (!this.driver) {
|
|
1755
|
-
return { healthy: false, reason: "not_initialized", message: "Driver not initialized" };
|
|
1756
|
-
}
|
|
1757
|
-
return this.driver.healthCheck();
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
buildPrompt(promptText, { useInitialImages = false } = {}) {
|
|
1761
|
-
let effectivePrompt = String(promptText || "").trim();
|
|
1762
|
-
if (!effectivePrompt) {
|
|
1763
|
-
return "";
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
if (this.pendingHistorySeed) {
|
|
1767
|
-
const historyText = this.history
|
|
1768
|
-
.map((item) => {
|
|
1769
|
-
const role = String(item?.role || "").toLowerCase() === "assistant" ? "Assistant" : "User";
|
|
1770
|
-
return `${role}: ${String(item?.content || "").trim()}`;
|
|
1771
|
-
})
|
|
1772
|
-
.filter(Boolean)
|
|
1773
|
-
.join("\n\n");
|
|
1774
|
-
if (historyText) {
|
|
1775
|
-
effectivePrompt = [
|
|
1776
|
-
"Continue the existing conversation with this history.",
|
|
1777
|
-
"",
|
|
1778
|
-
historyText,
|
|
1779
|
-
"",
|
|
1780
|
-
`User: ${effectivePrompt}`,
|
|
1781
|
-
].join("\n");
|
|
1782
|
-
}
|
|
1783
|
-
this.pendingHistorySeed = false;
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
const images = Array.isArray(this.options.initialImages) ? this.options.initialImages : [];
|
|
1787
|
-
if (useInitialImages && images.length > 0) {
|
|
1788
|
-
const imageContext = images.map((item, idx) => `${idx + 1}. ${item}`).join("\n");
|
|
1789
|
-
effectivePrompt = `${effectivePrompt}\n\nAttached image files:\n${imageContext}`;
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
return effectivePrompt;
|
|
1793
|
-
}
|
|
1794
|
-
|
|
1795
|
-
emitProgress(onProgress, payload) {
|
|
1796
|
-
if (typeof onProgress !== "function") {
|
|
1797
|
-
return;
|
|
1798
|
-
}
|
|
1799
|
-
try {
|
|
1800
|
-
onProgress(payload);
|
|
1801
|
-
} catch {
|
|
1802
|
-
// best effort
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
trace(message) {
|
|
1807
|
-
if (!this.tuiTrace) {
|
|
1808
|
-
return;
|
|
1809
|
-
}
|
|
1810
|
-
log(`[${this.backend}] [tui-trace] ${message}`);
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
formatSignalSummary(signals = {}) {
|
|
1814
|
-
return {
|
|
1815
|
-
prompt: sanitizeForLog(signals.promptLine || "", 100) || undefined,
|
|
1816
|
-
replyInProgress: Boolean(signals.replyInProgress),
|
|
1817
|
-
status: sanitizeForLog(signals.statusLine || "", 140) || undefined,
|
|
1818
|
-
done: sanitizeForLog(signals.statusDoneLine || "", 140) || undefined,
|
|
1819
|
-
replyPreview: sanitizeForLog(signals.replyText || "", 180) || undefined,
|
|
1820
|
-
blocks: Array.isArray(signals.replyBlocks) ? signals.replyBlocks.length : 0,
|
|
1821
|
-
};
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
logSignals(state, signals, snapshot) {
|
|
1825
|
-
const summary = this.formatSignalSummary(signals);
|
|
1826
|
-
const signature = JSON.stringify(summary);
|
|
1827
|
-
if (signature === this.lastSignalSignature) {
|
|
1828
|
-
return;
|
|
1829
|
-
}
|
|
1830
|
-
this.lastSignalSignature = signature;
|
|
1831
|
-
this.trace(
|
|
1832
|
-
`signals state=${state} hash=${snapshot?.hash || "n/a"} prompt="${summary.prompt || ""}" status="${summary.status || ""}" done="${summary.done || ""}" replyInProgress=${summary.replyInProgress} blocks=${summary.blocks} preview="${summary.replyPreview || ""}"`,
|
|
1833
|
-
);
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
logSnapshot(state, snapshot) {
|
|
1837
|
-
if (!snapshot || snapshot.hash === this.lastSnapshotHash) {
|
|
1838
|
-
return;
|
|
1839
|
-
}
|
|
1840
|
-
this.lastSnapshotHash = snapshot.hash;
|
|
1841
|
-
const viewportTail = sanitizeForLog(tailLines(snapshot.viewportText, this.tuiTraceLines), 400);
|
|
1842
|
-
this.trace(
|
|
1843
|
-
`snapshot state=${state} hash=${snapshot.hash} cursor=${snapshot.cursor?.x || 0},${snapshot.cursor?.y || 0} tail="${viewportTail}"`,
|
|
1844
|
-
);
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
async runTurn(promptText, { useInitialImages = false, onProgress } = {}) {
|
|
1848
|
-
if (this.closeRequested) {
|
|
1849
|
-
throw this.createSessionClosedError();
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
const effectivePrompt = this.buildPrompt(promptText, { useInitialImages });
|
|
1853
|
-
if (!effectivePrompt) {
|
|
1854
|
-
return {
|
|
1855
|
-
text: "",
|
|
1856
|
-
usage: null,
|
|
1857
|
-
items: [],
|
|
1858
|
-
events: [],
|
|
1859
|
-
};
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
log(`[${this.backend}] Running prompt: ${truncateText(effectivePrompt, 100)}...`);
|
|
1863
|
-
this.trace(`runTurn start promptLen=${effectivePrompt.length} useInitialImages=${Boolean(useInitialImages)}`);
|
|
1864
|
-
const useSessionFileReplyStream = this.usesSessionFileReplyStream();
|
|
1865
|
-
|
|
1866
|
-
const handleStateChange = (transition) => {
|
|
1867
|
-
this.trace(`state ${transition.from} -> ${transition.to}`);
|
|
1868
|
-
if (useSessionFileReplyStream) {
|
|
1869
|
-
return;
|
|
1870
|
-
}
|
|
1871
|
-
this.emitProgress(onProgress, {
|
|
1872
|
-
state: transition.to,
|
|
1873
|
-
phase: "state_change",
|
|
1874
|
-
source: "tui-driver",
|
|
1875
|
-
});
|
|
1876
|
-
};
|
|
1877
|
-
|
|
1878
|
-
this.driver.on("stateChange", handleStateChange);
|
|
1879
|
-
|
|
1880
|
-
const signalTimer = setInterval(() => {
|
|
1881
|
-
const signals = this.driver.getSignals();
|
|
1882
|
-
if (this.tuiTrace) {
|
|
1883
|
-
const pollSummary = this.formatSignalSummary(signals);
|
|
1884
|
-
const pollSignature = JSON.stringify({
|
|
1885
|
-
state: this.driver.state,
|
|
1886
|
-
replyInProgress: pollSummary.replyInProgress,
|
|
1887
|
-
status: pollSummary.status,
|
|
1888
|
-
done: pollSummary.done,
|
|
1889
|
-
preview: pollSummary.replyPreview,
|
|
1890
|
-
});
|
|
1891
|
-
if (pollSignature !== this.lastPollSignature) {
|
|
1892
|
-
this.lastPollSignature = pollSignature;
|
|
1893
|
-
this.trace(
|
|
1894
|
-
`poll state=${this.driver.state} replyInProgress=${pollSummary.replyInProgress} status="${pollSummary.status || ""}" done="${pollSummary.done || ""}" preview="${pollSummary.replyPreview || ""}"`,
|
|
1895
|
-
);
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
if (useSessionFileReplyStream) {
|
|
1899
|
-
return;
|
|
1900
|
-
}
|
|
1901
|
-
this.emitProgress(onProgress, {
|
|
1902
|
-
state: this.driver.state,
|
|
1903
|
-
phase: "signal_poll",
|
|
1904
|
-
source: "tui-driver",
|
|
1905
|
-
reply_in_progress: Boolean(signals.replyInProgress),
|
|
1906
|
-
status_line: signals.statusLine || undefined,
|
|
1907
|
-
status_done_line: signals.statusDoneLine || undefined,
|
|
1908
|
-
reply_preview: truncateText(signals.replyText || "", 240) || undefined,
|
|
1909
|
-
});
|
|
1910
|
-
}, 700);
|
|
1911
|
-
if (typeof signalTimer.unref === "function") {
|
|
1912
|
-
signalTimer.unref();
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
const previousCwd = process.cwd();
|
|
1916
|
-
const shouldSwitchCwd = this.cwd && this.cwd !== previousCwd;
|
|
1917
|
-
if (shouldSwitchCwd) {
|
|
1918
|
-
try {
|
|
1919
|
-
process.chdir(this.cwd);
|
|
1920
|
-
} catch (error) {
|
|
1921
|
-
throw new Error(`Failed to switch backend cwd to ${this.cwd}: ${error?.message || error}`);
|
|
1922
|
-
}
|
|
1923
|
-
}
|
|
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
|
-
}
|
|
1931
|
-
|
|
1932
|
-
try {
|
|
1933
|
-
const result = await Promise.race([this.driver.ask(effectivePrompt), closeGuard.promise, turnTimeoutGuard.promise]);
|
|
1934
|
-
const answer = String(result.answer || result.replyText || "").trim();
|
|
1935
|
-
this.trace(
|
|
1936
|
-
`runTurn finished success=${Boolean(result.success)} elapsedMs=${result.elapsedMs} answerLen=${answer.length} state=${this.driver.state}`,
|
|
1937
|
-
);
|
|
1938
|
-
|
|
1939
|
-
if (!useSessionFileReplyStream) {
|
|
1940
|
-
this.history.push({ role: "user", content: promptText });
|
|
1941
|
-
}
|
|
1942
|
-
if (answer && !useSessionFileReplyStream) {
|
|
1943
|
-
this.history.push({ role: "assistant", content: answer });
|
|
1944
|
-
}
|
|
1945
|
-
|
|
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
|
-
}
|
|
1957
|
-
|
|
1958
|
-
if (!result.success) {
|
|
1959
|
-
const error = result.error || new Error("tui-driver failed to complete this turn");
|
|
1960
|
-
throw error;
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
log(`[${this.backend}] Response received: ${truncateText(answer, 100)}...`);
|
|
1964
|
-
this.trace(`runTurn reply preview="${sanitizeForLog(answer, 220)}"`);
|
|
1965
|
-
|
|
1966
|
-
return {
|
|
1967
|
-
text: answer,
|
|
1968
|
-
usage: null,
|
|
1969
|
-
items: [],
|
|
1970
|
-
events: [],
|
|
1971
|
-
provider: this.backend,
|
|
1972
|
-
metadata: {
|
|
1973
|
-
source: "tui-driver",
|
|
1974
|
-
elapsed_ms: result.elapsedMs,
|
|
1975
|
-
signals: result.signals ?? null,
|
|
1976
|
-
},
|
|
1977
|
-
};
|
|
1978
|
-
} catch (error) {
|
|
1979
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1980
|
-
const errorReason = error?.reason || "unknown";
|
|
1981
|
-
if (errorReason === "session_closed") {
|
|
1982
|
-
this.trace("runTurn interrupted because backend session is closing");
|
|
1983
|
-
throw error instanceof Error ? error : new Error(errorMessage);
|
|
1984
|
-
}
|
|
1985
|
-
|
|
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") {
|
|
2003
|
-
this.emitProgress(onProgress, {
|
|
2004
|
-
state: "ERROR",
|
|
2005
|
-
phase: "login_required",
|
|
2006
|
-
source: "tui-driver",
|
|
2007
|
-
error: errorMessage,
|
|
2008
|
-
reason: errorReason,
|
|
2009
|
-
matched_pattern: error?.matchedPattern,
|
|
2010
|
-
});
|
|
2011
|
-
log(`[${this.backend}] Login required: ${errorMessage}`);
|
|
2012
|
-
log(`[${this.backend}] Please run "${this.command} login" or authenticate manually.`);
|
|
2013
|
-
} else if (errorReason === "permission_required") {
|
|
2014
|
-
this.emitProgress(onProgress, {
|
|
2015
|
-
state: "ERROR",
|
|
2016
|
-
phase: "permission_required",
|
|
2017
|
-
source: "tui-driver",
|
|
2018
|
-
error: errorMessage,
|
|
2019
|
-
reason: errorReason,
|
|
2020
|
-
matched_pattern: error?.matchedPattern,
|
|
2021
|
-
});
|
|
2022
|
-
log(`[${this.backend}] Permission required: ${errorMessage}`);
|
|
2023
|
-
} else {
|
|
2024
|
-
this.emitProgress(onProgress, {
|
|
2025
|
-
state: "ERROR",
|
|
2026
|
-
phase: "exception",
|
|
2027
|
-
source: "tui-driver",
|
|
2028
|
-
error: errorMessage,
|
|
2029
|
-
reason: errorReason,
|
|
2030
|
-
});
|
|
2031
|
-
log(`[${this.backend}] Error: ${errorMessage}`);
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
let latestSignals = {};
|
|
2035
|
-
try {
|
|
2036
|
-
latestSignals = this.driver.getSignals();
|
|
2037
|
-
} catch {
|
|
2038
|
-
// driver may already be disposed while closing
|
|
2039
|
-
}
|
|
2040
|
-
const summary = this.formatSignalSummary(latestSignals);
|
|
2041
|
-
this.trace(
|
|
2042
|
-
`runTurn exception state=${this.driver.state} error="${sanitizeForLog(errorMessage, 220)}" status="${summary.status || ""}" done="${summary.done || ""}" preview="${summary.replyPreview || ""}"`,
|
|
2043
|
-
);
|
|
2044
|
-
throw error instanceof Error ? error : new Error(errorMessage);
|
|
2045
|
-
} finally {
|
|
2046
|
-
if (shouldSwitchCwd) {
|
|
2047
|
-
try {
|
|
2048
|
-
process.chdir(previousCwd);
|
|
2049
|
-
} catch (error) {
|
|
2050
|
-
log(`Failed to restore cwd to ${previousCwd}: ${error?.message || error}`);
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2053
|
-
clearInterval(signalTimer);
|
|
2054
|
-
this.driver.off("stateChange", handleStateChange);
|
|
2055
|
-
turnTimeoutGuard.cleanup();
|
|
2056
|
-
closeGuard.cleanup();
|
|
2057
|
-
this.trace(`runTurn cleanup state=${this.driver.state}`);
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
937
|
export class BridgeRunner {
|
|
2063
938
|
constructor({
|
|
2064
939
|
backendSession,
|
|
@@ -2069,7 +944,7 @@ export class BridgeRunner {
|
|
|
2069
944
|
includeInitialImages,
|
|
2070
945
|
cliArgs,
|
|
2071
946
|
backendName,
|
|
2072
|
-
|
|
947
|
+
resumeSessionId,
|
|
2073
948
|
daemonName,
|
|
2074
949
|
}) {
|
|
2075
950
|
this.backendSession = backendSession;
|
|
@@ -2080,7 +955,10 @@ export class BridgeRunner {
|
|
|
2080
955
|
this.includeInitialImages = includeInitialImages;
|
|
2081
956
|
this.cliArgs = cliArgs;
|
|
2082
957
|
this.backendName = backendName || "codex";
|
|
2083
|
-
this.
|
|
958
|
+
this.resumeSessionId =
|
|
959
|
+
typeof resumeSessionId === "string" && resumeSessionId.trim()
|
|
960
|
+
? resumeSessionId.trim()
|
|
961
|
+
: "";
|
|
2084
962
|
this.isCopilotBackend = String(this.backendName).toLowerCase() === "copilot";
|
|
2085
963
|
this.copilotDebug =
|
|
2086
964
|
this.isCopilotBackend &&
|
|
@@ -2089,6 +967,7 @@ export class BridgeRunner {
|
|
|
2089
967
|
this.runningTurn = false;
|
|
2090
968
|
this.processedMessageIds = new Set();
|
|
2091
969
|
this.inFlightMessageIds = new Set();
|
|
970
|
+
this.sessionStreamReplyCounts = new Map();
|
|
2092
971
|
this.lastRuntimeStatusSignature = null;
|
|
2093
972
|
this.lastRuntimeStatusPayload = null;
|
|
2094
973
|
this.runtimeContextSnapshot = null;
|
|
@@ -2172,9 +1051,7 @@ export class BridgeRunner {
|
|
|
2172
1051
|
return;
|
|
2173
1052
|
}
|
|
2174
1053
|
const discoveredSessionId = String(sessionInfo?.sessionId || "").trim();
|
|
2175
|
-
const fallbackSessionId = this.
|
|
2176
|
-
? String(this.backendSession.threadId || "").trim()
|
|
2177
|
-
: "";
|
|
1054
|
+
const fallbackSessionId = this.resumeSessionId;
|
|
2178
1055
|
const sessionId = discoveredSessionId || fallbackSessionId;
|
|
2179
1056
|
const sessionFilePath = sessionInfo?.sessionFilePath ? String(sessionInfo.sessionFilePath).trim() : "";
|
|
2180
1057
|
const hasRealSessionId = Boolean(sessionId);
|
|
@@ -2207,13 +1084,15 @@ export class BridgeRunner {
|
|
|
2207
1084
|
{
|
|
2208
1085
|
state: this.useSessionFileReplyStream ? undefined : "WAIT_READY",
|
|
2209
1086
|
phase: "session_started",
|
|
2210
|
-
source: "
|
|
1087
|
+
source: "ai-sdk",
|
|
2211
1088
|
reply_in_progress: false,
|
|
2212
1089
|
status_done_line: this.useSessionFileReplyStream
|
|
2213
1090
|
? undefined
|
|
2214
1091
|
: `${this.backendName} session started`,
|
|
2215
1092
|
backend: this.backendName,
|
|
2216
1093
|
thread_id: hasRealSessionId ? sessionId : undefined,
|
|
1094
|
+
session_id: hasRealSessionId ? sessionId : undefined,
|
|
1095
|
+
session_file_path: sessionFilePath || undefined,
|
|
2217
1096
|
},
|
|
2218
1097
|
undefined,
|
|
2219
1098
|
);
|
|
@@ -2244,13 +1123,6 @@ export class BridgeRunner {
|
|
|
2244
1123
|
if (this.stopped) {
|
|
2245
1124
|
return;
|
|
2246
1125
|
}
|
|
2247
|
-
if (this.resumeMode) {
|
|
2248
|
-
await this.drainBufferedMessagesForResume();
|
|
2249
|
-
}
|
|
2250
|
-
if (this.stopped) {
|
|
2251
|
-
return;
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
1126
|
while (!this.stopped) {
|
|
2255
1127
|
if (this.needsReconnectRecovery && !this.runningTurn) {
|
|
2256
1128
|
await this.recoverAfterReconnect();
|
|
@@ -2325,41 +1197,6 @@ export class BridgeRunner {
|
|
|
2325
1197
|
await this.replayLastRuntimeStatus();
|
|
2326
1198
|
}
|
|
2327
1199
|
|
|
2328
|
-
async drainBufferedMessagesForResume() {
|
|
2329
|
-
let drainedCount = 0;
|
|
2330
|
-
let drainedBatches = 0;
|
|
2331
|
-
|
|
2332
|
-
while (!this.stopped) {
|
|
2333
|
-
const result = await this.conductor.receiveMessages(this.taskId, 50);
|
|
2334
|
-
const messages = Array.isArray(result?.messages) ? result.messages : [];
|
|
2335
|
-
if (messages.length === 0) {
|
|
2336
|
-
break;
|
|
2337
|
-
}
|
|
2338
|
-
|
|
2339
|
-
drainedBatches += 1;
|
|
2340
|
-
for (const message of messages) {
|
|
2341
|
-
const replyTo = message?.message_id ? String(message.message_id) : "";
|
|
2342
|
-
if (replyTo) {
|
|
2343
|
-
this.processedMessageIds.add(replyTo);
|
|
2344
|
-
}
|
|
2345
|
-
drainedCount += 1;
|
|
2346
|
-
}
|
|
2347
|
-
|
|
2348
|
-
const ackToken = result.next_ack_token || result.nextAckToken;
|
|
2349
|
-
if (ackToken) {
|
|
2350
|
-
await this.conductor.ackMessages(this.taskId, ackToken);
|
|
2351
|
-
}
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2354
|
-
if (drainedCount > 0) {
|
|
2355
|
-
log(
|
|
2356
|
-
`Resume startup skipped ${drainedCount} buffered message(s) in ${drainedBatches} batch(es) for task ${this.taskId}`,
|
|
2357
|
-
);
|
|
2358
|
-
} else {
|
|
2359
|
-
this.copilotLog("resume startup found no buffered messages to skip");
|
|
2360
|
-
}
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
1200
|
async processIncomingBatch() {
|
|
2364
1201
|
const result = await this.conductor.receiveMessages(this.taskId, 20);
|
|
2365
1202
|
const messages = Array.isArray(result?.messages) ? result.messages : [];
|
|
@@ -2457,15 +1294,6 @@ export class BridgeRunner {
|
|
|
2457
1294
|
.filter((item) => String(item?.role || "").toLowerCase() === "user")
|
|
2458
1295
|
.filter((item) => typeof item?.content === "string" && item.content.trim());
|
|
2459
1296
|
|
|
2460
|
-
if (this.resumeMode) {
|
|
2461
|
-
for (const historyId of historyUserIds) {
|
|
2462
|
-
this.processedMessageIds.add(historyId);
|
|
2463
|
-
}
|
|
2464
|
-
this.copilotLog(
|
|
2465
|
-
`resume mode: seeded processed ids=${historyUserIds.length}, skip startup backfill replay`,
|
|
2466
|
-
);
|
|
2467
|
-
return;
|
|
2468
|
-
}
|
|
2469
1297
|
this.copilotLog(
|
|
2470
1298
|
`backfill loaded history=${history.length} handledUsers=${handledUserIds.length} pendingUsers=${pendingUserMessages.length} lastSdkIndex=${lastSdkIndex}`,
|
|
2471
1299
|
);
|
|
@@ -2574,7 +1402,7 @@ export class BridgeRunner {
|
|
|
2574
1402
|
return {
|
|
2575
1403
|
state: state || undefined,
|
|
2576
1404
|
phase: phase || undefined,
|
|
2577
|
-
source: "
|
|
1405
|
+
source: payload.source ? String(payload.source) : "ai-sdk",
|
|
2578
1406
|
reply_in_progress: Boolean(payload.reply_in_progress),
|
|
2579
1407
|
status_line: statusLine || undefined,
|
|
2580
1408
|
status_done_line: statusDoneLine || undefined,
|
|
@@ -2583,12 +1411,13 @@ export class BridgeRunner {
|
|
|
2583
1411
|
backend: this.backendName,
|
|
2584
1412
|
thread_id:
|
|
2585
1413
|
String(
|
|
2586
|
-
payload.thread_id || runtimeContext?.session_id || "",
|
|
1414
|
+
payload.thread_id || payload.session_id || runtimeContext?.session_id || "",
|
|
2587
1415
|
).trim() || undefined,
|
|
2588
1416
|
daemon: runtimeContext?.daemon,
|
|
2589
1417
|
pid: runtimeContext?.pid,
|
|
2590
|
-
session_id:
|
|
2591
|
-
|
|
1418
|
+
session_id:
|
|
1419
|
+
String(payload.session_id || runtimeContext?.session_id || "").trim() || undefined,
|
|
1420
|
+
session_file_path: payload.session_file_path || runtimeContext?.session_file_path,
|
|
2592
1421
|
token_usage_percent: runtimeContext?.token_usage_percent,
|
|
2593
1422
|
context_usage_percent: runtimeContext?.context_usage_percent,
|
|
2594
1423
|
};
|
|
@@ -2643,8 +1472,10 @@ export class BridgeRunner {
|
|
|
2643
1472
|
if (this.stopped) {
|
|
2644
1473
|
return;
|
|
2645
1474
|
}
|
|
2646
|
-
const
|
|
2647
|
-
|
|
1475
|
+
const preserveWhitespace = Boolean(payload?.preserveWhitespace);
|
|
1476
|
+
const rawText = typeof payload?.text === "string" ? payload.text : "";
|
|
1477
|
+
const text = preserveWhitespace ? rawText : rawText.trim();
|
|
1478
|
+
if (!text || (!preserveWhitespace && !text.trim())) {
|
|
2648
1479
|
return;
|
|
2649
1480
|
}
|
|
2650
1481
|
|
|
@@ -2653,11 +1484,32 @@ export class BridgeRunner {
|
|
|
2653
1484
|
const sessionFilePath =
|
|
2654
1485
|
typeof payload?.sessionFilePath === "string" ? payload.sessionFilePath.trim() : "";
|
|
2655
1486
|
|
|
2656
|
-
|
|
1487
|
+
await this.sendSessionStreamMessage({
|
|
1488
|
+
text,
|
|
1489
|
+
replyTo,
|
|
1490
|
+
sessionId,
|
|
1491
|
+
sessionFilePath,
|
|
1492
|
+
timestamp: payload?.timestamp,
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
async sendSessionStreamMessage({
|
|
1497
|
+
text,
|
|
1498
|
+
replyTo,
|
|
1499
|
+
sessionId,
|
|
1500
|
+
sessionFilePath,
|
|
1501
|
+
timestamp,
|
|
1502
|
+
}) {
|
|
1503
|
+
const normalizedText = typeof text === "string" ? text : "";
|
|
1504
|
+
if (!normalizedText) {
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
logBackendReply(this.backendName, normalizedText, {
|
|
2657
1509
|
usage: null,
|
|
2658
1510
|
replyTo: replyTo || "latest",
|
|
2659
1511
|
});
|
|
2660
|
-
await this.conductor.sendMessage(this.taskId,
|
|
1512
|
+
await this.conductor.sendMessage(this.taskId, normalizedText, {
|
|
2661
1513
|
model: this.backendSession.threadOptions?.model || this.backendName,
|
|
2662
1514
|
backend: this.backendName,
|
|
2663
1515
|
thread_id: sessionId || this.backendSession.threadId,
|
|
@@ -2666,10 +1518,15 @@ export class BridgeRunner {
|
|
|
2666
1518
|
reply_to: replyTo || undefined,
|
|
2667
1519
|
cli_args: this.cliArgs,
|
|
2668
1520
|
session_stream: true,
|
|
2669
|
-
timestamp:
|
|
1521
|
+
timestamp: timestamp || undefined,
|
|
2670
1522
|
});
|
|
1523
|
+
const replyKey = this.normalizeSessionStreamReplyKey(replyTo);
|
|
1524
|
+
this.sessionStreamReplyCounts.set(
|
|
1525
|
+
replyKey,
|
|
1526
|
+
Number(this.sessionStreamReplyCounts.get(replyKey) || 0) + 1,
|
|
1527
|
+
);
|
|
2671
1528
|
this.copilotLog(
|
|
2672
|
-
`session_file sdk_message sent replyTo=${replyTo || "latest"} responseLen=${
|
|
1529
|
+
`session_file sdk_message sent replyTo=${replyTo || "latest"} responseLen=${normalizedText.length}`,
|
|
2673
1530
|
);
|
|
2674
1531
|
}
|
|
2675
1532
|
|
|
@@ -2677,6 +1534,55 @@ export class BridgeRunner {
|
|
|
2677
1534
|
await this.reportRuntimeStatus(payload, payload?.replyTo);
|
|
2678
1535
|
}
|
|
2679
1536
|
|
|
1537
|
+
normalizeSessionStreamReplyKey(replyTo) {
|
|
1538
|
+
const normalizedReplyTo = typeof replyTo === "string" ? replyTo.trim() : "";
|
|
1539
|
+
return normalizedReplyTo || "__latest__";
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
getSessionStreamReplyCount(replyTo) {
|
|
1543
|
+
return Number(
|
|
1544
|
+
this.sessionStreamReplyCounts.get(this.normalizeSessionStreamReplyKey(replyTo)) || 0,
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
isCodexCheckpointUnavailableError(errorMessage) {
|
|
1549
|
+
return String(errorMessage || "")
|
|
1550
|
+
.toLowerCase()
|
|
1551
|
+
.includes("codex session file checkpoint unavailable");
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
async settleCodexCheckpointUnavailableAfterStream(replyTo, errorMessage, { markProcessed = true } = {}) {
|
|
1555
|
+
if (!this.useSessionFileReplyStream) {
|
|
1556
|
+
return false;
|
|
1557
|
+
}
|
|
1558
|
+
if (String(this.backendName || "").toLowerCase() !== "codex") {
|
|
1559
|
+
return false;
|
|
1560
|
+
}
|
|
1561
|
+
if (!this.isCodexCheckpointUnavailableError(errorMessage)) {
|
|
1562
|
+
return false;
|
|
1563
|
+
}
|
|
1564
|
+
const streamedReplyCount = this.getSessionStreamReplyCount(replyTo);
|
|
1565
|
+
if (streamedReplyCount <= 0) {
|
|
1566
|
+
return false;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
this.copilotLog(
|
|
1570
|
+
`suppress checkpoint unavailable after streamed replies replyTo=${replyTo || "latest"} count=${streamedReplyCount}`,
|
|
1571
|
+
);
|
|
1572
|
+
await this.reportRuntimeStatus(
|
|
1573
|
+
{
|
|
1574
|
+
phase: "session_stream_settled",
|
|
1575
|
+
reply_in_progress: false,
|
|
1576
|
+
},
|
|
1577
|
+
replyTo,
|
|
1578
|
+
);
|
|
1579
|
+
if (markProcessed && replyTo) {
|
|
1580
|
+
this.processedMessageIds.add(replyTo);
|
|
1581
|
+
}
|
|
1582
|
+
this.resetErrorLoop();
|
|
1583
|
+
return true;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
2680
1586
|
resetErrorLoop() {
|
|
2681
1587
|
this.errorLoop = null;
|
|
2682
1588
|
}
|
|
@@ -2864,6 +1770,9 @@ export class BridgeRunner {
|
|
|
2864
1770
|
);
|
|
2865
1771
|
return;
|
|
2866
1772
|
}
|
|
1773
|
+
if (await this.settleCodexCheckpointUnavailableAfterStream(replyTo, errorMessage)) {
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
2867
1776
|
this.copilotLog(
|
|
2868
1777
|
`turn failed replyTo=${replyTo || "latest"} elapsedMs=${Date.now() - turnStartedAt} error="${sanitizeForLog(errorMessage, 200)}"`,
|
|
2869
1778
|
);
|
|
@@ -2969,14 +1878,18 @@ export class BridgeRunner {
|
|
|
2969
1878
|
this.copilotLog("synthetic session_file turn settled");
|
|
2970
1879
|
}
|
|
2971
1880
|
} catch (error) {
|
|
1881
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2972
1882
|
if (this.stopped && (this.remoteStopInfo || isSessionClosedError(error))) {
|
|
2973
1883
|
this.copilotLog(`synthetic turn interrupted by stop_task elapsedMs=${Date.now() - startedAt}`);
|
|
2974
1884
|
return;
|
|
2975
1885
|
}
|
|
1886
|
+
if (await this.settleCodexCheckpointUnavailableAfterStream("initial", errorMessage, { markProcessed: false })) {
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
2976
1889
|
this.copilotLog(
|
|
2977
|
-
`synthetic turn failed elapsedMs=${Date.now() - startedAt} error="${sanitizeForLog(
|
|
1890
|
+
`synthetic turn failed elapsedMs=${Date.now() - startedAt} error="${sanitizeForLog(errorMessage, 200)}"`,
|
|
2978
1891
|
);
|
|
2979
|
-
await this.reportError(`初始提示执行失败: ${
|
|
1892
|
+
await this.reportError(`初始提示执行失败: ${errorMessage}`);
|
|
2980
1893
|
} finally {
|
|
2981
1894
|
this.copilotLog(`synthetic turn end elapsedMs=${Date.now() - startedAt}`);
|
|
2982
1895
|
this.runningTurn = false;
|