tokenmaxxing 1.18.0 → 1.19.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 +2 -0
- package/README.md +4 -3
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/switching-policy/SKILL.md +1 -0
- package/agent-plugin/skills/switching-policy/references/policy.md +2 -1
- package/package.json +1 -1
- package/src/cli/check.ts +3 -0
- package/src/cli/config.ts +1 -0
- package/src/cli/status.ts +13 -3
- package/src/cli/switch.ts +63 -38
- package/src/entries/codexstophook.ts +1 -0
- package/src/entries/stopfailurehook.ts +9 -6
- package/src/entries/stophook.ts +1 -0
- package/src/entries/supervisor.ts +2 -1
- package/src/lib/bankedreset.ts +234 -0
- package/src/lib/codexdecide.ts +25 -1
- package/src/lib/codexpick.ts +3 -3
- package/src/lib/codexreset.ts +122 -0
- package/src/lib/codexusage.ts +9 -0
- package/src/lib/decide.ts +75 -8
- package/src/lib/oauth.ts +17 -4
- package/src/lib/paths.ts +1 -0
- package/src/lib/sample.ts +62 -17
- package/src/lib/state.ts +93 -3
- package/src/lib/swap.ts +87 -12
- package/src/lib/types.ts +54 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { MAX_WRAP_DEPTH, WRAP_DEPTH_ENV, resolveRealClaude } from "./claudebin.ts";
|
|
5
|
+
import { withClaudeRefreshLock } from "./claudelock.ts";
|
|
6
|
+
import { liveTarget, mergeIntoLive, readItem, writeItem } from "./credstore.ts";
|
|
7
|
+
import { http, safeErrorDetail } from "./http.ts";
|
|
8
|
+
import { log } from "./log.ts";
|
|
9
|
+
import { InvalidGrantError, fetchTokenIdentity, isAccessTokenExpiring, isDeadCredential, refreshCredential } from "./oauth.ts";
|
|
10
|
+
import { clearDepletedWait, clearNextCheck, loadAccounts, loadUsage, saveAccounts, saveLastResetAt, writeUsage } from "./state.ts";
|
|
11
|
+
import { keepRotatedPair } from "./swap.ts";
|
|
12
|
+
import { normalizeResetsAt } from "./usage.ts";
|
|
13
|
+
import { CredentialBlobSchema, type Account, type BankedResetOutcome, type BankedResetRecord, type OAuthCreds } from "./types.ts";
|
|
14
|
+
|
|
15
|
+
const EnvOverrideSchema = z.string().min(1).optional().catch(undefined);
|
|
16
|
+
const API_BASE_URL = EnvOverrideSchema.parse(process.env.TOKENMAXXING_OAUTH_API_BASE_URL) ?? "https://api.anthropic.com";
|
|
17
|
+
const OAUTH_BETA = "oauth-2025-04-20";
|
|
18
|
+
const RESET_PROGRAM = "juniper_tide";
|
|
19
|
+
const CLAIM_TIMEOUT_MS = 25_000;
|
|
20
|
+
const VERSION_PROBE_TIMEOUT_MS = 5_000;
|
|
21
|
+
const REFRESH_SKEW_MS = 300_000;
|
|
22
|
+
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
23
|
+
const INELIGIBLE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
24
|
+
|
|
25
|
+
const ClaimResponseSchema = z.looseObject({
|
|
26
|
+
result: z.enum(["reset", "already_used", "not_limited", "ineligible", "unavailable"]).catch("unavailable"),
|
|
27
|
+
next_available_at: z.union([z.string(), z.number()]).nullable().optional().catch(null),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const ClaimSchema = z.object({ outcome: z.enum(["reset", "pass"]) });
|
|
31
|
+
export type Claim = z.infer<typeof ClaimSchema>["outcome"];
|
|
32
|
+
|
|
33
|
+
export function bankedResetBelievedAvailable(account: Account, now: number): boolean {
|
|
34
|
+
const rec = account.bankedReset;
|
|
35
|
+
if (!rec) return true;
|
|
36
|
+
if (rec.nextAvailableAt != null) return rec.nextAvailableAt <= now;
|
|
37
|
+
if (rec.outcome === "reset" || rec.outcome === "already_used") return now - rec.at >= WEEK_MS;
|
|
38
|
+
if (rec.outcome === "ineligible") return now - rec.at >= INELIGIBLE_TTL_MS;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const VersionSchema = z.string().regex(/^\d+\.\d+\.\d+\S*$/);
|
|
43
|
+
const PackageJsonSchema = z.looseObject({ version: VersionSchema });
|
|
44
|
+
|
|
45
|
+
function claudeVersion(): string | undefined {
|
|
46
|
+
const bin = resolveRealClaude();
|
|
47
|
+
const fromPath = VersionSchema.safeParse(bin.match(/\/versions\/(\d+\.\d+\.\d+[^/]*)$/)?.[1]);
|
|
48
|
+
if (fromPath.success) return fromPath.data;
|
|
49
|
+
try {
|
|
50
|
+
const pkg = PackageJsonSchema.safeParse(JSON.parse(readFileSync(join(dirname(bin), "package.json"), "utf8")));
|
|
51
|
+
if (pkg.success) return pkg.data.version;
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const r = Bun.spawnSync([bin, "--version"], {
|
|
56
|
+
env: { ...process.env, TOKENMAXXING_PROBE: "1", [WRAP_DEPTH_ENV]: String(MAX_WRAP_DEPTH) },
|
|
57
|
+
stdout: "pipe",
|
|
58
|
+
stderr: "pipe",
|
|
59
|
+
timeout: VERSION_PROBE_TIMEOUT_MS,
|
|
60
|
+
});
|
|
61
|
+
const spawned = VersionSchema.safeParse(r.stdout.toString("utf8").trim().match(/^(\d+\.\d+\.\d+\S*)/)?.[1]);
|
|
62
|
+
if (spawned.success) return spawned.data;
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function claudeUserAgent(): string {
|
|
69
|
+
const version = claudeVersion();
|
|
70
|
+
return version ? `claude-cli/${version} (external, cli)` : "claude-cli (external, cli)";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const LiveTokenSchema = z.discriminatedUnion("ok", [
|
|
74
|
+
z.object({ ok: z.literal(true), accessToken: z.string(), organizationUuid: z.string() }),
|
|
75
|
+
z.object({ ok: z.literal(false), outcome: z.enum(["auth_error", "error"]), detail: z.string() }),
|
|
76
|
+
]);
|
|
77
|
+
type LiveToken = z.infer<typeof LiveTokenSchema>;
|
|
78
|
+
|
|
79
|
+
function liveAccessToken(raw: string | null): string | null {
|
|
80
|
+
if (raw == null) return null;
|
|
81
|
+
const parsed = CredentialBlobSchema.safeParse((() => {
|
|
82
|
+
try { return JSON.parse(raw); } catch { return null; }
|
|
83
|
+
})());
|
|
84
|
+
return parsed.success ? parsed.data.claudeAiOauth.accessToken : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function liveTokenLocked(account: Account, lock: { compromised: () => boolean }): Promise<LiveToken> {
|
|
88
|
+
const raw = await readItem(liveTarget());
|
|
89
|
+
if (!raw) return { ok: false, outcome: "auth_error", detail: "no live credential" };
|
|
90
|
+
let creds: OAuthCreds;
|
|
91
|
+
try {
|
|
92
|
+
creds = CredentialBlobSchema.parse(JSON.parse(raw)).claudeAiOauth;
|
|
93
|
+
} catch (e) {
|
|
94
|
+
return { ok: false, outcome: "error", detail: `live credential blob unreadable (${(e instanceof Error ? e.message : String(e)).slice(0, 80)})` };
|
|
95
|
+
}
|
|
96
|
+
if (isDeadCredential(creds)) return { ok: false, outcome: "auth_error", detail: "live credential was cleared after a failed refresh" };
|
|
97
|
+
if (isAccessTokenExpiring(creds, REFRESH_SKEW_MS)) {
|
|
98
|
+
let next: OAuthCreds;
|
|
99
|
+
try {
|
|
100
|
+
next = await refreshCredential(creds);
|
|
101
|
+
} catch (e) {
|
|
102
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
103
|
+
return { ok: false, outcome: e instanceof InvalidGrantError ? "auth_error" : "error", detail };
|
|
104
|
+
}
|
|
105
|
+
if (lock.compromised()) {
|
|
106
|
+
let owner: Account | null = null;
|
|
107
|
+
try {
|
|
108
|
+
const id = await fetchTokenIdentity(next.accessToken);
|
|
109
|
+
owner = loadAccounts().accounts.find((a) => a.accountUuid === id.accountUuid) ?? null;
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
const kept = await keepRotatedPair({ fresh: next, owner, fallback: account, liveOwnerUuid: account.accountUuid, expectedLiveToken: creds.accessToken, lock });
|
|
113
|
+
return { ok: false, outcome: "error", detail: `refresh lock compromised mid-refresh - the rotated token ${kept}` };
|
|
114
|
+
}
|
|
115
|
+
await writeItem(liveTarget(), mergeIntoLive(raw, next));
|
|
116
|
+
creds = next;
|
|
117
|
+
}
|
|
118
|
+
let identity;
|
|
119
|
+
try {
|
|
120
|
+
identity = await fetchTokenIdentity(creds.accessToken);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
return { ok: false, outcome: "error", detail: e instanceof Error ? e.message : String(e) };
|
|
123
|
+
}
|
|
124
|
+
if (identity.accountUuid !== account.accountUuid) {
|
|
125
|
+
return { ok: false, outcome: "error", detail: `live credential belongs to account ${identity.accountUuid.slice(0, 8)}, not the seat` };
|
|
126
|
+
}
|
|
127
|
+
return { ok: true, accessToken: creds.accessToken, organizationUuid: identity.organizationUuid };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const PostResultSchema = z.object({
|
|
131
|
+
outcome: z.enum(["reset", "already_used", "not_limited", "ineligible", "unavailable", "rate_limited", "auth_error", "error"]),
|
|
132
|
+
nextAvailableAt: z.number().nullable(),
|
|
133
|
+
detail: z.string(),
|
|
134
|
+
ambiguous: z.boolean().optional(),
|
|
135
|
+
});
|
|
136
|
+
type PostResult = z.infer<typeof PostResultSchema>;
|
|
137
|
+
|
|
138
|
+
async function postClaim(input: { accessToken: string; organizationUuid: string; userAgent: string }): Promise<PostResult> {
|
|
139
|
+
const url = `${API_BASE_URL}/api/organizations/${input.organizationUuid}/reset_rate_limits`;
|
|
140
|
+
let res: Response;
|
|
141
|
+
try {
|
|
142
|
+
res = await http.post(url, {
|
|
143
|
+
headers: {
|
|
144
|
+
Authorization: `Bearer ${input.accessToken}`,
|
|
145
|
+
"anthropic-beta": OAUTH_BETA,
|
|
146
|
+
"Content-Type": "application/json",
|
|
147
|
+
"User-Agent": input.userAgent,
|
|
148
|
+
},
|
|
149
|
+
body: JSON.stringify({ program: RESET_PROGRAM }),
|
|
150
|
+
timeout: CLAIM_TIMEOUT_MS,
|
|
151
|
+
});
|
|
152
|
+
} catch (e) {
|
|
153
|
+
return { outcome: "error", nextAvailableAt: null, detail: `reset endpoint unreachable: ${e instanceof Error ? e.message : String(e)}` };
|
|
154
|
+
}
|
|
155
|
+
let text: string;
|
|
156
|
+
try {
|
|
157
|
+
text = await res.text();
|
|
158
|
+
} catch (e) {
|
|
159
|
+
return { outcome: "error", nextAvailableAt: null, detail: `reset endpoint answered HTTP ${res.status} but the body could not be read: ${e instanceof Error ? e.message : String(e)}`, ambiguous: true };
|
|
160
|
+
}
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
const detail = `HTTP ${res.status}: ${safeErrorDetail({ text })}`;
|
|
163
|
+
if (res.status === 429) return { outcome: "rate_limited", nextAvailableAt: null, detail };
|
|
164
|
+
if (res.status === 401 || res.status === 403) return { outcome: "auth_error", nextAvailableAt: null, detail };
|
|
165
|
+
return { outcome: "error", nextAvailableAt: null, detail };
|
|
166
|
+
}
|
|
167
|
+
const parsed = ClaimResponseSchema.safeParse((() => {
|
|
168
|
+
try { return JSON.parse(text); } catch { return null; }
|
|
169
|
+
})());
|
|
170
|
+
if (!parsed.success) {
|
|
171
|
+
return { outcome: "error", nextAvailableAt: null, detail: `reset endpoint answered HTTP ${res.status} with an unrecognized body (${text.length} bytes, withheld)`, ambiguous: true };
|
|
172
|
+
}
|
|
173
|
+
return { outcome: parsed.data.result, nextAvailableAt: normalizeResetsAt(parsed.data.next_available_at), detail: parsed.data.result };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function record(accountUuid: string, rec: BankedResetRecord, reset: boolean, now: number): void {
|
|
177
|
+
const idx = loadAccounts();
|
|
178
|
+
const account = idx.accounts.find((a) => a.accountUuid === accountUuid);
|
|
179
|
+
if (!account) return;
|
|
180
|
+
account.bankedReset = rec;
|
|
181
|
+
if (reset) {
|
|
182
|
+
delete account.enforcedUntil;
|
|
183
|
+
if (account.lastUsage) {
|
|
184
|
+
account.lastUsage = { ...account.lastUsage, fiveHour: { usedPercentage: 0, resetsAt: null } };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
saveAccounts(idx);
|
|
188
|
+
if (!reset) return;
|
|
189
|
+
const u = loadUsage();
|
|
190
|
+
if (u && u.account === accountUuid) {
|
|
191
|
+
writeUsage({ ...u, fiveHour: { usedPercentage: 0, resetsAt: null }, ts: now }, { stamp: true });
|
|
192
|
+
}
|
|
193
|
+
saveLastResetAt(now);
|
|
194
|
+
clearDepletedWait();
|
|
195
|
+
clearNextCheck();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function claimBankedReset(input: { account: Account }): Promise<Claim> {
|
|
199
|
+
const { account } = input;
|
|
200
|
+
const short = account.accountUuid.slice(0, 8);
|
|
201
|
+
let result: PostResult;
|
|
202
|
+
let moved = false;
|
|
203
|
+
try {
|
|
204
|
+
const userAgent = claudeUserAgent();
|
|
205
|
+
({ result, moved } = await withClaudeRefreshLock(async (lock) => {
|
|
206
|
+
const token = await liveTokenLocked(account, lock);
|
|
207
|
+
if (!token.ok) return { result: { outcome: token.outcome, nextAvailableAt: null, detail: token.detail }, moved: false };
|
|
208
|
+
let posted = await postClaim({ accessToken: token.accessToken, organizationUuid: token.organizationUuid, userAgent });
|
|
209
|
+
if (posted.ambiguous) {
|
|
210
|
+
const again = await postClaim({ accessToken: token.accessToken, organizationUuid: token.organizationUuid, userAgent });
|
|
211
|
+
posted =
|
|
212
|
+
again.outcome === "already_used"
|
|
213
|
+
? { outcome: "reset", nextAvailableAt: again.nextAvailableAt, detail: "already_used on the confirming claim, so the ambiguous claim was the reset", ambiguous: true }
|
|
214
|
+
: again;
|
|
215
|
+
}
|
|
216
|
+
return { result: posted, moved: liveAccessToken(await readItem(liveTarget())) !== token.accessToken };
|
|
217
|
+
}));
|
|
218
|
+
} catch (e) {
|
|
219
|
+
result = { outcome: "error", nextAvailableAt: null, detail: `claim aborted: ${e instanceof Error ? e.message : String(e)}` };
|
|
220
|
+
}
|
|
221
|
+
const outcome: BankedResetOutcome = result.outcome;
|
|
222
|
+
const reset = outcome === "reset";
|
|
223
|
+
const completedAt = Date.now();
|
|
224
|
+
record(account.accountUuid, { outcome, at: completedAt, nextAvailableAt: result.nextAvailableAt }, reset, completedAt);
|
|
225
|
+
log(reset ? "bankedreset.claimed" : "bankedreset.refused", {
|
|
226
|
+
account: short,
|
|
227
|
+
outcome,
|
|
228
|
+
nextAvailableAt: result.nextAvailableAt,
|
|
229
|
+
moved,
|
|
230
|
+
ambiguous: result.ambiguous === true ? true : undefined,
|
|
231
|
+
detail: reset && result.ambiguous !== true ? undefined : result.detail.slice(0, 200),
|
|
232
|
+
});
|
|
233
|
+
return reset && !moved ? "reset" : "pass";
|
|
234
|
+
}
|
package/src/lib/codexdecide.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { performCodexSwap } from "./codexswap.ts";
|
|
|
11
11
|
import { CodexInvalidGrantError, refreshCodexAuth } from "./codexoauth.ts";
|
|
12
12
|
import { fetchCodexUsage } from "./codexusage.ts";
|
|
13
13
|
import { codexIdentityOf, isCodexAccessExpiring, readLiveCodexAuth, writeLiveCodexAuth, writeParkedCodexAuth } from "./codexauth.ts";
|
|
14
|
+
import { codexBankedResetVerdict, consumeLiveCodexBankedReset } from "./codexreset.ts";
|
|
14
15
|
import { liveCodexAccountId } from "./codexsample.ts";
|
|
15
16
|
import { livingCodexPresences, presentCodexAccountIds, targetableCodexAccounts } from "./codexpresence.ts";
|
|
16
17
|
import { terminalBars } from "./picker.ts";
|
|
@@ -21,6 +22,7 @@ const CodexSwapDecisionSchema = z.object({
|
|
|
21
22
|
swapped: z.boolean(),
|
|
22
23
|
account: CodexAccountSchema.nullable(),
|
|
23
24
|
reason: z.string(),
|
|
25
|
+
reset: z.boolean().optional(),
|
|
24
26
|
});
|
|
25
27
|
export type CodexSwapDecision = z.infer<typeof CodexSwapDecisionSchema>;
|
|
26
28
|
|
|
@@ -51,7 +53,7 @@ async function sampleLiveOntoOwner(input: { now: number }): Promise<string | nul
|
|
|
51
53
|
writeParkedCodexAuth({ credFile: owner.credFile, auth: live });
|
|
52
54
|
}
|
|
53
55
|
const usage = await fetchCodexUsage({ auth: live });
|
|
54
|
-
owner.lastUsage = { aggregate: usage.aggregate, perLimit: usage.perLimit };
|
|
56
|
+
owner.lastUsage = { aggregate: usage.aggregate, perLimit: usage.perLimit, resetCredits: usage.resetCredits, reachedType: usage.reachedType };
|
|
55
57
|
owner.lastUsageAt = now;
|
|
56
58
|
if (usage.email != null) owner.email = usage.email;
|
|
57
59
|
if (usage.planType != null) owner.planType = usage.planType;
|
|
@@ -137,6 +139,28 @@ export async function evaluateAndMaybeSwapCodex(input: { now?: number }): Promis
|
|
|
137
139
|
isCodexExhausted({ account: active, thresholds: bars, now });
|
|
138
140
|
if (!engaged) return { swapped: false, account: null, reason: "under-threshold-or-stale" };
|
|
139
141
|
|
|
142
|
+
if (cfg.policy.preferToUseBankedReset.includes("codex") && active.needsReauth !== true) {
|
|
143
|
+
const seat = active;
|
|
144
|
+
const verdict = codexBankedResetVerdict({ account: seat, thresholds: bars, now, pollTtlMs: cfg.policy.usagePollTtlMs });
|
|
145
|
+
if (verdict === "hold") {
|
|
146
|
+
log("codexdecide.banked_reset_hold", { account: seat.accountId.slice(0, 8) });
|
|
147
|
+
return { swapped: false, account: seat, reason: "banked-reset-hold" };
|
|
148
|
+
}
|
|
149
|
+
if (verdict === "claim") {
|
|
150
|
+
const claim = await consumeLiveCodexBankedReset({ account: seat, now });
|
|
151
|
+
if (claim === "reset") {
|
|
152
|
+
try {
|
|
153
|
+
await sampleLiveOntoOwner({ now: Date.now() });
|
|
154
|
+
} catch (e) {
|
|
155
|
+
log("codexdecide.resample_failed", { err: e instanceof Error ? e.message : String(e) });
|
|
156
|
+
}
|
|
157
|
+
const fresh = loadCodexAccounts().accounts.find((account) => account.accountId === activeId) ?? seat;
|
|
158
|
+
if (fresh.lastUsage) postSwapResweep({ liveAccountId: activeId, bars, now });
|
|
159
|
+
return { swapped: false, account: fresh, reason: "banked-reset", reset: true };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
140
164
|
if (!isCodexExhausted({ account: active, thresholds: bars, now })) {
|
|
141
165
|
while (true) {
|
|
142
166
|
const current = loadCodexAccounts();
|
package/src/lib/codexpick.ts
CHANGED
|
@@ -3,20 +3,20 @@ import { nextWeeklyReset } from "./picker.ts";
|
|
|
3
3
|
import { isSessionWindow, weeklyWindowOf } from "./codexusage.ts";
|
|
4
4
|
import type { CodexAccount, CodexWindow, Thresholds } from "./types.ts";
|
|
5
5
|
|
|
6
|
-
function liveUsed(input: { window: CodexWindow; now: number; sampledAt: number | null }): number {
|
|
6
|
+
export function liveUsed(input: { window: CodexWindow; now: number; sampledAt: number | null }): number {
|
|
7
7
|
const { window, now, sampledAt } = input;
|
|
8
8
|
if (window.resetsAt != null) return window.resetsAt <= now ? 0 : window.usedPercentage;
|
|
9
9
|
if (sampledAt != null && window.windowSeconds != null && now >= sampledAt + window.windowSeconds * 1000) return 0;
|
|
10
10
|
return window.usedPercentage;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
function allWindows(account: CodexAccount): CodexWindow[] {
|
|
13
|
+
export function allWindows(account: CodexAccount): CodexWindow[] {
|
|
14
14
|
const usage = account.lastUsage;
|
|
15
15
|
if (!usage) return [];
|
|
16
16
|
return [...usage.aggregate, ...Object.values(usage.perLimit).flat()];
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function barFor(input: { window: CodexWindow; thresholds: Thresholds }): number {
|
|
19
|
+
export function barFor(input: { window: CodexWindow; thresholds: Thresholds }): number {
|
|
20
20
|
return isSessionWindow({ window: input.window }) ? input.thresholds.session : input.thresholds.weekly;
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { codexIdentityOf, readLiveCodexAuth } from "./codexauth.ts";
|
|
3
|
+
import { allWindows, barFor, liveUsed } from "./codexpick.ts";
|
|
4
|
+
import { loadCodexAccounts, saveCodexAccounts } from "./codexstate.ts";
|
|
5
|
+
import { http, safeErrorDetail } from "./http.ts";
|
|
6
|
+
import { log } from "./log.ts";
|
|
7
|
+
import type { CodexAccount, CodexAuthJson, CodexBankedResetOutcome, Thresholds } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
const EnvOverrideSchema = z.string().min(1).optional().catch(undefined);
|
|
10
|
+
const CONSUME_URL =
|
|
11
|
+
EnvOverrideSchema.parse(process.env.TOKENMAXXING_CODEX_RESET_URL) ?? "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume";
|
|
12
|
+
const CONSUME_TIMEOUT_MS = 30_000;
|
|
13
|
+
const RETRY_KEY_TTL_MS = 10 * 60_000;
|
|
14
|
+
const WALL_PERCENT = 100;
|
|
15
|
+
const LIMIT_REACHED = "rate_limit_reached";
|
|
16
|
+
|
|
17
|
+
const VerdictSchema = z.enum(["hold", "claim", "pass"]);
|
|
18
|
+
export type CodexBankedVerdict = z.infer<typeof VerdictSchema>;
|
|
19
|
+
const ClaimSchema = z.enum(["reset", "pass"]);
|
|
20
|
+
export type CodexBankedClaim = z.infer<typeof ClaimSchema>;
|
|
21
|
+
|
|
22
|
+
export function codexBankedResetVerdict(input: { account: CodexAccount; thresholds: Thresholds; now: number; pollTtlMs: number }): CodexBankedVerdict {
|
|
23
|
+
const { account, thresholds, now, pollTtlMs } = input;
|
|
24
|
+
const usage = account.lastUsage;
|
|
25
|
+
if (!usage) return "pass";
|
|
26
|
+
const credits = usage.resetCredits ?? null;
|
|
27
|
+
if (credits == null || credits <= 0) return "pass";
|
|
28
|
+
const rec = account.bankedReset;
|
|
29
|
+
if (rec != null && rec.outcome !== "reset" && now - rec.at < pollTtlMs) return "pass";
|
|
30
|
+
const sampledAt = account.lastUsageAt ?? null;
|
|
31
|
+
const windows = allWindows(account);
|
|
32
|
+
const over = windows.some((window) => liveUsed({ window, now, sampledAt }) >= barFor({ window, thresholds }));
|
|
33
|
+
if (!over) return "pass";
|
|
34
|
+
const atWall = usage.reachedType === LIMIT_REACHED || windows.some((window) => liveUsed({ window, now, sampledAt }) >= WALL_PERCENT);
|
|
35
|
+
return atWall ? "claim" : "hold";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ConsumeResponseSchema = z.looseObject({
|
|
39
|
+
code: z.enum(["reset", "nothing_to_reset", "no_credit", "already_redeemed"]),
|
|
40
|
+
windows_reset: z.number().int().nullish(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const ConsumeResultSchema = z.object({ outcome: z.enum(["reset", "nothing_to_reset", "no_credit", "already_redeemed", "auth_error", "error"]), detail: z.string() });
|
|
44
|
+
type ConsumeResult = z.infer<typeof ConsumeResultSchema>;
|
|
45
|
+
|
|
46
|
+
export async function consumeCodexBankedReset(input: { auth: CodexAuthJson; idempotencyKey: string }): Promise<ConsumeResult> {
|
|
47
|
+
const { auth, idempotencyKey } = input;
|
|
48
|
+
const identity = codexIdentityOf({ auth });
|
|
49
|
+
let res: Response;
|
|
50
|
+
let text: string;
|
|
51
|
+
try {
|
|
52
|
+
res = await http.post(CONSUME_URL, {
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: `Bearer ${auth.tokens.access_token}`,
|
|
55
|
+
"ChatGPT-Account-Id": identity.accountId,
|
|
56
|
+
"User-Agent": "codex-cli",
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify({ redeem_request_id: idempotencyKey }),
|
|
60
|
+
timeout: CONSUME_TIMEOUT_MS,
|
|
61
|
+
});
|
|
62
|
+
text = await res.text();
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return { outcome: "error", detail: `reset endpoint unreachable: ${e instanceof Error ? e.message : String(e)}` };
|
|
65
|
+
}
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const detail = `HTTP ${res.status}: ${safeErrorDetail({ text })}`;
|
|
68
|
+
if (res.status === 401 || res.status === 403) return { outcome: "auth_error", detail };
|
|
69
|
+
return { outcome: "error", detail };
|
|
70
|
+
}
|
|
71
|
+
const parsed = ConsumeResponseSchema.safeParse((() => {
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(text);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
})());
|
|
78
|
+
if (!parsed.success) return { outcome: "error", detail: `reset endpoint returned an unrecognized body (${text.length} bytes, withheld)` };
|
|
79
|
+
return { outcome: parsed.data.code, detail: `${parsed.data.code} (windows reset: ${parsed.data.windows_reset ?? "unknown"})` };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function rememberPendingKey(accountId: string, key: string, now: number): void {
|
|
83
|
+
const index = loadCodexAccounts();
|
|
84
|
+
const entry = index.accounts.find((a) => a.accountId === accountId);
|
|
85
|
+
if (!entry) return;
|
|
86
|
+
entry.bankedReset = { outcome: "error", at: now, key };
|
|
87
|
+
saveCodexAccounts({ index });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function consumeLiveCodexBankedReset(input: { account: CodexAccount; now: number }): Promise<CodexBankedClaim> {
|
|
91
|
+
const { account, now } = input;
|
|
92
|
+
const short = account.accountId.slice(0, 8);
|
|
93
|
+
const live = readLiveCodexAuth();
|
|
94
|
+
let result: ConsumeResult;
|
|
95
|
+
let idempotencyKey: string | null = null;
|
|
96
|
+
if (!live) {
|
|
97
|
+
result = { outcome: "auth_error", detail: "live auth.json vanished" };
|
|
98
|
+
} else if (codexIdentityOf({ auth: live }).accountId !== account.accountId) {
|
|
99
|
+
result = { outcome: "error", detail: "live codex credential belongs to another account" };
|
|
100
|
+
} else {
|
|
101
|
+
const prior = account.bankedReset;
|
|
102
|
+
const retrying = prior != null && prior.outcome === "error" && prior.key != null && now - prior.at < RETRY_KEY_TTL_MS;
|
|
103
|
+
idempotencyKey = retrying ? prior.key! : crypto.randomUUID();
|
|
104
|
+
if (!retrying) rememberPendingKey(account.accountId, idempotencyKey, now);
|
|
105
|
+
result = await consumeCodexBankedReset({ auth: live, idempotencyKey });
|
|
106
|
+
if (result.outcome === "already_redeemed" && retrying) result = { outcome: "reset", detail: `${result.detail}; the retried key had already completed` };
|
|
107
|
+
}
|
|
108
|
+
const outcome: CodexBankedResetOutcome = result.outcome;
|
|
109
|
+
const reset = outcome === "reset";
|
|
110
|
+
const index = loadCodexAccounts();
|
|
111
|
+
const entry = index.accounts.find((a) => a.accountId === account.accountId);
|
|
112
|
+
if (entry) {
|
|
113
|
+
entry.bankedReset = { outcome, at: now, ...(outcome === "error" && idempotencyKey != null ? { key: idempotencyKey } : {}) };
|
|
114
|
+
if (reset) {
|
|
115
|
+
delete entry.lastUsage;
|
|
116
|
+
delete entry.lastUsageAt;
|
|
117
|
+
}
|
|
118
|
+
saveCodexAccounts({ index });
|
|
119
|
+
}
|
|
120
|
+
log(reset ? "codexreset.consumed" : "codexreset.refused", { account: short, outcome, detail: result.detail.slice(0, 200) });
|
|
121
|
+
return reset ? "reset" : "pass";
|
|
122
|
+
}
|
package/src/lib/codexusage.ts
CHANGED
|
@@ -25,6 +25,8 @@ const WireRateLimitSchema = z.looseObject({
|
|
|
25
25
|
secondary_window: WireWindowSchema.nullish(),
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
const WireReachedTypeSchema = z.union([z.string(), z.looseObject({ type: z.string().nullish(), kind: z.string().nullish() })]).nullish().catch(null);
|
|
29
|
+
|
|
28
30
|
const WireUsageSchema = z.looseObject({
|
|
29
31
|
account_id: z.string(),
|
|
30
32
|
email: z.string().nullish(),
|
|
@@ -33,6 +35,8 @@ const WireUsageSchema = z.looseObject({
|
|
|
33
35
|
additional_rate_limits: z
|
|
34
36
|
.array(z.looseObject({ limit_name: z.string(), rate_limit: WireRateLimitSchema.nullish() }))
|
|
35
37
|
.nullish(),
|
|
38
|
+
rate_limit_reset_credits: z.looseObject({ available_count: z.number().int().nullish() }).nullish(),
|
|
39
|
+
rate_limit_reached_type: WireReachedTypeSchema,
|
|
36
40
|
});
|
|
37
41
|
|
|
38
42
|
function toWindows(rateLimit: z.infer<typeof WireRateLimitSchema> | null | undefined): CodexWindow[] {
|
|
@@ -92,6 +96,11 @@ export async function fetchCodexUsage(input: { auth: CodexAuthJson }): Promise<C
|
|
|
92
96
|
planType: wire.plan_type ?? null,
|
|
93
97
|
aggregate: toWindows(wire.rate_limit),
|
|
94
98
|
perLimit,
|
|
99
|
+
resetCredits: wire.rate_limit_reset_credits?.available_count ?? null,
|
|
100
|
+
reachedType:
|
|
101
|
+
typeof wire.rate_limit_reached_type === "string"
|
|
102
|
+
? wire.rate_limit_reached_type
|
|
103
|
+
: (wire.rate_limit_reached_type?.type ?? wire.rate_limit_reached_type?.kind ?? null),
|
|
95
104
|
});
|
|
96
105
|
}
|
|
97
106
|
|
package/src/lib/decide.ts
CHANGED
|
@@ -2,8 +2,9 @@ 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 { MAX_CHECK_DELAY_TICKS, POST_SWAP_COOLDOWN_MS, maxCheckDelayMs, loadAccounts, loadConfig, loadDepletedWait, loadLastSwapAt, loadUsage, loadUsageSnapshot, loadModelUsage, saveAccounts, saveDepletedWait, saveModelUsage, writeUsage } from "./state.ts";
|
|
5
|
+
import { MAX_CHECK_DELAY_TICKS, POST_SWAP_COOLDOWN_MS, maxCheckDelayMs, loadAccounts, loadConfig, loadDepletedWait, loadLastSwapAt, loadUsage, loadUsageSnapshot, loadModelUsage, measureFamilyWindow, saveAccounts, saveDepletedWait, saveModelUsage, writeUsage } from "./state.ts";
|
|
6
6
|
import { readOAuthAccount } from "./claudejson.ts";
|
|
7
|
+
import { bankedResetBelievedAvailable, claimBankedReset } from "./bankedreset.ts";
|
|
7
8
|
import { chooseAndSwap, isSkippableSwapError, performSwap } from "./swap.ts";
|
|
8
9
|
import { currentWins, effectiveBars, hardBars, isExhausted, nextWeeklyReset, pickBest, pickEarliestReset, sessionLadder, usableAt } from "./picker.ts";
|
|
9
10
|
import { familyTokens, gatedFamilies, probeUsage, type EnforcedClass } from "./usage.ts";
|
|
@@ -15,6 +16,7 @@ const SwapDecisionSchema = z.object({
|
|
|
15
16
|
account: AccountSchema.nullable(),
|
|
16
17
|
reason: z.string(),
|
|
17
18
|
waitUntil: z.number().optional(),
|
|
19
|
+
reset: z.boolean().optional(),
|
|
18
20
|
});
|
|
19
21
|
export type SwapDecision = z.infer<typeof SwapDecisionSchema>;
|
|
20
22
|
|
|
@@ -35,6 +37,17 @@ function capForFamily(mu: ModelUsageState, family: string, now: number): UsageWi
|
|
|
35
37
|
return maxBy(rows, (w) => liveUsed({ window: w, windowMs: WEEK_MS, sampledAt: mu.sampledAt ?? mu.ts, now }));
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
function familyCaps(perModel: Record<string, UsageWindow>, families: string[]): Record<string, number> {
|
|
41
|
+
return Object.fromEntries(
|
|
42
|
+
families.flatMap((family) => {
|
|
43
|
+
const rows = Object.entries(perModel)
|
|
44
|
+
.filter(([k]) => familyTokens(k).includes(family))
|
|
45
|
+
.map(([, w]) => w.usedPercentage);
|
|
46
|
+
return rows.length > 0 ? [[family, Math.max(...rows)] as const] : [];
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
function overlayLive(accounts: Account[], u: UsageState | null, mu: ModelUsageState | null, account: string | null): Account[] {
|
|
39
52
|
return accounts.map((a) => {
|
|
40
53
|
if (account == null || a.accountUuid !== account) return a;
|
|
@@ -63,6 +76,41 @@ function needsPerModel(u: UsageState | null, cfg: Config): boolean {
|
|
|
63
76
|
return u != null && gatedFamilies(u.model, cfg.policy.switchModels).length > 0;
|
|
64
77
|
}
|
|
65
78
|
|
|
79
|
+
const BankedVerdictSchema = z.enum(["hold", "claim", "pass"]);
|
|
80
|
+
type BankedVerdict = z.infer<typeof BankedVerdictSchema>;
|
|
81
|
+
|
|
82
|
+
function bankedResetVerdict(input: {
|
|
83
|
+
seat: Account;
|
|
84
|
+
u: UsageState | null;
|
|
85
|
+
mu: ModelUsageState | null;
|
|
86
|
+
enforced: EnforcedLimit | null;
|
|
87
|
+
cfg: Config;
|
|
88
|
+
bars: Thresholds;
|
|
89
|
+
switchFamilies: string[];
|
|
90
|
+
now: number;
|
|
91
|
+
}): BankedVerdict {
|
|
92
|
+
const { seat, u, mu, enforced, cfg, bars, switchFamilies, now } = input;
|
|
93
|
+
if (enforced && enforced.kind !== "session") return "pass";
|
|
94
|
+
if (!u || u.account !== seat.accountUuid) return "pass";
|
|
95
|
+
if (!bankedResetBelievedAvailable(seat, now)) return "pass";
|
|
96
|
+
if (u.sampledAt == null || now - u.sampledAt > cfg.policy.usagePollTtlMs) return "pass";
|
|
97
|
+
const cost = u.sessionWindowWeeklyCost ?? seat.sessionWindowWeeklyCost;
|
|
98
|
+
if (cost == null) return "pass";
|
|
99
|
+
const weeklyUsed = liveUsed({ window: u.sevenDay, windowMs: WEEK_MS, sampledAt: u.ts, now });
|
|
100
|
+
if (weeklyUsed + cost > bars.weekly) return "pass";
|
|
101
|
+
const muSame = mu && mu.account === seat.accountUuid && now - (mu.sampledAt ?? mu.ts) <= cfg.policy.usagePollTtlMs ? mu : null;
|
|
102
|
+
for (const family of switchFamilies) {
|
|
103
|
+
const cap = muSame ? capForFamily(muSame, family, now) : undefined;
|
|
104
|
+
if (!cap || !muSame) return "pass";
|
|
105
|
+
const familyCost = muSame.familyCosts?.[family];
|
|
106
|
+
if (familyCost == null) return "pass";
|
|
107
|
+
const capUsed = liveUsed({ window: cap, windowMs: WEEK_MS, sampledAt: muSame.sampledAt ?? muSame.ts, now });
|
|
108
|
+
if (capUsed + familyCost > bars.weekly) return "pass";
|
|
109
|
+
}
|
|
110
|
+
const atWall = enforced?.kind === "session" || liveUsed({ window: u.fiveHour, windowMs: FIVE_HOURS_MS, sampledAt: u.ts, now }) >= hardBars(cfg).session;
|
|
111
|
+
return atWall ? "claim" : "hold";
|
|
112
|
+
}
|
|
113
|
+
|
|
66
114
|
function isEngaged(u: UsageState | null, mu: ModelUsageState | null, account: string | null, bars: Thresholds, cfg: Config, now: number): boolean {
|
|
67
115
|
if (!u || !account || u.account !== account) return false;
|
|
68
116
|
return liveUsed({ window: u.fiveHour, windowMs: FIVE_HOURS_MS, sampledAt: u.ts, now }) >= cfg.policy.greedySessionFloor || isOver(u, mu, account, bars, cfg, now);
|
|
@@ -98,6 +146,7 @@ async function loadFreshSnapshots(cfg: Config, account: string | null, now: numb
|
|
|
98
146
|
if (account && !probeAttempted && (!usageFresh(u, uAt, account, ttl, now) || needsPerModel(u, cfg))) {
|
|
99
147
|
const full = await probeUsage();
|
|
100
148
|
const ts = Date.now();
|
|
149
|
+
const prevMu = mu;
|
|
101
150
|
if (readOAuthAccount()?.accountUuid === account) {
|
|
102
151
|
if (full) {
|
|
103
152
|
const teed = loadUsageSnapshot();
|
|
@@ -109,7 +158,7 @@ async function loadFreshSnapshots(cfg: Config, account: string | null, now: numb
|
|
|
109
158
|
writeUsage(u);
|
|
110
159
|
uAt = ts;
|
|
111
160
|
}
|
|
112
|
-
mu = { perModel: full.perModel, account, ts, sampledAt: ts };
|
|
161
|
+
mu = { perModel: full.perModel, account, ts, sampledAt: ts, ...measureFamilyWindow(prevMu, account, full.session, familyCaps(full.perModel, cfg.policy.switchModels)) };
|
|
113
162
|
saveModelUsage(mu);
|
|
114
163
|
const expected = gatedFamilies(u?.model ?? null, cfg.policy.switchModels);
|
|
115
164
|
const rows = Object.keys(full.perModel);
|
|
@@ -117,7 +166,7 @@ async function loadFreshSnapshots(cfg: Config, account: string | null, now: numb
|
|
|
117
166
|
log("usage.no_permodel_row", { families: expected.join(","), rows: rows.join(",") });
|
|
118
167
|
}
|
|
119
168
|
} else {
|
|
120
|
-
mu = { perModel: mu?.account === account ? (mu?.perModel ?? {}) : {}, account, ts, sampledAt: mu?.account === account ? (mu?.sampledAt ?? mu?.ts) : undefined };
|
|
169
|
+
mu = { perModel: mu?.account === account ? (mu?.perModel ?? {}) : {}, account, ts, sampledAt: mu?.account === account ? (mu?.sampledAt ?? mu?.ts) : undefined, ...measureFamilyWindow(prevMu, account, null, {}) };
|
|
121
170
|
saveModelUsage(mu);
|
|
122
171
|
}
|
|
123
172
|
}
|
|
@@ -171,7 +220,8 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
|
|
|
171
220
|
let sampled = false;
|
|
172
221
|
if (tee && tee.state.account === account2 && (active.lastUsageAt == null || tee.at >= active.lastUsageAt)) {
|
|
173
222
|
active.lastUsage = { fiveHour: tee.state.fiveHour, sevenDay: tee.state.sevenDay };
|
|
174
|
-
active.lastUsageAt = tee.at;
|
|
223
|
+
active.lastUsageAt = tee.state.sampledAt ?? tee.at;
|
|
224
|
+
if (tee.state.sessionWindowWeeklyCost != null) active.sessionWindowWeeklyCost = tee.state.sessionWindowWeeklyCost;
|
|
175
225
|
sampled = true;
|
|
176
226
|
}
|
|
177
227
|
if (mu2 && mu2.account === account2 && Object.keys(mu2.perModel).length > 0) {
|
|
@@ -209,7 +259,7 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
|
|
|
209
259
|
const cur = loadAccounts();
|
|
210
260
|
const active = seatOf(cur);
|
|
211
261
|
const pool = usable(cur.accounts);
|
|
212
|
-
const ctxAll = { now, thresholds: barsOf(
|
|
262
|
+
const ctxAll = { now, thresholds: barsOf(cur.accounts), currentAccountUuid: null, switchFamilies, holdMargin };
|
|
213
263
|
if (currentWins(active, pool, ctxAll)) {
|
|
214
264
|
return { swapped: false, account: null, reason: "current-best" };
|
|
215
265
|
}
|
|
@@ -227,12 +277,26 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
|
|
|
227
277
|
};
|
|
228
278
|
if (!enforced2 && !isOver(u2, mu2, account2, bars, cfg, now)) return greedy(cfg.policy.greedySwapMargin);
|
|
229
279
|
|
|
280
|
+
const resetPreferred = cfg.policy.preferToUseBankedReset.includes("claude");
|
|
281
|
+
let resetTried = false;
|
|
230
282
|
while (true) {
|
|
231
283
|
const cur = loadAccounts();
|
|
232
284
|
const seat = seatOf(cur);
|
|
233
285
|
const pool = usable(cur.accounts);
|
|
234
|
-
const thresholds = barsOf(
|
|
286
|
+
const thresholds = barsOf(cur.accounts);
|
|
235
287
|
if (!enforced2 && seat && !seat.needsReauth && !isExhausted(seat, { now, thresholds, currentAccountUuid: null, switchFamilies })) return greedy(0);
|
|
288
|
+
if (resetPreferred && !resetTried && seat && !seat.needsReauth) {
|
|
289
|
+
const verdict = bankedResetVerdict({ seat, u: u2, mu: mu2, enforced: enforced2, cfg, bars: thresholds, switchFamilies, now });
|
|
290
|
+
if (verdict === "hold") {
|
|
291
|
+
log("decide.banked_reset_hold", { account: seat.accountUuid.slice(0, 8), enforced: enforced2 != null });
|
|
292
|
+
return { swapped: false, account: seat, reason: "banked-reset-hold" };
|
|
293
|
+
}
|
|
294
|
+
if (verdict === "claim") {
|
|
295
|
+
resetTried = true;
|
|
296
|
+
const claim = await claimBankedReset({ account: seat });
|
|
297
|
+
if (claim === "reset") return { swapped: false, account: seat, reason: "banked-reset", reset: true };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
236
300
|
const best = pickBest(pool, { now, thresholds, currentAccountUuid: seat?.accountUuid ?? null, switchFamilies });
|
|
237
301
|
if (!best) break;
|
|
238
302
|
try {
|
|
@@ -340,11 +404,12 @@ export async function recordEnforcedLimit(input: { limit: EnforcedClass; account
|
|
|
340
404
|
account: accountUuid,
|
|
341
405
|
ts: now,
|
|
342
406
|
sampledAt: resetsAt == null ? now : sampledAt ?? now,
|
|
407
|
+
...measureFamilyWindow(muSame, accountUuid, null, {}),
|
|
343
408
|
});
|
|
344
409
|
log("usage.enforced_limit", { kind: limit.kind, family: limit.family, resetsAt });
|
|
345
410
|
return { outcome: "stamped", resetsAt };
|
|
346
411
|
}
|
|
347
|
-
saveModelUsage({ perModel: carriedRows, account: accountUuid, ts: now, sampledAt });
|
|
412
|
+
saveModelUsage({ perModel: carriedRows, account: accountUuid, ts: now, sampledAt, ...measureFamilyWindow(muSame, accountUuid, null, {}) });
|
|
348
413
|
if (account && limit.resetsAt != null) {
|
|
349
414
|
account.enforcedUntil = limit.resetsAt;
|
|
350
415
|
saveAccounts(idx);
|
|
@@ -353,12 +418,14 @@ export async function recordEnforcedLimit(input: { limit: EnforcedClass; account
|
|
|
353
418
|
if (!carrier) return { outcome: "no-carrier", resetsAt: limit.resetsAt };
|
|
354
419
|
const window: UsageWindow = { usedPercentage: 100, resetsAt: limit.resetsAt };
|
|
355
420
|
writeUsage({
|
|
421
|
+
...(priorSame ?? {}),
|
|
422
|
+
...(priorSame == null && account?.lastUsageAt != null ? { sampledAt: account.lastUsageAt } : {}),
|
|
356
423
|
fiveHour: limit.kind === "session" ? window : carrier.fiveHour,
|
|
357
424
|
sevenDay: limit.kind === "weekly" ? window : carrier.sevenDay,
|
|
358
425
|
account: accountUuid,
|
|
359
426
|
ts: now,
|
|
360
427
|
model: carrier.model,
|
|
361
|
-
});
|
|
428
|
+
}, { stamp: true });
|
|
362
429
|
log("usage.enforced_limit", { kind: limit.kind, resetsAt: limit.resetsAt });
|
|
363
430
|
return { outcome: "stamped", resetsAt: limit.resetsAt };
|
|
364
431
|
});
|