tokenmaxxing 1.32.0 → 1.33.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/entries/statusline.ts +9 -3
- package/src/entries/stopfailurehook.ts +19 -22
- package/src/entries/supervisor.ts +1 -1
- package/src/lib/decide.ts +45 -75
- package/src/lib/types.ts +2 -2
- package/src/lib/usage.ts +2 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.0",
|
|
4
4
|
"description": "Automatic Claude Code account switching: pool multiple accounts and hot-swap when quota fills, resuming your session on the fresh account.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -80,11 +80,16 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
|
|
|
80
80
|
const seatUuid =
|
|
81
81
|
(ctx.liveAccount != null && ctx.accounts.accounts.some((a) => a.accountUuid === ctx.liveAccount) ? ctx.liveAccount : null) ??
|
|
82
82
|
ctx.accounts.activeAccountUuid;
|
|
83
|
+
const walled = (a: Account) => a.enforcedUntil != null && a.enforcedUntil > ctx.now;
|
|
84
|
+
const wallSeg = (wall: number) => seg("", { usedPercentage: 100, resetsAt: wall }, wall);
|
|
85
|
+
const seat = ctx.accounts.accounts.find((a) => a.accountUuid === seatUuid);
|
|
86
|
+
if (seat?.enforcedUntil != null && walled(seat)) windows.push(wallSeg(seat.enforcedUntil));
|
|
87
|
+
const seatMarker = seat && walled(seat) ? paint(100)("◆") : col.green("◆");
|
|
83
88
|
const active =
|
|
84
89
|
windows.length > 0
|
|
85
|
-
? `${
|
|
90
|
+
? `${seatMarker} ${windows.join(" ")}`
|
|
86
91
|
: seatUuid != null
|
|
87
|
-
? `${
|
|
92
|
+
? `${seatMarker} ?`
|
|
88
93
|
: "";
|
|
89
94
|
|
|
90
95
|
const parked = sortBy(
|
|
@@ -92,7 +97,8 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
|
|
|
92
97
|
[(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, ctx.now)],
|
|
93
98
|
);
|
|
94
99
|
const poolSeg = (a: Account): string => {
|
|
95
|
-
const marker = a.needsReauth ? col.red("✗") : col.cyan("◇");
|
|
100
|
+
const marker = a.needsReauth ? col.red("✗") : walled(a) ? paint(100)("◇") : col.cyan("◇");
|
|
101
|
+
if (a.enforcedUntil != null && walled(a)) return `${marker} ${wallSeg(a.enforcedUntil)}`;
|
|
96
102
|
const week = a.lastUsage?.sevenDay;
|
|
97
103
|
if (week == null) return `${marker} ?`;
|
|
98
104
|
const weekUsed = used(week);
|
|
@@ -3,14 +3,13 @@ import { z } from "zod";
|
|
|
3
3
|
import { paths } from "../lib/paths.ts";
|
|
4
4
|
import { writeFileAtomic } from "../lib/atomic.ts";
|
|
5
5
|
import { readOAuthAccount } from "../lib/claudejson.ts";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
7
|
+
import { withLock } from "../lib/lock.ts";
|
|
8
|
+
import { POST_SWAP_COOLDOWN_MS, loadConfig, loadLastSwapAt } from "../lib/state.ts";
|
|
8
9
|
import { classifyEnforcedLimit, findEnforcedRow, parseErrorBody, readTranscriptTail } from "../lib/usage.ts";
|
|
9
10
|
import { RespawnMarkerSchema, type EnforcedLimit } from "../lib/types.ts";
|
|
10
11
|
import { log } from "../lib/log.ts";
|
|
11
12
|
|
|
12
|
-
export const RETRIGGER_PROMPT = "Continue where the previous turn left off; it was interrupted by a usage limit and tokenmaxxing switched accounts.";
|
|
13
|
-
|
|
14
13
|
const StopFailureStdin = z.looseObject({
|
|
15
14
|
session_id: z.uuid().optional().catch(undefined),
|
|
16
15
|
transcript_path: z.string().optional().catch(undefined),
|
|
@@ -30,7 +29,8 @@ async function readStdin(): Promise<string> {
|
|
|
30
29
|
export async function runStopFailureHook(): Promise<number> {
|
|
31
30
|
if (process.env.TOKENMAXXING_PROBE) return 0;
|
|
32
31
|
|
|
33
|
-
const
|
|
32
|
+
const entry = await withLock(paths.lockFile, () => ({ swapClock: loadLastSwapAt(), account: readOAuthAccount()?.accountUuid ?? null }));
|
|
33
|
+
const account = entry.account;
|
|
34
34
|
const now = Date.now();
|
|
35
35
|
const raw = await readStdin();
|
|
36
36
|
const parsed = StopFailureStdin.safeParse((() => { try { return JSON.parse(raw); } catch { return {}; } })());
|
|
@@ -44,29 +44,27 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
44
44
|
const canPause = process.env.TOKENMAXXING_SUPERVISED === "1" && pinnedSid != null && mainLoop;
|
|
45
45
|
|
|
46
46
|
try {
|
|
47
|
+
const lastSwapAt = loadLastSwapAt();
|
|
48
|
+
if (lastSwapAt != null && lastSwapAt === entry.swapClock && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
|
|
49
|
+
log("stopfailure.cooldown", { sinceSwapMs: now - lastSwapAt });
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
47
52
|
const cfg = loadConfig();
|
|
48
|
-
const
|
|
53
|
+
const row = stdin.transcript_path
|
|
49
54
|
? findEnforcedRow({ rows: readTranscriptTail(stdin.transcript_path), lastAssistantMessage: stdin.last_assistant_message, now })
|
|
50
55
|
: null;
|
|
51
|
-
const limit =
|
|
56
|
+
const limit = row ? classifyEnforcedLimit(row, cfg.policy.switchModels) : null;
|
|
52
57
|
|
|
53
58
|
let enforced: EnforcedLimit | null = null;
|
|
54
|
-
if (limit &&
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
log("stopfailure.enforced", { kind: limit.kind, family: limit.kind === "model" ? limit.family : undefined, outcome: stamp.outcome, resetsAt: stamp.resetsAt, subagent: !mainLoop });
|
|
58
|
-
if (stamp.outcome !== "account-moved") {
|
|
59
|
-
enforced = { account, family: limit.kind === "model" ? limit.family : null, resetsAt: stamp.resetsAt, windowMs: enforcedWindowMs(limit) };
|
|
60
|
-
}
|
|
61
|
-
} else {
|
|
62
|
-
log("stopfailure.unproven", { kind: limit.kind });
|
|
63
|
-
}
|
|
59
|
+
if (limit && account) {
|
|
60
|
+
enforced = { account, kind: limit.kind, family: limit.kind === "model" ? limit.family : null, resetsAt: limit.resetsAt, blind: !mainLoop && limit.kind !== "model" };
|
|
61
|
+
log("stopfailure.enforced", { kind: limit.kind, family: enforced.family ?? undefined, resetsAt: limit.resetsAt, subagent: !mainLoop });
|
|
64
62
|
} else {
|
|
65
63
|
log("stopfailure.unclassified", {
|
|
66
|
-
row:
|
|
67
|
-
type:
|
|
68
|
-
transient:
|
|
69
|
-
body:
|
|
64
|
+
row: row != null,
|
|
65
|
+
type: row?.quotaLimits?.rateLimitType,
|
|
66
|
+
transient: row?.apiErrorIsTransient,
|
|
67
|
+
body: row ? parseErrorBody(row.errorDetails)?.error?.type : undefined,
|
|
70
68
|
});
|
|
71
69
|
}
|
|
72
70
|
|
|
@@ -78,7 +76,6 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
78
76
|
ts: Date.now(),
|
|
79
77
|
waitUntil: decision.waitUntil ?? now,
|
|
80
78
|
sessionId: stdinSid ?? pinnedSid,
|
|
81
|
-
prompt: RETRIGGER_PROMPT,
|
|
82
79
|
...(launchedAt != null ? { launchedAt } : {}),
|
|
83
80
|
});
|
|
84
81
|
writeFileAtomic(marker, JSON.stringify(payload));
|
|
@@ -292,7 +292,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
292
292
|
if (await countdownWait(m.account, m.waitUntil)) overriddenUntil = m.waitUntil;
|
|
293
293
|
} else process.stdout.write(`\n\x1b[36m↻ tokenmaxxing: switched to ${m.account} - resuming...\x1b[0m\n`);
|
|
294
294
|
saveSessionFlags(m.sessionId, persistable, process.cwd());
|
|
295
|
-
launchArgs = ["--resume", m.sessionId, ...persistable
|
|
295
|
+
launchArgs = ["--resume", m.sessionId, ...persistable];
|
|
296
296
|
continue;
|
|
297
297
|
}
|
|
298
298
|
log("supervisor.exit", { sid, respawns, code: child.exitCode, signal: child.signalCode });
|
package/src/lib/decide.ts
CHANGED
|
@@ -2,11 +2,11 @@ import { maxBy } from "es-toolkit";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { withLock } from "./lock.ts";
|
|
4
4
|
import { paths } from "./paths.ts";
|
|
5
|
-
import { POST_SWAP_COOLDOWN_MS, loadAccounts, loadConfig, loadDepletedWait, loadLastSwapAt,
|
|
5
|
+
import { POST_SWAP_COOLDOWN_MS, loadAccounts, loadConfig, loadDepletedWait, loadLastSwapAt, loadUsageSnapshot, saveAccounts, saveDepletedWait, writeUsage } from "./state.ts";
|
|
6
6
|
import { readOAuthAccount } from "./claudejson.ts";
|
|
7
7
|
import { isSkippableSwapError, performSwap } from "./swap.ts";
|
|
8
|
-
import { nextWeeklyReset, pickBest, pickEarliestReset, thresholdBars, usableAt } from "./picker.ts";
|
|
9
|
-
import { familyTokens, gatedFamilies, keepRows, probeUsage
|
|
8
|
+
import { isExhausted, nextWeeklyReset, pickBest, pickEarliestReset, thresholdBars, usableAt } from "./picker.ts";
|
|
9
|
+
import { familyTokens, gatedFamilies, keepRows, probeUsage } from "./usage.ts";
|
|
10
10
|
import { log } from "./log.ts";
|
|
11
11
|
import { AccountSchema, UsageStateSchema, type Account, type Config, type EnforcedLimit, type Thresholds, type UsageState, type UsageWindow } from "./types.ts";
|
|
12
12
|
|
|
@@ -40,7 +40,9 @@ function capForFamily(account: Account, family: string, now: number): UsageWindo
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function isOver(u: UsageState | null, account: Account | undefined, bars: Thresholds, cfg: Config, now: number): boolean {
|
|
43
|
-
if (!
|
|
43
|
+
if (!account) return false;
|
|
44
|
+
if (account.enforcedUntil != null && account.enforcedUntil > now) return true;
|
|
45
|
+
if (!u || u.account !== account.accountUuid) return false;
|
|
44
46
|
if (
|
|
45
47
|
liveUsed({ window: u.fiveHour, windowMs: FIVE_HOURS_MS, sampledAt: u.ts, now }) >= bars.session ||
|
|
46
48
|
liveUsed({ window: u.sevenDay, windowMs: WEEK_MS, sampledAt: u.ts, now }) >= bars.weekly
|
|
@@ -75,14 +77,15 @@ function freshest(u: UsageState | null, uAt: number | null, account: Account | u
|
|
|
75
77
|
return { ...u, ts: uAt };
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
async function loadFreshSnapshots(cfg: Config, account: string | null, now: number): Promise<Snapshots> {
|
|
80
|
+
async function loadFreshSnapshots(cfg: Config, account: string | null, now: number, probe: boolean): Promise<Snapshots> {
|
|
79
81
|
const snap = loadUsageSnapshot();
|
|
80
82
|
let u = snap?.state ?? null;
|
|
81
83
|
let uAt = snap?.at ?? null;
|
|
82
84
|
const ttl = cfg.policy.usagePollTtlMs;
|
|
83
85
|
const stored = loadAccounts().accounts.find((a) => a.accountUuid === account);
|
|
84
86
|
const probeAttempted = stored?.lastProbeAt != null && now - stored.lastProbeAt <= ttl;
|
|
85
|
-
|
|
87
|
+
const walled = stored?.enforcedUntil != null && stored.enforcedUntil > now;
|
|
88
|
+
if (probe && account && stored && !walled && !probeAttempted && (!usageFresh(u, uAt, account, ttl, now) || needsPerModel(u, cfg))) {
|
|
86
89
|
const startedAt = Date.now();
|
|
87
90
|
const full = await probeUsage();
|
|
88
91
|
const ts = Date.now();
|
|
@@ -121,21 +124,20 @@ async function loadFreshSnapshots(cfg: Config, account: string | null, now: numb
|
|
|
121
124
|
|
|
122
125
|
export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = false, enforced: EnforcedLimit | null = null): Promise<SwapDecision> {
|
|
123
126
|
const activeAccount = readOAuthAccount()?.accountUuid ?? null;
|
|
124
|
-
const enforced0 = enforced && enforced.account === activeAccount ? enforced : null;
|
|
125
127
|
|
|
126
128
|
const lastSwapAt = loadLastSwapAt();
|
|
127
|
-
if (!
|
|
129
|
+
if (!enforced && lastSwapAt != null && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
|
|
128
130
|
return depletedReplay(now) ?? { swapped: false, account: null, reason: "post-swap-cooldown" };
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
const cfg = loadConfig();
|
|
132
134
|
const bars = thresholdBars(cfg);
|
|
133
135
|
|
|
134
|
-
const { u: teeUsage, uAt } = await loadFreshSnapshots(cfg, activeAccount, now);
|
|
136
|
+
const { u: teeUsage, uAt } = await loadFreshSnapshots(cfg, activeAccount, now, enforced == null);
|
|
135
137
|
const stored = loadAccounts().accounts.find((a) => a.accountUuid === activeAccount);
|
|
136
138
|
const usage = freshest(teeUsage, uAt, stored);
|
|
137
139
|
|
|
138
|
-
if (!
|
|
140
|
+
if (!enforced && !isOver(usage, stored, bars, cfg, now)) {
|
|
139
141
|
const measured = usage != null && activeAccount != null && usage.account === activeAccount;
|
|
140
142
|
if (!measured) {
|
|
141
143
|
const replay = depletedReplay(now);
|
|
@@ -147,11 +149,20 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
|
|
|
147
149
|
return withLock(paths.lockFile, async () => {
|
|
148
150
|
const idx = loadAccounts();
|
|
149
151
|
const account2 = readOAuthAccount()?.accountUuid ?? null;
|
|
150
|
-
const enforced2 = enforced0 && enforced0.account === account2 ? enforced0 : null;
|
|
151
152
|
const tee = loadUsageSnapshot();
|
|
152
153
|
const active = account2 ? idx.accounts.find((a) => a.accountUuid === account2) : undefined;
|
|
153
154
|
const u2 = tee ? freshest(tee.state, tee.at, active) : usage;
|
|
154
155
|
|
|
156
|
+
const origin = enforced ? idx.accounts.find((a) => a.accountUuid === enforced.account) : undefined;
|
|
157
|
+
const prior = origin?.enforcedUntil != null && origin.enforcedUntil > now;
|
|
158
|
+
if (enforced && origin) {
|
|
159
|
+
origin.enforcedUntil = Math.max(origin.enforcedUntil ?? 0, enforcedWall(enforced, origin, now));
|
|
160
|
+
origin.lastProbeAt = now;
|
|
161
|
+
saveAccounts(idx);
|
|
162
|
+
log("usage.enforced_limit", { kind: enforced.kind, family: enforced.family ?? undefined, resetsAt: origin.enforcedUntil, blind: prior || enforced.blind, live: origin === active });
|
|
163
|
+
}
|
|
164
|
+
const enforced2 = enforced && origin && origin === active ? enforced : null;
|
|
165
|
+
|
|
155
166
|
if (account2 != null && !active) {
|
|
156
167
|
return { swapped: false, account: null, reason: "live-credential-not-in-pool" };
|
|
157
168
|
}
|
|
@@ -162,10 +173,14 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
|
|
|
162
173
|
saveAccounts(idx);
|
|
163
174
|
}
|
|
164
175
|
|
|
165
|
-
const
|
|
166
|
-
const
|
|
176
|
+
const walled = active?.enforcedUntil != null && active.enforcedUntil > now;
|
|
177
|
+
const blindScreen = (enforced != null && (prior || enforced.blind)) || (walled && !enforced2);
|
|
178
|
+
const gated = blindScreen ? cfg.policy.switchModels : gatedFamilies(u2?.model ?? null, cfg.policy.switchModels);
|
|
179
|
+
const family = enforced?.family ?? null;
|
|
180
|
+
const switchFamilies = family != null && !gated.includes(family) ? [...gated, family] : gated;
|
|
167
181
|
|
|
168
|
-
|
|
182
|
+
const seatExhausted = active != null && isExhausted(active, { now, thresholds: bars, currentAccountUuid: account2, switchFamilies });
|
|
183
|
+
if (!enforced2 && !isOver(u2, active, bars, cfg, now) && !(enforced && seatExhausted)) {
|
|
169
184
|
return depletedReplay(now) ?? { swapped: false, account: null, reason: "raced-already-swapped" };
|
|
170
185
|
}
|
|
171
186
|
|
|
@@ -201,8 +216,7 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
|
|
|
201
216
|
const fresh = loadAccounts();
|
|
202
217
|
const current = seatOf(fresh);
|
|
203
218
|
const ctx = { now, thresholds: bars, currentAccountUuid: current?.accountUuid ?? null, switchFamilies };
|
|
204
|
-
const
|
|
205
|
-
const currentAt = current ? Math.max(usableAt(current, ctx), enforcedUntil) : Number.POSITIVE_INFINITY;
|
|
219
|
+
const currentAt = current ? usableAt(current, ctx) : Number.POSITIVE_INFINITY;
|
|
206
220
|
const other = pickEarliestReset(usable(fresh.accounts), ctx);
|
|
207
221
|
|
|
208
222
|
let target: Account | null = null;
|
|
@@ -245,63 +259,19 @@ function depletedReplay(now: number): SwapDecision | null {
|
|
|
245
259
|
return { swapped: false, account, reason: "depleted-wait", waitUntil: rec.waitUntil };
|
|
246
260
|
}
|
|
247
261
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const { limit, account: accountUuid, now } = input;
|
|
264
|
-
return withLock(paths.lockFile, () => {
|
|
265
|
-
if ((readOAuthAccount()?.accountUuid ?? null) !== accountUuid) return { outcome: "account-moved", resetsAt: limit.resetsAt };
|
|
266
|
-
const prior = loadUsage();
|
|
267
|
-
const priorSame = prior && prior.account === accountUuid ? prior : null;
|
|
268
|
-
const idx = loadAccounts();
|
|
269
|
-
const account = idx.accounts.find((a) => a.accountUuid === accountUuid);
|
|
270
|
-
if (limit.kind === "model") {
|
|
271
|
-
const rows = account?.lastUsage?.perModel ?? {};
|
|
272
|
-
const knownReset = Object.entries(rows).filter(([k]) => familyTokens(k).includes(limit.family)).map(([, w]) => w.resetsAt).find((r): r is number => r != null) ?? null;
|
|
273
|
-
const weeklyReset = priorSame?.sevenDay.resetsAt ?? account?.lastUsage?.sevenDay.resetsAt ?? null;
|
|
274
|
-
const resetsAt = limit.resetsAt ?? nextWeeklyReset(knownReset ?? weeklyReset, now);
|
|
275
|
-
if (!account) return { outcome: "no-carrier", resetsAt };
|
|
276
|
-
if (account.lastUsage) {
|
|
277
|
-
const others = Object.fromEntries(Object.entries(rows).filter(([k]) => !familyTokens(k).includes(limit.family)));
|
|
278
|
-
account.lastUsage = { ...account.lastUsage, perModel: { ...others, [limit.family]: { usedPercentage: 100, resetsAt } }, ...(resetsAt == null ? { rowsAt: now } : {}) };
|
|
279
|
-
} else {
|
|
280
|
-
account.enforcedUntil = resetsAt ?? now + WEEK_MS;
|
|
281
|
-
}
|
|
282
|
-
account.lastProbeAt = now;
|
|
283
|
-
saveAccounts(idx);
|
|
284
|
-
log("usage.enforced_limit", { kind: limit.kind, family: limit.family, resetsAt });
|
|
285
|
-
return { outcome: "stamped", resetsAt };
|
|
286
|
-
}
|
|
287
|
-
if (account) {
|
|
288
|
-
account.lastProbeAt = now;
|
|
289
|
-
if (limit.resetsAt != null) account.enforcedUntil = limit.resetsAt;
|
|
290
|
-
saveAccounts(idx);
|
|
291
|
-
}
|
|
292
|
-
const carrier = priorSame ?? (account?.lastUsage ? { ...account.lastUsage, model: null } : null);
|
|
293
|
-
if (!carrier) return { outcome: "no-carrier", resetsAt: limit.resetsAt };
|
|
294
|
-
const window: UsageWindow = { usedPercentage: 100, resetsAt: limit.resetsAt };
|
|
295
|
-
writeUsage({
|
|
296
|
-
...(priorSame ?? {}),
|
|
297
|
-
...(priorSame == null && account?.lastUsageAt != null ? { sampledAt: account.lastUsageAt } : {}),
|
|
298
|
-
fiveHour: limit.kind === "session" ? window : carrier.fiveHour,
|
|
299
|
-
sevenDay: limit.kind === "weekly" ? window : carrier.sevenDay,
|
|
300
|
-
account: accountUuid,
|
|
301
|
-
ts: now,
|
|
302
|
-
model: carrier.model,
|
|
303
|
-
}, { stamp: true });
|
|
304
|
-
log("usage.enforced_limit", { kind: limit.kind, resetsAt: limit.resetsAt });
|
|
305
|
-
return { outcome: "stamped", resetsAt: limit.resetsAt };
|
|
306
|
-
});
|
|
262
|
+
function enforcedWall(limit: EnforcedLimit, account: Account, now: number): number {
|
|
263
|
+
const cached = account.lastUsage;
|
|
264
|
+
const { family } = limit;
|
|
265
|
+
const familyReset =
|
|
266
|
+
family == null
|
|
267
|
+
? null
|
|
268
|
+
: Object.entries(cached?.perModel ?? {})
|
|
269
|
+
.filter(([k]) => familyTokens(k).includes(family))
|
|
270
|
+
.map(([, w]) => w.resetsAt)
|
|
271
|
+
.find((r): r is number => r != null) ?? null;
|
|
272
|
+
const cachedReset =
|
|
273
|
+
limit.kind === "session"
|
|
274
|
+
? cached?.fiveHour.resetsAt != null && cached.fiveHour.resetsAt > now ? cached.fiveHour.resetsAt : null
|
|
275
|
+
: nextWeeklyReset(familyReset ?? cached?.sevenDay.resetsAt ?? null, now);
|
|
276
|
+
return limit.resetsAt ?? cachedReset ?? now + (limit.kind === "session" ? FIVE_HOURS_MS : WEEK_MS);
|
|
307
277
|
}
|
package/src/lib/types.ts
CHANGED
|
@@ -82,9 +82,10 @@ export type AccountsIndex = z.infer<typeof AccountsIndexSchema>;
|
|
|
82
82
|
|
|
83
83
|
export const EnforcedLimitSchema = z.object({
|
|
84
84
|
account: z.string(),
|
|
85
|
+
kind: z.enum(["session", "weekly", "model"]),
|
|
85
86
|
family: z.string().nullable(),
|
|
86
87
|
resetsAt: z.number().nullable(),
|
|
87
|
-
|
|
88
|
+
blind: z.boolean(),
|
|
88
89
|
});
|
|
89
90
|
export type EnforcedLimit = z.infer<typeof EnforcedLimitSchema>;
|
|
90
91
|
|
|
@@ -128,7 +129,6 @@ export const RespawnMarkerSchema = z.object({
|
|
|
128
129
|
ts: z.number(),
|
|
129
130
|
waitUntil: z.number(),
|
|
130
131
|
sessionId: z.string(),
|
|
131
|
-
prompt: z.string().optional(),
|
|
132
132
|
launchedAt: z.number().optional(),
|
|
133
133
|
});
|
|
134
134
|
|
package/src/lib/usage.ts
CHANGED
|
@@ -169,13 +169,7 @@ export function transcriptRowText(row: TranscriptRow): string {
|
|
|
169
169
|
|
|
170
170
|
const ROW_RECENCY_MS = 60_000;
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
row: TranscriptRowSchema,
|
|
174
|
-
errorAt: z.number().nullable(),
|
|
175
|
-
});
|
|
176
|
-
export type EnforcedRow = z.infer<typeof EnforcedRowSchema>;
|
|
177
|
-
|
|
178
|
-
export function findEnforcedRow(input: { rows: TranscriptRow[]; lastAssistantMessage: string | undefined; now: number }): EnforcedRow | null {
|
|
172
|
+
export function findEnforcedRow(input: { rows: TranscriptRow[]; lastAssistantMessage: string | undefined; now: number }): TranscriptRow | null {
|
|
179
173
|
const { rows, lastAssistantMessage, now } = input;
|
|
180
174
|
for (let i = rows.length - 1; i >= 0; i--) {
|
|
181
175
|
const row = rows[i]!;
|
|
@@ -183,8 +177,7 @@ export function findEnforcedRow(input: { rows: TranscriptRow[]; lastAssistantMes
|
|
|
183
177
|
const ts = row.timestamp ? Date.parse(row.timestamp) : Number.NaN;
|
|
184
178
|
const byContent = lastAssistantMessage != null && lastAssistantMessage !== "" && transcriptRowText(row) === lastAssistantMessage;
|
|
185
179
|
const byRecency = Number.isFinite(ts) && Math.abs(now - ts) <= ROW_RECENCY_MS;
|
|
186
|
-
if (
|
|
187
|
-
return { row, errorAt: Number.isFinite(ts) ? ts : null };
|
|
180
|
+
if (byContent || byRecency) return row;
|
|
188
181
|
}
|
|
189
182
|
return null;
|
|
190
183
|
}
|