tokenmaxxing 1.85.0 → 1.87.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/serve.ts +6 -5
- package/src/cli/status.ts +36 -15
- package/src/entries/stopfailurehook.ts +2 -2
- package/src/entries/supervisor.ts +6 -1
- package/src/lib/claude.ts +8 -8
- package/src/lib/codex.ts +41 -23
- package/src/lib/compact.ts +3 -1
- package/src/lib/decide.ts +2 -5
- package/src/lib/picker.ts +13 -5
- package/src/lib/sample.ts +6 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.87.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/serve.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type { Provider } from "../lib/provider.ts";
|
|
|
14
14
|
import { foldTee } from "../lib/sample.ts";
|
|
15
15
|
import { loadAccounts, loadConfig } from "../lib/state.ts";
|
|
16
16
|
import { OAUTH_USAGE_URL } from "../lib/usage.ts";
|
|
17
|
-
import type { Account, Config } from "../lib/types.ts";
|
|
17
|
+
import type { Account, Config, Thresholds } from "../lib/types.ts";
|
|
18
18
|
import { c, emitError } from "./render.ts";
|
|
19
19
|
import { usageReport, type UsageReport, type WindowReport } from "./status.ts";
|
|
20
20
|
|
|
@@ -24,7 +24,7 @@ const STALE_AFTER_MS = 2 * 60 * 60_000;
|
|
|
24
24
|
type HubPool = {
|
|
25
25
|
provider: Provider;
|
|
26
26
|
usageUrl: string;
|
|
27
|
-
fold: (account: Account) => void;
|
|
27
|
+
fold: (account: Account, thresholds: Thresholds) => void;
|
|
28
28
|
read: (account: Account, cfg: Config, now: number) => Promise<Account>;
|
|
29
29
|
body: (usage: UsageReport, account: Account) => unknown;
|
|
30
30
|
};
|
|
@@ -96,7 +96,7 @@ function authFiles(cfg: Config, now: number): unknown {
|
|
|
96
96
|
const files = POOLS.flatMap((pool) => {
|
|
97
97
|
const ctx = { now, thresholds: thresholdBars(cfg), currentId: null, families: pool.provider.gatedFamilies(cfg), seats: null };
|
|
98
98
|
return loadAccounts(pool.provider.pool).accounts.map((a) => {
|
|
99
|
-
pool.fold(a);
|
|
99
|
+
pool.fold(a, ctx.thresholds);
|
|
100
100
|
return {
|
|
101
101
|
id: a.id,
|
|
102
102
|
auth_index: authIndex(pool, a),
|
|
@@ -140,8 +140,9 @@ async function apiCall(req: Request): Promise<Response> {
|
|
|
140
140
|
const account = loadAccounts(pool.provider.pool).accounts.find((a) => authIndex(pool, a) === index);
|
|
141
141
|
if (!account) return json({ error: "auth credential not found for auth_index" }, 400);
|
|
142
142
|
const now = Date.now();
|
|
143
|
-
const
|
|
144
|
-
pool.
|
|
143
|
+
const cfg = loadConfig();
|
|
144
|
+
const current = await pool.read(account, cfg, now);
|
|
145
|
+
pool.fold(current, thresholdBars(cfg));
|
|
145
146
|
const usage = usageReport({ ...current, windows: current.windows.filter((w) => now - w.sampledAt <= STALE_AFTER_MS) }, now);
|
|
146
147
|
if (!usage || (!usage.fiveHour && !usage.week)) return json({ error: `no usage figure newer than ${STALE_AFTER_MS / 3_600_000}h for this account` }, 502);
|
|
147
148
|
return json({ status_code: 200, header: { "Content-Type": ["application/json"] }, body: JSON.stringify(pool.body(usage, current)) });
|
package/src/cli/status.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { opencodeGo } from "../lib/opencodego.ts";
|
|
|
6
6
|
import { loadAccounts, loadConfig, saveAccounts } from "../lib/state.ts";
|
|
7
7
|
import { withLock } from "../lib/lock.ts";
|
|
8
8
|
import { codexPool, grokPool, opencodeGoPool } from "../lib/paths.ts";
|
|
9
|
-
import { earliestReset, gatedWindows, isExhausted, isSessionWindow, limitWindows, liveUsed, nextWeeklyReset, sessionWindow, thresholdBars, weeklyWindow } from "../lib/picker.ts";
|
|
9
|
+
import { earliestReset, gatedWindows, landWindows, isExhausted, isSessionWindow, limitWindows, liveUsed, nextWeeklyReset, sessionWindow, thresholdBars, weeklyWindow } from "../lib/picker.ts";
|
|
10
10
|
import type { Provider, SampleReport } from "../lib/provider.ts";
|
|
11
11
|
import { bar, c, count, emitJson, fmtAgo } from "./render.ts";
|
|
12
12
|
import type { Account, Config, Thresholds, Window } from "../lib/types.ts";
|
|
@@ -84,25 +84,46 @@ function gatedNote(accounts: Account[], families: string[] | null, weeklyBar: nu
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
async function collect(p: Provider, cfg: Config, now: number, cached: boolean): Promise<PoolReport> {
|
|
87
|
+
const bars = thresholdBars(cfg);
|
|
87
88
|
let idx = loadAccounts(p.pool);
|
|
88
89
|
let reports = new Map<string, SampleReport>();
|
|
89
90
|
let liveId: string | null = null;
|
|
90
91
|
if (!cached && idx.accounts.length > 0) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
const notice = process.stderr.isTTY;
|
|
93
|
+
if (notice) process.stderr.write(c.dim(`sampling ${p.name} usage...`));
|
|
94
|
+
try {
|
|
95
|
+
liveId = p.liveId();
|
|
96
|
+
const retries = new Map(idx.accounts.map((a) => [a.id, a.usageRetryAt ?? null]));
|
|
97
|
+
reports = await p.samplePool(idx.accounts, liveId, now);
|
|
98
|
+
await withLock(p.pool.lockFile, () => {
|
|
99
|
+
const fresh = loadAccounts(p.pool);
|
|
100
|
+
let dirty = false;
|
|
101
|
+
for (const a of idx.accounts) {
|
|
102
|
+
const stored = fresh.accounts.find((x) => x.id === a.id);
|
|
103
|
+
if (!stored) continue;
|
|
104
|
+
if (a.needsReauth === true && stored.needsReauth !== true) {
|
|
105
|
+
stored.needsReauth = true;
|
|
106
|
+
dirty = true;
|
|
107
|
+
}
|
|
108
|
+
if ((a.usageRetryAt ?? null) !== retries.get(a.id)) {
|
|
109
|
+
stored.usageRetryAt = a.usageRetryAt;
|
|
110
|
+
dirty = true;
|
|
111
|
+
}
|
|
112
|
+
if (a.lastUsageAt != null && (stored.lastUsageAt == null || a.lastUsageAt > stored.lastUsageAt)) {
|
|
113
|
+
landWindows(stored, p.mergeWindows(a.windows, stored.windows), a.lastUsageAt, bars);
|
|
114
|
+
if (a.email != null) stored.email = a.email;
|
|
115
|
+
if (a.tier != null) stored.tier = a.tier;
|
|
116
|
+
dirty = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (dirty) saveAccounts(p.pool, fresh);
|
|
120
|
+
idx = fresh;
|
|
121
|
+
});
|
|
122
|
+
} finally {
|
|
123
|
+
if (notice) process.stderr.write("\r\x1b[2K");
|
|
124
|
+
}
|
|
103
125
|
}
|
|
104
126
|
|
|
105
|
-
const bars = thresholdBars(cfg);
|
|
106
127
|
const present = p.presence();
|
|
107
128
|
const ctx = { now, thresholds: bars, currentId: null, families: p.gatedFamilies(cfg), seats: null };
|
|
108
129
|
const ordered = sortBy(idx.accounts, [(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, now)]);
|
|
@@ -235,7 +256,7 @@ export async function cmdStatus(opts: { json?: boolean; cached?: boolean } = {})
|
|
|
235
256
|
console.log(c.dim("no claude accounts (run `tokenmaxxing init` to pool claude too)"));
|
|
236
257
|
console.log();
|
|
237
258
|
} else {
|
|
238
|
-
const header = `thresholds 5h ${claudeReport.thresholds.session}% weekly ${claudeReport.thresholds.weekly}% (${count({ n: claudeReport.accounts.length, noun: "claude account" })})`;
|
|
259
|
+
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" })})`;
|
|
239
260
|
renderPool(claude, claudeReport, header, Date.now(), cfg.policy.usagePollTtlMs);
|
|
240
261
|
}
|
|
241
262
|
}
|
|
@@ -80,8 +80,8 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
80
80
|
);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
const decision = await evaluateAndMaybeSwap(claude, now, canRespawn
|
|
84
|
-
if (
|
|
83
|
+
const decision = await evaluateAndMaybeSwap(claude, now, canRespawn, enforced, { waiterId: canRespawn ? session?.sid : undefined });
|
|
84
|
+
if (session && canRespawn && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
|
|
85
85
|
writeRespawnMarker({ session, accountId: decision.account.id, waitUntil: decision.waitUntil ?? now, compact: false, origin: "stopfailure" });
|
|
86
86
|
log("stopfailure.marker", { session: session.sid.slice(0, 8), live: session.live.slice(0, 8), account: decision.account.id.slice(0, 8), waitUntil: decision.waitUntil ?? now });
|
|
87
87
|
} else {
|
|
@@ -498,7 +498,12 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
498
498
|
delete compactEnv.TOKENMAXXING_SESSION_ID;
|
|
499
499
|
delete compactEnv.TOKENMAXXING_LAUNCHED_AT;
|
|
500
500
|
delete compactEnv.TOKENMAXXING_MODEL;
|
|
501
|
-
const outcome = await compactClaudeSession({ real, sid: m.sessionId, transcript, env: compactEnv });
|
|
501
|
+
const outcome = await compactClaudeSession({ real, sid: m.sessionId, transcript, env: compactEnv, onSpawn: (p) => { child = p; } });
|
|
502
|
+
child = null;
|
|
503
|
+
if (terminating) {
|
|
504
|
+
await releaseClaim();
|
|
505
|
+
return 143;
|
|
506
|
+
}
|
|
502
507
|
log("supervisor.compact", { sid: m.sessionId.slice(0, 8), seat: seat.id.slice(0, 8), ok: outcome.ok, reason: outcome.ok ? undefined : outcome.reason });
|
|
503
508
|
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})`);
|
|
504
509
|
compacted = outcome.ok;
|
package/src/lib/claude.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { withLock } from "./lock.ts";
|
|
|
8
8
|
import { errorMessage, log } from "./log.ts";
|
|
9
9
|
import { claudeTierLabel, describeIdentity, fetchTokenIdentity, isDeadCredential, InvalidGrantError } from "./oauth.ts";
|
|
10
10
|
import { claudePool, env, paths, seatFromEnv, storeDirFor } from "./paths.ts";
|
|
11
|
-
import { pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
|
|
11
|
+
import { landWindows, pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
|
|
12
12
|
import { deletePiStore } from "./piauth.ts";
|
|
13
13
|
import { seatCounts } from "./presence.ts";
|
|
14
14
|
import { StoreUnusableError, type Observation, type Provider, type SampleReport } from "./provider.ts";
|
|
@@ -48,8 +48,7 @@ async function observeLive(account: Account, cfg: Config, now: number, opts: { p
|
|
|
48
48
|
if (outcome.ok) {
|
|
49
49
|
a.probeFails = 0;
|
|
50
50
|
if (a.lastUsageAt == null || startedAt > a.lastUsageAt) {
|
|
51
|
-
a
|
|
52
|
-
a.lastUsageAt = startedAt;
|
|
51
|
+
landWindows(a, mergeWindows(windowsOf(outcome.usage, startedAt), a.windows), startedAt, thresholdBars(cfg));
|
|
53
52
|
}
|
|
54
53
|
} else {
|
|
55
54
|
a.probeFails = (a.probeFails ?? 0) + 1;
|
|
@@ -64,12 +63,13 @@ async function observeLive(account: Account, cfg: Config, now: number, opts: { p
|
|
|
64
63
|
|
|
65
64
|
async function samplePool(accounts: Account[]): Promise<Map<string, SampleReport>> {
|
|
66
65
|
const reports = new Map<string, SampleReport>();
|
|
66
|
+
const bars = thresholdBars(loadConfig());
|
|
67
67
|
await Promise.all(
|
|
68
68
|
accounts.map(async (a) => {
|
|
69
69
|
const tee = loadUsageSnapshot(a.id);
|
|
70
70
|
const teeAt = tee == null ? null : (tee.state.sampledAt ?? tee.state.ts);
|
|
71
71
|
if (teeAt != null && (a.lastUsageAt == null || teeAt >= a.lastUsageAt)) {
|
|
72
|
-
foldTee(a);
|
|
72
|
+
foldTee(a, bars);
|
|
73
73
|
reports.set(a.id, { ok: true, source: "statusline" });
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
@@ -80,8 +80,7 @@ async function samplePool(accounts: Account[]): Promise<Map<string, SampleReport
|
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
82
|
const at = Date.now();
|
|
83
|
-
a.
|
|
84
|
-
a.windows = mergeWindows(windowsOf(outcome.usage, at), a.windows);
|
|
83
|
+
landWindows(a, mergeWindows(windowsOf(outcome.usage, at), a.windows), at, bars);
|
|
85
84
|
reports.set(a.id, { ok: true, source: "probe" });
|
|
86
85
|
}),
|
|
87
86
|
);
|
|
@@ -120,11 +119,12 @@ async function storeUsable(a: Account): Promise<boolean> {
|
|
|
120
119
|
|
|
121
120
|
export function pickSeat(now: number, model: ModelInfo | null, eligible: (a: Account) => boolean = () => true): Account | null {
|
|
122
121
|
const cfg = loadConfig();
|
|
122
|
+
const bars = thresholdBars(cfg);
|
|
123
123
|
const idx = loadAccounts(claudePool);
|
|
124
124
|
let dirty = false;
|
|
125
|
-
for (const a of idx.accounts) dirty = foldTee(a) || dirty;
|
|
125
|
+
for (const a of idx.accounts) dirty = foldTee(a, bars) || dirty;
|
|
126
126
|
if (dirty) saveAccounts(claudePool, idx);
|
|
127
|
-
const ctx: PickCtx = { now, thresholds:
|
|
127
|
+
const ctx: PickCtx = { now, thresholds: bars, currentId: null, families: gatedFamilies(model, cfg.policy.switchModels), seats: presence() };
|
|
128
128
|
const candidates = idx.accounts.filter(eligible);
|
|
129
129
|
return pickBest(candidates, ctx) ?? pickEarliestReset(candidates, ctx)?.account ?? null;
|
|
130
130
|
}
|
package/src/lib/codex.ts
CHANGED
|
@@ -33,9 +33,10 @@ function applyUsage(account: Account, usage: CodexUsage, at: number): void {
|
|
|
33
33
|
if (usage.planType != null) account.tier = usage.planType;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
type
|
|
36
|
+
type CodexReadFailure = { ok: false; reason: string; deadGrant: boolean; expiring?: boolean };
|
|
37
|
+
type CodexReadOutcome = { ok: true; usage: CodexUsage; at: number } | CodexReadFailure;
|
|
37
38
|
|
|
38
|
-
async function
|
|
39
|
+
async function freshCodexAuth(account: Account, now: number, refresh: boolean, holder?: string): Promise<{ ok: true; auth: CodexAuthJson } | CodexReadFailure> {
|
|
39
40
|
let auth: CodexAuthJson | null;
|
|
40
41
|
try {
|
|
41
42
|
auth = readCodexStoreAuth(account.id);
|
|
@@ -43,22 +44,30 @@ async function readCodexUsage(account: Account, now: number, refresh: boolean, h
|
|
|
43
44
|
return { ok: false, reason: `store credential unreadable (${errorMessage(e).slice(0, 80)})`, deadGrant: false };
|
|
44
45
|
}
|
|
45
46
|
if (!auth) return { ok: false, reason: "no credential in this account's store - run `tokenmaxxing auth --codex`", deadGrant: false };
|
|
47
|
+
if (!isCodexAccessExpiring({ auth, now })) return { ok: true, auth };
|
|
48
|
+
if (!refresh) return { ok: false, reason: "stored access token is expiring and this read never refreshes a store", deadGrant: false, expiring: true };
|
|
49
|
+
if (livingPresences(codexPaths.presenceDir).some((p) => p.accountId === account.id && p.id !== holder)) {
|
|
50
|
+
return { ok: false, reason: "running in a live codex session (store refresh unsafe)", deadGrant: false };
|
|
51
|
+
}
|
|
46
52
|
try {
|
|
47
|
-
|
|
48
|
-
if (!refresh) {
|
|
49
|
-
return { ok: false, reason: "stored access token is expiring and this read never refreshes a store", deadGrant: false };
|
|
50
|
-
}
|
|
51
|
-
if (livingPresences(codexPaths.presenceDir).some((p) => p.accountId === account.id && p.id !== holder)) {
|
|
52
|
-
return { ok: false, reason: "running in a live codex session (store refresh unsafe)", deadGrant: false };
|
|
53
|
-
}
|
|
54
|
-
auth = await refreshCodexAuth({ auth, now });
|
|
55
|
-
writeCodexStoreAuth(account.id, auth);
|
|
56
|
-
}
|
|
57
|
-
const at = Date.now();
|
|
58
|
-
return { ok: true, usage: await fetchCodexUsage({ auth, at }), at };
|
|
53
|
+
auth = await refreshCodexAuth({ auth, now });
|
|
59
54
|
} catch (e) {
|
|
60
55
|
if (e instanceof CodexInvalidGrantError) return { ok: false, reason: e.message, deadGrant: true };
|
|
61
|
-
if (e instanceof CodexRefreshFailedError
|
|
56
|
+
if (e instanceof CodexRefreshFailedError) return { ok: false, reason: e.message, deadGrant: false };
|
|
57
|
+
throw e;
|
|
58
|
+
}
|
|
59
|
+
writeCodexStoreAuth(account.id, auth);
|
|
60
|
+
return { ok: true, auth };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function readCodexUsage(account: Account, now: number, refresh: boolean, holder?: string): Promise<CodexReadOutcome> {
|
|
64
|
+
const fresh = await freshCodexAuth(account, now, refresh, holder);
|
|
65
|
+
if (!fresh.ok) return fresh;
|
|
66
|
+
try {
|
|
67
|
+
const at = Date.now();
|
|
68
|
+
return { ok: true, usage: await fetchCodexUsage({ auth: fresh.auth, at }), at };
|
|
69
|
+
} catch (e) {
|
|
70
|
+
if (e instanceof CodexUsageReadError) return { ok: false, reason: e.message, deadGrant: false };
|
|
62
71
|
throw e;
|
|
63
72
|
}
|
|
64
73
|
}
|
|
@@ -86,18 +95,27 @@ export async function observeCodex(account: Account, cfg: Config, now: number, o
|
|
|
86
95
|
|
|
87
96
|
async function samplePool(accounts: Account[], _liveId: string | null, now: number): Promise<Map<string, SampleReport>> {
|
|
88
97
|
const reports = new Map<string, SampleReport>();
|
|
98
|
+
const record = (account: Account, outcome: CodexReadOutcome): void => {
|
|
99
|
+
if (outcome.ok) {
|
|
100
|
+
applyUsage(account, outcome.usage, outcome.at);
|
|
101
|
+
reports.set(account.id, { ok: true, source: "probe" });
|
|
102
|
+
} else {
|
|
103
|
+
if (outcome.deadGrant) account.needsReauth = true;
|
|
104
|
+
reports.set(account.id, { ok: false, reason: outcome.reason });
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const expiring: Account[] = [];
|
|
89
108
|
await Promise.all(
|
|
90
109
|
accounts.map(async (account) => {
|
|
91
|
-
const outcome = await readCodexUsage(account, now,
|
|
92
|
-
if (outcome.ok)
|
|
93
|
-
|
|
94
|
-
reports.set(account.id, { ok: true, source: "probe" });
|
|
95
|
-
} else {
|
|
96
|
-
if (outcome.deadGrant) account.needsReauth = true;
|
|
97
|
-
reports.set(account.id, { ok: false, reason: outcome.reason });
|
|
98
|
-
}
|
|
110
|
+
const outcome = await readCodexUsage(account, now, false);
|
|
111
|
+
if (!outcome.ok && outcome.expiring === true) expiring.push(account);
|
|
112
|
+
else record(account, outcome);
|
|
99
113
|
}),
|
|
100
114
|
);
|
|
115
|
+
if (expiring.length > 0) {
|
|
116
|
+
const rotated = await withLock(codexPool.lockFile, () => Promise.all(expiring.map(async (account) => ({ account, fresh: await freshCodexAuth(account, now, true) }))));
|
|
117
|
+
await Promise.all(rotated.map(async ({ account, fresh }) => record(account, fresh.ok ? await readCodexUsage(account, now, false) : fresh)));
|
|
118
|
+
}
|
|
101
119
|
return reports;
|
|
102
120
|
}
|
|
103
121
|
|
package/src/lib/compact.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
|
+
import type { Subprocess } from "bun";
|
|
2
3
|
import { delay } from "es-toolkit";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import pkg from "../../package.json" with { type: "json" };
|
|
@@ -13,7 +14,7 @@ const PIPE_GRACE_MS = 2_000;
|
|
|
13
14
|
|
|
14
15
|
const CompactBoundarySchema = z.looseObject({ type: z.literal("system"), subtype: z.literal("compact_boundary") });
|
|
15
16
|
|
|
16
|
-
export async function compactClaudeSession(input: { real: string; sid: string; transcript: string; env: Record<string, string | undefined
|
|
17
|
+
export async function compactClaudeSession(input: { real: string; sid: string; transcript: string; env: Record<string, string | undefined>; onSpawn: (child: Subprocess) => void }): Promise<CompactOutcome> {
|
|
17
18
|
const offset = statSync(input.transcript).size;
|
|
18
19
|
const p = Bun.spawn([input.real, "-p", "--resume", input.sid, "/compact"], {
|
|
19
20
|
env: input.env,
|
|
@@ -23,6 +24,7 @@ export async function compactClaudeSession(input: { real: string; sid: string; t
|
|
|
23
24
|
timeout: CLAUDE_COMPACT_KILL_MS,
|
|
24
25
|
killSignal: "SIGKILL",
|
|
25
26
|
});
|
|
27
|
+
input.onSpawn(p);
|
|
26
28
|
const reads = Promise.all([p.stdout.text(), p.stderr.text()]);
|
|
27
29
|
const settled = await Promise.race([reads, p.exited.then(() => delay(PIPE_GRACE_MS)).then(() => null)]);
|
|
28
30
|
await p.exited;
|
package/src/lib/decide.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { countBy } from "es-toolkit";
|
|
2
2
|
import { withLock } from "./lock.ts";
|
|
3
3
|
import { loadAccounts, loadConfig, liveWaitClaims, releaseWaitClaim, replaceWaitClaim, saveAccounts } from "./state.ts";
|
|
4
|
-
import { isExhausted, limitWindows, liveUsed, nextWeeklyReset, pickBest, pickWaitTarget, sessionWindow, thresholdBars, weeklyWindow, type PickCtx } from "./picker.ts";
|
|
4
|
+
import { isExhausted, landWindows, limitWindows, liveUsed, nextWeeklyReset, pickBest, pickWaitTarget, sessionWindow, thresholdBars, weeklyWindow, type PickCtx } from "./picker.ts";
|
|
5
5
|
import { familyTokens } from "./usage.ts";
|
|
6
6
|
import { errorMessage, log } from "./log.ts";
|
|
7
7
|
import type { Observation, Provider } from "./provider.ts";
|
|
@@ -100,10 +100,7 @@ export async function evaluateAndMaybeSwap(p: Provider, now = Date.now(), canRes
|
|
|
100
100
|
for (const a of idx.accounts) {
|
|
101
101
|
const obs = await p.observeLive(a, cfg, now, { probe: false, perModel: false });
|
|
102
102
|
if (a === active) obs2 = obs;
|
|
103
|
-
if (obs && (a.lastUsageAt == null || obs.at > a.lastUsageAt))
|
|
104
|
-
a.windows = p.mergeWindows(obs.windows, a.windows);
|
|
105
|
-
a.lastUsageAt = obs.at;
|
|
106
|
-
}
|
|
103
|
+
if (obs && (a.lastUsageAt == null || obs.at > a.lastUsageAt)) landWindows(a, p.mergeWindows(obs.windows, a.windows), obs.at, bars);
|
|
107
104
|
}
|
|
108
105
|
saveAccounts(p.pool, idx);
|
|
109
106
|
|
package/src/lib/picker.ts
CHANGED
|
@@ -53,16 +53,16 @@ function barFor(w: Window, thresholds: Thresholds): number {
|
|
|
53
53
|
return isSessionWindow(w) ? thresholds.session : thresholds.weekly;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function blockedUntil(w: Window, bar: number): number {
|
|
57
|
-
if (w
|
|
56
|
+
function blockedUntil(w: Window, bar: number, now: number): number {
|
|
57
|
+
if (liveUsed(w, now) < bar) return 0;
|
|
58
58
|
if (w.resetsAt != null) return w.resetsAt;
|
|
59
|
-
return w.
|
|
59
|
+
return w.sampledAt + (w.windowSeconds ?? 0) * 1000;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function blockingUntil(a: Account, ctx: PickCtx): number[] {
|
|
63
63
|
return [
|
|
64
|
-
...a.windows.filter((w) => w.name == null).map((w) => blockedUntil(w, barFor(w, ctx.thresholds))),
|
|
65
|
-
...gatedWindows(a, ctx.families).map((w) => blockedUntil(w, barFor(w, ctx.thresholds))),
|
|
64
|
+
...a.windows.filter((w) => w.name == null).map((w) => blockedUntil(w, barFor(w, ctx.thresholds), ctx.now)),
|
|
65
|
+
...gatedWindows(a, ctx.families).map((w) => blockedUntil(w, barFor(w, ctx.thresholds), ctx.now)),
|
|
66
66
|
...(a.enforcedUntil != null ? [a.enforcedUntil] : []),
|
|
67
67
|
];
|
|
68
68
|
}
|
|
@@ -71,6 +71,14 @@ export function isExhausted(a: Account, ctx: PickCtx): boolean {
|
|
|
71
71
|
return blockingUntil(a, ctx).some((t) => t > ctx.now);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
export function landWindows(a: Account, windows: Window[], at: number, thresholds: Thresholds): void {
|
|
75
|
+
const ctx: PickCtx = { now: at, thresholds, currentId: null, families: [], seats: null };
|
|
76
|
+
const blocked = a.enforcedUntil != null && isExhausted({ ...a, enforcedUntil: undefined }, ctx);
|
|
77
|
+
a.windows = windows;
|
|
78
|
+
a.lastUsageAt = at;
|
|
79
|
+
if (blocked && !isExhausted({ ...a, enforcedUntil: undefined }, ctx)) a.enforcedUntil = undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
export function nextWeeklyReset(resetsAt: number | null, now: number): number | null {
|
|
75
83
|
if (resetsAt == null || resetsAt > now) return resetsAt;
|
|
76
84
|
return resetsAt + (Math.floor((now - resetsAt) / WEEK_MS) + 1) * WEEK_MS;
|
package/src/lib/sample.ts
CHANGED
|
@@ -3,11 +3,12 @@ import { withLock } from "./lock.ts";
|
|
|
3
3
|
import { errorMessage, log } from "./log.ts";
|
|
4
4
|
import { claudeTierLabel, isDeadCredential } from "./oauth.ts";
|
|
5
5
|
import { claudePool, paths } from "./paths.ts";
|
|
6
|
+
import { landWindows, thresholdBars } from "./picker.ts";
|
|
6
7
|
import { seatCounts } from "./presence.ts";
|
|
7
8
|
import type { Observation } from "./provider.ts";
|
|
8
9
|
import { loadAccounts, loadUsageSnapshot, saveAccounts } from "./state.ts";
|
|
9
10
|
import { fetchUsageDirect, mergeWindows, windowsOf } from "./usage.ts";
|
|
10
|
-
import type { Account, Config, UsageWindows } from "./types.ts";
|
|
11
|
+
import type { Account, Config, Thresholds, UsageWindows } from "./types.ts";
|
|
11
12
|
|
|
12
13
|
export type SampleOutcome = { ok: true; usage: UsageWindows; via: "get" } | { ok: false; reason: string; retryAt?: number };
|
|
13
14
|
|
|
@@ -23,11 +24,10 @@ export function teeObservation(account: Account): Observation | null {
|
|
|
23
24
|
return { windows: mergeWindows(aggregate, account.windows), at };
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export function foldTee(account: Account): boolean {
|
|
27
|
+
export function foldTee(account: Account, thresholds: Thresholds): boolean {
|
|
27
28
|
const observed = teeObservation(account);
|
|
28
29
|
if (!observed || (account.lastUsageAt != null && observed.at <= account.lastUsageAt)) return false;
|
|
29
|
-
account.windows
|
|
30
|
-
account.lastUsageAt = observed.at;
|
|
30
|
+
landWindows(account, observed.windows, observed.at, thresholds);
|
|
31
31
|
return true;
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -79,7 +79,7 @@ export async function sampleOldest(cfg: Config): Promise<void> {
|
|
|
79
79
|
const now = Date.now();
|
|
80
80
|
const idx = loadAccounts(claudePool);
|
|
81
81
|
let dirty = false;
|
|
82
|
-
for (const a of idx.accounts) dirty = foldTee(a) || dirty;
|
|
82
|
+
for (const a of idx.accounts) dirty = foldTee(a, thresholdBars(cfg)) || dirty;
|
|
83
83
|
const sampledAt = (a: Account) => Math.max(a.lastUsageAt ?? 0, a.lastProbeAt ?? 0);
|
|
84
84
|
const seats = seatCounts(paths.presenceDir);
|
|
85
85
|
const stale = idx.accounts
|
|
@@ -123,8 +123,7 @@ export async function sampleOldest(cfg: Config): Promise<void> {
|
|
|
123
123
|
dirty = true;
|
|
124
124
|
}
|
|
125
125
|
if (stored && outcome.ok && (stored.lastUsageAt == null || startedAt > stored.lastUsageAt)) {
|
|
126
|
-
stored
|
|
127
|
-
stored.lastUsageAt = startedAt;
|
|
126
|
+
landWindows(stored, mergeWindows(windowsOf(outcome.usage, startedAt), stored.windows), startedAt, thresholdBars(cfg));
|
|
128
127
|
dirty = true;
|
|
129
128
|
}
|
|
130
129
|
if (dirty) saveAccounts(claudePool, idx);
|