tokenmaxxing 1.8.0 → 1.9.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.
Files changed (85) hide show
  1. package/DESIGN.md +2 -4
  2. package/README.md +1 -1
  3. package/agent-plugin/plugin.json +1 -1
  4. package/package.json +1 -1
  5. package/src/cli/add.ts +1 -8
  6. package/src/cli/auth.ts +0 -23
  7. package/src/cli/check.ts +19 -13
  8. package/src/cli/codexadd.ts +0 -17
  9. package/src/cli/codexinit.ts +0 -40
  10. package/src/cli/codexrm.ts +0 -13
  11. package/src/cli/codexswitch.ts +0 -15
  12. package/src/cli/config.ts +0 -30
  13. package/src/cli/doctor.ts +1 -14
  14. package/src/cli/init.ts +1 -33
  15. package/src/cli/ls.ts +0 -2
  16. package/src/cli/onboard.ts +0 -37
  17. package/src/cli/rename.ts +0 -19
  18. package/src/cli/render.ts +0 -23
  19. package/src/cli/rm.ts +0 -19
  20. package/src/cli/status.ts +0 -80
  21. package/src/cli/switch.ts +1 -49
  22. package/src/cli/watch.ts +0 -17
  23. package/src/entries/codexstophook.ts +2 -73
  24. package/src/entries/codexsupervisor.ts +1 -67
  25. package/src/entries/mcp.ts +0 -11
  26. package/src/entries/sessionstart.ts +1 -8
  27. package/src/entries/statusline.ts +0 -66
  28. package/src/entries/stopfailurehook.ts +93 -0
  29. package/src/entries/stophook.ts +3 -34
  30. package/src/entries/subagentstatusline.ts +0 -19
  31. package/src/entries/supervisor.ts +32 -132
  32. package/src/lib/atomic.ts +0 -16
  33. package/src/lib/claudebin.ts +4 -55
  34. package/src/lib/claudejson.ts +0 -10
  35. package/src/lib/claudelock.ts +13 -35
  36. package/src/lib/codexauth.ts +0 -29
  37. package/src/lib/codexbin.ts +0 -10
  38. package/src/lib/codexdecide.ts +1 -112
  39. package/src/lib/codexoauth.ts +0 -16
  40. package/src/lib/codexpick.ts +0 -31
  41. package/src/lib/codexpresence.ts +0 -35
  42. package/src/lib/codexsample.ts +0 -23
  43. package/src/lib/codexstate.ts +0 -7
  44. package/src/lib/codexswap.ts +0 -32
  45. package/src/lib/codexusage.ts +0 -28
  46. package/src/lib/credstore.ts +0 -24
  47. package/src/lib/decide.ts +127 -180
  48. package/src/lib/http.ts +0 -9
  49. package/src/lib/install.ts +6 -127
  50. package/src/lib/keychain.ts +1 -39
  51. package/src/lib/lock.ts +0 -24
  52. package/src/lib/log.ts +0 -14
  53. package/src/lib/oauth.ts +1 -31
  54. package/src/lib/paths.ts +1 -48
  55. package/src/lib/picker.ts +1 -84
  56. package/src/lib/proc.ts +0 -17
  57. package/src/lib/sample.ts +0 -68
  58. package/src/lib/sessions.ts +0 -13
  59. package/src/lib/settings.ts +15 -42
  60. package/src/lib/state.ts +23 -77
  61. package/src/lib/swap.ts +3 -87
  62. package/src/lib/tty.ts +0 -4
  63. package/src/lib/types.ts +13 -140
  64. package/src/lib/usage.ts +108 -196
  65. package/src/lib/worktree.ts +0 -8
  66. package/src/main.ts +5 -40
  67. package/src/sdk.ts +0 -59
  68. package/agent-plugin/agents/tokenmaxxing-claude.md +0 -43
  69. package/agent-plugin/agents/tokenmaxxing-codex.md +0 -40
  70. package/agent-plugin/hooks/cursor-relay.json +0 -14
  71. package/agent-plugin/skills/relay-session/SKILL.md +0 -118
  72. package/agent-plugin/skills/relay-session/references/ipc.md +0 -23
  73. package/src/cli/relay.ts +0 -323
  74. package/src/entries/relaypermission.ts +0 -105
  75. package/src/lib/relay/config.ts +0 -84
  76. package/src/lib/relay/decide.ts +0 -75
  77. package/src/lib/relay/gc.ts +0 -80
  78. package/src/lib/relay/install.ts +0 -143
  79. package/src/lib/relay/markers.ts +0 -148
  80. package/src/lib/relay/modes.ts +0 -82
  81. package/src/lib/relay/protocol.ts +0 -61
  82. package/src/lib/relay/registry.ts +0 -175
  83. package/src/lib/relay/tmux.ts +0 -109
  84. package/src/lib/relay/turn.ts +0 -137
  85. package/src/lib/relay/worker.ts +0 -141
