tokenmaxxing 1.34.0 → 1.38.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 (54) hide show
  1. package/DESIGN.md +3 -5
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/cli/add.ts +23 -40
  5. package/src/cli/auth.ts +38 -52
  6. package/src/cli/check.ts +2 -1
  7. package/src/cli/doctor.ts +13 -13
  8. package/src/cli/init.ts +29 -126
  9. package/src/cli/rename.ts +11 -45
  10. package/src/cli/render.ts +0 -7
  11. package/src/cli/rm.ts +24 -39
  12. package/src/cli/setuptoken.ts +16 -16
  13. package/src/cli/status.ts +95 -238
  14. package/src/cli/switch.ts +54 -46
  15. package/src/entries/codexstophook.ts +23 -23
  16. package/src/entries/codexsupervisor.ts +3 -3
  17. package/src/entries/sessionstart.ts +5 -3
  18. package/src/entries/statusline.ts +17 -19
  19. package/src/entries/stopfailurehook.ts +9 -7
  20. package/src/entries/stophook.ts +5 -4
  21. package/src/entries/subagentstatusline.ts +2 -7
  22. package/src/lib/atomic.ts +2 -8
  23. package/src/lib/claude.ts +320 -0
  24. package/src/lib/claudelock.ts +2 -3
  25. package/src/lib/codex.ts +345 -0
  26. package/src/lib/codexauth.ts +9 -7
  27. package/src/lib/codexoauth.ts +5 -11
  28. package/src/lib/codexpresence.ts +3 -19
  29. package/src/lib/codexusage.ts +17 -41
  30. package/src/lib/credstore.ts +3 -7
  31. package/src/lib/decide.ts +86 -174
  32. package/src/lib/http.ts +3 -6
  33. package/src/lib/install.ts +4 -7
  34. package/src/lib/oauth.ts +14 -12
  35. package/src/lib/paths.ts +15 -7
  36. package/src/lib/picker.ts +56 -37
  37. package/src/lib/proc.ts +4 -4
  38. package/src/lib/provider.ts +34 -0
  39. package/src/lib/sample.ts +33 -33
  40. package/src/lib/state.ts +51 -18
  41. package/src/lib/swap.ts +41 -38
  42. package/src/lib/types.ts +31 -51
  43. package/src/lib/usage.ts +28 -20
  44. package/src/main.ts +20 -20
  45. package/src/cli/codexadd.ts +0 -89
  46. package/src/cli/codexinit.ts +0 -120
  47. package/src/cli/codexrm.ts +0 -36
  48. package/src/cli/codexswitch.ts +0 -113
  49. package/src/cli/onboard.ts +0 -92
  50. package/src/lib/codexdecide.ts +0 -157
  51. package/src/lib/codexpick.ts +0 -80
  52. package/src/lib/codexsample.ts +0 -48
  53. package/src/lib/codexstate.ts +0 -22
  54. package/src/lib/codexswap.ts +0 -70
@@ -1,18 +1,17 @@
1
1
  import { join } from "node:path";
2
2
  import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
3
3
  import { z } from "zod";
4
- import { codexPaths } from "../lib/paths.ts";
4
+ import { codexPaths, codexPool } from "../lib/paths.ts";
5
5
  import { withLock } from "../lib/lock.ts";
6
6
  import { writeFileAtomic } from "../lib/atomic.ts";
7
- import { evaluateAndMaybeSwapCodex } from "../lib/codexdecide.ts";
8
- import { isCodexExhausted } from "../lib/codexpick.ts";
7
+ import { codex, codexPickCtx, reconcileSiblings } from "../lib/codex.ts";
8
+ import { liveCodexAccountId } from "../lib/codexauth.ts";
9
+ import { evaluateAndMaybeSwap } from "../lib/decide.ts";
10
+ import { isExhausted } from "../lib/picker.ts";
9
11
  import { livingCodexPresences } from "../lib/codexpresence.ts";
