tokenmaxxing 1.131.0 → 1.133.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenmaxxing",
3
- "version": "1.131.0",
3
+ "version": "1.133.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/key.ts ADDED
@@ -0,0 +1,147 @@
1
+ import { z } from "zod";
2
+ import { keySessions, loadApiKeys, removeApiKeySecret, saveApiKeys, writeApiKeySecret } from "../lib/apikeys.ts";
3
+ import { withLock } from "../lib/lock.ts";
4
+ import { claudePool } from "../lib/paths.ts";
5
+ import { readStdin } from "../lib/proc.ts";
6
+ import type { ApiKey } from "../lib/types.ts";
7
+ import { c, emitError, plain } from "./render.ts";
8
+
9
+ const USAGE = "usage: tokenmaxxing key add <label> [credit-usd] [workspace-id] (the key on stdin) | key credit <label> <usd> | key rm <label>";
10
+
11
+ const UsdSchema = z.coerce.number().finite().min(0);
12
+
13
+ const WorkspaceIdSchema = z
14
+ .string()
15
+ .min(1)
16
+ .refine((id) => id.trim() === id && !id.includes("\n") && !id.includes("\r"), "a workspace id is one line with no surrounding whitespace");
17
+
18
+ export function usd(amount: number): string {
19
+ return `$${amount.toFixed(2)}`;
20
+ }
21
+
22
+ function findKey(keys: ApiKey[], label: string): ApiKey | undefined {
23
+ return keys.find((k) => k.label.toLowerCase() === label.toLowerCase());
24
+ }
25
+
26
+ function parseUsd(raw: string): number | null {
27
+ const parsed = UsdSchema.safeParse(raw);
28
+ if (parsed.success) return parsed.data;
29
+ emitError({ message: `"${raw}" is not a dollar amount (a number at or above 0)` });
30
+ return null;
31
+ }
32
+
33
+ async function add(args: string[]): Promise<number> {
34
+ const [label, creditRaw, workspaceRaw] = args;
35
+ if (!label) {
36
+ emitError({ message: USAGE, paint: plain });
37
+ return 2;
38
+ }
39
+ const credit = creditRaw == null ? undefined : parseUsd(creditRaw);
40
+ if (credit === null) return 2;
41
+ const workspace = workspaceRaw == null ? undefined : WorkspaceIdSchema.safeParse(workspaceRaw);
42
+ if (workspace != null && !workspace.success) {
43
+ emitError({ message: `"${workspaceRaw}" is not a workspace id: ${workspace.error.issues[0]?.message ?? "invalid"}` });
44
+ return 2;
45
+ }
46
+ const workspaceId = workspace?.data;
47
+ if (process.stdin.isTTY) {
48
+ emitError({ message: "refusing to read the API key from a terminal, which would echo it - pipe it in: read -rs KEY && printf '%s' \"$KEY\" | tokenmaxxing key add <label> ...; unset KEY" });
49
+ return 2;
50
+ }
51
+ const secret = (await readStdin()).trim();
52
+ if (secret === "") {
53
+ emitError({ message: "no API key on stdin - pipe the key in, for example from your password manager" });
54
+ return 2;
55
+ }
56
+ if (secret.includes("\n") || secret.includes("\r")) {
57
+ emitError({ message: "stdin holds more than one line - pass one API key per `key add`" });
58
+ return 2;
59
+ }
60
+ if (secret.startsWith("sk-ant-admin")) {
61
+ emitError({ message: "that is an Admin API key, which cannot run inference - create a regular API key in the Console" });
62
+ return 2;
63
+ }
64
+ return withLock(claudePool.lockFile, () => {
65
+ const idx = loadApiKeys();
66
+ const taken = findKey(idx.keys, label);
67
+ if (taken) {
68
+ emitError({ message: `label "${label}" is already used by another API key - labels must be unique` });
69
+ return 1;
70
+ }
71
+ let id = crypto.randomUUID().slice(0, 8);
72
+ while (idx.keys.some((k) => k.id === id)) id = crypto.randomUUID().slice(0, 8);
73
+ const now = Date.now();
74
+ writeApiKeySecret(id, secret);
75
+ idx.keys.push({
76
+ id,
77
+ label,
78
+ ...(workspaceId != null ? { workspaceId } : {}),
79
+ ...(credit != null ? { creditUsd: credit } : {}),
80
+ spentUsd: 0,
81
+ creditSetAt: now,
82
+ addedAt: now,
83
+ });
84
+ saveApiKeys(idx);
85
+ console.log(`added API key ${c.bold(label)} (${credit != null ? `credit ${usd(credit)}` : "credit unknown, ranks after keys with a balance"}${workspaceId != null ? `, workspace ${workspaceId}` : ""})`);
86
+ return 0;
87
+ });
88
+ }
89
+
90
+ async function credit(args: string[]): Promise<number> {
91
+ const [label, amountRaw] = args;
92
+ if (!label || amountRaw == null || args.length > 2) {
93
+ emitError({ message: USAGE, paint: plain });
94
+ return 2;
95
+ }
96
+ const amount = parseUsd(amountRaw);
97
+ if (amount === null) return 2;
98
+ return withLock(claudePool.lockFile, () => {
99
+ const idx = loadApiKeys();
100
+ const key = findKey(idx.keys, label);
101
+ if (!key) {
102
+ emitError({ message: `no API key is labeled "${label}"` });
103
+ return 1;
104
+ }
105
+ key.creditUsd = amount;
106
+ key.spentUsd = 0;
107
+ key.creditSetAt = Date.now();
108
+ key.refusedAt = undefined;
109
+ saveApiKeys(idx);
110
+ console.log(`set ${c.bold(key.label)} credit to ${usd(amount)}`);
111
+ return 0;
112
+ });
113
+ }
114
+
115
+ async function rm(args: string[]): Promise<number> {
116
+ const [label] = args;
117
+ if (!label || args.length > 1) {
118
+ emitError({ message: USAGE, paint: plain });
119
+ return 2;
120
+ }
121
+ return withLock(claudePool.lockFile, () => {
122
+ const idx = loadApiKeys();
123
+ const key = findKey(idx.keys, label);
124
+ if (!key) {
125
+ emitError({ message: `no API key is labeled "${label}"` });
126
+ return 1;
127
+ }
128
+ if (keySessions().has(key.id)) {
129
+ emitError({ message: `${key.label} is running in a live session - close that session before removing the key.` });
130
+ return 1;
131
+ }
132
+ removeApiKeySecret(key.id);
133
+ idx.keys = idx.keys.filter((k) => k.id !== key.id);
134
+ saveApiKeys(idx);
135
+ console.log(`removed API key ${c.bold(key.label)} (${idx.keys.length} left)`);
136
+ return 0;
137
+ });
138
+ }
139
+
140
+ export async function cmdKey(args: string[]): Promise<number> {
141
+ const [action, ...rest] = args;
142
+ if (action === "add") return add(rest);
143
+ if (action === "credit") return credit(rest);
144
+ if (action === "rm") return rm(rest);
145
+ emitError({ message: USAGE, paint: plain });
146
+ return 2;
147
+ }
package/src/cli/status.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { chunk, sortBy } from "es-toolkit";
2
+ import { creditLeft, keySessions, loadApiKeys } from "../lib/apikeys.ts";
2
3
  import { claude } from "../lib/claude.ts";
