tokenmaxxing 1.87.0 → 1.88.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.
package/DESIGN.md CHANGED
@@ -37,7 +37,7 @@ It is a process manager only - spawn with inherited stdio (stdin through a relay
37
37
  - `usage/<uuid8>.json` - the statusline tee per account, written by the sessions running on it.
38
38
  - `live/<session-id>` - PID-validated presence per supervised session, naming its account; placement counts them.
39
39
  - `respawn/<session-id>` - per-session respawn markers (the hook→supervisor signal): the target account, the session to resume, a `waitUntil`, a `compact` flag, the `origin` (the hook or the seat watch that wrote it), and the launch time the marker was written for.
40
- - `wait-queue.json` - the depleted-pool wait claims (`{sessionId, accountId, at, waitUntil}`), so at most four sessions wait on one account; a session that arrives when every account already has four waiters takes the soonest reset anyway.
40
+ - `wait-queue.json` - the claims of supervised sessions that are moving or waiting (`{sessionId, accountId, at, waitUntil}`), naming the account each relaunches on: placement counts a claimed session there, and at most four sessions wait on one account; a session that arrives when every account already has four waiters takes the soonest reset anyway.
41
41
  - `bin/claude` and `bin/codex` - the supervisors, beside the `tokenmaxxing` and `xx` entry points; `bin/pi` after `init --pi`.
42
42
  - `pi-stores/claude/<uuid8>/` and `pi-stores/codex/<uuid8>/` - one pi store per pooled account that has a pi login: the account's own pi grant in `auth.json`, everything else symlinked to the shared `~/.pi/agent` ([pi support](docs/content/docs/pi.mdx)).
43
43
  - `sessions/<sid>.json`, `onboard/`, `hub-key`, `update.json`, `spawnrate.json`, `tokenmaxxing.log`, the codex `codex-onboard/`, `codex-respawn/`, and `codex-live/`, and the `grok-*` and `opencode-go-*` state of the status-only pools: the full table is in [State directory layout](docs/content/docs/state-directory.mdx).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.87.0",
3
+ "version": "1.88.0",
4
4
  "description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -386,6 +386,13 @@ export async function runPiSupervisor(argv: string[]): Promise<number> {
386
386
  savedTermios,
387
387
  });
388
388
  }
389
+ if (pool === "claude") {
390
+ try {
391
+ releaseWaitClaim(id);
392
+ } catch (e) {
393
+ log("pisupervisor.claim_release_failed", { err: errorMessage(e) });
394
+ }
395
+ }
389
396
  return { proc: spawned, seat: picked };
390
397
  });
391
398
  if (launched == null) {
@@ -422,7 +429,6 @@ export async function runPiSupervisor(argv: string[]): Promise<number> {
422
429
  if (waitUntil > Date.now()) {
423
430
  if (await countdownWait(target.label, waitUntil, { stream: false, say })) overriddenUntil = waitUntil;
424
431
  } else say(`\n\x1b[36m↻ tokenmaxxing: moving pi to ${target.label} - resuming...\x1b[0m\n`);
425
- await releaseClaim();
426
432
  wanted = target.id;
427
433
  launchArgs = sessionExists(dirs, sid) ? ["--session-id", sid, ...args.flags, "--", RESUME_PROMPT] : firstArgs;
428
434
  continue;
@@ -180,7 +180,24 @@ type MarkerGate = {
180
180
  overriddenUntil: number;
181
181
  };
182
182
 
183
- function consumableMarker(marker: string, gate: MarkerGate): z.infer<typeof RespawnMarkerSchema> | null {
183
+ function releaseClaimLocked(sid: string): void {
184
+ try {
185
+ releaseWaitClaim(sid);
186
+ } catch (e) {
187
+ log("supervisor.claim_release_failed", { err: errorMessage(e) });
188
+ }
189
+ }
190
+
191
+ async function discardMarker(marker: string, event: string, fields: Record<string, unknown>): Promise<null> {
192
+ await withLock(claudePool.lockFile, () => {
193
+ rmSync(marker, { force: true });
194
+ releaseClaimLocked(basename(marker));
195
+ });
196
+ log(event, fields);
197
+ return null;
198
+ }
199
+
200
+ async function consumableMarker(marker: string, gate: MarkerGate): Promise<z.infer<typeof RespawnMarkerSchema> | null> {
184
201
  let m: z.infer<typeof RespawnMarkerSchema>;
185
202
  try {
186
203
  m = readJsonFile(marker, RespawnMarkerSchema);
@@ -191,14 +208,10 @@ function consumableMarker(marker: string, gate: MarkerGate): z.infer<typeof Resp
191
208
  );
192
209
  }
193
210
  if (m.launchedAt !== undefined && m.launchedAt !== gate.launchedAt) {
194
- rmSync(marker, { force: true });
195
- log("supervisor.marker_stale", { markerLaunch: m.launchedAt, childLaunch: gate.launchedAt });
196
- return null;
211
+ return discardMarker(marker, "supervisor.marker_stale", { markerLaunch: m.launchedAt, childLaunch: gate.launchedAt });
197
212
  }
198
213
  if (m.waitUntil > Date.now() && m.waitUntil <= gate.overriddenUntil) {
199
- rmSync(marker, { force: true });
200
- log("supervisor.marker_overridden", { waitUntil: m.waitUntil });
201
- return null;
214
+ return discardMarker(marker, "supervisor.marker_overridden", { waitUntil: m.waitUntil });
202
215
  }
203
216
  return m;
204
217
  }
@@ -450,6 +463,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
450
463
  savedTermios,
451
464
  });
452
465
  }
466
+ releaseClaimLocked(sid);
453
467
  return { child: spawned, seat: picked };
454
468
  });
