tokenmaxxing 1.82.0 → 1.83.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 +3 -2
- package/README.md +16 -1
- package/package.json +1 -1
- package/src/cli/auth.ts +61 -13
- package/src/cli/doctor.ts +42 -3
- package/src/cli/init.ts +22 -1
- package/src/entries/pisupervisor.ts +442 -0
- package/src/entries/supervisor.ts +1 -31
- package/src/lib/claude.ts +6 -3
- package/src/lib/claudebin.ts +9 -0
- package/src/lib/codex.ts +12 -8
- package/src/lib/codexauth.ts +8 -0
- package/src/lib/install.ts +19 -6
- package/src/lib/paths.ts +21 -1
- package/src/lib/pi.ts +132 -0
- package/src/lib/piauth.ts +85 -0
- package/src/lib/sessions.ts +2 -2
- package/src/lib/state.ts +2 -1
- package/src/lib/supervise.ts +32 -1
- package/src/lib/types.ts +1 -0
- package/src/main.ts +23 -5
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { closeSync, openSync, readdirSync, readFileSync, readSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import type { Subprocess } from "bun";
|
|
4
|
+
import { sortBy, uniq } from "es-toolkit";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { PI_BIN, UNMANAGED_ENV, WRAP_DEPTH_ENV, resolveRealBin, wrapDepth } from "../lib/claudebin.ts";
|
|
7
|
+
import { observeCodex } from "../lib/codex.ts";
|
|
8
|
+
import { evaluateAndMaybeSwap, type SwapDecision } from "../lib/decide.ts";
|
|
9
|
+
import { withLock } from "../lib/lock.ts";
|
|
10
|
+
import { errorMessage, log } from "../lib/log.ts";
|
|
11
|
+
import { claudePool, codexPaths, expandTilde, optionalEnv, paths, piPaths, type PiPool } from "../lib/paths.ts";
|
|
12
|
+
import { pickPiSeat, piMovers } from "../lib/pi.ts";
|
|
13
|
+
import { ensurePiStoreHome, piStoreUsable } from "../lib/piauth.ts";
|
|
14
|
+
import { thresholdBars, usableAt } from "../lib/picker.ts";
|
|
15
|
+
import { clearPresence, livingPresences } from "../lib/presence.ts";
|
|
16
|
+
import { teeObservation } from "../lib/sample.ts";
|
|
17
|
+
import { countdownWait, exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough, SEAT_POLL_MS, SEAT_RETRY_MS } from "../lib/supervise.ts";
|
|
18
|
+
import { saveTermios } from "../lib/tty.ts";
|
|
19
|
+
import { loadAccounts, loadConfig, releaseWaitClaim } from "../lib/state.ts";
|
|
20
|
+
import { gatedFamilies, modelFromFlag } from "../lib/usage.ts";
|
|
21
|
+
import { ErrnoSchema, JsonTextSchema, type Account, type ModelInfo } from "../lib/types.ts";
|
|
22
|
+
|
|
23
|
+
const SUBCOMMANDS = new Set(["install", "remove", "uninstall", "update", "list", "config", "auth"]);
|
|
24
|
+
|
|
25
|
+
const UNMANAGED_FLAGS = new Set(["-h", "--help", "-v", "--version", "-p", "--print", "-r", "--resume", "--no-session", "--api-key", "--export", "--list-models"]);
|
|
26
|
+
|
|
27
|
+
const VALUE_FLAGS = new Set([
|
|
28
|
+
"--provider", "--model", "--system-prompt", "--append-system-prompt", "-n", "--name", "--session-dir",
|
|
29
|
+
"--models", "-t", "--tools", "-xt", "--exclude-tools", "--thinking", "-e", "--extension", "--skill",
|
|
30
|
+
"--prompt-template", "--theme", "--use-theme", "--mode", "--tui-mode",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const BOOLEAN_FLAGS = new Set([
|
|
34
|
+
"-nt", "--no-tools", "-nbt", "--no-builtin-tools", "-ne", "--no-extensions", "-ns", "--no-skills",
|
|
35
|
+
"-np", "--no-prompt-templates", "--no-themes", "-nc", "--no-context-files", "--verbose",
|
|
36
|
+
"-a", "--approve", "-na", "--no-approve", "--offline",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
type PiArgs = {
|
|
40
|
+
provider: string | null;
|
|
41
|
+
model: string | null;
|
|
42
|
+
sessionDir: string | null;
|
|
43
|
+
sessionId: string | null;
|
|
44
|
+
session: string | null;
|
|
45
|
+
fork: boolean;
|
|
46
|
+
continueLatest: boolean;
|
|
47
|
+
approve: boolean | null;
|
|
48
|
+
rest: string[];
|
|
49
|
+
flags: string[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function analyzePiArgs(argv: string[]): PiArgs | null {
|
|
53
|
+
if (argv[0] !== undefined && SUBCOMMANDS.has(argv[0])) return null;
|
|
54
|
+
const out: PiArgs = { provider: null, model: null, sessionDir: null, sessionId: null, session: null, fork: false, continueLatest: false, approve: null, rest: [], flags: [] };
|
|
55
|
+
for (let i = 0; i < argv.length; i++) {
|
|
56
|
+
const a = argv[i]!;
|
|
57
|
+
if (a === "--") {
|
|
58
|
+
out.rest.push(...argv.slice(i));
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
if (UNMANAGED_FLAGS.has(a)) return null;
|
|
62
|
+
if (a === "-c" || a === "--continue") {
|
|
63
|
+
out.continueLatest = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (a === "--session-id" || a === "--session") {
|
|
67
|
+
const value = argv[++i];
|
|
68
|
+
if (value === undefined) return null;
|
|
69
|
+
if (a === "--session-id") out.sessionId = value;
|
|
70
|
+
else out.session = value;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
out.rest.push(a);
|
|
74
|
+
if (a === "--fork") {
|
|
75
|
+
const value = argv[++i];
|
|
76
|
+
if (value === undefined) return null;
|
|
77
|
+
out.rest.push(value);
|
|
78
|
+
out.fork = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (VALUE_FLAGS.has(a)) {
|
|
82
|
+
const value = argv[i + 1];
|
|
83
|
+
if (value === undefined) return null;
|
|
84
|
+
i++;
|
|
85
|
+
out.rest.push(value);
|
|
86
|
+
out.flags.push(a, value);
|
|
87
|
+
if (a === "--mode" && value !== "text") return null;
|
|
88
|
+
if (a === "--provider") out.provider = value;
|
|
89
|
+
else if (a === "--model") out.model = value;
|
|
90
|
+
else if (a === "--session-dir") out.sessionDir = value;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (BOOLEAN_FLAGS.has(a)) {
|
|
94
|
+
out.flags.push(a);
|
|
95
|
+
if (a === "-a" || a === "--approve") out.approve = true;
|
|
96
|
+
else if (a === "-na" || a === "--no-approve") out.approve = false;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (a.startsWith("--")) {
|
|
100
|
+
const next = argv[i + 1];
|
|
101
|
+
if (!a.includes("=") && next !== undefined && !next.startsWith("-") && !next.startsWith("@")) {
|
|
102
|
+
i++;
|
|
103
|
+
out.rest.push(next);
|
|
104
|
+
out.flags.push(a, next);
|
|
105
|
+
} else out.flags.push(a);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (a.startsWith("-")) return null;
|
|
109
|
+
}
|
|
110
|
+
const selectors = [out.session != null, out.continueLatest, out.sessionId != null || out.fork].filter(Boolean).length;
|
|
111
|
+
return selectors > 1 ? null : out;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const PiSettingsSchema = z.looseObject({
|
|
115
|
+
defaultProvider: z.string().optional().catch(undefined),
|
|
116
|
+
sessionDir: z.string().optional().catch(undefined),
|
|
117
|
+
defaultProjectTrust: z.string().optional().catch(undefined),
|
|
118
|
+
});
|
|
119
|
+
type PiSettings = z.infer<typeof PiSettingsSchema>;
|
|
120
|
+
type PiSettingsScopes = { global: PiSettings; project: PiSettings; projectTrusted: boolean };
|
|
121
|
+
|
|
122
|
+
const PiTrustSchema = z.record(z.string(), z.unknown());
|
|
123
|
+
|
|
124
|
+
function readPiJson<T extends z.ZodType>(file: string, schema: T): z.output<T> | null {
|
|
125
|
+
let text: string;
|
|
126
|
+
try {
|
|
127
|
+
text = readFileSync(file, "utf8");
|
|
128
|
+
} catch (e) {
|
|
129
|
+
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return null;
|
|
130
|
+
throw e;
|
|
131
|
+
}
|
|
132
|
+
const parsed = schema.safeParse(JsonTextSchema.safeParse(text.startsWith("") ? text.slice(1) : text).data);
|
|
133
|
+
if (!parsed.success) log("pisupervisor.pi_file_unreadable", { file });
|
|
134
|
+
return parsed.success ? parsed.data : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function projectTrusted(args: PiArgs, global: PiSettings): boolean {
|
|
138
|
+
if (args.approve != null) return args.approve;
|
|
139
|
+
const trust = readPiJson(join(piPaths.home, "trust.json"), PiTrustSchema) ?? {};
|
|
140
|
+
let dir = realpathSync(process.cwd());
|
|
141
|
+
while (true) {
|
|
142
|
+
const decision = trust[dir];
|
|
143
|
+
if (decision === true || decision === false) return decision;
|
|
144
|
+
const parent = dirname(dir);
|
|
145
|
+
if (parent === dir) return global.defaultProjectTrust === "always";
|
|
146
|
+
dir = parent;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function piSettings(args: PiArgs): PiSettingsScopes {
|
|
151
|
+
const global = readPiJson(join(piPaths.home, "settings.json"), PiSettingsSchema) ?? {};
|
|
152
|
+
return {
|
|
153
|
+
global,
|
|
154
|
+
project: readPiJson(join(process.cwd(), ".pi", "settings.json"), PiSettingsSchema) ?? {},
|
|
155
|
+
projectTrusted: projectTrusted(args, global),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function poolOf(args: PiArgs, settings: PiSettingsScopes): PiPool | null {
|
|
160
|
+
const slash = args.model?.indexOf("/") ?? -1;
|
|
161
|
+
const defaultProvider = (settings.projectTrusted ? settings.project.defaultProvider : undefined) ?? settings.global.defaultProvider;
|
|
162
|
+
const modelProvider = args.model == null ? defaultProvider : slash > 0 ? args.model.slice(0, slash) : undefined;
|
|
163
|
+
const provider = (args.provider ?? modelProvider)?.toLowerCase();
|
|
164
|
+
return provider === "anthropic" ? "claude" : provider === "openai-codex" ? "codex" : null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function sessionModel(args: PiArgs): ModelInfo | null {
|
|
168
|
+
const id = args.model?.split(":")[0] ?? null;
|
|
169
|
+
return modelFromFlag(id == null ? null : id.slice(id.indexOf("/") + 1));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function sessionDirs(args: PiArgs, settings: PiSettingsScopes): string[] {
|
|
173
|
+
const custom = [args.sessionDir, optionalEnv("PI_CODING_AGENT_SESSION_DIR"), settings.project.sessionDir, settings.global.sessionDir]
|
|
174
|
+
.filter((d): d is string => d != null && d !== "")
|
|
175
|
+
.map((d) => resolve(expandTilde(d)));
|
|
176
|
+
const cwd = process.cwd();
|
|
177
|
+
const trimmed = cwd.startsWith("/") || cwd.startsWith("\\") ? cwd.slice(1) : cwd;
|
|
178
|
+
return uniq([...custom, join(piPaths.home, "sessions", `--${trimmed.replaceAll("/", "-").replaceAll("\\", "-").replaceAll(":", "-")}--`)]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const SessionHeaderSchema = z.looseObject({ type: z.literal("session"), id: z.string(), cwd: z.string() });
|
|
182
|
+
|
|
183
|
+
function sessionHeader(path: string): z.infer<typeof SessionHeaderSchema> | null {
|
|
184
|
+
let fd: number;
|
|
185
|
+
try {
|
|
186
|
+
fd = openSync(path, "r");
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const buf = Buffer.alloc(16 * 1024);
|
|
192
|
+
const text = buf.subarray(0, readSync(fd, buf, 0, buf.length, 0)).toString("utf8");
|
|
193
|
+
const end = text.indexOf("\n");
|
|
194
|
+
return SessionHeaderSchema.safeParse(JsonTextSchema.safeParse(end === -1 ? text : text.slice(0, end)).data).data ?? null;
|
|
195
|
+
} finally {
|
|
196
|
+
closeSync(fd);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function sessionFiles(dir: string): string[] {
|
|
201
|
+
try {
|
|
202
|
+
return readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
203
|
+
} catch {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function localSessions(dir: string): string[] {
|
|
209
|
+
const cwd = resolve(process.cwd());
|
|
210
|
+
const dated = sessionFiles(dir).flatMap((f) => {
|
|
211
|
+
try {
|
|
212
|
+
return [{ path: join(dir, f), mtime: statSync(join(dir, f)).mtimeMs }];
|
|
213
|
+
} catch {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
return sortBy(dated, [(x) => -x.mtime]).flatMap(({ path }) => {
|
|
218
|
+
const header = sessionHeader(path);
|
|
219
|
+
return header != null && resolve(header.cwd) === cwd ? [header.id] : [];
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function sessionIdFor(args: PiArgs, dir: string): string | null {
|
|
224
|
+
if (args.sessionId != null) return args.sessionId;
|
|
225
|
+
if (args.continueLatest) return localSessions(dir)[0] ?? crypto.randomUUID();
|
|
226
|
+
if (args.session == null) return crypto.randomUUID();
|
|
227
|
+
const arg = args.session;
|
|
228
|
+
if (arg.includes("/") || arg.includes("\\") || arg.endsWith(".jsonl")) return null;
|
|
229
|
+
const ids = localSessions(dir);
|
|
230
|
+
if (ids.includes(arg)) return arg;
|
|
231
|
+
const matches = ids.filter((s) => s.startsWith(arg));
|
|
232
|
+
return matches.length === 1 ? matches[0]! : null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function sessionExists(dirs: string[], sid: string): boolean {
|
|
236
|
+
return dirs.some((dir) => sessionFiles(dir).some((f) => f.endsWith(`_${sid}.jsonl`)));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
type Launch = { args: PiArgs; pool: PiPool; dirs: string[]; sid: string };
|
|
240
|
+
|
|
241
|
+
function managedLaunch(argv: string[]): Launch | null {
|
|
242
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true || process.env.TOKENMAXXING_PROBE || process.env[UNMANAGED_ENV]) return null;
|
|
243
|
+
const args = analyzePiArgs(argv);
|
|
244
|
+
if (args == null) return null;
|
|
245
|
+
const settings = piSettings(args);
|
|
246
|
+
const pool = poolOf(args, settings);
|
|
247
|
+
if (pool == null) {
|
|
248
|
+
log("pisupervisor.unpooled", {
|
|
249
|
+
provider: args.provider,
|
|
250
|
+
model: args.model,
|
|
251
|
+
projectTrusted: settings.projectTrusted,
|
|
252
|
+
defaultProvider: (settings.projectTrusted ? settings.project.defaultProvider : undefined) ?? settings.global.defaultProvider,
|
|
253
|
+
});
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
const dirs = sessionDirs(args, settings);
|
|
257
|
+
const sid = sessionIdFor(args, dirs[0]!);
|
|
258
|
+
if (sid == null) {
|
|
259
|
+
log("pisupervisor.session_unresolved", { session: args.session });
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
return { args, pool, dirs, sid };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const RESUME_PROMPT =
|
|
266
|
+
"tokenmaxxing resumed this session on an account with quota headroom. Continue the task from where the previous turn left off. If the previous turn ended waiting on the user, restate what you need and wait. The move restarted pi, which ended every command this session was still running. Relaunch what the task still needs.";
|
|
267
|
+
|
|
268
|
+
function validWanted(pool: PiPool, wanted: string | null, id: string): Account | null {
|
|
269
|
+
if (wanted == null) return null;
|
|
270
|
+
const a = loadAccounts(piMovers[pool].pool).accounts.find((x) => x.id === wanted);
|
|
271
|
+
if (!a || a.needsReauth === true || !piStoreUsable(pool, a.id)) return null;
|
|
272
|
+
if (pool === "codex" && livingPresences(codexPaths.presenceDir).some((p) => p.accountId === a.id && p.id !== id)) return null;
|
|
273
|
+
return a;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
type Watch = { decided: boolean; move: SwapDecision | null };
|
|
277
|
+
|
|
278
|
+
async function watchSeat(input: { pool: PiPool; seatId: string; id: string; families: string[] | null; overriddenUntil: number }): Promise<Watch> {
|
|
279
|
+
try {
|
|
280
|
+
const now = Date.now();
|
|
281
|
+
const cfg = loadConfig();
|
|
282
|
+
const mover = piMovers[input.pool];
|
|
283
|
+
const account = loadAccounts(mover.pool).accounts.find((a) => a.id === input.seatId);
|
|
284
|
+
if (!account) return { decided: false, move: null };
|
|
285
|
+
const observed = input.pool === "claude" ? teeObservation(account) : await observeCodex(account, cfg, now, { probe: true, refresh: true, holder: input.id });
|
|
286
|
+
const unmeasured = input.pool === "codex" && (observed == null || now - observed.at > cfg.policy.usagePollTtlMs);
|
|
287
|
+
if (unmeasured) log("pisupervisor.seat_unmeasured", { seat: account.id.slice(0, 8), usageAt: observed?.at });
|
|
288
|
+
const current = observed ? { ...account, windows: observed.windows } : account;
|
|
289
|
+
const until = usableAt(current, { now, thresholds: thresholdBars(cfg), currentId: account.id, families: input.families, seats: null });
|
|
290
|
+
if (until <= now || until <= input.overriddenUntil) return { decided: unmeasured, move: null };
|
|
291
|
+
const decision = await evaluateAndMaybeSwap(mover, now, true, null, { seatId: account.id, ...(input.families ? { sessionFamilies: input.families } : {}), waiterId: input.id });
|
|
292
|
+
log("pisupervisor.seat_exhausted", { pool: input.pool, seat: account.id.slice(0, 8), until, reason: decision.reason, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
|
|
293
|
+
return { decided: true, move: decision.account && (decision.swapped || decision.waitUntil !== undefined) ? decision : null };
|
|
294
|
+
} catch (e) {
|
|
295
|
+
log("pisupervisor.seat_watch_error", { err: errorMessage(e) });
|
|
296
|
+
return { decided: true, move: null };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function runPiSupervisor(argv: string[]): Promise<number> {
|
|
301
|
+
if (loopGuardTripped("pi")) return 1;
|
|
302
|
+
let real: string;
|
|
303
|
+
try {
|
|
304
|
+
real = resolveRealBin(PI_BIN);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
log("pisupervisor.resolve_failed", { err: errorMessage(e) });
|
|
307
|
+
throw e;
|
|
308
|
+
}
|
|
309
|
+
const childEnv = { ...process.env, [WRAP_DEPTH_ENV]: String(wrapDepth() + 1) };
|
|
310
|
+
|
|
311
|
+
let child: Subprocess | null = null;
|
|
312
|
+
let terminating = false;
|
|
313
|
+
let cleanup = async (): Promise<void> => {};
|
|
314
|
+
process.on("SIGTERM", () => {
|
|
315
|
+
terminating = true;
|
|
316
|
+
if (child) {
|
|
317
|
+
child.kill("SIGTERM");
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
cleanup().finally(() => {
|
|
321
|
+
if (child) child.kill("SIGTERM");
|
|
322
|
+
else process.exit(143);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const launch = managedLaunch(argv);
|
|
327
|
+
if (launch == null) return runPassthrough({ real, argv, env: childEnv, onSpawn: (p) => { child = p; } });
|
|
328
|
+
const { args, pool, dirs, sid } = launch;
|
|
329
|
+
|
|
330
|
+
const id = `pi-${crypto.randomUUID()}`;
|
|
331
|
+
const mover = piMovers[pool];
|
|
332
|
+
const presenceDir = pool === "claude" ? paths.presenceDir : codexPaths.presenceDir;
|
|
333
|
+
const releaseClaim = async (): Promise<void> => {
|
|
334
|
+
if (pool !== "claude") return;
|
|
335
|
+
try {
|
|
336
|
+
await withLock(claudePool.lockFile, () => {
|
|
337
|
+
releaseWaitClaim(id);
|
|
338
|
+
});
|
|
339
|
+
} catch (e) {
|
|
340
|
+
log("pisupervisor.claim_release_failed", { err: errorMessage(e) });
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
cleanup = async () => {
|
|
344
|
+
clearPresence({ dir: presenceDir, id });
|
|
345
|
+
await releaseClaim();
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const model = sessionModel(args);
|
|
349
|
+
const families = pool === "claude" ? gatedFamilies(model, loadConfig().policy.switchModels) : null;
|
|
350
|
+
const firstArgs = ["--session-id", sid, ...args.rest];
|
|
351
|
+
let launchArgs = firstArgs;
|
|
352
|
+
|
|
353
|
+
const savedTermios = saveTermios();
|
|
354
|
+
process.on("SIGINT", () => {});
|
|
355
|
+
process.on("SIGHUP", () => {});
|
|
356
|
+
const say = (terminal: string) => {
|
|
357
|
+
process.stderr.write(terminal);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
let respawns = 0;
|
|
361
|
+
let wanted: string | null = null;
|
|
362
|
+
let overriddenUntil = 0;
|
|
363
|
+
try {
|
|
364
|
+
while (true) {
|
|
365
|
+
const launchedAt = Date.now();
|
|
366
|
+
const launched = await withLock(mover.pool.lockFile, async () => {
|
|
367
|
+
clearPresence({ dir: presenceDir, id });
|
|
368
|
+
if (terminating) return null;
|
|
369
|
+
const picked = validWanted(pool, wanted, id) ?? pickPiSeat(pool, launchedAt, model);
|
|
370
|
+
log("pisupervisor.launch", { id, pool, respawns, seat: picked?.id.slice(0, 8) ?? null, args: launchArgs.join(" ") });
|
|
371
|
+
const spawned = Bun.spawn([real, ...launchArgs], {
|
|
372
|
+
stdin: "inherit",
|
|
373
|
+
stdout: "inherit",
|
|
374
|
+
stderr: "inherit",
|
|
375
|
+
env: { ...childEnv, ...(picked ? { PI_CODING_AGENT_DIR: ensurePiStoreHome(pool, picked.id) } : {}) },
|
|
376
|
+
});
|
|
377
|
+
child = spawned;
|
|
378
|
+
if (picked) {
|
|
379
|
+
await recordPresenceOrStop({
|
|
380
|
+
child: spawned,
|
|
381
|
+
dir: presenceDir,
|
|
382
|
+
id,
|
|
383
|
+
accountId: picked.id,
|
|
384
|
+
event: "pisupervisor.presence_failed",
|
|
385
|
+
message: "could not write the pi session's presence file - refusing to run a session whose seat placement cannot see",
|
|
386
|
+
savedTermios,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return { proc: spawned, seat: picked };
|
|
390
|
+
});
|
|
391
|
+
if (launched == null) {
|
|
392
|
+
await releaseClaim();
|
|
393
|
+
return 143;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const { proc, seat } = launched;
|
|
397
|
+
const pending: { move: SwapDecision | null; killed: boolean } = { move: null, killed: false };
|
|
398
|
+
let seatCheckAt = 0;
|
|
399
|
+
await raceMarkerOrExit({
|
|
400
|
+
child: proc,
|
|
401
|
+
tick: async () => {
|
|
402
|
+
if (!seat || terminating || Date.now() < seatCheckAt) return false;
|
|
403
|
+
const watched = await watchSeat({ pool, seatId: seat.id, id, families, overriddenUntil });
|
|
404
|
+
seatCheckAt = Date.now() + (watched.decided ? SEAT_RETRY_MS : SEAT_POLL_MS);
|
|
405
|
+
pending.move = watched.move;
|
|
406
|
+
return watched.move !== null;
|
|
407
|
+
},
|
|
408
|
+
savedTermios,
|
|
409
|
+
onKill: () => {
|
|
410
|
+
pending.killed = true;
|
|
411
|
+
},
|
|
412
|
+
onError: () => clearPresence({ dir: presenceDir, id }),
|
|
413
|
+
onExited: () => {
|
|
414
|
+
child = null;
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const target = pending.killed && !terminating ? (pending.move?.account ?? null) : null;
|
|
419
|
+
if (target) {
|
|
420
|
+
respawns++;
|
|
421
|
+
const waitUntil = pending.move?.waitUntil ?? 0;
|
|
422
|
+
if (waitUntil > Date.now()) {
|
|
423
|
+
if (await countdownWait(target.label, waitUntil, { stream: false, say })) overriddenUntil = waitUntil;
|
|
424
|
+
} else say(`\n\x1b[36m↻ tokenmaxxing: moving pi to ${target.label} - resuming...\x1b[0m\n`);
|
|
425
|
+
await releaseClaim();
|
|
426
|
+
wanted = target.id;
|
|
427
|
+
launchArgs = sessionExists(dirs, sid) ? ["--session-id", sid, ...args.flags, "--", RESUME_PROMPT] : firstArgs;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
clearPresence({ dir: presenceDir, id });
|
|
431
|
+
await releaseClaim();
|
|
432
|
+
log("pisupervisor.exit", { id, respawns, code: proc.exitCode, signal: proc.signalCode, terminated: terminating || undefined });
|
|
433
|
+
return exitStatus(proc);
|
|
434
|
+
}
|
|
435
|
+
} catch (e) {
|
|
436
|
+
const msg = errorMessage(e);
|
|
437
|
+
say(`\n\x1b[31m${msg.startsWith("tokenmaxxing:") ? msg : `tokenmaxxing: ${msg}`}\x1b[0m\n`);
|
|
438
|
+
log("pisupervisor.fatal", { err: msg });
|
|
439
|
+
await releaseClaim();
|
|
440
|
+
return 1;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
@@ -13,7 +13,7 @@ import { thresholdBars, usableAt } from "../lib/picker.ts";
|
|
|
13
13
|
import { clearPresence } from "../lib/presence.ts";
|
|
14
14
|
import { readLines } from "../lib/proc.ts";
|
|
15
15
|
import { teeObservation } from "../lib/sample.ts";
|
|
16
|
-
import { exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough } from "../lib/supervise.ts";
|
|
16
|
+
import { countdownWait, exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough, SEAT_POLL_MS, SEAT_RETRY_MS, type Say } from "../lib/supervise.ts";
|
|
17
17
|
import { saveTermios } from "../lib/tty.ts";
|
|
18
18
|
import { liveSessionId, loadSessionFlags, pruneStaleSessions, saveSessionFlags, writeRespawnMarker } from "../lib/sessions.ts";
|
|
19
19
|
import { loadAccounts, loadConfig, readJsonFile, releaseWaitClaim } from "../lib/state.ts";
|
|
@@ -203,9 +203,6 @@ function consumableMarker(marker: string, gate: MarkerGate): z.infer<typeof Resp
|
|
|
203
203
|
return m;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
-
const SEAT_POLL_MS = 10_000;
|
|
207
|
-
const SEAT_RETRY_MS = 60_000;
|
|
208
|
-
|
|
209
206
|
function seatBlockedUntil(seatId: string, now: number, families: string[], cfg: Config): number | null {
|
|
210
207
|
const account = loadAccounts(claudePool).accounts.find((a) => a.id === seatId);
|
|
211
208
|
if (!account) return null;
|
|
@@ -233,37 +230,10 @@ async function moveExhaustedSeat(seat: Account, sid: string, gate: MarkerGate, m
|
|
|
233
230
|
return true;
|
|
234
231
|
}
|
|
235
232
|
|
|
236
|
-
type Say = (terminal: string, text: string) => void;
|
|
237
|
-
|
|
238
233
|
function systemLine(sid: string, text: string): string {
|
|
239
234
|
return `${JSON.stringify({ type: "system", subtype: "informational", content: text, level: "warning", uuid: crypto.randomUUID(), session_id: sid })}\n`;
|
|
240
235
|
}
|
|
241
236
|
|
|
242
|
-
const clockFmt = new Intl.DateTimeFormat("en-US", { hour: "2-digit", minute: "2-digit", hourCycle: "h23", timeZoneName: "short" });
|
|
243
|
-
|
|
244
|
-
async function countdownWait(acct: string, until: number, out: { stream: boolean; say: Say }): Promise<boolean> {
|
|
245
|
-
let aborted = false;
|
|
246
|
-
const onInt = () => { aborted = true; };
|
|
247
|
-
process.on("SIGINT", onInt);
|
|
248
|
-
const minutes = Math.max(1, Math.ceil((until - Date.now()) / 60000));
|
|
249
|
-
out.say(
|
|
250
|
-
`\n\x1b[36m⏳ tokenmaxxing: all accounts at their limit. Resuming on ${acct} when it resets (Ctrl-C to resume now).\x1b[0m\n`,
|
|
251
|
-
`tokenmaxxing: all accounts at their limit. Resuming at ${clockFmt.format(until)} (in ${minutes} min) on ${acct}.`,
|
|
252
|
-
);
|
|
253
|
-
while (!aborted && Date.now() < until) {
|
|
254
|
-
if (!out.stream) {
|
|
255
|
-
const left = until - Date.now();
|
|
256
|
-
const m = Math.floor(left / 60000);
|
|
257
|
-
const s = Math.floor((left % 60000) / 1000);
|
|
258
|
-
process.stderr.write(`\r\x1b[36m resuming in ${m}m ${String(s).padStart(2, "0")}s \x1b[0m`);
|
|
259
|
-
}
|
|
260
|
-
await Bun.sleep(1000);
|
|
261
|
-
}
|
|
262
|
-
process.removeListener("SIGINT", onInt);
|
|
263
|
-
out.say(`\n\x1b[36m↻ resuming on ${acct}...\x1b[0m\n`, `tokenmaxxing: resuming on ${acct}.`);
|
|
264
|
-
return aborted;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
237
|
function resumePrompt(input: { compacted: boolean; origin: z.infer<typeof RespawnMarkerSchema>["origin"] }): string {
|
|
268
238
|
const moved = input.compacted
|
|
269
239
|
? "tokenmaxxing compacted this conversation and resumed the session on an account with quota headroom."
|
package/src/lib/claude.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { errorMessage, log } from "./log.ts";
|
|
|
9
9
|
import { claudeTierLabel, describeIdentity, fetchTokenIdentity, isDeadCredential, InvalidGrantError } from "./oauth.ts";
|
|
10
10
|
import { claudePool, env, paths, seatFromEnv, storeDirFor } from "./paths.ts";
|
|
11
11
|
import { pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
|
|
12
|
+
import { deletePiStore } from "./piauth.ts";
|
|
12
13
|
import { seatCounts } from "./presence.ts";
|
|
13
14
|
import { StoreUnusableError, type Observation, type Provider, type SampleReport } from "./provider.ts";
|
|
14
15
|
import { foldTee, sampleAccountUsage, teeObservation, usageBlockedUntil } from "./sample.ts";
|
|
@@ -117,20 +118,22 @@ async function storeUsable(a: Account): Promise<boolean> {
|
|
|
117
118
|
}
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
export function pickSeat(now: number, model: ModelInfo | null): Account | null {
|
|
121
|
+
export function pickSeat(now: number, model: ModelInfo | null, eligible: (a: Account) => boolean = () => true): Account | null {
|
|
121
122
|
const cfg = loadConfig();
|
|
122
123
|
const idx = loadAccounts(claudePool);
|
|
123
124
|
let dirty = false;
|
|
124
125
|
for (const a of idx.accounts) dirty = foldTee(a) || dirty;
|
|
125
126
|
if (dirty) saveAccounts(claudePool, idx);
|
|
126
127
|
const ctx: PickCtx = { now, thresholds: thresholdBars(cfg), currentId: null, families: gatedFamilies(model, cfg.policy.switchModels), seats: presence() };
|
|
127
|
-
|
|
128
|
+
const candidates = idx.accounts.filter(eligible);
|
|
129
|
+
return pickBest(candidates, ctx) ?? pickEarliestReset(candidates, ctx)?.account ?? null;
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
async function removeCredentials(a: Account): Promise<void> {
|
|
131
133
|
await deleteItem(storeTarget(a.id));
|
|
132
134
|
rmSync(storeDirFor(a.id), { recursive: true, force: true });
|
|
133
135
|
rmSync(`${storeDirFor(a.id)}.lock`, { recursive: true, force: true });
|
|
136
|
+
deletePiStore("claude", a.id);
|
|
134
137
|
clearUsageSnapshot(a.id);
|
|
135
138
|
}
|
|
136
139
|
|
|
@@ -236,7 +239,7 @@ async function importLive(): Promise<Harvest | null> {
|
|
|
236
239
|
return login();
|
|
237
240
|
}
|
|
238
241
|
|
|
239
|
-
function ensurePathAhead(): void {
|
|
242
|
+
export function ensurePathAhead(): void {
|
|
240
243
|
const rc = shellRcPath();
|
|
241
244
|
if (!rc) {
|
|
242
245
|
console.log(c.yellow(`⚠ add to your shell rc: export PATH="${paths.binDir}:$PATH"`));
|
package/src/lib/claudebin.ts
CHANGED
|
@@ -102,6 +102,15 @@ export function verifyRealBin(input: { bin: string; name: string; versionOk: (ve
|
|
|
102
102
|
|
|
103
103
|
export const CLAUDE_BIN = { name: "claude", key: "claudeBin", versionOk: (out: string) => out.toLowerCase().includes("claude") } as const;
|
|
104
104
|
export const CODEX_BIN = { name: "codex", key: "codexBin", versionOk: (out: string) => out.toLowerCase().includes("codex") } as const;
|
|
105
|
+
function isBareVersion(out: string): boolean {
|
|
106
|
+
try {
|
|
107
|
+
return Bun.semver.order(out, "0.0.0") >= 0;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const PI_BIN = { name: "pi", key: "piBin", versionOk: isBareVersion } as const;
|
|
105
114
|
|
|
106
115
|
export function resolveVerifiedClaude(): string {
|
|
107
116
|
const candidates: string[] = [];
|
package/src/lib/codex.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { writeFileAtomic } from "./atomic.ts";
|
|
|
4
4
|
import { CODEX_BIN, MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, resolveRealBin, verifyRealBin } from "./claudebin.ts";
|
|
5
5
|
import { codexIdentityOf, codexStoreUsable, deleteCodexStoreAuth, isCodexAccessExpiring, readCodexAuthAt, readCodexStoreAuth, writeCodexStoreAuth } from "./codexauth.ts";
|
|
6
6
|
import { CodexInvalidGrantError, CodexRefreshFailedError, refreshCodexAuth } from "./codexoauth.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { deletePiStore } from "./piauth.ts";
|
|
8
|
+
import { livingPresences, seatCounts } from "./presence.ts";
|
|
8
9
|
import { CodexUsageReadError, codexLimitLabel, fetchCodexUsage } from "./codexusage.ts";
|
|
9
10
|
import { codexSupervisorLink, ensurePathInRc, installCodexSupervisor, managedShellRcSkipLines, shellRcPath } from "./install.ts";
|
|
10
11
|
import { withLock } from "./lock.ts";
|
|
@@ -34,7 +35,7 @@ function applyUsage(account: Account, usage: CodexUsage, at: number): void {
|
|
|
34
35
|
|
|
35
36
|
type CodexReadOutcome = { ok: true; usage: CodexUsage; at: number } | { ok: false; reason: string; deadGrant: boolean };
|
|
36
37
|
|
|
37
|
-
async function readCodexUsage(account: Account, now: number, refresh: boolean): Promise<CodexReadOutcome> {
|
|
38
|
+
async function readCodexUsage(account: Account, now: number, refresh: boolean, holder?: string): Promise<CodexReadOutcome> {
|
|
38
39
|
let auth: CodexAuthJson | null;
|
|
39
40
|
try {
|
|
40
41
|
auth = readCodexStoreAuth(account.id);
|
|
@@ -47,7 +48,7 @@ async function readCodexUsage(account: Account, now: number, refresh: boolean):
|
|
|
47
48
|
if (!refresh) {
|
|
48
49
|
return { ok: false, reason: "stored access token is expiring and this read never refreshes a store", deadGrant: false };
|
|
49
50
|
}
|
|
50
|
-
if (
|
|
51
|
+
if (livingPresences(codexPaths.presenceDir).some((p) => p.accountId === account.id && p.id !== holder)) {
|
|
51
52
|
return { ok: false, reason: "running in a live codex session (store refresh unsafe)", deadGrant: false };
|
|
52
53
|
}
|
|
53
54
|
auth = await refreshCodexAuth({ auth, now });
|
|
@@ -62,9 +63,9 @@ async function readCodexUsage(account: Account, now: number, refresh: boolean):
|
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
export async function observeCodex(account: Account, cfg: Config, now: number, opts: { probe: boolean; refresh: boolean }): Promise<Observation | null> {
|
|
66
|
+
export async function observeCodex(account: Account, cfg: Config, now: number, opts: { probe: boolean; refresh: boolean; holder?: string }): Promise<Observation | null> {
|
|
66
67
|
if (opts.probe && (account.lastUsageAt == null || now - account.lastUsageAt > cfg.policy.usagePollTtlMs)) {
|
|
67
|
-
const outcome = await readCodexUsage(account, now, opts.refresh);
|
|
68
|
+
const outcome = await readCodexUsage(account, now, opts.refresh, opts.holder);
|
|
68
69
|
await withLock(codexPool.lockFile, () => {
|
|
69
70
|
const idx = loadAccounts(codexPool);
|
|
70
71
|
const a = idx.accounts.find((x) => x.id === account.id);
|
|
@@ -210,10 +211,10 @@ export function codexPickCtx(now: number, currentId: string | null): PickCtx {
|
|
|
210
211
|
return { now, thresholds: thresholdBars(loadConfig()), currentId, families: null, seats: null };
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
export function pickCodexSeat(now: number): Account | null {
|
|
214
|
+
export function pickCodexSeat(now: number, eligible: (a: Account) => boolean = (a) => codexStoreUsable(a.id)): Account | null {
|
|
214
215
|
const ctx = codexPickCtx(now, null);
|
|
215
216
|
const present = seatCounts(codexPaths.presenceDir);
|
|
216
|
-
const open = loadAccounts(codexPool).accounts.filter((a) => a.needsReauth !== true && !present.has(a.id) &&
|
|
217
|
+
const open = loadAccounts(codexPool).accounts.filter((a) => a.needsReauth !== true && !present.has(a.id) && eligible(a));
|
|
217
218
|
return pickBest(open.filter((a) => !isExhausted(a, ctx)), ctx) ?? pickEarliestReset(open, ctx)?.account ?? null;
|
|
218
219
|
}
|
|
219
220
|
|
|
@@ -232,7 +233,10 @@ export const codex: Provider = {
|
|
|
232
233
|
mergeWindows: (next) => next,
|
|
233
234
|
swap: prepareMove,
|
|
234
235
|
classifySwapError: (e) => (e instanceof CodexInvalidGrantError ? "dead-grant" : e instanceof StoreUnusableError ? "skip" : "fatal"),
|
|
235
|
-
removeCredentials: async (a) =>
|
|
236
|
+
removeCredentials: async (a) => {
|
|
237
|
+
deleteCodexStoreAuth(a.id);
|
|
238
|
+
deletePiStore("codex", a.id);
|
|
239
|
+
},
|
|
236
240
|
storeUsable: async (a) => codexStoreUsable(a.id),
|
|
237
241
|
login,
|
|
238
242
|
importLive,
|
package/src/lib/codexauth.ts
CHANGED
|
@@ -78,6 +78,14 @@ function decodeJwtPayload(input: { jwt: string }): unknown {
|
|
|
78
78
|
return JSON.parse(payload);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
export function chatgptAccountIdOf(input: { jwt: string }): string | null {
|
|
82
|
+
try {
|
|
83
|
+
return IdClaimsSchema.parse(decodeJwtPayload(input))["https://api.openai.com/auth"]?.chatgpt_account_id ?? null;
|
|
84
|
+
} catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
81
89
|
export type CodexIdentity = { accountId: string; email: string | null; planType: string | null };
|
|
82
90
|
|
|
83
91
|
export function codexIdentityOf(input: { auth: CodexAuthJson }): CodexIdentity {
|