tokenmaxxing 1.20.0 → 1.23.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 -4
- package/README.md +3 -30
- package/package.json +1 -7
- package/src/cli/check.ts +0 -3
- package/src/cli/config.ts +0 -1
- package/src/cli/status.ts +2 -12
- package/src/entries/codexstophook.ts +0 -1
- package/src/entries/stopfailurehook.ts +6 -9
- package/src/entries/stophook.ts +0 -1
- package/src/entries/supervisor.ts +1 -2
- package/src/lib/codexdecide.ts +1 -25
- package/src/lib/codexpick.ts +3 -3
- package/src/lib/codexusage.ts +0 -9
- package/src/lib/decide.ts +4 -69
- package/src/lib/paths.ts +0 -1
- package/src/lib/state.ts +2 -82
- package/src/lib/types.ts +0 -53
- package/agent-plugin/bin/tokenmaxxing-mcp +0 -7
- package/agent-plugin/mcp.json +0 -10
- package/agent-plugin/plugin.json +0 -20
- package/agent-plugin/skills/codex-pool/SKILL.md +0 -23
- package/agent-plugin/skills/codex-pool/references/codex.md +0 -5
- package/agent-plugin/skills/credentials-hygiene/SKILL.md +0 -27
- package/agent-plugin/skills/credentials-hygiene/references/credentials.md +0 -6
- package/agent-plugin/skills/doctor-diagnostics/SKILL.md +0 -26
- package/agent-plugin/skills/doctor-diagnostics/references/troubleshooting.md +0 -5
- package/agent-plugin/skills/pool-status/SKILL.md +0 -27
- package/agent-plugin/skills/pool-status/references/commands.md +0 -9
- package/agent-plugin/skills/safe-contribution/SKILL.md +0 -27
- package/agent-plugin/skills/safe-contribution/references/ship.md +0 -5
- package/agent-plugin/skills/sdk-pairing/SKILL.md +0 -34
- package/agent-plugin/skills/sdk-pairing/references/sdk.md +0 -6
- package/agent-plugin/skills/switching-policy/SKILL.md +0 -35
- package/agent-plugin/skills/switching-policy/references/policy.md +0 -10
- package/src/entries/mcp.ts +0 -277
- package/src/lib/bankedreset.ts +0 -234
- package/src/lib/codexreset.ts +0 -122
- package/src/sdk.ts +0 -55
package/DESIGN.md
CHANGED
|
@@ -72,7 +72,7 @@ Each terminal ran the supervisor, so each has its own child `claude` and its own
|
|
|
72
72
|
|
|
73
73
|
- **`tokenmaxxing init` imports the account you're already on - automatically, no prompts, no re-login.** It reads the live `Claude Code-credentials` keychain blob plus the `oauthAccount` object in `~/.claude.json` (email, `organizationUuid`, `accountUuid`, plan tier) and writes them as **account #1** into tokenmaxxing's store (`tokenmaxxing-cred-<accountUuid[:8]>` + an `accounts.json` index entry). Nothing about your current session changes - that account stays active; it's now just also a registered pool member. After this one command you already have a working (single-account) pool. `init` also installs the supervisor + the four settings entries.
|
|
74
74
|
- If the current auth is API-key mode (`ANTHROPIC_API_KEY`/`apiKeyHelper`) rather than a subscription `/login`, there's no quota-poolable subscription credential to import - `init` says so and points you to `/login` first (per-token API billing isn't what tokenmaxxing pools).
|
|
75
|
-
- **`tokenmaxxing add`** - registers *additional* accounts: logs one in via a throwaway `CLAUDE_CONFIG_DIR=~/.config/tokenmaxxing/onboard` (your primary login untouched), harvests it into the store, deletes the temp dir + its namespaced item. `CLAUDE_CONFIG_DIR` is SET by tokenmaxxing only for throwaway isolated stores like this one: the same harvest serves `auth`, and parked-account `/usage` sampling probes under `sample/<item>` the same way; an AMBIENT `CLAUDE_CONFIG_DIR` is refused by every CLI command
|
|
75
|
+
- **`tokenmaxxing add`** - registers *additional* accounts: logs one in via a throwaway `CLAUDE_CONFIG_DIR=~/.config/tokenmaxxing/onboard` (your primary login untouched), harvests it into the store, deletes the temp dir + its namespaced item. `CLAUDE_CONFIG_DIR` is SET by tokenmaxxing only for throwaway isolated stores like this one: the same harvest serves `auth`, and parked-account `/usage` sampling probes under `sample/<item>` the same way; an AMBIENT `CLAUDE_CONFIG_DIR` is refused by every CLI command.
|
|
76
76
|
- **`tokenmaxxing auth [sel | --all]`** - reauthenticates an *existing* pool member whose refresh token died (a needs-reauth account can never heal through a swap: the dead token is exactly what a swap would need). Same isolated-login harvest as `add`, but it states which email to sign in with and **requires the login to land on the target account** (harvested `accountUuid` must match, else nothing changes) - the credential write and the needs-reauth clear happen in one flock critical section so a concurrent swap's harvest cannot clobber the fresh backup. Bare `auth` lists the pool with emails and asks which; `--all` walks every flagged account one by one.
|
|
77
77
|
- Both commands exercise `security` reads/writes interactively (where a macOS keychain ACL prompt is acceptable), so the first access never happens cold inside a headless hook.
|
|
78
78
|
|
|
@@ -98,8 +98,6 @@ The [wall policy](docs/content/docs/switching.mdx#the-wall) defines Layer 2 reco
|
|
|
98
98
|
|
|
99
99
|
**Model-aware trigger.** Claude subscriptions also enforce **per-model weekly caps** - currently only for Sonnet and Fable (there is no Opus-only quota), and Fable's tighter limit binds *before* the aggregate (e.g. 80% week-Fable at only 50% week-all-models). This cap isn't in statusLine stdin, so when the active model is in `policy.switchModels` we read it from `claude -p '/usage'` (free, 0 tokens, TTL-cached) and add `week(<activeModel>) >= threshold` to the trigger. A Fable session switches on the Fable cap; a Sonnet session rides the aggregate. Both layers apply the per-model gate: a burnt Fable cap screens an account out of a Layer 1 switch, and a Fable cap at the wall screens it out of the Layer 2 squeeze too.
|
|
100
100
|
|
|
101
|
-
**Banked resets (opt-in).** Both CLIs can reset a hit limit on the same account: Claude Code's `/limit-reset` (the `juniper_tide` server program, claimed by `POST /api/organizations/<org>/reset_rate_limits`, once a week, and the reset window still counts toward the weekly limit; the slash command is interactive-only, so tokenmaxxing makes the claim call itself with the live credential) and Codex's banked reset (`POST .../wham/rate-limit-reset-credits/consume` with an idempotency key; it refreshes the 5h and weekly windows together). `policy.preferToUseBankedReset` (a provider list, default `[]`) makes the hard path spend one before swapping, because a swap cold-starts the per-org prompt cache while a reset keeps the seat. Claude holds between the session rung and the wall only while the account still has weekly room for one more session window at its measured rate (`sessionWindowWeeklyCost`, learned from the tee within one window; unmeasured swaps), claims at the wall, and on `reset` respawns the failed turn in place. Codex rides to the wall while the usage endpoint reports a credit, consumes one, re-samples, and never restarts. Detail: `docs/switching.mdx` §Banked resets, `src/lib/bankedreset.ts`, `src/lib/codexreset.ts`.
|
|
102
|
-
|
|
103
101
|
---
|
|
104
102
|
|
|
105
103
|
## 6. Honest papercuts
|
|
@@ -120,7 +118,7 @@ The [wall policy](docs/content/docs/switching.mdx#the-wall) defines Layer 2 reco
|
|
|
120
118
|
|
|
121
119
|
**v2:** projected-threshold pre-emption; a `UserPromptSubmit` guard that respawns *before* a turn starts when already over; Windows.
|
|
122
120
|
|
|
123
|
-
**
|
|
121
|
+
**Removed (1.23.0):** the programmatic SDK surface, the stdio MCP server, and the Agent Plugin that shipped from 0.11.0 through 1.22.0. The CLI, the hooks, and the supervisor are the only surfaces.
|
|
124
122
|
|
|
125
123
|
**Later:** tool-agnostic picker.
|
|
126
124
|
|
package/README.md
CHANGED
|
@@ -108,42 +108,15 @@ See [How switching decides](docs/content/docs/switching.mdx) for the policy and
|
|
|
108
108
|
"greedySessionFloor": 80,
|
|
109
109
|
"switchModels": ["fable"],
|
|
110
110
|
"usagePollTtlMs": 90000,
|
|
111
|
-
"checkIntervalMs": 60000
|
|
112
|
-
"preferToUseBankedReset": []
|
|
111
|
+
"checkIntervalMs": 60000
|
|
113
112
|
}
|
|
114
113
|
}
|
|
115
114
|
```
|
|
116
115
|
|
|
117
|
-
`thresholds.session` is a ladder: the bar is the lowest rung some pooled account still clears, so the default `[90]` is one bar, while with `[50, 80, 95]` a seat past 50 hands off while a sibling is under 50, and the bar climbs to 80 and then 95 as the whole pool fills; `projectionMargin` is a fixed safety margin subtracted from each threshold bar (effective bar = threshold - margin), so a large turn is less likely to blow past a bar between checks; `greedySessionFloor` is the session-used % at which the greedy convergence engages; `switchModels` names the models whose per-model cap triggers a switch; `usagePollTtlMs` is how long a `/usage` per-model poll stays fresh; `maxWaitMs` bounds the depleted-pool countdown - a soonest reset further out than this does not pause the session (no respawn marker is written and the session simply keeps hitting its limit until an account recovers); `checkIntervalMs` is the periodic check tick (default 60s), which `init` writes into the timer and which every headroom band is a multiple of, one tick near a bar up to five with plenty of headroom (a depleted wait and the post-swap cooldown sleep to their own deadlines instead) - re-run `tokenmaxxing init` after changing it so the timer unit picks up the new tick
|
|
116
|
+
`thresholds.session` is a ladder: the bar is the lowest rung some pooled account still clears, so the default `[90]` is one bar, while with `[50, 80, 95]` a seat past 50 hands off while a sibling is under 50, and the bar climbs to 80 and then 95 as the whole pool fills; `projectionMargin` is a fixed safety margin subtracted from each threshold bar (effective bar = threshold - margin), so a large turn is less likely to blow past a bar between checks; `greedySessionFloor` is the session-used % at which the greedy convergence engages; `switchModels` names the models whose per-model cap triggers a switch; `usagePollTtlMs` is how long a `/usage` per-model poll stays fresh; `maxWaitMs` bounds the depleted-pool countdown - a soonest reset further out than this does not pause the session (no respawn marker is written and the session simply keeps hitting its limit until an account recovers); `checkIntervalMs` is the periodic check tick (default 60s), which `init` writes into the timer and which every headroom band is a multiple of, one tick near a bar up to five with plenty of headroom (a depleted wait and the post-swap cooldown sleep to their own deadlines instead) - re-run `tokenmaxxing init` after changing it so the timer unit picks up the new tick.
|
|
118
117
|
|
|
119
118
|
State lives entirely in `~/.config/tokenmaxxing/`. Per-account credentials follow the platform's Claude Code store: the login keychain on macOS (`tokenmaxxing-cred-<uuid8>` items, never plaintext on disk), 0600 files under `~/.config/tokenmaxxing/creds/` on Linux (the same plaintext model claude itself uses for `~/.claude/.credentials.json`).
|
|
120
119
|
|
|
121
|
-
## Pairing with the Claude Agent SDK
|
|
122
|
-
|
|
123
|
-
For agents you build on the [Claude Agent SDK](https://docs.claude.com/en/api/agent-sdk/overview) against **your own** pooled accounts, `tokenmaxxing` is importable as a library (your agent app must run under Bun: tokenmaxxing ships TypeScript source and uses `bun:ffi`):
|
|
124
|
-
|
|
125
|
-
```ts
|
|
126
|
-
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
127
|
-
import { ensureBestAccount, pooledOptions, stopHookCheck } from "tokenmaxxing";
|
|
128
|
-
|
|
129
|
-
await ensureBestAccount();
|
|
130
|
-
|
|
131
|
-
for await (const message of query({
|
|
132
|
-
prompt: "...",
|
|
133
|
-
options: {
|
|
134
|
-
...pooledOptions(),
|
|
135
|
-
hooks: { Stop: [{ hooks: [stopHookCheck] }] },
|
|
136
|
-
},
|
|
137
|
-
})) {
|
|
138
|
-
}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
The loop body receives the message stream; keep the session id from the init message if you want `resume` across swaps.
|
|
142
|
-
|
|
143
|
-
The SDK reads credentials when it spawns the claude subprocess and has no statusLine, so none of the CLI-side supervisor machinery applies; the integration is boundary-driven instead. `ensureBestAccount()` runs the exact greedy decision the CLI hooks and timer run (screening bars, pace-pressure target, post-swap cooldown - all shared code); like them, it deliberately does nothing until the decision engages (the active session past `policy.greedySessionFloor`, or a bar crossed), so a fresh account rides instead of churning. `pooledOptions()` pins `pathToClaudeCodeExecutable` to the real claude binary and supplies a full replacement `env` with every ambient credential override (`ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, ...) scrubbed, so the subprocess resolves the pool's live credential and nothing else. The pooled surface requires the default Claude Code credential store: it fails fast if `CLAUDE_CONFIG_DIR` or `CLAUDE_SECURESTORAGE_CONFIG_DIR` is set in your app's environment, because a swap would write the live credential where those point while the spawned subprocess reads the default store. `stopHookCheck` re-runs the decision at turn boundaries; a swap it lands takes effect on the next subprocess spawn (it never yanks a mid-query token). If your app loads user settings (see the SDK's `settingSources`), the Stop hook `tokenmaxxing init` installed may already fire in SDK sessions too - `stopHookCheck` makes the check explicit and works when settings are restricted.
|
|
144
|
-
|
|
145
|
-
This is for pooling **your own** subscription accounts in agents you run yourself - the same personal-use posture as the CLI. Anthropic does not allow third-party products to offer claude.ai login or rate limits, including agents built on the Agent SDK; don't ship this surface to third parties.
|
|
146
|
-
|
|
147
120
|
## Codex support
|
|
148
121
|
|
|
149
122
|
The same pooling works for OpenAI's Codex CLI (your own ChatGPT-subscription accounts):
|
|
@@ -156,7 +129,7 @@ codex # use codex as always
|
|
|
156
129
|
|
|
157
130
|
Codex mechanics differ from Claude Code in one hard way: a running codex process refuses a credential swapped to a different account, so **a restart is the switch**. The installed Stop hook runs the same greedy pace-pressure decision at each turn boundary (usage read free from codex's own rate-limit endpoint: percentages plus absolute reset times, weekly aggregate and per-model caps alike); when it swaps, the supervisor relaunches `codex resume <session-id>` on the fresh account with the transcript intact. `tokenmaxxing switch --codex [sel]` does it manually, `status`/`watch`/`ls` show both pools.
|
|
158
131
|
|
|
159
|
-
Two codex-specific facts worth knowing: codex does not run hooks it has not been told to trust, so after `init --codex` you must open codex once and trust the tokenmaxxing Stop hook via `/hooks` (auto-switching is inert until then); and codex has no cross-process lock on `auth.json`, so tokenmaxxing serializes all of its own credential writes behind its own lock and swaps only at idle turn boundaries.
|
|
132
|
+
Two codex-specific facts worth knowing: codex does not run hooks it has not been told to trust, so after `init --codex` you must open codex once and trust the tokenmaxxing Stop hook via `/hooks` (auto-switching is inert until then); and codex has no cross-process lock on `auth.json`, so tokenmaxxing serializes all of its own credential writes behind its own lock and swaps only at idle turn boundaries.
|
|
160
133
|
|
|
161
134
|
## Honest limitations
|
|
162
135
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.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",
|
|
@@ -13,13 +13,8 @@
|
|
|
13
13
|
"bin": {
|
|
14
14
|
"tokenmaxxing": "./src/main.ts"
|
|
15
15
|
},
|
|
16
|
-
"exports": {
|
|
17
|
-
".": "./src/sdk.ts",
|
|
18
|
-
"./package.json": "./package.json"
|
|
19
|
-
},
|
|
20
16
|
"files": [
|
|
21
17
|
"src",
|
|
22
|
-
"agent-plugin",
|
|
23
18
|
"README.md",
|
|
24
19
|
"DESIGN.md"
|
|
25
20
|
],
|
|
@@ -41,7 +36,6 @@
|
|
|
41
36
|
"typescript": "^5.6.0"
|
|
42
37
|
},
|
|
43
38
|
"dependencies": {
|
|
44
|
-
"@modelcontextprotocol/sdk": "1.29.0",
|
|
45
39
|
"es-toolkit": "^1.49.0",
|
|
46
40
|
"ky": "^2.0.2",
|
|
47
41
|
"zod": "^4.4.3"
|
package/src/cli/check.ts
CHANGED
|
@@ -30,7 +30,6 @@ export async function cmdCheck(args: string[] = [], json = false): Promise<numbe
|
|
|
30
30
|
ok: true,
|
|
31
31
|
due: true,
|
|
32
32
|
swapped: d.swapped,
|
|
33
|
-
reset: d.reset === true,
|
|
34
33
|
account: d.account?.label ?? null,
|
|
35
34
|
reason: d.reason,
|
|
36
35
|
waitUntil: d.waitUntil ?? null,
|
|
@@ -41,8 +40,6 @@ export async function cmdCheck(args: string[] = [], json = false): Promise<numbe
|
|
|
41
40
|
const next = c.dim(`next in ${Math.round(delayMs / 1000)}s`);
|
|
42
41
|
if (d.swapped && d.account) {
|
|
43
42
|
console.log(`${c.green("↻")} switched to ${c.bold(d.account.label)} ${next}`);
|
|
44
|
-
} else if (d.reset && d.account) {
|
|
45
|
-
console.log(`${c.green("↻")} reset the session limit on ${c.bold(d.account.label)} ${next}`);
|
|
46
43
|
} else if (d.waitUntil !== undefined && d.account) {
|
|
47
44
|
console.log(c.yellow(`all accounts at limit - staying on ${c.bold(d.account.label)} (${fmtReset(d.waitUntil)})`) + ` ${next}`);
|
|
48
45
|
} else {
|
package/src/cli/config.ts
CHANGED
|
@@ -21,7 +21,6 @@ export const KNOWN_KEYS = [
|
|
|
21
21
|
"policy.usagePollTtlMs",
|
|
22
22
|
"policy.maxWaitMs",
|
|
23
23
|
"policy.checkIntervalMs",
|
|
24
|
-
"policy.preferToUseBankedReset",
|
|
25
24
|
] as const;
|
|
26
25
|
|
|
27
26
|
const TIMER_REWRITE_NOTE = "note: run `tokenmaxxing init` to rewrite the periodic timer at the new tick";
|
package/src/cli/status.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { isCodexExhausted } from "../lib/codexpick.ts";
|
|
|
12
12
|
import { codexLimitLabel, isSessionWindow } from "../lib/codexusage.ts";
|
|
13
13
|
import { bar, c, claudeTierLabel, count, emitJson, fmtAgo } from "./render.ts";
|
|
14
14
|
import { gatedFamilies, type FullUsage } from "../lib/usage.ts";
|
|
15
|
-
import {
|
|
15
|
+
import { ThresholdsSchema, UsageWindowSchema, type Account, type CodexWindow, type Config, type UsageWindow } from "../lib/types.ts";
|
|
16
16
|
|
|
17
17
|
const SampleReportSchema = z.discriminatedUnion("ok", [
|
|
18
18
|
z.object({ ok: z.literal(true), source: z.enum(["statusline", "probe"]) }),
|
|
@@ -35,9 +35,6 @@ const ClaudeStatusAccountSchema = z.object({
|
|
|
35
35
|
pingError: z.string().nullable(),
|
|
36
36
|
pingRejected: z.boolean(),
|
|
37
37
|
pinged: z.boolean(),
|
|
38
|
-
bankedReset: BankedResetRecordSchema.nullable(),
|
|
39
|
-
sessionWindowWeeklyCost: z.number().nullable(),
|
|
40
|
-
sessionWindowFamilyCosts: z.record(z.string(), z.number()).nullable(),
|
|
41
38
|
});
|
|
42
39
|
type ClaudeStatusAccount = z.infer<typeof ClaudeStatusAccountSchema>;
|
|
43
40
|
|
|
@@ -59,7 +56,6 @@ const CodexStatusAccountSchema = z.object({
|
|
|
59
56
|
.nullable(),
|
|
60
57
|
usageAt: z.number().nullable(),
|
|
61
58
|
sample: SampleReportSchema,
|
|
62
|
-
resetCredits: z.number().int().nullable(),
|
|
63
59
|
});
|
|
64
60
|
type CodexStatusAccount = z.infer<typeof CodexStatusAccountSchema>;
|
|
65
61
|
|
|
@@ -158,7 +154,6 @@ async function collectClaude(input: { cfg: Config; ping: boolean; pingCount: num
|
|
|
158
154
|
if (!outcome.ok) return;
|
|
159
155
|
a.lastUsage = { fiveHour: outcome.usage.session, sevenDay: outcome.usage.weekAll };
|
|
160
156
|
a.lastUsageAt = viaTee && teeAt != null ? (live?.sampledAt ?? teeAt) : Date.now();
|
|
161
|
-
if (viaTee && live?.sessionWindowWeeklyCost != null) a.sessionWindowWeeklyCost = live.sessionWindowWeeklyCost;
|
|
162
157
|
if (Object.keys(outcome.usage.perModel).length > 0) {
|
|
163
158
|
a.lastPerModel = outcome.usage.perModel;
|
|
164
159
|
a.lastPerModelAt = viaTee && modelUsage ? (modelUsage.sampledAt ?? modelUsage.ts) : a.lastUsageAt;
|
|
@@ -178,7 +173,6 @@ async function collectClaude(input: { cfg: Config; ping: boolean; pingCount: num
|
|
|
178
173
|
const families = gatedFamilies(loadUsage()?.model ?? null, cfg.policy.switchModels);
|
|
179
174
|
const bars = effectiveBars(cfg, { accounts: idx.accounts, now, switchFamilies: families });
|
|
180
175
|
const liveAccount = readOAuthAccount()?.accountUuid ?? null;
|
|
181
|
-
const liveModelUsage = loadModelUsage();
|
|
182
176
|
const ordered = sortBy(idx.accounts, [(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, now)]);
|
|
183
177
|
const accounts = ordered.map((a): ClaudeStatusAccount => {
|
|
184
178
|
const sampled = samples.get(a.accountUuid) ?? { outcome: { ok: false, reason: "not sampled" }, viaTee: false };
|
|
@@ -201,9 +195,6 @@ async function collectClaude(input: { cfg: Config; ping: boolean; pingCount: num
|
|
|
201
195
|
pingError: sampled.outcome.pingError ?? null,
|
|
202
196
|
pingRejected: sampled.outcome.pingRejected === true,
|
|
203
197
|
pinged: pings.has(a.accountUuid) && sampled.outcome.ok && sampled.outcome.pingError == null && aggregate != null && aggregate.fiveHour.resetsAt == null,
|
|
204
|
-
bankedReset: a.bankedReset ?? null,
|
|
205
|
-
sessionWindowWeeklyCost: a.sessionWindowWeeklyCost ?? null,
|
|
206
|
-
sessionWindowFamilyCosts: liveModelUsage && liveModelUsage.account === a.accountUuid ? (liveModelUsage.familyCosts ?? null) : null,
|
|
207
198
|
};
|
|
208
199
|
});
|
|
209
200
|
return {
|
|
@@ -231,7 +222,7 @@ async function collectCodex(input: { cfg: Config; now: number }): Promise<Status
|
|
|
231
222
|
const outcome = await sampleCodexAccount({ account, liveAccountId: liveId, now });
|
|
232
223
|
outcomes.set(account.accountId, outcome);
|
|
233
224
|
if (outcome.ok) {
|
|
234
|
-
account.lastUsage = { aggregate: outcome.usage.aggregate, perLimit: outcome.usage.perLimit
|
|
225
|
+
account.lastUsage = { aggregate: outcome.usage.aggregate, perLimit: outcome.usage.perLimit };
|
|
235
226
|
account.lastUsageAt = Date.now();
|
|
236
227
|
if (outcome.usage.email != null) account.email = outcome.usage.email;
|
|
237
228
|
if (outcome.usage.planType != null) account.planType = outcome.usage.planType;
|
|
@@ -272,7 +263,6 @@ async function collectCodex(input: { cfg: Config; now: number }): Promise<Status
|
|
|
272
263
|
: null,
|
|
273
264
|
usageAt: account.lastUsageAt ?? null,
|
|
274
265
|
sample: outcome.ok ? { ok: true, source: "probe" } : { ok: false, reason: outcome.reason },
|
|
275
|
-
resetCredits: usage?.resetCredits ?? null,
|
|
276
266
|
};
|
|
277
267
|
});
|
|
278
268
|
return { bars, accounts };
|
|
@@ -99,7 +99,6 @@ export async function handleCodexStop(input: { rawStdin: string }): Promise<void
|
|
|
99
99
|
}
|
|
100
100
|
if (await promoteReconcile({ supervisorId, sessionId })) return;
|
|
101
101
|
const decision = await evaluateAndMaybeSwapCodex({});
|
|
102
|
-
if (decision.reset && decision.account) log("codexstop.reset", { account: decision.account.accountId.slice(0, 8) });
|
|
103
102
|
if (decision.swapped && decision.account) {
|
|
104
103
|
mkdirSync(codexPaths.respawnDir, { recursive: true });
|
|
105
104
|
const payload = CodexRespawnMarkerSchema.parse({
|
|
@@ -4,13 +4,12 @@ import { paths } from "../lib/paths.ts";
|
|
|
4
4
|
import { writeFileAtomic } from "../lib/atomic.ts";
|
|
5
5
|
import { readOAuthAccount } from "../lib/claudejson.ts";
|
|
6
6
|
import { enforcedWindowMs, evaluateAndMaybeSwap, postSwapProof, recordEnforcedLimit } from "../lib/decide.ts";
|
|
7
|
-
import { loadConfig,
|
|
7
|
+
import { loadConfig, loadLastSwapAt } from "../lib/state.ts";
|
|
8
8
|
import { classifyEnforcedLimit, findEnforcedRow, parseErrorBody, readTranscriptTail } from "../lib/usage.ts";
|
|
9
9
|
import { RespawnMarkerSchema, type EnforcedLimit } from "../lib/types.ts";
|
|
10
10
|
import { log } from "../lib/log.ts";
|
|
11
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
|
-
export const RETRIGGER_RESET_PROMPT = "Continue where the previous turn left off; it was interrupted by the session limit and tokenmaxxing reset that limit on the same account.";
|
|
14
13
|
|
|
15
14
|
const StopFailureStdin = z.looseObject({
|
|
16
15
|
session_id: z.uuid().optional().catch(undefined),
|
|
@@ -53,11 +52,11 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
53
52
|
|
|
54
53
|
let enforced: EnforcedLimit | null = null;
|
|
55
54
|
if (limit && found && account) {
|
|
56
|
-
if (postSwapProof({ swapAt:
|
|
55
|
+
if (postSwapProof({ swapAt: loadLastSwapAt(), launchedAt, errorAt: found.errorAt, now })) {
|
|
57
56
|
const stamp = await recordEnforcedLimit({ limit, account, now });
|
|
58
57
|
log("stopfailure.enforced", { kind: limit.kind, family: limit.kind === "model" ? limit.family : undefined, outcome: stamp.outcome, resetsAt: stamp.resetsAt, subagent: !mainLoop });
|
|
59
58
|
if (stamp.outcome !== "account-moved") {
|
|
60
|
-
enforced = { account,
|
|
59
|
+
enforced = { account, family: limit.kind === "model" ? limit.family : null, resetsAt: stamp.resetsAt, windowMs: enforcedWindowMs(limit) };
|
|
61
60
|
}
|
|
62
61
|
} else {
|
|
63
62
|
log("stopfailure.unproven", { kind: limit.kind });
|
|
@@ -72,20 +71,18 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
const decision = await evaluateAndMaybeSwap(now, canPause && enforced != null, enforced);
|
|
75
|
-
|
|
76
|
-
if (enforced && canPause && pinnedSid && decision.account && (decision.swapped || decision.waitUntil !== undefined || reset)) {
|
|
74
|
+
if (enforced && canPause && pinnedSid && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
|
|
77
75
|
const marker = join(paths.respawnDir, pinnedSid);
|
|
78
76
|
const payload = RespawnMarkerSchema.parse({
|
|
79
77
|
account: decision.account.label,
|
|
80
78
|
ts: Date.now(),
|
|
81
79
|
waitUntil: decision.waitUntil ?? now,
|
|
82
80
|
sessionId: stdinSid ?? pinnedSid,
|
|
83
|
-
prompt:
|
|
81
|
+
prompt: RETRIGGER_PROMPT,
|
|
84
82
|
...(launchedAt != null ? { launchedAt } : {}),
|
|
85
|
-
...(reset ? { reset } : {}),
|
|
86
83
|
});
|
|
87
84
|
writeFileAtomic(marker, JSON.stringify(payload));
|
|
88
|
-
log("stopfailure.marker", { session: (stdinSid ?? pinnedSid).slice(0, 8), account: decision.account.accountUuid.slice(0, 8), waitUntil: payload.waitUntil
|
|
85
|
+
log("stopfailure.marker", { session: (stdinSid ?? pinnedSid).slice(0, 8), account: decision.account.accountUuid.slice(0, 8), waitUntil: payload.waitUntil });
|
|
89
86
|
} else {
|
|
90
87
|
log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.accountUuid.slice(0, 8), waitUntil: decision.waitUntil });
|
|
91
88
|
}
|
package/src/entries/stophook.ts
CHANGED
|
@@ -25,7 +25,6 @@ export async function runStopHook(): Promise<number> {
|
|
|
25
25
|
try {
|
|
26
26
|
const canPause = process.env.TOKENMAXXING_SUPERVISED === "1" && pinnedSid != null;
|
|
27
27
|
const decision = await evaluateAndMaybeSwap(Date.now(), canPause);
|
|
28
|
-
if (decision.reset && decision.account) log("stop.reset", { account: decision.account.accountUuid.slice(0, 8) });
|
|
29
28
|
if (decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
|
|
30
29
|
log(decision.swapped ? "stop.swapped" : "stop.wait", { account: decision.account.accountUuid.slice(0, 8), waitUntil: decision.waitUntil });
|
|
31
30
|
if (decision.waitUntil !== undefined && canPause && pinnedSid) {
|
|
@@ -286,8 +286,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
286
286
|
respawns++;
|
|
287
287
|
if (m.waitUntil > Date.now()) {
|
|
288
288
|
if (await countdownWait(m.account, m.waitUntil)) overriddenUntil = m.waitUntil;
|
|
289
|
-
} else
|
|
290
|
-
else process.stdout.write(`\n\x1b[36m↻ tokenmaxxing: switched to ${m.account} - resuming...\x1b[0m\n`);
|
|
289
|
+
} else process.stdout.write(`\n\x1b[36m↻ tokenmaxxing: switched to ${m.account} - resuming...\x1b[0m\n`);
|
|
291
290
|
saveSessionFlags(m.sessionId, persistable, process.cwd());
|
|
292
291
|
launchArgs = ["--resume", m.sessionId, ...persistable, ...(m.prompt ? ["--", m.prompt] : [])];
|
|
293
292
|
continue;
|
package/src/lib/codexdecide.ts
CHANGED
|
@@ -11,7 +11,6 @@ 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";
|
|
15
14
|
import { liveCodexAccountId } from "./codexsample.ts";
|
|
16
15
|
import { livingCodexPresences, presentCodexAccountIds, targetableCodexAccounts } from "./codexpresence.ts";
|
|
17
16
|
import { terminalBars } from "./picker.ts";
|
|
@@ -22,7 +21,6 @@ const CodexSwapDecisionSchema = z.object({
|
|
|
22
21
|
swapped: z.boolean(),
|
|
23
22
|
account: CodexAccountSchema.nullable(),
|
|
24
23
|
reason: z.string(),
|
|
25
|
-
reset: z.boolean().optional(),
|
|
26
24
|
});
|
|
27
25
|
export type CodexSwapDecision = z.infer<typeof CodexSwapDecisionSchema>;
|
|
28
26
|
|
|
@@ -53,7 +51,7 @@ async function sampleLiveOntoOwner(input: { now: number }): Promise<string | nul
|
|
|
53
51
|
writeParkedCodexAuth({ credFile: owner.credFile, auth: live });
|
|
54
52
|
}
|
|
55
53
|
const usage = await fetchCodexUsage({ auth: live });
|
|
56
|
-
owner.lastUsage = { aggregate: usage.aggregate, perLimit: usage.perLimit
|
|
54
|
+
owner.lastUsage = { aggregate: usage.aggregate, perLimit: usage.perLimit };
|
|
57
55
|
owner.lastUsageAt = now;
|
|
58
56
|
if (usage.email != null) owner.email = usage.email;
|
|
59
57
|
if (usage.planType != null) owner.planType = usage.planType;
|
|
@@ -139,28 +137,6 @@ export async function evaluateAndMaybeSwapCodex(input: { now?: number }): Promis
|
|
|
139
137
|
isCodexExhausted({ account: active, thresholds: bars, now });
|
|
140
138
|
if (!engaged) return { swapped: false, account: null, reason: "under-threshold-or-stale" };
|
|
141
139
|
|
|
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
|
-
|
|
164
140
|
if (!isCodexExhausted({ account: active, thresholds: bars, now })) {
|
|
165
141
|
while (true) {
|
|
166
142
|
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
|
-
|
|
6
|
+
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
|
-
|
|
13
|
+
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
|
-
|
|
19
|
+
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
|
|
package/src/lib/codexusage.ts
CHANGED
|
@@ -25,8 +25,6 @@ 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
|
-
|
|
30
28
|
const WireUsageSchema = z.looseObject({
|
|
31
29
|
account_id: z.string(),
|
|
32
30
|
email: z.string().nullish(),
|
|
@@ -35,8 +33,6 @@ const WireUsageSchema = z.looseObject({
|
|
|
35
33
|
additional_rate_limits: z
|
|
36
34
|
.array(z.looseObject({ limit_name: z.string(), rate_limit: WireRateLimitSchema.nullish() }))
|
|
37
35
|
.nullish(),
|
|
38
|
-
rate_limit_reset_credits: z.looseObject({ available_count: z.number().int().nullish() }).nullish(),
|
|
39
|
-
rate_limit_reached_type: WireReachedTypeSchema,
|
|
40
36
|
});
|
|
41
37
|
|
|
42
38
|
function toWindows(rateLimit: z.infer<typeof WireRateLimitSchema> | null | undefined): CodexWindow[] {
|
|
@@ -96,11 +92,6 @@ export async function fetchCodexUsage(input: { auth: CodexAuthJson }): Promise<C
|
|
|
96
92
|
planType: wire.plan_type ?? null,
|
|
97
93
|
aggregate: toWindows(wire.rate_limit),
|
|
98
94
|
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),
|
|
104
95
|
});
|
|
105
96
|
}
|
|
106
97
|
|
package/src/lib/decide.ts
CHANGED
|
@@ -2,9 +2,8 @@ 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,
|
|
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";
|
|
6
6
|
import { readOAuthAccount } from "./claudejson.ts";
|
|
7
|
-
import { bankedResetBelievedAvailable, claimBankedReset } from "./bankedreset.ts";
|
|
8
7
|
import { chooseAndSwap, isSkippableSwapError, performSwap, type VerifyVerdict } from "./swap.ts";
|
|
9
8
|
import { probeParkedUsage } from "./sample.ts";
|
|
10
9
|
import { currentWins, effectiveBars, hardBars, isExhausted, nextWeeklyReset, pickBest, pickEarliestReset, sessionLadder, usableAt, type PickCtx } from "./picker.ts";
|
|
@@ -17,7 +16,6 @@ const SwapDecisionSchema = z.object({
|
|
|
17
16
|
account: AccountSchema.nullable(),
|
|
18
17
|
reason: z.string(),
|
|
19
18
|
waitUntil: z.number().optional(),
|
|
20
|
-
reset: z.boolean().optional(),
|
|
21
19
|
});
|
|
22
20
|
export type SwapDecision = z.infer<typeof SwapDecisionSchema>;
|
|
23
21
|
|
|
@@ -40,17 +38,6 @@ function capForFamily(mu: ModelUsageState, family: string, now: number): UsageWi
|
|
|
40
38
|
return maxBy(rows, (w) => liveUsed({ window: w, windowMs: WEEK_MS, sampledAt: mu.sampledAt ?? mu.ts, now }));
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
function familyCaps(perModel: Record<string, UsageWindow>, families: string[]): Record<string, number> {
|
|
44
|
-
return Object.fromEntries(
|
|
45
|
-
families.flatMap((family) => {
|
|
46
|
-
const rows = Object.entries(perModel)
|
|
47
|
-
.filter(([k]) => familyTokens(k).includes(family))
|
|
48
|
-
.map(([, w]) => w.usedPercentage);
|
|
49
|
-
return rows.length > 0 ? [[family, Math.max(...rows)] as const] : [];
|
|
50
|
-
}),
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
41
|
function overlayLive(accounts: Account[], u: UsageState | null, mu: ModelUsageState | null, account: string | null): Account[] {
|
|
55
42
|
return accounts.map((a) => {
|
|
56
43
|
if (account == null || a.accountUuid !== account) return a;
|
|
@@ -79,41 +66,6 @@ function needsPerModel(u: UsageState | null, cfg: Config): boolean {
|
|
|
79
66
|
return u != null && gatedFamilies(u.model, cfg.policy.switchModels).length > 0;
|
|
80
67
|
}
|
|
81
68
|
|
|
82
|
-
const BankedVerdictSchema = z.enum(["hold", "claim", "pass"]);
|
|
83
|
-
type BankedVerdict = z.infer<typeof BankedVerdictSchema>;
|
|
84
|
-
|
|
85
|
-
function bankedResetVerdict(input: {
|
|
86
|
-
seat: Account;
|
|
87
|
-
u: UsageState | null;
|
|
88
|
-
mu: ModelUsageState | null;
|
|
89
|
-
enforced: EnforcedLimit | null;
|
|
90
|
-
cfg: Config;
|
|
91
|
-
bars: Thresholds;
|
|
92
|
-
switchFamilies: string[];
|
|
93
|
-
now: number;
|
|
94
|
-
}): BankedVerdict {
|
|
95
|
-
const { seat, u, mu, enforced, cfg, bars, switchFamilies, now } = input;
|
|
96
|
-
if (enforced && enforced.kind !== "session") return "pass";
|
|
97
|
-
if (!u || u.account !== seat.accountUuid) return "pass";
|
|
98
|
-
if (!bankedResetBelievedAvailable(seat, now)) return "pass";
|
|
99
|
-
if (u.sampledAt == null || now - u.sampledAt > cfg.policy.usagePollTtlMs) return "pass";
|
|
100
|
-
const cost = u.sessionWindowWeeklyCost ?? seat.sessionWindowWeeklyCost;
|
|
101
|
-
if (cost == null) return "pass";
|
|
102
|
-
const weeklyUsed = liveUsed({ window: u.sevenDay, windowMs: WEEK_MS, sampledAt: u.ts, now });
|
|
103
|
-
if (weeklyUsed + cost > bars.weekly) return "pass";
|
|
104
|
-
const muSame = mu && mu.account === seat.accountUuid && now - (mu.sampledAt ?? mu.ts) <= cfg.policy.usagePollTtlMs ? mu : null;
|
|
105
|
-
for (const family of switchFamilies) {
|
|
106
|
-
const cap = muSame ? capForFamily(muSame, family, now) : undefined;
|
|
107
|
-
if (!cap || !muSame) return "pass";
|
|
108
|
-
const familyCost = muSame.familyCosts?.[family];
|
|
109
|
-
if (familyCost == null) return "pass";
|
|
110
|
-
const capUsed = liveUsed({ window: cap, windowMs: WEEK_MS, sampledAt: muSame.sampledAt ?? muSame.ts, now });
|
|
111
|
-
if (capUsed + familyCost > bars.weekly) return "pass";
|
|
112
|
-
}
|
|
113
|
-
const atWall = enforced?.kind === "session" || liveUsed({ window: u.fiveHour, windowMs: FIVE_HOURS_MS, sampledAt: u.ts, now }) >= hardBars(cfg).session;
|
|
114
|
-
return atWall ? "claim" : "hold";
|
|
115
|
-
}
|
|
116
|
-
|
|
117
69
|
function isEngaged(u: UsageState | null, mu: ModelUsageState | null, account: string | null, bars: Thresholds, cfg: Config, now: number): boolean {
|
|
118
70
|
if (!u || !account || u.account !== account) return false;
|
|
119
71
|
return liveUsed({ window: u.fiveHour, windowMs: FIVE_HOURS_MS, sampledAt: u.ts, now }) >= cfg.policy.greedySessionFloor || isOver(u, mu, account, bars, cfg, now);
|
|
@@ -149,7 +101,6 @@ async function loadFreshSnapshots(cfg: Config, account: string | null, now: numb
|
|
|
149
101
|
if (account && !probeAttempted && (!usageFresh(u, uAt, account, ttl, now) || needsPerModel(u, cfg))) {
|
|
150
102
|
const full = await probeUsage();
|
|
151
103
|
const ts = Date.now();
|
|
152
|
-
const prevMu = mu;
|
|
153
104
|
if (readOAuthAccount()?.accountUuid === account) {
|
|
154
105
|
if (full) {
|
|
155
106
|
const teed = loadUsageSnapshot();
|
|
@@ -161,7 +112,7 @@ async function loadFreshSnapshots(cfg: Config, account: string | null, now: numb
|
|
|
161
112
|
writeUsage(u);
|
|
162
113
|
uAt = ts;
|
|
163
114
|
}
|
|
164
|
-
mu = { perModel: full.perModel, account, ts, sampledAt: ts
|
|
115
|
+
mu = { perModel: full.perModel, account, ts, sampledAt: ts };
|
|
165
116
|
saveModelUsage(mu);
|
|
166
117
|
const expected = gatedFamilies(u?.model ?? null, cfg.policy.switchModels);
|
|
167
118
|
const rows = Object.keys(full.perModel);
|
|
@@ -169,7 +120,7 @@ async function loadFreshSnapshots(cfg: Config, account: string | null, now: numb
|
|
|
169
120
|
log("usage.no_permodel_row", { families: expected.join(","), rows: rows.join(",") });
|
|
170
121
|
}
|
|
171
122
|
} else {
|
|
172
|
-
mu = { perModel: mu?.account === account ? (mu?.perModel ?? {}) : {}, account, ts, sampledAt: mu?.account === account ? (mu?.sampledAt ?? mu?.ts) : undefined
|
|
123
|
+
mu = { perModel: mu?.account === account ? (mu?.perModel ?? {}) : {}, account, ts, sampledAt: mu?.account === account ? (mu?.sampledAt ?? mu?.ts) : undefined };
|
|
173
124
|
saveModelUsage(mu);
|
|
174
125
|
}
|
|
175
126
|
}
|
|
@@ -224,7 +175,6 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
|
|
|
224
175
|
if (tee && tee.state.account === account2 && (active.lastUsageAt == null || tee.at >= active.lastUsageAt)) {
|
|
225
176
|
active.lastUsage = { fiveHour: tee.state.fiveHour, sevenDay: tee.state.sevenDay };
|
|
226
177
|
active.lastUsageAt = tee.state.sampledAt ?? tee.at;
|
|
227
|
-
if (tee.state.sessionWindowWeeklyCost != null) active.sessionWindowWeeklyCost = tee.state.sessionWindowWeeklyCost;
|
|
228
178
|
sampled = true;
|
|
229
179
|
}
|
|
230
180
|
if (mu2 && mu2.account === account2 && Object.keys(mu2.perModel).length > 0) {
|
|
@@ -322,26 +272,12 @@ export async function evaluateAndMaybeSwap(now = Date.now(), anticipatory = fals
|
|
|
322
272
|
};
|
|
323
273
|
if (!enforced2 && !isOver(u2, mu2, account2, bars, cfg, now)) return greedy(cfg.policy.greedySwapMargin);
|
|
324
274
|
|
|
325
|
-
const resetPreferred = cfg.policy.preferToUseBankedReset.includes("claude");
|
|
326
|
-
let resetTried = false;
|
|
327
275
|
while (true) {
|
|
328
276
|
const cur = loadAccounts();
|
|
329
277
|
const seat = seatOf(cur);
|
|
330
278
|
const pool = usable(cur.accounts);
|
|
331
279
|
const thresholds = barsOf(cur.accounts);
|
|
332
280
|
if (!enforced2 && seat && !seat.needsReauth && !isExhausted(seat, { now, thresholds, currentAccountUuid: null, switchFamilies })) return greedy(0);
|
|
333
|
-
if (resetPreferred && !resetTried && seat && !seat.needsReauth) {
|
|
334
|
-
const verdict = bankedResetVerdict({ seat, u: u2, mu: mu2, enforced: enforced2, cfg, bars: thresholds, switchFamilies, now });
|
|
335
|
-
if (verdict === "hold") {
|
|
336
|
-
log("decide.banked_reset_hold", { account: seat.accountUuid.slice(0, 8), enforced: enforced2 != null });
|
|
337
|
-
return { swapped: false, account: seat, reason: "banked-reset-hold" };
|
|
338
|
-
}
|
|
339
|
-
if (verdict === "claim") {
|
|
340
|
-
resetTried = true;
|
|
341
|
-
const claim = await claimBankedReset({ account: seat });
|
|
342
|
-
if (claim === "reset") return { swapped: false, account: seat, reason: "banked-reset", reset: true };
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
281
|
const ctx = { now, thresholds, currentAccountUuid: seat?.accountUuid ?? null, currentOrganizationUuid: seat?.organizationUuid ?? null, orgAffinityFloor: cfg.policy.greedySessionFloor, switchFamilies };
|
|
346
282
|
const best = pickBest(pool, ctx);
|
|
347
283
|
if (!best) break;
|
|
@@ -452,12 +388,11 @@ export async function recordEnforcedLimit(input: { limit: EnforcedClass; account
|
|
|
452
388
|
account: accountUuid,
|
|
453
389
|
ts: now,
|
|
454
390
|
sampledAt: resetsAt == null ? now : sampledAt ?? now,
|
|
455
|
-
...measureFamilyWindow(muSame, accountUuid, null, {}),
|
|
456
391
|
});
|
|
457
392
|
log("usage.enforced_limit", { kind: limit.kind, family: limit.family, resetsAt });
|
|
458
393
|
return { outcome: "stamped", resetsAt };
|
|
459
394
|
}
|
|
460
|
-
saveModelUsage({ perModel: carriedRows, account: accountUuid, ts: now, sampledAt
|
|
395
|
+
saveModelUsage({ perModel: carriedRows, account: accountUuid, ts: now, sampledAt });
|
|
461
396
|
if (account && limit.resetsAt != null) {
|
|
462
397
|
account.enforcedUntil = limit.resetsAt;
|
|
463
398
|
saveAccounts(idx);
|
package/src/lib/paths.ts
CHANGED
|
@@ -19,7 +19,6 @@ export const paths = {
|
|
|
19
19
|
usageJson: join(TM_HOME, "usage.json"),
|
|
20
20
|
modelUsageJson: join(TM_HOME, "model-usage.json"),
|
|
21
21
|
lastSwapJson: join(TM_HOME, "lastswap.json"),
|
|
22
|
-
lastResetJson: join(TM_HOME, "lastreset.json"),
|
|
23
22
|
depletedJson: join(TM_HOME, "depleted.json"),
|
|
24
23
|
nextCheckJson: join(TM_HOME, "nextcheck.json"),
|
|
25
24
|
respawnDir: join(TM_HOME, "respawn"),
|