tokenmaxxing 1.45.0 → 1.46.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 +1 -1
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/lib/picker.ts +17 -6
- package/src/lib/types.ts +3 -3
package/DESIGN.md
CHANGED
|
@@ -95,7 +95,7 @@ The [profile](docs/content/docs/switching-profile.mdx) records observed cache re
|
|
|
95
95
|
|
|
96
96
|
## 6. Honest papercuts
|
|
97
97
|
- **A move restarts the process.** Every move is a `claude` stop and `--resume`; anything typed in the split second before the SIGTERM is lost, the supervisor resets terminal mode so nothing is left garbled, and the first turn on the new account re-uploads context once (prompt cache is org-scoped). A bar-triggered move compacts on the old account first so that upload is the summary, not the transcript; a move after a refusal cannot, and re-uploads the full context.
|
|
98
|
-
- **Single-turn overshoot.** If one turn jumps from under the threshold straight past the wall, that turn ends rate-limited; the StopFailure hook walls the seat and moves the session, and the next prompt continues the conversation on the fresh account.
|
|
98
|
+
- **Single-turn overshoot.** If one turn jumps from under the threshold straight past the wall, that turn ends rate-limited; the StopFailure hook walls the seat and moves the session, and the next prompt continues the conversation on the fresh account. The static session-bar margin (`policy.projectionMargin`, default 3) reduces this.
|
|
99
99
|
- **Unsupervised sessions are not moved.** A claude started outside the supervisor runs on whatever login its environment names, by default Claude Code's default store, which tokenmaxxing never writes.
|
|
100
100
|
- **One shared identity file.** `~/.claude.json` keeps a single `oauthAccount`, which Claude Code rewrites after whichever session refreshed last, so `/status` can show another session's email. The store decides which credential a session uses.
|
|
101
101
|
- **Dead stores.** A refresh Claude Code cannot complete dead-clears the store; the sampler and the decision recognize the empty tokens without a request, flag the account, and `auth` repairs it in place.
|
package/README.md
CHANGED
|
@@ -101,7 +101,7 @@ See [How switching decides](docs/content/docs/switching.mdx) for the policy and
|
|
|
101
101
|
{
|
|
102
102
|
"thresholds": { "session": 90, "weekly": 98 },
|
|
103
103
|
"policy": {
|
|
104
|
-
"projectionMargin":
|
|
104
|
+
"projectionMargin": 3,
|
|
105
105
|
"switchModels": ["fable"],
|
|
106
106
|
"usagePollTtlMs": 90000,
|
|
107
107
|
"checkIntervalMs": 60000
|
|
@@ -109,7 +109,7 @@ See [How switching decides](docs/content/docs/switching.mdx) for the policy and
|
|
|
109
109
|
}
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
`thresholds.session` and `thresholds.weekly` are the two bars, one number each (an array `thresholds.session`, the former ladder, fails config loading with a message naming the field); `projectionMargin` is a fixed safety margin subtracted from
|
|
112
|
+
`thresholds.session` and `thresholds.weekly` are the two bars, one number each (an array `thresholds.session`, the former ladder, fails config loading with a message naming the field); `projectionMargin` is a fixed safety margin subtracted from the session bar only (default 3), so a large turn is less likely to blow past the 5-hour bar between checks; the weekly bar takes no margin because one turn cannot overshoot a week; `switchModels` names the models whose per-model cap triggers a move; `usagePollTtlMs` is how long a `/usage` attempt stays fresh, for a seat's per-model poll and for the account each check tick samples; `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 - re-run `tokenmaxxing init` after changing it so the timer unit picks up the new tick.
|
|
113
113
|
|
|
114
114
|
State lives entirely in `~/.config/tokenmaxxing/`. Each account's credential store is `stores/<uuid8>/`, which Claude Code reads and refreshes as its own: on macOS the credential is a login-keychain item keyed by the store path (never plaintext on disk), on Linux a 0600 `.credentials.json` inside the store (the same plaintext model claude itself uses for `~/.claude/.credentials.json`).
|
|
115
115
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.46.0",
|
|
4
4
|
"description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/lib/picker.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { AccountSchema, ThresholdsSchema, type Account, type Config, type Thresh
|
|
|
6
6
|
export function thresholdBars(cfg: Config): Thresholds {
|
|
7
7
|
return {
|
|
8
8
|
session: cfg.thresholds.session - cfg.policy.projectionMargin,
|
|
9
|
-
weekly: cfg.thresholds.weekly
|
|
9
|
+
weekly: cfg.thresholds.weekly,
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -87,11 +87,22 @@ export function earliestReset(a: Account, now: number): number {
|
|
|
87
87
|
return Math.min(session != null && session > now ? session : Number.POSITIVE_INFINITY, weeklyExpiry(a, now));
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
function windowResetAt(w: Window, now: number): number | null {
|
|
91
|
+
if (w.resetsAt != null) return nextWeeklyReset(w.resetsAt, now);
|
|
92
|
+
if (w.windowSeconds != null) return w.sampledAt + w.windowSeconds * 1000;
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function pacePressure(a: Account, ctx: PickCtx): number {
|
|
97
|
+
const binding = minBy(gatedWindows(a, ctx.families), (w) => Math.max(0, 100 - liveUsed(w, ctx.now)));
|
|
98
|
+
if (binding != null) {
|
|
99
|
+
const reset = windowResetAt(binding, ctx.now);
|
|
100
|
+
if (reset != null) return Math.max(0, 100 - liveUsed(binding, ctx.now)) / Math.max(1, reset - ctx.now);
|
|
101
|
+
}
|
|
91
102
|
const weekly = weeklyWindow(a);
|
|
92
|
-
const reset = nextWeeklyReset(weekly?.resetsAt ?? null, now);
|
|
103
|
+
const reset = nextWeeklyReset(weekly?.resetsAt ?? null, ctx.now);
|
|
93
104
|
if (weekly == null || reset == null) return 0;
|
|
94
|
-
return Math.max(0, 100 - liveUsed(weekly, now)) / Math.max(1, reset - now);
|
|
105
|
+
return Math.max(0, 100 - liveUsed(weekly, ctx.now)) / Math.max(1, reset - ctx.now);
|
|
95
106
|
}
|
|
96
107
|
|
|
97
108
|
export function seatHeadroom(a: Account, ctx: PickCtx): number {
|
|
@@ -102,7 +113,7 @@ export function seatHeadroom(a: Account, ctx: PickCtx): number {
|
|
|
102
113
|
|
|
103
114
|
const swapPreference = (ctx: PickCtx) => [
|
|
104
115
|
...(ctx.seats == null ? [] : [(a: Account) => -seatHeadroom(a, ctx)]),
|
|
105
|
-
(a: Account) => -pacePressure(a, ctx
|
|
116
|
+
(a: Account) => -pacePressure(a, ctx),
|
|
106
117
|
(a: Account) => weeklyExpiry(a, ctx.now),
|
|
107
118
|
(a: Account) => weeklyWindow(a)?.usedPercentage ?? 101,
|
|
108
119
|
];
|
|
@@ -116,7 +127,7 @@ export function currentWins(active: Account | null, accounts: Account[], ctx: Pi
|
|
|
116
127
|
if (!active || active.needsReauth || isExhausted(active, ctx)) return false;
|
|
117
128
|
const best = pickBest(accounts, { ...ctx, currentId: null });
|
|
118
129
|
if (best == null || best.id === active.id) return true;
|
|
119
|
-
if (margin > 1) return pacePressure(best, ctx
|
|
130
|
+
if (margin > 1) return pacePressure(best, ctx) <= pacePressure(active, ctx) * margin;
|
|
120
131
|
return swapPreference(ctx).every((k) => k(active) === k(best));
|
|
121
132
|
}
|
|
122
133
|
|
package/src/lib/types.ts
CHANGED
|
@@ -114,7 +114,7 @@ export const ConfigSchema = z
|
|
|
114
114
|
opencodeBin: z.string().default(""),
|
|
115
115
|
policy: z
|
|
116
116
|
.object({
|
|
117
|
-
projectionMargin: z.number().min(0).max(100).default(
|
|
117
|
+
projectionMargin: z.number().min(0).max(100).default(3),
|
|
118
118
|
switchModels: z
|
|
119
119
|
.array(z.string())
|
|
120
120
|
.default(["fable"])
|
|
@@ -125,9 +125,9 @@ export const ConfigSchema = z
|
|
|
125
125
|
})
|
|
126
126
|
.prefault({}),
|
|
127
127
|
})
|
|
128
|
-
.refine((cfg) => cfg.policy.projectionMargin <
|
|
128
|
+
.refine((cfg) => cfg.policy.projectionMargin < cfg.thresholds.session, {
|
|
129
129
|
path: ["policy", "projectionMargin"],
|
|
130
|
-
message: "must be strictly below
|
|
130
|
+
message: "must be strictly below the session threshold (the session bar would hit zero and every account would read as exhausted)",
|
|
131
131
|
});
|
|
132
132
|
export type Config = z.infer<typeof ConfigSchema>;
|
|
133
133
|
|