tokenmaxxing 1.8.0 → 1.9.0

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.
Files changed (85) hide show
  1. package/DESIGN.md +2 -4
  2. package/README.md +1 -1
  3. package/agent-plugin/plugin.json +1 -1
  4. package/package.json +1 -1
  5. package/src/cli/add.ts +1 -8
  6. package/src/cli/auth.ts +0 -23
  7. package/src/cli/check.ts +19 -13
  8. package/src/cli/codexadd.ts +0 -17
  9. package/src/cli/codexinit.ts +0 -40
  10. package/src/cli/codexrm.ts +0 -13
  11. package/src/cli/codexswitch.ts +0 -15
  12. package/src/cli/config.ts +0 -30
  13. package/src/cli/doctor.ts +1 -14
  14. package/src/cli/init.ts +1 -33
  15. package/src/cli/ls.ts +0 -2
  16. package/src/cli/onboard.ts +0 -37
  17. package/src/cli/rename.ts +0 -19
  18. package/src/cli/render.ts +0 -23
  19. package/src/cli/rm.ts +0 -19
  20. package/src/cli/status.ts +0 -80
  21. package/src/cli/switch.ts +1 -49
  22. package/src/cli/watch.ts +0 -17
  23. package/src/entries/codexstophook.ts +2 -73
  24. package/src/entries/codexsupervisor.ts +1 -67
  25. package/src/entries/mcp.ts +0 -11
  26. package/src/entries/sessionstart.ts +1 -8
  27. package/src/entries/statusline.ts +0 -66
  28. package/src/entries/stopfailurehook.ts +93 -0
  29. package/src/entries/stophook.ts +3 -34
  30. package/src/entries/subagentstatusline.ts +0 -19
  31. package/src/entries/supervisor.ts +32 -132
  32. package/src/lib/atomic.ts +0 -16
  33. package/src/lib/claudebin.ts +4 -55
  34. package/src/lib/claudejson.ts +0 -10
  35. package/src/lib/claudelock.ts +13 -35
  36. package/src/lib/codexauth.ts +0 -29
  37. package/src/lib/codexbin.ts +0 -10
  38. package/src/lib/codexdecide.ts +1 -112
  39. package/src/lib/codexoauth.ts +0 -16
  40. package/src/lib/codexpick.ts +0 -31
  41. package/src/lib/codexpresence.ts +0 -35
  42. package/src/lib/codexsample.ts +0 -23
  43. package/src/lib/codexstate.ts +0 -7
  44. package/src/lib/codexswap.ts +0 -32
  45. package/src/lib/codexusage.ts +0 -28
  46. package/src/lib/credstore.ts +0 -24
  47. package/src/lib/decide.ts +127 -180
  48. package/src/lib/http.ts +0 -9
  49. package/src/lib/install.ts +6 -127
  50. package/src/lib/keychain.ts +1 -39
  51. package/src/lib/lock.ts +0 -24
  52. package/src/lib/log.ts +0 -14
  53. package/src/lib/oauth.ts +1 -31
  54. package/src/lib/paths.ts +1 -48
  55. package/src/lib/picker.ts +1 -84
  56. package/src/lib/proc.ts +0 -17
  57. package/src/lib/sample.ts +0 -68
  58. package/src/lib/sessions.ts +0 -13
  59. package/src/lib/settings.ts +15 -42
  60. package/src/lib/state.ts +23 -77
  61. package/src/lib/swap.ts +3 -87
  62. package/src/lib/tty.ts +0 -4
  63. package/src/lib/types.ts +13 -140
  64. package/src/lib/usage.ts +108 -196
  65. package/src/lib/worktree.ts +0 -8
  66. package/src/main.ts +5 -40
  67. package/src/sdk.ts +0 -59
  68. package/agent-plugin/agents/tokenmaxxing-claude.md +0 -43
  69. package/agent-plugin/agents/tokenmaxxing-codex.md +0 -40
  70. package/agent-plugin/hooks/cursor-relay.json +0 -14
  71. package/agent-plugin/skills/relay-session/SKILL.md +0 -118
  72. package/agent-plugin/skills/relay-session/references/ipc.md +0 -23
  73. package/src/cli/relay.ts +0 -323
  74. package/src/entries/relaypermission.ts +0 -105
  75. package/src/lib/relay/config.ts +0 -84
  76. package/src/lib/relay/decide.ts +0 -75
  77. package/src/lib/relay/gc.ts +0 -80
  78. package/src/lib/relay/install.ts +0 -143
  79. package/src/lib/relay/markers.ts +0 -148
  80. package/src/lib/relay/modes.ts +0 -82
  81. package/src/lib/relay/protocol.ts +0 -61
  82. package/src/lib/relay/registry.ts +0 -175
  83. package/src/lib/relay/tmux.ts +0 -109
  84. package/src/lib/relay/turn.ts +0 -137
  85. package/src/lib/relay/worker.ts +0 -141
@@ -1,6 +1,3 @@
1
- // Read/replace the `oauthAccount` identity object in ~/.claude.json.
2
- // We touch ONLY oauthAccount and preserve every other key verbatim.
3
-
4
1
  import { existsSync, readFileSync } from "node:fs";
5
2
  import { z } from "zod";
6
3
  import { paths } from "./paths.ts";
@@ -19,20 +16,13 @@ export function readOAuthAccount(): OAuthAccount | null {
19
16
  return parsed.success ? parsed.data : null;
20
17
  }
21
18
 