3
4
  import { codex } from "../lib/codex.ts";
4
5
  import { grok } from "../lib/grok.ts";
@@ -7,6 +8,7 @@ import { withLock } from "../lib/lock.ts";
7
8
  import { codexPool, grokPool } from "../lib/paths.ts";
8
9
  import { barFor, bufferedUsed, earliestReset, gatedWindows, landGrant, landWindows, isExhausted, isSessionWindow, limitWindows, liveUsed, nextWeeklyReset, sessionWindow, thresholdBars, weeklyWindow } from "../lib/picker.ts";
9
10
  import type { Provider, SampleReport } from "../lib/provider.ts";
11
+ import { usd } from "./key.ts";
10
12
  import { bar, c, count, emitJson, fmtAgo } from "./render.ts";
11
13
  import type { Account, AccountThresholds, Bars, Config, Thresholds, Window } from "../lib/types.ts";
12
14
 
@@ -38,7 +40,48 @@ type PoolReport = {
38
40
  accounts: StatusAccount[];
39
41
  };
40
42
 
41
- export type StatusReport = { now: number; claude: PoolReport; codex: PoolReport; grok: PoolReport };
43
+ type StatusApiKey = {
44
+ label: string;
45
+ id: string;
46
+ workspaceId: string | null;
47
+ creditUsd: number | null;
48
+ spentUsd: number;
49
+ creditLeftUsd: number | null;
50
+ refused: boolean;
51
+ refusedAt: number | null;
52
+ sessions: number;
53
+ };
54
+
55
+ export type StatusReport = { now: number; claude: PoolReport; codex: PoolReport; grok: PoolReport; apiKeys?: StatusApiKey[] };
56
+
57
+ function apiKeyReport(): StatusApiKey[] {
58
+ const sessions = keySessions();
59
+ return loadApiKeys().keys.map((k) => ({
60
+ label: k.label,
61
+ id: k.id,
62
+ workspaceId: k.workspaceId ?? null,
63
+ creditUsd: k.creditUsd ?? null,
64
+ spentUsd: k.spentUsd,
65
+ creditLeftUsd: creditLeft(k),
66
+ refused: k.refusedAt != null,
67
+ refusedAt: k.refusedAt ?? null,
68
+ sessions: sessions.get(k.id) ?? 0,
69
+ }));
70
+ }
71
+
72
+ function renderApiKeys(keys: StatusApiKey[]): void {
73
+ console.log(c.dim(`api keys (${count({ n: keys.length, noun: "key" })}, used only while every claude account is at its limit)`));
74
+ console.log();
75
+ for (const k of keys) {
76
+ const badges: string[] = [];
77
+ if (k.sessions > 0) badges.push(c.green(count({ n: k.sessions, noun: "session" })));
78
+ if (k.refused) badges.push(c.red("refused"));
79
+ else if (k.creditLeftUsd != null && k.creditLeftUsd <= 0) badges.push(c.yellow("spent"));
80
+ const credit = k.creditLeftUsd == null ? c.dim("credit unknown") : `credit left ${usd(k.creditLeftUsd)}`;
81
+ console.log(`${k.sessions > 0 ? c.green("●") : c.dim("○")} ${c.bold(k.label)} ${credit}${badges.length ? ` ${badges.join(" ")}` : ""}`);
82
+ }
83
+ console.log();
84
+ }
42
85
 
