comfyui-mcp 0.52.49 → 0.52.51
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/dist/orchestrator/codex-backend.js +256 -52
- package/dist/orchestrator/codex-backend.js.map +1 -1
- package/dist/orchestrator/panel-arg-aliases.js +296 -0
- package/dist/orchestrator/panel-arg-aliases.js.map +1 -0
- package/dist/orchestrator/panel-tools.js +235 -36
- package/dist/orchestrator/panel-tools.js.map +1 -1
- package/dist/services/panel-graph-cmd-tools.js +14 -0
- package/dist/services/panel-graph-cmd-tools.js.map +1 -1
- package/package.json +1 -1
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
// thread/start has no instructions field).
|
|
39
39
|
import { spawn, spawnSync } from "node:child_process";
|
|
40
40
|
import { createRequire } from "node:module";
|
|
41
|
-
import { promises as fsp } from "node:fs";
|
|
41
|
+
import { copyFileSync, existsSync, mkdirSync, promises as fsp } from "node:fs";
|
|
42
42
|
import os from "node:os";
|
|
43
43
|
import path from "node:path";
|
|
44
44
|
import readline from "node:readline";
|
|
@@ -71,7 +71,10 @@ const configuredHostSpawnRetryMs = Number(process.env.COMFYUI_MCP_CODEX_HOST_SPA
|
|
|
71
71
|
const CODEX_HOST_SPAWN_RETRY_MS = Number.isFinite(configuredHostSpawnRetryMs) && configuredHostSpawnRetryMs >= 0
|
|
72
72
|
? configuredHostSpawnRetryMs
|
|
73
73
|
: 150;
|
|
74
|
-
|
|
74
|
+
// Two attempts: a stale WinGet path (os error 3) can be followed immediately
|
|
75
|
+
// by a sharing-violation (os error 32) while the replacement image is copied
|
|
76
|
+
// or scanned (#2045). A single retry died on that second failure.
|
|
77
|
+
const CODEX_HOST_SPAWN_RETRIES = 2;
|
|
75
78
|
async function settlesWithin(promise, timeoutMs) {
|
|
76
79
|
let timer;
|
|
77
80
|
try {
|
|
@@ -592,6 +595,139 @@ export function isWindowsCodeModeHostSharingViolation(message) {
|
|
|
592
595
|
m.includes("utilise par un autre processus") ||
|
|
593
596
|
m.includes("utilizado por otro proceso"));
|
|
594
597
|
}
|
|
598
|
+
/**
|
|
599
|
+
* #2045 — Windows ERROR_PATH_NOT_FOUND (os error 3) when WinGet deletes a
|
|
600
|
+
* versioned Node package dir (`node-vX.Y.Z-win-x64\\...`) out from under a live
|
|
601
|
+
* app-server. Distinct from os error 2 (file not found in an existing dir),
|
|
602
|
+
* which stays terminal (#1929).
|
|
603
|
+
*/
|
|
604
|
+
export function isWindowsCodeModeHostPathNotFound(message) {
|
|
605
|
+
if (!message)
|
|
606
|
+
return false;
|
|
607
|
+
const m = message.toLowerCase();
|
|
608
|
+
const isHost = m.includes("code-mode host") || m.includes("codex-code-mode-host");
|
|
609
|
+
if (!isHost)
|
|
610
|
+
return false;
|
|
611
|
+
if (/\bos error 2\b/.test(m))
|
|
612
|
+
return false;
|
|
613
|
+
return (/\bos error 3\b/.test(m) ||
|
|
614
|
+
m.includes("cannot find the path specified") ||
|
|
615
|
+
m.includes("chemin d'acces specifie est introuvable") ||
|
|
616
|
+
m.includes("chemin d'accès spécifié est introuvable") ||
|
|
617
|
+
m.includes("no puede encontrar la ruta"));
|
|
618
|
+
}
|
|
619
|
+
export function isWindowsCodeModeHostSpawnRetryable(message) {
|
|
620
|
+
return isWindowsCodeModeHostSharingViolation(message) || isWindowsCodeModeHostPathNotFound(message);
|
|
621
|
+
}
|
|
622
|
+
const CODEX_PLATFORM_PACKAGE = {
|
|
623
|
+
"win32-x64": { pkg: "@openai/codex-win32-x64", triple: "x86_64-pc-windows-msvc" },
|
|
624
|
+
"win32-arm64": { pkg: "@openai/codex-win32-arm64", triple: "aarch64-pc-windows-msvc" },
|
|
625
|
+
"darwin-x64": { pkg: "@openai/codex-darwin-x64", triple: "x86_64-apple-darwin" },
|
|
626
|
+
"darwin-arm64": { pkg: "@openai/codex-darwin-arm64", triple: "aarch64-apple-darwin" },
|
|
627
|
+
"linux-x64": { pkg: "@openai/codex-linux-x64", triple: "x86_64-unknown-linux-musl" },
|
|
628
|
+
"linux-arm64": { pkg: "@openai/codex-linux-arm64", triple: "aarch64-unknown-linux-musl" },
|
|
629
|
+
};
|
|
630
|
+
function vendorHostName(platform = process.platform) {
|
|
631
|
+
return platform === "win32" ? "codex-code-mode-host.exe" : "codex-code-mode-host";
|
|
632
|
+
}
|
|
633
|
+
function vendorExeName(platform = process.platform) {
|
|
634
|
+
return platform === "win32" ? "codex.exe" : "codex";
|
|
635
|
+
}
|
|
636
|
+
function platformOfHostPath(hostPath) {
|
|
637
|
+
return hostPath.toLowerCase().endsWith(".exe") ? "win32" : process.platform;
|
|
638
|
+
}
|
|
639
|
+
/** WinGet's versioned Node dir and nvm-windows layouts vanish on an update. */
|
|
640
|
+
export function isVolatileCodexPackagePath(p) {
|
|
641
|
+
const n = p.replace(/\\/g, "/").toLowerCase();
|
|
642
|
+
return (n.includes("/winget/packages/") ||
|
|
643
|
+
/\/node-v\d+\.\d+\.\d+-win/.test(n) ||
|
|
644
|
+
/\/nvm\/v?\d/.test(n) ||
|
|
645
|
+
n.includes("/.nvm/"));
|
|
646
|
+
}
|
|
647
|
+
export function codeModeHostPathFromError(message) {
|
|
648
|
+
const m = message.match(/code-mode host\s+(.+?):\s/i);
|
|
649
|
+
const p = m?.[1]?.trim();
|
|
650
|
+
return p || null;
|
|
651
|
+
}
|
|
652
|
+
export function resolveCodexCodeModeHostPath(opts = {}) {
|
|
653
|
+
const platform = opts.platform ?? process.platform;
|
|
654
|
+
const arch = opts.arch ?? process.arch;
|
|
655
|
+
const spec = CODEX_PLATFORM_PACKAGE[`${platform}-${arch}`];
|
|
656
|
+
if (!spec)
|
|
657
|
+
return null;
|
|
658
|
+
const exists = opts.existsSync ?? existsSync;
|
|
659
|
+
const resolve = opts.resolve ?? ((id) => createRequire(import.meta.url).resolve(id));
|
|
660
|
+
try {
|
|
661
|
+
const pkgJson = resolve(`${spec.pkg}/package.json`);
|
|
662
|
+
const host = path.join(path.dirname(pkgJson), "vendor", spec.triple, "bin", vendorHostName(platform));
|
|
663
|
+
return exists(host) ? host : null;
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
export function stableCodexVendorDir(home = os.homedir()) {
|
|
670
|
+
const override = process.env.COMFYUI_MCP_CODEX_VENDOR_DIR?.trim();
|
|
671
|
+
return override || path.join(home, ".comfyui-mcp", "codex-vendor");
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Copy the vendor `codex` + `codex-code-mode-host` pair out of a volatile
|
|
675
|
+
* WinGet/nvm package dir into a stable location. If the source is already gone
|
|
676
|
+
* or the dest is locked (os error 32), keep a usable dest copy.
|
|
677
|
+
*/
|
|
678
|
+
export function ensureStableCodexVendor(srcHost, opts = {}) {
|
|
679
|
+
const exists = opts.existsSync ?? existsSync;
|
|
680
|
+
const mkdir = opts.mkdirSync ?? mkdirSync;
|
|
681
|
+
const copy = opts.copyFileSync ?? copyFileSync;
|
|
682
|
+
const destDir = opts.destDir ?? stableCodexVendorDir();
|
|
683
|
+
const plat = platformOfHostPath(srcHost);
|
|
684
|
+
const destHost = path.join(destDir, vendorHostName(plat));
|
|
685
|
+
const destExe = path.join(destDir, vendorExeName(plat));
|
|
686
|
+
const srcExe = path.join(path.dirname(srcHost), vendorExeName(plat));
|
|
687
|
+
const destUsable = exists(destHost) && exists(destExe);
|
|
688
|
+
if (!exists(srcHost) || !exists(srcExe))
|
|
689
|
+
return destUsable ? destHost : null;
|
|
690
|
+
try {
|
|
691
|
+
mkdir(destDir, { recursive: true });
|
|
692
|
+
copy(srcExe, destExe);
|
|
693
|
+
copy(srcHost, destHost);
|
|
694
|
+
return exists(destHost) && exists(destExe) ? destHost : destUsable ? destHost : null;
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
return destUsable ? destHost : null;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
export function resolveCodexLaunchBin(opts = {}) {
|
|
701
|
+
const exists = opts.existsSync ?? existsSync;
|
|
702
|
+
const host = (opts.resolveHost ?? (() => resolveCodexCodeModeHostPath({ existsSync: exists })))();
|
|
703
|
+
if (host && isVolatileCodexPackagePath(host)) {
|
|
704
|
+
const ensure = opts.ensureStable ?? ((src) => ensureStableCodexVendor(src));
|
|
705
|
+
const stableHost = ensure(host);
|
|
706
|
+
if (stableHost) {
|
|
707
|
+
const exe = path.join(path.dirname(stableHost), vendorExeName(platformOfHostPath(stableHost)));
|
|
708
|
+
if (exists(exe))
|
|
709
|
+
return exe;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
try {
|
|
713
|
+
const js = opts.resolveJs?.() ??
|
|
714
|
+
(() => {
|
|
715
|
+
const require = createRequire(import.meta.url);
|
|
716
|
+
const pkgPath = require.resolve("@openai/codex/package.json");
|
|
717
|
+
const pkg = require("@openai/codex/package.json");
|
|
718
|
+
const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.codex;
|
|
719
|
+
if (!binRel)
|
|
720
|
+
return null;
|
|
721
|
+
return path.join(pkgPath.replace(/[\\/]package\.json$/, ""), binRel.replace(/^\.[\\/]/, ""));
|
|
722
|
+
})();
|
|
723
|
+
if (js && exists(js))
|
|
724
|
+
return js;
|
|
725
|
+
}
|
|
726
|
+
catch {
|
|
727
|
+
// bundled package not installed — fall through to PATH.
|
|
728
|
+
}
|
|
729
|
+
return "codex";
|
|
730
|
+
}
|
|
595
731
|
/**
|
|
596
732
|
* The Codex app-server adapter. One instance per PanelAgent; it holds the live
|
|
597
733
|
* app-server client + current thread/turn ids and re-opens on each `run()`.
|
|
@@ -676,31 +812,25 @@ export class CodexBackend {
|
|
|
676
812
|
return closing;
|
|
677
813
|
}
|
|
678
814
|
/**
|
|
679
|
-
* Resolve the codex binary: prefer the
|
|
680
|
-
*
|
|
681
|
-
*
|
|
815
|
+
* Resolve the codex binary: prefer a stable copy of the vendor exe when the
|
|
816
|
+
* active package lives under a WinGet/nvm versioned dir (#2045), else the
|
|
817
|
+
* bundled `@openai/codex` launcher, else a `codex` on PATH. A cached path
|
|
818
|
+
* that no longer exists is dropped so the next spawn cannot keep a stale
|
|
819
|
+
* WinGet Node folder.
|
|
682
820
|
*/
|
|
683
821
|
resolveBin() {
|
|
684
|
-
if (this.bin)
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
const pkg = require("@openai/codex/package.json");
|
|
692
|
-
const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.codex;
|
|
693
|
-
if (binRel) {
|
|
694
|
-
const sep = pkgPath.includes("\\") ? "\\" : "/";
|
|
695
|
-
const pkgDir = pkgPath.replace(/[\\/]package\.json$/, "");
|
|
696
|
-
this.bin = `${pkgDir}${sep}${binRel.replace(/^\.[\\/]/, "")}`;
|
|
822
|
+
if (this.bin && this.bin !== "codex") {
|
|
823
|
+
if (!existsSync(this.bin))
|
|
824
|
+
this.bin = null;
|
|
825
|
+
else if (!/\.(c|m)?js$/i.test(this.bin)) {
|
|
826
|
+
const host = path.join(path.dirname(this.bin), vendorHostName(platformOfHostPath(this.bin)));
|
|
827
|
+
if (!existsSync(host))
|
|
828
|
+
this.bin = null;
|
|
697
829
|
}
|
|
698
830
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
if (!this.bin)
|
|
703
|
-
this.bin = "codex"; // PATH fallback (a `codex` on PATH)
|
|
831
|
+
if (this.bin)
|
|
832
|
+
return this.bin;
|
|
833
|
+
this.bin = resolveCodexLaunchBin();
|
|
704
834
|
return this.bin;
|
|
705
835
|
}
|
|
706
836
|
/**
|
|
@@ -967,11 +1097,16 @@ export class CodexBackend {
|
|
|
967
1097
|
// signal — every raw app-server notification for the active turn re-arms
|
|
968
1098
|
// PanelAgent's idle watchdog so a long, quiet generation doesn't falsely trip.
|
|
969
1099
|
let turnSeq = 0;
|
|
1100
|
+
let liveClient = client;
|
|
970
1101
|
for await (const turn of opts.channel) {
|
|
971
|
-
|
|
972
|
-
//
|
|
973
|
-
//
|
|
974
|
-
if (this.client
|
|
1102
|
+
// A timed-out interrupt nulls this.client — stop draining so PanelAgent
|
|
1103
|
+
// can start a fresh app-server. A stale-host recycle (#2045) replaces
|
|
1104
|
+
// the client in place; keep going on the replacement.
|
|
1105
|
+
if (!this.client)
|
|
1106
|
+
return;
|
|
1107
|
+
liveClient = this.client;
|
|
1108
|
+
yield* stampTurn(this.runTurn(liveClient, turn, opts.onActivity), ++turnSeq);
|
|
1109
|
+
if (!this.client)
|
|
975
1110
|
return;
|
|
976
1111
|
}
|
|
977
1112
|
}
|
|
@@ -982,6 +1117,7 @@ export class CodexBackend {
|
|
|
982
1117
|
* so a stale/interleaved same-thread notification can't complete the wrong turn. */
|
|
983
1118
|
async *runTurn(client, turn, onActivity) {
|
|
984
1119
|
const threadId = this.threadId;
|
|
1120
|
+
let liveClient = client;
|
|
985
1121
|
// Event queue bridging the push-based notification handler to this pull-based
|
|
986
1122
|
// async generator. The handler enqueues normalized AgentEvents; we drain.
|
|
987
1123
|
const queue = [];
|
|
@@ -1117,16 +1253,38 @@ export class CodexBackend {
|
|
|
1117
1253
|
// Assigned after turn input is built — the retry timer only fires after a
|
|
1118
1254
|
// turn/start has already run, so this is never invoked while still a no-op.
|
|
1119
1255
|
let issueTurnStart = () => { };
|
|
1256
|
+
let handler = () => { };
|
|
1257
|
+
let hostRecycleInFlight = false;
|
|
1258
|
+
const watchExit = (watched) => {
|
|
1259
|
+
void watched.exitPromise.then(() => {
|
|
1260
|
+
if (done)
|
|
1261
|
+
return;
|
|
1262
|
+
if (watched !== liveClient)
|
|
1263
|
+
return;
|
|
1264
|
+
// Closing the old app-server is the point of a stale-host recycle;
|
|
1265
|
+
// its exit must not finish the replacement turn (#2045).
|
|
1266
|
+
if (hostRecycleInFlight)
|
|
1267
|
+
return;
|
|
1268
|
+
emitTerminalError(watched.exitError ? msgOf(watched.exitError) : "codex app-server connection closed.");
|
|
1269
|
+
});
|
|
1270
|
+
};
|
|
1120
1271
|
const tryRetryCodeModeHostSpawn = (message) => {
|
|
1121
1272
|
if (interrupted || finishedResult || retryPending)
|
|
1122
1273
|
return false;
|
|
1123
1274
|
if (hostSpawnRetries >= CODEX_HOST_SPAWN_RETRIES)
|
|
1124
1275
|
return false;
|
|
1125
|
-
if (!
|
|
1276
|
+
if (!isWindowsCodeModeHostSpawnRetryable(message))
|
|
1126
1277
|
return false;
|
|
1127
1278
|
hostSpawnRetries += 1;
|
|
1128
1279
|
retryPending = true;
|
|
1129
|
-
|
|
1280
|
+
const reportedHost = codeModeHostPathFromError(message);
|
|
1281
|
+
const recycle = isWindowsCodeModeHostPathNotFound(message) &&
|
|
1282
|
+
typeof this.bin === "string" &&
|
|
1283
|
+
this.bin !== "codex" &&
|
|
1284
|
+
(!reportedHost || !existsSync(reportedHost) || isVolatileCodexPackagePath(reportedHost));
|
|
1285
|
+
logger.warn(recycle
|
|
1286
|
+
? `[codex-backend] code-mode host spawn hit a stale path; recycling the app-server then retrying after ${CODEX_HOST_SPAWN_RETRY_MS}ms (#2045)`
|
|
1287
|
+
: `[codex-backend] code-mode host spawn failed; retrying after ${CODEX_HOST_SPAWN_RETRY_MS}ms (#1929/#2045)`);
|
|
1130
1288
|
try {
|
|
1131
1289
|
onActivity?.();
|
|
1132
1290
|
}
|
|
@@ -1135,13 +1293,63 @@ export class CodexBackend {
|
|
|
1135
1293
|
}
|
|
1136
1294
|
retryTimer = setTimeout(() => {
|
|
1137
1295
|
retryTimer = undefined;
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1296
|
+
void (async () => {
|
|
1297
|
+
if (interrupted || finishedResult || this.disposed) {
|
|
1298
|
+
retryPending = false;
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (recycle) {
|
|
1302
|
+
hostRecycleInFlight = true;
|
|
1303
|
+
this.bin = null;
|
|
1304
|
+
if (this.client === liveClient)
|
|
1305
|
+
this.client = null;
|
|
1306
|
+
await this.beginClientClose(liveClient, "stale code-mode host recycle teardown failed");
|
|
1307
|
+
if (interrupted || finishedResult || this.disposed) {
|
|
1308
|
+
hostRecycleInFlight = false;
|
|
1309
|
+
retryPending = false;
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
try {
|
|
1313
|
+
await this.prepare();
|
|
1314
|
+
}
|
|
1315
|
+
catch (err) {
|
|
1316
|
+
hostRecycleInFlight = false;
|
|
1317
|
+
retryPending = false;
|
|
1318
|
+
emitTerminalError(msgOf(err));
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
const next = this.client;
|
|
1322
|
+
if (!next) {
|
|
1323
|
+
hostRecycleInFlight = false;
|
|
1324
|
+
retryPending = false;
|
|
1325
|
+
emitTerminalError("codex app-server not initialized");
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
liveClient = next;
|
|
1329
|
+
hostRecycleInFlight = false;
|
|
1330
|
+
liveClient.notificationHandler = handler;
|
|
1331
|
+
watchExit(liveClient);
|
|
1332
|
+
try {
|
|
1333
|
+
await liveClient.request("thread/resume", {
|
|
1334
|
+
threadId,
|
|
1335
|
+
cwd: this.deps.cwd ?? process.cwd(),
|
|
1336
|
+
model: this.resolveTurnModel(),
|
|
1337
|
+
approvalPolicy: "never",
|
|
1338
|
+
sandbox: this.sandbox,
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
catch {
|
|
1342
|
+
// Thread may not survive a process recycle; still retry the turn.
|
|
1343
|
+
}
|
|
1344
|
+
if (interrupted || finishedResult || this.disposed) {
|
|
1345
|
+
retryPending = false;
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
turnIdKnown = false;
|
|
1350
|
+
buffered.length = 0;
|
|
1351
|
+
issueTurnStart();
|
|
1352
|
+
})();
|
|
1145
1353
|
}, CODEX_HOST_SPAWN_RETRY_MS);
|
|
1146
1354
|
retryTimer.unref?.();
|
|
1147
1355
|
return true;
|
|
@@ -1254,8 +1462,8 @@ export class CodexBackend {
|
|
|
1254
1462
|
if (params.willRetry === true)
|
|
1255
1463
|
break;
|
|
1256
1464
|
const message = e.message ?? "Codex error";
|
|
1257
|
-
// #1929 — a Windows sharing-violation
|
|
1258
|
-
// the
|
|
1465
|
+
// #1929 / #2045 — a Windows sharing-violation or stale WinGet path on
|
|
1466
|
+
// the bundled code-mode host is the same transient /restart recovered.
|
|
1259
1467
|
if (tryRetryCodeModeHostSpawn(message))
|
|
1260
1468
|
break;
|
|
1261
1469
|
// A non-retrying `error` ends the turn: emit it AND finish, so a turn that
|
|
@@ -1277,8 +1485,8 @@ export class CodexBackend {
|
|
|
1277
1485
|
break;
|
|
1278
1486
|
}
|
|
1279
1487
|
};
|
|
1280
|
-
const prev =
|
|
1281
|
-
|
|
1488
|
+
const prev = liveClient.notificationHandler;
|
|
1489
|
+
handler = (msg) => {
|
|
1282
1490
|
// Until the turnId is known, buffer everything — we can't yet tell which
|
|
1283
1491
|
// turn a notification belongs to, so we can't yet decide whether it counts
|
|
1284
1492
|
// as liveness either. Replayed (and bumped) after turn/start resolves, so
|
|
@@ -1294,21 +1502,16 @@ export class CodexBackend {
|
|
|
1294
1502
|
}
|
|
1295
1503
|
apply(msg);
|
|
1296
1504
|
};
|
|
1505
|
+
liveClient.notificationHandler = handler;
|
|
1297
1506
|
// Watch for the app-server child dying mid-turn: reject/finish the turn so the
|
|
1298
1507
|
// local drain below is woken instead of waiting forever (P0-2). Crucially this
|
|
1299
1508
|
// ALWAYS routes through emitTerminalError so even a child that dies while
|
|
1300
1509
|
// turn/start is still pending leaves the turn with a terminal `result` — the
|
|
1301
1510
|
// turn-start .catch() running first (rejecting the pending request) no longer
|
|
1302
1511
|
// lets this watcher finish() without a result and hang the gate (P0-B).
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
// emitTerminalError is a no-op if a result already fired, so it's safe to
|
|
1307
|
-
// call alongside the turn-start .catch() (which may run first when the child
|
|
1308
|
-
// dies while turn/start is pending) — it guarantees the turn still ends with
|
|
1309
|
-
// exactly one terminal result and never hangs the gate (P0-B).
|
|
1310
|
-
emitTerminalError(client.exitError ? msgOf(client.exitError) : "codex app-server connection closed.");
|
|
1311
|
-
});
|
|
1512
|
+
// A stale-host recycle (#2045) replaces liveClient; the old child's exit
|
|
1513
|
+
// must not finish the replacement turn.
|
|
1514
|
+
watchExit(liveClient);
|
|
1312
1515
|
// FIRST-TURN PERSONA: the app-server has no thread-level instructions field,
|
|
1313
1516
|
// so the panel system prompt is prepended to the first turn's input as a
|
|
1314
1517
|
// clearly-marked system/context preamble (later turns send plain text).
|
|
@@ -1372,7 +1575,7 @@ export class CodexBackend {
|
|
|
1372
1575
|
issueTurnStart = () => {
|
|
1373
1576
|
// turn/start delivers the user text plus any resolved image input items.
|
|
1374
1577
|
const turnModel = this.resolveTurnModel();
|
|
1375
|
-
|
|
1578
|
+
liveClient
|
|
1376
1579
|
.request("turn/start", {
|
|
1377
1580
|
threadId,
|
|
1378
1581
|
input: turnInput,
|
|
@@ -1461,8 +1664,9 @@ export class CodexBackend {
|
|
|
1461
1664
|
this.abortActiveTurn = null;
|
|
1462
1665
|
// Restore the prior handler ONLY if it's still ours (close() may have nulled
|
|
1463
1666
|
// it during shutdown — don't resurrect a stale handler onto a dead client).
|
|
1464
|
-
if (
|
|
1465
|
-
|
|
1667
|
+
if (liveClient.notificationHandler && !liveClient.exitError) {
|
|
1668
|
+
liveClient.notificationHandler = prev ?? null;
|
|
1669
|
+
}
|
|
1466
1670
|
this.turnId = null;
|
|
1467
1671
|
// #1152 — the app-server does NOT necessarily read a localImage at
|
|
1468
1672
|
// turn/start, which is what this used to assume ("the bytes were read at
|