tokenmaxxing 1.7.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 (67) hide show
  1. package/DESIGN.md +2 -2
  2. package/README.md +1 -1
  3. package/agent-plugin/plugin.json +8 -2
  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 +11 -42
  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 +8 -34
  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 -63
  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 -23
  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 +57 -124
  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 -45
  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 -35
  67. package/src/sdk.ts +0 -59
package/src/cli/switch.ts CHANGED
@@ -1,19 +1,3 @@
1
- // `tokenmaxxing switch [selector]`.
2
- // No selector → greedy: rank EVERY account (current included) by pace pressure
3
- // (furthest behind its own weekly pace first, see picker.ts) among those with
4
- // session/week under threshold, off the cached windows.
5
- // When the current account already wins (or ties - swapping between equals buys
6
- // nothing), do nothing: the command is idempotent, so running it periodically
7
- // converges on the right account. With a selector → switch to that one. Runs
8
- // under the flock. When everything is depleted it stays on / switches to
9
- // whichever account recovers soonest.
10
- //
11
- // The active LABEL can drift from the live login (manual /login is the
12
- // surviving drift source). A no-op would freeze that drift forever, so when
13
- // ~/.claude.json names a different account than accounts.json we swap for real
14
- // instead of trusting the label - performSwap resolves the live credential's
15
- // true owner, harvesting the drifted login correctly.
16
-
17
1
  import { withLock } from "../lib/lock.ts";
18
2
  import { paths } from "../lib/paths.ts";
19
3
  import { loadAccounts, loadConfig } from "../lib/state.ts";
