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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.85.0",
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 current = await pool.read(account, loadConfig(), now);
144
- pool.fold(current);
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
- await withLock(p.pool.lockFile, async () => {
92
- idx = loadAccounts(p.pool);
93
- const notice = process.stderr.isTTY;
94
- if (notice) process.stderr.write(c.dim(`sampling ${p.name} usage...`));
95
- try {
96
- liveId = p.liveId();
97
- reports = await p.samplePool(idx.accounts, liveId, now);
98
- } finally {
99
- if (notice) process.stderr.write("\r\x1b[2K");
100
- }
101
- saveAccounts(p.pool, idx);
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 && enforced != null, enforced, { waiterId: canRespawn ? session?.sid : undefined });
84
- if (enforced && session && canRespawn && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
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.windows = mergeWindows(windowsOf(outcome.usage, startedAt), a.windows);
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.lastUsageAt = at;
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: thresholdBars(cfg), currentId: null, families: gatedFamilies(model, cfg.policy.switchModels), seats: presence() };
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 CodexReadOutcome = { ok: true; usage: CodexUsage; at: number } | { ok: false; reason: string; deadGrant: boolean };
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 readCodexUsage(account: Account, now: number, refresh: boolean, holder?: string): Promise<CodexReadOutcome> {
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
- if (isCodexAccessExpiring({ auth, now })) {
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 || e instanceof CodexUsageReadError) return { ok: false, reason: e.message, deadGrant: false };
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, true);
92
- if (outcome.ok) {
93
- applyUsage(account, outcome.usage, outcome.at);
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
 
@@ -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> }): Promise<CompactOutcome> {
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.usedPercentage < bar) return 0;
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.windowSeconds != null ? w.sampledAt + w.windowSeconds * 1000 : Number.POSITIVE_INFINITY;
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 = observed.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.windows = mergeWindows(windowsOf(outcome.usage, startedAt), stored.windows);
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);