tokenmaxxing 1.126.0 → 1.128.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 CHANGED
@@ -62,7 +62,7 @@ The Stop hook's stdin has no usage data, but the **statusLine does** (`rate_limi
62
62
  4. Write `respawn/<session-id>` (atomic temp+rename) naming the target, with `waitUntil` now for a plain move or the assigned reset when the pool is depleted. No credential is written.
63
63
 
64
64
  ### 3.3 Move and depleted-pool pause (supervisor)
65
- The supervisor sees `respawn/<sid>`, SIGTERMs its child at the already-committed turn boundary, deletes the marker, resets the terminal, compacts the conversation when the marker asks for it and the old seat's record carries no live `enforcedUntil` (`compact: true` from the Stop and SessionStart hooks and from the supervisor's own seat watch, `false` from StopFailure: it runs `claude -p --resume <live-id> /compact` under the old seat's store, bounded at five minutes; the compaction counts only when a `compact_boundary` row lands in the transcript, because a refused compaction still exits 0, and a failure only prints why), and (when `waitUntil` is in the future) shows an interruptible countdown to the reset; then it relaunches `claude --resume <live-id>` (`--session-id <live-id>` when that id has no transcript yet, because `--resume` exits with `No conversation found` for it) with `CLAUDE_SECURESTORAGE_CONFIG_DIR` set to the target's store and rewrites the session's presence. The marker file is keyed by the launch id `<sid>`; the live id inside it is the session id the supervised process runs now, which `/clear` and an in-session `/resume` change. A hook adopts a differing stdin id as the live id (`current` in `sessions/<sid>.json`) only when its own process descends, through shell processes alone, from the presence record's pid; any other differing id is a nested session and writes nothing. The resumed process reads that store cold → runs on the target account, same conversation. The `--session-id` path is the exception: the conversation has no prior turn (a `/clear` with no turn since, or a move before the first turn), so it starts with no compaction and no first prompt, and in a stream-json session the relay replays the harness lines the killed child received: `user` lines and the `initialize` and session-state control requests in `REPLAYED_REQUESTS`; every other control request gets an error `control_response` on stdout, because a repeat could run its effect twice, and the `control_response` answers to the killed child's own requests are dropped. A first launch with `--resume <id>` takes the same path when the supervisor holds `sessions/<id>.json` for that id and no project directory holds its transcript. On the `--resume` path the relaunch submits a first prompt that names the move (and the compaction, when it landed). After a Stop move the previous turn had finished, so the prompt asks Claude to end the turn without restating it, and to relaunch any background Bash task, Monitor, or Workflow run that turn waited on, because the move ended it. After any other move the prompt asks Claude to continue the task, says the restart ended every background Bash task, Monitor, and Workflow run the session started, and asks Claude to relaunch what the task still needs; a SessionStart move is in that group because a prompt sent inside the marker poll window can already have started on the old seat. The conversation goes on without a keystroke: in a terminal session it is the positional prompt after the session id; in a stream-json session (`--input-format stream-json`, where the binary ignores a positional prompt) the supervisor owns stdin, relays the harness's lines to the child through a pipe, sends the new process the latest `initialize` and session-state control requests the harness sent, because the Agent SDK sends `initialize` once per query and it carries the appended system prompt, the hooks, and the SDK MCP servers, and then writes the prompt as the first `user` line. In a terminal session the supervisor's own notices go to stderr. In a stream-json session (`--output-format stream-json`, read from the effective flags like the relay) there is no terminal and no statusline, so the supervisor writes each notice (the compaction and its failure, the move, the depleted-pool wait with its resume time, the resume) to stdout as a `system` line of subtype `informational` and level `warning`, the shape Claude Code emits for its own print-mode notices, which the harness renders in the conversation while the stream stays parseable; the per-second countdown ticks stay terminal-only, and the supervisor writes only while no child is running. The `SessionStart` hook (every source) re-runs the decision before the first turn and writes the same marker if the seat is already over its bar or the pool is depleted. The supervisor also watches its own seat every ten seconds while the child runs (account record plus tee, the same bars) and writes the marker itself when the seat is at or over a bar, so a move never depends on a hook firing: a harness session between turns whose background workflow burns the window, or a subagent refusal that stamped the wall, is moved within seconds. SIGTERM to the supervisor is forwarded to the child and the supervisor exits after it, so a harness that stops the wrapper (the Agent SDK does) never leaves an orphaned claude on the old seat. A marker whose `launchedAt` does not match the running child is discarded, and after a Ctrl-C on the countdown a later marker whose `waitUntil` falls inside the skipped wait is ignored. A marker that fails to parse is never deleted and never treated as absent: the supervisor stops the child, restores the terminal, and exits naming the marker path. In a stream-json session that fatal, the wrap-depth abort, the wrapper-chain abort, and a failed presence write also go to stdout as the same `informational`/`warning` line.
65
+ The supervisor sees `respawn/<sid>`, SIGTERMs its child at the already-committed turn boundary, deletes the marker, resets the terminal, compacts the conversation when the marker asks for it and the old seat's record carries no live `enforcedUntil` (`compact: true` from the Stop and SessionStart hooks and from the supervisor's own seat watch, `false` from StopFailure: it runs `claude -p --resume <live-id> /compact` under the old seat's store, bounded at five minutes; the compaction counts only when a `compact_boundary` row lands in the transcript, because a refused compaction still exits 0, and a failure only prints why), and (when `waitUntil` is in the future) shows an interruptible countdown to the reset; then it relaunches `claude --resume <live-id>` (`--session-id <live-id>` when that id has no transcript yet, because `--resume` exits with `No conversation found` for it) with `CLAUDE_SECURESTORAGE_CONFIG_DIR` set to the target's store and rewrites the session's presence. The marker file is keyed by the launch id `<sid>`; the live id inside it is the session id the supervised process runs now, which `/clear` and an in-session `/resume` change. A hook adopts a differing stdin id as the live id (`current` in `sessions/<sid>.json`) only when its own process descends, through shell processes alone, from the presence record's pid; any other differing id is a nested session and writes nothing. The resumed process reads that store cold → runs on the target account, same conversation. The `--session-id` path is the exception: the conversation has no prior turn (a `/clear` with no turn since, or a move before the first turn), so it starts with no compaction and no first prompt, and in a stream-json session the relay replays the harness lines the killed child received: `user` lines and the `initialize` and session-state control requests in `REPLAYED_REQUESTS`; every other control request gets an error `control_response` on stdout, because a repeat could run its effect twice, and the `control_response` answers to the killed child's own requests are dropped. A first launch with `--resume <id>` takes the same path when the supervisor holds `sessions/<id>.json` for that id and no project directory holds its transcript. On the `--resume` path the relaunch submits a first prompt that names the move (and the compaction, when it landed). After a Stop move the previous turn had finished, so the prompt asks Claude to end the turn without restating it, and to relaunch any background Bash task, Monitor, or Workflow run that turn waited on, because the move ended it. After any other move the prompt asks Claude to continue the task, says the restart ended every background Bash task, Monitor, and Workflow run the session started, and asks Claude to relaunch what the task still needs; a SessionStart move is in that group because a prompt sent inside the marker poll window can already have started on the old seat. The conversation goes on without a keystroke: in a terminal session it is the positional prompt after the session id; in a stream-json session (`--input-format stream-json`, where the binary ignores a positional prompt) the supervisor owns stdin, relays the harness's lines to the child through a pipe, sends the new process the latest `initialize` and session-state control requests the harness sent, because the Agent SDK sends `initialize` once per query and it carries the appended system prompt, the hooks, and the SDK MCP servers, and then writes the prompt as the first `user` line. In a terminal session the supervisor's own notices go to stderr. In a stream-json session (`--output-format stream-json`, read from the effective flags like the relay) there is no terminal and no statusline, so the supervisor writes each notice (the compaction and its failure, the move, the depleted-pool wait with its resume time, the resume) to stdout as a `system` line of subtype `informational` and level `warning`, the shape Claude Code emits for its own print-mode notices, which the harness renders in the conversation while the stream stays parseable; the per-second countdown ticks stay terminal-only, and the supervisor writes only while no child is running. Before the first notice of a move it writes a `task_notification` line with `status: "stopped"` and `reason: "worker_restart"` for each task the old child started on stdout (`task_started`) and did not end there (a `task_notification`, or a `task_updated` to `completed`, `failed`, or `killed`), the line Claude Code writes when a restarted process finds a task orphaned, because the move ended those tasks and a harness that tracks them from the stream, as T3 Code does, otherwise shows them running until the session exits. The `SessionStart` hook (every source) re-runs the decision before the first turn and writes the same marker if the seat is already over its bar or the pool is depleted. The supervisor also watches its own seat every ten seconds while the child runs (account record plus tee, the same bars) and writes the marker itself when the seat is at or over a bar, so a move never depends on a hook firing: a harness session between turns whose background workflow burns the window, or a subagent refusal that stamped the wall, is moved within seconds. SIGTERM to the supervisor is forwarded to the child and the supervisor exits after it, so a harness that stops the wrapper (the Agent SDK does) never leaves an orphaned claude on the old seat. A marker whose `launchedAt` does not match the running child is discarded, and after a Ctrl-C on the countdown a later marker whose `waitUntil` falls inside the skipped wait is ignored. A marker that fails to parse is never deleted and never treated as absent: the supervisor stops the child, restores the terminal, and exits naming the marker path. In a stream-json session that fatal, the wrap-depth abort, the wrapper-chain abort, and a failed presence write also go to stdout as the same `informational`/`warning` line.
66
66
 
