tokenmaxxing 1.39.0 → 1.41.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 +70 -62
- package/README.md +31 -31
- package/package.json +2 -2
- package/src/cli/auth.ts +14 -10
- package/src/cli/check.ts +2 -2
- package/src/cli/doctor.ts +19 -32
- package/src/cli/init.ts +2 -2
- package/src/cli/rm.ts +3 -12
- package/src/cli/setuptoken.ts +2 -2
- package/src/cli/status.ts +8 -4
- package/src/cli/switch.ts +12 -71
- package/src/entries/codexstophook.ts +2 -2
- package/src/entries/codexsupervisor.ts +3 -3
- package/src/entries/sessionstart.ts +9 -10
- package/src/entries/statusline.ts +5 -11
- package/src/entries/stopfailurehook.ts +12 -38
- package/src/entries/stophook.ts +8 -28
- package/src/entries/supervisor.ts +50 -10
- package/src/lib/claude.ts +130 -154
- package/src/lib/claudelock.ts +2 -3
- package/src/lib/codex.ts +19 -14
- package/src/lib/credstore.ts +10 -21
- package/src/lib/decide.ts +24 -35
- package/src/lib/oauth.ts +2 -68
- package/src/lib/paths.ts +23 -13
- package/src/lib/picker.ts +8 -0
- package/src/lib/presence.ts +67 -0
- package/src/lib/provider.ts +3 -4
- package/src/lib/sample.ts +66 -259
- package/src/lib/sessions.ts +25 -0
- package/src/lib/state.ts +17 -42
- package/src/lib/types.ts +2 -11
- package/src/lib/usage.ts +7 -4
- package/src/main.ts +11 -18
- package/src/lib/claudejson.ts +0 -30
- package/src/lib/codexpresence.ts +0 -64
- package/src/lib/swap.ts +0 -198
package/src/cli/check.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { claude } from "../lib/claude.ts";
|
|
2
2
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { sampleOldest } from "../lib/sample.ts";
|
|
4
4
|
import { loadConfig } from "../lib/state.ts";
|
|
5
5
|
import { maybeAutoUpdate } from "../lib/update.ts";
|
|
6
6
|
import { log } from "../lib/log.ts";
|
|
@@ -11,7 +11,7 @@ export async function cmdCheck(json = false): Promise<number> {
|
|
|
11
11
|
let d;
|
|
12
12
|
try {
|
|
13
13
|
d = await evaluateAndMaybeSwap(claude, now);
|
|
14
|
-
await
|
|
14
|
+
await sampleOldest(loadConfig());
|
|
15
15
|
await maybeAutoUpdate();
|
|
16
16
|
} catch (e) {
|
|
17
17
|
const detail = e instanceof Error ? e.message : String(e);
|
package/src/cli/doctor.ts
CHANGED
|
@@ -2,20 +2,13 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { verifyRealClaude } from "../lib/claudebin.ts";
|
|
3
3
|
import { checkSettings, installedBin } from "../lib/settings.ts";
|
|
4
4
|
import { checkTimerHealthy, findClaudeShadowers, isBinDirAhead, shellRcPath, timerActivationHint } from "../lib/install.ts";
|
|
5
|
-
import { claudePool,
|
|
5
|
+
import { claudePool, paths } from "../lib/paths.ts";
|
|
6
6
|
import { loadAccounts, loadConfig } from "../lib/state.ts";
|
|
7
|
-
import {
|
|
8
|
-
import { isAccessTokenExpiring, fetchTokenIdentity, describeIdentity } from "../lib/oauth.ts";
|
|
9
|
-
import { CredentialBlobSchema, type TokenIdentity } from "../lib/types.ts";
|
|
7
|
+
import { readStore } from "../lib/credstore.ts";
|
|
8
|
+
import { isAccessTokenExpiring, isDeadCredential, fetchTokenIdentity, describeIdentity } from "../lib/oauth.ts";
|
|
10
9
|
import { SETUP_TOKEN_STALE_MS, loadSetupTokens } from "../lib/setuptokens.ts";
|
|
11
10
|
import { c, fmtAgo } from "./render.ts";
|
|
12
11
|
|
|
13
|
-
async function blobIdentity(raw: string): Promise<TokenIdentity | null> {
|
|
14
|
-
const creds = CredentialBlobSchema.parse(JSON.parse(raw)).claudeAiOauth;
|
|
15
|
-
if (isAccessTokenExpiring(creds)) return null;
|
|
16
|
-
return fetchTokenIdentity(creds.accessToken);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
12
|
export async function cmdDoctor(): Promise<number> {
|
|
20
13
|
let failed = 0;
|
|
21
14
|
const check = (cond: boolean, label: string, hint?: string) => {
|
|
@@ -39,32 +32,26 @@ export async function cmdDoctor(): Promise<number> {
|
|
|
39
32
|
|
|
40
33
|
const idx = loadAccounts(claudePool);
|
|
41
34
|
check(idx.accounts.length > 0, "at least one account in the pool", "run `tokenmaxxing init`");
|
|
42
|
-
check(!!idx.activeId, "an active account is set");
|
|
43
35
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const active = idx.accounts.find((a) => a.id === idx.activeId);
|
|
48
|
-
if (live && active) {
|
|
36
|
+
for (const a of idx.accounts) {
|
|
37
|
+
let creds;
|
|
49
38
|
try {
|
|
50
|
-
|
|
51
|
-
if (identity) check(identity.accountUuid === active.id, `live credential identity matches active (${active.label})`, `token belongs to ${describeIdentity(identity)} - run \`tokenmaxxing switch\``);
|
|
52
|
-
else note("live credential identity unverifiable (access token expired)");
|
|
39
|
+
creds = await readStore(a.id);
|
|
53
40
|
} catch (e) {
|
|
54
|
-
check(false, `
|
|
41
|
+
check(false, `store credential readable for ${a.label}`, (e instanceof Error ? e.message : String(e)).slice(0, 100));
|
|
42
|
+
continue;
|
|
55
43
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
check(false, `parked credential identity matches ${a.label}`, (e instanceof Error ? e.message : String(e)).slice(0, 100));
|
|
44
|
+
check(creds != null, `store credential present for ${a.label}`, `run \`tokenmaxxing auth ${a.label}\``);
|
|
45
|
+
if (creds) {
|
|
46
|
+
if (isDeadCredential(creds)) check(false, `${a.label}'s store credential is usable`, `cleared after a failed refresh - run \`tokenmaxxing auth ${a.label}\``);
|
|
47
|
+
else if (isAccessTokenExpiring(creds)) note(`${a.label} identity unverifiable (access token expired)`);
|
|
48
|
+
else {
|
|
49
|
+
try {
|
|
50
|
+
const identity = await fetchTokenIdentity(creds.accessToken);
|
|
51
|
+
check(identity.accountUuid === a.id, `store credential identity matches ${a.label}`, `token belongs to ${describeIdentity(identity)} - run \`tokenmaxxing auth ${a.label}\``);
|
|
52
|
+
} catch (e) {
|
|
53
|
+
check(false, `store credential identity matches ${a.label}`, (e instanceof Error ? e.message : String(e)).slice(0, 100));
|
|
54
|
+
}
|
|
68
55
|
}
|
|
69
56
|
}
|
|
70
57
|
if (a.needsReauth) check(false, `${a.label} needs re-auth`, `run \`tokenmaxxing auth ${a.label}\` to re-login`);
|
package/src/cli/init.ts
CHANGED
|
@@ -25,7 +25,7 @@ export async function cmdInit(p: Provider): Promise<number> {
|
|
|
25
25
|
p.install();
|
|
26
26
|
const active = existing.accounts.find((a) => a.id === existing.activeId);
|
|
27
27
|
console.log(`${c.green("✓")} re-installed (pool already has ${count({ n: existing.accounts.length, noun: "account" })} - not re-importing)`);
|
|
28
|
-
console.log(` active: ${c.bold(active
|
|
28
|
+
if (active && p.seats === "live") console.log(` active: ${c.bold(active.label)}`);
|
|
29
29
|
printUsage(p);
|
|
30
30
|
return 0;
|
|
31
31
|
}
|
|
@@ -37,7 +37,7 @@ export async function cmdInit(p: Provider): Promise<number> {
|
|
|
37
37
|
await harvested.park();
|
|
38
38
|
const idx = loadAccounts(p.pool);
|
|
39
39
|
const imported = upsertAccount(idx, harvested, p.mergeWindows);
|
|
40
|
-
idx.activeId = harvested.id;
|
|
40
|
+
idx.activeId = p.seats === "live" ? harvested.id : null;
|
|
41
41
|
saveAccounts(p.pool, idx);
|
|
42
42
|
return imported;
|
|
43
43
|
});
|
package/src/cli/rm.ts
CHANGED
|
@@ -20,20 +20,11 @@ export async function cmdRm(p: Provider, selector?: string): Promise<number> {
|
|
|
20
20
|
emitError({ message: `${a.label} is the ACTIVE account - switch away before removing it.` });
|
|
21
21
|
return 1;
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
liveOwner = await p.liveOwner();
|
|
26
|
-
} catch (e) {
|
|
27
|
-
emitError({
|
|
28
|
-
message: `cannot verify which account the LIVE credential belongs to (${e instanceof Error ? e.message : String(e)}) - refusing to remove while the live owner is unknown; repair the live credential or retry once the profile endpoint is reachable.`,
|
|
29
|
-
});
|
|
23
|
+
if (p.liveId() === a.id) {
|
|
24
|
+
emitError({ message: `${a.label} is the ${p.name} login this command runs under - remove it from a session on another account.` });
|
|
30
25
|
return 1;
|
|
31
26
|
}
|
|
32
|
-
if (
|
|
33
|
-
emitError({ message: `${a.label}'s credential is currently LIVE (the active label is stale - a manual login drifted it); run \`tokenmaxxing switch${p.flag}\` to move off it first.` });
|
|
34
|
-
return 1;
|
|
35
|
-
}
|
|
36
|
-
if (p.presentIds().has(a.id)) {
|
|
27
|
+
if (p.presence().has(a.id)) {
|
|
37
28
|
emitError({ message: `${a.label} is running in a live ${p.name} session - close that session before removing it.` });
|
|
38
29
|
return 1;
|
|
39
30
|
}
|
package/src/cli/setuptoken.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdirSync, rmSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { resolveRealClaude } from "../lib/claudebin.ts";
|
|
4
4
|
import { withLock } from "../lib/lock.ts";
|
|
5
|
-
import { claudePool,
|
|
5
|
+
import { claudePool, paths, shortId } from "../lib/paths.ts";
|
|
6
6
|
import { loadAccounts } from "../lib/state.ts";
|
|
7
7
|
import { scrubCredentialEnv } from "../lib/usage.ts";
|
|
8
8
|
import { CURSOR_SECRET_VALUE_CAP_BYTES, cursorSecretValue, loadSetupTokens, saveSetupTokens, type SetupToken } from "../lib/setuptokens.ts";
|
|
@@ -102,7 +102,7 @@ export async function cmdSetupToken(args: string[]): Promise<number> {
|
|
|
102
102
|
for (const a of missing) {
|
|
103
103
|
console.log();
|
|
104
104
|
console.log(`${c.bold(a.label)} - sign into this account in the browser (${a.email ?? a.label})`);
|
|
105
|
-
const token = await mint({ real, item:
|
|
105
|
+
const token = await mint({ real, item: shortId(a.id) });
|
|
106
106
|
if (!token) return 1;
|
|
107
107
|
const stored = await withLock(claudePool.lockFile, async () => {
|
|
108
108
|
if (!loadAccounts(claudePool).accounts.some((x) => x.id === a.id)) return false;
|
package/src/cli/status.ts
CHANGED
|
@@ -28,6 +28,7 @@ const StatusAccountSchema = z.object({
|
|
|
28
28
|
id: z.string(),
|
|
29
29
|
tier: z.string().nullable(),
|
|
30
30
|
active: z.boolean(),
|
|
31
|
+
sessions: z.number(),
|
|
31
32
|
needsReauth: z.boolean(),
|
|
32
33
|
exhausted: z.boolean(),
|
|
33
34
|
usage: z
|
|
@@ -77,7 +78,7 @@ function usageReport(a: Account, now: number): StatusAccount["usage"] {
|
|
|
77
78
|
async function collect(p: Provider, cfg: Config, now: number, cached: boolean): Promise<PoolReport> {
|
|
78
79
|
let idx = loadAccounts(p.pool);
|
|
79
80
|
let reports = new Map<string, SampleReport>();
|
|
80
|
-
let liveId: string | null = idx.activeId;
|
|
81
|
+
let liveId: string | null = p.seats === "live" ? idx.activeId : null;
|
|
81
82
|
if (!cached) {
|
|
82
83
|
liveId = null;
|
|
83
84
|
if (idx.accounts.length > 0) {
|
|
@@ -92,7 +93,8 @@ async function collect(p: Provider, cfg: Config, now: number, cached: boolean):
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
const bars = thresholdBars(cfg);
|
|
95
|
-
const
|
|
96
|
+
const present = p.presence();
|
|
97
|
+
const ctx = { now, thresholds: bars, currentId: idx.activeId, families: p.gatedFamilies(cfg), seats: null };
|
|
96
98
|
const ordered = sortBy(idx.accounts, [(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, now)]);
|
|
97
99
|
const accounts = ordered.map((a): StatusAccount => {
|
|
98
100
|
const limits = limitWindows(a);
|
|
@@ -101,7 +103,8 @@ async function collect(p: Provider, cfg: Config, now: number, cached: boolean):
|
|
|
101
103
|
email: a.email,
|
|
102
104
|
id: a.id,
|
|
103
105
|
tier: a.tier,
|
|
104
|
-
active: liveId != null && a.id === liveId,
|
|
106
|
+
active: (liveId != null && a.id === liveId) || present.has(a.id),
|
|
107
|
+
sessions: present.get(a.id) ?? 0,
|
|
105
108
|
needsReauth: a.needsReauth === true,
|
|
106
109
|
exhausted: isExhausted(a, ctx),
|
|
107
110
|
usage: usageReport(a, now),
|
|
@@ -190,10 +193,11 @@ function cachedNote(usageAt: number | null, now: number): Note {
|
|
|
190
193
|
return { paint: c.dim, text: usageAt != null ? `cached ${fmtAgo(usageAt, now)}` : "never sampled" };
|
|
191
194
|
}
|
|
192
195
|
|
|
193
|
-
function headerLine(input: { active: boolean; needsReauth: boolean; exhausted: boolean; name: string; tier: string | null }): string {
|
|
196
|
+
function headerLine(input: { active: boolean; sessions: number; needsReauth: boolean; exhausted: boolean; name: string; tier: string | null }): string {
|
|
194
197
|
const marker = input.active ? c.green("●") : c.dim("○");
|
|
195
198
|
const badges: string[] = [];
|
|
196
199
|
if (input.active) badges.push(c.green("active"));
|
|
200
|
+
if (input.sessions > 0) badges.push(c.green(count({ n: input.sessions, noun: "session" })));
|
|
197
201
|
if (input.needsReauth) badges.push(c.red("needs-reauth"));
|
|
198
202
|
if (input.exhausted) badges.push(c.yellow("exhausted"));
|
|
199
203
|
return `${marker} ${c.bold(input.name)}${input.tier ? ` ${c.dim(input.tier)}` : ""}${badges.length ? ` ${badges.join(" ")}` : ""}`;
|
package/src/cli/switch.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { withLock } from "../lib/lock.ts";
|
|
2
|
-
import
|
|
2
|
+
import { codex } from "../lib/codex.ts";
|
|
3
3
|
import { loadAccounts, loadConfig } from "../lib/state.ts";
|
|
4
|
-
import { currentWins, pickBest,
|
|
4
|
+
import { currentWins, pickBest, thresholdBars, weeklyExpiry, type PickCtx } from "../lib/picker.ts";
|
|
5
5
|
import { findAccount } from "./rename.ts";
|
|
6
6
|
import { log } from "../lib/log.ts";
|
|
7
7
|
import { c, emitError, emitJson, fmtReset } from "./render.ts";
|
|
8
8
|
import type { Account } from "../lib/types.ts";
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
const SWITCH_MARGIN = 1.2;
|
|
11
|
+
|
|
12
|
+
export async function cmdSwitch(selector?: string, json = false): Promise<number> {
|
|
13
|
+
const p = codex;
|
|
11
14
|
const deadGrants: string[] = [];
|
|
12
15
|
const withDeadGrants = (report: Record<string, unknown>) => (deadGrants.length > 0 ? { ...report, deadGrants } : report);
|
|
13
16
|
const emit = (text: string, report: Record<string, unknown>): void => {
|
|
@@ -20,7 +23,7 @@ export async function cmdSwitch(p: Provider, selector?: string, json = false): P
|
|
|
20
23
|
};
|
|
21
24
|
const reauthHint = (a: Account) => `run \`tokenmaxxing auth${p.flag} ${a.label}\``;
|
|
22
25
|
const deadGrantMessage = (a: Account) => `${a.label}'s refresh token is dead - ${reauthHint(a)}`;
|
|
23
|
-
const switched = (target: Account) => `${c.green("↻")} switched to ${c.bold(target.label)}
|
|
26
|
+
const switched = (target: Account) => `${c.green("↻")} switched to ${c.bold(target.label)} (takes effect on the next codex start)`;
|
|
24
27
|
|
|
25
28
|
const idx0 = loadAccounts(p.pool);
|
|
26
29
|
if (idx0.accounts.length < 2) return fail(`need at least 2 accounts to switch - add one with \`tokenmaxxing add${p.flag}\``, { paint: c.yellow });
|
|
@@ -31,7 +34,7 @@ export async function cmdSwitch(p: Provider, selector?: string, json = false): P
|
|
|
31
34
|
const idx = loadAccounts(p.pool);
|
|
32
35
|
const claimed = p.liveId();
|
|
33
36
|
const drifted = claimed != null && claimed !== idx.activeId;
|
|
34
|
-
const present = p.
|
|
37
|
+
const present = p.presence();
|
|
35
38
|
|
|
36
39
|
const swapTo = async (target: Account, reason: string, extra: Record<string, unknown> = {}): Promise<number> => {
|
|
37
40
|
try {
|
|
@@ -62,7 +65,7 @@ export async function cmdSwitch(p: Provider, selector?: string, json = false): P
|
|
|
62
65
|
return swapTo(target, "selected");
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
const everyone: PickCtx = { now, thresholds: thresholdBars(cfg), currentId: null, families: p.gatedFamilies(cfg) };
|
|
68
|
+
const everyone: PickCtx = { now, thresholds: thresholdBars(cfg), currentId: null, families: p.gatedFamilies(cfg), seats: null };
|
|
66
69
|
const rejected = new Set<string>();
|
|
67
70
|
const candidatesOf = (accounts: Account[]): Account[] => accounts.filter((a) => !rejected.has(a.id) && (a.id === claimed || !present.has(a.id)));
|
|
68
71
|
while (true) {
|
|
@@ -72,7 +75,7 @@ export async function cmdSwitch(p: Provider, selector?: string, json = false): P
|
|
|
72
75
|
(claimed != null ? cur.accounts.find((a) => a.id === claimed) : null) ??
|
|
73
76
|
cur.accounts.find((a) => a.id === cur.activeId) ??
|
|
74
77
|
null;
|
|
75
|
-
if (active != null && currentWins(active, pool, everyone,
|
|
78
|
+
if (active != null && currentWins(active, pool, everyone, SWITCH_MARGIN)) {
|
|
76
79
|
if (drifted) return swapTo(active, "drift-reconciled");
|
|
77
80
|
const expiry = weeklyExpiry(active, now);
|
|
78
81
|
const why = Number.isFinite(expiry) ? ` (weekly ${fmtReset(expiry, now)})` : "";
|
|
@@ -107,69 +110,7 @@ export async function cmdSwitch(p: Provider, selector?: string, json = false): P
|
|
|
107
110
|
return 0;
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return fail(`no usable ${p.name} switch target (all at their bars, unmeasured, or needing reauth)`, { paint: c.yellow, extra: { reauthNeeded: reauth } });
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
while (true) {
|
|
116
|
-
const fresh = loadAccounts(p.pool);
|
|
117
|
-
const pool = candidatesOf(fresh.accounts);
|
|
118
|
-
const earliest = pickEarliestReset(pool, everyone);
|
|
119
|
-
const reauth = fresh.accounts.filter((a) => a.needsReauth).map((a) => a.label);
|
|
120
|
-
if (!earliest) {
|
|
121
|
-
if (reauth.length > 0) {
|
|
122
|
-
return fail(`no switchable account - reauth needed (run \`tokenmaxxing auth${p.flag} --all\`): ${reauth.join(", ")}`, {
|
|
123
|
-
paint: c.yellow,
|
|
124
|
-
extra: { reauthNeeded: reauth },
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
const freshActive = fresh.accounts.find((a) => a.id === fresh.activeId) ?? null;
|
|
128
|
-
if (drifted && freshActive) return swapTo(freshActive, "drift-reconciled");
|
|
129
|
-
emit(c.yellow("all accounts at their limit with unknown reset times (unparsed reset clocks? see tokenmaxxing.log) - staying put"), {
|
|
130
|
-
switched: false,
|
|
131
|
-
account: freshActive?.label ?? null,
|
|
132
|
-
reason: "unknown-resets",
|
|
133
|
-
});
|
|
134
|
-
return 0;
|
|
135
|
-
}
|
|
136
|
-
const reauthNote = reauth.length ? ` - re-auth needed: ${reauth.join(", ")}` : "";
|
|
137
|
-
const availableAt = earliest.availableAt > now ? earliest.availableAt : null;
|
|
138
|
-
if (earliest.account.id === fresh.activeId && !drifted) {
|
|
139
|
-
const msg = availableAt == null
|
|
140
|
-
? `staying on ${c.bold(earliest.account.label)} - no usable switch target${reauthNote}`
|
|
141
|
-
: `all accounts at limit - staying on ${c.bold(earliest.account.label)} (${fmtReset(availableAt, now)})${reauthNote}`;
|
|
142
|
-
emit(c.yellow(msg), {
|
|
143
|
-
switched: false,
|
|
144
|
-
account: earliest.account.label,
|
|
145
|
-
reason: availableAt == null ? "no-target" : "all-at-limit",
|
|
146
|
-
availableAt,
|
|
147
|
-
reauthNeeded: reauth,
|
|
148
|
-
});
|
|
149
|
-
return 0;
|
|
150
|
-
}
|
|
151
|
-
try {
|
|
152
|
-
await p.swap(earliest.account);
|
|
153
|
-
} catch (e) {
|
|
154
|
-
rejected.add(earliest.account.id);
|
|
155
|
-
const kind = p.classifySwapError(e);
|
|
156
|
-
if (kind === "dead-grant") {
|
|
157
|
-
deadGrants.push(earliest.account.label);
|
|
158
|
-
if (!json) console.error(c.red(deadGrantMessage(earliest.account)));
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
if (kind === "skip") {
|
|
162
|
-
if (!json) console.error(c.yellow(`${earliest.account.label}: ${e instanceof Error ? e.message : String(e)} - skipped for this run`));
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
throw e;
|
|
166
|
-
}
|
|
167
|
-
log("switch.manual", { account: earliest.account.id.slice(0, 8), reason: "earliest-reset" });
|
|
168
|
-
emit(switched(earliest.account), { switched: true, account: earliest.account.label, reason: "earliest-reset", availableAt, reauthNeeded: reauth });
|
|
169
|
-
if (availableAt != null && !json) {
|
|
170
|
-
console.log(c.yellow(`all accounts at limit - ${c.bold(earliest.account.label)} recovers soonest (${fmtReset(availableAt, now)})${reauthNote}`));
|
|
171
|
-
}
|
|
172
|
-
return 0;
|
|
173
|
-
}
|
|
113
|
+
const reauth = loadAccounts(p.pool).accounts.filter((a) => a.needsReauth).map((a) => a.label);
|
|
114
|
+
return fail(`no usable ${p.name} switch target (all at their bars, unmeasured, or needing reauth)`, { paint: c.yellow, extra: { reauthNeeded: reauth } });
|
|
174
115
|
});
|
|
175
116
|
}
|
|
@@ -8,7 +8,7 @@ import { codex, codexPickCtx, reconcileSiblings } from "../lib/codex.ts";
|
|
|
8
8
|
import { liveCodexAccountId } from "../lib/codexauth.ts";
|
|
9
9
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
10
10
|
import { isExhausted } from "../lib/picker.ts";
|
|
11
|
-
import {
|
|
11
|
+
import { livingPresences } from "../lib/presence.ts";
|
|
12
12
|
import { loadAccounts } from "../lib/state.ts";
|
|
13
13
|
import { CODEX_SUPERVISOR_ID_ENV } from "./codexsupervisor.ts";
|
|
14
14
|
import { CodexReconcileMarkerSchema, CodexRespawnMarkerSchema, CodexStopStdinSchema, JsonTextSchema } from "../lib/types.ts";
|
|
@@ -37,7 +37,7 @@ function promoteReconcileLocked(input: { supervisorId: string; sessionId: string
|
|
|
37
37
|
log("codexstop.reconcile_unparsable", {});
|
|
38
38
|
return false;
|
|
39
39
|
}
|
|
40
|
-
const presence =
|
|
40
|
+
const presence = livingPresences(codexPaths.presenceDir).find((p) => p.id === input.supervisorId) ?? null;
|
|
41
41
|
if (presence == null || presence.accountId !== parsed.data.accountId) {
|
|
42
42
|
rmSync(markerPath, { force: true });
|
|
43
43
|
log("codexstop.reconcile_stale", {});
|
|
@@ -5,7 +5,7 @@ import { codexPaths, codexPool, paths } from "../lib/paths.ts";
|
|
|
5
5
|
import { withLock } from "../lib/lock.ts";
|
|
6
6
|
import { LOOP_DIAGNOSIS, MAX_WRAP_DEPTH, UNMANAGED_ENV, WRAP_DEPTH_ENV, WRAP_RATE_MAX, WRAP_RATE_WINDOW_MS, wrapDepth, wrapperEntryRateTripped } from "../lib/claudebin.ts";
|
|
7
7
|
import { resolveRealCodex } from "../lib/codexbin.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { clearPresence, writePresence } from "../lib/presence.ts";
|
|
9
9
|
import { liveCodexAccountId } from "../lib/codexauth.ts";
|
|
10
10
|
import { saveTermios, restoreTermios } from "../lib/tty.ts";
|
|
11
11
|
import { CodexRespawnMarkerSchema } from "../lib/types.ts";
|
|
@@ -106,7 +106,7 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
|
|
|
106
106
|
if (spawnAccountId) {
|
|
107
107
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
108
108
|
try {
|
|
109
|
-
|
|
109
|
+
writePresence({ dir: codexPaths.presenceDir, id: supervisorId, accountId: spawnAccountId, pid: spawned.pid });
|
|
110
110
|
break;
|
|
111
111
|
} catch (e) {
|
|
112
112
|
if (spawned.exitCode !== null || spawned.signalCode !== null) break;
|
|
@@ -160,7 +160,7 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
|
|
|
160
160
|
launchArgs = payload.sessionId ? ["resume", payload.sessionId] : ["resume", "--last"];
|
|
161
161
|
continue;
|
|
162
162
|
}
|
|
163
|
-
|
|
163
|
+
clearPresence({ dir: codexPaths.presenceDir, id: supervisorId });
|
|
164
164
|
rmSync(join(codexPaths.reconcileDir, supervisorId), { force: true });
|
|
165
165
|
log("codexsupervisor.exit", { supervisorId: supervisorId.slice(0, 8), respawns, code: child.exitCode, signal: child.signalCode });
|
|
166
166
|
return child.exitCode ?? (child.signalCode ? 1 : 0);
|
|
@@ -1,31 +1,30 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { claude } from "../lib/claude.ts";
|
|
3
3
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
4
|
+
import { supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
4
5
|
import { log } from "../lib/log.ts";
|
|
6
|
+
import { readStdin } from "./statusline.ts";
|
|
5
7
|
import { JsonTextSchema } from "../lib/types.ts";
|
|
6
8
|
|
|
7
9
|
const SessionStartStdin = z.looseObject({
|
|
8
10
|
source: z.string().optional(),
|
|
9
|
-
session_id: z.
|
|
11
|
+
session_id: z.uuid().optional().catch(undefined),
|
|
10
12
|
});
|
|
11
13
|
|
|
12
|
-
async function readStdin(): Promise<string> {
|
|
13
|
-
const chunks: Uint8Array[] = [];
|
|
14
|
-
for await (const c of Bun.stdin.stream()) chunks.push(c);
|
|
15
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
14
|
export async function runSessionStart(): Promise<number> {
|
|
19
15
|
if (process.env.TOKENMAXXING_PROBE) return 0;
|
|
20
16
|
|
|
21
17
|
const raw = await readStdin();
|
|
22
18
|
const parsed = SessionStartStdin.safeParse(JsonTextSchema.safeParse(raw).data);
|
|
23
19
|
const source = parsed.success ? parsed.data.source : undefined;
|
|
20
|
+
const stdinSid = parsed.success ? parsed.data.session_id : undefined;
|
|
21
|
+
const session = supervisedSession();
|
|
24
22
|
|
|
25
23
|
try {
|
|
26
|
-
const decision = await evaluateAndMaybeSwap(claude);
|
|
27
|
-
if (decision.
|
|
28
|
-
|
|
24
|
+
const decision = await evaluateAndMaybeSwap(claude, Date.now(), session != null);
|
|
25
|
+
if (session && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
|
|
26
|
+
writeRespawnMarker({ session, sessionId: stdinSid ?? session.sid, accountId: decision.account.id, waitUntil: decision.waitUntil ?? Date.now() });
|
|
27
|
+
log("sessionstart.marker", { source, account: decision.account.id.slice(0, 8), waitUntil: decision.waitUntil });
|
|
29
28
|
}
|
|
30
29
|
} catch (e) {
|
|
31
30
|
log("sessionstart.error", { err: e instanceof Error ? e.message : String(e) });
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { sortBy } from "es-toolkit";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { loadAccounts, loadConfig, loadLastSwapAt, writeUsage } from "../lib/state.ts";
|
|
3
|
+
import { claudePool, seatFromEnv } from "../lib/paths.ts";
|
|
4
|
+
import { loadAccounts, loadConfig, writeUsage } from "../lib/state.ts";
|
|
6
5
|
import { familyTokens, matchedFamily, parseStatusLineStdin, parseStatusLineModel } from "../lib/usage.ts";
|
|
7
6
|
import { earliestReset, limitWindows, weeklyExpiry, weeklyWindow } from "../lib/picker.ts";
|
|
8
7
|
import { worktreeName } from "../lib/worktree.ts";
|
|
@@ -18,8 +17,6 @@ import {
|
|
|
18
17
|
type UsageWindow,
|
|
19
18
|
} from "../lib/types.ts";
|
|
20
19
|
|
|
21
|
-
const ADOPTION_GRACE_MS = 45_000;
|
|
22
|
-
|
|
23
20
|
const RenderCtxSchema = z.object({
|
|
24
21
|
accounts: AccountsIndexSchema,
|
|
25
22
|
perModel: z.record(z.string(), WindowSchema),
|
|
@@ -79,9 +76,7 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
|
|
|
79
76
|
windows.push(seg("", wins.fiveHour, wins.fiveHour.resetsAt));
|
|
80
77
|
windows.push(seg("", wins.sevenDay, wins.sevenDay.resetsAt));
|
|
81
78
|
}
|
|
82
|
-
const seatUuid =
|
|
83
|
-
(ctx.liveAccount != null && ctx.accounts.accounts.some((a) => a.id === ctx.liveAccount) ? ctx.liveAccount : null) ??
|
|
84
|
-
ctx.accounts.activeId;
|
|
79
|
+
const seatUuid = ctx.liveAccount;
|
|
85
80
|
const walled = (a: Account) => a.enforcedUntil != null && a.enforcedUntil > ctx.now;
|
|
86
81
|
const wallSeg = (wall: number) => seg("", { usedPercentage: 100, resetsAt: wall }, wall);
|
|
87
82
|
const seat = ctx.accounts.accounts.find((a) => a.id === seatUuid);
|
|
@@ -124,10 +119,9 @@ export async function runStatusline(): Promise<number> {
|
|
|
124
119
|
|
|
125
120
|
let account: string | null = null;
|
|
126
121
|
try {
|
|
127
|
-
account =
|
|
122
|
+
account = seatFromEnv(loadAccounts(claudePool).accounts.map((a) => a.id));
|
|
128
123
|
const windows = obj == null ? null : parseStatusLineStdin(obj);
|
|
129
|
-
|
|
130
|
-
if (windows && (lastSwapAt == null || now - lastSwapAt >= ADOPTION_GRACE_MS)) {
|
|
124
|
+
if (windows && account != null) {
|
|
131
125
|
const state: UsageState = { fiveHour: windows.fiveHour, sevenDay: windows.sevenDay, account, ts: now, model: parseStatusLineModel(obj) };
|
|
132
126
|
writeUsage(state);
|
|
133
127
|
}
|
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
1
|
import { z } from "zod";
|
|
3
|
-
import { paths } from "../lib/paths.ts";
|
|
4
|
-
import { writeFileAtomic } from "../lib/atomic.ts";
|
|
5
2
|
import { claude } from "../lib/claude.ts";
|
|
6
|
-
import { readOAuthAccount } from "../lib/claudejson.ts";
|
|
7
3
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { POST_SWAP_COOLDOWN_MS, loadConfig, loadLastSwapAt } from "../lib/state.ts";
|
|
4
|
+
import { supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
5
|
+
import { loadConfig } from "../lib/state.ts";
|
|
11
6
|
import { classifyEnforcedLimit, findEnforcedRow, parseErrorBody, readTranscriptTail } from "../lib/usage.ts";
|
|
12
|
-
import { JsonTextSchema,
|
|
7
|
+
import { JsonTextSchema, type EnforcedLimit } from "../lib/types.ts";
|
|
13
8
|
import { log } from "../lib/log.ts";
|
|
9
|
+
import { readStdin } from "./statusline.ts";
|
|
14
10
|
|
|
15
11
|
const StopFailureStdin = z.looseObject({
|
|
16
12
|
session_id: z.uuid().optional().catch(undefined),
|
|
@@ -20,19 +16,10 @@ const StopFailureStdin = z.looseObject({
|
|
|
20
16
|
last_assistant_message: z.string().optional().catch(undefined),
|
|
21
17
|
});
|
|
22
18
|
|
|
23
|
-
const LaunchedAtSchema = z.coerce.number().finite().optional().catch(undefined);
|
|
24
|
-
|
|
25
|
-
async function readStdin(): Promise<string> {
|
|
26
|
-
const chunks: Uint8Array[] = [];
|
|
27
|
-
for await (const c of Bun.stdin.stream()) chunks.push(c);
|
|
28
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
19
|
export async function runStopFailureHook(): Promise<number> {
|
|
32
20
|
if (process.env.TOKENMAXXING_PROBE) return 0;
|
|
33
21
|
|
|
34
|
-
const
|
|
35
|
-
const account = entry.account;
|
|
22
|
+
const account = claude.liveId();
|
|
36
23
|
const now = Date.now();
|
|
37
24
|
const raw = await readStdin();
|
|
38
25
|
const parsed = StopFailureStdin.safeParse(JsonTextSchema.safeParse(raw).data);
|
|
@@ -40,17 +27,11 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
40
27
|
if (stdin.error !== undefined && stdin.error !== "rate_limit") return 0;
|
|
41
28
|
|
|
42
29
|
const stdinSid = stdin.session_id;
|
|
43
|
-
const
|
|
44
|
-
const launchedAt = LaunchedAtSchema.parse(process.env.TOKENMAXXING_LAUNCHED_AT) ?? null;
|
|
30
|
+
const session = supervisedSession();
|
|
45
31
|
const mainLoop = stdin.agent_id === undefined;
|
|
46
|
-
const
|
|
32
|
+
const canRespawn = session != null && mainLoop;
|
|
47
33
|
|
|
48
34
|
try {
|
|
49
|
-
const lastSwapAt = loadLastSwapAt(claudePool);
|
|
50
|
-
if (lastSwapAt != null && lastSwapAt === entry.swapClock && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
|
|
51
|
-
log("stopfailure.cooldown", { sinceSwapMs: now - lastSwapAt });
|
|
52
|
-
return 0;
|
|
53
|
-
}
|
|
54
35
|
const cfg = loadConfig();
|
|
55
36
|
const row = stdin.transcript_path
|
|
56
37
|
? findEnforcedRow({ rows: readTranscriptTail(stdin.transcript_path), lastAssistantMessage: stdin.last_assistant_message, now })
|
|
@@ -63,6 +44,7 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
63
44
|
log("stopfailure.enforced", { kind: limit.kind, family: enforced.family ?? undefined, resetsAt: limit.resetsAt, subagent: !mainLoop });
|
|
64
45
|
} else {
|
|
65
46
|
log("stopfailure.unclassified", {
|
|
47
|
+
seat: account != null,
|
|
66
48
|
row: row != null,
|
|
67
49
|
type: row?.quotaLimits?.rateLimitType,
|
|
68
50
|
transient: row?.apiErrorIsTransient,
|
|
@@ -70,18 +52,10 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
70
52
|
});
|
|
71
53
|
}
|
|
72
54
|
|
|
73
|
-
const decision = await evaluateAndMaybeSwap(claude, now,
|
|
74
|
-
if (enforced &&
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
account: decision.account.label,
|
|
78
|
-
ts: Date.now(),
|
|
79
|
-
waitUntil: decision.waitUntil ?? now,
|
|
80
|
-
sessionId: stdinSid ?? pinnedSid,
|
|
81
|
-
...(launchedAt != null ? { launchedAt } : {}),
|
|
82
|
-
});
|
|
83
|
-
writeFileAtomic(marker, JSON.stringify(payload));
|
|
84
|
-
log("stopfailure.marker", { session: (stdinSid ?? pinnedSid).slice(0, 8), account: decision.account.id.slice(0, 8), waitUntil: payload.waitUntil });
|
|
55
|
+
const decision = await evaluateAndMaybeSwap(claude, now, canRespawn && enforced != null, enforced);
|
|
56
|
+
if (enforced && session && canRespawn && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
|
|
57
|
+
writeRespawnMarker({ session, sessionId: stdinSid ?? session.sid, accountId: decision.account.id, waitUntil: decision.waitUntil ?? now });
|
|
58
|
+
log("stopfailure.marker", { session: session.sid.slice(0, 8), account: decision.account.id.slice(0, 8), waitUntil: decision.waitUntil ?? now });
|
|
85
59
|
} else {
|
|
86
60
|
log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
|
|
87
61
|
}
|
package/src/entries/stophook.ts
CHANGED
|
@@ -1,46 +1,26 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
1
|
import { z } from "zod";
|
|
3
|
-
import { paths } from "../lib/paths.ts";
|
|
4
|
-
import { writeFileAtomic } from "../lib/atomic.ts";
|
|
5
2
|
import { claude } from "../lib/claude.ts";
|
|
6
3
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
7
|
-
import {
|
|
4
|
+
import { supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
5
|
+
import { JsonTextSchema } from "../lib/types.ts";
|
|
8
6
|
import { log } from "../lib/log.ts";
|
|
7
|
+
import { readStdin } from "./statusline.ts";
|
|
9
8
|
|
|
10
9
|
const StopStdin = z.looseObject({ session_id: z.uuid().optional().catch(undefined) });
|
|
11
10
|
|
|
12
|
-
async function readStdin(): Promise<string> {
|
|
13
|
-
const chunks: Uint8Array[] = [];
|
|
14
|
-
for await (const c of Bun.stdin.stream()) chunks.push(c);
|
|
15
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
11
|
export async function runStopHook(): Promise<number> {
|
|
19
12
|
if (process.env.TOKENMAXXING_PROBE) return 0;
|
|
20
13
|
|
|
21
14
|
const raw = await readStdin();
|
|
22
15
|
const parsed = StopStdin.safeParse(JsonTextSchema.safeParse(raw).data);
|
|
23
16
|
const stdinSid = parsed.success ? parsed.data.session_id : undefined;
|
|
24
|
-
const
|
|
17
|
+
const session = supervisedSession();
|
|
25
18
|
|
|
26
19
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
log(decision.
|
|
31
|
-
if (decision.waitUntil !== undefined && canPause && pinnedSid) {
|
|
32
|
-
const marker = join(paths.respawnDir, pinnedSid);
|
|
33
|
-
const launchedAt = z.coerce.number().finite().optional().catch(undefined).parse(process.env.TOKENMAXXING_LAUNCHED_AT);
|
|
34
|
-
const payload = RespawnMarkerSchema.parse({
|
|
35
|
-
account: decision.account.label,
|
|
36
|
-
ts: Date.now(),
|
|
37
|
-
waitUntil: decision.waitUntil,
|
|
38
|
-
sessionId: stdinSid ?? pinnedSid,
|
|
39
|
-
...(launchedAt != null ? { launchedAt } : {}),
|
|
40
|
-
});
|
|
41
|
-
writeFileAtomic(marker, JSON.stringify(payload));
|
|
42
|
-
log("stop.marker", { session: (stdinSid ?? pinnedSid).slice(0, 8) });
|
|
43
|
-
}
|
|
20
|
+
const decision = await evaluateAndMaybeSwap(claude, Date.now(), session != null);
|
|
21
|
+
if (session && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
|
|
22
|
+
writeRespawnMarker({ session, sessionId: stdinSid ?? session.sid, accountId: decision.account.id, waitUntil: decision.waitUntil ?? Date.now() });
|
|
23
|
+
log(decision.waitUntil !== undefined ? "stop.wait" : "stop.move", { account: decision.account.id.slice(0, 8), waitUntil: decision.waitUntil, session: session.sid.slice(0, 8) });
|
|
44
24
|
}
|
|
45
25
|
} catch (e) {
|
|
46
26
|
log("stop.error", { err: e instanceof Error ? e.message : String(e) });
|