@@ -1,82 +0,0 @@
1
- // Claude Code permission-mode vocabulary for the relay worker, plus the Codex
2
- // sandbox / ask-for-approval mapping. Host terms only: never invent alternate
3
- // security labels. `manual` aliases Claude's `default` (≥2.1.200 UI name).
4
-
5
- import { z } from "zod";
6
-
7
- export const ClaudePermissionModes = [
8
- "default",
9
- "acceptEdits",
10
- "plan",
11
- "auto",
12
- "dontAsk",
13
- "bypassPermissions",
14
- ] as const;
15
-
16
- export const ClaudePermissionModeSchema = z.enum(ClaudePermissionModes);
17
- export type ClaudePermissionMode = z.infer<typeof ClaudePermissionModeSchema>;
18
-
19
- const AliasSchema = z.enum(["manual", "yolo", "dangerous"]);
20
-
21
- /** Parse a host/CLI permission-mode token into a Claude mode. */
22
- export function parsePermissionMode(input: { raw: string }): ClaudePermissionMode {
23
- const trimmed = input.raw.trim();
24
- const alias = AliasSchema.safeParse(trimmed);
25
- if (alias.success) {
26
- if (alias.data === "manual") return "default";
27
- return "bypassPermissions";
28
- }
29
- return ClaudePermissionModeSchema.parse(trimmed);
30
- }
31
-
32
- export function tryParsePermissionMode(input: { raw: string }): ClaudePermissionMode | null {
33
- try {
34
- return parsePermissionMode(input);
35
- } catch {
36
- return null;
37
- }
38
- }
39
-
40
- export type CodexAskForApproval = "untrusted" | "on-failure" | "on-request" | "never";
41
-
42
- export type CodexSpawnFlags = {
43
- sandbox: "read-only" | "workspace-write" | "danger-full-access";
44
- askForApproval: CodexAskForApproval;
45
- };
46
-
47
- /** Map a Claude permission-mode name onto Codex CLI flags. */
48
- export function codexFlagsForMode(input: { mode: ClaudePermissionMode }): CodexSpawnFlags {
49
- switch (input.mode) {
50
- case "plan":
51
- case "default":
52
- return { sandbox: "read-only", askForApproval: "on-request" };
53
- case "acceptEdits":
54
- return { sandbox: "workspace-write", askForApproval: "on-request" };
55
- case "auto":
56
- return { sandbox: "workspace-write", askForApproval: "on-request" };
57
- case "dontAsk":
58
- return { sandbox: "read-only", askForApproval: "never" };
59
- case "bypassPermissions":
60
- return { sandbox: "danger-full-access", askForApproval: "never" };
61
- }
62
- }
63
-
64
- /** Claude argv fragment for a permission mode (includes allow-dangerously when needed). */
65
- export function claudeArgvForMode(input: { mode: ClaudePermissionMode }): string[] {
66
- const args = ["--permission-mode", input.mode];
67
- if (input.mode === "bypassPermissions") {
68
- args.push("--dangerously-skip-permissions");
69
- }
70
- return args;
71
- }
72
-
73
- /** Codex argv fragment for a Claude-named mode. */
74
- export function codexArgvForMode(input: { mode: ClaudePermissionMode }): string[] {
75
- const flags = codexFlagsForMode(input);
76
- return ["--sandbox", flags.sandbox, "--ask-for-approval", flags.askForApproval];
77
- }
78
-
79
- /** Under bypassPermissions, worker permission pings must not surface to main. */
80
- export function permissionPingsEnabled(input: { mode: ClaudePermissionMode }): boolean {
81
- return input.mode !== "bypassPermissions";
82
- }
@@ -1,61 +0,0 @@
1
- // Shared stdout contract for cheap host relay agents.
2
-
3
- import type { ClaudePermissionMode } from "./modes.ts";
4
-
5
- export type TurnOutput = {
6
- sessionId: string;
7
- permissionMode: ClaudePermissionMode;
8
- kind: "turn-done";
9
- text: string;
10
- };
11
-
12
- export type PermissionNeededOutput = {
13
- sessionId: string;
14
- permissionMode: ClaudePermissionMode;
15
- kind: "permission-needed";
16
- requestId: string;
17
- summary: string;
18
- detail: string;
19
- };
20
-
21
- export type RelayStdout = TurnOutput | PermissionNeededOutput;
22
-
23
- export function formatRelayStdout(input: { payload: RelayStdout }): string {
24
- const p = input.payload;
25
- const lines = [
26
- `session: ${p.sessionId}`,
27
- `permission-mode: ${p.permissionMode}`,
28
- ];
29
- if (p.kind === "permission-needed") {
30
- lines.push(`permission-needed: ${p.requestId}`);
31
- lines.push(`summary: ${p.summary}`);
32
- lines.push(`detail: ${p.detail}`);
33
- lines.push(`session: ${p.sessionId}`);
34
- lines.push(`permission-mode: ${p.permissionMode}`);
35
- } else if (p.text.trim() !== "") {
36
- lines.push(p.text.replace(/\s+$/, ""));
37
- }
38
- return lines.join("\n") + "\n";
39
- }
40
-
41
- export function parseRelayStdout(input: { text: string }): {
42
- sessionId: string | null;
43
- permissionMode: string | null;
44
- requestId: string | null;
45
- summary: string | null;
46
- detail: string | null;
47
- } {
48
- let sessionId: string | null = null;
49
- let permissionMode: string | null = null;
50
- let requestId: string | null = null;
51
- let summary: string | null = null;
52
- let detail: string | null = null;
53
- for (const line of input.text.split("\n")) {
54
- if (line.startsWith("session: ")) sessionId = line.slice("session: ".length).trim();
55
- else if (line.startsWith("permission-mode: ")) permissionMode = line.slice("permission-mode: ".length).trim();
56
- else if (line.startsWith("permission-needed: ")) requestId = line.slice("permission-needed: ".length).trim();
57
- else if (line.startsWith("summary: ")) summary = line.slice("summary: ".length);
58
- else if (line.startsWith("detail: ")) detail = line.slice("detail: ".length);
59
- }
60
- return { sessionId, permissionMode, requestId, summary, detail };
61
- }
@@ -1,175 +0,0 @@
1
- // Per-session registry under $TOKENMAXXING_HOME/relay/sessions/<id>.json.
2
- // Per-session flock only (high churn); never a global relay lock for turns.
3
-
4
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
5
- import { join } from "node:path";
6
- import { z } from "zod";
7
- import { paths } from "../paths.ts";
8
- import { writeFileAtomic } from "../atomic.ts";
9
- import { withLock } from "../lock.ts";
10
- import { ClaudePermissionModeSchema, type ClaudePermissionMode } from "./modes.ts";
11
- import { loadRelayConfig, type RelayWorker } from "./config.ts";
12
-
13
- export const RelaySessionStateSchema = z.enum([
14
- "idle",
15
- "running",
16
- "permission-needed",
17
- "destroyed",
18
- ]);
19
- export type RelaySessionState = z.infer<typeof RelaySessionStateSchema>;
20
-
21
- export const RelayRegistryEntrySchema = z.object({
22
- sessionId: z.uuid(),
23
- tmuxName: z.string().min(1),
24
- worker: z.enum(["claude", "codex"]),
25
- permissionMode: ClaudePermissionModeSchema,
26
- cwd: z.string().min(1),
27
- state: RelaySessionStateSchema,
28
- createdAt: z.number().int().nonnegative(),
29
- lastActiveAt: z.number().int().nonnegative(),
30
- pendingRequestId: z.string().min(1).optional(),
31
- });
32
- export type RelayRegistryEntry = z.infer<typeof RelayRegistryEntrySchema>;
33
-
34
- export function relaySessionsDir(): string {
35
- return join(paths.relayDir, "sessions");
36
- }
37
-
38
- export function relayLocksDir(): string {
39
- return join(paths.relayDir, "locks");
40
- }
41
-
42
- export function relayTurnDoneDir(): string {
43
- return join(paths.relayDir, "turn-done");
44
- }
45
-
46
- export function relayPendingDir(): string {
47
- return join(paths.relayDir, "pending");
48
- }
49
-
50
- export function relayDecisionsDir(): string {
51
- return join(paths.relayDir, "decisions");
52
- }
53
-
54
- export function sessionEntryPath(input: { sessionId: string }): string {
55
- return join(relaySessionsDir(), `${input.sessionId}.json`);
56
- }
57
-
58
- export function sessionLockPath(input: { sessionId: string }): string {
59
- return join(relayLocksDir(), input.sessionId);
60
- }
61
-
62
- export function turnDonePath(input: { sessionId: string }): string {
63
- return join(relayTurnDoneDir(), input.sessionId);
64
- }
65
-
66
- export function pendingRequestPath(input: { sessionId: string; requestId: string }): string {
67
- return join(relayPendingDir(), input.sessionId, `${input.requestId}.json`);
68
- }
69
-
70
- export function decisionPath(input: { sessionId: string; requestId: string }): string {
71
- return join(relayDecisionsDir(), input.sessionId, `${input.requestId}.json`);
72
- }
73
-
74
- export function tmuxNameFor(input: { sessionId: string; prefix?: string }): string {
75
- const prefix = input.prefix ?? loadRelayConfig().sessionPrefix;
76
- return `${prefix}${input.sessionId}`;
77
- }
78
-
79
- export function newSessionId(): string {
80
- return crypto.randomUUID();
81
- }
82
-
83
- export function readEntry(input: { sessionId: string }): RelayRegistryEntry | null {
84
- const path = sessionEntryPath(input);
85
- if (!existsSync(path)) return null;
86
- return RelayRegistryEntrySchema.parse(JSON.parse(readFileSync(path, "utf8")));
87
- }
88
-
89
- export function writeEntry(input: { entry: RelayRegistryEntry }): void {
90
- mkdirSync(relaySessionsDir(), { recursive: true });
91
- writeFileAtomic(sessionEntryPath({ sessionId: input.entry.sessionId }), JSON.stringify(input.entry, null, 2) + "\n");
92
- }
93
-
94
- export function deleteEntry(input: { sessionId: string }): void {
95
- rmSync(sessionEntryPath(input), { force: true });
96
- }
97
-
98
- export async function withSessionLock<T>(input: {
99
- sessionId: string;
100
- fn: () => Promise<T> | T;
101
- }): Promise<T> {
102
- mkdirSync(relayLocksDir(), { recursive: true });
103
- return withLock(sessionLockPath({ sessionId: input.sessionId }), input.fn);
104
- }
105
-
106
- export function listEntries(): RelayRegistryEntry[] {
107
- const dir = relaySessionsDir();
108
- if (!existsSync(dir)) return [];
109
- const out: RelayRegistryEntry[] = [];
110
- for (const name of readdirSync(dir)) {
111
- if (!name.endsWith(".json")) continue;
112
- const path = join(dir, name);
113
- try {
114
- out.push(RelayRegistryEntrySchema.parse(JSON.parse(readFileSync(path, "utf8"))));
115
- } catch {
116
- // skip corrupt; gc can reap
117
- }
118
- }
119
- return out;
120
- }
121
-
122
- export function createEntry(input: {
123
- sessionId: string;
124
- worker: RelayWorker;
125
- permissionMode: ClaudePermissionMode;
126
- cwd: string;
127
- now?: number;
128
- }): RelayRegistryEntry {
129
- const now = input.now ?? Date.now();
130
- const entry = RelayRegistryEntrySchema.parse({
131
- sessionId: input.sessionId,
132
- tmuxName: tmuxNameFor({ sessionId: input.sessionId }),
133
- worker: input.worker,
134
- permissionMode: input.permissionMode,
135
- cwd: input.cwd,
136
- state: "idle",
137
- createdAt: now,
138
- lastActiveAt: now,
139
- });
140
- writeEntry({ entry });
141
- return entry;
142
- }
143
-
144
- export function touchEntry(input: {
145
- sessionId: string;
146
- state?: RelaySessionState;
147
- permissionMode?: ClaudePermissionMode;
148
- pendingRequestId?: string | null;
149
- now?: number;
150
- }): RelayRegistryEntry {
151
- const prev = readEntry({ sessionId: input.sessionId });
152
- if (prev == null) throw new Error(`relay session not found: ${input.sessionId}`);
153
- const next = RelayRegistryEntrySchema.parse({
154
- ...prev,
155
- state: input.state ?? prev.state,
156
- permissionMode: input.permissionMode ?? prev.permissionMode,
157
- lastActiveAt: input.now ?? Date.now(),
158
- pendingRequestId: input.pendingRequestId === null
159
- ? undefined
160
- : (input.pendingRequestId ?? prev.pendingRequestId),
161
- });
162
- writeEntry({ entry: next });
163
- return next;
164
- }
165
-
166
- /** True when a registry entry exists for this relay session id. */
167
- export function registryHas(input: { sessionId: string }): boolean {
168
- return readEntry(input) != null;
169
- }
170
-
171
- export function entryMtimeMs(input: { sessionId: string }): number | null {
172
- const path = sessionEntryPath(input);
173
- if (!existsSync(path)) return null;
174
- return statSync(path).mtimeMs;
175
- }
@@ -1,109 +0,0 @@
1
- // Thin tmux wrapper. Exact session names only; never pattern-kill. Injectable
2
- // for hermetic tests that must not require a real tmux server or workers.
3
-
4
- import { z } from "zod";
5
-
6
- export type TmuxBackend = {
7
- hasSession: (input: { name: string }) => boolean;
8
- newSession: (input: { name: string; cwd: string; command: string }) => void;
9
- killSession: (input: { name: string }) => void;
10
- sendKeys: (input: { name: string; text: string; enter?: boolean }) => void;
11
- capturePane: (input: { name: string }) => string;
12
- listSessions: () => string[];
13
- };
14
-
15
- function runTmux(args: string[]): { ok: boolean; stdout: string; stderr: string; code: number } {
16
- const res = Bun.spawnSync(["tmux", ...args], {
17
- stdout: "pipe",
18
- stderr: "pipe",
19
- env: process.env,
20
- });
21
- return {
22
- ok: res.exitCode === 0,
23
- code: res.exitCode ?? 1,
24
- stdout: res.stdout.toString(),
25
- stderr: res.stderr.toString(),
26
- };
27
- }
28
-
29
- export const liveTmux: TmuxBackend = {
30
- hasSession: ({ name }) => runTmux(["has-session", "-t", name]).ok,
31
- newSession: ({ name, cwd, command }) => {
32
- const res = runTmux(["new-session", "-d", "-s", name, "-c", cwd, command]);
33
- if (!res.ok) throw new Error(`tmux new-session failed for ${name}: ${res.stderr.trim() || res.stdout.trim()}`);
34
- },
35
- killSession: ({ name }) => {
36
- // Exact name only. Missing session is success for destroy/gc idempotence.
37
- runTmux(["kill-session", "-t", name]);
38
- },
39
- sendKeys: ({ name, text, enter = true }) => {
40
- const res = runTmux(["send-keys", "-t", name, "-l", "--", text]);
41
- if (!res.ok) throw new Error(`tmux send-keys failed for ${name}: ${res.stderr.trim()}`);
42
- if (enter) {
43
- const enterRes = runTmux(["send-keys", "-t", name, "Enter"]);
44
- if (!enterRes.ok) throw new Error(`tmux send-keys Enter failed for ${name}: ${enterRes.stderr.trim()}`);
45
- }
46
- },
47
- capturePane: ({ name }) => {
48
- const res = runTmux(["capture-pane", "-p", "-t", name, "-S", "-200"]);
49
- if (!res.ok) return "";
50
- return res.stdout;
51
- },
52
- listSessions: () => {
53
- const res = runTmux(["list-sessions", "-F", "#{session_name}"]);
54
- if (!res.ok) return [];
55
- return res.stdout.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
56
- },
57
- };
58
-
59
- let backend: TmuxBackend = liveTmux;
60
-
61
- export function getTmux(): TmuxBackend {
62
- return backend;
63
- }
64
-
65
- /** Test-only: replace the tmux backend. */
66
- export function setTmuxBackend(input: { backend: TmuxBackend }): void {
67
- backend = input.backend;
68
- }
69
-
70
- export function resetTmuxBackend(): void {
71
- backend = liveTmux;
72
- }
73
-
74
- const MemorySessionSchema = z.object({
75
- name: z.string(),
76
- cwd: z.string(),
77
- command: stringOrEmpty(),
78
- keys: z.array(z.string()).default([]),
79
- pane: z.string().default(""),
80
- });
81
-
82
- function stringOrEmpty() {
83
- return z.string();
84
- }
85
-
86
- /** In-memory tmux for hermetic tests. */
87
- export function createMemoryTmux(): TmuxBackend & { sessions: Map<string, z.infer<typeof MemorySessionSchema>> } {
88
- const sessions = new Map<string, z.infer<typeof MemorySessionSchema>>();
89
- const api: TmuxBackend & { sessions: typeof sessions } = {
90
- sessions,
91
- hasSession: ({ name }) => sessions.has(name),
92
- newSession: ({ name, cwd, command }) => {
93
- if (sessions.has(name)) throw new Error(`tmux session already exists: ${name}`);
94
- sessions.set(name, MemorySessionSchema.parse({ name, cwd, command, keys: [], pane: "" }));
95
- },
96
- killSession: ({ name }) => {
97
- sessions.delete(name);
98
- },
99
- sendKeys: ({ name, text, enter = true }) => {
100
- const s = sessions.get(name);
101
- if (!s) throw new Error(`no tmux session: ${name}`);
102
- s.keys.push(enter ? `${text}\n` : text);
103
- s.pane += enter ? `${text}\n` : text;
104
- },
105
- capturePane: ({ name }) => sessions.get(name)?.pane ?? "",
106
- listSessions: () => [...sessions.keys()],
107
- };
108
- return api;
109
- }
@@ -1,137 +0,0 @@
1
- // relay turn: ensure session, send prompt, wait until turn-done OR permission-needed.
2
-
3
- import { delay } from "es-toolkit";
4
- import { loadRelayConfig } from "./config.ts";
5
- import {
6
- clearPendingRequest,
7
- clearTurnDoneMarker,
8
- listPendingRequests,
9
- readTurnDoneMarker,
10
- } from "./markers.ts";
11
- import { permissionPingsEnabled, type ClaudePermissionMode } from "./modes.ts";
12
- import { formatRelayStdout, type RelayStdout } from "./protocol.ts";
13
- import { readEntry, touchEntry, withSessionLock } from "./registry.ts";
14
- import { getTmux } from "./tmux.ts";
15
- import { ensureSession, sendPrompt } from "./worker.ts";
16
- import type { RelayWorker } from "./config.ts";
17
-
18
- const POLL_MS = 50;
19
-
20
- export type TurnParams = {
21
- sessionId?: string;
22
- worker?: RelayWorker;
23
- permissionMode?: ClaudePermissionMode;
24
- cwd: string;
25
- prompt?: string;
26
- /** When true, do not send a prompt; only wait for the next marker. */
27
- waitOnly?: boolean;
28
- timeoutMs?: number;
29
- now?: () => number;
30
- sleep?: (ms: number) => Promise<void>;
31
- };
32
-
33
- export type TurnResult = {
34
- exitCode: number;
35
- stdout: string;
36
- payload: RelayStdout;
37
- entrySessionId: string;
38
- };
39
-
40
- export async function runTurn(input: TurnParams): Promise<TurnResult> {
41
- const cfg = loadRelayConfig();
42
- const sleep = input.sleep ?? delay;
43
- const now = input.now ?? Date.now;
44
- const timeoutMs = input.timeoutMs ?? cfg.turnTimeoutMs;
45
-
46
- const entry = await ensureSession({
47
- sessionId: input.sessionId,
48
- worker: input.worker,
49
- permissionMode: input.permissionMode,
50
- cwd: input.cwd,
51
- now: now(),
52
- });
53
-
54
- if (!input.waitOnly) {
55
- const prompt = input.prompt ?? "";
56
- if (prompt.trim() === "") throw new Error("relay turn requires a prompt (argv or stdin)");
57
- await sendPrompt({ sessionId: entry.sessionId, prompt });
58
- } else {
59
- await withSessionLock({
60
- sessionId: entry.sessionId,
61
- fn: () => touchEntry({ sessionId: entry.sessionId, state: "running", now: now() }),
62
- });
63
- }
64
-
65
- const deadline = now() + timeoutMs;
66
- while (now() < deadline) {
67
- const pending = listPendingRequests({ sessionId: entry.sessionId });
68
- const live = readEntry({ sessionId: entry.sessionId });
69
- const mode = live?.permissionMode ?? entry.permissionMode;
70
- if (pending.length > 0 && permissionPingsEnabled({ mode })) {
71
- const req = pending[0]!;
72
- await withSessionLock({
73
- sessionId: entry.sessionId,
74
- fn: () => touchEntry({
75
- sessionId: entry.sessionId,
76
- state: "permission-needed",
77
- pendingRequestId: req.requestId,
78
- now: now(),
79
- }),
80
- });
81
- const payload: RelayStdout = {
82
- kind: "permission-needed",
83
- sessionId: entry.sessionId,
84
- permissionMode: mode,
85
- requestId: req.requestId,
86
- summary: req.summary,
87
- detail: req.detail,
88
- };
89
- return {
90
- exitCode: 0,
91
- stdout: formatRelayStdout({ payload }),
92
- payload,
93
- entrySessionId: entry.sessionId,
94
- };
95
- }
96
- // Under bypassPermissions, auto-clear any stray pending (should not fire).
97
- if (pending.length > 0 && !permissionPingsEnabled({ mode })) {
98
- for (const req of pending) {
99
- clearPendingRequest({ sessionId: entry.sessionId, requestId: req.requestId });
100
- }
101
- }
102
-
103
- const done = readTurnDoneMarker({ sessionId: entry.sessionId });
104
- if (done != null) {
105
- const text = getTmux().capturePane({ name: entry.tmuxName });
106
- clearTurnDoneMarker({ sessionId: entry.sessionId });
107
- await withSessionLock({
108
- sessionId: entry.sessionId,
109
- fn: () => touchEntry({
110
- sessionId: entry.sessionId,
111
- state: "idle",
112
- pendingRequestId: null,
113
- now: now(),
114
- }),
115
- });
116
- const payload: RelayStdout = {
117
- kind: "turn-done",
118
- sessionId: entry.sessionId,
119
- permissionMode: mode,
120
- text,
121
- };
122
- return {
123
- exitCode: 0,
124
- stdout: formatRelayStdout({ payload }),
125
- payload,
126
- entrySessionId: entry.sessionId,
127
- };
128
- }
129
-
130
- await sleep(POLL_MS);
131
- }
132
-
133
- throw new Error(`relay turn timed out after ${timeoutMs}ms (session ${entry.sessionId})`);
134
- }
135
-
136
- // re-export for callers that type worker from turn
137
- export type { RelayWorker };