tokenmaxxing 1.131.0 → 1.133.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/package.json +1 -1
- package/src/cli/key.ts +147 -0
- package/src/cli/status.ts +48 -3
- package/src/entries/codexsupervisor.ts +7 -5
- package/src/entries/statusline.ts +10 -0
- package/src/entries/stopfailurehook.ts +24 -6
- package/src/entries/stophook.ts +7 -6
- package/src/entries/supervisor.ts +106 -74
- package/src/lib/apikeys.ts +142 -0
- package/src/lib/claude.ts +25 -7
- package/src/lib/compact.ts +3 -4
- package/src/lib/decide.ts +27 -6
- package/src/lib/paths.ts +3 -0
- package/src/lib/pi.ts +1 -0
- package/src/lib/presence.ts +6 -5
- package/src/lib/provider.ts +8 -1
- package/src/lib/sessions.ts +7 -2
- package/src/lib/supervise.ts +53 -3
- package/src/lib/types.ts +30 -0
- package/src/lib/usage.ts +1 -1
- package/src/main.ts +7 -2
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { once } from "node:events";
|
|
2
1
|
import { existsSync, mkdirSync, rmSync, readdirSync, statSync } from "node:fs";
|
|
3
2
|
import { basename, join } from "node:path";
|
|
4
3
|
import type { FileSink, Subprocess } from "bun";
|
|
@@ -6,20 +5,21 @@ import { maxBy, omit } from "es-toolkit";
|
|
|
6
5
|
import { z } from "zod";
|
|
7
6
|
import { claudePool, paths, storeDirFor } from "../lib/paths.ts";
|
|
8
7
|
import { CLAUDE_BIN, UNMANAGED_ENV, WRAP_DEPTH_ENV, resolveRealBin, wrapDepth } from "../lib/claudebin.ts";
|
|
9
|
-
import {
|
|
8
|
+
import { apiKeyEnv, clearSessionCost, foldSessionCost, loadApiKeys, readApiKey, sendApiKey, usableApiKey, writeSessionCost } from "../lib/apikeys.ts";
|
|
9
|
+
import { claude, pickLaunch, type LaunchTarget } from "../lib/claude.ts";
|
|
10
10
|
import { compactClaudeSession } from "../lib/compact.ts";
|
|
11
|
-
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
11
|
+
import { evaluateAndMaybeSwap, moveTarget } from "../lib/decide.ts";
|
|
12
12
|
import { withLock } from "../lib/lock.ts";
|
|
13
13
|
import { thresholdBars, usableAt } 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";
|
|
17
|
-
import { countdownWait, exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough, SEAT_POLL_MS, SEAT_RETRY_MS, type Say } from "../lib/supervise.ts";
|
|
17
|
+
import { countdownWait, exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, relayStdout, runPassthrough, SEAT_POLL_MS, SEAT_RETRY_MS, type Say } from "../lib/supervise.ts";
|
|
18
18
|
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, writeUsage } from "../lib/state.ts";
|
|
21
21
|
import { gatedFamilies, modelFromFlag, parseStreamLine, scrubCredEnv } from "../lib/usage.ts";
|
|
22
|
-
import { JsonTextSchema, RespawnMarkerSchema, type Account, type Config, type ModelInfo, type UsageWindows } from "../lib/types.ts";
|
|
22
|
+
import { CostLineSchema, JsonTextSchema, RespawnMarkerSchema, type Account, type ApiKey, type Config, type ModelInfo, type UsageWindows } from "../lib/types.ts";
|
|
23
23
|
import { errorMessage, log } from "../lib/log.ts";
|
|
24
24
|
|
|
25
25
|
const SUBCOMMANDS = new Set([
|
|
@@ -254,10 +254,11 @@ async function moveExhaustedSeat(seat: Account, sid: string, gate: MarkerGate, m
|
|
|
254
254
|
const families = gatedFamilies(model, cfg.policy.switchModels);
|
|
255
255
|
const until = seatBlockedUntil(seat.id, now, families, cfg);
|
|
256
256
|
if (until == null || until <= gate.overriddenUntil) return false;
|
|
257
|
-
const decision = await evaluateAndMaybeSwap(claude, now, true, null, { seatId: seat.id, sessionFamilies: families, waiterId: sid, refused });
|
|
258
|
-
log("supervisor.seat_exhausted", { seat: seat.id.slice(0, 8), until, reason: decision.reason, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
|
|
259
|
-
|
|
260
|
-
|
|
257
|
+
const decision = await evaluateAndMaybeSwap(claude, now, true, null, { seatId: seat.id, sessionFamilies: families, waiterId: sid, refused, apiKeys: true });
|
|
258
|
+
log("supervisor.seat_exhausted", { seat: seat.id.slice(0, 8), until, reason: decision.reason, account: decision.account?.id.slice(0, 8), key: decision.apiKey?.id, waitUntil: decision.waitUntil });
|
|
259
|
+
const target = moveTarget(decision);
|
|
260
|
+
if (target) {
|
|
261
|
+
writeRespawnMarker({ session: { sid, launchedAt: gate.launchedAt, live: liveSessionId(sid) }, ...target, waitUntil: target.waitUntil ?? now, compact: true, origin: "seatwatch" });
|
|
261
262
|
}
|
|
262
263
|
} catch (e) {
|
|
263
264
|
log("supervisor.seat_watch_error", { err: errorMessage(e) });
|
|
@@ -265,14 +266,54 @@ async function moveExhaustedSeat(seat: Account, sid: string, gate: MarkerGate, m
|
|
|
265
266
|
return true;
|
|
266
267
|
}
|
|
267
268
|
|
|
269
|
+
async function moveKeySession(key: ApiKey, sid: string, gate: MarkerGate, model: ModelInfo | null, refused: string[], resumed: Set<string>): Promise<boolean> {
|
|
270
|
+
await foldCost(sid, key.id, resumed);
|
|
271
|
+
try {
|
|
272
|
+
if (usableApiKey(key.id) != null) return false;
|
|
273
|
+
const now = Date.now();
|
|
274
|
+
const cfg = loadConfig();
|
|
275
|
+
const families = gatedFamilies(model, cfg.policy.switchModels);
|
|
276
|
+
const decision = await evaluateAndMaybeSwap(claude, now, true, null, { seatId: null, sessionFamilies: families, waiterId: sid, refused, apiKeys: true, apiKeyId: key.id });
|
|
277
|
+
log("supervisor.key_watch", { key: key.id, reason: decision.reason, account: decision.account?.id.slice(0, 8), next: decision.apiKey?.id, waitUntil: decision.waitUntil });
|
|
278
|
+
const target = moveTarget(decision);
|
|
279
|
+
if (target && (target.waitUntil === undefined || target.waitUntil > gate.overriddenUntil)) {
|
|
280
|
+
writeRespawnMarker({ session: { sid, launchedAt: gate.launchedAt, live: liveSessionId(sid) }, ...target, waitUntil: target.waitUntil ?? now, compact: true, origin: "seatwatch" });
|
|
281
|
+
}
|
|
282
|
+
} catch (e) {
|
|
283
|
+
log("supervisor.key_watch_error", { err: errorMessage(e) });
|
|
284
|
+
}
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function resolveLaunch(wanted: { accountId: string | null; apiKeyId: string | null } | null, now: number, model: ModelInfo | null): LaunchTarget | null {
|
|
289
|
+
if (wanted?.apiKeyId != null) {
|
|
290
|
+
const key = usableApiKey(wanted.apiKeyId);
|
|
291
|
+
if (key) return { account: null, apiKey: key };
|
|
292
|
+
} else if (wanted?.accountId != null) {
|
|
293
|
+
const id = wanted.accountId;
|
|
294
|
+
const account = loadAccounts(claudePool).accounts.find((a) => a.id === id);
|
|
295
|
+
if (account) return { account, apiKey: null };
|
|
296
|
+
}
|
|
297
|
+
return pickLaunch(now, model);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function foldCost(sid: string, keyId: string | null, resumed: Set<string>): Promise<void> {
|
|
301
|
+
try {
|
|
302
|
+
await foldSessionCost(sid, keyId, resumed);
|
|
303
|
+
} catch (e) {
|
|
304
|
+
log("supervisor.cost_fold_failed", { err: errorMessage(e) });
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
268
308
|
function systemLine(sid: string, text: string): string {
|
|
269
309
|
return `${JSON.stringify({ type: "system", subtype: "informational", content: text, level: "warning", uuid: crypto.randomUUID(), session_id: sid })}\n`;
|
|
270
310
|
}
|
|
271
311
|
|
|
272
|
-
function resumePrompt(input: { compacted: boolean; origin: z.infer<typeof RespawnMarkerSchema>["origin"] }): string {
|
|
312
|
+
function resumePrompt(input: { compacted: boolean; origin: z.infer<typeof RespawnMarkerSchema>["origin"]; onKey: boolean }): string {
|
|
313
|
+
const where = input.onKey ? "on an Anthropic API key, because every pooled account is at its limit or the previous key ran out" : "on an account with quota headroom";
|
|
273
314
|
const moved = input.compacted
|
|
274
|
-
?
|
|
275
|
-
:
|
|
315
|
+
? `tokenmaxxing compacted this conversation and resumed the session ${where}.`
|
|
316
|
+
: `tokenmaxxing resumed this session ${where}.`;
|
|
276
317
|
if (input.origin === "stop") {
|
|
277
318
|
return `${moved} The previous turn finished before the move. If it waited on the user or completed the task, end this turn without restating it. If it waited on a background Bash task, Monitor, or Workflow run, relaunch that work, because the move ended it.`;
|
|
278
319
|
}
|
|
@@ -283,8 +324,6 @@ function userLine(text: string): string {
|
|
|
283
324
|
return `${JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text }] } })}\n`;
|
|
284
325
|
}
|
|
285
326
|
|
|
286
|
-
const STDOUT_QUIET_MS = 1000;
|
|
287
|
-
|
|
288
327
|
const TaskLineSchema = z.discriminatedUnion("subtype", [
|
|
289
328
|
z.looseObject({ type: z.literal("system"), subtype: z.literal("task_started"), task_id: z.string(), description: z.string(), session_id: z.string() }),
|
|
290
329
|
z.looseObject({ type: z.literal("system"), subtype: z.literal("task_notification"), task_id: z.string() }),
|
|
@@ -304,11 +343,14 @@ function stoppedTaskLines(open: OpenTasks): string[] {
|
|
|
304
343
|
return [...open].map(([id, task]) => `${JSON.stringify({ type: "system", subtype: "task_notification", task_id: id, status: "stopped", reason: "worker_restart", output_file: "", summary: `Stopped by a tokenmaxxing restart: ${task.description || id}`, uuid: crypto.randomUUID(), session_id: task.sessionId })}\n`);
|
|
305
344
|
}
|
|
306
345
|
|
|
307
|
-
function seatTee(seatId: string, model: ModelInfo | null, open: OpenTasks): (line: string) => void {
|
|
346
|
+
function seatTee(seatId: string | null, model: ModelInfo | null, open: OpenTasks, costSid: string | null): (line: string) => void {
|
|
308
347
|
let last: UsageWindows | null = null;
|
|
309
348
|
return (line) => {
|
|
310
349
|
const value = JsonTextSchema.safeParse(line).data;
|
|
311
350
|
trackTask(open, value);
|
|
351
|
+
const cost = costSid == null ? null : (CostLineSchema.safeParse(value).data ?? null);
|
|
352
|
+
if (costSid != null && cost != null) writeSessionCost(costSid, cost);
|
|
353
|
+
if (seatId == null) return;
|
|
312
354
|
const msg = parseStreamLine(value);
|
|
313
355
|
if (msg?.windows) last = msg.windows;
|
|
314
356
|
else if (msg?.type !== "assistant") return;
|
|
@@ -316,42 +358,6 @@ function seatTee(seatId: string, model: ModelInfo | null, open: OpenTasks): (lin
|
|
|
316
358
|
};
|
|
317
359
|
}
|
|
318
360
|
|
|
319
|
-
function relayStdout(source: ReadableStream<Uint8Array>, onLine: (line: string) => void): () => Promise<void> {
|
|
320
|
-
let ended = false;
|
|
321
|
-
let blocked = false;
|
|
322
|
-
let readAt = Date.now();
|
|
323
|
-
const forwarded = source.pipeThrough(
|
|
324
|
-
new TransformStream<Uint8Array, Uint8Array>({
|
|
325
|
-
async transform(chunk, controller) {
|
|
326
|
-
readAt = Date.now();
|
|
327
|
-
if (!process.stdout.write(chunk)) {
|
|
328
|
-
blocked = true;
|
|
329
|
-
await once(process.stdout, "drain");
|
|
330
|
-
blocked = false;
|
|
331
|
-
}
|
|
332
|
-
controller.enqueue(chunk);
|
|
333
|
-
},
|
|
334
|
-
}),
|
|
335
|
-
);
|
|
336
|
-
void (async () => {
|
|
337
|
-
for await (const line of readLines(forwarded)) {
|
|
338
|
-
try {
|
|
339
|
-
onLine(line);
|
|
340
|
-
} catch (e) {
|
|
341
|
-
log("supervisor.tee_failed", { err: errorMessage(e) });
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
})()
|
|
345
|
-
.catch((e: unknown) => log("supervisor.stdout_relay_failed", { err: errorMessage(e) }))
|
|
346
|
-
.finally(() => {
|
|
347
|
-
ended = true;
|
|
348
|
-
});
|
|
349
|
-
return async () => {
|
|
350
|
-
const exitedAt = Date.now();
|
|
351
|
-
while (!ended && (blocked || Date.now() - Math.max(readAt, exitedAt) < STDOUT_QUIET_MS)) await Bun.sleep(50);
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
|
|
355
361
|
const REPLAYED_REQUESTS = new Set(["initialize", "set_max_thinking_tokens", "set_model", "set_permission_mode"]);
|
|
356
362
|
|
|
357
363
|
const RelayLineSchema = z.discriminatedUnion("type", [
|
|
@@ -508,7 +514,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
508
514
|
});
|
|
509
515
|
|
|
510
516
|
if (!info.manage || process.env[UNMANAGED_ENV]) {
|
|
511
|
-
const passthroughEnv = omit(childEnv, ["TOKENMAXXING_SUPERVISED", "TOKENMAXXING_SESSION_ID", "TOKENMAXXING_MODEL", "TOKENMAXXING_REFUSED"]);
|
|
517
|
+
const passthroughEnv = omit(childEnv, ["TOKENMAXXING_SUPERVISED", "TOKENMAXXING_SESSION_ID", "TOKENMAXXING_MODEL", "TOKENMAXXING_REFUSED", "TOKENMAXXING_API_KEY_ID"]);
|
|
512
518
|
return runPassthrough({ real, argv, env: passthroughEnv, onSpawn: (p) => { child = p; } });
|
|
513
519
|
}
|
|
514
520
|
|
|
@@ -536,6 +542,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
536
542
|
pruneStaleSessions(Date.now());
|
|
537
543
|
|
|
538
544
|
let launchArgs = resume ? ["--resume", sid, ...base] : ["--session-id", sid, ...base];
|
|
545
|
+
const resumed = new Set<string>(resume ? [sid] : []);
|
|
539
546
|
let pendingTranscript = resume ? null : transcriptPath(sid);
|
|
540
547
|
|
|
541
548
|
mkdirSync(paths.respawnDir, { recursive: true });
|
|
@@ -557,7 +564,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
557
564
|
let firstLine: string | null = null;
|
|
558
565
|
let respawns = 0;
|
|
559
566
|
let overriddenUntil = 0;
|
|
560
|
-
let wanted: string | null = null;
|
|
567
|
+
let wanted: { accountId: string | null; apiKeyId: string | null } | null = null;
|
|
561
568
|
let refused: string[] = [];
|
|
562
569
|
let drainStdout: (() => Promise<void>) | null = null;
|
|
563
570
|
try {
|
|
@@ -567,55 +574,62 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
567
574
|
const gate: MarkerGate = { launchedAt: Date.now(), overriddenUntil };
|
|
568
575
|
const launched = await withLock(claudePool.lockFile, async () => {
|
|
569
576
|
if (terminating) return null;
|
|
570
|
-
const
|
|
571
|
-
|
|
577
|
+
const target = resolveLaunch(wanted, gate.launchedAt, model);
|
|
578
|
+
const picked = target?.account ?? null;
|
|
579
|
+
const key = target?.apiKey ?? null;
|
|
580
|
+
log("supervisor.launch", { sid, respawns, seat: picked?.id.slice(0, 8) ?? null, key: key?.id, args: launchArgs.join(" "), injected: firstLine !== null });
|
|
572
581
|
const open: OpenTasks = new Map();
|
|
573
|
-
const
|
|
582
|
+
const costSid = existsSync(claudePool.apiKeysJson) ? sid : null;
|
|
583
|
+
const tee = stream && (picked || costSid != null) ? seatTee(picked?.id ?? null, model, open, costSid) : null;
|
|
584
|
+
const secret = key ? readApiKey(key) : null;
|
|
585
|
+
const extra: "pipe"[] = secret == null ? [] : ["pipe"];
|
|
586
|
+
const scrubbed = scrubCredEnv(childEnv);
|
|
574
587
|
const spawned = Bun.spawn([real, ...launchArgs], {
|
|
575
|
-
|
|
576
|
-
stdout: tee === null ? "inherit" : "pipe",
|
|
577
|
-
stderr: "inherit",
|
|
588
|
+
stdio: [relay === null ? "inherit" : "pipe", tee === null ? "inherit" : "pipe", "inherit", ...extra],
|
|
578
589
|
env: {
|
|
579
|
-
...(picked ? { ...
|
|
590
|
+
...(picked ? { ...scrubbed, CLAUDE_SECURESTORAGE_CONFIG_DIR: storeDirFor(picked.id) } : key ? apiKeyEnv(scrubbed, key) : childEnv),
|
|
580
591
|
TOKENMAXXING_SUPERVISED: "1",
|
|
581
592
|
TOKENMAXXING_SESSION_ID: sid,
|
|
582
593
|
TOKENMAXXING_LAUNCHED_AT: String(gate.launchedAt),
|
|
583
594
|
TOKENMAXXING_MODEL: effective.model ?? "",
|
|
584
595
|
TOKENMAXXING_REFUSED: refused.join(","),
|
|
596
|
+
TOKENMAXXING_API_KEY_ID: key?.id ?? "",
|
|
585
597
|
},
|
|
586
598
|
});
|
|
587
599
|
child = spawned;
|
|
600
|
+
if (secret != null) sendApiKey(spawned, secret);
|
|
588
601
|
const drain = tee !== null && spawned.stdout instanceof ReadableStream ? relayStdout(spawned.stdout, tee) : null;
|
|
589
602
|
if (relay !== null) relay.attach(spawned.stdin!, firstLine, pendingTranscript);
|
|
590
603
|
firstLine = null;
|
|
591
|
-
if (picked) {
|
|
604
|
+
if (picked || key) {
|
|
592
605
|
await recordPresenceOrStop({
|
|
593
606
|
child: spawned,
|
|
594
607
|
dir: paths.presenceDir,
|
|
595
608
|
id: sid,
|
|
596
|
-
accountId: picked
|
|
609
|
+
accountId: picked?.id ?? "",
|
|
610
|
+
...(key ? { apiKeyId: key.id } : {}),
|
|
597
611
|
event: "supervisor.presence_failed",
|
|
598
612
|
message: "could not write the session presence file - refusing to run a session whose seat placement cannot see",
|
|
599
613
|
savedTermios,
|
|
600
614
|
});
|
|
601
615
|
}
|
|
602
616
|
releaseClaimLocked(sid);
|
|
603
|
-
return { child: spawned, seat: picked, drain, open };
|
|
617
|
+
return { child: spawned, seat: picked, key, drain, open };
|
|
604
618
|
});
|
|
605
619
|
if (launched == null) {
|
|
606
620
|
await releaseClaim();
|
|
607
621
|
return 143;
|
|
608
622
|
}
|
|
609
623
|
|
|
610
|
-
const { child: proc, seat, open } = launched;
|
|
624
|
+
const { child: proc, seat, key, open } = launched;
|
|
611
625
|
drainStdout = launched.drain;
|
|
612
626
|
let seatCheckAt = 0;
|
|
613
627
|
await raceMarkerOrExit({
|
|
614
628
|
child: proc,
|
|
615
629
|
tick: async () => {
|
|
616
630
|
if (existsSync(marker) && (await consumableMarker(marker, gate)) != null) return true;
|
|
617
|
-
if (seat && !terminating && Date.now() >= seatCheckAt) {
|
|
618
|
-
const decided = await moveExhaustedSeat(seat, sid, gate, model, refused);
|
|
631
|
+
if ((seat || key) && !terminating && Date.now() >= seatCheckAt) {
|
|
632
|
+
const decided = seat ? await moveExhaustedSeat(seat, sid, gate, model, refused) : key ? await moveKeySession(key, sid, gate, model, refused, resumed) : false;
|
|
619
633
|
seatCheckAt = Date.now() + (decided ? SEAT_RETRY_MS : SEAT_POLL_MS);
|
|
620
634
|
}
|
|
621
635
|
return false;
|
|
@@ -630,6 +644,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
630
644
|
});
|
|
631
645
|
await drainStdout?.();
|
|
632
646
|
drainStdout = null;
|
|
647
|
+
await foldCost(sid, key?.id ?? null, resumed);
|
|
633
648
|
|
|
634
649
|
const m = !terminating && existsSync(marker) ? await consumableMarker(marker, gate) : null;
|
|
635
650
|
if (m) {
|
|
@@ -638,7 +653,8 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
638
653
|
noticeSid = m.sessionId;
|
|
639
654
|
for (const line of stoppedTaskLines(open)) process.stdout.write(line);
|
|
640
655
|
const accounts = loadAccounts(claudePool).accounts;
|
|
641
|
-
const
|
|
656
|
+
const nextKey = m.apiKeyId == null ? null : (loadApiKeys().keys.find((k) => k.id === m.apiKeyId) ?? null);
|
|
657
|
+
const label = m.apiKeyId != null ? `API key ${nextKey?.label ?? m.apiKeyId}` : (accounts.find((a) => a.id === m.accountId)?.label ?? m.accountId.slice(0, 8));
|
|
642
658
|
const walledUntil = accounts.find((a) => a.id === seat?.id)?.enforcedUntil ?? 0;
|
|
643
659
|
const transcript = findTranscript(m.sessionId);
|
|
644
660
|
const resumable = transcript !== null;
|
|
@@ -646,34 +662,50 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
646
662
|
for (const id of relay.replay()) process.stdout.write(controlErrorLine(id));
|
|
647
663
|
}
|
|
648
664
|
let compacted = false;
|
|
665
|
+
const compactKey = key != null && usableApiKey(key.id) != null ? key : null;
|
|
666
|
+
const compactOn = seat?.label ?? (compactKey ? `API key ${compactKey.label}` : null);
|
|
649
667
|
if (m.compact && seat && resumable && walledUntil > Date.now()) {
|
|
650
668
|
log("supervisor.compact_skipped", { sid: m.sessionId.slice(0, 8), seat: seat.id.slice(0, 8), until: walledUntil });
|
|
651
|
-
} else if (m.compact &&
|
|
652
|
-
say(`\n\x1b[36m↻ tokenmaxxing: compacting the conversation on ${
|
|
653
|
-
const
|
|
654
|
-
const
|
|
669
|
+
} else if (m.compact && compactOn != null && resumable) {
|
|
670
|
+
say(`\n\x1b[36m↻ tokenmaxxing: compacting the conversation on ${compactOn} before the move...\x1b[0m\n`, `tokenmaxxing: compacting the conversation on ${compactOn} before the move.`);
|
|
671
|
+
const baseEnv = { ...omit(scrubCredEnv(childEnv), ["TOKENMAXXING_SUPERVISED", "TOKENMAXXING_SESSION_ID", "TOKENMAXXING_LAUNCHED_AT", "TOKENMAXXING_MODEL", "TOKENMAXXING_REFUSED", "TOKENMAXXING_API_KEY_ID"]), TOKENMAXXING_PROBE: "1" };
|
|
672
|
+
const compactEnv = seat ? { ...baseEnv, CLAUDE_SECURESTORAGE_CONFIG_DIR: storeDirFor(seat.id) } : compactKey ? apiKeyEnv(baseEnv, compactKey) : baseEnv;
|
|
673
|
+
const compactSecret = seat == null && compactKey ? readApiKey(compactKey) : null;
|
|
674
|
+
const outcome = await compactClaudeSession({
|
|
675
|
+
real,
|
|
676
|
+
sid: m.sessionId,
|
|
677
|
+
transcript,
|
|
678
|
+
env: compactEnv,
|
|
679
|
+
keyPipe: compactSecret != null,
|
|
680
|
+
onSpawn: (p) => {
|
|
681
|
+
child = p;
|
|
682
|
+
if (compactSecret != null) sendApiKey(p, compactSecret);
|
|
683
|
+
},
|
|
684
|
+
});
|
|
655
685
|
child = null;
|
|
656
686
|
if (terminating) {
|
|
657
687
|
await releaseClaim();
|
|
658
688
|
return 143;
|
|
659
689
|
}
|
|
660
|
-
log("supervisor.compact", { sid: m.sessionId.slice(0, 8), seat: seat
|
|
690
|
+
log("supervisor.compact", { sid: m.sessionId.slice(0, 8), seat: seat?.id.slice(0, 8), key: compactKey?.id, ok: outcome.ok, reason: outcome.ok ? undefined : outcome.reason });
|
|
661
691
|
if (!outcome.ok) say(`\x1b[33m compaction did not land (${outcome.reason}) - resuming with the full context\x1b[0m\n`, `tokenmaxxing: compaction did not land; resuming with the full context. (${outcome.reason})`);
|
|
662
692
|
compacted = outcome.ok;
|
|
663
693
|
}
|
|
664
694
|
if (m.waitUntil > Date.now()) {
|
|
665
695
|
if (await countdownWait(label, m.waitUntil, { stream, say })) overriddenUntil = m.waitUntil;
|
|
666
696
|
} else say(`\n\x1b[36m↻ tokenmaxxing: moving to ${label} - resuming...\x1b[0m\n`, `tokenmaxxing: moving to ${label} and resuming.`);
|
|
667
|
-
wanted = m.accountId;
|
|
697
|
+
wanted = m.apiKeyId != null ? { accountId: null, apiKeyId: m.apiKeyId } : { accountId: m.accountId, apiKeyId: null };
|
|
668
698
|
refused = m.refused ?? refused;
|
|
669
699
|
saveSessionFlags(m.sessionId, persistable, process.cwd());
|
|
670
|
-
const prompt = resumable ? resumePrompt({ compacted, origin: m.origin }) : null;
|
|
700
|
+
const prompt = resumable ? resumePrompt({ compacted, origin: m.origin, onKey: m.apiKeyId != null }) : null;
|
|
671
701
|
firstLine = relay !== null && prompt !== null ? userLine(prompt) : null;
|
|
672
702
|
pendingTranscript = resumable ? null : transcriptPath(m.sessionId);
|
|
703
|
+
if (resumable) resumed.add(m.sessionId);
|
|
673
704
|
launchArgs = [resumable ? "--resume" : "--session-id", m.sessionId, ...(relay === null && prompt !== null ? [prompt] : []), ...persistable];
|
|
674
705
|
continue;
|
|
675
706
|
}
|
|
676
707
|
clearPresence({ dir: paths.presenceDir, id: sid });
|
|
708
|
+
clearSessionCost(sid);
|
|
677
709
|
await releaseClaim();
|
|
678
710
|
log("supervisor.exit", { sid, respawns, code: proc.exitCode, signal: proc.signalCode, terminated: terminating || undefined });
|
|
679
711
|
return exitStatus(proc);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { chmodSync, closeSync, existsSync, mkdirSync, readFileSync, rmSync, writeSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { countBy, isEqual, sortBy } from "es-toolkit";
|
|
4
|
+
import { writeFileAtomic } from "./atomic.ts";
|
|
5
|
+
import { withLock } from "./lock.ts";
|
|
6
|
+
import { claudePool, optionalEnv, paths } from "./paths.ts";
|
|
7
|
+
import { livingPresences } from "./presence.ts";
|
|
8
|
+
import type { ApiKeyPool } from "./provider.ts";
|
|
9
|
+
import { readJsonFile } from "./state.ts";
|
|
10
|
+
import { ApiKeysIndexSchema, SessionCostSchema, type ApiKey, type ApiKeysIndex, type SessionCost } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
export const API_KEY_ENV = "TOKENMAXXING_API_KEY_ID";
|
|
13
|
+
export const API_KEY_FD = 3;
|
|
14
|
+
|
|
15
|
+
export function apiKeyEnv(env: Record<string, string | undefined>, key: ApiKey): Record<string, string | undefined> {
|
|
16
|
+
const header = key.workspaceId == null ? null : `anthropic-workspace-id: ${key.workspaceId}`;
|
|
17
|
+
const headers = header == null ? env.ANTHROPIC_CUSTOM_HEADERS : env.ANTHROPIC_CUSTOM_HEADERS ? `${env.ANTHROPIC_CUSTOM_HEADERS}\n${header}` : header;
|
|
18
|
+
return { ...env, CLAUDE_CODE_API_KEY_FILE_DESCRIPTOR: String(API_KEY_FD), ...(headers != null ? { ANTHROPIC_CUSTOM_HEADERS: headers } : {}) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function readApiKey(key: ApiKey): string {
|
|
22
|
+
return `${readFileSync(apiKeySecretFor(key.id), "utf8").trim()}\n`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function sendApiKey(child: { stdio: readonly (number | null)[] }, secret: string): void {
|
|
26
|
+
const fd = child.stdio[API_KEY_FD];
|
|
27
|
+
if (fd == null) throw new Error(`the child has no descriptor ${API_KEY_FD} to receive the API key`);
|
|
28
|
+
try {
|
|
29
|
+
const written = writeSync(fd, secret);
|
|
30
|
+
if (written !== Buffer.byteLength(secret)) throw new Error(`wrote ${written} of ${Buffer.byteLength(secret)} bytes of the API key to descriptor ${API_KEY_FD}`);
|
|
31
|
+
} finally {
|
|
32
|
+
closeSync(fd);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function apiKeySecretFor(id: string): string {
|
|
37
|
+
return join(paths.apiKeysDir, id);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function loadApiKeys(): ApiKeysIndex {
|
|
41
|
+
if (!existsSync(claudePool.apiKeysJson)) return { version: 1, keys: [], baselines: {} };
|
|
42
|
+
return readJsonFile(claudePool.apiKeysJson, ApiKeysIndexSchema);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function saveApiKeys(idx: ApiKeysIndex): void {
|
|
46
|
+
writeFileAtomic(claudePool.apiKeysJson, JSON.stringify(ApiKeysIndexSchema.parse(idx), null, 2) + "\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function writeApiKeySecret(id: string, secret: string): void {
|
|
50
|
+
mkdirSync(paths.apiKeysDir, { recursive: true, mode: 0o700 });
|
|
51
|
+
chmodSync(paths.apiKeysDir, 0o700);
|
|
52
|
+
writeFileAtomic(apiKeySecretFor(id), secret);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function removeApiKeySecret(id: string): void {
|
|
56
|
+
rmSync(apiKeySecretFor(id), { force: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function creditLeft(k: ApiKey): number | null {
|
|
60
|
+
return k.creditUsd == null ? null : k.creditUsd - k.spentUsd;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function keyUsable(k: ApiKey): boolean {
|
|
64
|
+
const left = creditLeft(k);
|
|
65
|
+
return k.refusedAt == null && (left == null || left > 0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function keySessions(): Map<string, number> {
|
|
69
|
+
const ids = livingPresences(paths.presenceDir).flatMap((p) => (p.apiKeyId == null ? [] : [p.apiKeyId]));
|
|
70
|
+
return new Map(Object.entries(countBy(ids, (id) => id)));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function pickApiKey(keys: ApiKey[], sessions: Map<string, number>, exclude: string[]): ApiKey | null {
|
|
74
|
+
const candidates = keys.filter((k) => keyUsable(k) && !exclude.includes(k.id));
|
|
75
|
+
return sortBy(candidates, [(k) => (creditLeft(k) == null ? 1 : 0), (k) => -(creditLeft(k) ?? 0), (k) => sessions.get(k.id) ?? 0, (k) => k.addedAt])[0] ?? null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function availableKeys(): ApiKey[] {
|
|
79
|
+
return loadApiKeys().keys.filter((k) => existsSync(apiKeySecretFor(k.id)));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function usableApiKey(id: string): ApiKey | null {
|
|
83
|
+
const key = availableKeys().find((k) => k.id === id);
|
|
84
|
+
return key != null && keyUsable(key) ? key : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function launchApiKey(): ApiKey | null {
|
|
88
|
+
return pickApiKey(availableKeys(), keySessions(), []);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function liveApiKeyId(env: Record<string, string | undefined> = process.env): string | null {
|
|
92
|
+
return optionalEnv(API_KEY_ENV, env) ?? null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const claudeApiKeys: ApiKeyPool = {
|
|
96
|
+
liveId: () => liveApiKeyId(),
|
|
97
|
+
holds: (id) => usableApiKey(id) != null,
|
|
98
|
+
pick: (exclude) => pickApiKey(availableKeys(), keySessions(), exclude),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export async function refuseApiKey(id: string, now: number): Promise<boolean> {
|
|
102
|
+
return withLock(claudePool.lockFile, () => {
|
|
103
|
+
const idx = loadApiKeys();
|
|
104
|
+
const key = idx.keys.find((k) => k.id === id);
|
|
105
|
+
if (!key) return false;
|
|
106
|
+
key.refusedAt = now;
|
|
107
|
+
saveApiKeys(idx);
|
|
108
|
+
return true;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const costFileFor = (sid: string): string => join(paths.costDir, `${sid}.json`);
|
|
113
|
+
|
|
114
|
+
export function writeSessionCost(sid: string, cost: SessionCost): void {
|
|
115
|
+
const file = costFileFor(sid);
|
|
116
|
+
if (existsSync(file) && isEqual(readJsonFile(file, SessionCostSchema), cost)) return;
|
|
117
|
+
writeFileAtomic(file, JSON.stringify(cost));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function clearSessionCost(sid: string): void {
|
|
121
|
+
rmSync(costFileFor(sid), { force: true });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const BASELINE_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
125
|
+
|
|
126
|
+
export async function foldSessionCost(sid: string, keyId: string | null, resumed: Set<string>): Promise<void> {
|
|
127
|
+
const file = costFileFor(sid);
|
|
128
|
+
if (!existsSync(file) || !existsSync(claudePool.apiKeysJson)) return;
|
|
129
|
+
const seen = readJsonFile(file, SessionCostSchema);
|
|
130
|
+
await withLock(claudePool.lockFile, () => {
|
|
131
|
+
const idx = loadApiKeys();
|
|
132
|
+
const prev = idx.baselines[seen.sessionId];
|
|
133
|
+
if (prev?.usd === seen.usd) return;
|
|
134
|
+
const delta = prev != null ? Math.max(0, seen.usd - prev.usd) : resumed.has(seen.sessionId) ? 0 : seen.usd;
|
|
135
|
+
const key = keyId == null ? undefined : idx.keys.find((k) => k.id === keyId);
|
|
136
|
+
if (key) key.spentUsd += delta;
|
|
137
|
+
const now = Date.now();
|
|
138
|
+
idx.baselines = Object.fromEntries(Object.entries(idx.baselines).filter(([, b]) => now - b.at <= BASELINE_RETENTION_MS));
|
|
139
|
+
idx.baselines[seen.sessionId] = { usd: seen.usd, at: now };
|
|
140
|
+
saveApiKeys(idx);
|
|
141
|
+
});
|
|
142
|
+
}
|
package/src/lib/claude.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { countBy } from "es-toolkit";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { claudeApiKeys, launchApiKey } from "./apikeys.ts";
|
|
5
6
|
import { readItem, writeItem, deleteItem, isolatedTarget, readStore, storeTarget, claudeAiOauthOnly } from "./credstore.ts";
|
|
6
7
|
import { CLAUDE_BIN, resolveRealBin, resolveVerifiedClaude } from "./claudebin.ts";
|
|
7
8
|
import { activationHint, CHECK_JOB, ensurePathInRc, HUB_JOB, installSupervisor, managedShellRcSkipLines, shellRcPath } from "./install.ts";
|
|
@@ -17,7 +18,7 @@ import { claimSample, foldTee, landRead, readyToSample, runSample, sampleAccount
|
|
|
17
18
|
import { clearUsageSnapshot, liveWaitClaims, loadAccounts, loadConfig, loadUsageSnapshot, pinBinOverride, readJsonFile, saveAccounts, type Harvest } from "./state.ts";
|
|
18
19
|
import { saveTermios, restoreTermios } from "./tty.ts";
|
|
19
20
|
import { claimResetGrant, fetchUsageDirect, gatedFamilies, mergeWindows, modelFromFlag, scrubCredEnv, windowsOf } from "./usage.ts";
|
|
20
|
-
import { CredentialBlobSchema, JsonTextSchema, OAuthAccountSchema, type Account, type Config, type ModelInfo } from "./types.ts";
|
|
21
|
+
import { CredentialBlobSchema, JsonTextSchema, OAuthAccountSchema, type Account, type ApiKey, type Config, type ModelInfo } from "./types.ts";
|
|
21
22
|
import { c } from "../cli/render.ts";
|
|
22
23
|
|
|
23
24
|
function liveId(): string | null {
|
|
@@ -25,7 +26,7 @@ function liveId(): string | null {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function presence(living = livingPresences(paths.presenceDir)): Map<string, number> {
|
|
28
|
-
const seats = new Map(living.map((s) => [s.id, s.accountId]));
|
|
29
|
+
const seats = new Map(living.filter((s) => s.apiKeyId == null).map((s) => [s.id, s.accountId]));
|
|
29
30
|
for (const claim of liveWaitClaims(Date.now())) seats.set(claim.sessionId, claim.accountId);
|
|
30
31
|
return new Map(Object.entries(countBy([...seats.values()], (id) => id)));
|
|
31
32
|
}
|
|
@@ -138,7 +139,7 @@ async function storeUsable(a: Account): Promise<boolean> {
|
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
|
|
142
|
+
function seatContext(now: number, model: ModelInfo | null, eligible: (a: Account) => boolean): { candidates: Account[]; ctx: PickCtx; fallback: () => Account | null } {
|
|
142
143
|
const cfg = loadConfig();
|
|
143
144
|
const bars = thresholdBars(cfg);
|
|
144
145
|
const idx = loadAccounts(claudePool);
|
|
@@ -148,12 +149,28 @@ export function pickSeat(now: number, model: ModelInfo | null, eligible: (a: Acc
|
|
|
148
149
|
const seats = presence();
|
|
149
150
|
const ctx: PickCtx = { now, thresholds: bars, currentId: null, families: gatedFamilies(model, cfg.policy.switchModels), seats };
|
|
150
151
|
const candidates = idx.accounts.filter(eligible);
|
|
151
|
-
|
|
152
|
-
pickBest(candidates, ctx) ??
|
|
152
|
+
const fallback = () =>
|
|
153
153
|
pickWaitTarget(candidates, ctx, seats, MAX_WAITERS_PER_ACCOUNT, now + cfg.policy.maxWaitMs, now + 2 * cfg.policy.maxWaitMs)?.account ??
|
|
154
154
|
pickEarliestReset(candidates, ctx)?.account ??
|
|
155
|
-
null
|
|
156
|
-
|
|
155
|
+
null;
|
|
156
|
+
return { candidates, ctx, fallback };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function pickSeat(now: number, model: ModelInfo | null, eligible: (a: Account) => boolean = () => true): Account | null {
|
|
160
|
+
const { candidates, ctx, fallback } = seatContext(now, model, eligible);
|
|
161
|
+
return pickBest(candidates, ctx) ?? fallback();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export type LaunchTarget = { account: Account; apiKey: null } | { account: null; apiKey: ApiKey };
|
|
165
|
+
|
|
166
|
+
export function pickLaunch(now: number, model: ModelInfo | null): LaunchTarget | null {
|
|
167
|
+
const { candidates, ctx, fallback } = seatContext(now, model, () => true);
|
|
168
|
+
const best = pickBest(candidates, ctx);
|
|
169
|
+
if (best) return { account: best, apiKey: null };
|
|
170
|
+
const key = launchApiKey();
|
|
171
|
+
if (key) return { account: null, apiKey: key };
|
|
172
|
+
const account = fallback();
|
|
173
|
+
return account ? { account, apiKey: null } : null;
|
|
157
174
|
}
|
|
158
175
|
|
|
159
176
|
export async function borrowClaudeSeat(pid: number): Promise<SeatBorrow> {
|
|
@@ -335,6 +352,7 @@ export const claude: Provider = {
|
|
|
335
352
|
seats: "shared",
|
|
336
353
|
waitsWhenDepleted: true,
|
|
337
354
|
statusOnly: false,
|
|
355
|
+
apiKeys: claudeApiKeys,
|
|
338
356
|
liveId,
|
|
339
357
|
presence,
|
|
340
358
|
gatedFamilies: (cfg) => {
|
package/src/lib/compact.ts
CHANGED
|
@@ -13,13 +13,12 @@ const PIPE_GRACE_MS = 2_000;
|
|
|
13
13
|
|
|
14
14
|
const CompactBoundarySchema = z.looseObject({ type: z.literal("system"), subtype: z.literal("compact_boundary") });
|
|
15
15
|
|
|
16
|
-
export async function compactClaudeSession(input: { real: string; sid: string; transcript: string; env: Record<string, string | undefined>; onSpawn: (child: Subprocess) => void }): Promise<CompactOutcome> {
|
|
16
|
+
export async function compactClaudeSession(input: { real: string; sid: string; transcript: string; env: Record<string, string | undefined>; keyPipe: boolean; onSpawn: (child: Subprocess) => void }): Promise<CompactOutcome> {
|
|
17
17
|
const offset = statSync(input.transcript).size;
|
|
18
|
+
const extra: "pipe"[] = input.keyPipe ? ["pipe"] : [];
|
|
18
19
|
const p = Bun.spawn([input.real, "-p", "--resume", input.sid, "/compact"], {
|
|
19
20
|
env: input.env,
|
|
20
|
-
|
|
21
|
-
stdout: "pipe",
|
|
22
|
-
stderr: "pipe",
|
|
21
|
+
stdio: ["ignore", "pipe", "pipe", ...extra],
|
|
23
22
|
timeout: CLAUDE_COMPACT_KILL_MS,
|
|
24
23
|
killSignal: "SIGKILL",
|
|
25
24
|
});
|