22
- /** Detect API-key / helper auth mode (no poolable subscription credential). */
23
19
  export function isApiKeyMode(): boolean {
24
20
  if (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN) return true;
25
21
  const j = readClaudeJson();
26
- // apiKeyHelper is configured in settings, but a persisted flag may appear here too.
27
22
  if (z.string().min(1).safeParse(j["apiKeyHelper"]).success) return true;
28
23
  return false;
29
24
  }
30
25
 
31
- /**
32
- * Atomically replace ONLY oauthAccount, preserving all other keys and their
33
- * insertion order. Reads fresh from disk so we never clobber concurrent edits
34
- * to unrelated keys with a stale in-memory copy.
35
- */
36
26
  export function swapOAuthAccount(next: OAuthAccount): void {
37
27
  const j = readClaudeJson();
38
28
  j["oauthAccount"] = next;
@@ -1,22 +1,3 @@
1
- // Interlock with Claude Code's OWN credential-refresh locks so our live-store
2
- // writes can't interleave with a concurrent token refresh. Binary-verified
3
- // against 2.1.214's embedded refresh code: claude takes TWO proper-lockfile
4
- // (mkdir-based) locks rooted at its credentials dir - the primary
5
- // <credDir>/.oauth_refresh.lock plus a legacy sibling <realpath(credDir)>.lock
6
- // (~/.claude.lock by default, kept for coordination with older versions) -
7
- // with stale: 60s, a 5s lock-mtime heartbeat while held, and on contention it
8
- // retries 5x with 1-2s jitter then FAILS the refresh (lock_timeout) instead of
9
- // proceeding unlocked. We mirror all of that, including the failure: a lock
10
- // still held past the retries means a refresh is mid-flight, which is exactly
11
- // when a live-store write could clobber a rotation. Callers retry next tick.
12
- //
13
- // Theft is detected the way proper-lockfile does it: the heartbeat re-stats
14
- // each lock and compares against the mtime WE last stored - a deviation or a
15
- // vanished dir means someone reclaimed the lock (only possible if this process
16
- // stalled past the 60s stale bar). fn gets a `compromised()` probe to check
17
- // before persisting, release skips dirs we no longer own, and the wrapper
18
- // throws terminally so no caller trusts work done on a stolen lock.
19
-
20
1
  import { mkdirSync, realpathSync, rmdirSync, statSync, utimesSync } from "node:fs";
21
2
  import { join } from "node:path";
22
3
  import { delay } from "es-toolkit";
@@ -24,12 +5,11 @@ import { z } from "zod";
24
5
  import { credDir } from "./paths.ts";
25
6
  import { log } from "./log.ts";
26
7
 
27
- const STALE_MS = 60_000; // claude's own proper-lockfile `stale`
28
- const HEARTBEAT_MS = 5_000; // claude's own `update` cadence
29
- const ATTEMPTS = 5; // claude's own ELOCKED retry budget
30
- const RETRY_MS = 1_000; // claude sleeps 1000 + rand(1000) between attempts
8
+ const STALE_MS = 60_000;
9
+ const HEARTBEAT_MS = 5_000;
10
+ const ATTEMPTS = 5;
11
+ const RETRY_MS = 1_000;
31
12
 
32
- /** mkdir-take one lock dir, reclaiming it when older than claude's stale bar. */
33
13
  function tryAcquire(lockDir: string): boolean {
34
14
  try {
35
15
  mkdirSync(lockDir);
@@ -44,7 +24,7 @@ function tryAcquire(lockDir: string): boolean {
44
24
  mkdirSync(lockDir);
45
25
  return true;
46
26
  }
47
- } catch { /* raced: another actor reclaimed it first */ }
27
+ } catch { }
48
28
  return false;
49
29
  }
50
30
 
@@ -55,13 +35,13 @@ export async function withClaudeRefreshLock<T>(
55
35
  const attempts = opts.attempts ?? ATTEMPTS;
56
36
  const retryMs = opts.retryMs ?? RETRY_MS;
57
37
  const dir = credDir();
58
- mkdirSync(dir, { recursive: true }); // claude mkdirs it before locking too
38
+ mkdirSync(dir, { recursive: true });
59
39
  const primary = join(dir, ".oauth_refresh.lock");
60
40
  let legacyRoot = dir;
61
- try { legacyRoot = realpathSync(dir); } catch { /* unresolvable: lock the raw path, like claude */ }
41
+ try { legacyRoot = realpathSync(dir); } catch { }
62
42
  const legacy = `${legacyRoot}.lock`;
63
43
 
64
- const held: string[] = []; // in release order: legacy first, then primary (claude's order)
44
+ const held: string[] = [];
65
45
  for (let attempt = 1; held.length === 0; attempt++) {
66
46
  if (tryAcquire(primary)) {
67
47
  try {
@@ -70,12 +50,11 @@ export async function withClaudeRefreshLock<T>(
70
50
  break;
71
51
  }
72
52
  } catch (e) {
73
- // claude proceeds on a legacy acquire ERROR; only contention aborts.
74
53
  log("claudelock.legacy_error", { err: e instanceof Error ? e.message : String(e) });
75
54
  held.push(primary);
76
55
  break;
77
56
  }
78
- rmdirSync(primary); // legacy contested: release and re-take both, like claude
57
+ rmdirSync(primary);
79
58
  }
80
59
  if (attempt >= attempts) {
81
60
  log("claudelock.contested", { attempts: attempt });
@@ -86,7 +65,6 @@ export async function withClaudeRefreshLock<T>(
86
65
  await delay(retryMs + Math.random() * retryMs);
87
66
  }
88
67
 
89
- // the mtime we last stored per lock; a deviation means the lock was stolen.
90
68
  const ours = new Map<string, number>();
91
69
  for (const d of held) ours.set(d, statSync(d).mtimeMs);
92
70
  let compromised = false;
@@ -101,13 +79,13 @@ export async function withClaudeRefreshLock<T>(
101
79
  for (const d of held) {
102
80
  try {
103
81
  if (statSync(d).mtimeMs !== ours.get(d)) {
104
- markCompromised(); // someone re-took it; stop touching their lock
82
+ markCompromised();
105
83
  continue;
106
84
  }
107
85
  utimesSync(d, now, now);
108
86
  ours.set(d, statSync(d).mtimeMs);
109
87
  } catch {
110
- markCompromised(); // dir vanished: stolen or force-cleaned
88
+ markCompromised();
111
89
  }
112
90
  }
113
91
  }, opts.heartbeatMs ?? HEARTBEAT_MS);
@@ -122,8 +100,8 @@ export async function withClaudeRefreshLock<T>(
122
100
  clearInterval(heartbeat);
123
101
  for (const d of held) {
124
102
  try {
125
- if (statSync(d).mtimeMs === ours.get(d)) rmdirSync(d); // release only what is still ours
126
- } catch { /* already gone */ }
103
+ if (statSync(d).mtimeMs === ours.get(d)) rmdirSync(d);
104
+ } catch { }
127
105
  }
128
106
  }
129
107
  }
