tokenmaxxing 1.48.0 → 1.50.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 +4 -4
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/entries/stopfailurehook.ts +11 -0
- package/src/entries/supervisor.ts +118 -19
package/DESIGN.md
CHANGED
|
@@ -21,7 +21,7 @@ supervisor (you type `claude`) → real claude, CLAUDE_SECURESTORAGE_CONFIG_DI
|
|
|
21
21
|
▲______________ relaunch --resume <sid> under another store ______________|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
It is a process manager only - spawn with inherited stdio plus saved `stty -g` termios (not a PTY copy), wait, restore the terminal, relaunch. It never proxies API traffic and never handles tokens. Everything else about `claude` is unchanged.
|
|
24
|
+
It is a process manager only - spawn with inherited stdio (stdin through a relay pipe for a stream-json session, see §3.3) plus saved `stty -g` termios (not a PTY copy), wait, restore the terminal, relaunch. It never proxies API traffic and never handles tokens. Everything else about `claude` is unchanged.
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
@@ -58,7 +58,7 @@ The Stop hook's stdin has no usage data, but the **statusLine does** (`rate_limi
|
|
|
58
58
|
4. Write `respawn/<session-id>` (atomic temp+rename) naming the target, with `waitUntil` now for a plain move or the soonest reset when the pool is depleted. No credential is written.
|
|
59
59
|
|
|
60
60
|
### 3.3 Move and depleted-pool pause (supervisor)
|
|
61
|
-
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 (`compact: true` from the Stop and SessionStart hooks, `false` from StopFailure: it runs `claude -p --resume <sid> /compact` under the old seat's store, bounded at five minutes, 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 <sid>` with `CLAUDE_SECURESTORAGE_CONFIG_DIR` set to the target's store and rewrites the session's presence. The resumed process reads that store cold → runs on the target account, same conversation. The `SessionStart` hook (source `resume`) 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. A marker that fails to parse is never deleted and treated as absent: the supervisor stops the child, restores the terminal, and exits naming the marker path.
|
|
61
|
+
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 (`compact: true` from the Stop and SessionStart hooks, `false` from StopFailure: it runs `claude -p --resume <sid> /compact` under the old seat's store, bounded at five minutes, 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 <sid>` with `CLAUDE_SECURESTORAGE_CONFIG_DIR` set to the target's store and rewrites the session's presence. The resumed process reads that store cold → runs on the target account, same conversation. The relaunch submits a first prompt that names the move (and the compaction, when it landed) and asks Claude to continue the task, so 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, and writes the prompt as the first `user` line of the new process. The supervisor's own notices go to stderr, so a stream-json stdout stays parseable. The `SessionStart` hook (source `resume`) 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. A marker that fails to parse is never deleted and treated as absent: the supervisor stops the child, restores the terminal, and exits naming the marker path.
|
|
62
62
|
|
|
63
63
|
### 3.4 Placement (supervisor, under the pool lock)
|
|
64
64
|
1. Fold every account's tee into the index.
|
|
@@ -95,7 +95,7 @@ The [profile](docs/content/docs/switching-profile.mdx) records observed cache re
|
|
|
95
95
|
|
|
96
96
|
## 6. Honest papercuts
|
|
97
97
|
- **A move restarts the process.** Every move is a `claude` stop and `--resume`; anything typed in the split second before the SIGTERM is lost, the supervisor resets terminal mode so nothing is left garbled, and the first turn on the new account re-uploads context once (prompt cache is org-scoped). A bar-triggered move compacts on the old account first so that upload is the summary, not the transcript; a move after a refusal cannot, and re-uploads the full context.
|
|
98
|
-
- **Single-turn overshoot.** If one turn jumps from under the threshold straight past the wall, that turn ends rate-limited; the StopFailure hook walls the seat and moves the session, and the
|
|
98
|
+
- **Single-turn overshoot.** If one turn jumps from under the threshold straight past the wall, that turn ends rate-limited; the StopFailure hook walls the seat and moves the session, and the relaunch continues the conversation on the fresh account from the supervisor's resume prompt. The static session-bar margin (`policy.projectionMargin`, default 3) reduces this.
|
|
99
99
|
- **Unsupervised sessions are not moved.** A claude started outside the supervisor runs on whatever login its environment names, by default Claude Code's default store, which tokenmaxxing never writes.
|
|
100
100
|
- **One shared identity file.** `~/.claude.json` keeps a single `oauthAccount`, which Claude Code rewrites after whichever session refreshed last, so `/status` can show another session's email. The store decides which credential a session uses.
|
|
101
101
|
- **Dead stores.** A refresh Claude Code cannot complete dead-clears the store; the sampler and the decision recognize the empty tokens without a request, flag the account, and `auth` repairs it in place.
|
|
@@ -123,7 +123,7 @@ The [profile](docs/content/docs/switching-profile.mdx) records observed cache re
|
|
|
123
123
|
---
|
|
124
124
|
|
|
125
125
|
## 8. Stack
|
|
126
|
-
TypeScript on Bun, shipped as source: one multi-call entry (`src/main.ts`, `#!/usr/bin/env bun`) serves the CLI, the `claude` supervisor, the statusLine shim, and the hooks; `init` installs a 2-line shim that `exec`s bun on the installed package's entry (the Stop path runs every turn; bun's start-up stays low-millisecond). Published to npm as `tokenmaxxing` (source, platform-independent - a compiled binary was tried and shipped one architecture's Mach-O to every platform). Shipping is PR-based since 2026-07-18: work reaches main only through a pull request (branch, PR, CI green, a 10-minute review wait, every review handled, merge, teardown), and a release is a PR-landed version bump followed by `gh release create` (details in AGENTS.md "Release and CI"). The supervisor spawns claude with inherited stdio and restores saved `stty -g` termios between runs - no PTY layer; resize and signals flow through the shared foreground process group.
|
|
126
|
+
TypeScript on Bun, shipped as source: one multi-call entry (`src/main.ts`, `#!/usr/bin/env bun`) serves the CLI, the `claude` supervisor, the statusLine shim, and the hooks; `init` installs a 2-line shim that `exec`s bun on the installed package's entry (the Stop path runs every turn; bun's start-up stays low-millisecond). Published to npm as `tokenmaxxing` (source, platform-independent - a compiled binary was tried and shipped one architecture's Mach-O to every platform). Shipping is PR-based since 2026-07-18: work reaches main only through a pull request (branch, PR, CI green, a 10-minute review wait, every review handled, merge, teardown), and a release is a PR-landed version bump followed by `gh release create` (details in AGENTS.md "Release and CI"). The supervisor spawns claude with inherited stdio (a relay pipe on stdin for a stream-json session) and restores saved `stty -g` termios between runs - no PTY layer; resize and signals flow through the shared foreground process group.
|
|
127
127
|
|
|
128
128
|
---
|
|
129
129
|
|
package/README.md
CHANGED
|
@@ -133,7 +133,7 @@ A Cursor Cloud Agent can hand substantial work to Claude Code running on your ow
|
|
|
133
133
|
|
|
134
134
|
## Honest limitations
|
|
135
135
|
|
|
136
|
-
- **A move restarts the process.** The session resumes the same transcript under the new account, but the process restarts and the first turn on the new account re-uploads context once (prompt cache is org-scoped).
|
|
136
|
+
- **A move restarts the process.** The session resumes the same transcript under the new account and continues on its own from a first prompt the supervisor submits, but the process restarts and the first turn on the new account re-uploads context once (prompt cache is org-scoped).
|
|
137
137
|
- **Depleted-pause hiccup.** When the whole pool is at its limit, `claude` stops for the countdown; anything typed in that split second is lost.
|
|
138
138
|
- **Unsupervised sessions are not moved.** A claude started outside the supervisor runs on Claude Code's own login, which tokenmaxxing never writes.
|
|
139
139
|
- **One shared identity file.** `~/.claude.json` holds one `oauthAccount` that Claude Code rewrites after whichever session refreshed last, so `/status` can show another session's email; the statusline's `◆` marks the account a session actually uses.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.50.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",
|
|
@@ -4,6 +4,7 @@ import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
|
4
4
|
import { supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
5
5
|
import { loadConfig } from "../lib/state.ts";
|
|
6
6
|
import { classifyEnforcedLimit, findEnforcedRow, parseErrorBody, readTranscriptTail } from "../lib/usage.ts";
|
|
7
|
+
import { paths } from "../lib/paths.ts";
|
|
7
8
|
import { JsonTextSchema, type EnforcedLimit } from "../lib/types.ts";
|
|
8
9
|
import { log } from "../lib/log.ts";
|
|
9
10
|
import { readStdin } from "./statusline.ts";
|
|
@@ -52,6 +53,16 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
52
53
|
});
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
if (session == null && limit != null) {
|
|
57
|
+
const shim = `${paths.binDir}/claude`;
|
|
58
|
+
log("stopfailure.unsupervised_hint", { sid: stdinSid });
|
|
59
|
+
process.stdout.write(
|
|
60
|
+
`${JSON.stringify({
|
|
61
|
+
systemMessage: `tokenmaxxing: this session runs outside the supervisor, so it cannot move to another account at the limit. Move it yourself: ${stdinSid ? `${shim} --resume ${stdinSid}` : `resume it through ${shim}`}`,
|
|
62
|
+
})}\n`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
const decision = await evaluateAndMaybeSwap(claude, now, canRespawn && enforced != null, enforced);
|
|
56
67
|
if (enforced && session && canRespawn && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
|
|
57
68
|
writeRespawnMarker({ session, sessionId: stdinSid ?? session.sid, accountId: decision.account.id, waitUntil: decision.waitUntil ?? now, compact: false });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, rmSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
|
-
import type { Subprocess } from "bun";
|
|
3
|
+
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";
|
|
@@ -22,18 +22,21 @@ const NONINTERACTIVE_SUBCMDS = new Set([
|
|
|
22
22
|
|
|
23
23
|
const VALUE_TAKING_ROOT_FLAGS = new Set([
|
|
24
24
|
"--agent", "--agents", "--append-system-prompt", "--append-system-prompt-file",
|
|
25
|
-
"--debug-file", "--effort", "--
|
|
26
|
-
"--
|
|
27
|
-
"--
|
|
28
|
-
"--
|
|
29
|
-
"--
|
|
25
|
+
"--autocompact", "--debug-file", "--effort", "--environment", "--fallback-model",
|
|
26
|
+
"--input-format", "--json-schema", "--managed-settings", "--max-budget-usd",
|
|
27
|
+
"--max-thinking-tokens", "--max-turns", "--model", "-n", "--name",
|
|
28
|
+
"--output-format", "--permission-mode", "--permission-prompt-tool", "--permission-prompts",
|
|
29
|
+
"--plugin-dir", "--plugin-dir-no-mcp", "--plugin-url", "--remote-control-session-name-prefix",
|
|
30
|
+
"--setting-sources", "--settings", "--system-prompt", "--system-prompt-snapshot",
|
|
31
|
+
"--task-budget", "--thinking", "--thinking-display",
|
|
30
32
|
]);
|
|
31
33
|
const VARIADIC_ROOT_FLAGS = new Set([
|
|
32
34
|
"--add-dir", "--allowedTools", "--allowed-tools", "--betas",
|
|
33
35
|
"--disallowedTools", "--disallowed-tools", "--file", "--mcp-config", "--tools",
|
|
34
36
|
]);
|
|
35
37
|
const OPTIONAL_VALUE_ROOT_FLAGS = new Set([
|
|
36
|
-
"-d", "--debug", "--from-pr", "--prompt-suggestions", "--remote-control",
|
|
38
|
+
"--cloud", "-d", "--debug", "--from-pr", "--prompt-suggestions", "--remote-control",
|
|
39
|
+
"--teleport", "-w", "--worktree",
|
|
37
40
|
]);
|
|
38
41
|
|
|
39
42
|
const isUuid = (s: string) => z.uuid().safeParse(s).success;
|
|
@@ -43,6 +46,7 @@ const AnalysisSchema = z.object({
|
|
|
43
46
|
sessionId: z.string().nullable(),
|
|
44
47
|
resumeId: z.string().nullable(),
|
|
45
48
|
continueLatest: z.boolean(),
|
|
49
|
+
streamInput: z.boolean(),
|
|
46
50
|
});
|
|
47
51
|
type Analysis = z.infer<typeof AnalysisSchema>;
|
|
48
52
|
|
|
@@ -50,6 +54,7 @@ export function analyzeArgs(argv: string[]): Analysis {
|
|
|
50
54
|
let sessionId: string | null = null;
|
|
51
55
|
let resumeId: string | null = null;
|
|
52
56
|
let continueLatest = false;
|
|
57
|
+
let streamInput = false;
|
|
53
58
|
let printMode = false;
|
|
54
59
|
let invalidSessionArg = false;
|
|
55
60
|
let pickerResume = false;
|
|
@@ -82,6 +87,8 @@ export function analyzeArgs(argv: string[]): Analysis {
|
|
|
82
87
|
if (isUuid(value)) resumeId = value;
|
|
83
88
|
else pickerResume = true;
|
|
84
89
|
}
|
|
90
|
+
else if (a === "--input-format") streamInput = argv[++i] === "stream-json";
|
|
91
|
+
else if (a.startsWith("--input-format=")) streamInput = a.slice("--input-format=".length) === "stream-json";
|
|
85
92
|
else if (VALUE_TAKING_ROOT_FLAGS.has(a)) i++;
|
|
86
93
|
else if (VARIADIC_ROOT_FLAGS.has(a)) {
|
|
87
94
|
while (i + 1 < argv.length && !argv[i + 1]!.startsWith("-")) i++;
|
|
@@ -97,7 +104,7 @@ export function analyzeArgs(argv: string[]): Analysis {
|
|
|
97
104
|
const isSubcmd = firstPositional !== null && NONINTERACTIVE_SUBCMDS.has(firstPositional);
|
|
98
105
|
const forkResume = forkSession && (resumeId !== null || continueLatest);
|
|
99
106
|
const manage = !printMode && !isSubcmd && !invalidSessionArg && !pickerResume && !forkResume && !process.env.TOKENMAXXING_PROBE;
|
|
100
|
-
return { manage, sessionId, resumeId, continueLatest };
|
|
107
|
+
return { manage, sessionId, resumeId, continueLatest, streamInput };
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
export function stripSessionFlags(argv: string[]): string[] {
|
|
@@ -187,19 +194,97 @@ async function countdownWait(acct: string, until: number): Promise<boolean> {
|
|
|
187
194
|
let aborted = false;
|
|
188
195
|
const onInt = () => { aborted = true; };
|
|
189
196
|
process.on("SIGINT", onInt);
|
|
190
|
-
process.
|
|
197
|
+
process.stderr.write(`\n\x1b[36m⏳ tokenmaxxing: all accounts at their limit. Resuming on ${acct} when it resets (Ctrl-C to resume now).\x1b[0m\n`);
|
|
191
198
|
while (!aborted && Date.now() < until) {
|
|
192
199
|
const left = until - Date.now();
|
|
193
200
|
const m = Math.floor(left / 60000);
|
|
194
201
|
const s = Math.floor((left % 60000) / 1000);
|
|
195
|
-
process.
|
|
202
|
+
process.stderr.write(`\r\x1b[36m resuming in ${m}m ${String(s).padStart(2, "0")}s \x1b[0m`);
|
|
196
203
|
await Bun.sleep(1000);
|
|
197
204
|
}
|
|
198
205
|
process.removeListener("SIGINT", onInt);
|
|
199
|
-
process.
|
|
206
|
+
process.stderr.write(`\n\x1b[36m↻ resuming on ${acct}...\x1b[0m\n`);
|
|
200
207
|
return aborted;
|
|
201
208
|
}
|
|
202
209
|
|
|
210
|
+
function resumePrompt(compacted: boolean): string {
|
|
211
|
+
const moved = compacted
|
|
212
|
+
? "tokenmaxxing compacted this conversation and resumed the session on an account with quota headroom."
|
|
213
|
+
: "tokenmaxxing resumed this session on an account with quota headroom.";
|
|
214
|
+
return `${moved} Continue the task from where the previous turn left off. If the previous turn ended waiting on the user, restate what you need and wait.`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function userLine(text: string): string {
|
|
218
|
+
return `${JSON.stringify({ type: "user", message: { role: "user", content: [{ type: "text", text }] } })}\n`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
class StdinRelay {
|
|
222
|
+
private sink: FileSink | null = null;
|
|
223
|
+
private queue: string[] = [];
|
|
224
|
+
private ended = false;
|
|
225
|
+
|
|
226
|
+
constructor() {
|
|
227
|
+
this.pump()
|
|
228
|
+
.catch((e: unknown) => log("supervisor.relay_read_failed", { err: e instanceof Error ? e.message : String(e) }))
|
|
229
|
+
.finally(() => {
|
|
230
|
+
this.ended = true;
|
|
231
|
+
this.close();
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
attach(sink: FileSink, first: string | null): void {
|
|
236
|
+
this.sink = sink;
|
|
237
|
+
const lines = first === null ? this.queue : [first, ...this.queue];
|
|
238
|
+
this.queue = [];
|
|
239
|
+
for (const line of lines) this.forward(line);
|
|
240
|
+
if (this.ended) this.close();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
detach(): void {
|
|
244
|
+
this.sink = null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private async pump(): Promise<void> {
|
|
248
|
+
const decoder = new TextDecoder();
|
|
249
|
+
let pending = "";
|
|
250
|
+
for await (const chunk of Bun.stdin.stream()) {
|
|
251
|
+
pending += decoder.decode(chunk, { stream: true });
|
|
252
|
+
let nl = pending.indexOf("\n");
|
|
253
|
+
while (nl !== -1) {
|
|
254
|
+
this.forward(pending.slice(0, nl + 1));
|
|
255
|
+
pending = pending.slice(nl + 1);
|
|
256
|
+
nl = pending.indexOf("\n");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
pending += decoder.decode();
|
|
260
|
+
if (pending.length > 0) this.forward(`${pending}\n`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private forward(line: string): void {
|
|
264
|
+
if (this.sink !== null) {
|
|
265
|
+
try {
|
|
266
|
+
this.sink.write(line);
|
|
267
|
+
this.sink.flush();
|
|
268
|
+
return;
|
|
269
|
+
} catch (e) {
|
|
270
|
+
log("supervisor.relay_write_failed", { err: e instanceof Error ? e.message : String(e) });
|
|
271
|
+
this.sink = null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
this.queue.push(line);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private close(): void {
|
|
278
|
+
if (this.sink === null) return;
|
|
279
|
+
try {
|
|
280
|
+
this.sink.end();
|
|
281
|
+
} catch (e) {
|
|
282
|
+
log("supervisor.relay_end_failed", { err: e instanceof Error ? e.message : String(e) });
|
|
283
|
+
}
|
|
284
|
+
this.sink = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
203
288
|
async function recordPresence(child: Subprocess, sid: string, seat: Account, savedTermios: string | null): Promise<void> {
|
|
204
289
|
for (let attempt = 0; ; attempt++) {
|
|
205
290
|
try {
|
|
@@ -280,6 +365,8 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
280
365
|
process.on("SIGINT", () => {});
|
|
281
366
|
process.on("SIGHUP", () => {});
|
|
282
367
|
|
|
368
|
+
const relay = analyzeArgs(base).streamInput ? new StdinRelay() : null;
|
|
369
|
+
let firstLine: string | null = null;
|
|
283
370
|
let respawns = 0;
|
|
284
371
|
let overriddenUntil = 0;
|
|
285
372
|
let wanted: string | null = null;
|
|
@@ -289,9 +376,9 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
289
376
|
const gate: MarkerGate = { launchedAt: Date.now(), overriddenUntil };
|
|
290
377
|
const { child, seat } = await withLock(claudePool.lockFile, async () => {
|
|
291
378
|
const picked = (wanted == null ? null : (loadAccounts(claudePool).accounts.find((a) => a.id === wanted) ?? null)) ?? pickSeat(gate.launchedAt);
|
|
292
|
-
log("supervisor.launch", { sid, respawns, seat: picked?.id.slice(0, 8) ?? null, args: launchArgs.join(" ") });
|
|
379
|
+
log("supervisor.launch", { sid, respawns, seat: picked?.id.slice(0, 8) ?? null, args: launchArgs.join(" "), injected: firstLine !== null });
|
|
293
380
|
const spawned = Bun.spawn([real, ...launchArgs], {
|
|
294
|
-
stdin: "inherit",
|
|
381
|
+
stdin: relay === null ? "inherit" : "pipe",
|
|
295
382
|
stdout: "inherit",
|
|
296
383
|
stderr: "inherit",
|
|
297
384
|
env: {
|
|
@@ -302,6 +389,8 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
302
389
|
...(picked ? { CLAUDE_SECURESTORAGE_CONFIG_DIR: storeDirFor(picked.id) } : {}),
|
|
303
390
|
},
|
|
304
391
|
});
|
|
392
|
+
if (relay !== null) relay.attach(spawned.stdin!, firstLine);
|
|
393
|
+
firstLine = null;
|
|
305
394
|
if (picked) await recordPresence(spawned, sid, picked, savedTermios);
|
|
306
395
|
return { child: spawned, seat: picked };
|
|
307
396
|
});
|
|
@@ -317,13 +406,18 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
317
406
|
const exited = child.exited.then(() => { done = true; return "exit" as const; });
|
|
318
407
|
try {
|
|
319
408
|
const winner = await Promise.race([exited, markerWatch.then((m) => (m ? "marker" : "exit"))]);
|
|
320
|
-
if (winner === "marker")
|
|
409
|
+
if (winner === "marker") {
|
|
410
|
+
relay?.detach();
|
|
411
|
+
child.kill();
|
|
412
|
+
}
|
|
321
413
|
} catch (e) {
|
|
414
|
+
relay?.detach();
|
|
322
415
|
child.kill();
|
|
323
416
|
clearPresence({ dir: paths.presenceDir, id: sid });
|
|
324
417
|
throw e;
|
|
325
418
|
} finally {
|
|
326
419
|
await child.exited;
|
|
420
|
+
relay?.detach();
|
|
327
421
|
done = true;
|
|
328
422
|
await markerWatch.catch(() => {});
|
|
329
423
|
restoreTermios(savedTermios);
|
|
@@ -334,22 +428,27 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
334
428
|
rmSync(marker, { force: true });
|
|
335
429
|
respawns++;
|
|
336
430
|
const label = loadAccounts(claudePool).accounts.find((a) => a.id === m.accountId)?.label ?? m.accountId.slice(0, 8);
|
|
337
|
-
|
|
338
|
-
|
|
431
|
+
const resumable = existsSync(transcriptPath(m.sessionId));
|
|
432
|
+
let compacted = false;
|
|
433
|
+
if (m.compact && seat && resumable) {
|
|
434
|
+
process.stderr.write(`\n\x1b[36m↻ tokenmaxxing: compacting the conversation on ${seat.label} before the move...\x1b[0m\n`);
|
|
339
435
|
const compactEnv: Record<string, string | undefined> = { ...childEnv, TOKENMAXXING_PROBE: "1", CLAUDE_SECURESTORAGE_CONFIG_DIR: storeDirFor(seat.id) };
|
|
340
436
|
delete compactEnv.TOKENMAXXING_SUPERVISED;
|
|
341
437
|
delete compactEnv.TOKENMAXXING_SESSION_ID;
|
|
342
438
|
delete compactEnv.TOKENMAXXING_LAUNCHED_AT;
|
|
343
439
|
const outcome = await compactClaudeSession({ real, sid: m.sessionId, env: compactEnv });
|
|
344
440
|
log("supervisor.compact", { sid: m.sessionId.slice(0, 8), seat: seat.id.slice(0, 8), ok: outcome.ok, reason: outcome.ok ? undefined : outcome.reason });
|
|
345
|
-
if (!outcome.ok) process.
|
|
441
|
+
if (!outcome.ok) process.stderr.write(`\x1b[33m compaction did not land (${outcome.reason}) - resuming with the full context\x1b[0m\n`);
|
|
442
|
+
compacted = outcome.ok;
|
|
346
443
|
}
|
|
347
444
|
if (m.waitUntil > Date.now()) {
|
|
348
445
|
if (await countdownWait(label, m.waitUntil)) overriddenUntil = m.waitUntil;
|
|
349
|
-
} else process.
|
|
446
|
+
} else process.stderr.write(`\n\x1b[36m↻ tokenmaxxing: moving to ${label} - resuming...\x1b[0m\n`);
|
|
350
447
|
wanted = m.accountId;
|
|
351
448
|
saveSessionFlags(m.sessionId, persistable, process.cwd());
|
|
352
|
-
|
|
449
|
+
const prompt = resumable ? resumePrompt(compacted) : null;
|
|
450
|
+
firstLine = relay !== null && prompt !== null ? userLine(prompt) : null;
|
|
451
|
+
launchArgs = ["--resume", m.sessionId, ...(relay === null && prompt !== null ? [prompt] : []), ...persistable];
|
|
353
452
|
continue;
|
|
354
453
|
}
|
|
355
454
|
clearPresence({ dir: paths.presenceDir, id: sid });
|