@@ -36,10 +20,6 @@ export async function cmdSwitch(selector?: string): Promise<number> {
36
20
 
37
21
  return withLock(paths.lockFile, async () => {
38
22
  const idx = loadAccounts();
39
- // ONE claude.json read for both identity fields: two reads could straddle
40
- // a concurrent /login and describe two different live identities - the
41
- // drift check and the seat must come from the same snapshot (cubic review
42
- // catch, PR #33).
43
23
  const liveClaim = readOAuthAccount();
44
24
  const claimed = liveClaim?.accountUuid ?? null;
45
25
  const claimedOrg = liveClaim?.organizationUuid ?? null;
@@ -64,26 +44,9 @@ export async function cmdSwitch(selector?: string): Promise<number> {
64
44
  return swapTo(target);
65
45
  }
66
46
 
67
- // auto: greedy over everyone, current included - a no-op when current wins.
68
- // No session context here, so every configured per-model family gates.
69
- // Dead-token fallback mirrors decide.ts's greedy loop, NOT chooseAndSwap:
70
- // its fallback lands on the next usable candidate unconditionally, which
71
- // is right when over a bar but wrong here - a dead grant on the pace
72
- // winner must re-check the seat, or a healthy current account gets a real
73
- // swap onto a pace-WORSE account and the next periodic check bounces it
74
- // straight back (closing-review catch). performSwap persists needs-reauth
75
- // before throwing, so each reload shrinks the candidate set and the loop
76
- // terminates.
77
47
  const everyone: PickCtx = { now, thresholds: effectiveBars(cfg), currentAccountUuid: null, switchFamilies: cfg.policy.switchModels };
78
48
  while (true) {
79
49
  const cur = loadAccounts();
80
- // The seat is the LIVE login's pooled account when resolvable, the
81
- // stored label only as fallback - the same identity rule AND the same
82
- // identity KEY as decide.ts's seatOf: the organizationUuid, since quota
83
- // is metered per org and the org is what the roles endpoint verifies
84
- // (bugbot review catches, PR #33). Under drift this also makes ties
85
- // favor the live account: the realign swap then keeps the same
86
- // credential instead of hopping accounts on a tie.
87
50
  const active =
88
51
  (claimedOrg != null ? cur.accounts.find((a) => a.organizationUuid === claimedOrg) : null) ??
89
52
  cur.accounts.find((a) => a.accountUuid === cur.activeAccountUuid) ??
@@ -96,7 +59,7 @@ export async function cmdSwitch(selector?: string): Promise<number> {
96
59
  return 0;
97
60
  }
98
61
  const best = pickBest(cur.accounts, { ...everyone, currentAccountUuid: active?.accountUuid ?? null });
99
- if (!best) break; // no usable target: the depleted path below decides
62
+ if (!best) break;
100
63
  try {
101
64
  await performSwap(best);
102
65
  } catch (e) {
@@ -110,20 +73,11 @@ export async function cmdSwitch(selector?: string): Promise<number> {
110
73
  return 0;
111
74
  }
112
75
 
113
- // No usable target swapped in: everything is depleted, or the remaining
114
- // candidates' refresh tokens just died (performSwap persists needs-reauth
115
- // before throwing, hence the reload). Stay on / switch to whichever
116
- // recovers soonest. (Layer 2 - the wall squeeze - is deliberately confined
117
- // to the automatic decision path in decide.ts, which decides off the live
118
- // statusLine tee; bare `xx switch` stays cache-only and simply parks here.)
119
76
  const fresh = loadAccounts();
120
77
  const earliest = pickEarliestReset(fresh.accounts, everyone);
121
78
  if (!earliest) {
122
- // Either every account needs re-auth, or every account is blocked with no
123
- // recoverable bound (unparsed reset clocks AND no sample time - see log).
124
79
  const reauth = fresh.accounts.filter((a) => a.needsReauth).map((a) => a.label);
125
80
  if (reauth.length > 0) { console.error(c.yellow(`no switchable account - reauth needed (run \`tokenmaxxing auth --all\`): ${reauth.join(", ")}`)); return 1; }
126
- // never freeze a label drift behind a no-op (see header).
127
81
  const freshActive = fresh.accounts.find((a) => a.accountUuid === fresh.activeAccountUuid) ?? null;
128
82
  if (drifted && freshActive) return swapTo(freshActive);
129
83
  console.log(c.yellow("all accounts at their limit with unknown reset times (unparsed reset clocks? see tokenmaxxing.log) - staying put"));
@@ -132,8 +86,6 @@ export async function cmdSwitch(selector?: string): Promise<number> {
132
86
  const reauth = fresh.accounts.filter((a) => a.needsReauth).map((a) => a.label);
133
87
  const reauthNote = reauth.length ? ` - re-auth needed: ${reauth.join(", ")}` : "";
134
88
  if (earliest.account.accountUuid === fresh.activeAccountUuid && !drifted) {
135
- // availableAt in the past means the current account is fine and the
136
- // others are simply unusable - do not claim a limit that is not there.
137
89
  const msg = earliest.availableAt <= now
138
90
  ? `staying on ${c.bold(earliest.account.label)} - no usable switch target${reauthNote}`
139
91
  : `all accounts at limit - staying on ${c.bold(earliest.account.label)} (${fmtReset(earliest.availableAt, now)})${reauthNote}`;
package/src/cli/watch.ts CHANGED
@@ -1,23 +1,12 @@
1
- // `tokenmaxxing watch [seconds]`: native live status (issue #3) - re-render
2
- // `status` on an interval instead of `watch -n 120 'tokenmaxxing status'`.
3
- // Plain `status` per tick, never `--force`: a ping meters real quota and starts
4
- // 5h session windows, so watching must stay free; each refresh costs only the
5
- // parked accounts' `/usage` probes the one-shot status already does.
6
-
7
1
  import { clamp, delay } from "es-toolkit";
8
2
  import { loadAccounts } from "../lib/state.ts";
9
3
  import { cmdStatus } from "./status.ts";
10
4
  import { c } from "./render.ts";
11
5
 
12
6
  const DEFAULT_INTERVAL_S = 120;
13
- /** Floor: each tick spawns a `/usage` probe per parked account (seconds each,
14
- * under the flock); anything faster than this just queues probes. */
15
7
  const MIN_INTERVAL_S = 30;
16
- /** Cap: past ~24.8 days the setTimeout ms overflow collapses the sleep to ~1ms
17
- * and the loop runs hot; a day is already beyond any sane watch cadence. */
18
8
  const MAX_INTERVAL_S = 86_400;
19
9
 
20
- /** Seconds between refreshes, clamped; null when the argument is not a positive number. */
21
10
  export function resolveWatchInterval(arg?: string): number | null {
22
11
  if (arg === undefined) return DEFAULT_INTERVAL_S;
23
12
  const n = Number(arg);
@@ -25,9 +14,6 @@ export function resolveWatchInterval(arg?: string): number | null {
25
14
  return clamp(n, MIN_INTERVAL_S, MAX_INTERVAL_S);
26
15
  }
27
16
 
28
- /** Home + clear screen + clear scrollback. Written only once the fresh frame is
29
- * ready to paint (cmdStatus preRender), so the previous frame stays readable
30
- * through the multi-second sample instead of blanking - watch(1) semantics. */
31
17
  const CLEAR = "\x1b[H\x1b[2J\x1b[3J";
32
18
 
33
19
  export async function cmdWatch(intervalArg?: string): Promise<number> {
@@ -43,9 +29,6 @@ export async function cmdWatch(intervalArg?: string): Promise<number> {
43
29
  console.log(c.dim(`watch: every ${intervalS}s, ${new Date().toLocaleTimeString()}, ctrl-c to quit`));
44
30
  };
45
31
  while (true) {
46
- // One failed tick (a mid-write ~/.claude.json read, a transient probe or
47
- // lock error) must not kill an hours-long monitor: report it and keep the
48
- // cadence, exactly like watch(1) showing a failing command's output.
49
32
  try {
50
33
  await cmdStatus(false, paintHeader);
51
34
  } catch (e) {
@@ -1,15 +1,3 @@
1
- // Codex Stop hook (installed in ~/.codex/hooks.json by `init --codex`). Fires
2
- // when codex finishes a turn: the transcript is committed and the process is
3
- // idle, the one boundary where killing it loses nothing. If the decision swaps
4
- // accounts and this session runs under the codex supervisor, drop a respawn
5
- // marker keyed by the supervisor's id; the supervisor SIGTERMs codex and
6
- // relaunches `codex resume <session-id>` on the new account (a running codex
7
- // never adopts a different account's credential: restart IS the switch).
8
- //
9
- // Contract with codex (verified against the 0.144.4 binary + hooks reference):
10
- // stdin carries session_id, stdout `{}` on exit 0 is the documented no-op, and
11
- // a hook failure must never block the stop - errors are logged, not thrown.
12
-
13
1
  import { join } from "node:path";
14
2
  import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
15
3
  import { z } from "zod";
@@ -29,21 +17,6 @@ import { log } from "../lib/log.ts";
29
17
 
30
18
  const SupervisorIdSchema = z.string().min(1).optional().catch(undefined);
31
19
 
32
- /** Consume a cross-session reconcile signal addressed to THIS supervisor
33
- * (owner-approved option b, 2026-07-20): a deciding actor saw this session
34
- * running on an exhausted/dead account and asked it to respawn onto the live
35
- * seat. The Stop boundary is the only safe respawn point, and only THIS
36
- * hook's stdin carries the session id a resume needs, so promotion happens
37
- * here: the signal becomes a normal respawn marker the supervisor already
38
- * consumes. Returns true when the respawn marker was written (the session is
39
- * about to die - skip the normal decision). Everything past the cheap
40
- * no-marker fast path runs under the codex FLOCK (pullfrog review catch,
41
- * PR #34): the usability revalidation is only as good as its atomicity with
42
- * the marker write - unlocked, a concurrent `xx switch --codex` could move
43
- * the live seat onto a blocked account between the check and the write. The
44
- * flock serializes promotion against every tokenmaxxing actor (codex's own
45
- * actions are unserialized as ever); no nesting occurs because the hook
46
- * calls promote strictly before or after the evaluation's own lock. */
47
20
  async function promoteReconcile(input: { supervisorId: string; sessionId: string | null }): Promise<boolean> {
48
21
  const markerPath = join(codexPaths.reconcileDir, input.supervisorId);
49
22
  if (!existsSync(markerPath)) return false;
@@ -52,7 +25,7 @@ async function promoteReconcile(input: { supervisorId: string; sessionId: string
52
25
 
53
26
  function promoteReconcileLocked(input: { supervisorId: string; sessionId: string | null }): boolean {
54
27
  const markerPath = join(codexPaths.reconcileDir, input.supervisorId);
55
- if (!existsSync(markerPath)) return false; // re-checked under the lock: a raced consumer may have taken it
28
+ if (!existsSync(markerPath)) return false;
56
29
  const parsed = CodexReconcileMarkerSchema.safeParse((() => {
57
30
  try {
58
31
  return JSON.parse(readFileSync(markerPath, "utf8"));
@@ -61,15 +34,10 @@ function promoteReconcileLocked(input: { supervisorId: string; sessionId: string
61
34
  }
62
35
  })());
63
36
  if (!parsed.success) {
64
- // unlike a presence file, a broken signal guards nothing: drop it loudly.
65
37
  rmSync(markerPath, { force: true });
66
38
  log("codexstop.reconcile_unparsable", {});
67
39
  return false;
68
40
  }
69
- // Staleness guard: the signal names the account this session was seen on;
70
- // if the session has since respawned onto another account the signal is
71
- // moot. Same when the live seat changed to (or still is) our own account -
72
- // a respawn would land right back where we are.
73
41
  const presence = livingCodexPresences().find((p) => p.supervisorId === input.supervisorId) ?? null;
74
42
  if (presence == null || presence.accountId !== parsed.data.accountId) {
75
43
  rmSync(markerPath, { force: true });
@@ -82,14 +50,6 @@ function promoteReconcileLocked(input: { supervisorId: string; sessionId: string
82
50
  log("codexstop.reconcile_moot", {});
83
51
  return false;
84
52
  }
85
- // Revalidate the DESTINATION at consumption time (bugbot/pullfrog/vercel/
86
- // cubic review catches, PR #34): a signal can sit across resets and usage
87
- // changes, and identity checks alone would move a session onto a live seat
88
- // that has since become exhausted, needs-reauth, or left the pool entirely.
89
- // The SOURCE seat's state is deliberately not re-checked (owner ruling
90
- // 2026-07-20: every pooled non-live sibling follows the seat, healthy or
91
- // not - a non-live session wedges at token expiry regardless of quota).
92
- // Dropped signals are cheap: the sweep re-signals next evaluation.
93
53
  const now = Date.now();
94
54
  const bars = effectiveBars(loadConfig());
95
55
  const index = loadCodexAccounts();
@@ -101,11 +61,7 @@ function promoteReconcileLocked(input: { supervisorId: string; sessionId: string
101
61
  log("codexstop.reconcile_blocked_target", {});
102
62
  return false;
103
63
  }
104
- // blank counts as missing: the supervisor treats a falsy sessionId as
105
- // "resume --last", the exact fallback this guard exists to avoid.
106
64
  if (input.sessionId == null || input.sessionId.trim() === "") {
107
- // keep the signal for the next boundary, whose stdin will carry a real
108
- // id - a resume without one could revive the wrong transcript.
109
65
  log("codexstop.reconcile_no_session", {});
110
66
  return false;
111
67
  }
@@ -125,8 +81,6 @@ async function readStdin(): Promise<string> {
125
81
  return Buffer.concat(chunks).toString("utf8");
126
82
  }
127
83
 
128
- /** The testable core: decide, and on a swap under a supervisor, drop the
129
- * respawn marker. Never throws (a hook failure must not block the stop). */
130
84
  export async function handleCodexStop(input: { rawStdin: string }): Promise<void> {
131
85
  const parsed = CodexStopStdinSchema.safeParse((() => {
132
86
  try {
@@ -138,21 +92,11 @@ export async function handleCodexStop(input: { rawStdin: string }): Promise<void
138
92
  const sessionId = parsed.success ? (parsed.data.session_id ?? null) : null;
139
93
 
140
94
  try {
141
- // No supervisor = no decision AT ALL, checked before evaluate can swap:
142
- // hooks.json is global, so this hook also fires in sessions launched
143
- // around the PATH shim (IDE extension, absolute path), and a swap with
144
- // nobody to respawn strands that session - codex cannot hot-adopt, and
145
- // its guarded reload refuses a cross-account auth.json, so the session
146
- // dies on its stale token with "Please sign in again" (closing-review
147
- // catch). Restart IS the switch; without a restarter, do not switch.
148
95
  const supervisorId = SupervisorIdSchema.parse(process.env[CODEX_SUPERVISOR_ID_ENV]);
149
96
  if (supervisorId === undefined) {
150
97
  log("codexstop.unsupervised_skip", {});
151
98
  return;
152
99
  }
153
- // A pending reconcile signal outranks the normal decision: this session
154
- // is about to respawn onto the live seat, so evaluating it would waste a
155
- // sample (and could even swap the seat out from under the respawn).
156
100
  if (await promoteReconcile({ supervisorId, sessionId })) return;
157
101
  const decision = await evaluateAndMaybeSwapCodex({});
158
102
  if (decision.swapped && decision.account) {
@@ -166,11 +110,6 @@ export async function handleCodexStop(input: { rawStdin: string }): Promise<void
166
110
  log("codexstop.marker", { supervisorId: supervisorId.slice(0, 8) });
167
111
  return;
168
112
  }
169
- // The evaluation's sweep may have signaled THIS session (its own account
170
- // is the wedged one while the live seat is healthy - the lone-stranded
171
- // case the removed self-skip used to lose, bugbot/cubic review catch,
172
- // PR #34): consume it at this very boundary instead of burning one more
173
- // turn on the dead account.
174
113
  await promoteReconcile({ supervisorId, sessionId });
175
114
  } catch (e) {
176
115
  log("codexstop.error", { err: e instanceof Error ? e.message : String(e) });
@@ -181,6 +120,6 @@ export async function runCodexStopHook(): Promise<number> {
181
120
  if (!process.env.TOKENMAXXING_PROBE) {
182
121
  await handleCodexStop({ rawStdin: await readStdin() });
183
122
  }
184
- process.stdout.write("{}"); // documented no-op hook output; never block the stop
123
+ process.stdout.write("{}");
185
124
  return 0;
186
125
  }
@@ -1,13 +1,3 @@
1
- // The `codex` supervisor. Invoked in place of codex (via ~/.config/tokenmaxxing/
2
- // bin/codex on PATH). Codex REQUIRES a restart to change accounts: a running
3
- // process refuses an auth.json swap to a different account (verified
4
- // rust-v0.144.5 reload_if_account_id_matches), so unlike the claude supervisor
5
- // (whose child hot-adopts swaps) this respawn IS the switch mechanism, not just
6
- // UX. The codex Stop hook performs the swap at an idle turn boundary and drops
7
- // a marker keyed by THIS supervisor's id (passed down via env, so N concurrent
8
- // sessions pair correctly); the supervisor then SIGTERMs its child and
9
- // relaunches `codex resume <session-id>` on the freshly-installed account.
10
-
11
1
  import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
12
2
  import { join } from "node:path";
13
3
  import { z } from "zod";
@@ -23,7 +13,6 @@ import { log } from "../lib/log.ts";
23
13
 
24
14
  export const CODEX_SUPERVISOR_ID_ENV = "TOKENMAXXING_CODEX_SUPERVISOR_ID";
25
15
 
26
- /** Subcommands that never host an interactive session worth managing. */
27
16
  const NONINTERACTIVE_SUBCMDS = new Set([
28
17
  "exec", "review", "login", "logout", "mcp", "plugin", "mcp-server", "app-server",
29
18
  "remote-control", "app", "completion", "update", "doctor", "sandbox", "debug",
@@ -32,11 +21,6 @@ const NONINTERACTIVE_SUBCMDS = new Set([
32
21
 
33
22
  const PASSTHROUGH_FLAGS = new Set(["--version", "-V", "--help", "-h"]);
34
23
 
35
- /** Read + validate a codex respawn marker. An unparseable one (version-skew
36
- * hook, corruption) is dropped loudly and reported as absent: the watcher
37
- * checks validity BEFORE the SIGTERM, so garbage never kills the session, and
38
- * the post-exit consume never throws after the child is already dead (PR #36
39
- * review catch, mirroring the claude supervisor). */
40
24
  function consumableCodexMarker(marker: string): z.infer<typeof CodexRespawnMarkerSchema> | null {
41
25
  try {
42
26
  return CodexRespawnMarkerSchema.parse(JSON.parse(readFileSync(marker, "utf8")));
@@ -47,9 +31,6 @@ function consumableCodexMarker(marker: string): z.infer<typeof CodexRespawnMarke
47
31
  }
48
32
  }
49
33
 
50
- /** Root options that consume the NEXT token as their value (verified against
51
- * `codex --help` 0.144.4): without skipping them, `codex -m gpt exec ...`
52
- * would read "gpt" as the subcommand and wrongly supervise an exec run. */
53
34
  const VALUE_TAKING_ROOT_FLAGS = new Set([
54
35
  "-c", "--config", "-i", "--image", "-m", "--model", "--local-provider", "-p", "--profile",
55
36
  "-s", "--sandbox", "-a", "--ask-for-approval", "-C", "--cd", "--add-dir", "--enable",
@@ -70,7 +51,6 @@ export function shouldManageCodex(input: { argv: string[] }): boolean {
70
51
  return firstPositional === null || !NONINTERACTIVE_SUBCMDS.has(firstPositional);
71
52
  }
72
53
 
73
- /** Entry point: `codex ...args` through the on-PATH shim. */
74
54
  export async function runCodexSupervisor(input: { argv: string[] }): Promise<number> {
75
55
  const { argv } = input;
76
56
  const depth = wrapDepth();
@@ -92,19 +72,7 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
92
72
  const real = resolveRealCodex();
93
73
  const childEnv = { ...process.env, [WRAP_DEPTH_ENV]: String(depth + 1) };
94
74
 
95
- // The unmanaged-zone sentinel forces passthrough regardless of argv, exactly
96
- // like the claude shim: a serve turn's agent running `codex exec` must reach
97
- // the real codex instead of dying at the shared depth cap.
98
75
  if (!shouldManageCodex({ argv }) || process.env[UNMANAGED_ENV]) {
99
- // STRIP the supervisor pairing env from unmanaged spawns: a nested codex
100
- // launched from inside a supervised session (e.g. its agent running
101
- // `codex exec ...`) would otherwise inherit the OUTER session's id, and
102
- // its global Stop hook could then write a respawn marker that SIGTERMs
103
- // the outer session MID-TURN and resumes it onto the nested transcript
104
- // (closing-review catch). A managed nested launch is already safe - it
105
- // exports its own fresh id below; only a shim-bypassed absolute-path
106
- // nested launch keeps the inherited env, the same accepted gap as
107
- // claude's bg-daemon bypass.
108
76
  const passthroughEnv: Record<string, string | undefined> = { ...childEnv };
109
77
  delete passthroughEnv[CODEX_SUPERVISOR_ID_ENV];
110
78
  const p = Bun.spawn([real, ...argv], { stdin: "inherit", stdout: "inherit", stderr: "inherit", env: passthroughEnv });
@@ -120,28 +88,12 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
120
88
  process.on("SIGINT", () => {});
121
89
  process.on("SIGHUP", () => {});
122
90
 
123
- // On respawn the ONLY reliable relaunch is `codex resume <session-id>`:
124
- // codex generates its own session ids (there is no flag to pin one at
125
- // launch), so original launch args are used verbatim only for the first
126
- // spawn. Model/sandbox preferences persist in config.toml either way.
127
91
  let launchArgs = argv;
128
92
  let respawns = 0;
129
93
  while (true) {
130
94
  if (existsSync(marker)) rmSync(marker, { force: true });
131
95
  log("codexsupervisor.launch", { supervisorId: supervisorId.slice(0, 8), respawns, args: launchArgs.join(" ") });
132
96
 
133
- // Declare which account THIS session runs on (the live identity at spawn):
134
- // the picker must never target it and the sampler must never rotate its
135
- // parked token while the session lives. Rewritten every respawn (the swap
136
- // changed the live identity); cleared on exit; PID-validated by readers.
137
- // Read + presence-write + spawn run under the codex FLOCK (closing-review
138
- // catch): unlocked, a swap could land between the read and the child's
139
- // auth.json read, seating the child on the NEW account while presence
140
- // named the old one for the session's whole life - un-benching the
141
- // running account for samplers and the picker. Under the flock no swap
142
- // can interleave until after the spawn; the residual window (child
143
- // startup vs a swap acquiring the lock immediately after) is sub-ms in
144
- // practice against a swap's network-bound critical section.
145
97
  const child = await withLock(codexPaths.lockFile, async () => {
146
98
  const spawnAccountId = liveCodexAccountId();
147
99
  const spawned = Bun.spawn([real, ...launchArgs], {
@@ -150,32 +102,16 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
150
102
  stderr: "inherit",
151
103
  env: { ...childEnv, [CODEX_SUPERVISOR_ID_ENV]: supervisorId },
152
104
  });
153
- // Presence pins the CHILD's pid, written after the spawn (still inside
154
- // the flock): the session IS the codex process, and pinning this
155
- // supervisor's pid let a SIGKILLed supervisor prune the presence while
156
- // its orphaned codex kept rotating the account's token (closing-review
157
- // catch). Brief retries cover ps visibility lag on a just-spawned pid.
158
- // FAIL CLOSED on final failure (PR #36 review catch): a session running
159
- // without presence is exactly the unprotected state presence exists to
160
- // prevent - its account would look swappable and samplable - so kill
161
- // the just-spawned child (nothing is in flight yet) and surface the
162
- // error instead of running unprotected.
163
105
  if (spawnAccountId) {
164
106
  for (let attempt = 0; attempt < 10; attempt++) {
165
107
  try {
166
108
  writeCodexPresence({ supervisorId, accountId: spawnAccountId, pid: spawned.pid });
167
109
  break;
168
110
  } catch (e) {
169
- // a child that already exited needs no presence (its absence is
170
- // correct) and must keep its own exit result - the normal exit
171
- // path below handles it (PR #36 second-round catch)
172
111
  if (spawned.exitCode !== null || spawned.signalCode !== null) break;
173
112
  if (attempt === 9) {
174
113
  log("codexsupervisor.presence_failed", { err: e instanceof Error ? e.message : String(e) });
175
114
  spawned.kill();
176
- // the child may have entered raw mode during the retries: await
177
- // its death and restore the terminal before surfacing (PR #36
178
- // second-round catch)
179
115
  await spawned.exited;
180
116
  restoreTermios(savedTermios);
181
117
  throw new Error("could not write the codex presence file - refusing to run an unprotected session (its account would look like a swap target)");
@@ -202,7 +138,7 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
202
138
  const winner = await Promise.race([exited, markerWatch.then((found) => (found ? "marker" : "exit"))]);
203
139
 
204
140
  if (winner === "marker") {
205
- child.kill(); // SIGTERM at the committed turn boundary the Stop hook chose
141
+ child.kill();
206
142
  }
207
143
  await child.exited;
208
144
  done = true;
@@ -218,8 +154,6 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
218
154
  continue;
219
155
  }
220
156
  clearCodexPresence({ supervisorId });
221
- // a reconcile signal addressed to this now-gone session is moot; the
222
- // deciding actor's sweep would gc it eventually, this is just prompt.
223
157
  rmSync(join(codexPaths.reconcileDir, supervisorId), { force: true });
224
158
  log("codexsupervisor.exit", { supervisorId: supervisorId.slice(0, 8), respawns, code: child.exitCode, signal: child.signalCode });
225
159
  return child.exitCode ?? (child.signalCode ? 1 : 0);
@@ -1,7 +1,3 @@
1
- // Stdio MCP entry for the portable Agent Plugin (agent-plugin/).
2
- // Tools wrap existing CLI commands. stdout is reserved for MCP JSON-RPC, so
3
- // every CLI call captures console.log / console.error and returns the text.
4
-
5
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
3
  import { z } from "zod";
@@ -27,13 +23,11 @@ function packageVersion(): string {
27
23
  }
28
24
  }
29
25
 
30
- /** Refuse ambient Claude store overrides the same way the CLI and SDK do. */
31
26
  export function refuseAmbientStoreEnv(): string | null {
32
27
  const nonEmpty = (v: string | undefined) => (v != null && v !== "" ? v : null);
33
28
  return nonEmpty(process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR) ?? nonEmpty(process.env.CLAUDE_CONFIG_DIR);
34
29
  }
35
30
 
36
- /** Redact token-shaped spans so tool results never echo credentials. */
37
31
  export function scrubSecrets(text: string): string {
38
32
  return text
39
33
  .replace(/\b(Bearer\s+)[A-Za-z0-9._\-+/=]+/gi, "$1[redacted]")
@@ -43,11 +37,8 @@ export function scrubSecrets(text: string): string {
43
37
 
44
38
  type CaptureResult = { code: number; stdout: string; stderr: string };
45
39
 
46
- /** Serialize captureCli: console.log/error are process-global, so concurrent
47
- * tool calls would interleave stdout into each other and corrupt JSON-RPC. */
48
40
  let captureChain: Promise<unknown> = Promise.resolve();
49
41
 
50
- /** Run a CLI cmd while keeping stdout clean for the MCP transport. */
51
42
  export async function captureCli(run: () => number | Promise<number>): Promise<CaptureResult> {
52
43
  const job = async (): Promise<CaptureResult> => {
53
44
  const out: string[] = [];
@@ -264,8 +255,6 @@ export function createTokenmaxxingMcpServer(): McpServer {
264
255
  return server;
265
256
  }
266
257
 
267
- /** Entrypoint for the Agent Plugin launcher (`agent-plugin/bin/tokenmaxxing-mcp`).
268
- * Exported because that bin imports this module (so `import.meta.main` is false here). */
269
258
  export async function main(): Promise<void> {
270
259
  const ambient = refuseAmbientStoreEnv();
271
260
  if (ambient != null) {
@@ -1,15 +1,9 @@
1
- // SessionStart hook. A launch/resume backstop: if the active account is already
2
- // over threshold with FRESH usage (e.g. a prior session left it exhausted), swap
3
- // the credential before this session's first turn so it starts on a good account.
4
- // Right after a respawn, the post-swap cooldown in evaluateAndMaybeSwap makes
5
- // this correctly no-op.
6
-
7
1
  import { z } from "zod";
8
2
  import { evaluateAndMaybeSwap } from "../lib/decide.ts";
9
3
  import { log } from "../lib/log.ts";
10
4
 
11
5
  const SessionStartStdin = z.looseObject({
12
- source: z.string().optional(), // startup | resume | clear | compact
6
+ source: z.string().optional(),
13
7
  session_id: z.string().optional(),
14
8
  });
15
9
 
@@ -29,7 +23,6 @@ export async function runSessionStart(): Promise<number> {
29
23
  try {
30
24
  const decision = await evaluateAndMaybeSwap();
31
25
  if (decision.swapped && decision.account) {
32
- // fresh session → it will read the new credential on its first API call.
33
26
  log("sessionstart.swapped", { source, account: decision.account.accountUuid.slice(0, 8) });
34
27
  }
35
28
  } catch (e) {