tokenmaxxing 1.38.0 → 1.40.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 +30 -30
- package/package.json +2 -2
- package/src/cli/check.ts +4 -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 +122 -156
- package/src/lib/claudelock.ts +2 -3
- package/src/lib/codex.ts +12 -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 +2 -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/update.ts +133 -0
- package/src/lib/usage.ts +7 -4
- package/src/main.ts +10 -17
- package/src/lib/claudejson.ts +0 -30
- package/src/lib/codexpresence.ts +0 -64
- package/src/lib/swap.ts +0 -198
package/src/lib/sample.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import { mkdirSync
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { minBy } from "es-toolkit";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { withClaudeRefreshLock } from "./claudelock.ts";
|
|
5
|
+
import { writeFileAtomic } from "./atomic.ts";
|
|
6
|
+
import { readStore } from "./credstore.ts";
|
|
8
7
|
import { withLock } from "./lock.ts";
|
|
9
|
-
import { readOAuthAccount } from "./claudejson.ts";
|
|
10
|
-
import { claudeTierLabel, refreshCredential, isAccessTokenExpiring, isDeadCredential, fetchTokenIdentity, describeIdentity, IdentityUnavailableError, InvalidGrantError } from "./oauth.ts";
|
|
11
|
-
import { mergeWindows, probeUsage, windowsOf } from "./usage.ts";
|
|
12
|
-
import { POST_SWAP_COOLDOWN_MS, loadAccounts, loadLastSwapAt, saveAccounts } from "./state.ts";
|
|
13
|
-
import { keepRotatedPair } from "./swap.ts";
|
|
14
8
|
import { log } from "./log.ts";
|
|
15
|
-
import {
|
|
9
|
+
import { claudeTierLabel, isDeadCredential } from "./oauth.ts";
|
|
10
|
+
import { claudePool, sampleDirFor, storeDirFor } from "./paths.ts";
|
|
11
|
+
import type { Observation } from "./provider.ts";
|
|
12
|
+
import { loadAccounts, loadUsageSnapshot, saveAccounts } from "./state.ts";
|
|
13
|
+
import { mergeWindows, probeUsage, windowsOf } from "./usage.ts";
|
|
14
|
+
import { UsageWindowsSchema, type Account, type Config } from "./types.ts";
|
|
16
15
|
|
|
17
16
|
const SampleOutcomeSchema = z.discriminatedUnion("ok", [
|
|
18
17
|
z.object({ ok: z.literal(true), usage: UsageWindowsSchema }),
|
|
@@ -20,288 +19,96 @@ const SampleOutcomeSchema = z.discriminatedUnion("ok", [
|
|
|
20
19
|
]);
|
|
21
20
|
export type SampleOutcome = z.infer<typeof SampleOutcomeSchema>;
|
|
22
21
|
|
|
23
|
-
const
|
|
24
|
-
z.object({
|
|
25
|
-
z.object({
|
|
26
|
-
z.object({ status: z.literal("unavailable"), reason: z.string(), stale: z.boolean() }),
|
|
22
|
+
const PreparedProbeSchema = z.discriminatedUnion("ok", [
|
|
23
|
+
z.object({ ok: z.literal(true), dir: z.string() }),
|
|
24
|
+
z.object({ ok: z.literal(false), reason: z.string() }),
|
|
27
25
|
]);
|
|
28
|
-
type
|
|
26
|
+
type PreparedProbe = z.infer<typeof PreparedProbeSchema>;
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
stale: e instanceof IdentityUnavailableError && e.status === 401,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
if (identity.accountUuid === account.id) return { status: "match" };
|
|
42
|
-
return { status: "mismatch", reason: `credential actually belongs to ${describeIdentity(identity)}`, owner: identity };
|
|
28
|
+
export function teeObservation(account: Account): Observation | null {
|
|
29
|
+
const stored = account.lastUsageAt != null ? { windows: account.windows, at: account.lastUsageAt } : null;
|
|
30
|
+
const snap = loadUsageSnapshot(account.id);
|
|
31
|
+
if (!snap) return stored;
|
|
32
|
+
const at = snap.state.sampledAt ?? snap.state.ts;
|
|
33
|
+
if (stored != null && at <= stored.at) return stored;
|
|
34
|
+
const aggregate = windowsOf({ fiveHour: snap.state.fiveHour, sevenDay: snap.state.sevenDay, perModel: {} }, at);
|
|
35
|
+
return { windows: mergeWindows(aggregate, account.windows), at };
|
|
43
36
|
}
|
|
44
37
|
|
|
45
|
-
function
|
|
46
|
-
|
|
38
|
+
export function foldTee(account: Account): boolean {
|
|
39
|
+
const observed = teeObservation(account);
|
|
40
|
+
if (!observed || (account.lastUsageAt != null && observed.at <= account.lastUsageAt)) return false;
|
|
41
|
+
account.windows = observed.windows;
|
|
42
|
+
account.lastUsageAt = observed.at;
|
|
43
|
+
return true;
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const PreparedProbeSchema = z.discriminatedUnion("ok", [z.object({ ok: z.literal(true), dir: z.string(), installed: z.string() }), ProbeFailureSchema]);
|
|
52
|
-
type PreparedProbe = z.infer<typeof PreparedProbeSchema>;
|
|
53
|
-
|
|
54
|
-
async function prepareParkedProbe(account: Account, dir: string, signal?: AbortSignal): Promise<PreparedProbe> {
|
|
55
|
-
const backup = parkedTarget(credItemFor(account.id));
|
|
56
|
-
const parkedRaw = await readItem(backup);
|
|
57
|
-
if (!parkedRaw) return { ok: false, reason: "no parked credential - run `tokenmaxxing auth`" };
|
|
58
|
-
if (!account.oauthAccount) return { ok: false, reason: "account record has no oauthAccount - run `tokenmaxxing auth`" };
|
|
59
|
-
|
|
60
|
-
let creds: OAuthCreds;
|
|
46
|
+
export async function prepareProbe(account: Account, suffix = ""): Promise<PreparedProbe> {
|
|
47
|
+
let creds;
|
|
61
48
|
try {
|
|
62
|
-
creds =
|
|
49
|
+
creds = await readStore(account.id);
|
|
63
50
|
} catch (e) {
|
|
64
|
-
return { ok: false, reason: `
|
|
51
|
+
return { ok: false, reason: `store credential unreadable (${(e instanceof Error ? e.message : String(e)).slice(0, 80)}) - run \`tokenmaxxing auth\`` };
|
|
65
52
|
}
|
|
53
|
+
if (!creds) return { ok: false, reason: "no credential in this account's store - run `tokenmaxxing auth`" };
|
|
66
54
|
if (isDeadCredential(creds)) {
|
|
67
55
|
account.needsReauth = true;
|
|
68
|
-
return { ok: false, reason: "
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const liveRaw = await readItem(liveTarget());
|
|
72
|
-
let liveAccount: string | null = null;
|
|
73
|
-
let liveToken: string | null = null;
|
|
74
|
-
let liveRefresh: string | null = null;
|
|
75
|
-
if (liveRaw != null) {
|
|
76
|
-
let liveCreds: OAuthCreds;
|
|
77
|
-
try {
|
|
78
|
-
liveCreds = CredentialBlobSchema.parse(JSON.parse(liveRaw)).claudeAiOauth;
|
|
79
|
-
} catch (e) {
|
|
80
|
-
return { ok: false, reason: `cannot read the live credential (${(e instanceof Error ? e.message : String(e)).slice(0, 80)}) - refusing to sample a possibly-live account` };
|
|
81
|
-
}
|
|
82
|
-
if (!isDeadCredential(liveCreds)) {
|
|
83
|
-
liveToken = liveCreds.accessToken;
|
|
84
|
-
liveRefresh = liveCreds.refreshToken;
|
|
85
|
-
try {
|
|
86
|
-
liveAccount = (await fetchTokenIdentity(liveCreds.accessToken, signal)).accountUuid;
|
|
87
|
-
} catch (e) {
|
|
88
|
-
return { ok: false, reason: `cannot verify the live credential's owner (${(e instanceof Error ? e.message : String(e)).slice(0, 80)}) - refusing to sample a possibly-live account` };
|
|
89
|
-
}
|
|
90
|
-
if (liveAccount === account.id) {
|
|
91
|
-
return { ok: false, reason: "this account holds the LIVE login (active label drifted) - run `tokenmaxxing switch` to reconcile" };
|
|
92
|
-
}
|
|
93
|
-
}
|
|
56
|
+
return { ok: false, reason: "the store's credential was cleared after a failed refresh - re-auth with `tokenmaxxing auth`" };
|
|
94
57
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (pooled == null || pooled.id === liveAccount) return "was left in place";
|
|
99
|
-
await writeItem(parkedTarget(credItemFor(pooled.id)), JSON.stringify({ claudeAiOauth: creds }));
|
|
100
|
-
log("swap.parked_relocated", { account: pooled.id.slice(0, 8) });
|
|
101
|
-
return "was copied to that account's slot";
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
if (isAccessTokenExpiring(creds, 300_000)) {
|
|
105
|
-
const owner = await checkIdentity(creds, account, signal);
|
|
106
|
-
if (owner.status === "mismatch") {
|
|
107
|
-
const kept = await relocate(owner.owner);
|
|
108
|
-
account.needsReauth = true;
|
|
109
|
-
return { ok: false, reason: `${owner.reason} - refusing to spend another account's grant; the pair ${kept}; re-auth with \`tokenmaxxing auth\`` };
|
|
110
|
-
}
|
|
111
|
-
if (owner.status === "unavailable" && !owner.stale) {
|
|
112
|
-
return { ok: false, reason: `${owner.reason} - refusing to refresh a parked credential whose owner cannot be verified` };
|
|
113
|
-
}
|
|
114
|
-
const rotate = async (): Promise<{ fresh: OAuthCreds } | { failed: ProbeFailure }> => {
|
|
115
|
-
try {
|
|
116
|
-
return { fresh: await refreshCredential(creds, Date.now(), signal) };
|
|
117
|
-
} catch (e) {
|
|
118
|
-
if (e instanceof InvalidGrantError) {
|
|
119
|
-
account.needsReauth = true;
|
|
120
|
-
return { failed: { ok: false, reason: "refresh token dead - re-auth with `tokenmaxxing auth`" } };
|
|
121
|
-
}
|
|
122
|
-
return { failed: { ok: false, reason: `token refresh failed: ${e instanceof Error ? e.message : String(e)}` } };
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
let result: { fresh: OAuthCreds } | { failed: ProbeFailure };
|
|
126
|
-
if (owner.status === "match") {
|
|
127
|
-
result = await rotate();
|
|
128
|
-
} else {
|
|
129
|
-
if (liveRefresh != null && creds.refreshToken === liveRefresh) {
|
|
130
|
-
return { ok: false, reason: "this slot holds a copy of the LIVE login's grant - refusing to rotate it from a parked slot; run `tokenmaxxing switch` to reconcile" };
|
|
131
|
-
}
|
|
132
|
-
try {
|
|
133
|
-
signal?.throwIfAborted();
|
|
134
|
-
result = await withClaudeRefreshLock(async (lock) => {
|
|
135
|
-
const rotated = await rotate();
|
|
136
|
-
if ("failed" in rotated) return rotated;
|
|
137
|
-
const verified = await checkIdentity(rotated.fresh, account, signal);
|
|
138
|
-
if (verified.status === "match") return rotated;
|
|
139
|
-
if (verified.status === "unavailable") {
|
|
140
|
-
await writeItem(backup, JSON.stringify({ claudeAiOauth: rotated.fresh }));
|
|
141
|
-
return { failed: { ok: false, reason: `${verified.reason} - the rotated pair stays in this slot unverified until the next pass` } };
|
|
142
|
-
}
|
|
143
|
-
const trueOwner = loadAccounts(claudePool).accounts.find((a) => a.id === verified.owner.accountUuid) ?? null;
|
|
144
|
-
const kept = await keepRotatedPair({ fresh: rotated.fresh, owner: trueOwner, fallback: account, liveOwnerId: liveAccount, expectedLiveToken: liveToken, lock });
|
|
145
|
-
account.needsReauth = true;
|
|
146
|
-
return { failed: { ok: false, reason: `${verified.reason}, whose grant this refresh rotated - the rotated token ${kept}; re-auth with \`tokenmaxxing auth\`` } };
|
|
147
|
-
});
|
|
148
|
-
} catch (e) {
|
|
149
|
-
return { ok: false, reason: `cannot arbitrate a stale parked credential: ${e instanceof Error ? e.message : String(e)}` };
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
if ("failed" in result) return result.failed;
|
|
153
|
-
creds = result.fresh;
|
|
154
|
-
await writeItem(backup, JSON.stringify({ claudeAiOauth: creds }));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const identity = await checkIdentity(creds, account, signal);
|
|
158
|
-
if (identity.status === "mismatch") {
|
|
159
|
-
const kept = await relocate(identity.owner);
|
|
160
|
-
account.needsReauth = true;
|
|
161
|
-
return { ok: false, reason: `${identity.reason} - this account's own credential is gone; the pair ${kept}; re-auth with \`tokenmaxxing auth\`` };
|
|
162
|
-
}
|
|
163
|
-
if (identity.status === "unavailable") {
|
|
164
|
-
return { ok: false, reason: identity.reason };
|
|
165
|
-
}
|
|
166
|
-
refreshTier(account, creds);
|
|
167
|
-
|
|
168
|
-
rmSync(dir, { recursive: true, force: true });
|
|
58
|
+
if (!account.oauthAccount) return { ok: false, reason: "account record has no oauthAccount - run `tokenmaxxing auth`" };
|
|
59
|
+
account.tier = claudeTierLabel(creds) ?? account.tier;
|
|
60
|
+
const dir = sampleDirFor(account.id, suffix);
|
|
169
61
|
mkdirSync(dir, { recursive: true });
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
await writeItem(isolatedTarget(dir), installed);
|
|
173
|
-
writeFileSync(join(dir, ".claude.json"), JSON.stringify({ oauthAccount: account.oauthAccount, hasCompletedOnboarding: true }));
|
|
174
|
-
} catch (e) {
|
|
175
|
-
await finishParkedProbe(account, { dir, installed });
|
|
176
|
-
throw e;
|
|
177
|
-
}
|
|
178
|
-
return { ok: true, dir, installed };
|
|
62
|
+
writeFileAtomic(join(dir, ".claude.json"), JSON.stringify({ oauthAccount: account.oauthAccount, hasCompletedOnboarding: true }));
|
|
63
|
+
return { ok: true, dir };
|
|
179
64
|
}
|
|
180
65
|
|
|
181
|
-
async function
|
|
182
|
-
const usage = await probeUsage(dir, Date.now(), { retries: opts.retries });
|
|
66
|
+
export async function runProbe(account: Account, dir: string, opts: { retries?: number }): Promise<SampleOutcome> {
|
|
67
|
+
const usage = await probeUsage({ configDir: dir, store: storeDirFor(account.id) }, Date.now(), { retries: opts.retries });
|
|
183
68
|
return usage ? { ok: true, usage } : { ok: false, reason: "`/usage` returned no limit data (see log)" };
|
|
184
69
|
}
|
|
185
70
|
|
|
186
|
-
async function
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
const afterIso = await readItem(isoTarget);
|
|
191
|
-
if (afterIso && afterIso !== prepared.installed) {
|
|
192
|
-
const parkedNow = await readItem(backup);
|
|
193
|
-
const installedToken = CredentialBlobSchema.parse(JSON.parse(prepared.installed)).claudeAiOauth.accessToken;
|
|
194
|
-
const parkedToken = parkedNow == null ? null : CredentialBlobSchema.parse(JSON.parse(parkedNow)).claudeAiOauth.accessToken;
|
|
195
|
-
if (parkedToken === installedToken) await writeItem(backup, claudeAiOauthOnly(afterIso));
|
|
196
|
-
else log("sample.harvest_skipped", { account: account.id.slice(0, 8) });
|
|
197
|
-
}
|
|
198
|
-
} finally {
|
|
199
|
-
await deleteItem(isoTarget);
|
|
200
|
-
rmSync(prepared.dir, { recursive: true, force: true });
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export async function probeParkedUsage(account: Account): Promise<SampleOutcome> {
|
|
205
|
-
const prepared = await prepareParkedProbe(account, join(paths.sampleDir, credItemFor(account.id)));
|
|
206
|
-
if (!prepared.ok) return prepared;
|
|
207
|
-
try {
|
|
208
|
-
return await runParkedProbe(prepared.dir, {});
|
|
209
|
-
} finally {
|
|
210
|
-
await finishParkedProbe(account, prepared);
|
|
211
|
-
}
|
|
71
|
+
export async function probeAccountUsage(account: Account): Promise<SampleOutcome> {
|
|
72
|
+
const prepared = await prepareProbe(account);
|
|
73
|
+
return prepared.ok ? runProbe(account, prepared.dir, {}) : prepared;
|
|
212
74
|
}
|
|
213
75
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
export async function sampleOldestParked(cfg: Config): Promise<void> {
|
|
76
|
+
export async function sampleOldest(cfg: Config): Promise<void> {
|
|
217
77
|
const reserved = await withLock(claudePool.lockFile, async () => {
|
|
218
78
|
const now = Date.now();
|
|
219
|
-
const lastSwapAt = loadLastSwapAt(claudePool);
|
|
220
|
-
if (lastSwapAt != null && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) return null;
|
|
221
79
|
const idx = loadAccounts(claudePool);
|
|
222
|
-
|
|
80
|
+
let dirty = false;
|
|
81
|
+
for (const a of idx.accounts) dirty = foldTee(a) || dirty;
|
|
223
82
|
const sampledAt = (a: Account) => Math.max(a.lastUsageAt ?? 0, a.lastProbeAt ?? 0);
|
|
224
|
-
const stale = idx.accounts.filter((a) => a.
|
|
83
|
+
const stale = idx.accounts.filter((a) => a.needsReauth !== true && now - sampledAt(a) > cfg.policy.usagePollTtlMs);
|
|
225
84
|
const target = minBy(stale, sampledAt);
|
|
226
|
-
if (!target)
|
|
85
|
+
if (!target) {
|
|
86
|
+
if (dirty) saveAccounts(claudePool, idx);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
227
89
|
target.lastProbeAt = now;
|
|
228
|
-
const prepared = await
|
|
90
|
+
const prepared = await prepareProbe(target, "-tick");
|
|
229
91
|
saveAccounts(claudePool, idx);
|
|
230
92
|
if (!prepared.ok) {
|
|
231
|
-
log("sample.
|
|
93
|
+
log("sample.failed", { account: target.id.slice(0, 8), reason: prepared.reason.slice(0, 200) });
|
|
232
94
|
return null;
|
|
233
95
|
}
|
|
234
|
-
return { account: target, prepared };
|
|
96
|
+
return { account: target, dir: prepared.dir };
|
|
235
97
|
});
|
|
236
98
|
if (!reserved) return;
|
|
237
|
-
const { account, prepared } = reserved;
|
|
238
99
|
const startedAt = Date.now();
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
saveAccounts(claudePool, idx);
|
|
252
|
-
}
|
|
253
|
-
log(outcome.ok ? "sample.parked_ok" : "sample.parked_failed", {
|
|
254
|
-
account: account.id.slice(0, 8),
|
|
255
|
-
...(outcome.ok ? {} : { reason: outcome.reason.slice(0, 200) }),
|
|
256
|
-
});
|
|
100
|
+
const outcome = await runProbe(reserved.account, reserved.dir, { retries: 0 });
|
|
101
|
+
await withLock(claudePool.lockFile, () => {
|
|
102
|
+
const idx = loadAccounts(claudePool);
|
|
103
|
+
const stored = idx.accounts.find((a) => a.id === reserved.account.id);
|
|
104
|
+
if (stored && outcome.ok && (stored.lastUsageAt == null || startedAt > stored.lastUsageAt)) {
|
|
105
|
+
stored.windows = mergeWindows(windowsOf(outcome.usage, startedAt), stored.windows);
|
|
106
|
+
stored.lastUsageAt = startedAt;
|
|
107
|
+
saveAccounts(claudePool, idx);
|
|
108
|
+
}
|
|
109
|
+
log(outcome.ok ? "sample.ok" : "sample.failed", {
|
|
110
|
+
account: reserved.account.id.slice(0, 8),
|
|
111
|
+
...(outcome.ok ? {} : { reason: outcome.reason.slice(0, 200) }),
|
|
257
112
|
});
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export async function ensureLiveTokenFresh(): Promise<void> {
|
|
262
|
-
const liveRaw = await readItem(liveTarget());
|
|
263
|
-
if (!liveRaw) return;
|
|
264
|
-
let creds: OAuthCreds;
|
|
265
|
-
try {
|
|
266
|
-
creds = CredentialBlobSchema.parse(JSON.parse(liveRaw)).claudeAiOauth;
|
|
267
|
-
} catch {
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
if (isDeadCredential(creds)) throw new InvalidGrantError("live credential was cleared after a failed refresh");
|
|
271
|
-
if (!isAccessTokenExpiring(creds, 300_000)) return;
|
|
272
|
-
await withClaudeRefreshLock(async (lock) => {
|
|
273
|
-
const raw2 = await readItem(liveTarget());
|
|
274
|
-
if (raw2 == null) throw new Error("live credential vanished while waiting for the refresh lock");
|
|
275
|
-
const current = CredentialBlobSchema.parse(JSON.parse(raw2)).claudeAiOauth;
|
|
276
|
-
if (isDeadCredential(current)) throw new InvalidGrantError("live credential was cleared after a failed refresh");
|
|
277
|
-
const next = isAccessTokenExpiring(current, 300_000) ? await refreshCredential(current) : current;
|
|
278
|
-
if (next === current) return;
|
|
279
|
-
if (lock.compromised()) throw new Error("refresh lock compromised mid-refresh - discarding the live rewrite");
|
|
280
|
-
await writeItem(liveTarget(), mergeIntoLive(raw2, next));
|
|
281
113
|
});
|
|
282
114
|
}
|
|
283
|
-
|
|
284
|
-
export async function probeActiveUsage(account: Account): Promise<SampleOutcome> {
|
|
285
|
-
try {
|
|
286
|
-
await ensureLiveTokenFresh();
|
|
287
|
-
} catch (e) {
|
|
288
|
-
if (e instanceof InvalidGrantError) return { ok: false, reason: "live refresh token dead - run `claude` and `/login`" };
|
|
289
|
-
return { ok: false, reason: `token refresh failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
290
|
-
}
|
|
291
|
-
const liveRaw = await readItem(liveTarget());
|
|
292
|
-
if (!liveRaw) return { ok: false, reason: "no live credential - run `claude` and `/login`" };
|
|
293
|
-
let creds: OAuthCreds;
|
|
294
|
-
try {
|
|
295
|
-
creds = CredentialBlobSchema.parse(JSON.parse(liveRaw)).claudeAiOauth;
|
|
296
|
-
} catch (e) {
|
|
297
|
-
return { ok: false, reason: `live credential blob unreadable (${(e instanceof Error ? e.message : String(e)).slice(0, 80)})` };
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const identity = await checkIdentity(creds, account);
|
|
301
|
-
if (identity.status === "mismatch") return { ok: false, reason: `live ${identity.reason} - active label drifted; run \`tokenmaxxing switch\`` };
|
|
302
|
-
if (identity.status === "unavailable") return { ok: false, reason: identity.reason };
|
|
303
|
-
refreshTier(account, creds);
|
|
304
|
-
|
|
305
|
-
const usage = await probeUsage();
|
|
306
|
-
return usage ? { ok: true, usage } : { ok: false, reason: "`/usage` returned no limit data (see log)" };
|
|
307
|
-
}
|
package/src/lib/sessions.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { paths } from "./paths.ts";
|
|
5
5
|
import { writeFileAtomic } from "./atomic.ts";
|
|
6
|
+
import { RespawnMarkerSchema } from "./types.ts";
|
|
6
7
|
|
|
7
8
|
const SessionSchema = z.object({ flags: z.array(z.string()), cwd: z.string() });
|
|
8
9
|
|
|
@@ -34,3 +35,27 @@ export function pruneStaleSessions(now: number): void {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
}
|
|
38
|
+
|
|
39
|
+
const SupervisedSessionSchema = z.object({ sid: z.string().min(1), launchedAt: z.number().finite().nullable() });
|
|
40
|
+
export type SupervisedSession = z.infer<typeof SupervisedSessionSchema>;
|
|
41
|
+
|
|
42
|
+
const LaunchedAtSchema = z.coerce.number().finite().optional().catch(undefined);
|
|
43
|
+
|
|
44
|
+
export function supervisedSession(env: Record<string, string | undefined> = process.env): SupervisedSession | null {
|
|
45
|
+
if (env.TOKENMAXXING_SUPERVISED !== "1") return null;
|
|
46
|
+
const sid = env.TOKENMAXXING_SESSION_ID;
|
|
47
|
+
if (sid == null || sid === "") return null;
|
|
48
|
+
return SupervisedSessionSchema.parse({ sid, launchedAt: LaunchedAtSchema.parse(env.TOKENMAXXING_LAUNCHED_AT) ?? null });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function writeRespawnMarker(input: { session: SupervisedSession; sessionId: string; accountId: string; waitUntil: number }): void {
|
|
52
|
+
mkdirSync(paths.respawnDir, { recursive: true });
|
|
53
|
+
const payload = RespawnMarkerSchema.parse({
|
|
54
|
+
accountId: input.accountId,
|
|
55
|
+
ts: Date.now(),
|
|
56
|
+
waitUntil: input.waitUntil,
|
|
57
|
+
sessionId: input.sessionId,
|
|
58
|
+
...(input.session.launchedAt != null ? { launchedAt: input.session.launchedAt } : {}),
|
|
59
|
+
});
|
|
60
|
+
writeFileAtomic(join(paths.respawnDir, input.session.sid), JSON.stringify(payload));
|
|
61
|
+
}
|
package/src/lib/state.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { closeSync, existsSync, fstatSync, openSync, readFileSync, rmSync, utimesSync } from "node:fs";
|
|
2
2
|
import { isEqual } from "es-toolkit";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { paths, realClaudeBinFromEnv, realCodexBinFromEnv, type PoolPaths } from "./paths.ts";
|
|
4
|
+
import { paths, realClaudeBinFromEnv, realCodexBinFromEnv, usageJsonFor, type PoolPaths } from "./paths.ts";
|
|
5
5
|
import { writeFileAtomic } from "./atomic.ts";
|
|
6
6
|
import {
|
|
7
7
|
AccountsIndexSchema,
|
|
@@ -99,17 +99,19 @@ export function upsertAccount(
|
|
|
99
99
|
return existing ?? fresh;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
export
|
|
102
|
+
export type UsageSnapshot = { state: UsageState; at: number };
|
|
103
|
+
|
|
104
|
+
export function loadUsageSnapshot(accountId: string): UsageSnapshot | null {
|
|
103
105
|
let fd: number;
|
|
104
106
|
try {
|
|
105
|
-
fd = openSync(
|
|
107
|
+
fd = openSync(usageJsonFor(accountId), "r");
|
|
106
108
|
} catch {
|
|
107
109
|
return null;
|
|
108
110
|
}
|
|
109
111
|
try {
|
|
110
112
|
const at = fstatSync(fd).mtimeMs;
|
|
111
113
|
const parsed = UsageStateSchema.safeParse(JSON.parse(readFileSync(fd, "utf8")));
|
|
112
|
-
return parsed.success ? { state: parsed.data, at } : null;
|
|
114
|
+
return parsed.success && parsed.data.account === accountId ? { state: parsed.data, at } : null;
|
|
113
115
|
} catch {
|
|
114
116
|
return null;
|
|
115
117
|
} finally {
|
|
@@ -117,16 +119,12 @@ export function loadUsageSnapshot(): { state: UsageState; at: number } | null {
|
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
121
|
|
|
120
|
-
export function
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function clearUsageSnapshots(): void {
|
|
125
|
-
rmSync(paths.usageJson, { force: true });
|
|
122
|
+
export function clearUsageSnapshot(accountId: string): void {
|
|
123
|
+
rmSync(usageJsonFor(accountId), { force: true });
|
|
126
124
|
}
|
|
127
125
|
|
|
128
126
|
export function loadLastSwapAt(pool: PoolPaths): number | null {
|
|
129
|
-
if (!existsSync(pool.lastSwapJson)) return null;
|
|
127
|
+
if (pool.lastSwapJson == null || !existsSync(pool.lastSwapJson)) return null;
|
|
130
128
|
let json: unknown;
|
|
131
129
|
try {
|
|
132
130
|
json = JSON.parse(readFileSync(pool.lastSwapJson, "utf8"));
|
|
@@ -137,55 +135,32 @@ export function loadLastSwapAt(pool: PoolPaths): number | null {
|
|
|
137
135
|
}
|
|
138
136
|
|
|
139
137
|
export function saveLastSwapAt(pool: PoolPaths, ts: number): void {
|
|
138
|
+
if (pool.lastSwapJson == null) throw new Error("this pool keeps no swap clock");
|
|
140
139
|
writeFileAtomic(pool.lastSwapJson, JSON.stringify(LastSwapSchema.parse({ ts })));
|
|
141
140
|
}
|
|
142
141
|
|
|
143
|
-
const DepletedWaitSchema = z.object({ waitUntil: z.number(), id: z.string(), ts: z.number() });
|
|
144
|
-
export type DepletedWait = z.infer<typeof DepletedWaitSchema>;
|
|
145
|
-
|
|
146
|
-
export function loadDepletedWait(): DepletedWait | null {
|
|
147
|
-
if (!existsSync(paths.depletedJson)) return null;
|
|
148
|
-
let json: unknown;
|
|
149
|
-
try {
|
|
150
|
-
json = JSON.parse(readFileSync(paths.depletedJson, "utf8"));
|
|
151
|
-
} catch {
|
|
152
|
-
throw new Error(`${paths.depletedJson} is corrupt (unparsable JSON) - repair or remove the file`);
|
|
153
|
-
}
|
|
154
|
-
return DepletedWaitSchema.parse(json);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function saveDepletedWait(rec: DepletedWait): void {
|
|
158
|
-
writeFileAtomic(paths.depletedJson, JSON.stringify(DepletedWaitSchema.parse(rec)));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function clearDepletedWait(): void {
|
|
162
|
-
rmSync(paths.depletedJson, { force: true });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
142
|
export const POST_SWAP_COOLDOWN_MS = 45_000;
|
|
166
143
|
|
|
167
144
|
const USAGE_TS_REFRESH_MS = 10 * 60_000;
|
|
168
145
|
const SAMPLED_AT_REFRESH_MS = 30_000;
|
|
169
146
|
|
|
170
|
-
export function writeUsage(input: UsageState
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
const sampledAt = stamp ? (carriedSampleAt ?? input.sampledAt) : input.ts;
|
|
175
|
-
const next: UsageState = { ...input, ...(sampledAt != null ? { sampledAt } : {}) };
|
|
147
|
+
export function writeUsage(input: UsageState): boolean {
|
|
148
|
+
const file = usageJsonFor(input.account);
|
|
149
|
+
const prev = loadUsageSnapshot(input.account)?.state ?? null;
|
|
150
|
+
const next: UsageState = { ...input, sampledAt: input.ts };
|
|
176
151
|
if (
|
|
177
152
|
prev &&
|
|
178
153
|
isEqual({ ...prev, ts: 0, sampledAt: 0 }, { ...next, ts: 0, sampledAt: 0 }) &&
|
|
179
154
|
next.ts - prev.ts < USAGE_TS_REFRESH_MS &&
|
|
180
|
-
|
|
155
|
+
next.ts - (prev.sampledAt ?? prev.ts) < SAMPLED_AT_REFRESH_MS
|
|
181
156
|
) {
|
|
182
157
|
try {
|
|
183
|
-
utimesSync(
|
|
158
|
+
utimesSync(file, new Date(next.ts), new Date(next.ts));
|
|
184
159
|
} catch (e) {
|
|
185
160
|
if (ErrnoSchema.safeParse(e).data?.code !== "ENOENT") throw e;
|
|
186
161
|
}
|
|
187
162
|
return false;
|
|
188
163
|
}
|
|
189
|
-
writeFileAtomic(
|
|
164
|
+
writeFileAtomic(file, JSON.stringify(next));
|
|
190
165
|
return true;
|
|
191
166
|
}
|
package/src/lib/types.ts
CHANGED
|
@@ -54,7 +54,7 @@ const ModelInfoSchema = z.object({ id: z.string(), display: z.string() });
|
|
|
54
54
|
export type ModelInfo = z.infer<typeof ModelInfoSchema>;
|
|
55
55
|
|
|
56
56
|
export const UsageStateSchema = AggregateWindowsSchema.extend({
|
|
57
|
-
account: z.string()
|
|
57
|
+
account: z.string(),
|
|
58
58
|
ts: z.number(),
|
|
59
59
|
model: ModelInfoSchema.nullable().default(null),
|
|
60
60
|
sampledAt: z.number().optional(),
|
|
@@ -130,7 +130,7 @@ export const ConfigSchema = z
|
|
|
130
130
|
export type Config = z.infer<typeof ConfigSchema>;
|
|
131
131
|
|
|
132
132
|
export const RespawnMarkerSchema = z.object({
|
|
133
|
-
|
|
133
|
+
accountId: z.string(),
|
|
134
134
|
ts: z.number(),
|
|
135
135
|
waitUntil: z.number(),
|
|
136
136
|
sessionId: z.string(),
|
|
@@ -186,15 +186,6 @@ export const SubagentStatusLineStdinSchema = z.looseObject({
|
|
|
186
186
|
.catch(undefined),
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
-
export const RefreshResponseSchema = z.looseObject({
|
|
190
|
-
access_token: z.string(),
|
|
191
|
-
refresh_token: z.string().optional(),
|
|
192
|
-
expires_in: z.number().optional(),
|
|
193
|
-
refresh_token_expires_in: z.number().optional(),
|
|
194
|
-
scope: z.string().optional(),
|
|
195
|
-
token_type: z.string().optional(),
|
|
196
|
-
});
|
|
197
|
-
|
|
198
189
|
export const ProfileResponseSchema = z.looseObject({
|
|
199
190
|
account: z.looseObject({ uuid: z.string(), email: z.string().nullish() }),
|
|
200
191
|
organization: z.looseObject({ uuid: z.string(), name: z.string().nullish() }),
|