tokenmaxxing 1.34.0 → 1.38.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 +3 -5
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli/add.ts +23 -40
- package/src/cli/auth.ts +38 -52
- package/src/cli/check.ts +2 -1
- package/src/cli/doctor.ts +13 -13
- package/src/cli/init.ts +29 -126
- package/src/cli/rename.ts +11 -45
- package/src/cli/render.ts +0 -7
- package/src/cli/rm.ts +24 -39
- package/src/cli/setuptoken.ts +16 -16
- package/src/cli/status.ts +95 -238
- package/src/cli/switch.ts +54 -46
- package/src/entries/codexstophook.ts +23 -23
- package/src/entries/codexsupervisor.ts +3 -3
- package/src/entries/sessionstart.ts +5 -3
- package/src/entries/statusline.ts +17 -19
- package/src/entries/stopfailurehook.ts +9 -7
- package/src/entries/stophook.ts +5 -4
- package/src/entries/subagentstatusline.ts +2 -7
- package/src/lib/atomic.ts +2 -8
- package/src/lib/claude.ts +320 -0
- package/src/lib/claudelock.ts +2 -3
- package/src/lib/codex.ts +345 -0
- package/src/lib/codexauth.ts +9 -7
- package/src/lib/codexoauth.ts +5 -11
- package/src/lib/codexpresence.ts +3 -19
- package/src/lib/codexusage.ts +17 -41
- package/src/lib/credstore.ts +3 -7
- package/src/lib/decide.ts +86 -174
- package/src/lib/http.ts +3 -6
- package/src/lib/install.ts +4 -7
- package/src/lib/oauth.ts +14 -12
- package/src/lib/paths.ts +15 -7
- package/src/lib/picker.ts +56 -37
- package/src/lib/proc.ts +4 -4
- package/src/lib/provider.ts +34 -0
- package/src/lib/sample.ts +33 -33
- package/src/lib/state.ts +51 -18
- package/src/lib/swap.ts +41 -38
- package/src/lib/types.ts +31 -51
- package/src/lib/usage.ts +28 -20
- package/src/main.ts +20 -20
- package/src/cli/codexadd.ts +0 -89
- package/src/cli/codexinit.ts +0 -120
- package/src/cli/codexrm.ts +0 -36
- package/src/cli/codexswitch.ts +0 -113
- package/src/cli/onboard.ts +0 -92
- package/src/lib/codexdecide.ts +0 -157
- package/src/lib/codexpick.ts +0 -80
- package/src/lib/codexsample.ts +0 -48
- package/src/lib/codexstate.ts +0 -22
- package/src/lib/codexswap.ts +0 -70
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { codexPaths } from "../lib/paths.ts";
|
|
4
|
+
import { codexPaths, codexPool } from "../lib/paths.ts";
|
|
5
5
|
import { withLock } from "../lib/lock.ts";
|
|
6
6
|
import { writeFileAtomic } from "../lib/atomic.ts";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { codex, codexPickCtx, reconcileSiblings } from "../lib/codex.ts";
|
|
8
|
+
import { liveCodexAccountId } from "../lib/codexauth.ts";
|
|
9
|
+
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
10
|
+
import { isExhausted } from "../lib/picker.ts";
|
|
9
11
|
import { livingCodexPresences } from "../lib/codexpresence.ts";
|
|
10
|
-
import {
|
|
11
|
-
import { loadCodexAccounts } from "../lib/codexstate.ts";
|
|
12
|
-
import { loadConfig } from "../lib/state.ts";
|
|
13
|
-
import { thresholdBars } from "../lib/picker.ts";
|
|
12
|
+
import { loadAccounts } from "../lib/state.ts";
|
|
14
13
|
import { CODEX_SUPERVISOR_ID_ENV } from "./codexsupervisor.ts";
|
|
15
|
-
import { CodexReconcileMarkerSchema, CodexRespawnMarkerSchema, CodexStopStdinSchema,
|
|
14
|
+
import { CodexReconcileMarkerSchema, CodexRespawnMarkerSchema, CodexStopStdinSchema, JsonTextSchema } from "../lib/types.ts";
|
|
16
15
|
import { log } from "../lib/log.ts";
|
|
17
16
|
|
|
18
17
|
const SupervisorIdSchema = z.string().min(1).optional().catch(undefined);
|
|
@@ -20,7 +19,7 @@ const SupervisorIdSchema = z.string().min(1).optional().catch(undefined);
|
|
|
20
19
|
async function promoteReconcile(input: { supervisorId: string; sessionId: string | null }): Promise<boolean> {
|
|
21
20
|
const markerPath = join(codexPaths.reconcileDir, input.supervisorId);
|
|
22
21
|
if (!existsSync(markerPath)) return false;
|
|
23
|
-
return withLock(
|
|
22
|
+
return withLock(codexPool.lockFile, async () => promoteReconcileLocked(input));
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
function promoteReconcileLocked(input: { supervisorId: string; sessionId: string | null }): boolean {
|
|
@@ -51,12 +50,8 @@ function promoteReconcileLocked(input: { supervisorId: string; sessionId: string
|
|
|
51
50
|
return false;
|
|
52
51
|
}
|
|
53
52
|
const now = Date.now();
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const unusable = (account: CodexAccount): boolean =>
|
|
57
|
-
account.needsReauth === true || isCodexExhausted({ account, thresholds: bars, now });
|
|
58
|
-
const liveAccount = index.accounts.find((a) => a.accountId === liveId);
|
|
59
|
-
if (!liveAccount || unusable(liveAccount)) {
|
|
53
|
+
const liveAccount = loadAccounts(codexPool).accounts.find((a) => a.id === liveId);
|
|
54
|
+
if (!liveAccount || liveAccount.needsReauth === true || isExhausted(liveAccount, codexPickCtx(now, liveId))) {
|
|
60
55
|
rmSync(markerPath, { force: true });
|
|
61
56
|
log("codexstop.reconcile_blocked_target", {});
|
|
62
57
|
return false;
|
|
@@ -75,6 +70,14 @@ function promoteReconcileLocked(input: { supervisorId: string; sessionId: string
|
|
|
75
70
|
return true;
|
|
76
71
|
}
|
|
77
72
|
|
|
73
|
+
async function sweepSiblings(now: number): Promise<void> {
|
|
74
|
+
try {
|
|
75
|
+
await withLock(codexPool.lockFile, () => reconcileSiblings(now));
|
|
76
|
+
} catch (e) {
|
|
77
|
+
log("codexstop.resweep_failed", { err: e instanceof Error ? e.message : String(e) });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
78
81
|
async function readStdin(): Promise<string> {
|
|
79
82
|
const chunks: Uint8Array[] = [];
|
|
80
83
|
for await (const chunk of Bun.stdin.stream()) chunks.push(chunk);
|
|
@@ -82,13 +85,7 @@ async function readStdin(): Promise<string> {
|
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
export async function handleCodexStop(input: { rawStdin: string }): Promise<void> {
|
|
85
|
-
const parsed = CodexStopStdinSchema.safeParse((
|
|
86
|
-
try {
|
|
87
|
-
return JSON.parse(input.rawStdin);
|
|
88
|
-
} catch {
|
|
89
|
-
return {};
|
|
90
|
-
}
|
|
91
|
-
})());
|
|
88
|
+
const parsed = CodexStopStdinSchema.safeParse(JsonTextSchema.safeParse(input.rawStdin).data);
|
|
92
89
|
const sessionId = parsed.success ? (parsed.data.session_id ?? null) : null;
|
|
93
90
|
|
|
94
91
|
try {
|
|
@@ -98,8 +95,11 @@ export async function handleCodexStop(input: { rawStdin: string }): Promise<void
|
|
|
98
95
|
return;
|
|
99
96
|
}
|
|
100
97
|
if (await promoteReconcile({ supervisorId, sessionId })) return;
|
|
101
|
-
const
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
await withLock(codexPool.lockFile, () => reconcileSiblings(now));
|
|
100
|
+
const decision = await evaluateAndMaybeSwap(codex, now);
|
|
102
101
|
if (decision.swapped && decision.account) {
|
|
102
|
+
await sweepSiblings(now);
|
|
103
103
|
mkdirSync(codexPaths.respawnDir, { recursive: true });
|
|
104
104
|
const payload = CodexRespawnMarkerSchema.parse({
|
|
105
105
|
account: decision.account.label,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { codexPaths, paths } from "../lib/paths.ts";
|
|
4
|
+
import { codexPaths, codexPool, paths } from "../lib/paths.ts";
|
|
5
5
|
import { withLock } from "../lib/lock.ts";
|
|
6
6
|
import { LOOP_DIAGNOSIS, MAX_WRAP_DEPTH, UNMANAGED_ENV, WRAP_DEPTH_ENV, WRAP_RATE_MAX, WRAP_RATE_WINDOW_MS, wrapDepth, wrapperEntryRateTripped } from "../lib/claudebin.ts";
|
|
7
7
|
import { resolveRealCodex } from "../lib/codexbin.ts";
|
|
8
8
|
import { clearCodexPresence, writeCodexPresence } from "../lib/codexpresence.ts";
|
|
9
|
-
import { liveCodexAccountId } from "../lib/
|
|
9
|
+
import { liveCodexAccountId } from "../lib/codexauth.ts";
|
|
10
10
|
import { saveTermios, restoreTermios } from "../lib/tty.ts";
|
|
11
11
|
import { CodexRespawnMarkerSchema } from "../lib/types.ts";
|
|
12
12
|
import { log } from "../lib/log.ts";
|
|
@@ -95,7 +95,7 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
|
|
|
95
95
|
if (existsSync(marker)) rmSync(marker, { force: true });
|
|
96
96
|
log("codexsupervisor.launch", { supervisorId: supervisorId.slice(0, 8), respawns, args: launchArgs.join(" ") });
|
|
97
97
|
|
|
98
|
-
const child = await withLock(
|
|
98
|
+
const child = await withLock(codexPool.lockFile, async () => {
|
|
99
99
|
const spawnAccountId = liveCodexAccountId();
|
|
100
100
|
const spawned = Bun.spawn([real, ...launchArgs], {
|
|
101
101
|
stdin: "inherit",
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { claude } from "../lib/claude.ts";
|
|
2
3
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
3
4
|
import { log } from "../lib/log.ts";
|
|
5
|
+
import { JsonTextSchema } from "../lib/types.ts";
|
|
4
6
|
|
|
5
7
|
const SessionStartStdin = z.looseObject({
|
|
6
8
|
source: z.string().optional(),
|
|
@@ -17,13 +19,13 @@ export async function runSessionStart(): Promise<number> {
|
|
|
17
19
|
if (process.env.TOKENMAXXING_PROBE) return 0;
|
|
18
20
|
|
|
19
21
|
const raw = await readStdin();
|
|
20
|
-
const parsed = SessionStartStdin.safeParse(
|
|
22
|
+
const parsed = SessionStartStdin.safeParse(JsonTextSchema.safeParse(raw).data);
|
|
21
23
|
const source = parsed.success ? parsed.data.source : undefined;
|
|
22
24
|
|
|
23
25
|
try {
|
|
24
|
-
const decision = await evaluateAndMaybeSwap();
|
|
26
|
+
const decision = await evaluateAndMaybeSwap(claude);
|
|
25
27
|
if (decision.swapped && decision.account) {
|
|
26
|
-
log("sessionstart.swapped", { source, account: decision.account.
|
|
28
|
+
log("sessionstart.swapped", { source, account: decision.account.id.slice(0, 8) });
|
|
27
29
|
}
|
|
28
30
|
} catch (e) {
|
|
29
31
|
log("sessionstart.error", { err: e instanceof Error ? e.message : String(e) });
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { sortBy } from "es-toolkit";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { readOAuthAccount } from "../lib/claudejson.ts";
|
|
4
|
+
import { claudePool } from "../lib/paths.ts";
|
|
4
5
|
import { loadAccounts, loadConfig, loadLastSwapAt, writeUsage } from "../lib/state.ts";
|
|
5
6
|
import { familyTokens, matchedFamily, parseStatusLineStdin, parseStatusLineModel } from "../lib/usage.ts";
|
|
6
|
-
import { earliestReset, weeklyExpiry } from "../lib/picker.ts";
|
|
7
|
+
import { earliestReset, limitWindows, weeklyExpiry, weeklyWindow } from "../lib/picker.ts";
|
|
7
8
|
import { worktreeName } from "../lib/worktree.ts";
|
|
8
9
|
import { makeColors, makeUsagePaint } from "../cli/render.ts";
|
|
9
10
|
import { fmtResetShort } from "../lib/usage.ts";
|
|
10
11
|
import {
|
|
11
12
|
AccountsIndexSchema,
|
|
13
|
+
JsonTextSchema,
|
|
12
14
|
StatusLineStdinSchema,
|
|
13
|
-
|
|
15
|
+
WindowSchema,
|
|
14
16
|
type Account,
|
|
15
17
|
type UsageState,
|
|
16
18
|
type UsageWindow,
|
|
@@ -20,7 +22,7 @@ const ADOPTION_GRACE_MS = 45_000;
|
|
|
20
22
|
|
|
21
23
|
const RenderCtxSchema = z.object({
|
|
22
24
|
accounts: AccountsIndexSchema,
|
|
23
|
-
perModel: z.record(z.string(),
|
|
25
|
+
perModel: z.record(z.string(), WindowSchema),
|
|
24
26
|
switchModels: z.array(z.string()),
|
|
25
27
|
worktree: z.string().nullable(),
|
|
26
28
|
liveAccount: z.string().nullable(),
|
|
@@ -78,11 +80,11 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
|
|
|
78
80
|
windows.push(seg("", wins.sevenDay, wins.sevenDay.resetsAt));
|
|
79
81
|
}
|
|
80
82
|
const seatUuid =
|
|
81
|
-
(ctx.liveAccount != null && ctx.accounts.accounts.some((a) => a.
|
|
82
|
-
ctx.accounts.
|
|
83
|
+
(ctx.liveAccount != null && ctx.accounts.accounts.some((a) => a.id === ctx.liveAccount) ? ctx.liveAccount : null) ??
|
|
84
|
+
ctx.accounts.activeId;
|
|
83
85
|
const walled = (a: Account) => a.enforcedUntil != null && a.enforcedUntil > ctx.now;
|
|
84
86
|
const wallSeg = (wall: number) => seg("", { usedPercentage: 100, resetsAt: wall }, wall);
|
|
85
|
-
const seat = ctx.accounts.accounts.find((a) => a.
|
|
87
|
+
const seat = ctx.accounts.accounts.find((a) => a.id === seatUuid);
|
|
86
88
|
if (seat?.enforcedUntil != null && walled(seat)) windows.push(wallSeg(seat.enforcedUntil));
|
|
87
89
|
const seatMarker = seat && walled(seat) ? paint(100)("◆") : col.green("◆");
|
|
88
90
|
const active =
|
|
@@ -93,20 +95,20 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
|
|
|
93
95
|
: "";
|
|
94
96
|
|
|
95
97
|
const parked = sortBy(
|
|
96
|
-
ctx.accounts.accounts.filter((a) => a.
|
|
98
|
+
ctx.accounts.accounts.filter((a) => a.id !== seatUuid),
|
|
97
99
|
[(a) => (a.needsReauth ? 1 : 0), (a) => earliestReset(a, ctx.now)],
|
|
98
100
|
);
|
|
99
101
|
const poolSeg = (a: Account): string => {
|
|
100
102
|
const marker = a.needsReauth ? col.red("✗") : walled(a) ? paint(100)("◇") : col.cyan("◇");
|
|
101
103
|
if (a.enforcedUntil != null && walled(a)) return `${marker} ${wallSeg(a.enforcedUntil)}`;
|
|
102
|
-
const week = a
|
|
104
|
+
const week = weeklyWindow(a);
|
|
103
105
|
if (week == null) return `${marker} ?`;
|
|
104
106
|
const weekUsed = used(week);
|
|
105
107
|
if (Math.round(weekUsed) <= 0) return `${marker} ${paint(0)("full")}`;
|
|
106
108
|
|
|
107
109
|
const parts: string[] = [];
|
|
108
|
-
for (const
|
|
109
|
-
if (used(w) > weekUsed) parts.push(seg(initial(name), w, null));
|
|
110
|
+
for (const w of limitWindows(a)) {
|
|
111
|
+
if (used(w) > weekUsed) parts.push(seg(initial(w.name ?? ""), w, null));
|
|
110
112
|
}
|
|
111
113
|
const expiry = weeklyExpiry(a, ctx.now);
|
|
112
114
|
parts.push(seg("", week, Number.isFinite(expiry) ? expiry : null));
|
|
@@ -117,19 +119,14 @@ export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
|
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
export async function runStatusline(): Promise<number> {
|
|
120
|
-
const
|
|
121
|
-
let obj: unknown = null;
|
|
122
|
-
try {
|
|
123
|
-
obj = JSON.parse(raw);
|
|
124
|
-
} catch {
|
|
125
|
-
}
|
|
122
|
+
const obj = JsonTextSchema.safeParse(await readStdin()).data ?? null;
|
|
126
123
|
const now = Date.now();
|
|
127
124
|
|
|
128
125
|
let account: string | null = null;
|
|
129
126
|
try {
|
|
130
127
|
account = readOAuthAccount()?.accountUuid ?? null;
|
|
131
128
|
const windows = obj == null ? null : parseStatusLineStdin(obj);
|
|
132
|
-
const lastSwapAt = loadLastSwapAt();
|
|
129
|
+
const lastSwapAt = loadLastSwapAt(claudePool);
|
|
133
130
|
if (windows && (lastSwapAt == null || now - lastSwapAt >= ADOPTION_GRACE_MS)) {
|
|
134
131
|
const state: UsageState = { fiveHour: windows.fiveHour, sevenDay: windows.sevenDay, account, ts: now, model: parseStatusLineModel(obj) };
|
|
135
132
|
writeUsage(state);
|
|
@@ -140,13 +137,14 @@ export async function runStatusline(): Promise<number> {
|
|
|
140
137
|
let line: string;
|
|
141
138
|
try {
|
|
142
139
|
const cfg = loadConfig();
|
|
143
|
-
const accounts = loadAccounts();
|
|
140
|
+
const accounts = loadAccounts(claudePool);
|
|
144
141
|
const stdin = StatusLineStdinSchema.safeParse(obj);
|
|
145
142
|
const dir = stdin.success ? (stdin.data.workspace?.current_dir ?? stdin.data.workspace?.project_dir ?? null) : null;
|
|
146
143
|
const colorterm = z.string().optional().parse(process.env.COLORTERM);
|
|
144
|
+
const live = accounts.accounts.find((a) => a.id === account);
|
|
147
145
|
const ctx: RenderCtx = {
|
|
148
146
|
accounts,
|
|
149
|
-
perModel:
|
|
147
|
+
perModel: Object.fromEntries((live ? limitWindows(live) : []).map((w) => [w.name ?? "", w])),
|
|
150
148
|
switchModels: cfg.policy.switchModels,
|
|
151
149
|
worktree: dir == null ? null : worktreeName(dir),
|
|
152
150
|
liveAccount: account,
|
|
@@ -2,12 +2,14 @@ import { join } from "node:path";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { paths } from "../lib/paths.ts";
|
|
4
4
|
import { writeFileAtomic } from "../lib/atomic.ts";
|
|
5
|
+
import { claude } from "../lib/claude.ts";
|
|
5
6
|
import { readOAuthAccount } from "../lib/claudejson.ts";
|
|
6
7
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
7
8
|
import { withLock } from "../lib/lock.ts";
|
|
9
|
+
import { claudePool } from "../lib/paths.ts";
|
|
8
10
|
import { POST_SWAP_COOLDOWN_MS, loadConfig, loadLastSwapAt } from "../lib/state.ts";
|
|
9
11
|
import { classifyEnforcedLimit, findEnforcedRow, parseErrorBody, readTranscriptTail } from "../lib/usage.ts";
|
|
10
|
-
import { RespawnMarkerSchema, type EnforcedLimit } from "../lib/types.ts";
|
|
12
|
+
import { JsonTextSchema, RespawnMarkerSchema, type EnforcedLimit } from "../lib/types.ts";
|
|
11
13
|
import { log } from "../lib/log.ts";
|
|
12
14
|
|
|
13
15
|
const StopFailureStdin = z.looseObject({
|
|
@@ -29,11 +31,11 @@ async function readStdin(): Promise<string> {
|
|
|
29
31
|
export async function runStopFailureHook(): Promise<number> {
|
|
30
32
|
if (process.env.TOKENMAXXING_PROBE) return 0;
|
|
31
33
|
|
|
32
|
-
const entry = await withLock(
|
|
34
|
+
const entry = await withLock(claudePool.lockFile, () => ({ swapClock: loadLastSwapAt(claudePool), account: readOAuthAccount()?.accountUuid ?? null }));
|
|
33
35
|
const account = entry.account;
|
|
34
36
|
const now = Date.now();
|
|
35
37
|
const raw = await readStdin();
|
|
36
|
-
const parsed = StopFailureStdin.safeParse(
|
|
38
|
+
const parsed = StopFailureStdin.safeParse(JsonTextSchema.safeParse(raw).data);
|
|
37
39
|
const stdin = parsed.success ? parsed.data : {};
|
|
38
40
|
if (stdin.error !== undefined && stdin.error !== "rate_limit") return 0;
|
|
39
41
|
|
|
@@ -44,7 +46,7 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
44
46
|
const canPause = process.env.TOKENMAXXING_SUPERVISED === "1" && pinnedSid != null && mainLoop;
|
|
45
47
|
|
|
46
48
|
try {
|
|
47
|
-
const lastSwapAt = loadLastSwapAt();
|
|
49
|
+
const lastSwapAt = loadLastSwapAt(claudePool);
|
|
48
50
|
if (lastSwapAt != null && lastSwapAt === entry.swapClock && now - lastSwapAt < POST_SWAP_COOLDOWN_MS) {
|
|
49
51
|
log("stopfailure.cooldown", { sinceSwapMs: now - lastSwapAt });
|
|
50
52
|
return 0;
|
|
@@ -68,7 +70,7 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
68
70
|
});
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
const decision = await evaluateAndMaybeSwap(now, canPause && enforced != null, enforced);
|
|
73
|
+
const decision = await evaluateAndMaybeSwap(claude, now, canPause && enforced != null, enforced);
|
|
72
74
|
if (enforced && canPause && pinnedSid && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
|
|
73
75
|
const marker = join(paths.respawnDir, pinnedSid);
|
|
74
76
|
const payload = RespawnMarkerSchema.parse({
|
|
@@ -79,9 +81,9 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
79
81
|
...(launchedAt != null ? { launchedAt } : {}),
|
|
80
82
|
});
|
|
81
83
|
writeFileAtomic(marker, JSON.stringify(payload));
|
|
82
|
-
log("stopfailure.marker", { session: (stdinSid ?? pinnedSid).slice(0, 8), account: decision.account.
|
|
84
|
+
log("stopfailure.marker", { session: (stdinSid ?? pinnedSid).slice(0, 8), account: decision.account.id.slice(0, 8), waitUntil: payload.waitUntil });
|
|
83
85
|
} else {
|
|
84
|
-
log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.
|
|
86
|
+
log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
|
|
85
87
|
}
|
|
86
88
|
} catch (e) {
|
|
87
89
|
log("stopfailure.error", { err: e instanceof Error ? e.message : String(e) });
|
package/src/entries/stophook.ts
CHANGED
|
@@ -2,8 +2,9 @@ import { join } from "node:path";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { paths } from "../lib/paths.ts";
|
|
4
4
|
import { writeFileAtomic } from "../lib/atomic.ts";
|
|
5
|
+
import { claude } from "../lib/claude.ts";
|
|
5
6
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
6
|
-
import { RespawnMarkerSchema } from "../lib/types.ts";
|
|
7
|
+
import { JsonTextSchema, RespawnMarkerSchema } from "../lib/types.ts";
|
|
7
8
|
import { log } from "../lib/log.ts";
|
|
8
9
|
|
|
9
10
|
const StopStdin = z.looseObject({ session_id: z.uuid().optional().catch(undefined) });
|
|
@@ -18,15 +19,15 @@ export async function runStopHook(): Promise<number> {
|
|
|
18
19
|
if (process.env.TOKENMAXXING_PROBE) return 0;
|
|
19
20
|
|
|
20
21
|
const raw = await readStdin();
|
|
21
|
-
const parsed = StopStdin.safeParse(
|
|
22
|
+
const parsed = StopStdin.safeParse(JsonTextSchema.safeParse(raw).data);
|
|
22
23
|
const stdinSid = parsed.success ? parsed.data.session_id : undefined;
|
|
23
24
|
const pinnedSid = process.env.TOKENMAXXING_SESSION_ID;
|
|
24
25
|
|
|
25
26
|
try {
|
|
26
27
|
const canPause = process.env.TOKENMAXXING_SUPERVISED === "1" && pinnedSid != null;
|
|
27
|
-
const decision = await evaluateAndMaybeSwap(Date.now(), canPause);
|
|
28
|
+
const decision = await evaluateAndMaybeSwap(claude, Date.now(), canPause);
|
|
28
29
|
if (decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
|
|
29
|
-
log(decision.swapped ? "stop.swapped" : "stop.wait", { account: decision.account.
|
|
30
|
+
log(decision.swapped ? "stop.swapped" : "stop.wait", { account: decision.account.id.slice(0, 8), waitUntil: decision.waitUntil });
|
|
30
31
|
if (decision.waitUntil !== undefined && canPause && pinnedSid) {
|
|
31
32
|
const marker = join(paths.respawnDir, pinnedSid);
|
|
32
33
|
const launchedAt = z.coerce.number().finite().optional().catch(undefined).parse(process.env.TOKENMAXXING_LAUNCHED_AT);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { makeColors, makeUsagePaint } from "../cli/render.ts";
|
|
3
3
|
import { readStdin } from "./statusline.ts";
|
|
4
|
-
import { SubagentStatusLineStdinSchema } from "../lib/types.ts";
|
|
4
|
+
import { JsonTextSchema, SubagentStatusLineStdinSchema } from "../lib/types.ts";
|
|
5
5
|
|
|
6
6
|
const RowCtxSchema = z.object({ color: z.boolean(), truecolor: z.boolean() });
|
|
7
7
|
export type RowCtx = z.infer<typeof RowCtxSchema>;
|
|
@@ -43,12 +43,7 @@ export function renderSubagentRows(stdinObj: unknown, ctx: RowCtx): string[] {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export async function runSubagentStatusline(): Promise<number> {
|
|
46
|
-
const
|
|
47
|
-
let obj: unknown = null;
|
|
48
|
-
try {
|
|
49
|
-
obj = JSON.parse(raw);
|
|
50
|
-
} catch {
|
|
51
|
-
}
|
|
46
|
+
const obj = JsonTextSchema.safeParse(await readStdin()).data ?? null;
|
|
52
47
|
const colorterm = z.string().optional().parse(process.env.COLORTERM);
|
|
53
48
|
const rows = renderSubagentRows(obj, {
|
|
54
49
|
color: !process.env.NO_COLOR,
|
package/src/lib/atomic.ts
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
|
-
import { closeSync, mkdirSync, openSync, renameSync, rmSync,
|
|
1
|
+
import { closeSync, mkdirSync, openSync, renameSync, rmSync, writeFileSync, fsyncSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
|
|
4
4
|
export function writeFileAtomic(file: string, data: string | Uint8Array, mode = 0o600): void {
|
|
5
5
|
const dir = dirname(file);
|
|
6
6
|
mkdirSync(dir, { recursive: true });
|
|
7
7
|
const tmp = `${file}.tmp.${process.pid}.${Math.floor(Math.random() * 1e9).toString(36)}`;
|
|
8
|
-
const bytes = data instanceof Uint8Array ? data : new TextEncoder().encode(data);
|
|
9
8
|
const fd = openSync(tmp, "wx", mode);
|
|
10
9
|
try {
|
|
11
10
|
try {
|
|
12
|
-
|
|
13
|
-
while (offset < bytes.length) {
|
|
14
|
-
const written = writeSync(fd, bytes, offset);
|
|
15
|
-
if (written <= 0) throw new Error(`short write on ${tmp}: ${offset}/${bytes.length} bytes (disk full?)`);
|
|
16
|
-
offset += written;
|
|
17
|
-
}
|
|
11
|
+
writeFileSync(fd, data);
|
|
18
12
|
fsyncSync(fd);
|
|
19
13
|
} finally {
|
|
20
14
|
closeSync(fd);
|