67
67
  ### 3.4 Placement (supervisor, under the pool lock)
68
68
  1. Fold every account's tee into the index.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.126.0",
3
+ "version": "1.128.0",
4
4
  "description": "Automatic Claude Code account switching: pool multiple accounts, start each session on its own account, and resume a session on a fresher account when quota fills.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli/add.ts CHANGED
@@ -1,14 +1,18 @@
1
1
  import { withLock } from "../lib/lock.ts";
2
2
  import type { Provider } from "../lib/provider.ts";
3
- import { loadAccounts, saveAccounts, upsertAccount } from "../lib/state.ts";
4
- import { sessionWindow, weeklyWindow } from "../lib/picker.ts";
3
+ import { loadAccounts, loadConfig, saveAccounts, upsertAccount } from "../lib/state.ts";
4
+ import { bufferedUsed, liveUsed, sessionWindow, thresholdBars, weeklyWindow } from "../lib/picker.ts";
5
5
  import { c, count } from "./render.ts";
6
- import type { Account } from "../lib/types.ts";
6
+ import type { Account, Window } from "../lib/types.ts";
7
7
 
8
- export function usageNote(account: Account): string {
8
+ export function usageNote(p: Provider, account: Account): string {
9
9
  const session = sessionWindow(account);
10
10
  const week = weeklyWindow(account);
11
- return session && week ? ` (session ${session.usedPercentage}% / week ${week.usedPercentage}%)` : "";
11
+ if (!session || !week) return "";
12
+ const bars = thresholdBars(loadConfig());
13
+ const now = Date.now();
14
+ const pct = (w: Window) => Math.round(p.statusOnly ? liveUsed(w, now) : bufferedUsed(account, w, bars, now));
15
+ return ` (session ${pct(session)}% / week ${pct(week)}%)`;
12
16
  }
13
17
 
14
18
  export async function cmdAdd(p: Provider): Promise<number> {
@@ -28,7 +32,7 @@ export async function cmdAdd(p: Provider): Promise<number> {
28
32
  });
29
33
 
30
34
  console.log();
31
- const note = harvested.sample ? usageNote(account) : "";
35
+ const note = harvested.sample ? usageNote(p, account) : "";
32
36
  console.log(`${c.green("✓")} added ${c.bold(account.label)} (${account.tier ?? "?"})${note} → pool now has ${count({ n: poolSize, noun: "account" })}`);
33
37
  return 0;
34
38
  }