10
- import { liveCodexAccountId } from "../lib/codexsample.ts";
11
- import { loadCodexAccounts } from "../lib/codexstate.ts";
12
- import { loadConfig } from "../lib/state.ts";
13
- import { thresholdBars } from "../lib/picker.ts";
12
+ import { loadAccounts } from "../lib/state.ts";
14
13
  import { CODEX_SUPERVISOR_ID_ENV } from "./codexsupervisor.ts";
15
- import { CodexReconcileMarkerSchema, CodexRespawnMarkerSchema, CodexStopStdinSchema, type CodexAccount } from "../lib/types.ts";
14
+ import { CodexReconcileMarkerSchema, CodexRespawnMarkerSchema, CodexStopStdinSchema, JsonTextSchema } from "../lib/types.ts";
16
15
  import { log } from "../lib/log.ts";
17
16
 
18
17
  const SupervisorIdSchema = z.string().min(1).optional().catch(undefined);
@@ -20,7 +19,7 @@ const SupervisorIdSchema = z.string().min(1).optional().catch(undefined);
20
19
  async function promoteReconcile(input: { supervisorId: string; sessionId: string | null }): Promise<boolean> {
21
20
  const markerPath = join(codexPaths.reconcileDir, input.supervisorId);
22
21
  if (!existsSync(markerPath)) return false;
23
- return withLock(codexPaths.lockFile, async () => promoteReconcileLocked(input));
22
+ return withLock(codexPool.lockFile, async () => promoteReconcileLocked(input));
24
23
  }
25
24
 
