tokenmaxxing 1.161.0 → 1.163.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 +1 -1
- package/package.json +1 -1
- package/src/entries/supervisor.ts +112 -3
- package/src/lib/picker.ts +13 -2
package/DESIGN.md
CHANGED
|
@@ -66,7 +66,7 @@ The supervisor sees `respawn/<sid>`, SIGTERMs its child at the already-committed
|
|
|
66
66
|
|
|
67
67
|
### 3.4 Placement (supervisor, under the pool lock)
|
|
68
68
|
1. Fold every account's tee into the index.
|
|
69
|
-
2. Rank the usable accounts (§5) with the live session counts from `live/`, or the soonest-recovering account when none is usable; with no pooled account or every account needing re-auth, launch without a store (still supervised, on whatever login the environment names). Print mode, every subcommand, the bare resume picker, an invalid `--session-id`, `--fork-session` with a resume, and `--no-session-persistence` (no transcript, so a move could not resume the run) are passed through unmanaged.
|
|
69
|
+
2. Rank the usable accounts (§5) with the live session counts from `live/`, or the soonest-recovering account when none is usable; with no pooled account or every account needing re-auth, launch without a store (still supervised, on whatever login the environment names). Print mode, every subcommand, the bare resume picker, an invalid `--session-id`, `--fork-session` with a resume, and `--no-session-persistence` (no transcript, so a move could not resume the run) are passed through unmanaged. One exception: an invocation that carries T3 Code's capability probe shape (the stream-json flag set with `--strict-mcp-config`, `--setting-sources=user,project,local`, `--settings {"disableAllHooks":true}`, `--no-session-persistence`, and the probe environment `ENABLE_CLAUDEAI_MCP_SERVERS=false`, `CLAUDE_CODE_AUTO_CONNECT_IDE=0`, and `CLAUDE_CODE_IDE_SKIP_AUTO_INSTALL=1`) is placed on the account placement picks, or the soonest-recovering account when none is usable, and its `get_usage` control request is answered from that account's sampled windows instead of reaching the child, so the probe sends no usage read. It writes no presence file and no session flags, and no other `--no-session-persistence` run is seated.
|
|
70
70
|
3. Spawn claude with the store env set, then write `live/<sid>` with the child's pid and start time. The lock covers the pick and the presence write, so concurrent launches see each other.
|
|
71
71
|
|
|
72
72
|
### 3.5 Multiple concurrent sessions
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.163.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",
|
|
@@ -6,11 +6,11 @@ import { z } from "zod";
|
|
|
6
6
|
import { claudePool, paths, storeDirFor } from "../lib/paths.ts";
|
|
7
7
|
import { CLAUDE_BIN, UNMANAGED_ENV, WRAP_DEPTH_ENV, resolveRealBin, wrapDepth } from "../lib/claudebin.ts";
|
|
8
8
|
import { apiKeyEnv, clearSessionCost, foldCompactionCost, foldSessionCost, loadApiKeys, readApiKey, sendApiKey, usableApiKey, writeSessionCost } from "../lib/apikeys.ts";
|
|
9
|
-
import { claude, pickLaunch, type LaunchTarget } from "../lib/claude.ts";
|
|
9
|
+
import { claude, pickLaunch, pickSeat, type LaunchTarget } from "../lib/claude.ts";
|
|
10
10
|
import { compactClaudeSession } from "../lib/compact.ts";
|
|
11
11
|
import { evaluateAndMaybeSwap, moveTarget } from "../lib/decide.ts";
|
|
12
12
|
import { withLock } from "../lib/lock.ts";
|
|
13
|
-
import { thresholdBars, usableAt } from "../lib/picker.ts";
|
|
13
|
+
import { limitWindows, liveUsed, sessionWindow, thresholdBars, usableAt, weeklyWindow } from "../lib/picker.ts";
|
|
14
14
|
import { clearPresence } from "../lib/presence.ts";
|
|
15
15
|
import { readLines } from "../lib/proc.ts";
|
|
16
16
|
import { teeObservation } from "../lib/sample.ts";
|
|
@@ -19,7 +19,7 @@ import { saveTermios } from "../lib/tty.ts";
|
|
|
19
19
|
import { liveSessionId, loadSessionFlags, pruneStaleSessions, saveSessionFlags, writeRespawnMarker } from "../lib/sessions.ts";
|
|
20
20
|
import { loadAccounts, loadConfig, readJsonFile, releaseWaitClaim, replaceWaitClaim, writeUsage } from "../lib/state.ts";
|
|
21
21
|
import { gatedFamilies, modelFromFlag, parseStreamLine, scrubCredEnv } from "../lib/usage.ts";
|
|
22
|
-
import { CostLineSchema, CostStateRowSchema, JsonTextSchema, RespawnMarkerSchema, type Account, type ApiKey, type Config, type ModelInfo, type SessionCost, type UsageWindows } from "../lib/types.ts";
|
|
22
|
+
import { CostLineSchema, CostStateRowSchema, JsonTextSchema, RespawnMarkerSchema, type Account, type ApiKey, type Config, type ModelInfo, type SessionCost, type UsageWindows, type Window } from "../lib/types.ts";
|
|
23
23
|
import { errorMessage, log } from "../lib/log.ts";
|
|
24
24
|
|
|
25
25
|
const SUBCOMMANDS = new Set([
|
|
@@ -128,6 +128,37 @@ export function analyzeArgs(argv: string[]): Analysis {
|
|
|
128
128
|
return { manage, sessionId, resumeId, continueLatest, streamInput, streamOutput, model };
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
const PROBE_VALUE_FLAGS = new Set(["--output-format", "--input-format", "--setting-sources", "--settings", "--permission-mode", "--debug-file"]);
|
|
132
|
+
const PROBE_BOOLEAN_FLAGS = new Set(["--verbose", "--strict-mcp-config", "--no-session-persistence"]);
|
|
133
|
+
const ProbeSettingsSchema = z.strictObject({ disableAllHooks: z.literal(true) });
|
|
134
|
+
|
|
135
|
+
export function isCapabilitiesProbe(argv: string[], envLike: Record<string, string | undefined>): boolean {
|
|
136
|
+
if (envLike.ENABLE_CLAUDEAI_MCP_SERVERS !== "false" || envLike.CLAUDE_CODE_AUTO_CONNECT_IDE !== "0" || envLike.CLAUDE_CODE_IDE_SKIP_AUTO_INSTALL !== "1") return false;
|
|
137
|
+
const values = new Map<string, string>();
|
|
138
|
+
const booleans = new Set<string>();
|
|
139
|
+
for (let i = 0; i < argv.length; i++) {
|
|
140
|
+
const a = argv[i]!;
|
|
141
|
+
const eq = a.indexOf("=");
|
|
142
|
+
const flag = eq > 0 ? a.slice(0, eq) : a;
|
|
143
|
+
if (PROBE_VALUE_FLAGS.has(flag)) {
|
|
144
|
+
if (values.has(flag)) return false;
|
|
145
|
+
const value = eq > 0 ? a.slice(eq + 1) : argv[++i];
|
|
146
|
+
if (value === undefined) return false;
|
|
147
|
+
values.set(flag, value);
|
|
148
|
+
} else if (eq === -1 && PROBE_BOOLEAN_FLAGS.has(a)) {
|
|
149
|
+
if (booleans.has(a)) return false;
|
|
150
|
+
booleans.add(a);
|
|
151
|
+
} else return false;
|
|
152
|
+
}
|
|
153
|
+
if (!booleans.has("--verbose") || !booleans.has("--strict-mcp-config") || !booleans.has("--no-session-persistence")) return false;
|
|
154
|
+
if (values.get("--output-format") !== "stream-json" || values.get("--input-format") !== "stream-json") return false;
|
|
155
|
+
if (values.get("--setting-sources") !== "user,project,local") return false;
|
|
156
|
+
const permissionMode = values.get("--permission-mode");
|
|
157
|
+
if (permissionMode !== undefined && permissionMode !== "default") return false;
|
|
158
|
+
const settings = values.get("--settings");
|
|
159
|
+
return settings !== undefined && ProbeSettingsSchema.safeParse(JsonTextSchema.safeParse(settings).data).success;
|
|
160
|
+
}
|
|
161
|
+
|
|
131
162
|
export function stripSessionFlags(argv: string[]): string[] {
|
|
132
163
|
const out: string[] = [];
|
|
133
164
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -478,6 +509,74 @@ class StdinRelay {
|
|
|
478
509
|
}
|
|
479
510
|
}
|
|
480
511
|
|
|
512
|
+
const ProbeCancelLineSchema = z.looseObject({ type: z.literal("control_cancel_request") });
|
|
513
|
+
|
|
514
|
+
function probeUsageLine(requestId: string, account: Account): string {
|
|
515
|
+
const now = Date.now();
|
|
516
|
+
const observed = teeObservation(account);
|
|
517
|
+
const current = observed ? { ...account, windows: observed.windows } : account;
|
|
518
|
+
const win = (w: Window | undefined) => (w == null ? null : { utilization: liveUsed(w, now), resets_at: w.resetsAt != null && w.resetsAt > now ? new Date(w.resetsAt).toISOString() : null });
|
|
519
|
+
const fiveHour = win(sessionWindow(current));
|
|
520
|
+
const sevenDay = win(weeklyWindow(current));
|
|
521
|
+
const scoped = limitWindows(current).map((w) => ({ display_name: w.name, utilization: liveUsed(w, now), resets_at: w.resetsAt != null && w.resetsAt > now ? new Date(w.resetsAt).toISOString() : null }));
|
|
522
|
+
const available = fiveHour != null || sevenDay != null;
|
|
523
|
+
return `${JSON.stringify({
|
|
524
|
+
type: "control_response",
|
|
525
|
+
response: {
|
|
526
|
+
subtype: "success",
|
|
527
|
+
request_id: requestId,
|
|
528
|
+
response: {
|
|
529
|
+
session: { total_cost_usd: 0, total_api_duration_ms: 0, total_duration_ms: 0, total_lines_added: 0, total_lines_removed: 0, model_usage: {} },
|
|
530
|
+
subscription_type: null,
|
|
531
|
+
rate_limits_available: available,
|
|
532
|
+
rate_limits: available ? { five_hour: fiveHour, seven_day: sevenDay, ...(scoped.length > 0 ? { model_scoped: scoped } : {}) } : null,
|
|
533
|
+
behaviors: null,
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
})}\n`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async function runCapabilitiesProbe(input: { real: string; argv: string[]; env: Record<string, string | undefined>; seat: Account; onSpawn: (child: Subprocess) => void }): Promise<number> {
|
|
540
|
+
const proc = Bun.spawn([input.real, ...input.argv], { stdin: "pipe", stdout: "pipe", stderr: "inherit", env: input.env });
|
|
541
|
+
input.onSpawn(proc);
|
|
542
|
+
log("supervisor.probe_launch", { seat: input.seat.id.slice(0, 8) });
|
|
543
|
+
const stdoutPump = (async () => {
|
|
544
|
+
for await (const line of readLines(proc.stdout as ReadableStream<Uint8Array>)) process.stdout.write(`${line}\n`);
|
|
545
|
+
})().catch((e: unknown) => log("supervisor.probe_stdout_failed", { err: errorMessage(e) }));
|
|
546
|
+
void (async () => {
|
|
547
|
+
for await (const line of readLines(Bun.stdin.stream())) {
|
|
548
|
+
const parsed = RelayLineSchema.safeParse(JsonTextSchema.safeParse(line).data);
|
|
549
|
+
const request = parsed.success && parsed.data.type === "control_request" ? parsed.data : null;
|
|
550
|
+
if (request?.request.subtype === "get_usage") {
|
|
551
|
+
process.stdout.write(probeUsageLine(request.request_id, input.seat));
|
|
552
|
+
log("supervisor.probe_usage", { seat: input.seat.id.slice(0, 8) });
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const forwarded = (parsed.success && parsed.data.type === "control_response") || request?.request.subtype === "initialize" || ProbeCancelLineSchema.safeParse(JsonTextSchema.safeParse(line).data).success;
|
|
556
|
+
if (!forwarded) {
|
|
557
|
+
log("supervisor.probe_refused", { seat: input.seat.id.slice(0, 8), kind: parsed.success ? parsed.data.type : "unparsed" });
|
|
558
|
+
proc.kill("SIGKILL");
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
proc.stdin.write(`${line}\n`);
|
|
563
|
+
proc.stdin.flush();
|
|
564
|
+
} catch (e) {
|
|
565
|
+
log("supervisor.probe_stdin_failed", { err: errorMessage(e) });
|
|
566
|
+
break;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
proc.stdin.end();
|
|
571
|
+
} catch (e) {
|
|
572
|
+
log("supervisor.probe_stdin_failed", { err: errorMessage(e) });
|
|
573
|
+
}
|
|
574
|
+
})().catch((e: unknown) => log("supervisor.probe_stdin_failed", { err: errorMessage(e) }));
|
|
575
|
+
await proc.exited;
|
|
576
|
+
await Promise.race([stdoutPump, Bun.sleep(1000)]);
|
|
577
|
+
return exitStatus(proc);
|
|
578
|
+
}
|
|
579
|
+
|
|
481
580
|
function bareResumeStream(info: Analysis, argv: string[], sid: string | null): boolean {
|
|
482
581
|
if (!info.manage || process.env[UNMANAGED_ENV] || info.sessionId != null || stripSessionFlags(argv).length !== 0 || sid == null) return false;
|
|
483
582
|
const persisted = loadSessionFlags(sid);
|
|
@@ -532,6 +631,16 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
532
631
|
|
|
533
632
|
if (!info.manage || process.env[UNMANAGED_ENV]) {
|
|
534
633
|
const passthroughEnv = omit(childEnv, ["TOKENMAXXING_SUPERVISED", "TOKENMAXXING_SESSION_ID", "TOKENMAXXING_MODEL", "TOKENMAXXING_REFUSED", "TOKENMAXXING_API_KEY_ID"]);
|
|
634
|
+
if (!process.env[UNMANAGED_ENV] && !process.env.TOKENMAXXING_PROBE && isCapabilitiesProbe(argv, process.env)) {
|
|
635
|
+
const seat = await withLock(claudePool.lockFile, () => pickSeat(Date.now(), null)?.account ?? null).catch((e: unknown) => {
|
|
636
|
+
log("supervisor.probe_pick_failed", { err: errorMessage(e) });
|
|
637
|
+
return null;
|
|
638
|
+
});
|
|
639
|
+
if (seat != null) {
|
|
640
|
+
const probeEnv = { ...scrubCredEnv(passthroughEnv), CLAUDE_SECURESTORAGE_CONFIG_DIR: storeDirFor(seat.id) };
|
|
641
|
+
return runCapabilitiesProbe({ real, argv, env: probeEnv, seat, onSpawn: (p) => { child = p; } });
|
|
642
|
+
}
|
|
643
|
+
}
|
|
535
644
|
return runPassthrough({ real, argv, env: passthroughEnv, onSpawn: (p) => { child = p; } });
|
|
536
645
|
}
|
|
537
646
|
|
package/src/lib/picker.ts
CHANGED
|
@@ -164,6 +164,15 @@ export function seatHeadroom(a: Account, ctx: PickCtx): number {
|
|
|
164
164
|
return (barFor(a, session, ctx.thresholds, ctx.now) - liveUsed(session, ctx.now)) / ((ctx.seats?.get(a.id) ?? 0) + 1);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
const MIN_SEAT_HEADROOM = 5;
|
|
168
|
+
|
|
169
|
+
function seatFull(a: Account, ctx: PickCtx): boolean {
|
|
170
|
+
const session = sessionWindow(a);
|
|
171
|
+
if (ctx.seats == null || session == null || spendsCredits(a, ctx.thresholds)) return false;
|
|
172
|
+
const fresh = barFor(a, session, ctx.thresholds, windowEnd(session)) / ((ctx.seats.get(a.id) ?? 0) + 1);
|
|
173
|
+
return seatHeadroom(a, ctx) < MIN_SEAT_HEADROOM && fresh >= MIN_SEAT_HEADROOM;
|
|
174
|
+
}
|
|
175
|
+
|
|
167
176
|
export function onCredits(a: Account, ctx: PickCtx): boolean {
|
|
168
177
|
return spendsCredits(a, ctx.thresholds) && isExhausted({ ...a, hasCredits: false }, ctx);
|
|
169
178
|
}
|
|
@@ -178,13 +187,15 @@ const swapPreference = (ctx: PickCtx) => [
|
|
|
178
187
|
];
|
|
179
188
|
|
|
180
189
|
export function pickBest(accounts: Account[], ctx: PickCtx): Account | null {
|
|
181
|
-
const usable = accounts.filter((a) => a.id !== ctx.currentId && !a.needsReauth && !isExhausted(a, ctx));
|
|
190
|
+
const usable = accounts.filter((a) => a.id !== ctx.currentId && !a.needsReauth && !isExhausted(a, ctx) && !seatFull(a, ctx));
|
|
182
191
|
return sortBy(usable, swapPreference(ctx))[0] ?? null;
|
|
183
192
|
}
|
|
184
193
|
|
|
185
194
|
export function usableAt(a: Account, ctx: PickCtx): number {
|
|
186
195
|
const blocking = blockingUntil(a, ctx).filter((t) => t > ctx.now);
|
|
187
|
-
|
|
196
|
+
const at = blocking.length ? Math.max(...blocking) : ctx.now;
|
|
197
|
+
const session = sessionWindow(a);
|
|
198
|
+
return session != null && seatFull(a, { ...ctx, now: at }) ? windowEnd(session) : at;
|
|
188
199
|
}
|
|
189
200
|
|
|
190
201
|
export type EarliestReset = { account: Account; availableAt: number };
|