455
469
  if (launched == null) {
@@ -462,7 +476,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
462
476
  await raceMarkerOrExit({
463
477
  child: proc,
464
478
  tick: async () => {
465
- if (existsSync(marker) && consumableMarker(marker, gate) != null) return true;
479
+ if (existsSync(marker) && (await consumableMarker(marker, gate)) != null) return true;
466
480
  if (seat && !terminating && Date.now() >= seatCheckAt) {
467
481
  const decided = await moveExhaustedSeat(seat, sid, gate, model);
468
482
  seatCheckAt = Date.now() + (decided ? SEAT_RETRY_MS : SEAT_POLL_MS);
@@ -478,7 +492,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
478
492
  },
479
493
  });
480
494
 
481
- const m = !terminating && existsSync(marker) ? consumableMarker(marker, gate) : null;
495
+ const m = !terminating && existsSync(marker) ? await consumableMarker(marker, gate) : null;
482
496
  if (m) {
483
497
  rmSync(marker, { force: true });
484
498
  respawns++;
@@ -511,7 +525,6 @@ export async function runSupervisor(argv: string[]): Promise<number> {
511
525
  if (m.waitUntil > Date.now()) {
512
526
  if (await countdownWait(label, m.waitUntil, { stream, say })) overriddenUntil = m.waitUntil;
513
527
  } else say(`\n\x1b[36m↻ tokenmaxxing: moving to ${label} - resuming...\x1b[0m\n`, `tokenmaxxing: moving to ${label} and resuming.`);
514
- await releaseClaim();
515
528
  wanted = m.accountId;
516
529
  saveSessionFlags(m.sessionId, persistable, process.cwd());
517
530
  const prompt = resumable ? resumePrompt({ compacted, origin: m.origin }) : null;
package/src/lib/claude.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { mkdirSync, readFileSync, rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { countBy } from "es-toolkit";
3
4
  import { z } from "zod";
4
5
  import { readItem, writeItem, deleteItem, isolatedTarget, readStore, storeTarget, claudeAiOauthOnly } from "./credstore.ts";
5
6
  import { CLAUDE_BIN, resolveRealBin, resolveVerifiedClaude } from "./claudebin.ts";
@@ -10,10 +11,10 @@ import { claudeTierLabel, describeIdentity, fetchTokenIdentity, isDeadCredential
10
11
  import { claudePool, env, paths, seatFromEnv, storeDirFor } from "./paths.ts";
11
12
  import { landWindows, pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
12
13
  import { deletePiStore } from "./piauth.ts";
13
- import { seatCounts } from "./presence.ts";
14
+ import { livingPresences } from "./presence.ts";
14
15
  import { StoreUnusableError, type Observation, type Provider, type SampleReport } from "./provider.ts";
15
16
  import { foldTee, sampleAccountUsage, teeObservation, usageBlockedUntil } from "./sample.ts";
16
- import { clearUsageSnapshot, loadAccounts, loadConfig, loadUsageSnapshot, pinBinOverride, readJsonFile, saveAccounts, type Harvest } from "./state.ts";
17
+ import { clearUsageSnapshot, liveWaitClaims, loadAccounts, loadConfig, loadUsageSnapshot, pinBinOverride, readJsonFile, saveAccounts, type Harvest } from "./state.ts";
17
18
  import { saveTermios, restoreTermios } from "./tty.ts";
18
19
  import { fetchUsageDirect, gatedFamilies, mergeWindows, modelFromFlag, scrubCredentialEnv, windowsOf } from "./usage.ts";
19
20
  import { CredentialBlobSchema, JsonTextSchema, OAuthAccountSchema, type Account, type Config, type ModelInfo } from "./types.ts";
@@ -24,7 +25,9 @@ function liveId(): string | null {
24
25
  }
25
26
 
26
27
  function presence(): Map<string, number> {
27
- return seatCounts(paths.presenceDir);
28
+ const seats = new Map(livingPresences(paths.presenceDir).map((s) => [s.id, s.accountId]));
29
+ for (const claim of liveWaitClaims(Date.now())) seats.set(claim.sessionId, claim.accountId);
30
+ return new Map(Object.entries(countBy([...seats.values()], (id) => id)));
28
31
  }
29
32
 
30
33
  const PROBE_BACKOFF_CAP_MS = 30 * 60 * 1000;
package/src/lib/decide.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { countBy } from "es-toolkit";
2
+ import { CLAUDE_COMPACT_KILL_MS } from "./compact.ts";
2
3
  import { withLock } from "./lock.ts";
3
4
  import { loadAccounts, loadConfig, liveWaitClaims, releaseWaitClaim, replaceWaitClaim, saveAccounts } from "./state.ts";
4
5
  import { isExhausted, landWindows, limitWindows, liveUsed, nextWeeklyReset, pickBest, pickWaitTarget, sessionWindow, thresholdBars, weeklyWindow, type PickCtx } from "./picker.ts";
@@ -16,6 +17,7 @@ export type EvalOpts = {
16
17
  };
17
18
 
18
19
  const MAX_WAITERS_PER_ACCOUNT = 4;
20
+ const MOVE_CLAIM_MS = 2 * CLAUDE_COMPACT_KILL_MS;
19
21
 
20
22
  const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
21
23
  const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
@@ -143,6 +145,7 @@ export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRes
143
145
  continue;
144
146
  }
145
147
  log("decide.swap", { account: best.id.slice(0, 8), enforced: enforced2 != null });
148
+ if (waiterId != null && p.seats === "shared") replaceWaitClaim({ sessionId: waiterId, accountId: best.id, at: now, waitUntil: now + MOVE_CLAIM_MS });
146
149
  return { swapped: true, account: best, reason: "swapped" };
147
150
  }
148
151