package/src/cli/auth.ts CHANGED
@@ -91,7 +91,7 @@ async function reauthOne(p: Provider, target: Account): Promise<boolean> {
91
91
  });
92
92
  if (account === null) return false;
93
93
 
94
- const note = harvested.sample ? usageNote(account) : "";
94
+ const note = harvested.sample ? usageNote(p, account) : "";
95
95
  console.log(`${c.green("✓")} reauthed ${c.bold(account.email ?? account.label)} (${account.tier ?? "?"})${note}`);
96
96
  return true;
97
97
  }
package/src/cli/serve.ts CHANGED
@@ -34,11 +34,11 @@ function iso(epochMs: number | null): string | null {
34
34
  }
35
35
 
36
36
  function claudeBody(usage: UsageReport): unknown {
37
- const win = (w: WindowReport | null) => (w ? { utilization: w.usedPercentage, resets_at: iso(w.resetsAt) } : null);
37
+ const win = (w: WindowReport | null) => (w ? { utilization: w.bufferedPercentage, resets_at: iso(w.resetsAt) } : null);
38
38
  return {
39
39
  five_hour: win(usage.fiveHour),
40
40
  seven_day: win(usage.week),
41
- limits: usage.limits.map((w) => ({ kind: "weekly_scoped", percent: w.usedPercentage, resets_at: iso(w.resetsAt), scope: { model: { display_name: w.name } } })),
41
+ limits: usage.limits.map((w) => ({ kind: "weekly_scoped", percent: w.bufferedPercentage, resets_at: iso(w.resetsAt), scope: { model: { display_name: w.name } } })),
42
42
  };
43
43
  }