@@ -1,9 +1,3 @@
1
- // Read/write codex credential blobs ($CODEX_HOME/auth.json and parked copies)
2
- // and decode the id_token's identity claims. Codex has NO cross-process lock on
3
- // auth.json (its refresh serialization is an in-process semaphore, verified
4
- // rust-v0.144.5 manager.rs), so every mutation here runs under tokenmaxxing's
5
- // own codex flock, held by the caller.
6
-
7
1
  import { readFileSync, rmSync } from "node:fs";
8
2
  import { join } from "node:path";
9
3
  import { z } from "zod";
@@ -15,14 +9,6 @@ function isEnoent(e: unknown): boolean {
15
9
  return e instanceof Error && "code" in e && e.code === "ENOENT";
16
10
  }
17
11
 
18
- /** auth.json at an explicit path (the live file, or an onboard dir's), or null
19
- * when absent. Throws on a present-but-unparsable file: that is drift to
20
- * surface, not to paper over. One valid-but-unpoolable state maps to null
21
- * instead of throwing: `codex login --with-api-key` writes auth.json with
22
- * `tokens` OMITTED (serde skip_serializing_if, verified rust-v0.144.5) - the
23
- * real binary runs fine on it, so the shim and status must too, and with no
24
- * ChatGPT tokens there is nothing tokenmaxxing can pool (closing-review
25
- * catch). */
26
12
  export function readCodexAuthAt(input: { path: string }): CodexAuthJson | null {
27
13
  let raw: string;
28
14
  try {
@@ -37,7 +23,6 @@ export function readCodexAuthAt(input: { path: string }): CodexAuthJson | null {
37
23
  return CodexAuthJsonSchema.parse(parsed);
38
24
  }
39
25
 
40
- /** The live auth.json, or null when codex has no login here. */
41
26
  export function readLiveCodexAuth(): CodexAuthJson | null {
42
27
  return readCodexAuthAt({ path: codexPaths.authJson });
43
28
  }
@@ -65,15 +50,10 @@ export function writeParkedCodexAuth(input: { credFile: string; auth: CodexAuthJ
65
50
  writeFileAtomic(parkedPath(input), JSON.stringify(CodexAuthJsonSchema.parse(input.auth), null, 2), 0o600);
66
51
  }
67
52
 
68
- /** `rm --codex` uses this so the path shape (.json suffix) has one owner:
69
- * a hand-built path without the suffix silently missed the real file under
70
- * rmSync force (PR #37 review catch). */
71
53
  export function deleteParkedCodexAuth(input: { credFile: string }): void {
72
54
  rmSync(parkedPath(input), { force: true });
73
55
  }
74
56
 
75
- /** Identity claims inside the id_token JWT payload (verified against a live
76
- * 0.144.4 token: the chatgpt fields sit under the api.openai.com/auth claim). */
77
57
  const IdClaimsSchema = z.looseObject({
78
58
  email: z.string().optional(),
79
59
  "https://api.openai.com/auth": z
@@ -100,12 +80,6 @@ const CodexIdentitySchema = z.object({
100
80
  });
101
81
  export type CodexIdentity = z.infer<typeof CodexIdentitySchema>;
102
82
 
103
- /**
104
- * The blob's own identity, from its token material alone (no network): the
105
- * explicit tokens.account_id, else the id_token's chatgpt_account_id claim.
106
- * Parking MUST key on this (or the usage endpoint's echo of it), never on a
107
- * stored label - labels drift, tokens cannot lie.
108
- */
109
83
  export function codexIdentityOf(input: { auth: CodexAuthJson }): CodexIdentity {
110
84
  const { auth } = input;
111
85
  const claims = IdClaimsSchema.parse(decodeJwtPayload({ jwt: auth.tokens.id_token }));
@@ -121,9 +95,6 @@ export function codexIdentityOf(input: { auth: CodexAuthJson }): CodexIdentity {
121
95
  });
122
96
  }
123
97
 
124
- /** True when the access token is within `skewMs` of its JWT exp (or exp is
125
- * unreadable). Codex's own proactive refresh margin is 5 minutes; matching it
126
- * means we never install a token codex would immediately refresh. */
127
98
  export function isCodexAccessExpiring(input: { auth: CodexAuthJson; skewMs?: number; now?: number }): boolean {
128
99
  const { auth, skewMs = 300_000, now = Date.now() } = input;
129
100
  let exp: number | undefined;
@@ -1,15 +1,9 @@
1
- // Resolve the REAL codex binary (never our shim on PATH). Same guarantees as
2
- // claudebin.ts: a configured-but-missing pin fails fast instead of degrading
3
- // to a scan, and anything that realpath-resolves into our binDir is refused
4
- // (spawning it as codex would recurse through the codex supervisor).
5
-
6
1
  import { existsSync, statSync } from "node:fs";
7
2
  import { join } from "node:path";
8
3
  import { LOOP_DIAGNOSIS, MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, pointsBackAtUs } from "./claudebin.ts";
9
4
  import { loadConfig } from "./state.ts";
10
5
  import { paths } from "./paths.ts";
11
6
 
12
- /** First PATH entry with a `codex` that is not us. null when PATH has none. */
13
7
  function scanPathForCodex(): string | null {
14
8
  for (const d of (process.env.PATH ?? "").split(":")) {
15
9
  if (!d) continue;
@@ -41,10 +35,6 @@ export function resolveRealCodex(): string {
41
35
  throw new Error("could not locate the real `codex` binary (set codexBin in config.json)");
42
36
  }
43
37
 
44
- /** Behavioral check used by `init --codex` before pinning: the binary must
45
- * answer `--version` identifying itself as codex, without re-entering our
46
- * wrapper (depth preset to the cap kills a poisoned pin on first entry).
47
- * Returns null when the binary passes, else the failure detail. */
48
38
  export function verifyRealCodex(input: { bin: string }): string | null {
49
39
  const env = { ...process.env, [WRAP_DEPTH_ENV]: String(MAX_WRAP_DEPTH), TOKENMAXXING_PROBE: "1" };
50
40
  let p: ReturnType<typeof Bun.spawnSync>;
@@ -1,11 +1,3 @@
1
- // Shared codex switch decision, used by the codex Stop hook and `xx switch
2
- // --codex`. Same policy as decide.ts, reshaped for codex mechanics: usage
3
- // comes from the free direct GET (no statusLine tee exists), the greedy floor
4
- // reads against every window class (current codex plans have no 5h window:
5
- // the weekly aggregate is primary, verified live 2026-07-16), and there is no
6
- // depleted pre-park (a swap only lands where a window is usable NOW; a
7
- // depleted pool stays put and recovers when a cached reset passes).
8
-
9
1
  import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
10
2
  import { join } from "node:path";
11
3
  import { z } from "zod";
@@ -34,18 +26,6 @@ export type CodexSwapDecision = z.infer<typeof CodexSwapDecisionSchema>;
34
26
 
35
27
  const POST_SWAP_COOLDOWN_MS = 45_000;
36
28
 
37
- /**
38
- * Sample the LIVE credential's usage and stamp it onto its TRUE owner in the
39
- * pool (the id_token's own identity: labels drift, the token cannot lie).
40
- * Refreshes the live blob first when it is near expiry (and no supervised
41
- * session is running it - see the presence guard below), persisting the
42
- * rotation to the live file AND the owner's parked copy in the same step -
43
- * before the usage fetch, so a failed fetch can never strand the parked copy
44
- * on the reuse-punished superseded refresh token. A dead live grant marks the
45
- * owner needs-reauth and keeps the cached snapshot, so the decision below can
46
- * still swap AWAY from it. Returns the owner id, or null when the live
47
- * credential is absent or unpooled.
48
- */
49
29
  async function sampleLiveOntoOwner(input: { now: number }): Promise<string | null> {
50
30
  const { now } = input;
51
31
  let live = readLiveCodexAuth();
@@ -55,13 +35,6 @@ async function sampleLiveOntoOwner(input: { now: number }): Promise<string | nul
55
35
  const owner = index.accounts.find((account) => account.accountId === identity.accountId);
56
36
  if (!owner) return null;
57
37
 
58
- // The near-expiry refresh is skipped while any supervised session RUNS the
59
- // live account: a sibling's Stop hook (or the timer) can land mid-turn of
60
- // that session, and two concurrent POSTs of the same rotating refresh token
61
- // reuse-punish the loser into a dead grant family (closing-review catch).
62
- // The unrotated token stays valid within the margin, so the fetch below
63
- // still samples; once expired it fails loudly and the next cycle, after the
64
- // running session's own refresh, recovers.
65
38
  if (isCodexAccessExpiring({ auth: live, now }) && !presentCodexAccountIds().has(identity.accountId)) {
66
39
  try {
67
40
  live = await refreshCodexAuth({ auth: live, now });
@@ -86,33 +59,6 @@ async function sampleLiveOntoOwner(input: { now: number }): Promise<string | nul
86
59
  return owner.accountId;
87
60
  }
88
61
 
89
- /**
90
- * OWNER-APPROVED sibling reconcile (option b 2026-07-20, widened to ALL
91
- * non-live siblings by the in-session owner ruling later that day): codex
92
- * cannot hot-swap, so a pool swap respawns only the deciding session -
93
- * siblings keep running whatever account they started on, cannot refresh it
94
- * cross-account, and would wedge at token expiry. The deciding actor
95
- * therefore SIGNALS every pooled non-live supervisor cross-session: a
96
- * reconcile marker addressed by supervisorId (presence filenames carry it).
97
- * The signal is a marker, never a kill: the only safe respawn point is the
98
- * sibling's own turn boundary, so its Stop hook promotes the marker into a
99
- * respawn marker there, adding the session id its stdin alone knows. No
100
- * respawn-cost margin applies - the alternative to a respawn is a guaranteed
101
- * eventual wedge - and no signal is ever sent while the LIVE seat is itself
102
- * unusable (the no-depleted-pre-park analog: never move a session onto a
103
- * blocked account). Orphaned markers (their supervisor exited before
104
- * promoting) are garbage-collected here. Idempotent per supervisor: an
105
- * existing marker is left alone.
106
- *
107
- * There is deliberately NO self-exclusion (bugbot/pullfrog/cubic review
108
- * catches, PR #34): a session whose presence names a non-live account IS the
109
- * stranded sibling even inside its own hook - its "normal decision" evaluates
110
- * only the live seat and would never rescue it. A signal the sweep writes for
111
- * the calling session is consumed by the promote pass its own hook runs right
112
- * after the evaluation, same boundary. The deciding session that just swapped
113
- * away leaves a self-addressed signal behind too; the promote staleness guard
114
- * (presence rewritten at respawn) drops it as moot.
115
- */
116
62
  function reconcileNonLiveSiblings(input: {
117
63
  index: { accounts: CodexAccount[] };
118
64
  liveAccountId: string;
@@ -125,8 +71,6 @@ function reconcileNonLiveSiblings(input: {
125
71
  const liveAccount = index.accounts.find((account) => account.accountId === liveAccountId);
126
72
  if (!liveAccount || unusable(liveAccount)) return;
127
73
  const living = livingCodexPresences();
128
- // gc signals addressed to supervisors that no longer live (exited before
129
- // promoting): nothing would ever consume them.
130
74
  if (existsSync(codexPaths.reconcileDir)) {
131
75
  const alive = new Set(living.map((presence) => presence.supervisorId));
132
76
  for (const name of readdirSync(codexPaths.reconcileDir)) {
@@ -134,16 +78,8 @@ function reconcileNonLiveSiblings(input: {
134
78
  }
135
79
  }
136
80
  for (const presence of living) {
137
- if (presence.accountId === liveAccountId) continue; // riding the live seat: fine
81
+ if (presence.accountId === liveAccountId) continue;
138
82
  const seated = index.accounts.find((account) => account.accountId === presence.accountId);
139
- // OWNER RULING (2026-07-20, in-session, superseding the exhausted-only
140
- // scope of option b): EVERY pooled non-live sibling is signaled, healthy
141
- // included. Codex's guarded reload refuses a cross-account auth.json
142
- // refresh, so a non-live session WILL wedge with "Please sign in again"
143
- // at its access token's expiry (hours) - the safe-boundary respawn onto
144
- // the live seat costs one visible restart, keeps the transcript
145
- // (`codex resume <sid>`), and makes the whole pool follow the seat.
146
- // Unpooled seats stay untouched: not ours to move.
147
83
  if (!seated) continue;
148
84
  const markerPath = join(codexPaths.reconcileDir, presence.supervisorId);
149
85
  if (existsSync(markerPath)) continue;
@@ -153,9 +89,6 @@ function reconcileNonLiveSiblings(input: {
153
89
  }
154
90
  }
155
91
 
156
- /** The re-sweep after a PERSISTED swap: failures are logged, never thrown -
157
- * the caller must still report swapped:true so the Stop hook writes the
158
- * deciding session's own respawn marker. */
159
92
  function postSwapResweep(input: { liveAccountId: string; bars: { session: number; weekly: number }; now: number }): void {
160
93
  try {
161
94
  reconcileNonLiveSiblings({ index: loadCodexAccounts(), liveAccountId: input.liveAccountId, bars: input.bars, now: input.now });
@@ -173,23 +106,11 @@ export async function evaluateAndMaybeSwapCodex(input: { now?: number }): Promis
173
106
  let index = loadCodexAccounts();
174
107
  if (index.accounts.length === 0) return { swapped: false, account: null, reason: "no-pool" };
175
108
 
176
- // The current account is ALWAYS the live auth.json's own identity: the
177
- // stored activeAccountId label drifts (manual `codex login`, a crash
178
- // before saveCodexAccounts), and trusting it once let the decision target
179
- // the RUNNING account (adversarial review catch, 2026-07-16). A live
180
- // identity outside the pool is the org-guard analog: do nothing, a swap
181
- // over an unknown credential could destroy its only copy.
182
109
  const activeId = liveCodexAccountId();
183
110
  if (activeId == null || !index.accounts.some((account) => account.accountId === activeId)) {
184
111
  return { swapped: false, account: null, reason: "live-credential-not-in-pool" };
185
112
  }
186
113
 
187
- // Cross-session sibling reconcile runs on EVERY evaluation, BEFORE both
188
- // the cooldown return and the engagement gate: a swap strands siblings at
189
- // exactly the moment the cooldown starts (bugbot/cubic review catch,
190
- // PR #34), and a sibling can be dead while the live seat is healthy and
191
- // disengaged. Cached windows are enough here - the promote pass
192
- // revalidates the destination's usability at consumption time.
193
114
  reconcileNonLiveSiblings({ index, liveAccountId: activeId, bars, now });
194
115
 
195
116
  const lastSwapAt = loadCodexLastSwapAt();
@@ -197,8 +118,6 @@ export async function evaluateAndMaybeSwapCodex(input: { now?: number }): Promis
197
118
  return { swapped: false, account: null, reason: "post-swap-cooldown" };
198
119
  }
199
120
 
200
- // Freshness: re-sample the live credential once its owner's cached
201
- // snapshot ages past the poll TTL (there is no push feed in between).
202
121
  const activeEntry = index.accounts.find((account) => account.accountId === activeId);
203
122
  const stale = activeEntry?.lastUsageAt == null || now - activeEntry.lastUsageAt > cfg.policy.usagePollTtlMs;
204
123
  if (stale) {
@@ -212,20 +131,12 @@ export async function evaluateAndMaybeSwapCodex(input: { now?: number }): Promis
212
131
  const active = index.accounts.find((account) => account.accountId === activeId) ?? null;
213
132
  if (!active) return { swapped: false, account: null, reason: "no-active-account" };
214
133
 
215
- // A dead live grant always engages: the seat is unusable regardless of
216
- // cached usage, and codexCurrentWins/pickBestCodex already exclude
217
- // needs-reauth accounts, so both branches route onto a healthy target.
218
134
  const engaged =
219
135
  active.needsReauth === true ||
220
136
  isCodexEngaged({ account: active, floor: cfg.policy.greedySessionFloor, now }) ||
221
137
  isCodexExhausted({ account: active, thresholds: bars, now });
222
138
  if (!engaged) return { swapped: false, account: null, reason: "under-threshold-or-stale" };
223
139
 
224
- // Greedy path: engaged but under every bar. Swap only onto an account
225
- // that beats the seat by the respawn-cost margin, never onto one RUNNING
226
- // in another session (presence files); re-rank after a dead grant
227
- // (performCodexSwap persists needs-reauth before throwing, so the loop
228
- // terminates).
229
140
  if (!isCodexExhausted({ account: active, thresholds: bars, now })) {
230
141
  while (true) {
231
142
  const current = loadCodexAccounts();
@@ -243,30 +154,11 @@ export async function evaluateAndMaybeSwapCodex(input: { now?: number }): Promis
243
154
  throw e;
244
155
  }
245
156
  log("codexdecide.greedy_swap", { account: best.accountId.slice(0, 8) });
246
- // Re-sweep with the NEW live seat: siblings on the account this swap
247
- // just departed were invisible to the entry sweep (their account WAS
248
- // the live seat then), and the cooldown blocks the next evaluation's
249
- // sweep-entry for 45s (pullfrog review catch, PR #34). GUARDED: the
250
- // swap is already persisted, so a sweep failure (e.g. a corrupt
251
- // presence file, which livingCodexPresences throws on by design) must
252
- // not eat the swapped:true return - the Stop hook needs it to write
253
- // THIS session's respawn marker (bugbot/cubic P0 catch, PR #34). The
254
- // entry sweep stays fail-loud: there nothing irreversible has
255
- // happened yet.
256
157
  postSwapResweep({ liveAccountId: best.accountId, bars, now });
257
158
  return { swapped: true, account: best, reason: "swapped" };
258
159
  }
259
160
  }
260
161
 
261
- // Hard path: a bar is crossed. Land on the best usable candidate, walking
262
- // past dead grants; a fully depleted pool stays put (no pre-park: nothing
263
- // can pause a codex session for a countdown yet). Layer 2 (the wall) is
264
- // deliberately claude-only: a running codex cannot hot-adopt a swapped
265
- // credential (restart IS the switch), so a last-drop-swap onto a still-
266
- // under-wall account would strand any concurrent sibling on the departed
267
- // account (the reconcile can only signal siblings onto a Layer-1-usable
268
- // seat), and a hold-only Layer 2 is identical to codex already staying put
269
- // here - so codex just rides the current account to its wall.
270
162
  const tried = new Set<string>();
271
163
  while (true) {
272
164
  const current = loadCodexAccounts();
@@ -283,9 +175,6 @@ export async function evaluateAndMaybeSwapCodex(input: { now?: number }): Promis
283
175
  throw e;
284
176
  }
285
177
  log("codexdecide.hard_swap", { account: best.accountId.slice(0, 8) });
286
- // Same guarded post-swap re-sweep as the greedy branch: the departed
287
- // account's siblings become signalable only once it stops being the
288
- // live seat, and a sweep failure must not eat the swapped:true return.
289
178
  postSwapResweep({ liveAccountId: best.accountId, bars, now });
290
179
  return { swapped: true, account: best, reason: "swapped" };
291
180
  }
@@ -1,10 +1,3 @@
1
- // The one OAuth call tokenmaxxing makes for codex: the refresh_token grant.
2
- // Verified against openai/codex rust-v0.144.5 (login/src/auth/manager.rs):
3
- // POST auth.openai.com/oauth/token, JSON body {client_id, grant_type,
4
- // refresh_token}, no auth headers. The server ROTATES the refresh token and
5
- // punishes reuse of a superseded one (refresh_token_reused), so every rotation
6
- // must be persisted back into the blob it came from immediately.
7
-
8
1
  import { z } from "zod";
9
2
  import { http, safeErrorDetail } from "./http.ts";
10
3
  import { CodexAuthJsonSchema, type CodexAuthJson } from "./types.ts";
@@ -13,7 +6,6 @@ const EnvOverrideSchema = z.string().min(1).optional().catch(undefined);
13
6
  const TOKEN_URL = EnvOverrideSchema.parse(process.env.TOKENMAXXING_CODEX_TOKEN_URL) ?? "https://auth.openai.com/oauth/token";
14
7
  const CLIENT_ID = EnvOverrideSchema.parse(process.env.TOKENMAXXING_CODEX_CLIENT_ID) ?? "app_EMoamEEZ73f0CkXaXp7hrann";
15
8
 
16
- /** Refresh token dead, superseded, or revoked: mark needs-reauth and move on. */
17
9
  export class CodexInvalidGrantError extends Error {
18
10
  constructor(detail: string) {
19
11
  super(`codex invalid grant: ${detail}`);
@@ -21,8 +13,6 @@ export class CodexInvalidGrantError extends Error {
21
13
  }
22
14
  }
23
15
 
24
- /** The refresh could not run (endpoint unreachable, throttled, drifted body):
25
- * an operational miss to retry later, NOT a dead grant and NOT a bug. */
26
16
  export class CodexRefreshFailedError extends Error {
27
17
  constructor(detail: string) {
28
18
  super(`codex token refresh failed: ${detail}`);
@@ -43,12 +33,6 @@ const DEAD_GRANT_MARKERS = [
43
33
  "refresh_token_invalidated",
44
34
  ];
45
35
 
46
- /**
47
- * Exchange the blob's refresh token for fresh tokens. Returns a NEW auth.json
48
- * value with rotated token material and last_refresh restamped; every sibling
49
- * field rides along verbatim. Throws CodexInvalidGrantError on a dead grant.
50
- * Error paths only ever surface response-body snippets, never request headers.
51
- */
52
36
  export async function refreshCodexAuth(input: { auth: CodexAuthJson; now?: number }): Promise<CodexAuthJson> {
53
37
  const { auth, now = Date.now() } = input;
54
38
  let res: Response;
@@ -1,23 +1,8 @@
1
- // Choose the codex account to switch TO. Same policy as the claude picker:
2
- // among usable accounts (no reauth, no window at/over its screening bar that
3
- // has not reset), take the one furthest behind its own weekly pace (highest
4
- // pacePressure), because weekly allowance is forfeited at a fixed per-account
5
- // reset. Windows are duration-classified (isSessionWindow), never assumed:
6
- // codex's 5h window is absent on current Plus/Pro plans (July 2026), so the
7
- // weekly aggregate may be the only bar there is.
8
-
9
1
  import { sortBy } from "es-toolkit";
10
2
  import { nextWeeklyReset } from "./picker.ts";
11
3
  import { isSessionWindow, weeklyWindowOf } from "./codexusage.ts";
12
4
  import type { CodexAccount, CodexWindow, Thresholds } from "./types.ts";
13
5
 
14
- /** A window whose reset has passed reads as empty. A NULL reset self-bounds
15
- * at sampledAt + the window's own duration (mirroring the claude picker's
16
- * blockedUntil, closing-review catch): the wire schema allows reset_at null,
17
- * and without the bound an over-bar null-reset window benched the account
18
- * FOREVER on a headless box where nothing re-samples parked accounts - the
19
- * no-permanent-bench invariant. No duration or sample time = trust the
20
- * reading (unmeasured recovery must not look safe). */
21
6
  function liveUsed(input: { window: CodexWindow; now: number; sampledAt: number | null }): number {
22
7
  const { window, now, sampledAt } = input;
23
8
  if (window.resetsAt != null) return window.resetsAt <= now ? 0 : window.usedPercentage;
@@ -35,7 +20,6 @@ function barFor(input: { window: CodexWindow; thresholds: Thresholds }): number
35
20
  return isSessionWindow({ window: input.window }) ? input.thresholds.session : input.thresholds.weekly;
36
21
  }
37
22
 
38
- /** A window at/over its bar whose reset has not passed blocks the account. */
39
23
  export function isCodexExhausted(input: { account: CodexAccount; thresholds: Thresholds; now: number }): boolean {
40
24
  const { account, thresholds, now } = input;
41
25
  const sampledAt = account.lastUsageAt ?? null;
@@ -44,9 +28,6 @@ export function isCodexExhausted(input: { account: CodexAccount; thresholds: Thr
44
28
  );
45
29
  }
46
30
 
47
- /** Forward pace pressure on the weekly aggregate: the burn rate the remaining
48
- * weekly quota demands before its reset forfeits it. No sampled weekly window
49
- * or no reset anchor ranks last (0): unmeasured must not look urgent. */
50
31
  export function codexPacePressure(input: { account: CodexAccount; now: number }): number {
51
32
  const { account, now } = input;
52
33
  const weekly = account.lastUsage ? weeklyWindowOf({ aggregate: account.lastUsage.aggregate }) : null;
@@ -83,17 +64,8 @@ export function pickBestCodex(input: {
83
64
  return sortBy(usable, codexSwapPreference(now))[0] ?? null;
84
65
  }
85
66
 
86
- /** A codex greedy swap is a SIGTERM + respawn of a live session (codex cannot
87
- * hot-adopt), and engagement is chronic on plans whose only window is weekly:
88
- * without a margin, every hair of pace-pressure drift would visibly restart
89
- * the user's session. The challenger must beat the incumbent by this factor
90
- * (adversarial review catch, 2026-07-16); a crossed bar bypasses the margin
91
- * entirely via the hard path. */
92
67
  export const CODEX_SWAP_IMPROVEMENT = 1.2;
93
68
 
94
- /** Greedy idempotence, mirroring picker.currentWins with the respawn-cost
95
- * margin: the active account keeps its seat while usable unless a challenger
96
- * beats its pace pressure by CODEX_SWAP_IMPROVEMENT. */
97
69
  export function codexCurrentWins(input: {
98
70
  active: CodexAccount | null;
99
71
  accounts: CodexAccount[];
@@ -107,9 +79,6 @@ export function codexCurrentWins(input: {
107
79
  return codexPacePressure({ account: best, now }) <= codexPacePressure({ account: active, now }) * CODEX_SWAP_IMPROVEMENT;
108
80
  }
109
81
 
110
- /** True once any window is at/over the greedy engagement floor: with no 5h
111
- * window on current codex plans, the floor reads against every window class
112
- * rather than the session window alone. */
113
82
  export function isCodexEngaged(input: { account: CodexAccount; floor: number; now: number }): boolean {
114
83
  const { account, floor, now } = input;
115
84
  const sampledAt = account.lastUsageAt ?? null;
@@ -1,15 +1,3 @@
1
- // Which codex accounts are RUNNING right now. Codex cannot hot-swap, so a swap
2
- // respawns only the session whose Stop hook decided it; sibling supervised
3
- // sessions keep running on whatever account they started with, rotating that
4
- // account's token live. Such an account is a landmine for the rest of the
5
- // pool: its parked copy is superseded (refresh trips reuse punishment) and
6
- // installing it as live would yank the running session's grant. Supervisors
7
- // therefore declare their session's account in a presence file at every
8
- // (re)spawn; the picker refuses to target present accounts and the sampler
9
- // refuses to refresh their parked blobs. Staleness is checked by process
10
- // IDENTITY (pid + ps lstart), not bare pid-aliveness: after a supervisor
11
- // crash a recycled pid would otherwise keep its account benched forever.
12
-
13
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from "node:fs";
14
2
  import { join } from "node:path";
15
3
  import { z } from "zod";
@@ -23,17 +11,9 @@ const PresenceSchema = z.object({
23
11
  startedAt: z.string(),
24
12
  });
25
13
 
26
- /** `pid` should be the CODEX CHILD's pid when known (the supervisor passes
27
- * it): the session IS the child, and pinning the supervisor's own pid let a
28
- * SIGKILLed supervisor prune the presence while its orphaned codex kept
29
- * running and rotating the account's token - un-benching a live account for
30
- * samplers and the picker (closing-review catch). Defaults to process.pid
31
- * for callers that ARE the session-owning process (tests, future uses). */
32
14
  export function writeCodexPresence(input: { supervisorId: string; accountId: string; pid?: number }): void {
33
15
  const pid = input.pid ?? process.pid;
34
16
  const startedAt = pidStartTime(pid);
35
- // The pid must be ps-visible; a null here means ps broke or the process
36
- // already died - corrupt state to fail loudly on, never a masked placeholder.
37
17
  if (startedAt == null) throw new Error(`could not read pid ${pid}'s start time (ps lstart) - refusing to write an unverifiable presence file`);
38
18
  mkdirSync(codexPaths.presenceDir, { recursive: true });
39
19
  writeFileAtomic(
@@ -49,13 +29,6 @@ export function clearCodexPresence(input: { supervisorId: string }): void {
49
29
  const LivingPresenceSchema = z.object({ supervisorId: z.string(), accountId: z.string() });
50
30
  export type LivingPresence = z.infer<typeof LivingPresenceSchema>;
51
31
 
52
- /** Every LIVING supervised session (pid + start-time identity match), as
53
- * {supervisorId, accountId} pairs - the reconcile sweep needs to address a
54
- * specific supervisor, not just know which accounts are busy. Dead or
55
- * recycled-pid presences are removed. A file that exists but fails to parse
56
- * THROWS (review catch, PR #31): a presence file is what keeps a RUNNING
57
- * session's account from being swapped out from under it, so damaged state
58
- * must fail the decision loudly, never silently drop the protection. */
59
32
  export function livingCodexPresences(): LivingPresence[] {
60
33
  const living: LivingPresence[] = [];
61
34
  if (!existsSync(codexPaths.presenceDir)) return living;
@@ -65,7 +38,6 @@ export function livingCodexPresences(): LivingPresence[] {
65
38
  try {
66
39
  raw = readFileSync(file, "utf8");
67
40
  } catch (e) {
68
- // a supervisor exiting between readdir and read clears its own file
69
41
  const errno = z.object({ code: z.string() }).safeParse(e);
70
42
  if (errno.success && errno.data.code === "ENOENT") continue;
71
43
  throw e;
@@ -82,10 +54,6 @@ export function livingCodexPresences(): LivingPresence[] {
82
54
  }
83
55
  const observed = pidStartTime(parsed.data.pid);
84
56
  if (observed !== parsed.data.startedAt) {
85
- // A null lstart is ambiguous: dead pid, or ps itself failing. Deleting
86
- // on a ps failure would silently unbench a RUNNING session's account
87
- // (review catch, PR #31), so only a confirmed-dead pid - or a live pid
88
- // with a DIFFERENT start time, a recycle - may clear the file.
89
57
  if (observed == null && pidExists(parsed.data.pid)) {
90
58
  throw new Error(`ps could not read the start time of live pid ${parsed.data.pid} (${file}) - refusing to clear a presence file that may guard a RUNNING codex session`);
91
59
  }
@@ -97,13 +65,10 @@ export function livingCodexPresences(): LivingPresence[] {
97
65
  return living;
98
66
  }
99
67
 
100
- /** Account ids with a LIVING supervisor - the picker/sampler exclusion view. */
101
68
  export function presentCodexAccountIds(): Set<string> {
102
69
  return new Set(livingCodexPresences().map((presence) => presence.accountId));
103
70
  }
104
71
 
105
- /** The accounts a swap may target: running accounts are off limits, except the
106
- * seat itself (it is ranked as the incumbent, never installed over itself). */
107
72
  export function targetableCodexAccounts<T extends { accountId: string }>(input: {
108
73
  accounts: T[];
109
74
  activeAccountId: string | null;