tokenmaxxing 1.57.0 → 1.58.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 +6 -6
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/cli/auth.ts +6 -17
- package/src/cli/check.ts +2 -2
- package/src/cli/cloudrun.ts +12 -19
- package/src/cli/config.ts +2 -1
- package/src/cli/doctor.ts +5 -4
- package/src/cli/init.ts +0 -1
- package/src/cli/render.ts +24 -9
- package/src/cli/rm.ts +0 -4
- package/src/cli/status.ts +34 -57
- package/src/entries/codexstophook.ts +10 -23
- package/src/entries/codexsupervisor.ts +30 -72
- package/src/entries/statusline.ts +17 -26
- package/src/entries/stopfailurehook.ts +4 -5
- package/src/entries/stophook.ts +12 -10
- package/src/entries/subagentstatusline.ts +4 -10
- package/src/entries/supervisor.ts +51 -103
- package/src/lib/claude.ts +11 -12
- package/src/lib/claudebin.ts +3 -2
- package/src/lib/claudelock.ts +2 -2
- package/src/lib/cloud.ts +4 -8
- package/src/lib/codex.ts +3 -3
- package/src/lib/codexauth.ts +2 -11
- package/src/lib/codexbin.ts +2 -1
- package/src/lib/codexoauth.ts +3 -8
- package/src/lib/codexusage.ts +5 -5
- package/src/lib/compact.ts +5 -18
- package/src/lib/credstore.ts +1 -6
- package/src/lib/decide.ts +6 -24
- package/src/lib/grok.ts +38 -152
- package/src/lib/http.ts +1 -2
- package/src/lib/install.ts +20 -38
- package/src/lib/keychain.ts +1 -4
- package/src/lib/log.ts +5 -5
- package/src/lib/oauth.ts +6 -5
- package/src/lib/opencodego.ts +31 -140
- package/src/lib/paths.ts +6 -22
- package/src/lib/picker.ts +9 -20
- package/src/lib/presence.ts +4 -5
- package/src/lib/proc.ts +22 -0
- package/src/lib/sample.ts +25 -24
- package/src/lib/sessions.ts +5 -6
- package/src/lib/settings.ts +1 -8
- package/src/lib/state.ts +6 -29
- package/src/lib/statusonly.ts +143 -0
- package/src/lib/supervise.ts +111 -0
- package/src/lib/types.ts +27 -36
- package/src/lib/update.ts +6 -7
- package/src/lib/usage.ts +40 -191
- package/src/main.ts +5 -5
- package/src/entries/sessionstart.ts +0 -33
|
@@ -1,17 +1,18 @@
|
|
|
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, codexPool
|
|
4
|
+
import { codexPaths, codexPool } from "../lib/paths.ts";
|
|
5
5
|
import { withLock } from "../lib/lock.ts";
|
|
6
|
-
import {
|
|
6
|
+
import { UNMANAGED_ENV, WRAP_DEPTH_ENV, wrapDepth } from "../lib/claudebin.ts";
|
|
7
7
|
import { resolveRealCodex } from "../lib/codexbin.ts";
|
|
8
8
|
import { ensureCodexStoreHome } from "../lib/codexauth.ts";
|
|
9
9
|
import { pickCodexSeat } from "../lib/codex.ts";
|
|
10
|
-
import { clearPresence
|
|
11
|
-
import {
|
|
10
|
+
import { clearPresence } from "../lib/presence.ts";
|
|
11
|
+
import { exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough } from "../lib/supervise.ts";
|
|
12
|
+
import { saveTermios } from "../lib/tty.ts";
|
|
12
13
|
import { loadAccounts } from "../lib/state.ts";
|
|
13
14
|
import { CodexRespawnMarkerSchema } from "../lib/types.ts";
|
|
14
|
-
import { log } from "../lib/log.ts";
|
|
15
|
+
import { errorMessage, log } from "../lib/log.ts";
|
|
15
16
|
|
|
16
17
|
export const CODEX_SUPERVISOR_ID_ENV = "TOKENMAXXING_CODEX_SUPERVISOR_ID";
|
|
17
18
|
|
|
@@ -27,7 +28,7 @@ function readCodexMarker(marker: string): z.infer<typeof CodexRespawnMarkerSchem
|
|
|
27
28
|
try {
|
|
28
29
|
return CodexRespawnMarkerSchema.parse(JSON.parse(readFileSync(marker, "utf8")));
|
|
29
30
|
} catch (e) {
|
|
30
|
-
log("codexsupervisor.marker_invalid", { err:
|
|
31
|
+
log("codexsupervisor.marker_invalid", { err: errorMessage(e) });
|
|
31
32
|
throw new Error(
|
|
32
33
|
`${marker} is corrupt (unparsable JSON or off-schema) - the codex session was stopped instead of resumed on a stale target; inspect and remove the marker, then run \`codex resume\``,
|
|
33
34
|
);
|
|
@@ -54,52 +55,17 @@ export function shouldManageCodex(input: { argv: string[] }): boolean {
|
|
|
54
55
|
return firstPositional === null || !NONINTERACTIVE_SUBCMDS.has(firstPositional);
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
async function recordCodexPresence(child: { pid: number; exitCode: number | null; signalCode: string | null; kill: () => void; exited: Promise<unknown> }, supervisorId: string, seat: { id: string }, savedTermios: string | null): Promise<void> {
|
|
58
|
-
for (let attempt = 0; ; attempt++) {
|
|
59
|
-
try {
|
|
60
|
-
writePresence({ dir: codexPaths.presenceDir, id: supervisorId, accountId: seat.id, pid: child.pid });
|
|
61
|
-
return;
|
|
62
|
-
} catch (e) {
|
|
63
|
-
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
64
|
-
if (attempt >= 9) {
|
|
65
|
-
log("codexsupervisor.presence_failed", { err: e instanceof Error ? e.message : String(e) });
|
|
66
|
-
child.kill();
|
|
67
|
-
await child.exited;
|
|
68
|
-
restoreTermios(savedTermios);
|
|
69
|
-
throw new Error("could not write the codex presence file - refusing to run an unprotected session (its account would look like a swap target)");
|
|
70
|
-
}
|
|
71
|
-
await Bun.sleep(100);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
58
|
export async function runCodexSupervisor(input: { argv: string[] }): Promise<number> {
|
|
77
59
|
const { argv } = input;
|
|
78
|
-
|
|
79
|
-
if (depth >= MAX_WRAP_DEPTH) {
|
|
80
|
-
console.error(
|
|
81
|
-
`tokenmaxxing: ${LOOP_DIAGNOSIS} (depth ${depth}) - codexBin in ${paths.configJson} does not launch the real codex binary. Fix codexBin, then run \`tokenmaxxing doctor\`.`,
|
|
82
|
-
);
|
|
83
|
-
log("codexsupervisor.loop_abort", { depth });
|
|
84
|
-
return 1;
|
|
85
|
-
}
|
|
86
|
-
if (wrapperEntryRateTripped(Date.now())) {
|
|
87
|
-
console.error(
|
|
88
|
-
`tokenmaxxing: ${LOOP_DIAGNOSIS} (over ${WRAP_RATE_MAX} wrapper entries in ${WRAP_RATE_WINDOW_MS / 1000}s) - codexBin in ${paths.configJson} does not launch the real codex binary. Fix codexBin, then run \`tokenmaxxing doctor\`.`,
|
|
89
|
-
);
|
|
90
|
-
log("codexsupervisor.rate_abort", { max: WRAP_RATE_MAX });
|
|
91
|
-
return 1;
|
|
92
|
-
}
|
|
60
|
+
if (loopGuardTripped("codex")) return 1;
|
|
93
61
|
|
|
94
62
|
const real = resolveRealCodex();
|
|
95
|
-
const childEnv = { ...process.env, [WRAP_DEPTH_ENV]: String(
|
|
63
|
+
const childEnv = { ...process.env, [WRAP_DEPTH_ENV]: String(wrapDepth() + 1) };
|
|
96
64
|
|
|
97
65
|
if (!shouldManageCodex({ argv }) || process.env[UNMANAGED_ENV]) {
|
|
98
66
|
const passthroughEnv: Record<string, string | undefined> = { ...childEnv };
|
|
99
67
|
delete passthroughEnv[CODEX_SUPERVISOR_ID_ENV];
|
|
100
|
-
|
|
101
|
-
await p.exited;
|
|
102
|
-
return p.exitCode ?? (p.signalCode ? 1 : 0);
|
|
68
|
+
return runPassthrough({ real, argv, env: passthroughEnv });
|
|
103
69
|
}
|
|
104
70
|
|
|
105
71
|
const supervisorId = crypto.randomUUID();
|
|
@@ -131,37 +97,29 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
|
|
|
131
97
|
...(store ? { CODEX_HOME: store } : {}),
|
|
132
98
|
},
|
|
133
99
|
});
|
|
134
|
-
if (picked)
|
|
100
|
+
if (picked) {
|
|
101
|
+
await recordPresenceOrStop({
|
|
102
|
+
child: spawned,
|
|
103
|
+
dir: codexPaths.presenceDir,
|
|
104
|
+
id: supervisorId,
|
|
105
|
+
accountId: picked.id,
|
|
106
|
+
event: "codexsupervisor.presence_failed",
|
|
107
|
+
message: "could not write the codex presence file - refusing to run an unprotected session (its account would look like a swap target)",
|
|
108
|
+
savedTermios,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
135
111
|
return { child: spawned, seat: picked };
|
|
136
112
|
});
|
|
137
113
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (existsSync(marker))
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
return false;
|
|
148
|
-
})();
|
|
149
|
-
const exited = child.exited.then(() => {
|
|
150
|
-
done = true;
|
|
151
|
-
return "exit";
|
|
114
|
+
await raceMarkerOrExit({
|
|
115
|
+
child,
|
|
116
|
+
tick: () => {
|
|
117
|
+
if (!existsSync(marker)) return false;
|
|
118
|
+
readCodexMarker(marker);
|
|
119
|
+
return true;
|
|
120
|
+
},
|
|
121
|
+
savedTermios,
|
|
152
122
|
});
|
|
153
|
-
try {
|
|
154
|
-
const winner = await Promise.race([exited, markerWatch.then((found) => (found ? "marker" : "exit"))]);
|
|
155
|
-
if (winner === "marker") child.kill();
|
|
156
|
-
} catch (e) {
|
|
157
|
-
child.kill();
|
|
158
|
-
throw e;
|
|
159
|
-
} finally {
|
|
160
|
-
await child.exited;
|
|
161
|
-
done = true;
|
|
162
|
-
await markerWatch.catch(() => false);
|
|
163
|
-
restoreTermios(savedTermios);
|
|
164
|
-
}
|
|
165
123
|
|
|
166
124
|
const payload = existsSync(marker) ? readCodexMarker(marker) : null;
|
|
167
125
|
if (payload) {
|
|
@@ -175,6 +133,6 @@ export async function runCodexSupervisor(input: { argv: string[] }): Promise<num
|
|
|
175
133
|
}
|
|
176
134
|
clearPresence({ dir: codexPaths.presenceDir, id: supervisorId });
|
|
177
135
|
log("codexsupervisor.exit", { supervisorId: supervisorId.slice(0, 8), respawns, code: child.exitCode, signal: child.signalCode });
|
|
178
|
-
return
|
|
136
|
+
return exitStatus(child);
|
|
179
137
|
}
|
|
180
138
|
}
|
|
@@ -1,39 +1,32 @@
|
|
|
1
1
|
import { sortBy } from "es-toolkit";
|
|
2
|
-
import { z } from "zod";
|
|
3
2
|
import { claudePool, seatFromEnv } from "../lib/paths.ts";
|
|
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
6
|
import { earliestReset, limitWindows, weeklyExpiry, weeklyWindow } from "../lib/picker.ts";
|
|
7
|
+
import { readStdin } from "../lib/proc.ts";
|
|
7
8
|
import { worktreeName } from "../lib/worktree.ts";
|
|
8
|
-
import { makeColors, makeUsagePaint } from "../cli/render.ts";
|
|
9
|
-
import { fmtResetShort } from "../lib/usage.ts";
|
|
9
|
+
import { fmtResetShort, makeColors, makeUsagePaint, statuslineColor } from "../cli/render.ts";
|
|
10
10
|
import {
|
|
11
|
-
AccountsIndexSchema,
|
|
12
11
|
JsonTextSchema,
|
|
13
12
|
StatusLineStdinSchema,
|
|
14
|
-
WindowSchema,
|
|
15
13
|
type Account,
|
|
14
|
+
type AccountsIndex,
|
|
16
15
|
type UsageState,
|
|
17
16
|
type UsageWindow,
|
|
17
|
+
type Window,
|
|
18
18
|
} from "../lib/types.ts";
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
accounts:
|
|
22
|
-
perModel:
|
|
23
|
-
switchModels:
|
|
24
|
-
worktree:
|
|
25
|
-
liveAccount:
|
|
26
|
-
now:
|
|
27
|
-
color:
|
|
28
|
-
truecolor:
|
|
29
|
-
}
|
|
30
|
-
export type RenderCtx = z.infer<typeof RenderCtxSchema>;
|
|
31
|
-
|
|
32
|
-
export async function readStdin(): Promise<string> {
|
|
33
|
-
const chunks: Uint8Array[] = [];
|
|
34
|
-
for await (const c of Bun.stdin.stream()) chunks.push(c);
|
|
35
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
36
|
-
}
|
|
20
|
+
export type RenderCtx = {
|
|
21
|
+
accounts: AccountsIndex;
|
|
22
|
+
perModel: Record<string, Window>;
|
|
23
|
+
switchModels: string[];
|
|
24
|
+
worktree: string | null;
|
|
25
|
+
liveAccount: string | null;
|
|
26
|
+
now: number;
|
|
27
|
+
color: boolean;
|
|
28
|
+
truecolor: boolean;
|
|
29
|
+
};
|
|
37
30
|
|
|
38
31
|
export function renderStatusline(stdinObj: unknown, ctx: RenderCtx): string {
|
|
39
32
|
const col = makeColors(ctx.color);
|
|
@@ -134,7 +127,6 @@ export async function runStatusline(): Promise<number> {
|
|
|
134
127
|
const accounts = loadAccounts(claudePool);
|
|
135
128
|
const stdin = StatusLineStdinSchema.safeParse(obj);
|
|
136
129
|
const dir = stdin.success ? (stdin.data.workspace?.current_dir ?? stdin.data.workspace?.project_dir ?? null) : null;
|
|
137
|
-
const colorterm = z.string().optional().parse(process.env.COLORTERM);
|
|
138
130
|
const live = accounts.accounts.find((a) => a.id === account);
|
|
139
131
|
const ctx: RenderCtx = {
|
|
140
132
|
accounts,
|
|
@@ -143,12 +135,11 @@ export async function runStatusline(): Promise<number> {
|
|
|
143
135
|
worktree: dir == null ? null : worktreeName(dir),
|
|
144
136
|
liveAccount: account,
|
|
145
137
|
now,
|
|
146
|
-
|
|
147
|
-
truecolor: colorterm != null && (colorterm.includes("truecolor") || colorterm.includes("24bit")),
|
|
138
|
+
...statuslineColor(),
|
|
148
139
|
};
|
|
149
140
|
line = renderStatusline(obj, ctx);
|
|
150
141
|
} catch (e) {
|
|
151
|
-
line = `tokenmaxxing: ${
|
|
142
|
+
line = `tokenmaxxing: ${errorMessage(e)}`;
|
|
152
143
|
}
|
|
153
144
|
process.stdout.write(line + "\n");
|
|
154
145
|
return 0;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { claude } from "../lib/claude.ts";
|
|
3
3
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
4
|
+
import { readStdin } from "../lib/proc.ts";
|
|
4
5
|
import { supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
5
6
|
import { loadConfig } from "../lib/state.ts";
|
|
6
7
|
import { classifyEnforcedLimit, findEnforcedRow, parseErrorBody, readTranscriptTail, type TranscriptRow } from "../lib/usage.ts";
|
|
7
8
|
import { paths } from "../lib/paths.ts";
|
|
8
9
|
import { JsonTextSchema, type EnforcedLimit } from "../lib/types.ts";
|
|
9
|
-
import { log } from "../lib/log.ts";
|
|
10
|
-
import { readStdin } from "./statusline.ts";
|
|
10
|
+
import { errorMessage, log } from "../lib/log.ts";
|
|
11
11
|
|
|
12
12
|
const StopFailureStdin = z.looseObject({
|
|
13
13
|
session_id: z.uuid().optional().catch(undefined),
|
|
@@ -34,8 +34,7 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
34
34
|
|
|
35
35
|
const account = claude.liveId();
|
|
36
36
|
const now = Date.now();
|
|
37
|
-
const
|
|
38
|
-
const parsed = StopFailureStdin.safeParse(JsonTextSchema.safeParse(raw).data);
|
|
37
|
+
const parsed = StopFailureStdin.safeParse(JsonTextSchema.safeParse(await readStdin()).data);
|
|
39
38
|
const stdin = parsed.success ? parsed.data : {};
|
|
40
39
|
if (stdin.error !== undefined && stdin.error !== "rate_limit") return 0;
|
|
41
40
|
|
|
@@ -84,7 +83,7 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
84
83
|
log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
|
|
85
84
|
}
|
|
86
85
|
} catch (e) {
|
|
87
|
-
log("stopfailure.error", { err:
|
|
86
|
+
log("stopfailure.error", { err: errorMessage(e) });
|
|
88
87
|
}
|
|
89
88
|
return 0;
|
|
90
89
|
}
|
package/src/entries/stophook.ts
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { claude } from "../lib/claude.ts";
|
|
3
3
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
4
|
+
import { readStdin } from "../lib/proc.ts";
|
|
4
5
|
import { supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
5
6
|
import { JsonTextSchema } from "../lib/types.ts";
|
|
6
|
-
import { log } from "../lib/log.ts";
|
|
7
|
-
import { readStdin } from "./statusline.ts";
|
|
7
|
+
import { errorMessage, log } from "../lib/log.ts";
|
|
8
8
|
|
|
9
|
-
const
|
|
9
|
+
const BoundaryStdin = z.looseObject({
|
|
10
|
+
source: z.string().optional(),
|
|
11
|
+
session_id: z.uuid().optional().catch(undefined),
|
|
12
|
+
});
|
|
10
13
|
|
|
11
|
-
export async function
|
|
14
|
+
export async function runBoundaryHook(event: "stop" | "sessionstart"): Promise<number> {
|
|
12
15
|
if (process.env.TOKENMAXXING_PROBE) return 0;
|
|
13
16
|
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const stdinSid = parsed.success ? parsed.data.session_id : undefined;
|
|
17
|
+
const parsed = BoundaryStdin.safeParse(JsonTextSchema.safeParse(await readStdin()).data);
|
|
18
|
+
const stdin = parsed.success ? parsed.data : {};
|
|
17
19
|
const session = supervisedSession();
|
|
18
20
|
|
|
19
21
|
try {
|
|
20
22
|
const decision = await evaluateAndMaybeSwap(claude, Date.now(), session != null);
|
|
21
23
|
if (session && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
|
|
22
|
-
writeRespawnMarker({ session, sessionId:
|
|
23
|
-
log(decision.waitUntil !== undefined ? "
|
|
24
|
+
writeRespawnMarker({ session, sessionId: stdin.session_id ?? session.sid, accountId: decision.account.id, waitUntil: decision.waitUntil ?? Date.now(), compact: true });
|
|
25
|
+
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) });
|
|
24
26
|
}
|
|
25
27
|
} catch (e) {
|
|
26
|
-
log(
|
|
28
|
+
log(`${event}.error`, { err: errorMessage(e) });
|
|
27
29
|
}
|
|
28
30
|
return 0;
|
|
29
31
|
}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { readStdin } from "./statusline.ts";
|
|
1
|
+
import { makeColors, makeUsagePaint, statuslineColor } from "../cli/render.ts";
|
|
2
|
+
import { readStdin } from "../lib/proc.ts";
|
|
4
3
|
import { JsonTextSchema, SubagentStatusLineStdinSchema } from "../lib/types.ts";
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
export type RowCtx = z.infer<typeof RowCtxSchema>;
|
|
5
|
+
export type RowCtx = { color: boolean; truecolor: boolean };
|
|
8
6
|
|
|
9
7
|
function modelFamily(id: string): string {
|
|
10
8
|
const [head, family] = id.split("-");
|
|
@@ -44,11 +42,7 @@ export function renderSubagentRows(stdinObj: unknown, ctx: RowCtx): string[] {
|
|
|
44
42
|
|
|
45
43
|
export async function runSubagentStatusline(): Promise<number> {
|
|
46
44
|
const obj = JsonTextSchema.safeParse(await readStdin()).data ?? null;
|
|
47
|
-
const
|
|
48
|
-
const rows = renderSubagentRows(obj, {
|
|
49
|
-
color: !process.env.NO_COLOR,
|
|
50
|
-
truecolor: colorterm != null && (colorterm.includes("truecolor") || colorterm.includes("24bit")),
|
|
51
|
-
});
|
|
45
|
+
const rows = renderSubagentRows(obj, statuslineColor());
|
|
52
46
|
if (rows.length > 0) process.stdout.write(rows.join("\n") + "\n");
|
|
53
47
|
return 0;
|
|
54
48
|
}
|
|
@@ -4,19 +4,21 @@ import type { FileSink, Subprocess } from "bun";
|
|
|
4
4
|
import { maxBy } from "es-toolkit";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { claudePool, paths, storeDirFor } from "../lib/paths.ts";
|
|
7
|
-
import {
|
|
7
|
+
import { UNMANAGED_ENV, WRAP_DEPTH_ENV, resolveRealClaude, wrapDepth } from "../lib/claudebin.ts";
|
|
8
8
|
import { claude, pickSeat } from "../lib/claude.ts";
|
|
9
9
|
import { compactClaudeSession } from "../lib/compact.ts";
|
|
10
10
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
11
11
|
import { withLock } from "../lib/lock.ts";
|
|
12
12
|
import { thresholdBars, usableAt } from "../lib/picker.ts";
|
|
13
|
-
import { clearPresence
|
|
13
|
+
import { clearPresence } from "../lib/presence.ts";
|
|
14
|
+
import { readLines } from "../lib/proc.ts";
|
|
14
15
|
import { teeObservation } from "../lib/sample.ts";
|
|
15
|
-
import {
|
|
16
|
+
import { exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough } from "../lib/supervise.ts";
|
|
17
|
+
import { saveTermios } from "../lib/tty.ts";
|
|
16
18
|
import { loadSessionFlags, pruneStaleSessions, saveSessionFlags, writeRespawnMarker } from "../lib/sessions.ts";
|
|
17
19
|
import { loadAccounts, loadConfig } from "../lib/state.ts";
|
|
18
20
|
import { RespawnMarkerSchema, type Account } from "../lib/types.ts";
|
|
19
|
-
import { log } from "../lib/log.ts";
|
|
21
|
+
import { errorMessage, log } from "../lib/log.ts";
|
|
20
22
|
|
|
21
23
|
const NONINTERACTIVE_SUBCMDS = new Set([
|
|
22
24
|
"mcp", "config", "doctor", "update", "install", "migrate-installer",
|
|
@@ -44,15 +46,14 @@ const OPTIONAL_VALUE_ROOT_FLAGS = new Set([
|
|
|
44
46
|
|
|
45
47
|
const isUuid = (s: string) => z.uuid().safeParse(s).success;
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
manage:
|
|
49
|
-
sessionId:
|
|
50
|
-
resumeId:
|
|
51
|
-
continueLatest:
|
|
52
|
-
streamInput:
|
|
53
|
-
streamOutput:
|
|
54
|
-
}
|
|
55
|
-
type Analysis = z.infer<typeof AnalysisSchema>;
|
|
49
|
+
type Analysis = {
|
|
50
|
+
manage: boolean;
|
|
51
|
+
sessionId: string | null;
|
|
52
|
+
resumeId: string | null;
|
|
53
|
+
continueLatest: boolean;
|
|
54
|
+
streamInput: boolean;
|
|
55
|
+
streamOutput: boolean;
|
|
56
|
+
};
|
|
56
57
|
|
|
57
58
|
export function analyzeArgs(argv: string[]): Analysis {
|
|
58
59
|
let sessionId: string | null = null;
|
|
@@ -169,18 +170,17 @@ function latestSessionForCwd(): string | null {
|
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
|
|
173
|
-
launchedAt:
|
|
174
|
-
overriddenUntil:
|
|
175
|
-
}
|
|
176
|
-
type MarkerGate = z.infer<typeof MarkerGateSchema>;
|
|
173
|
+
type MarkerGate = {
|
|
174
|
+
launchedAt: number;
|
|
175
|
+
overriddenUntil: number;
|
|
176
|
+
};
|
|
177
177
|
|
|
178
178
|
function consumableMarker(marker: string, gate: MarkerGate): z.infer<typeof RespawnMarkerSchema> | null {
|
|
179
179
|
let m: z.infer<typeof RespawnMarkerSchema>;
|
|
180
180
|
try {
|
|
181
181
|
m = RespawnMarkerSchema.parse(JSON.parse(readFileSync(marker, "utf8")));
|
|
182
182
|
} catch (e) {
|
|
183
|
-
log("supervisor.marker_invalid", { err:
|
|
183
|
+
log("supervisor.marker_invalid", { err: errorMessage(e) });
|
|
184
184
|
throw new Error(
|
|
185
185
|
`${marker} is corrupt (unparsable JSON or off-schema) - the session was stopped instead of guessing whether the pool is depleted; inspect the marker, then run \`claude --resume ${basename(marker)}\` (a fresh launch clears it)`,
|
|
186
186
|
);
|
|
@@ -222,7 +222,7 @@ async function moveExhaustedSeat(seat: Account, sid: string, gate: MarkerGate):
|
|
|
222
222
|
writeRespawnMarker({ session: { sid, launchedAt: gate.launchedAt }, sessionId: sid, accountId: decision.account.id, waitUntil: decision.waitUntil ?? now, compact: true });
|
|
223
223
|
}
|
|
224
224
|
} catch (e) {
|
|
225
|
-
log("supervisor.seat_watch_error", { err:
|
|
225
|
+
log("supervisor.seat_watch_error", { err: errorMessage(e) });
|
|
226
226
|
}
|
|
227
227
|
return true;
|
|
228
228
|
}
|
|
@@ -276,7 +276,7 @@ class StdinRelay {
|
|
|
276
276
|
|
|
277
277
|
constructor() {
|
|
278
278
|
this.pump()
|
|
279
|
-
.catch((e: unknown) => log("supervisor.relay_read_failed", { err:
|
|
279
|
+
.catch((e: unknown) => log("supervisor.relay_read_failed", { err: errorMessage(e) }))
|
|
280
280
|
.finally(() => {
|
|
281
281
|
this.ended = true;
|
|
282
282
|
this.close();
|
|
@@ -296,19 +296,7 @@ class StdinRelay {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
private async pump(): Promise<void> {
|
|
299
|
-
const
|
|
300
|
-
let pending = "";
|
|
301
|
-
for await (const chunk of Bun.stdin.stream()) {
|
|
302
|
-
pending += decoder.decode(chunk, { stream: true });
|
|
303
|
-
let nl = pending.indexOf("\n");
|
|
304
|
-
while (nl !== -1) {
|
|
305
|
-
this.forward(pending.slice(0, nl + 1));
|
|
306
|
-
pending = pending.slice(nl + 1);
|
|
307
|
-
nl = pending.indexOf("\n");
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
pending += decoder.decode();
|
|
311
|
-
if (pending.length > 0) this.forward(`${pending}\n`);
|
|
299
|
+
for await (const line of readLines(Bun.stdin.stream())) this.forward(`${line}\n`);
|
|
312
300
|
}
|
|
313
301
|
|
|
314
302
|
private forward(line: string): void {
|
|
@@ -318,7 +306,7 @@ class StdinRelay {
|
|
|
318
306
|
this.sink.flush();
|
|
319
307
|
return;
|
|
320
308
|
} catch (e) {
|
|
321
|
-
log("supervisor.relay_write_failed", { err:
|
|
309
|
+
log("supervisor.relay_write_failed", { err: errorMessage(e) });
|
|
322
310
|
this.sink = null;
|
|
323
311
|
}
|
|
324
312
|
}
|
|
@@ -330,50 +318,17 @@ class StdinRelay {
|
|
|
330
318
|
try {
|
|
331
319
|
this.sink.end();
|
|
332
320
|
} catch (e) {
|
|
333
|
-
log("supervisor.relay_end_failed", { err:
|
|
321
|
+
log("supervisor.relay_end_failed", { err: errorMessage(e) });
|
|
334
322
|
}
|
|
335
323
|
this.sink = null;
|
|
336
324
|
}
|
|
337
325
|
}
|
|
338
326
|
|
|
339
|
-
async function recordPresence(child: Subprocess, sid: string, seat: Account, savedTermios: string | null): Promise<void> {
|
|
340
|
-
for (let attempt = 0; ; attempt++) {
|
|
341
|
-
try {
|
|
342
|
-
writePresence({ dir: paths.presenceDir, id: sid, accountId: seat.id, pid: child.pid });
|
|
343
|
-
return;
|
|
344
|
-
} catch (e) {
|
|
345
|
-
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
346
|
-
if (attempt >= 9) {
|
|
347
|
-
log("supervisor.presence_failed", { err: e instanceof Error ? e.message : String(e) });
|
|
348
|
-
child.kill();
|
|
349
|
-
await child.exited;
|
|
350
|
-
restoreTermios(savedTermios);
|
|
351
|
-
throw new Error("could not write the session presence file - refusing to run a session whose seat placement cannot see");
|
|
352
|
-
}
|
|
353
|
-
await Bun.sleep(100);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
327
|
export async function runSupervisor(argv: string[]): Promise<number> {
|
|
359
|
-
|
|
360
|
-
if (depth >= MAX_WRAP_DEPTH) {
|
|
361
|
-
console.error(
|
|
362
|
-
`tokenmaxxing: ${LOOP_DIAGNOSIS} (depth ${depth}) - claudeBin in ${paths.configJson} does not launch the real Claude binary. Fix claudeBin, then run \`tokenmaxxing doctor\`.`,
|
|
363
|
-
);
|
|
364
|
-
log("supervisor.loop_abort", { depth });
|
|
365
|
-
return 1;
|
|
366
|
-
}
|
|
367
|
-
if (wrapperEntryRateTripped(Date.now())) {
|
|
368
|
-
console.error(
|
|
369
|
-
`tokenmaxxing: ${LOOP_DIAGNOSIS} (over ${WRAP_RATE_MAX} wrapper entries in ${WRAP_RATE_WINDOW_MS / 1000}s) - claudeBin in ${paths.configJson} does not launch the real Claude binary. Fix claudeBin, then run \`tokenmaxxing doctor\`.`,
|
|
370
|
-
);
|
|
371
|
-
log("supervisor.rate_abort", { max: WRAP_RATE_MAX });
|
|
372
|
-
return 1;
|
|
373
|
-
}
|
|
328
|
+
if (loopGuardTripped("claude")) return 1;
|
|
374
329
|
const real = resolveRealClaude();
|
|
375
330
|
const info = analyzeArgs(argv);
|
|
376
|
-
const childEnv = { ...process.env, [WRAP_DEPTH_ENV]: String(
|
|
331
|
+
const childEnv = { ...process.env, [WRAP_DEPTH_ENV]: String(wrapDepth() + 1) };
|
|
377
332
|
|
|
378
333
|
let child: Subprocess | null = null;
|
|
379
334
|
let terminating = false;
|
|
@@ -387,10 +342,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
387
342
|
const passthroughEnv: Record<string, string | undefined> = { ...childEnv };
|
|
388
343
|
delete passthroughEnv.TOKENMAXXING_SUPERVISED;
|
|
389
344
|
delete passthroughEnv.TOKENMAXXING_SESSION_ID;
|
|
390
|
-
|
|
391
|
-
child = p;
|
|
392
|
-
await p.exited;
|
|
393
|
-
return p.exitCode ?? (p.signalCode ? 1 : 0);
|
|
345
|
+
return runPassthrough({ real, argv, env: passthroughEnv, onSpawn: (p) => { child = p; } });
|
|
394
346
|
}
|
|
395
347
|
|
|
396
348
|
let base = stripSessionFlags(argv);
|
|
@@ -458,44 +410,40 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
458
410
|
});
|
|
459
411
|
if (relay !== null) relay.attach(spawned.stdin!, firstLine);
|
|
460
412
|
firstLine = null;
|
|
461
|
-
if (picked)
|
|
413
|
+
if (picked) {
|
|
414
|
+
await recordPresenceOrStop({
|
|
415
|
+
child: spawned,
|
|
416
|
+
dir: paths.presenceDir,
|
|
417
|
+
id: sid,
|
|
418
|
+
accountId: picked.id,
|
|
419
|
+
event: "supervisor.presence_failed",
|
|
420
|
+
message: "could not write the session presence file - refusing to run a session whose seat placement cannot see",
|
|
421
|
+
savedTermios,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
462
424
|
return { child: spawned, seat: picked };
|
|
463
425
|
});
|
|
464
426
|
|
|
465
427
|
child = proc;
|
|
466
|
-
let done = false;
|
|
467
428
|
let seatCheckAt = 0;
|
|
468
|
-
|
|
469
|
-
|
|
429
|
+
await raceMarkerOrExit({
|
|
430
|
+
child: proc,
|
|
431
|
+
tick: async () => {
|
|
470
432
|
if (existsSync(marker) && consumableMarker(marker, gate) != null) return true;
|
|
471
433
|
if (seat && !terminating && Date.now() >= seatCheckAt) {
|
|
472
434
|
const decided = await moveExhaustedSeat(seat, sid, gate);
|
|
473
435
|
seatCheckAt = Date.now() + (decided ? SEAT_RETRY_MS : SEAT_POLL_MS);
|
|
474
436
|
}
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if (winner === "marker") {
|
|
437
|
+
return false;
|
|
438
|
+
},
|
|
439
|
+
savedTermios,
|
|
440
|
+
onKill: () => relay?.detach(),
|
|
441
|
+
onError: () => clearPresence({ dir: paths.presenceDir, id: sid }),
|
|
442
|
+
onExited: () => {
|
|
443
|
+
child = null;
|
|
483
444
|
relay?.detach();
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
} catch (e) {
|
|
487
|
-
relay?.detach();
|
|
488
|
-
proc.kill();
|
|
489
|
-
clearPresence({ dir: paths.presenceDir, id: sid });
|
|
490
|
-
throw e;
|
|
491
|
-
} finally {
|
|
492
|
-
await proc.exited;
|
|
493
|
-
child = null;
|
|
494
|
-
relay?.detach();
|
|
495
|
-
done = true;
|
|
496
|
-
await markerWatch.catch(() => {});
|
|
497
|
-
restoreTermios(savedTermios);
|
|
498
|
-
}
|
|
445
|
+
},
|
|
446
|
+
});
|
|
499
447
|
|
|
500
448
|
const m = !terminating && existsSync(marker) ? consumableMarker(marker, gate) : null;
|
|
501
449
|
if (m) {
|
|
@@ -528,6 +476,6 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
528
476
|
}
|
|
529
477
|
clearPresence({ dir: paths.presenceDir, id: sid });
|
|
530
478
|
log("supervisor.exit", { sid, respawns, code: proc.exitCode, signal: proc.signalCode, terminated: terminating || undefined });
|
|
531
|
-
return
|
|
479
|
+
return exitStatus(proc);
|
|
532
480
|
}
|
|
533
481
|
}
|