From 3ab484ddc00043b2078042b010018a19a08db358 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Fri, 30 Oct 2026 16:06:20 +0700 Subject: [PATCH] fix(pool): a Codex ChatGPT pool pane runs on the login placed for it Codex >= 0.164 hands a plain TUI (no -c override) to the CODEX_HOME's shared app-server daemon, which outlives every TUI and keeps the login it started with. A ChatGPT pool account written into a member's auth.json therefore reached no pane while their daemon still ran on the previous login (reproduced on 0.164.0: the daemon kept sending the old key, a --no-daemon TUI sent the new one). Such a pane now runs `codex --no-daemon`, only when that binary's --help lists the flag (probed once per path/size/mtime), so an older Codex still starts. --- docs/SERVER-MODE.md | 4 +++ server/__tests__/codex-pool.test.js | 28 +++++++++++++++++- server/lib/codex-pool.js | 46 +++++++++++++++++++++++++++-- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/docs/SERVER-MODE.md b/docs/SERVER-MODE.md index 5c38c2f4..64bc5b94 100644 --- a/docs/SERVER-MODE.md +++ b/docs/SERVER-MODE.md @@ -555,6 +555,10 @@ agenthub accounts pin team-key mia --tool codex # pins work as for Claude is used only for a pane that runs as its developer (one Linux user per account): AgentHub writes the account's `auth.json` into that developer's `~/.codex` as them (0600) just before the pane starts. + - The pane runs `codex --no-daemon` (when that Codex lists the flag in + `--help`): Codex ≥ 0.164 otherwise hands a plain session to the home's + shared app-server daemon, which outlives every pane and keeps the login + it started with — the new `auth.json` would not be used. - A host pane never gets one: its `CODEX_HOME` holds the host's own login. - A developer who signed Codex in themselves keeps their own login. - While a pane of theirs runs on a pool account, their next pane uses the diff --git a/server/__tests__/codex-pool.test.js b/server/__tests__/codex-pool.test.js index a069eada..f00e6468 100644 --- a/server/__tests__/codex-pool.test.js +++ b/server/__tests__/codex-pool.test.js @@ -243,6 +243,29 @@ describe("codex pool: spawn", () => { assert.equal(row.account, "c"); }); + it("runs a ChatGPT pane without the shared daemon, only when its Codex has the flag", () => { + // The home's app-server daemon keeps the login it started with: a placed + // login reaches only a pane that runs its own app server. + const fakeCodex = (name, help) => { + const file = path.join(TMP, name); + fs.writeFileSync(file, `#!/bin/sh\necho '${help}'\n`, { mode: 0o755 }); + return file; + }; + const prev = process.env.AGENTHUB_CODEX_BIN; + try { + pool.addAccount("c", { auth: AUTH("acct-c") }); + process.env.AGENTHUB_CODEX_BIN = fakeCodex("codex-new", " --no-daemon Run without"); + assert.deepEqual(pool.forSpawn(MIA).argv, ["--no-daemon"]); + process.env.AGENTHUB_CODEX_BIN = fakeCodex("codex-old", " --oss Use a local model"); + assert.deepEqual(pool.forSpawn(MIA).argv, [], "an older Codex would refuse the flag"); + process.env.AGENTHUB_CODEX_BIN = path.join(TMP, "no-such-codex"); + assert.deepEqual(pool.forSpawn(MIA).argv, []); + } finally { + if (prev === undefined) delete process.env.AGENTHUB_CODEX_BIN; + else process.env.AGENTHUB_CODEX_BIN = prev; + } + }); + it("never replaces a member's own login, and stays on the account their live pane uses", () => { pool.addAccount("c", { auth: AUTH("acct-c") }); pool.addAccount("d", { auth: AUTH("acct-d") }); @@ -272,7 +295,10 @@ describe("codex pool: spawn", () => { fs.writeFileSync(MIA_AUTH, JSON.stringify(own)); assert.equal(pool.pickAccount(MIA), null); pty.spawnRun({ laneId: 50, cwd: path.join(MIA.home, "p"), tool: "codex" }); - assert.equal(JSON.parse(fs.readFileSync(MIA_AUTH, "utf8")).tokens.refresh_token, "rt-mias-seat"); + assert.equal( + JSON.parse(fs.readFileSync(MIA_AUTH, "utf8")).tokens.refresh_token, + "rt-mias-seat" + ); }); it("copies the newest auth.json back into the pool", () => { diff --git a/server/lib/codex-pool.js b/server/lib/codex-pool.js index f706478f..06e00b08 100644 --- a/server/lib/codex-pool.js +++ b/server/lib/codex-pool.js @@ -19,7 +19,9 @@ * home AgentHub placed that account in, never dated in the future, and * only when its id_token names the same account (`mayCopyBack`); the 2026-10-08 * spike showed an old refresh token still refreshes, so panes sharing an - * account do not log each other out. + * account do not log each other out. Such a pane runs `--no-daemon` + * (when its Codex has the flag): the home's shared app-server daemon + * would keep serving the login it started with. * * Records live in `/agenthub-codex-accounts/.json` (dir * 0700, file 0600); no route returns a key or a token. @@ -424,6 +426,46 @@ function liveInHome(name, member) { }); } +// Codex ≥ 0.164 hands a plain TUI (no `-c` override) to the CODEX_HOME's shared +// app-server daemon, which outlives every TUI and keeps the login it started +// with — so a ChatGPT login placed in the home reached no pane while the +// member's daemon ran on the previous one (reproduced on 0.164.0). A pool pane +// runs its own app server instead. An older Codex has neither the daemon nor +// the flag (it would refuse to start), so the flag goes only to a binary whose +// --help lists it: probed once per binary (path, size, mtime). +const noDaemonCache = new Map(); + +function noDaemonArgv(member) { + const bin = + process.env.AGENTHUB_CODEX_BIN || + (member + ? require("./shared-tools").sharedPath("codex") + : require("./ai-tools").detect("codex").path); + if (!bin) return []; + let key; + try { + const st = fs.statSync(bin); + key = `${fs.realpathSync(bin)}:${st.size}:${st.mtimeMs}`; + } catch { + return []; + } + if (!noDaemonCache.has(key)) { + let help = ""; + try { + help = require("node:child_process").execFileSync(bin, ["--help"], { + encoding: "utf8", + timeout: 5000, + stdio: ["ignore", "pipe", "ignore"], + windowsHide: true, + }); + } catch { + /* no answer: leave the flag out, as before */ + } + noDaemonCache.set(key, /--no-daemon\b/.test(help)); + } + return noDaemonCache.get(key) ? ["--no-daemon"] : []; +} + /** `{name, kind}` of the account a new Codex pane would get, or null. */ function pickAccount(member = null, ownerId = null) { const a = pick(member, ownerId); @@ -449,7 +491,7 @@ function forSpawn(member = null, ownerId = null) { } return { name: a.name, - argv: [], + argv: noDaemonArgv(member), env: {}, place: () => { syncHome(member.home); // a live pane may hold a newer login than ours