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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.133.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",
|
package/src/cli/key.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { keySessions, loadApiKeys, removeApiKeySecret, saveApiKeys, writeApiKeySecret } from "../lib/apikeys.ts";
|
|
3
|
+
import { withLock } from "../lib/lock.ts";
|
|
4
|
+
import { claudePool } from "../lib/paths.ts";
|
|
5
|
+
import { readStdin } from "../lib/proc.ts";
|
|
6
|
+
import type { ApiKey } from "../lib/types.ts";
|
|
7
|
+
import { c, emitError, plain } from "./render.ts";
|
|
8
|
+
|
|
9
|
+
const USAGE = "usage: tokenmaxxing key add <label> [credit-usd] [workspace-id] (the key on stdin) | key credit <label> <usd> | key rm <label>";
|
|
10
|
+
|
|
11
|
+
const UsdSchema = z.coerce.number().finite().min(0);
|
|
12
|
+
|
|
13
|
+
const WorkspaceIdSchema = z
|
|
14
|
+
.string()
|
|
15
|
+
.min(1)
|
|
16
|
+
.refine((id) => id.trim() === id && !id.includes("\n") && !id.includes("\r"), "a workspace id is one line with no surrounding whitespace");
|
|
17
|
+
|
|
18
|
+
export function usd(amount: number): string {
|
|
19
|
+
return `$${amount.toFixed(2)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function findKey(keys: ApiKey[], label: string): ApiKey | undefined {
|
|
23
|
+
return keys.find((k) => k.label.toLowerCase() === label.toLowerCase());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseUsd(raw: string): number | null {
|
|
27
|
+
const parsed = UsdSchema.safeParse(raw);
|
|
28
|
+
if (parsed.success) return parsed.data;
|
|
29
|
+
emitError({ message: `"${raw}" is not a dollar amount (a number at or above 0)` });
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function add(args: string[]): Promise<number> {
|
|
34
|
+
const [label, creditRaw, workspaceRaw] = args;
|
|
35
|
+
if (!label) {
|
|
36
|
+
emitError({ message: USAGE, paint: plain });
|
|
37
|
+
return 2;
|
|
38
|
+
}
|
|
39
|
+
const credit = creditRaw == null ? undefined : parseUsd(creditRaw);
|
|
40
|
+
if (credit === null) return 2;
|
|
41
|
+
const workspace = workspaceRaw == null ? undefined : WorkspaceIdSchema.safeParse(workspaceRaw);
|
|
42
|
+
if (workspace != null && !workspace.success) {
|
|
43
|
+
emitError({ message: `"${workspaceRaw}" is not a workspace id: ${workspace.error.issues[0]?.message ?? "invalid"}` });
|
|
44
|
+
return 2;
|
|
45
|
+
}
|
|
46
|
+
const workspaceId = workspace?.data;
|
|
47
|
+
if (process.stdin.isTTY) {
|
|
48
|
+
emitError({ message: "refusing to read the API key from a terminal, which would echo it - pipe it in: read -rs KEY && printf '%s' \"$KEY\" | tokenmaxxing key add <label> ...; unset KEY" });
|
|
49
|
+
return 2;
|
|
50
|
+
}
|
|
51
|
+
const secret = (await readStdin()).trim();
|
|
52
|
+
if (secret === "") {
|
|
53
|
+
emitError({ message: "no API key on stdin - pipe the key in, for example from your password manager" });
|
|
54
|
+
return 2;
|
|
55
|
+
}
|
|
56
|
+
if (secret.includes("\n") || secret.includes("\r")) {
|
|
57
|
+
emitError({ message: "stdin holds more than one line - pass one API key per `key add`" });
|
|
58
|
+
return 2;
|
|
59
|
+
}
|
|
60
|
+
if (secret.startsWith("sk-ant-admin")) {
|
|
61
|
+
emitError({ message: "that is an Admin API key, which cannot run inference - create a regular API key in the Console" });
|
|
62
|
+
return 2;
|
|
63
|
+
}
|
|
64
|
+
return withLock(claudePool.lockFile, () => {
|
|
65
|
+
const idx = loadApiKeys();
|
|
66
|
+
const taken = findKey(idx.keys, label);
|
|
67
|
+
if (taken) {
|
|
68
|
+
emitError({ message: `label "${label}" is already used by another API key - labels must be unique` });
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
let id = crypto.randomUUID().slice(0, 8);
|
|
72
|
+
while (idx.keys.some((k) => k.id === id)) id = crypto.randomUUID().slice(0, 8);
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
writeApiKeySecret(id, secret);
|
|
75
|
+
idx.keys.push({
|
|
76
|
+
id,
|
|
77
|
+
label,
|
|
78
|
+
...(workspaceId != null ? { workspaceId } : {}),
|
|
79
|
+
...(credit != null ? { creditUsd: credit } : {}),
|
|
80
|
+
spentUsd: 0,
|
|
81
|
+
creditSetAt: now,
|
|
82
|
+
addedAt: now,
|
|
83
|
+
});
|
|
84
|
+
saveApiKeys(idx);
|
|
85
|
+
console.log(`added API key ${c.bold(label)} (${credit != null ? `credit ${usd(credit)}` : "credit unknown, ranks after keys with a balance"}${workspaceId != null ? `, workspace ${workspaceId}` : ""})`);
|
|
86
|
+
return 0;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function credit(args: string[]): Promise<number> {
|
|
91
|
+
const [label, amountRaw] = args;
|
|
92
|
+
if (!label || amountRaw == null || args.length > 2) {
|
|
93
|
+
emitError({ message: USAGE, paint: plain });
|
|
94
|
+
return 2;
|
|
95
|
+
}
|
|
96
|
+
const amount = parseUsd(amountRaw);
|
|
97
|
+
if (amount === null) return 2;
|
|
98
|
+
return withLock(claudePool.lockFile, () => {
|
|
99
|
+
const idx = loadApiKeys();
|
|
100
|
+
const key = findKey(idx.keys, label);
|
|
101
|
+
if (!key) {
|
|
102
|
+
emitError({ message: `no API key is labeled "${label}"` });
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
key.creditUsd = amount;
|
|
106
|
+
key.spentUsd = 0;
|
|
107
|
+
key.creditSetAt = Date.now();
|
|
108
|
+
key.refusedAt = undefined;
|
|
109
|
+
saveApiKeys(idx);
|
|
110
|
+
console.log(`set ${c.bold(key.label)} credit to ${usd(amount)}`);
|
|
111
|
+
return 0;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function rm(args: string[]): Promise<number> {
|
|
116
|
+
const [label] = args;
|
|
117
|
+
if (!label || args.length > 1) {
|
|
118
|
+
emitError({ message: USAGE, paint: plain });
|
|
119
|
+
return 2;
|
|
120
|
+
}
|
|
121
|
+
return withLock(claudePool.lockFile, () => {
|
|
122
|
+
const idx = loadApiKeys();
|
|
123
|
+
const key = findKey(idx.keys, label);
|
|
124
|
+
if (!key) {
|
|
125
|
+
emitError({ message: `no API key is labeled "${label}"` });
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
if (keySessions().has(key.id)) {
|
|
129
|
+
emitError({ message: `${key.label} is running in a live session - close that session before removing the key.` });
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
removeApiKeySecret(key.id);
|
|
133
|
+
idx.keys = idx.keys.filter((k) => k.id !== key.id);
|
|
134
|
+
saveApiKeys(idx);
|
|
135
|
+
console.log(`removed API key ${c.bold(key.label)} (${idx.keys.length} left)`);
|
|
136
|
+
return 0;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function cmdKey(args: string[]): Promise<number> {
|
|
141
|
+
const [action, ...rest] = args;
|
|
142
|
+
if (action === "add") return add(rest);
|
|
143
|
+
if (action === "credit") return credit(rest);
|
|
144
|
+
if (action === "rm") return rm(rest);
|
|
145
|
+
emitError({ message: USAGE, paint: plain });
|
|
146
|
+
return 2;
|
|
147
|
+
}
|
package/src/cli/status.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { chunk, sortBy } from "es-toolkit";
|
|
2
|
+
import { creditLeft, keySessions, loadApiKeys } from "../lib/apikeys.ts";
|
|
2
3
|
import { claude } from "../lib/claude.ts";
|
|
3
4
|
import { codex } from "../lib/codex.ts";
|
|
4
5
|
import { grok } from "../lib/grok.ts";
|
|
@@ -7,6 +8,7 @@ import { withLock } from "../lib/lock.ts";
|
|
|
7
8
|
import { codexPool, grokPool } from "../lib/paths.ts";
|
|
8
9
|
import { barFor, bufferedUsed, earliestReset, gatedWindows, landGrant, landWindows, isExhausted, isSessionWindow, limitWindows, liveUsed, nextWeeklyReset, sessionWindow, thresholdBars, weeklyWindow } from "../lib/picker.ts";
|
|
9
10
|
import type { Provider, SampleReport } from "../lib/provider.ts";
|
|
11
|
+
import { usd } from "./key.ts";
|
|
10
12
|
import { bar, c, count, emitJson, fmtAgo } from "./render.ts";
|
|
11
13
|
import type { Account, AccountThresholds, Bars, Config, Thresholds, Window } from "../lib/types.ts";
|
|
12
14
|
|
|
@@ -38,7 +40,48 @@ type PoolReport = {
|
|
|
38
40
|
accounts: StatusAccount[];
|
|
39
41
|
};
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
type StatusApiKey = {
|
|
44
|
+
label: string;
|
|
45
|
+
id: string;
|
|
46
|
+
workspaceId: string | null;
|
|
47
|
+
creditUsd: number | null;
|
|
48
|
+
spentUsd: number;
|
|
49
|
+
creditLeftUsd: number | null;
|
|
50
|
+
refused: boolean;
|
|
51
|
+
refusedAt: number | null;
|
|
52
|
+
sessions: number;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type StatusReport = { now: number; claude: PoolReport; codex: PoolReport; grok: PoolReport; apiKeys?: StatusApiKey[] };
|
|
56
|
+
|
|
57
|
+
function apiKeyReport(): StatusApiKey[] {
|
|
58
|
+
const sessions = keySessions();
|
|
59
|
+
return loadApiKeys().keys.map((k) => ({
|
|
60
|
+
label: k.label,
|
|
61
|
+
id: k.id,
|
|
62
|
+
workspaceId: k.workspaceId ?? null,
|
|
63
|
+
creditUsd: k.creditUsd ?? null,
|
|
64
|
+
spentUsd: k.spentUsd,
|
|
65
|
+
creditLeftUsd: creditLeft(k),
|
|
66
|
+
refused: k.refusedAt != null,
|
|
67
|
+
refusedAt: k.refusedAt ?? null,
|
|
68
|
+
sessions: sessions.get(k.id) ?? 0,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function renderApiKeys(keys: StatusApiKey[]): void {
|
|
73
|
+
console.log(c.dim(`api keys (${count({ n: keys.length, noun: "key" })}, used only while every claude account is at its limit)`));
|
|
74
|
+
console.log();
|
|
75
|
+
for (const k of keys) {
|
|
76
|
+
const badges: string[] = [];
|
|
77
|
+
if (k.sessions > 0) badges.push(c.green(count({ n: k.sessions, noun: "session" })));
|
|
78
|
+
if (k.refused) badges.push(c.red("refused"));
|
|
79
|
+
else if (k.creditLeftUsd != null && k.creditLeftUsd <= 0) badges.push(c.yellow("spent"));
|
|
80
|
+
const credit = k.creditLeftUsd == null ? c.dim("credit unknown") : `credit left ${usd(k.creditLeftUsd)}`;
|
|
81
|
+
console.log(`${k.sessions > 0 ? c.green("●") : c.dim("○")} ${c.bold(k.label)} ${credit}${badges.length ? ` ${badges.join(" ")}` : ""}`);
|
|
82
|
+
}
|
|
83
|
+
console.log();
|
|
84
|
+
}
|
|
42
85
|
|
|
43
86
|
function currentWindow(a: Account, w: Window, bars: Bars | null, now: number): WindowReport {
|
|
44
87
|
const passed = w.resetsAt != null && w.resetsAt <= now;
|
|
@@ -259,11 +302,12 @@ export async function cmdStatus(opts: { json?: boolean; cached?: boolean } = {})
|
|
|
259
302
|
const cfg = loadConfig();
|
|
260
303
|
const now = Date.now();
|
|
261
304
|
const claudeReport = await collect(claude, cfg, now, cached);
|
|
305
|
+
const apiKeys = apiKeyReport();
|
|
262
306
|
if (!json) {
|
|
263
307
|
const codexPooled = loadAccounts(codexPool).accounts.length > 0;
|
|
264
308
|
const grokPooled = loadAccounts(grokPool).accounts.length > 0;
|
|
265
309
|
if (claudeReport.accounts.length === 0) {
|
|
266
|
-
if (!codexPooled && !grokPooled) {
|
|
310
|
+
if (!codexPooled && !grokPooled && apiKeys.length === 0) {
|
|
267
311
|
console.log(c.dim("no accounts yet, run `tokenmaxxing init` (or `tokenmaxxing init --codex`, `--grok`)"));
|
|
268
312
|
return 0;
|
|
269
313
|
}
|
|
@@ -273,11 +317,12 @@ export async function cmdStatus(opts: { json?: boolean; cached?: boolean } = {})
|
|
|
273
317
|
const header = `thresholds 5h ${claudeReport.thresholds.session}% weekly ${claudeReport.thresholds.weekly}% bars 5h ${claudeReport.bars.session}% weekly ${claudeReport.bars.weekly}% (${count({ n: claudeReport.accounts.length, noun: "claude account" })})`;
|
|
274
318
|
renderPool(claude, claudeReport, header, Date.now(), cfg.policy.usagePollTtlMs);
|
|
275
319
|
}
|
|
320
|
+
if (apiKeys.length > 0) renderApiKeys(apiKeys);
|
|
276
321
|
}
|
|
277
322
|
const codexReport = await collect(codex, cfg, now, cached);
|
|
278
323
|
const grokReport = await collect(grok, cfg, now, cached);
|
|
279
324
|
if (json) {
|
|
280
|
-
const report: StatusReport = { now, claude: claudeReport, codex: codexReport, grok: grokReport };
|
|
325
|
+
const report: StatusReport = { now, claude: claudeReport, codex: codexReport, grok: grokReport, ...(apiKeys.length > 0 ? { apiKeys } : {}) };
|
|
281
326
|
emitJson({ ok: true, ...report });
|
|
282
327
|
return 0;
|
|
283
328
|
}
|
|
@@ -10,7 +10,7 @@ import { codexStoreUsable, ensureCodexStoreHome } from "../lib/codexauth.ts";
|
|
|
10
10
|
import { CODEX_CRED_ENV, CODEX_SUPERVISOR_ID_ENV, borrowCodexSeat, codexPickCtx, pickCodexSeat } from "../lib/codex.ts";
|
|
11
11
|
import { clearPresence, livingPresences } from "../lib/presence.ts";
|
|
12
12
|
import { isExhausted } from "../lib/picker.ts";
|
|
13
|
-
import { exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough } from "../lib/supervise.ts";
|
|
13
|
+
import { childStdout, exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, relayStdout, runPassthrough } from "../lib/supervise.ts";
|
|
14
14
|
import { saveTermios } from "../lib/tty.ts";
|
|
15
15
|
import { loadAccounts, readJsonFile } from "../lib/state.ts";
|
|
16
16
|
import { CodexRespawnMarkerSchema, type Account } from "../lib/types.ts";
|
|
@@ -137,14 +137,14 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
|
|
|
137
137
|
if (existsSync(marker)) rmSync(marker, { force: true });
|
|
138
138
|
|
|
139
139
|
const launchedAt = Date.now();
|
|
140
|
-
const { child, seat } = await withLock(codexPool.lockFile, async () => {
|
|
140
|
+
const { child, seat, drain } = await withLock(codexPool.lockFile, async () => {
|
|
141
141
|
clearPresence({ dir: codexPaths.presenceDir, id: supervisorId });
|
|
142
142
|
const picked = validWanted({ wanted, now: launchedAt, supervisorId }) ?? pickCodexSeat(launchedAt);
|
|
143
143
|
log("codexsupervisor.launch", { supervisorId: supervisorId.slice(0, 8), respawns, seat: picked?.id.slice(0, 8) ?? null, args: launchArgs.join(" ") });
|
|
144
144
|
const store = picked ? ensureCodexStoreHome(picked.id) : undefined;
|
|
145
145
|
const spawned = Bun.spawn([real, ...launchArgs], {
|
|
146
146
|
stdin: "inherit",
|
|
147
|
-
stdout:
|
|
147
|
+
stdout: childStdout(),
|
|
148
148
|
stderr: "inherit",
|
|
149
149
|
env: {
|
|
150
150
|
...(store ? omit(childEnv, CODEX_CRED_ENV) : childEnv),
|
|
@@ -152,6 +152,7 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
|
|
|
152
152
|
...(store ? { CODEX_HOME: store } : {}),
|
|
153
153
|
},
|
|
154
154
|
});
|
|
155
|
+
const drain = spawned.stdout instanceof ReadableStream ? relayStdout(spawned.stdout) : null;
|
|
155
156
|
if (picked) {
|
|
156
157
|
await recordPresenceOrStop({
|
|
157
158
|
child: spawned,
|
|
@@ -163,10 +164,10 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
|
|
|
163
164
|
savedTermios,
|
|
164
165
|
});
|
|
165
166
|
}
|
|
166
|
-
return { child: spawned, seat: picked };
|
|
167
|
+
return { child: spawned, seat: picked, drain };
|
|
167
168
|
});
|
|
168
169
|
if (respawns > 0) {
|
|
169
|
-
process.
|
|
170
|
+
process.stderr.write(`\n\x1b[36m↻ tokenmaxxing: switched codex to ${seat?.label ?? "the ambient codex login"} - resuming...\x1b[0m\n`);
|
|
170
171
|
}
|
|
171
172
|
|
|
172
173
|
await raceMarkerOrExit({
|
|
@@ -178,6 +179,7 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
|
|
|
178
179
|
},
|
|
179
180
|
savedTermios,
|
|
180
181
|
});
|
|
182
|
+
await drain?.();
|
|
181
183
|
|
|
182
184
|
const payload = existsSync(marker) ? readCodexMarker(marker) : null;
|
|
183
185
|
if (payload) {
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { sortBy } from "es-toolkit";
|
|
3
|
+
import { writeSessionCost } from "../lib/apikeys.ts";
|
|
2
4
|
import { claudePool, seatFromEnv } from "../lib/paths.ts";
|
|
5
|
+
import { supervisedSession } from "../lib/sessions.ts";
|
|
3
6
|
import { errorMessage } from "../lib/log.ts";
|
|
4
7
|
import { loadAccounts, loadConfig, writeUsage } from "../lib/state.ts";
|
|
5
8
|
import { familyTokens, matchedFamily, parseStatusLineStdin, parseStatusLineModel } from "../lib/usage.ts";
|
|
@@ -8,6 +11,7 @@ import { readStdin } from "../lib/proc.ts";
|
|
|
8
11
|
import { worktreeName } from "../lib/worktree.ts";
|
|
9
12
|
import { fmtResetShort, makeColors, makeUsagePaint, statuslineColor } from "../cli/render.ts";
|
|
10
13
|
import {
|
|
14
|
+
CostLineSchema,
|
|
11
15
|
JsonTextSchema,
|
|
12
16
|
StatusLineStdinSchema,
|
|
13
17
|
type Account,
|
|
@@ -127,6 +131,12 @@ export async function runStatusline(): Promise<number> {
|
|
|
127
131
|
}
|
|
128
132
|
} catch {
|
|
129
133
|
}
|
|
134
|
+
try {
|
|
135
|
+
const session = existsSync(claudePool.apiKeysJson) ? supervisedSession() : null;
|
|
136
|
+
const cost = session == null ? null : (CostLineSchema.safeParse(obj).data ?? null);
|
|
137
|
+
if (session != null && cost != null && cost.sessionId === session.live) writeSessionCost(session.sid, cost);
|
|
138
|
+
} catch {
|
|
139
|
+
}
|
|
130
140
|
|
|
131
141
|
let line: string;
|
|
132
142
|
try {
|
|
@@ -2,9 +2,10 @@ import { existsSync, readdirSync } from "node:fs";
|
|
|
2
2
|
import { basename, dirname, join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { claude } from "../lib/claude.ts";
|
|
5
|
-
import {
|
|
5
|
+
import { liveApiKeyId, refuseApiKey } from "../lib/apikeys.ts";
|
|
6
|
+
import { evaluateAndMaybeSwap, moveTarget } from "../lib/decide.ts";
|
|
6
7
|
import { readStdin } from "../lib/proc.ts";
|
|
7
|
-
import { adoptLiveSession, refusedAccounts, supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
8
|
+
import { adoptLiveSession, keyMovesSupported, refusedAccounts, supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
8
9
|
import { loadConfig } from "../lib/state.ts";
|
|
9
10
|
import { classifyEnforcedLimit, ENFORCED_ERRORS, findEnforcedRow, parseErrorBody, readTranscriptTail, type TranscriptRow } from "../lib/usage.ts";
|
|
10
11
|
import { paths } from "../lib/paths.ts";
|
|
@@ -60,6 +61,22 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
60
61
|
return 0;
|
|
61
62
|
}
|
|
62
63
|
const canRespawn = session != null && mainLoop;
|
|
64
|
+
const keyId = account == null ? liveApiKeyId() : null;
|
|
65
|
+
if (keyId != null) {
|
|
66
|
+
if (error !== "billing_error") return 0;
|
|
67
|
+
const known = await refuseApiKey(keyId, now);
|
|
68
|
+
log("stopfailure.key_refused", { key: keyId, known, subagent: !mainLoop });
|
|
69
|
+
const decision = await evaluateAndMaybeSwap(claude, now, canRespawn, null, { waiterId: canRespawn ? session?.sid : undefined, refused: refusedAccounts(), apiKeys: true, apiKeyId: keyId });
|
|
70
|
+
const target = moveTarget(decision);
|
|
71
|
+
if (session && canRespawn && target) {
|
|
72
|
+
writeRespawnMarker({ session, ...target, waitUntil: target.waitUntil ?? now, compact: false, origin: "stopfailure" });
|
|
73
|
+
log("stopfailure.marker", { session: session.sid.slice(0, 8), live: session.live.slice(0, 8), account: target.accountId.slice(0, 8), key: target.apiKeyId, waitUntil: target.waitUntil ?? now });
|
|
74
|
+
} else {
|
|
75
|
+
log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
|
|
76
|
+
}
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
if (error === "billing_error") return 0;
|
|
63
80
|
const cfg = loadConfig();
|
|
64
81
|
const row = stdin.transcript_path
|
|
65
82
|
? await awaitEnforcedRow({ transcriptPath: stdin.transcript_path, agentId: stdin.agent_id, error, lastAssistantMessage: stdin.last_assistant_message })
|
|
@@ -92,10 +109,11 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
92
109
|
}
|
|
93
110
|
|
|
94
111
|
const refused = enforced?.kind === "credits" ? [...new Set([...refusedAccounts(), enforced.account])] : undefined;
|
|
95
|
-
const decision = await evaluateAndMaybeSwap(claude, now, canRespawn, enforced, { waiterId: canRespawn ? session?.sid : undefined, exclude: refused });
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
const decision = await evaluateAndMaybeSwap(claude, now, canRespawn, enforced, { waiterId: canRespawn ? session?.sid : undefined, exclude: refused, apiKeys: canRespawn && keyMovesSupported() });
|
|
113
|
+
const target = moveTarget(decision);
|
|
114
|
+
if (session && canRespawn && target) {
|
|
115
|
+
writeRespawnMarker({ session, ...target, waitUntil: target.waitUntil ?? now, compact: false, origin: "stopfailure", refused });
|
|
116
|
+
log("stopfailure.marker", { session: session.sid.slice(0, 8), live: session.live.slice(0, 8), account: target.accountId.slice(0, 8), key: target.apiKeyId, waitUntil: target.waitUntil ?? now });
|
|
99
117
|
} else {
|
|
100
118
|
log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
|
|
101
119
|
}
|
package/src/entries/stophook.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { claude } from "../lib/claude.ts";
|
|
3
|
-
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
3
|
+
import { evaluateAndMaybeSwap, moveTarget } from "../lib/decide.ts";
|
|
4
4
|
import { readStdin } from "../lib/proc.ts";
|
|
5
|
-
import { adoptLiveSession, refusedAccounts, supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
5
|
+
import { adoptLiveSession, keyMovesSupported, refusedAccounts, supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
6
6
|
import { JsonTextSchema } from "../lib/types.ts";
|
|
7
7
|
import { errorMessage, log } from "../lib/log.ts";
|
|
8
8
|
|
|
@@ -24,10 +24,11 @@ export async function runBoundaryHook(event: "stop" | "sessionstart"): Promise<n
|
|
|
24
24
|
log(`${event}.nested_session`, { stdin: stdin.session_id?.slice(0, 8), supervised: supervised.live.slice(0, 8), source: stdin.source });
|
|
25
25
|
return 0;
|
|
26
26
|
}
|
|
27
|
-
const decision = await evaluateAndMaybeSwap(claude, Date.now(), session != null, null, { waiterId: session?.sid, refused: refusedAccounts() });
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
const decision = await evaluateAndMaybeSwap(claude, Date.now(), session != null, null, { waiterId: session?.sid, refused: refusedAccounts(), apiKeys: session != null && keyMovesSupported() });
|
|
28
|
+
const target = moveTarget(decision);
|
|
29
|
+
if (session && target) {
|
|
30
|
+
writeRespawnMarker({ session, ...target, waitUntil: target.waitUntil ?? Date.now(), compact: true, origin: event });
|
|
31
|
+
log(`${event}.${target.waitUntil !== undefined ? "wait" : "move"}`, { source: stdin.source, account: target.accountId.slice(0, 8), key: target.apiKeyId, waitUntil: target.waitUntil, session: session.sid.slice(0, 8), live: session.live.slice(0, 8) });
|
|
31
32
|
}
|
|
32
33
|
} catch (e) {
|
|
33
34
|
log(`${event}.error`, { err: errorMessage(e) });
|