43
86
  function currentWindow(a: Account, w: Window, bars: Bars | null, now: number): WindowReport {
44
87
  const passed = w.resetsAt != null && w.resetsAt <= now;
@@ -259,11 +302,12 @@ export async function cmdStatus(opts: { json?: boolean; cached?: boolean } = {})
259
302
  const cfg = loadConfig();
260
303
  const now = Date.now();
261
304
  const claudeReport = await collect(claude, cfg, now, cached);
305
+ const apiKeys = apiKeyReport();
262
306
  if (!json) {
263
307
  const codexPooled = loadAccounts(codexPool).accounts.length > 0;
264
308
  const grokPooled = loadAccounts(grokPool).accounts.length > 0;
265
309
  if (claudeReport.accounts.length === 0) {
266
- if (!codexPooled && !grokPooled) {
310
+ if (!codexPooled && !grokPooled && apiKeys.length === 0) {
267
311
  console.log(c.dim("no accounts yet, run `tokenmaxxing init` (or `tokenmaxxing init --codex`, `--grok`)"));
268
312
  return 0;
269
313
  }
@@ -273,11 +317,12 @@ export async function cmdStatus(opts: { json?: boolean; cached?: boolean } = {})
273
317
  const header = `thresholds 5h ${claudeReport.thresholds.session}% weekly ${claudeReport.thresholds.weekly}% bars 5h ${claudeReport.bars.session}% weekly ${claudeReport.bars.weekly}% (${count({ n: claudeReport.accounts.length, noun: "claude account" })})`;
274
318
  renderPool(claude, claudeReport, header, Date.now(), cfg.policy.usagePollTtlMs);
275
319
  }
320
+ if (apiKeys.length > 0) renderApiKeys(apiKeys);
276
321
  }
277
322
  const codexReport = await collect(codex, cfg, now, cached);
278
323
  const grokReport = await collect(grok, cfg, now, cached);
279
324
  if (json) {
280
- const report: StatusReport = { now, claude: claudeReport, codex: codexReport, grok: grokReport };
325
+ const report: StatusReport = { now, claude: claudeReport, codex: codexReport, grok: grokReport, ...(apiKeys.length > 0 ? { apiKeys } : {}) };
281
326
  emitJson({ ok: true, ...report });
282
327
  return 0;
283
328
  }
@@ -10,7 +10,7 @@ import { codexStoreUsable, ensureCodexStoreHome } from "../lib/codexauth.ts";
10
10
  import { CODEX_CRED_ENV, CODEX_SUPERVISOR_ID_ENV, borrowCodexSeat, codexPickCtx, pickCodexSeat } from "../lib/codex.ts";
11
11
  import { clearPresence, livingPresences } from "../lib/presence.ts";
12
12
  import { isExhausted } from "../lib/picker.ts";
13
- import { exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough } from "../lib/supervise.ts";
13
+ import { childStdout, exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, relayStdout, runPassthrough } from "../lib/supervise.ts";
14
14
  import { saveTermios } from "../lib/tty.ts";
15
15
  import { loadAccounts, readJsonFile } from "../lib/state.ts";
16
16
  import { CodexRespawnMarkerSchema, type Account } from "../lib/types.ts";
@@ -137,14 +137,14 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
137
137
  if (existsSync(marker)) rmSync(marker, { force: true });
138
138
 
139
139
  const launchedAt = Date.now();
140
- const { child, seat } = await withLock(codexPool.lockFile, async () => {
140
+ const { child, seat, drain } = await withLock(codexPool.lockFile, async () => {
141
141
  clearPresence({ dir: codexPaths.presenceDir, id: supervisorId });
142
142
  const picked = validWanted({ wanted, now: launchedAt, supervisorId }) ?? pickCodexSeat(launchedAt);
143
143
  log("codexsupervisor.launch", { supervisorId: supervisorId.slice(0, 8), respawns, seat: picked?.id.slice(0, 8) ?? null, args: launchArgs.join(" ") });
144
144
  const store = picked ? ensureCodexStoreHome(picked.id) : undefined;
145
145
  const spawned = Bun.spawn([real, ...launchArgs], {
146
146
  stdin: "inherit",
147
- stdout: "inherit",
147
+ stdout: childStdout(),
148
148
  stderr: "inherit",
149
149
  env: {
150
150
  ...(store ? omit(childEnv, CODEX_CRED_ENV) : childEnv),
@@ -152,6 +152,7 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
152
152
  ...(store ? { CODEX_HOME: store } : {}),
153
153
  },
154
154
  });
155
+ const drain = spawned.stdout instanceof ReadableStream ? relayStdout(spawned.stdout) : null;
155
156
  if (picked) {
156
157
  await recordPresenceOrStop({
157
158
  child: spawned,
@@ -163,10 +164,10 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
163
164
  savedTermios,
164
165
  });
165
166
  }
166
- return { child: spawned, seat: picked };
167
+ return { child: spawned, seat: picked, drain };
167
168
  });
168
169
  if (respawns > 0) {
169
- process.stdout.write(`\n\x1b[36m↻ tokenmaxxing: switched codex to ${seat?.label ?? "the ambient codex login"} - resuming...\x1b[0m\n`);
170
+ process.stderr.write(`\n\x1b[36m↻ tokenmaxxing: switched codex to ${seat?.label ?? "the ambient codex login"} - resuming...\x1b[0m\n`);
170
171
  }
171
172
 
172
173
  await raceMarkerOrExit({
@@ -178,6 +179,7 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
178
179
  },
179
180
  savedTermios,
180
181
  });
182
+ await drain?.();
181
183
 
182
184
  const payload = existsSync(marker) ? readCodexMarker(marker) : null;
183
185
  if (payload) {
@@ -1,5 +1,8 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { sortBy } from "es-toolkit";
3
+ import { writeSessionCost } from "../lib/apikeys.ts";
2
4
  import { claudePool, seatFromEnv } from "../lib/paths.ts";
5
+ import { supervisedSession } from "../lib/sessions.ts";
3
6
  import { errorMessage } from "../lib/log.ts";
4
7
  import { loadAccounts, loadConfig, writeUsage } from "../lib/state.ts";
5
8
  import { familyTokens, matchedFamily, parseStatusLineStdin, parseStatusLineModel } from "../lib/usage.ts";
@@ -8,6 +11,7 @@ import { readStdin } from "../lib/proc.ts";
8
11
  import { worktreeName } from "../lib/worktree.ts";
9
12
  import { fmtResetShort, makeColors, makeUsagePaint, statuslineColor } from "../cli/render.ts";
10
13
  import {
14
+ CostLineSchema,
11
15
  JsonTextSchema,
12
16
  StatusLineStdinSchema,
13
17
  type Account,
@@ -127,6 +131,12 @@ export async function runStatusline(): Promise<number> {
127
131
  }
128
132
  } catch {
129
133
  }
134
+ try {
135
+ const session = existsSync(claudePool.apiKeysJson) ? supervisedSession() : null;
136
+ const cost = session == null ? null : (CostLineSchema.safeParse(obj).data ?? null);
137
+ if (session != null && cost != null && cost.sessionId === session.live) writeSessionCost(session.sid, cost);
138
+ } catch {
139
+ }
130
140
 
131
141
  let line: string;
132
142
  try {
@@ -2,9 +2,10 @@ import { existsSync, readdirSync } from "node:fs";
2
2
  import { basename, dirname, join } from "node:path";
3
3
  import { z } from "zod";
4
4
  import { claude } from "../lib/claude.ts";
5
- import { evaluateAndMaybeSwap } from "../lib/decide.ts";
5
+ import { liveApiKeyId, refuseApiKey } from "../lib/apikeys.ts";
6
+ import { evaluateAndMaybeSwap, moveTarget } from "../lib/decide.ts";
6
7
  import { readStdin } from "../lib/proc.ts";
7
- import { adoptLiveSession, refusedAccounts, supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
8
+ import { adoptLiveSession, keyMovesSupported, refusedAccounts, supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
8
9
  import { loadConfig } from "../lib/state.ts";
9
10
  import { classifyEnforcedLimit, ENFORCED_ERRORS, findEnforcedRow, parseErrorBody, readTranscriptTail, type TranscriptRow } from "../lib/usage.ts";
10
11
  import { paths } from "../lib/paths.ts";
@@ -60,6 +61,22 @@ export async function runStopFailureHook(): Promise<number> {
60
61
  return 0;
61
62
  }
62
63
  const canRespawn = session != null && mainLoop;
64
+ const keyId = account == null ? liveApiKeyId() : null;
65
+ if (keyId != null) {
66
+ if (error !== "billing_error") return 0;
67
+ const known = await refuseApiKey(keyId, now);
68
+ log("stopfailure.key_refused", { key: keyId, known, subagent: !mainLoop });
69
+ const decision = await evaluateAndMaybeSwap(claude, now, canRespawn, null, { waiterId: canRespawn ? session?.sid : undefined, refused: refusedAccounts(), apiKeys: true, apiKeyId: keyId });
70
+ const target = moveTarget(decision);
71
+ if (session && canRespawn && target) {
72
+ writeRespawnMarker({ session, ...target, waitUntil: target.waitUntil ?? now, compact: false, origin: "stopfailure" });
73
+ log("stopfailure.marker", { session: session.sid.slice(0, 8), live: session.live.slice(0, 8), account: target.accountId.slice(0, 8), key: target.apiKeyId, waitUntil: target.waitUntil ?? now });
74
+ } else {
75
+ log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
76
+ }
77
+ return 0;
78
+ }
79
+ if (error === "billing_error") return 0;
63
80
  const cfg = loadConfig();
64
81
  const row = stdin.transcript_path
65
82
  ? await awaitEnforcedRow({ transcriptPath: stdin.transcript_path, agentId: stdin.agent_id, error, lastAssistantMessage: stdin.last_assistant_message })
@@ -92,10 +109,11 @@ export async function runStopFailureHook(): Promise<number> {
92
109
  }
93
110
 
94
111
  const refused = enforced?.kind === "credits" ? [...new Set([...refusedAccounts(), enforced.account])] : undefined;
95
- const decision = await evaluateAndMaybeSwap(claude, now, canRespawn, enforced, { waiterId: canRespawn ? session?.sid : undefined, exclude: refused });
96
- if (session && canRespawn && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
97
- writeRespawnMarker({ session, accountId: decision.account.id, waitUntil: decision.waitUntil ?? now, compact: false, origin: "stopfailure", refused });
98
- log("stopfailure.marker", { session: session.sid.slice(0, 8), live: session.live.slice(0, 8), account: decision.account.id.slice(0, 8), waitUntil: decision.waitUntil ?? now });
112
+ const decision = await evaluateAndMaybeSwap(claude, now, canRespawn, enforced, { waiterId: canRespawn ? session?.sid : undefined, exclude: refused, apiKeys: canRespawn && keyMovesSupported() });
113
+ const target = moveTarget(decision);
114
+ if (session && canRespawn && target) {
115
+ writeRespawnMarker({ session, ...target, waitUntil: target.waitUntil ?? now, compact: false, origin: "stopfailure", refused });
116
+ log("stopfailure.marker", { session: session.sid.slice(0, 8), live: session.live.slice(0, 8), account: target.accountId.slice(0, 8), key: target.apiKeyId, waitUntil: target.waitUntil ?? now });
99
117
  } else {
100
118
  log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
101
119
  }
@@ -1,8 +1,8 @@
1
1
  import { z } from "zod";
2
2
  import { claude } from "../lib/claude.ts";
3
- import { evaluateAndMaybeSwap } from "../lib/decide.ts";
3
+ import { evaluateAndMaybeSwap, moveTarget } from "../lib/decide.ts";
4
4
  import { readStdin } from "../lib/proc.ts";
5
- import { adoptLiveSession, refusedAccounts, supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
5
+ import { adoptLiveSession, keyMovesSupported, refusedAccounts, supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
6
6
  import { JsonTextSchema } from "../lib/types.ts";
7
7
  import { errorMessage, log } from "../lib/log.ts";
8
8
 
@@ -24,10 +24,11 @@ export async function runBoundaryHook(event: "stop" | "sessionstart"): Promise<n
24
24
  log(`${event}.nested_session`, { stdin: stdin.session_id?.slice(0, 8), supervised: supervised.live.slice(0, 8), source: stdin.source });
25
25
  return 0;
26
26
  }
27
- const decision = await evaluateAndMaybeSwap(claude, Date.now(), session != null, null, { waiterId: session?.sid, refused: refusedAccounts() });
28
- if (session && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
29
- writeRespawnMarker({ session, accountId: decision.account.id, waitUntil: decision.waitUntil ?? Date.now(), compact: true, origin: event });
30
- log(`${event}.${decision.waitUntil !== undefined ? "wait" : "move"}`, { source: stdin.source, account: decision.account.id.slice(0, 8), waitUntil: decision.waitUntil, session: session.sid.slice(0, 8), live: session.live.slice(0, 8) });
27
+ const decision = await evaluateAndMaybeSwap(claude, Date.now(), session != null, null, { waiterId: session?.sid, refused: refusedAccounts(), apiKeys: session != null && keyMovesSupported() });
28
+ const target = moveTarget(decision);
29
+ if (session && target) {
30
+ writeRespawnMarker({ session, ...target, waitUntil: target.waitUntil ?? Date.now(), compact: true, origin: event });
31
+ log(`${event}.${target.waitUntil !== undefined ? "wait" : "move"}`, { source: stdin.source, account: target.accountId.slice(0, 8), key: target.apiKeyId, waitUntil: target.waitUntil, session: session.sid.slice(0, 8), live: session.live.slice(0, 8) });
31
32
  }
32
33
  } catch (e) {
33
34
  log(`${event}.error`, { err: errorMessage(e) });