44
44
 
@@ -46,7 +46,7 @@ function codexBody(usage: UsageReport, account: Account): unknown {
46
46
  const win = (w: WindowReport | undefined) =>
47
47
  w
48
48
  ? {
49
- used_percent: w.usedPercentage,
49
+ used_percent: w.bufferedPercentage,
50
50
  reset_at: w.resetsAt == null ? null : Math.round(w.resetsAt / 1000),
51
51
  ...(w.windowSeconds == null ? {} : { limit_window_seconds: w.windowSeconds }),
52
52
  }
@@ -142,8 +142,9 @@ async function apiCall(req: Request): Promise<Response> {
142
142
  const now = Date.now();
143
143
  const cfg = loadConfig();
144
144
  const current = await pool.read(account, cfg, now);
145
- pool.fold(current, thresholdBars(cfg));
146
- const usage = usageReport({ ...current, windows: current.windows.filter((w) => now - w.sampledAt <= STALE_AFTER_MS) }, now);
145
+ const bars = thresholdBars(cfg);
146
+ pool.fold(current, bars);
147
+ const usage = usageReport({ ...current, windows: current.windows.filter((w) => now - w.sampledAt <= STALE_AFTER_MS) }, bars, now);
147
148
  if (!usage || (!usage.fiveHour && !usage.week)) return json({ error: `no usage figure newer than ${STALE_AFTER_MS / 3_600_000}h for this account` }, 502);
148
149
  return json({ status_code: 200, header: { "Content-Type": ["application/json"] }, body: JSON.stringify(pool.body(usage, current)) });
149
150
  }
package/src/cli/status.ts CHANGED
@@ -6,12 +6,12 @@ import { opencodeGo } from "../lib/opencodego.ts";
6
6
  import { loadAccounts, loadConfig, saveAccounts } from "../lib/state.ts";
7
7
  import { withLock } from "../lib/lock.ts";
8
8
  import { codexPool, grokPool, opencodeGoPool } from "../lib/paths.ts";
9
- import { barFor, earliestReset, gatedWindows, landGrant, landWindows, isExhausted, isSessionWindow, limitWindows, liveUsed, nextWeeklyReset, sessionWindow, thresholdBars, weeklyWindow } from "../lib/picker.ts";
9
+ import { barFor, bufferedUsed, earliestReset, gatedWindows, landGrant, landWindows, isExhausted, isSessionWindow, limitWindows, liveUsed, nextWeeklyReset, sessionWindow, thresholdBars, weeklyWindow } from "../lib/picker.ts";
10
10
  import type { Provider, SampleReport } from "../lib/provider.ts";
11
11
  import { bar, c, count, emitJson, fmtAgo } from "./render.ts";
12
12
  import type { Account, AccountThresholds, Bars, Config, Thresholds, Window } from "../lib/types.ts";
13
13
 
14
- export type WindowReport = { usedPercentage: number; resetsAt: number | null; windowSeconds: number | null };
14
+ export type WindowReport = { usedPercentage: number; bufferedPercentage: number; resetsAt: number | null; windowSeconds: number | null };
15
15
 
16
16
  export type UsageReport = { fiveHour: WindowReport | null; week: WindowReport | null; limits: (WindowReport & { name: string })[] };
17
17
 
@@ -41,23 +41,24 @@ type PoolReport = {
41
41
 
42
42
  export type StatusReport = { now: number; claude: PoolReport; codex: PoolReport; grok: PoolReport; opencodeGo: PoolReport };
43
43
 
44
- function currentWindow(w: Window, now: number): WindowReport {
44
+ function currentWindow(a: Account, w: Window, bars: Bars | null, now: number): WindowReport {
45
45
  const passed = w.resetsAt != null && w.resetsAt <= now;
46
46
  return {
47
- usedPercentage: passed ? 0 : w.usedPercentage,
47
+ usedPercentage: liveUsed(w, now),
48
+ bufferedPercentage: bars == null ? liveUsed(w, now) : bufferedUsed(a, w, bars, now),
48
49
  resetsAt: isSessionWindow(w) ? (passed ? null : w.resetsAt) : nextWeeklyReset(w.resetsAt, now),
49
50
  windowSeconds: w.windowSeconds,
50
51
  };
51
52
  }
52
53
 
53
- export function usageReport(a: Account, now: number): UsageReport | null {
54
+ export function usageReport(a: Account, bars: Bars | null, now: number): UsageReport | null {
54
55
  if (a.lastUsageAt == null) return null;
55
56
  const session = sessionWindow(a);
56
57
  const week = weeklyWindow(a);
57
58
  return {
58
- fiveHour: session ? currentWindow(session, now) : null,
59
- week: week ? currentWindow(week, now) : null,
60
- limits: limitWindows(a).map((w) => ({ name: w.name ?? "", ...currentWindow(w, now) })),
59
+ fiveHour: session ? currentWindow(a, session, bars, now) : null,
60
+ week: week ? currentWindow(a, week, bars, now) : null,
61
+ limits: limitWindows(a).map((w) => ({ name: w.name ?? "", ...currentWindow(a, w, bars, now) })),
61
62
  };
62
63
  }
63
64
 
@@ -151,7 +152,7 @@ async function collect(p: Provider, cfg: Config, now: number, cached: boolean):
151
152
  needsReauth: a.needsReauth === true,
152
153
  exhausted: isExhausted(a, ctx),
153
154
  thresholds: own.get(a.label) ?? null,
154
- usage: usageReport(a, now),
155
+ usage: usageReport(a, p.statusOnly ? null : bars, now),
155
156
  usageAt: a.lastUsageAt ?? null,
156
157
  limitsAt: limits.length > 0 ? Math.max(...limits.map((w) => w.sampledAt)) : null,
157
158
  sample: reports.get(a.id) ?? (cached ? { ok: true, source: "cached" } : { ok: false, reason: "not sampled" }),
@@ -194,7 +195,7 @@ function renderGrid(cards: Card[]): void {
194
195
  }
195
196
 
196
197
  function usageRow(name: string, w: WindowReport): string {
197
- return `${NOTE_INDENT}${name.padEnd(5)} ${bar(w.usedPercentage)}`;
198
+ return `${NOTE_INDENT}${name.padEnd(5)} ${bar(w.bufferedPercentage)}`;
198
199
  }
199
200
 
200
201
  function sampleFailedNotes(input: { cached: boolean; usageAt: number | null; reason: string; now: number }): Note[] {
@@ -245,6 +246,7 @@ function card(p: Provider, a: StatusAccount, now: number, staleAfterMs: number):
245
246
 
246
247
  function renderPool(p: Provider, pool: PoolReport, header: string, now: number, staleAfterMs: number): void {
247
248
  console.log(c.dim(header));
249
+ if (!p.statusOnly) console.log(c.dim("buffered usage quota: percent of each account's switch threshold, so it reads higher than the usage Claude or Codex reports"));
248
250
  console.log();
249
251
  renderGrid(pool.accounts.map((a) => card(p, a, now, staleAfterMs)));
250
252
  if (pool.gatedNote) {
@@ -3,7 +3,7 @@ import { claudePool, seatFromEnv } from "../lib/paths.ts";
3
3
  import { errorMessage } from "../lib/log.ts";
4
4
  import { loadAccounts, loadConfig, writeUsage } from "../lib/state.ts";
5
5
  import { familyTokens, matchedFamily, parseStatusLineStdin, parseStatusLineModel } from "../lib/usage.ts";
6
- import { earliestReset, limitWindows, weeklyExpiry, weeklyWindow } from "../lib/picker.ts";
6
+ import { bufferedUsed, earliestReset, limitWindows, liveUsed, thresholdBars, weeklyExpiry, weeklyWindow } from "../lib/picker.ts";
7
7
  import { readStdin } from "../lib/proc.ts";
8
8
  import { worktreeName } from "../lib/worktree.ts";
9
9
  import { fmtResetShort, makeColors, makeUsagePaint, statuslineColor } from "../cli/render.ts";
@@ -12,13 +12,18 @@ import {
12
12
  StatusLineStdinSchema,
13
13
  type Account,
14
14
  type AccountsIndex,
15
+ type Bars,
15
16
  type UsageState,
16
17
  type UsageWindow,
17
18
  type Window,
18
19
  } from "../lib/types.ts";
19
20
 
21
+ const FIVE_HOUR_S = 5 * 3600;
22
+ const SEVEN_DAY_S = 7 * 24 * 3600;
23
+
20
24
  export type RenderCtx = {
21
25
  accounts: AccountsIndex;
26
+ bars: Bars;
22
27
  perModel: Record<string, Window>;
23
28
  switchModels: string[];
24
29
  worktree: string | null;
@@ -31,7 +36,7 @@ export type RenderCtx = {
31
36
  export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
32
37
  const col = makeColors(ctx.color);
33
38
  const paint = makeUsagePaint({ enabled: ctx.color, truecolor: ctx.truecolor });
34
- const used = (w: UsageWindow) => (w.resetsAt != null && w.resetsAt <= ctx.now ? 0 : w.usedPercentage);
39
+ const used = (a: Account, w: Window) => bufferedUsed(a, w, ctx.bars, ctx.now);
35
40
  const reset = (epochMs: number | null) => fmtResetShort(epochMs, ctx.now);
36
41
 
37
42
  const parsed = StatusLineStdinSchema.safeParse(stdinObj);
@@ -51,8 +56,7 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
51
56
  const removed = d?.cost?.total_lines_removed ?? 0;
52
57
  if (added > 0 || removed > 0) info.push(`${col.green(`+${added}`)}/-${removed}`);
53
58
 
54
- const seg = (label: string, w: UsageWindow, resetAt: number | null) => {
55
- const u = used(w);
59
+ const seg = (label: string, u: number, resetAt: number | null) => {
56
60
  if (!ctx.color) return `${label}${reset(resetAt)}${Math.round(u)}`;
57
61
  const body = label !== "" ? label : reset(resetAt);
58
62
  return col.bold(paint(u)(body !== "" ? body : u > 0 ? "?" : "0"));
@@ -64,15 +68,17 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
64
68
  if (family && !Object.keys(ctx.perModel).some((k) => familyTokens(k).includes(family))) {
65
69
  windows.push(`${initial(family)}?`);
66
70
  }
67
- for (const [name, w] of Object.entries(ctx.perModel)) windows.push(seg(initial(name), w, null));
71
+ const seatUuid = ctx.liveAccount;
72
+ const seat = ctx.accounts.accounts.find((a) => a.id === seatUuid);
73
+ const seatUsed = (w: Window) => (seat ? used(seat, w) : liveUsed(w, ctx.now));
74
+ const stdinWindow = (w: UsageWindow, windowSeconds: number): Window => ({ name: null, ...w, windowSeconds, sampledAt: ctx.now });
75
+ for (const [name, w] of Object.entries(ctx.perModel)) windows.push(seg(initial(name), seatUsed(w), null));
68
76
  if (wins) {
69
- windows.push(seg("", wins.fiveHour, wins.fiveHour.resetsAt));
70
- windows.push(seg("", wins.sevenDay, wins.sevenDay.resetsAt));
77
+ windows.push(seg("", seatUsed(stdinWindow(wins.fiveHour, FIVE_HOUR_S)), wins.fiveHour.resetsAt));
78
+ windows.push(seg("", seatUsed(stdinWindow(wins.sevenDay, SEVEN_DAY_S)), wins.sevenDay.resetsAt));
71
79
  }
72
- const seatUuid = ctx.liveAccount;
73
80
  const walled = (a: Account) => a.enforcedUntil != null && a.enforcedUntil > ctx.now;
74
- const wallSeg = (wall: number) => seg("", { usedPercentage: 100, resetsAt: wall }, wall);
75
- const seat = ctx.accounts.accounts.find((a) => a.id === seatUuid);
81
+ const wallSeg = (wall: number) => seg("", 100, wall);
76
82
  if (seat?.enforcedUntil != null && walled(seat)) windows.push(wallSeg(seat.enforcedUntil));
77
83
  const seatMarker = seat && walled(seat) ? paint(100)("◆") : col.green("◆");
78
84
  const active =
@@ -91,15 +97,16 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
91
97
  if (a.enforcedUntil != null && walled(a)) return `${marker} ${wallSeg(a.enforcedUntil)}`;
92
98
  const week = weeklyWindow(a);
93
99
  if (week == null) return `${marker} ?`;
94
- const weekUsed = used(week);
100
+ const weekUsed = used(a, week);
95
101
  if (Math.round(weekUsed) <= 0) return `${marker} ${paint(0)("full")}`;
96
102
 
97
103
  const parts: string[] = [];
98
104
  for (const w of limitWindows(a)) {
99
- if (used(w) > weekUsed) parts.push(seg(initial(w.name ?? ""), w, null));
105
+ const u = used(a, w);
106
+ if (u > weekUsed) parts.push(seg(initial(w.name ?? ""), u, null));
100
107
  }
101
108
  const expiry = weeklyExpiry(a, ctx.now);
102
- parts.push(seg("", week, Number.isFinite(expiry) ? expiry : null));
109
+ parts.push(seg("", weekUsed, Number.isFinite(expiry) ? expiry : null));
103
110
  return `${marker} ${parts.join(" ")}`;
104
111
  };
105
112
 
@@ -130,6 +137,7 @@ export async function runStatusline(): Promise<number> {
130
137
  const live = accounts.accounts.find((a) => a.id === account);
131
138
  const ctx: RenderCtx = {
132
139
  accounts,
140
+ bars: thresholdBars(cfg),
133
141
  perModel: Object.fromEntries((live ? limitWindows(live) : []).map((w) => [w.name ?? "", w])),
134
142
  switchModels: cfg.policy.switchModels,
135
143
  worktree: dir == null ? null : worktreeName(dir),
@@ -285,10 +285,31 @@ function userLine(text: string): string {
285
285
 
286
286
  const STDOUT_QUIET_MS = 1000;
287
287
 
288
- function seatTee(seatId: string, model: ModelInfo | null): (line: string) => void {
288
+ const TaskLineSchema = z.discriminatedUnion("subtype", [
289
+ z.looseObject({ type: z.literal("system"), subtype: z.literal("task_started"), task_id: z.string(), description: z.string(), session_id: z.string() }),
290
+ z.looseObject({ type: z.literal("system"), subtype: z.literal("task_notification"), task_id: z.string() }),
291
+ z.looseObject({ type: z.literal("system"), subtype: z.literal("task_updated"), task_id: z.string(), patch: z.looseObject({ status: z.enum(["completed", "failed", "killed"]) }) }),
292
+ ]);
293
+
294
+ type OpenTasks = Map<string, { description: string; sessionId: string }>;
295
+
296
+ function trackTask(open: OpenTasks, value: unknown): void {
297
+ const task = TaskLineSchema.safeParse(value);
298
+ if (!task.success) return;
299
+ if (task.data.subtype === "task_started") open.set(task.data.task_id, { description: task.data.description, sessionId: task.data.session_id });
300
+ else open.delete(task.data.task_id);
301
+ }
302
+
303
+ function stoppedTaskLines(open: OpenTasks): string[] {
304
+ return [...open].map(([id, task]) => `${JSON.stringify({ type: "system", subtype: "task_notification", task_id: id, status: "stopped", reason: "worker_restart", output_file: "", summary: `Stopped by a tokenmaxxing restart: ${task.description || id}`, uuid: crypto.randomUUID(), session_id: task.sessionId })}\n`);
305
+ }
306
+
307
+ function seatTee(seatId: string, model: ModelInfo | null, open: OpenTasks): (line: string) => void {
289
308
  let last: UsageWindows | null = null;
290
309
  return (line) => {
291
- const msg = parseStreamLine(line);
310
+ const value = JsonTextSchema.safeParse(line).data;
311
+ trackTask(open, value);
312
+ const msg = parseStreamLine(value);
292
313
  if (msg?.windows) last = msg.windows;
293
314
  else if (msg?.type !== "assistant") return;
294
315
  if (last) writeUsage({ fiveHour: last.fiveHour, sevenDay: last.sevenDay, account: seatId, ts: Date.now(), model });
@@ -548,7 +569,8 @@ export async function runSupervisor(argv: string[]): Promise<number> {
548
569
  if (terminating) return null;
549
570
  const picked = (wanted == null ? null : (loadAccounts(claudePool).accounts.find((a) => a.id === wanted) ?? null)) ?? pickSeat(gate.launchedAt, model);
550
571
  log("supervisor.launch", { sid, respawns, seat: picked?.id.slice(0, 8) ?? null, args: launchArgs.join(" "), injected: firstLine !== null });
551
- const tee = stream && picked ? seatTee(picked.id, model) : null;
572
+ const open: OpenTasks = new Map();
573
+ const tee = stream && picked ? seatTee(picked.id, model, open) : null;
552
574
  const spawned = Bun.spawn([real, ...launchArgs], {
553
575
  stdin: relay === null ? "inherit" : "pipe",
554
576
  stdout: tee === null ? "inherit" : "pipe",
@@ -578,14 +600,14 @@ export async function runSupervisor(argv: string[]): Promise<number> {
578
600
  });
579
601
  }
580
602
  releaseClaimLocked(sid);
581
- return { child: spawned, seat: picked, drain };
603
+ return { child: spawned, seat: picked, drain, open };
582
604
  });
583
605
  if (launched == null) {
584
606
  await releaseClaim();
585
607
  return 143;
586
608
  }
587
609
 
588
- const { child: proc, seat } = launched;
610
+ const { child: proc, seat, open } = launched;
589
611
  drainStdout = launched.drain;
590
612
  let seatCheckAt = 0;
591
613
  await raceMarkerOrExit({
@@ -614,6 +636,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
614
636
  rmSync(marker, { force: true });
615
637
  respawns++;
616
638
  noticeSid = m.sessionId;
639
+ for (const line of stoppedTaskLines(open)) process.stdout.write(line);
617
640
  const accounts = loadAccounts(claudePool).accounts;
618
641
  const label = accounts.find((a) => a.id === m.accountId)?.label ?? m.accountId.slice(0, 8);
619
642
  const walledUntil = accounts.find((a) => a.id === seat?.id)?.enforcedUntil ?? 0;
package/src/lib/picker.ts CHANGED
@@ -64,6 +64,13 @@ export function barFor(a: Account, w: Window, bars: Bars, now: number): number {
64
64
  return released ? Math.max(pick(own), pick(bars)) : pick(own);
65
65
  }
66
66
 
67
+ export function bufferedUsed(a: Account, w: Window, bars: Bars, now: number): number {
68
+ const used = liveUsed(w, now);
69
+ const bar = barFor(a, w, bars, now);
70
+ if (bar <= 0) return used > 0 ? 100 : 0;
71
+ return Math.min(100, (used / bar) * 100);
72
+ }
73
+
67
74
  function windowEnd(w: Window): number {
68
75
  return w.resetsAt ?? w.sampledAt + (w.windowSeconds ?? 0) * 1000;
69
76
  }
package/src/lib/usage.ts CHANGED
@@ -30,8 +30,8 @@ const StreamLineSchema = z.looseObject({
30
30
  .optional(),
31
31
  });
32
32
 
33
- export function parseStreamLine(line: string): { type: string; windows: UsageWindows | null } | null {
34
- const parsed = StreamLineSchema.safeParse(JsonTextSchema.safeParse(line).data);
33
+ export function parseStreamLine(value: unknown): { type: string; windows: UsageWindows | null } | null {
34
+ const parsed = StreamLineSchema.safeParse(value);
35
35
  if (!parsed.success) return null;
36
36
  const unified = parsed.data.type === "rate_limit_event" ? parsed.data.rate_limit_info?.unifiedWindows : undefined;
37
37
  const win = (w: z.infer<typeof StreamWindowSchema>): UsageWindow => ({ usedPercentage: Math.round(w.utilization * 100), resetsAt: w.resetsAt });