tokenmaxxing 1.62.0 → 1.64.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 +1 -1
- package/package.json +1 -1
- package/src/entries/stopfailurehook.ts +10 -9
- package/src/entries/stophook.ts +9 -9
- package/src/entries/supervisor.ts +3 -3
- package/src/lib/claude.ts +1 -2
- package/src/lib/paths.ts +1 -5
- package/src/lib/presence.ts +11 -0
- package/src/lib/proc.ts +25 -0
- package/src/lib/sessions.ts +110 -22
package/DESIGN.md
CHANGED
|
@@ -59,7 +59,7 @@ The Stop hook's stdin has no usage data, but the **statusLine does** (`rate_limi
|
|
|
59
59
|
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.
|
|
60
60
|
|
|
61
61
|
### 3.3 Move and depleted-pool pause (supervisor)
|
|
62
|
-
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 and from the supervisor's own seat watch, `false` from StopFailure: it runs `claude -p --resume <
|
|
62
|
+
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 and from the supervisor's own seat watch, `false` from StopFailure: it runs `claude -p --resume <live-id> /compact` under the old seat's store, bounded at five minutes, and a failure only prints why), and (when `waitUntil` is in the future) shows an interruptible countdown to the reset; then it relaunches `claude --resume <live-id>` (`--session-id <live-id>` when that id has no transcript yet, because `--resume` exits with `No conversation found` for it) with `CLAUDE_SECURESTORAGE_CONFIG_DIR` set to the target's store and rewrites the session's presence. The marker file is keyed by the launch id `<sid>`; the live id inside it is the session id the supervised process runs now, which `/clear` and an in-session `/resume` change. A hook adopts a differing stdin id as the live id (`current` in `sessions/<sid>.json`) only when its own process descends, through shell processes alone, from the presence record's pid; any other differing id is a nested session and writes nothing. The resumed process reads that store cold → runs on the target account, same conversation. The `--session-id` path is the exception: the cleared conversation has no prior turn, so it starts empty with no compaction and no first prompt. On the `--resume` path 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. In a terminal session the supervisor's own notices go to stderr. In a stream-json session (`--output-format stream-json`, read from the effective flags like the relay) there is no terminal and no statusline, so the supervisor writes each notice (the compaction and its failure, the move, the depleted-pool wait with its resume time, the resume) to stdout as a `system` line of subtype `informational` and level `warning`, the shape Claude Code emits for its own print-mode notices, which the harness renders in the conversation while the stream stays parseable; the per-second countdown ticks stay terminal-only, and the supervisor writes only while no child is running. The `SessionStart` hook (every source) re-runs the decision before the first turn and writes the same marker if the seat is already over its bar or the pool is depleted. The supervisor also watches its own seat every ten seconds while the child runs (account record plus tee, the same bars) and writes the marker itself when the seat is at or over a bar, so a move never depends on a hook firing: a harness session between turns whose background workflow burns the window, or a subagent refusal that stamped the wall, is moved within seconds. SIGTERM to the supervisor is forwarded to the child and the supervisor exits after it, so a harness that stops the wrapper (the Agent SDK does) never leaves an orphaned claude on the old seat. A marker whose `launchedAt` does not match the running child is discarded, and after a Ctrl-C on the countdown a later marker whose `waitUntil` falls inside the skipped wait is ignored. A marker that fails to parse is never deleted and never treated as absent: the supervisor stops the child, restores the terminal, and exits naming the marker path. In a stream-json session that fatal, the wrap-depth abort, the wrap-rate abort, and a failed presence write also go to stdout as the same `informational`/`warning` line.
|
|
63
63
|
|
|
64
64
|
### 3.4 Placement (supervisor, under the pool lock)
|
|
65
65
|
1. Fold every account's tee into the index.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokenmaxxing",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.64.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",
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { claude } from "../lib/claude.ts";
|
|
3
3
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
4
4
|
import { readStdin } from "../lib/proc.ts";
|
|
5
|
-
import { supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
5
|
+
import { adoptLiveSession, supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
6
6
|
import { loadConfig } from "../lib/state.ts";
|
|
7
7
|
import { classifyEnforcedLimit, findEnforcedRow, parseErrorBody, readTranscriptTail, type TranscriptRow } from "../lib/usage.ts";
|
|
8
8
|
import { paths } from "../lib/paths.ts";
|
|
@@ -39,15 +39,16 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
39
39
|
if (stdin.error !== undefined && stdin.error !== "rate_limit") return 0;
|
|
40
40
|
|
|
41
41
|
const stdinSid = stdin.session_id;
|
|
42
|
-
const session = supervisedSession();
|
|
43
42
|
const mainLoop = stdin.agent_id === undefined;
|
|
44
|
-
if (stdinSid != null && session != null && stdinSid !== session.sid) {
|
|
45
|
-
log("stopfailure.nested_session", { stdin: stdinSid.slice(0, 8), supervised: session.sid.slice(0, 8) });
|
|
46
|
-
return 0;
|
|
47
|
-
}
|
|
48
|
-
const canRespawn = session != null && mainLoop;
|
|
49
43
|
|
|
50
44
|
try {
|
|
45
|
+
const supervised = supervisedSession();
|
|
46
|
+
const session = supervised == null ? null : adoptLiveSession(supervised, stdinSid);
|
|
47
|
+
if (supervised != null && session == null) {
|
|
48
|
+
log("stopfailure.nested_session", { stdin: stdinSid?.slice(0, 8), supervised: supervised.live.slice(0, 8) });
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
const canRespawn = session != null && mainLoop;
|
|
51
52
|
const cfg = loadConfig();
|
|
52
53
|
const row = stdin.transcript_path
|
|
53
54
|
? await awaitEnforcedRow({ transcriptPath: stdin.transcript_path, lastAssistantMessage: stdin.last_assistant_message, now })
|
|
@@ -81,8 +82,8 @@ export async function runStopFailureHook(): Promise<number> {
|
|
|
81
82
|
|
|
82
83
|
const decision = await evaluateAndMaybeSwap(claude, now, canRespawn && enforced != null, enforced);
|
|
83
84
|
if (enforced && session && canRespawn && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
|
|
84
|
-
writeRespawnMarker({ session,
|
|
85
|
-
log("stopfailure.marker", { session: session.sid.slice(0, 8), account: decision.account.id.slice(0, 8), waitUntil: decision.waitUntil ?? now });
|
|
85
|
+
writeRespawnMarker({ session, accountId: decision.account.id, waitUntil: decision.waitUntil ?? now, compact: false });
|
|
86
|
+
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 });
|
|
86
87
|
} else {
|
|
87
88
|
log("stopfailure.decision", { reason: decision.reason, swapped: decision.swapped, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
|
|
88
89
|
}
|
package/src/entries/stophook.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { claude } from "../lib/claude.ts";
|
|
3
3
|
import { evaluateAndMaybeSwap } from "../lib/decide.ts";
|
|
4
4
|
import { readStdin } from "../lib/proc.ts";
|
|
5
|
-
import { supervisedSession, writeRespawnMarker } from "../lib/sessions.ts";
|
|
5
|
+
import { adoptLiveSession, 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
|
|
|
@@ -16,18 +16,18 @@ export async function runBoundaryHook(event: "stop" | "sessionstart"): Promise<n
|
|
|
16
16
|
|
|
17
17
|
const parsed = BoundaryStdin.safeParse(JsonTextSchema.safeParse(await readStdin()).data);
|
|
18
18
|
const stdin = parsed.success ? parsed.data : {};
|
|
19
|
-
const session = supervisedSession();
|
|
20
|
-
const nested = stdin.session_id != null && session != null && stdin.session_id !== session.sid;
|
|
21
|
-
if (nested) {
|
|
22
|
-
log(`${event}.nested_session`, { stdin: stdin.session_id?.slice(0, 8), supervised: session?.sid.slice(0, 8) });
|
|
23
|
-
return 0;
|
|
24
|
-
}
|
|
25
19
|
|
|
26
20
|
try {
|
|
21
|
+
const supervised = supervisedSession();
|
|
22
|
+
const session = supervised == null ? null : adoptLiveSession(supervised, stdin.session_id);
|
|
23
|
+
if (supervised != null && session == null) {
|
|
24
|
+
log(`${event}.nested_session`, { stdin: stdin.session_id?.slice(0, 8), supervised: supervised.live.slice(0, 8), source: stdin.source });
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
27
|
const decision = await evaluateAndMaybeSwap(claude, Date.now(), session != null);
|
|
28
28
|
if (session && decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
|
|
29
|
-
writeRespawnMarker({ session,
|
|
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) });
|
|
29
|
+
writeRespawnMarker({ session, accountId: decision.account.id, waitUntil: decision.waitUntil ?? Date.now(), compact: true });
|
|
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) });
|
|
31
31
|
}
|
|
32
32
|
} catch (e) {
|
|
33
33
|
log(`${event}.error`, { err: errorMessage(e) });
|
|
@@ -15,7 +15,7 @@ import { readLines } from "../lib/proc.ts";
|
|
|
15
15
|
import { teeObservation } from "../lib/sample.ts";
|
|
16
16
|
import { exitStatus, loopGuardTripped, raceMarkerOrExit, recordPresenceOrStop, runPassthrough } from "../lib/supervise.ts";
|
|
17
17
|
import { saveTermios } from "../lib/tty.ts";
|
|
18
|
-
import { loadSessionFlags, pruneStaleSessions, saveSessionFlags, writeRespawnMarker } from "../lib/sessions.ts";
|
|
18
|
+
import { liveSessionId, loadSessionFlags, pruneStaleSessions, saveSessionFlags, writeRespawnMarker } from "../lib/sessions.ts";
|
|
19
19
|
import { loadAccounts, loadConfig } from "../lib/state.ts";
|
|
20
20
|
import { gatedFamilies, modelFromFlag } from "../lib/usage.ts";
|
|
21
21
|
import { RespawnMarkerSchema, type Account, type Config, type ModelInfo } from "../lib/types.ts";
|
|
@@ -225,7 +225,7 @@ async function moveExhaustedSeat(seat: Account, sid: string, gate: MarkerGate, m
|
|
|
225
225
|
const decision = await evaluateAndMaybeSwap(claude, now, true, null, seat.id, families);
|
|
226
226
|
log("supervisor.seat_exhausted", { seat: seat.id.slice(0, 8), until, reason: decision.reason, account: decision.account?.id.slice(0, 8), waitUntil: decision.waitUntil });
|
|
227
227
|
if (decision.account && (decision.swapped || decision.waitUntil !== undefined)) {
|
|
228
|
-
writeRespawnMarker({ session: { sid, launchedAt: gate.launchedAt
|
|
228
|
+
writeRespawnMarker({ session: { sid, launchedAt: gate.launchedAt, live: liveSessionId(sid) }, accountId: decision.account.id, waitUntil: decision.waitUntil ?? now, compact: true });
|
|
229
229
|
}
|
|
230
230
|
} catch (e) {
|
|
231
231
|
log("supervisor.seat_watch_error", { err: errorMessage(e) });
|
|
@@ -508,7 +508,7 @@ export async function runSupervisor(argv: string[]): Promise<number> {
|
|
|
508
508
|
saveSessionFlags(m.sessionId, persistable, process.cwd());
|
|
509
509
|
const prompt = resumable ? resumePrompt(compacted) : null;
|
|
510
510
|
firstLine = relay !== null && prompt !== null ? userLine(prompt) : null;
|
|
511
|
-
launchArgs = ["--resume", m.sessionId, ...(relay === null && prompt !== null ? [prompt] : []), ...persistable];
|
|
511
|
+
launchArgs = [resumable ? "--resume" : "--session-id", m.sessionId, ...(relay === null && prompt !== null ? [prompt] : []), ...persistable];
|
|
512
512
|
continue;
|
|
513
513
|
}
|
|
514
514
|
clearPresence({ dir: paths.presenceDir, id: sid });
|
package/src/lib/claude.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { ensurePathInRc, installSupervisor, managedShellRcSkipLines, shellRcPath
|
|
|
8
8
|
import { withLock } from "./lock.ts";
|
|
9
9
|
import { errorMessage, log } from "./log.ts";
|
|
10
10
|
import { claudeTierLabel, describeIdentity, fetchTokenIdentity, isDeadCredential, InvalidGrantError } from "./oauth.ts";
|
|
11
|
-
import { claudePool, env, paths,
|
|
11
|
+
import { claudePool, env, paths, seatFromEnv, storeDirFor } from "./paths.ts";
|
|
12
12
|
import { pickBest, pickEarliestReset, thresholdBars, type PickCtx } from "./picker.ts";
|
|
13
13
|
import { seatCounts } from "./presence.ts";
|
|
14
14
|
import type { Observation, Provider, SampleReport } from "./provider.ts";
|
|
@@ -138,7 +138,6 @@ async function removeCredentials(a: Account): Promise<void> {
|
|
|
138
138
|
await deleteItem(storeTarget(a.id));
|
|
139
139
|
rmSync(storeDirFor(a.id), { recursive: true, force: true });
|
|
140
140
|
rmSync(`${storeDirFor(a.id)}.lock`, { recursive: true, force: true });
|
|
141
|
-
for (const dir of [sampleDirFor(a.id), sampleDirFor(a.id, "-tick")]) rmSync(dir, { recursive: true, force: true });
|
|
142
141
|
clearUsageSnapshot(a.id);
|
|
143
142
|
const setupTokens = loadSetupTokens();
|
|
144
143
|
if (setupTokens.tokens.some((t) => t.accountUuid === a.id)) {
|
package/src/lib/paths.ts
CHANGED
|
@@ -26,10 +26,10 @@ export const paths = {
|
|
|
26
26
|
supervisorLink: join(TM_HOME, "bin", "claude"),
|
|
27
27
|
logFile: join(TM_HOME, "tokenmaxxing.log"),
|
|
28
28
|
onboardDir: join(TM_HOME, "onboard"),
|
|
29
|
-
sampleDir: join(TM_HOME, "sample"),
|
|
30
29
|
storesDir: join(TM_HOME, "stores"),
|
|
31
30
|
setupTokensJson: join(TM_HOME, "setup-tokens.json"),
|
|
32
31
|
setupTokenDir: join(TM_HOME, "setup-token"),
|
|
32
|
+
cloudDir: join(TM_HOME, "cloud"),
|
|
33
33
|
cloudSessionsJson: join(TM_HOME, "cloud", "sessions.json"),
|
|
34
34
|
cloudWalledJson: join(TM_HOME, "cloud", "walled.json"),
|
|
35
35
|
cloudLockFile: join(TM_HOME, "cloud", "lock"),
|
|
@@ -139,10 +139,6 @@ export function storeDirFor(accountId: string): string {
|
|
|
139
139
|
return join(paths.storesDir, shortId(accountId));
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
export function sampleDirFor(accountId: string, suffix = ""): string {
|
|
143
|
-
return join(paths.sampleDir, `${shortId(accountId)}${suffix}`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
142
|
export function usageJsonFor(accountId: string): string {
|
|
147
143
|
return join(paths.usageDir, `${shortId(accountId)}.json`);
|
|
148
144
|
}
|
package/src/lib/presence.ts
CHANGED
|
@@ -18,6 +18,17 @@ export function writePresence(input: { dir: string; id: string; accountId: strin
|
|
|
18
18
|
writeFileAtomic(join(input.dir, input.id), JSON.stringify(PresenceSchema.parse({ accountId: input.accountId, pid: input.pid, startedAt })));
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export function presencePid(input: { dir: string; id: string }): number | null {
|
|
22
|
+
let raw: string;
|
|
23
|
+
try {
|
|
24
|
+
raw = readFileSync(join(input.dir, input.id), "utf8");
|
|
25
|
+
} catch (e) {
|
|
26
|
+
if (ErrnoSchema.safeParse(e).data?.code === "ENOENT") return null;
|
|
27
|
+
throw e;
|
|
28
|
+
}
|
|
29
|
+
return PresenceSchema.parse(JSON.parse(raw)).pid;
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
export function clearPresence(input: { dir: string; id: string }): void {
|
|
22
33
|
rmSync(join(input.dir, input.id), { force: true });
|
|
23
34
|
}
|
package/src/lib/proc.ts
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
1
2
|
import { ErrnoSchema } from "./types.ts";
|
|
2
3
|
|
|
4
|
+
const SHELLS = new Set(["sh", "bash", "dash", "zsh", "ksh", "fish"]);
|
|
5
|
+
const MAX_SHELL_HOPS = 4;
|
|
6
|
+
|
|
7
|
+
function parentAndName(pid: number): { ppid: number; comm: string } | null {
|
|
8
|
+
const res = Bun.spawnSync(["ps", "-p", String(pid), "-o", "ppid=", "-o", "comm="], { env: { ...process.env, LC_ALL: "C" } });
|
|
9
|
+
if (res.exitCode !== 0) return null;
|
|
10
|
+
const row = res.stdout.toString().trim();
|
|
11
|
+
const cut = row.indexOf(" ");
|
|
12
|
+
if (cut === -1) return null;
|
|
13
|
+
const ppid = Number(row.slice(0, cut));
|
|
14
|
+
return Number.isInteger(ppid) ? { ppid, comm: row.slice(cut).trim() } : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function spawnedThroughShellsBy(ancestor: number): boolean {
|
|
18
|
+
let cur = process.ppid;
|
|
19
|
+
for (let hop = 0; hop <= MAX_SHELL_HOPS; hop++) {
|
|
20
|
+
if (cur === ancestor) return true;
|
|
21
|
+
const proc = parentAndName(cur);
|
|
22
|
+
if (proc == null || !SHELLS.has(basename(proc.comm))) return false;
|
|
23
|
+
cur = proc.ppid;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
3
28
|
export function pidStartTime(pid: number): string | null {
|
|
4
29
|
const res = Bun.spawnSync(["ps", "-p", String(pid), "-o", "lstart="], { env: { ...process.env, LC_ALL: "C" } });
|
|
5
30
|
if (res.exitCode !== 0) return null;
|
package/src/lib/sessions.ts
CHANGED
|
@@ -1,54 +1,130 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, statSync, type Dirent } from "node:fs";
|
|
2
|
+
import { basename, join, sep } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import {
|
|
4
|
+
import { errorMessage, log } from "./log.ts";
|
|
5
|
+
import { codexPaths, grokPaths, opencodeGoPaths, paths } from "./paths.ts";
|
|
5
6
|
import { writeFileAtomic } from "./atomic.ts";
|
|
7
|
+
import { presencePid } from "./presence.ts";
|
|
8
|
+
import { spawnedThroughShellsBy } from "./proc.ts";
|
|
6
9
|
import type { RespawnMarkerSchema } from "./types.ts";
|
|
7
10
|
|
|
8
|
-
const SessionSchema = z.object({ flags: z.array(z.string()), cwd: z.string() });
|
|
11
|
+
const SessionSchema = z.object({ flags: z.array(z.string()), cwd: z.string(), current: z.uuid().optional() });
|
|
9
12
|
|
|
10
13
|
const SESSION_RETENTION_MS = 30 * 24 * 3600 * 1000;
|
|
11
14
|
|
|
15
|
+
const sessionsDir = (): string => join(paths.home, "sessions");
|
|
16
|
+
|
|
12
17
|
function sessionFile(sid: string): string {
|
|
13
|
-
return join(
|
|
18
|
+
return join(sessionsDir(), `${sid}.json`);
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
export function saveSessionFlags(sid: string, flags: string[], cwd: string): void {
|
|
17
|
-
mkdirSync(
|
|
22
|
+
mkdirSync(sessionsDir(), { recursive: true });
|
|
18
23
|
writeFileAtomic(sessionFile(sid), JSON.stringify({ flags, cwd }));
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
function loadSession(sid: string): z.infer<typeof SessionSchema> | null {
|
|
22
27
|
const f = sessionFile(sid);
|
|
23
28
|
if (!existsSync(f)) return null;
|
|
24
|
-
return SessionSchema.parse(JSON.parse(readFileSync(f, "utf8")))
|
|
29
|
+
return SessionSchema.parse(JSON.parse(readFileSync(f, "utf8")));
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
export function loadSessionFlags(sid: string): string[] | null {
|
|
33
|
+
return loadSession(sid)?.flags ?? null;
|
|
34
|
+
}
|
|
28
35
|
|
|
29
|
-
function
|
|
30
|
-
|
|
36
|
+
export function liveSessionId(sid: string): string {
|
|
37
|
+
return loadSession(sid)?.current ?? sid;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function recordLiveSession(sid: string, current: string): void {
|
|
41
|
+
const session = loadSession(sid);
|
|
42
|
+
mkdirSync(sessionsDir(), { recursive: true });
|
|
43
|
+
writeFileAtomic(sessionFile(sid), JSON.stringify({ flags: session?.flags ?? [], cwd: session?.cwd ?? process.cwd(), current }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const DEAD_STATE_ENTRIES = [
|
|
47
|
+
"model-usage.json",
|
|
48
|
+
"accounts.json.v1-backup",
|
|
49
|
+
"codex-accounts.json.v1-backup",
|
|
50
|
+
"nextcheck.json",
|
|
51
|
+
"usage.json",
|
|
52
|
+
"lastswap.json",
|
|
53
|
+
"depleted.json",
|
|
54
|
+
"codex-lastswap.json",
|
|
55
|
+
"creds",
|
|
56
|
+
"codex-creds",
|
|
57
|
+
"codex-reconcile",
|
|
58
|
+
"sample",
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const TMP_MARKER = ".tmp.";
|
|
62
|
+
const TMP_GRACE_MS = 3600 * 1000;
|
|
63
|
+
|
|
64
|
+
const STORE_PARENTS = [paths.storesDir, codexPaths.storesDir, grokPaths.storesDir, opencodeGoPaths.storesDir];
|
|
65
|
+
|
|
66
|
+
function listDir(dir: string, root: string): Dirent[] {
|
|
67
|
+
if (!existsSync(dir)) return [];
|
|
68
|
+
try {
|
|
69
|
+
const resolved = realpathSync(dir);
|
|
70
|
+
if (resolved !== root && !resolved.startsWith(root + sep)) return [];
|
|
71
|
+
return readdirSync(dir, { withFileTypes: true });
|
|
72
|
+
} catch (e) {
|
|
73
|
+
log("state.sweep_unreadable", { dir, err: errorMessage(e) });
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function tmpSweepDirs(root: string): string[] {
|
|
79
|
+
const dirs = [paths.home, paths.usageDir, paths.presenceDir, paths.respawnDir, sessionsDir(), paths.binDir, paths.cloudDir, codexPaths.presenceDir, codexPaths.respawnDir, codexPaths.onboardDir];
|
|
80
|
+
for (const parent of STORE_PARENTS) {
|
|
81
|
+
for (const child of listDir(parent, root)) {
|
|
82
|
+
if (child.isDirectory()) dirs.push(join(parent, child.name));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return dirs;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function pruneDeadState(now: number, root: string): void {
|
|
89
|
+
for (const name of DEAD_STATE_ENTRIES) {
|
|
31
90
|
try {
|
|
32
|
-
rmSync(join(paths.home, name), { force: true });
|
|
33
|
-
} catch {
|
|
91
|
+
rmSync(join(paths.home, name), { recursive: true, force: true });
|
|
92
|
+
} catch (e) {
|
|
93
|
+
log("state.dead_entry_failed", { name, err: errorMessage(e) });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const dir of tmpSweepDirs(root)) {
|
|
97
|
+
for (const f of listDir(dir, root)) {
|
|
98
|
+
if (!f.isFile() || !f.name.includes(TMP_MARKER)) continue;
|
|
99
|
+
const p = join(dir, f.name);
|
|
100
|
+
try {
|
|
101
|
+
if (now - statSync(p).mtimeMs > TMP_GRACE_MS) rmSync(p, { force: true });
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
34
104
|
}
|
|
35
105
|
}
|
|
36
106
|
}
|
|
37
107
|
|
|
38
108
|
export function pruneStaleSessions(now: number): void {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
109
|
+
let root: string;
|
|
110
|
+
try {
|
|
111
|
+
root = realpathSync(paths.home);
|
|
112
|
+
} catch {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
pruneDeadState(now, root);
|
|
116
|
+
const dir = sessionsDir();
|
|
117
|
+
for (const f of listDir(dir, root)) {
|
|
118
|
+
const p = join(dir, f.name);
|
|
44
119
|
try {
|
|
120
|
+
if (existsSync(join(paths.presenceDir, basename(f.name, ".json")))) continue;
|
|
45
121
|
if (now - statSync(p).mtimeMs > SESSION_RETENTION_MS) rmSync(p, { force: true });
|
|
46
122
|
} catch {
|
|
47
123
|
}
|
|
48
124
|
}
|
|
49
125
|
}
|
|
50
126
|
|
|
51
|
-
export type SupervisedSession = { sid: string; launchedAt: number | null };
|
|
127
|
+
export type SupervisedSession = { sid: string; launchedAt: number | null; live: string };
|
|
52
128
|
|
|
53
129
|
const LaunchedAtSchema = z.coerce.number().finite().optional().catch(undefined);
|
|
54
130
|
|
|
@@ -56,16 +132,28 @@ export function supervisedSession(env: Record<string, string | undefined> = proc
|
|
|
56
132
|
if (env.TOKENMAXXING_SUPERVISED !== "1") return null;
|
|
57
133
|
const sid = env.TOKENMAXXING_SESSION_ID;
|
|
58
134
|
if (sid == null || sid === "") return null;
|
|
59
|
-
return { sid, launchedAt: LaunchedAtSchema.parse(env.TOKENMAXXING_LAUNCHED_AT) ?? null };
|
|
135
|
+
return { sid, launchedAt: LaunchedAtSchema.parse(env.TOKENMAXXING_LAUNCHED_AT) ?? null, live: liveSessionId(sid) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function adoptLiveSession(session: SupervisedSession, stdinSid: string | undefined): SupervisedSession | null {
|
|
139
|
+
if (stdinSid == null || stdinSid === session.live) return session;
|
|
140
|
+
const child = presencePid({ dir: paths.presenceDir, id: session.sid });
|
|
141
|
+
if (child == null || !spawnedThroughShellsBy(child)) {
|
|
142
|
+
log("session.live_id_refused", { reason: child == null ? "no-presence" : "not-descendant", stdin: stdinSid.slice(0, 8), live: session.live.slice(0, 8), session: session.sid.slice(0, 8) });
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
recordLiveSession(session.sid, stdinSid);
|
|
146
|
+
log("session.live_id", { from: session.live.slice(0, 8), to: stdinSid.slice(0, 8), session: session.sid.slice(0, 8) });
|
|
147
|
+
return { ...session, live: stdinSid };
|
|
60
148
|
}
|
|
61
149
|
|
|
62
|
-
export function writeRespawnMarker(input: { session: SupervisedSession;
|
|
150
|
+
export function writeRespawnMarker(input: { session: SupervisedSession; accountId: string; waitUntil: number; compact: boolean }): void {
|
|
63
151
|
mkdirSync(paths.respawnDir, { recursive: true });
|
|
64
152
|
const payload: z.infer<typeof RespawnMarkerSchema> = {
|
|
65
153
|
accountId: input.accountId,
|
|
66
154
|
ts: Date.now(),
|
|
67
155
|
waitUntil: input.waitUntil,
|
|
68
|
-
sessionId: input.
|
|
156
|
+
sessionId: input.session.live,
|
|
69
157
|
compact: input.compact,
|
|
70
158
|
...(input.session.launchedAt != null ? { launchedAt: input.session.launchedAt } : {}),
|
|
71
159
|
};
|