26
25
  function promoteReconcileLocked(input: { supervisorId: string; sessionId: string | null }): boolean {
@@ -51,12 +50,8 @@ function promoteReconcileLocked(input: { supervisorId: string; sessionId: string
51
50
  return false;
52
51
  }
53
52
  const now = Date.now();
54
- const bars = thresholdBars(loadConfig());
55
- const index = loadCodexAccounts();
56
- const unusable = (account: CodexAccount): boolean =>
57
- account.needsReauth === true || isCodexExhausted({ account, thresholds: bars, now });
58
- const liveAccount = index.accounts.find((a) => a.accountId === liveId);
59
- if (!liveAccount || unusable(liveAccount)) {
53
+ const liveAccount = loadAccounts(codexPool).accounts.find((a) => a.id === liveId);
54
+ if (!liveAccount || liveAccount.needsReauth === true || isExhausted(liveAccount, codexPickCtx(now, liveId))) {
60
55
  rmSync(markerPath, { force: true });
61
56
  log("codexstop.reconcile_blocked_target", {});
62
57
  return false;
@@ -75,6 +70,14 @@ function promoteReconcileLocked(input: { supervisorId: string; sessionId: string
75
70
  return true;
76
71
  }
77
72
 
73
+ async function sweepSiblings(now: number): Promise<void> {
74
+ try {
75
+ await withLock(codexPool.lockFile, () => reconcileSiblings(now));
76
+ } catch (e) {
77
+ log("codexstop.resweep_failed", { err: e instanceof Error ? e.message : String(e) });
78
+ }
79
+ }
80
+
78
81
  async function readStdin(): Promise<string> {
79
82
  const chunks: Uint8Array[] = [];
80
83
  for await (const chunk of Bun.stdin.stream()) chunks.push(chunk);
@@ -82,13 +85,7 @@ async function readStdin(): Promise<string> {
82
85
  }
83
86
 
84
87
  export async function handleCodexStop(input: { rawStdin: string }): Promise<void> {
85
- const parsed = CodexStopStdinSchema.safeParse((() => {
86
- try {
87
- return JSON.parse(input.rawStdin);
88
- } catch {
89
- return {};
90
- }
91
- })());
88
+ const parsed = CodexStopStdinSchema.safeParse(JsonTextSchema.safeParse(input.rawStdin).data);
92
89
  const sessionId = parsed.success ? (parsed.data.session_id ?? null) : null;
93
90
 
94
91
  try {
@@ -98,8 +95,11 @@ export async function handleCodexStop(input: { rawStdin: string }): Promise<void
98
95
  return;
99
96
  }
100
97
  if (await promoteReconcile({ supervisorId, sessionId })) return;
101
- const decision = await evaluateAndMaybeSwapCodex({});
98
+ const now = Date.now();
99
+ await withLock(codexPool.lockFile, () => reconcileSiblings(now));
100
+ const decision = await evaluateAndMaybeSwap(codex, now);
102
101
  if (decision.swapped && decision.account) {
102
+ await sweepSiblings(now);
103
103
  mkdirSync(codexPaths.respawnDir, { recursive: true });
104
104
  const payload = CodexRespawnMarkerSchema.parse({
105
105
  account: decision.account.label,
@@ -1,12 +1,12 @@
1
1
  import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { z } from "zod";
4
- import { codexPaths, paths } from "../lib/paths.ts";
4
+ import { codexPaths, codexPool, paths } from "../lib/paths.ts";
5
5
  import { withLock } from "../lib/lock.ts";
6
6
  import { LOOP_DIAGNOSIS, MAX_WRAP_DEPTH, UNMANAGED_ENV, WRAP_DEPTH_ENV, WRAP_RATE_MAX, WRAP_RATE_WINDOW_MS, wrapDepth, wrapperEntryRateTripped } from "../lib/claudebin.ts";
7
7
  import { resolveRealCodex } from "../lib/codexbin.ts";
8
8
  import { clearCodexPresence, writeCodexPresence } from "../lib/codexpresence.ts";
9
- import { liveCodexAccountId } from "../lib/codexsample.ts";
9
+ import { liveCodexAccountId } from "../lib/codexauth.ts";
10
10
  import { saveTermios, restoreTermios } from "../lib/tty.ts";
11
11
  import { CodexRespawnMarkerSchema } from "../lib/types.ts";
12
12
  import { log } from "../lib/log.ts";
@@ -95,7 +95,7 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
95
95
  if (existsSync(marker)) rmSync(marker, { force: true });
96
96
  log("codexsupervisor.launch", { supervisorId: supervisorId.slice(0, 8), respawns, args: launchArgs.join(" ") });
97
97
 
98
- const child = await withLock(codexPaths.lockFile, async () => {
98
+ const child = await withLock(codexPool.lockFile, async () => {
99
99
  const spawnAccountId = liveCodexAccountId();
100
100
  const spawned = Bun.spawn([real, ...launchArgs], {
101
101
  stdin: "inherit",
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
+ import { claude } from "../lib/claude.ts";
2
3
  import { evaluateAndMaybeSwap } from "../lib/decide.ts";
3
4
  import { log } from "../lib/log.ts";
5
+ import { JsonTextSchema } from "../lib/types.ts";
4
6
 
5
7
  const SessionStartStdin = z.looseObject({
6
8
  source: z.string().optional(),
@@ -17,13 +19,13 @@ export async function runSessionStart(): Promise<number> {
17
19
  if (process.env.TOKENMAXXING_PROBE) return 0;
18
20
 
19
21
  const raw = await readStdin();
20
- const parsed = SessionStartStdin.safeParse((() => { try { return JSON.parse(raw); } catch { return {}; } })());
22
+ const parsed = SessionStartStdin.safeParse(JsonTextSchema.safeParse(raw).data);
21
23
  const source = parsed.success ? parsed.data.source : undefined;
22
24
 
23
25
  try {
24
- const decision = await evaluateAndMaybeSwap();
26
+ const decision = await evaluateAndMaybeSwap(claude);
25
27
  if (decision.swapped && decision.account) {
26
- log("sessionstart.swapped", { source, account: decision.account.accountUuid.slice(0, 8) });
28
+ log("sessionstart.swapped", { source, account: decision.account.id.slice(0, 8) });
27
29
  }
28
30
  } catch (e) {
29
31
  log("sessionstart.error", { err: e instanceof Error ? e.message : String(e) });
@@ -1,16 +1,18 @@
1
1
  import { sortBy } from "es-toolkit";
2
2
  import { z } from "zod";
3
3
  import { readOAuthAccount } from "../lib/claudejson.ts";
4
+ import { claudePool } from "../lib/paths.ts";
4
5
  import { loadAccounts, loadConfig, loadLastSwapAt, writeUsage } from "../lib/state.ts";
5
6
  import { familyTokens, matchedFamily, parseStatusLineStdin, parseStatusLineModel } from "../lib/usage.ts";
6
- import { earliestReset, weeklyExpiry } from "../lib/picker.ts";
7
+ import { earliestReset, limitWindows, weeklyExpiry, weeklyWindow } from "../lib/picker.ts";
7
8
  import { worktreeName } from "../lib/worktree.ts";
8
9
  import { makeColors, makeUsagePaint } from "../cli/render.ts";
9
10
  import { fmtResetShort } from "../lib/usage.ts";
10
11
  import {
11
12
  AccountsIndexSchema,
13
+ JsonTextSchema,
12
14
  StatusLineStdinSchema,
13
- UsageWindowSchema,
15
+ WindowSchema,
14
16
  type Account,
15
17
  type UsageState,
16
18
  type UsageWindow,
@@ -20,7 +22,7 @@ const ADOPTION_GRACE_MS = 45_000;
20
22
 
21
23
  const RenderCtxSchema = z.object({
22
24
  accounts: AccountsIndexSchema,
23
- perModel: z.record(z.string(), UsageWindowSchema),
25
+ perModel: z.record(z.string(), WindowSchema),
24
26
  switchModels: z.array(z.string()),
25
27
  worktree: z.string().nullable(),
26
28
  liveAccount: z.string().nullable(),
@@ -78,11 +80,11 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
78
80
  windows.push(seg("", wins.sevenDay, wins.sevenDay.resetsAt));
79
81
  }
80
82
  const seatUuid =
81
- (ctx.liveAccount != null && ctx.accounts.accounts.some((a) => a.accountUuid === ctx.liveAccount) ? ctx.liveAccount : null) ??
82
- ctx.accounts.activeAccountUuid;
83
+ (ctx.liveAccount != null && ctx.accounts.accounts.some((a) => a.id === ctx.liveAccount) ? ctx.liveAccount : null) ??
84
+ ctx.accounts.activeId;
83
85
  const walled = (a: Account) => a.enforcedUntil != null && a.enforcedUntil > ctx.now;
84
86
  const wallSeg = (wall: number) => seg("", { usedPercentage: 100, resetsAt: wall }, wall);
85
- const seat = ctx.accounts.accounts.find((a) => a.accountUuid === seatUuid);
87
+ const seat = ctx.accounts.accounts.find((a) => a.id === seatUuid);
86
88
  if (seat?.enforcedUntil != null && walled(seat)) windows.push(wallSeg(seat.enforcedUntil));
87
89
  const seatMarker = seat && walled(seat) ? paint(100)("◆") : col.green("◆");
88
90
  const active =
@@ -93,20 +95,20 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
93
95
  : "";
94
96
 
95
97
  const parked = sortBy(
96
- ctx.accounts.accounts.filter((a) => a.accountUuid !== seatUuid),
98
+ ctx.accounts.accounts.filter((a) => a.id !== seatUuid),
97
99
  [(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, ctx.now)],
98
100
  );
99
101
  const poolSeg = (a: Account): string => {
100
102
  const marker = a.needsReauth ? col.red("✗") : walled(a) ? paint(100)("◇") : col.cyan("◇");
101
103
  if (a.enforcedUntil != null && walled(a)) return `${marker} ${wallSeg(a.enforcedUntil)}`;
102
- const week = a.lastUsage?.sevenDay;
104
+ const week = weeklyWindow(a);
103
105
  if (week == null) return `${marker} ?`;
104
106
  const weekUsed = used(week);
105
107
  if (Math.round(weekUsed) <= 0) return `${marker} ${paint(0)("full")}`;
106
108
 
107
109
  const parts: string[] = [];
108
- for (const [name, w] of Object.entries(a.lastUsage?.perModel ?? {})) {
109
- if (used(w) > weekUsed) parts.push(seg(initial(name), w, null));
110
+ for (const w of limitWindows(a)) {
111
+ if (used(w) > weekUsed) parts.push(seg(initial(w.name ?? ""), w, null));
110
112
  }
111
113
  const expiry = weeklyExpiry(a, ctx.now);
112
114
  parts.push(seg("", week, Number.isFinite(expiry) ? expiry : null));
@@ -117,19 +119,14 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
117
119
  }
118
120
 
119
121
  export async function runStatusline(): Promise<number> {
120
- const raw = await readStdin();
121
- let obj: unknown = null;
122
- try {
123
- obj = JSON.parse(raw);
124
- } catch {
125
- }
122
+ const obj = JsonTextSchema.safeParse(await readStdin()).data ?? null;
126
123
  const now = Date.now();
127
124
 
128
125
  let account: string | null = null;
129
126
  try {
130
127
  account = readOAuthAccount()?.accountUuid ?? null;
131
128
  const windows = obj == null ? null : parseStatusLineStdin(obj);
132
- const lastSwapAt = loadLastSwapAt();
129
+ const lastSwapAt = loadLastSwapAt(claudePool);
133
130
  if (windows && (lastSwapAt == null || now - lastSwapAt >= ADOPTION_GRACE_MS)) {
134
131
  const state: UsageState = { fiveHour: windows.fiveHour, sevenDay: windows.sevenDay, account, ts: now, model: parseStatusLineModel(obj) };
135
132
  writeUsage(state);
@@ -140,13 +137,14 @@ export async function runStatusline(): Promise<number> {
140
137
  let line: string;
141
138
  try {
142
139
  const cfg = loadConfig();
143
- const accounts = loadAccounts();
140
+ const accounts = loadAccounts(claudePool);
144
141
  const stdin = StatusLineStdinSchema.safeParse(obj);
145
142
  const dir = stdin.success ? (stdin.data.workspace?.current_dir ?? stdin.data.workspace?.project_dir ?? null) : null;
146
143
  const colorterm = z.string().optional().parse(process.env.COLORTERM);
144
+ const live = accounts.accounts.find((a) => a.id === account);
147
145
  const ctx: RenderCtx = {
148
146
  accounts,
149
- perModel: accounts.accounts.find((a) => a.accountUuid === account)?.lastUsage?.perModel ?? {},
147
+ perModel: Object.fromEntries((live ? limitWindows(live) : []).map((w) => [w.name ?? "", w])),
150
148
  switchModels: cfg.policy.switchModels,
151
149
  worktree: dir == null ? null : worktreeName(dir),
152
150
  liveAccount: account,
@@ -2,12 +2,14 @@ import { join } from "node:path";
2
2
  import { z } from "zod";
3
3
  import { paths } from "../lib/paths.ts";
4
4
  import { writeFileAtomic } from "../lib/atomic.ts";
5
+ import { claude } from "../lib/claude.ts";
5
6
  import { readOAuthAccount } from "../lib/claudejson.ts";
6
7
  import { evaluateAndMaybeSwap } from "../lib/decide.ts";
7
8
  import { withLock } from "../lib/lock.ts";
9
+ import { claudePool } from "../lib/paths.ts";
8
10
  import { POST_SWAP_COOLDOWN_MS, loadConfig, loadLastSwapAt } from "../lib/state.ts";
9
11
  import { classifyEnforcedLimit, findEnforcedRow, parseErrorBody, readTranscriptTail } from "../lib/usage.ts";
10
- import { RespawnMarkerSchema, type EnforcedLimit } from "../lib/types.ts";
12
+ import { JsonTextSchema, RespawnMarkerSchema, type EnforcedLimit } from "../lib/types.ts";
11
13
  import { log } from "../lib/log.ts";
12
14
 
13
15
  const StopFailureStdin = z.looseObject({
@@ -29,11 +31,11 @@ async function readStdin(): Promise<string> {
29
31
  export async function runStopFailureHook(): Promise<number> {
30
32
  if (process.env.TOKENMAXXING_PROBE) return 0;
31
33
 
32
- const entry = await withLock(paths.lockFile, () => ({ swapClock: loadLastSwapAt(), account: readOAuthAccount()?.accountUuid ?? null }));
34
+ const entry = await withLock(claudePool.lockFile, () => ({ swapClock: loadLastSwapAt(claudePool), account: readOAuthAccount()?.accountUuid ?? null }));
33
35
  const account = entry.account;
34
36
  const now = Date.now();
35
37
  const raw = await readStdin();
36
- const parsed = StopFailureStdin.safeParse((() => { try { return JSON.parse(raw); } catch { return {}; } })());
38
+ const parsed = StopFailureStdin.safeParse(JsonTextSchema.safeParse(raw).data);
37
39
  const stdin = parsed.success ? parsed.data : {};
38
40
  if (stdin.error !== undefined && stdin.error !== "rate_limit") return 0;
39
41
 
@@ -44,7 +46,7 @@ export async function runStopFailureHook(): Promise<number> {
44
46
  const canPause = process.env.TOKENMAXXING_SUPERVISED === "1" && pinnedSid != null && mainLoop;
45
47
 
46
48
  try {
47
- const lastSwapAt = loadLastSwapAt();
49
+ const lastSwapAt = loadLastSwapAt(claudePool);
48
50
  if (lastSwapAt != null && lastSwapAt === entry.swapClock && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
49
51
  log("stopfailure.cooldown", { sinceSwapMs: now - lastSwapAt });
50
52
  return 0;
@@ -68,7 +70,7 @@ export async function runStopFailureHook(): Promise<number> {
68
70
  });
69
71
  }
70
72
 
71
- const decision = await evaluateAndMaybeSwap(now, canPause && enforced != null, enforced);
73
+ const decision = await evaluateAndMaybeSwap(claude, now, canPause && enforced != null, enforced);
72
74
  if (enforced && canPause && pinnedSid && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
73
75
  const marker = join(paths.respawnDir, pinnedSid);
74
76
  const payload = RespawnMarkerSchema.parse({
@@ -79,9 +81,9 @@ export async function runStopFailureHook(): Promise<number> {
79
81
  ...(launchedAt != null ? { launchedAt } : {}),
80
82
  });
81
83
  writeFileAtomic(marker, JSON.stringify(payload));
82
- log("stopfailure.marker", { session: (stdinSid ?? pinnedSid).slice(0, 8), account: decision.account.accountUuid.slice(0, 8), waitUntil: payload.waitUntil });
84
+ log("stopfailure.marker", { session: (stdinSid ?? pinnedSid).slice(0, 8), account: decision.account.id.slice(0, 8), waitUntil: payload.waitUntil });
83
85
  } else {
84
- log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.accountUuid.slice(0, 8), waitUntil: decision.waitUntil });
86
+ log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
85
87
  }
86
88
  } catch (e) {
87
89
  log("stopfailure.error", { err: e instanceof Error ? e.message : String(e) });
@@ -2,8 +2,9 @@ import { join } from "node:path";
2
2
  import { z } from "zod";
3
3
  import { paths } from "../lib/paths.ts";
4
4
  import { writeFileAtomic } from "../lib/atomic.ts";
5
+ import { claude } from "../lib/claude.ts";
5
6
  import { evaluateAndMaybeSwap } from "../lib/decide.ts";
6
- import { RespawnMarkerSchema } from "../lib/types.ts";
7
+ import { JsonTextSchema, RespawnMarkerSchema } from "../lib/types.ts";
7
8
  import { log } from "../lib/log.ts";
8
9
 
9
10
  const StopStdin = z.looseObject({ session_id: z.uuid().optional().catch(undefined) });
@@ -18,15 +19,15 @@ export async function runStopHook(): Promise<number> {
18
19
  if (process.env.TOKENMAXXING_PROBE) return 0;
19
20
 
20
21
  const raw = await readStdin();
21
- const parsed = StopStdin.safeParse((() => { try { return JSON.parse(raw); } catch { return {}; } })());
22
+ const parsed = StopStdin.safeParse(JsonTextSchema.safeParse(raw).data);
22
23
  const stdinSid = parsed.success ? parsed.data.session_id : undefined;
23
24
  const pinnedSid = process.env.TOKENMAXXING_SESSION_ID;
24
25
 
25
26
  try {
26
27
  const canPause = process.env.TOKENMAXXING_SUPERVISED === "1" && pinnedSid != null;
27
- const decision = await evaluateAndMaybeSwap(Date.now(), canPause);
28
+ const decision = await evaluateAndMaybeSwap(claude, Date.now(), canPause);
28
29
  if (decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
29
- log(decision.swapped ? "stop.swapped" : "stop.wait", { account: decision.account.accountUuid.slice(0, 8), waitUntil: decision.waitUntil });
30
+ log(decision.swapped ? "stop.swapped" : "stop.wait", { account: decision.account.id.slice(0, 8), waitUntil: decision.waitUntil });
30
31
  if (decision.waitUntil !== undefined && canPause && pinnedSid) {
31
32
  const marker = join(paths.respawnDir, pinnedSid);
32
33
  const launchedAt = z.coerce.number().finite().optional().catch(undefined).parse(process.env.TOKENMAXXING_LAUNCHED_AT);
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { makeColors, makeUsagePaint } from "../cli/render.ts";
3
3
  import { readStdin } from "./statusline.ts";
4
- import { SubagentStatusLineStdinSchema } from "../lib/types.ts";
4
+ import { JsonTextSchema, SubagentStatusLineStdinSchema } from "../lib/types.ts";
5
5
 
6
6
  const RowCtxSchema = z.object({ color: z.boolean(), truecolor: z.boolean() });
7
7
  export type RowCtx = z.infer<typeof RowCtxSchema>;
@@ -43,12 +43,7 @@ export function renderSubagentRows(stdinObj: unknown, ctx: RowCtx): string[] {
43
43
  }
44
44
 
45
45
  export async function runSubagentStatusline(): Promise<number> {
46
- const raw = await readStdin();
47
- let obj: unknown = null;
48
- try {
49
- obj = JSON.parse(raw);
50
- } catch {
51
- }
46
+ const obj = JsonTextSchema.safeParse(await readStdin()).data ?? null;
52
47
  const colorterm = z.string().optional().parse(process.env.COLORTERM);
53
48
  const rows = renderSubagentRows(obj, {
54
49
  color: !process.env.NO_COLOR,
package/src/lib/atomic.ts CHANGED
@@ -1,20 +1,14 @@
1
- import { closeSync, mkdirSync, openSync, renameSync, rmSync, writeSync, fsyncSync } from "node:fs";
1
+ import { closeSync, mkdirSync, openSync, renameSync, rmSync, writeFileSync, fsyncSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
 
4
4
  export function writeFileAtomic(file: string, data: string | Uint8Array, mode = 0o600): void {
5
5
  const dir = dirname(file);
6
6
  mkdirSync(dir, { recursive: true });
7
7
  const tmp = `${file}.tmp.${process.pid}.${Math.floor(Math.random() * 1e9).toString(36)}`;
8
- const bytes = data instanceof Uint8Array ? data : new TextEncoder().encode(data);
9
8
  const fd = openSync(tmp, "wx", mode);
10
9
  try {
11
10
  try {
12
- let offset = 0;
13
- while (offset < bytes.length) {
14
- const written = writeSync(fd, bytes, offset);
15
- if (written <= 0) throw new Error(`short write on ${tmp}: ${offset}/${bytes.length} bytes (disk full?)`);
16
- offset += written;
17
- }
11
+ writeFileSync(fd, data);
18
12
  fsyncSync(fd);
19
13
  } finally {
20
14
  closeSync(fd);