From 836d6f8bf7f99a025fa1ccf53aaed80317123025 Mon Sep 17 00:00:00 2001 From: Axel Delafosse Date: Wed, 25 Mar 2026 14:00:54 -0700 Subject: [PATCH 1/2] Speed up paired tmux startup --- src/loop/tmux.ts | 72 +-------------- tests/loop/tmux.test.ts | 200 +++++++++++++++++++++++++++++++++------- 2 files changed, 171 insertions(+), 101 deletions(-) diff --git a/src/loop/tmux.ts b/src/loop/tmux.ts index a0741d7..5899c6a 100644 --- a/src/loop/tmux.ts +++ b/src/loop/tmux.ts @@ -46,15 +46,10 @@ const CLAUDE_EXIT_OPTION = "No, exit"; const CLAUDE_DEV_CHANNELS_PROMPT = "WARNING: Loading development channels"; const CLAUDE_DEV_CHANNELS_CONFIRM = "I am using this for local development"; const CLAUDE_CHANNEL_SCOPE = "local"; -const CLAUDE_PROMPT_INITIAL_POLLS = 8; +const CLAUDE_PROMPT_INITIAL_POLLS = 4; const CLAUDE_PROMPT_POLL_DELAY_MS = 250; const CLAUDE_PROMPT_SETTLE_POLLS = 2; -const CODEX_READY_POLL_DELAY_MS = 250; -const CODEX_READY_POLLS = 20; -const CODEX_SEND_FOOTER = "Ctrl+J newline"; const MCP_ALREADY_EXISTS_RE = /already exists/i; -const PROMPT_DISPATCH_DELAY_MS = 500; -const REVIEWER_BOOT_DELAY_MS = 1500; interface SpawnResult { exitCode: number; @@ -749,8 +744,6 @@ const detectClaudePrompt = (text: string): "bypass" | "confirm" | undefined => { return undefined; }; -const codexReady = (text: string): boolean => text.includes(CODEX_SEND_FOOTER); - const unblockClaudePane = async ( session: string, deps: TmuxDeps @@ -793,49 +786,6 @@ const unblockClaudePane = async ( } }; -const waitForCodexReady = async ( - session: string, - deps: TmuxDeps -): Promise => { - const pane = `${session}:0.1`; - for (let attempt = 0; attempt < CODEX_READY_POLLS; attempt += 1) { - if (codexReady(deps.capturePane(pane))) { - return; - } - await deps.sleep(CODEX_READY_POLL_DELAY_MS); - } -}; - -const seedPanePrompt = async ( - pane: string, - prompt: string, - deps: TmuxDeps -): Promise => { - const lines = prompt.split("\n"); - for (let index = 0; index < lines.length; index += 1) { - deps.sendText(pane, lines[index] ?? ""); - if (index < lines.length - 1) { - deps.sendKeys(pane, ["C-j"]); - } - } - await deps.sleep(100); - deps.sendKeys(pane, ["Enter"]); -}; - -const submitCodexPrompt = async ( - session: string, - prompt: string, - deps: TmuxDeps -): Promise => { - const pane = `${session}:0.1`; - await waitForCodexReady(session, deps); - await seedPanePrompt(pane, prompt, deps); - await deps.sleep(CODEX_READY_POLL_DELAY_MS); - if (codexReady(deps.capturePane(pane))) { - deps.sendKeys(pane, ["Enter"]); - } -}; - const startPairedSession = async ( deps: TmuxDeps, launch: PairedTmuxLaunch @@ -890,7 +840,8 @@ const startPairedSession = async ( ...buildCodexCommand( codexProxyUrl, resolveTmuxModel("codex", launch.opts), - launch.opts.codexMcpConfigArgs ?? [] + launch.opts.codexMcpConfigArgs ?? [], + hadCodexThread ? undefined : codexPrompt ), ]); @@ -927,25 +878,8 @@ const startPairedSession = async ( "even-horizontal", ]); await unblockClaudePane(session, deps); - await deps.sleep(PROMPT_DISPATCH_DELAY_MS); - const peerPane = - launch.opts.agent === "claude" ? `${session}:0.1` : `${session}:0.0`; const primaryPane = launch.opts.agent === "claude" ? `${session}:0.0` : `${session}:0.1`; - const peerPrompt = - launch.opts.agent === "claude" ? codexPrompt : claudePrompt; - const primaryPrompt = - launch.opts.agent === "claude" ? claudePrompt : codexPrompt; - - if (!hadCodexThread && peerPane.endsWith(":0.1")) { - await submitCodexPrompt(session, peerPrompt, deps); - } - if (!(hadClaudeSession && hadCodexThread)) { - await deps.sleep(REVIEWER_BOOT_DELAY_MS); - } - if (!hadCodexThread && primaryPane.endsWith(":0.1")) { - await submitCodexPrompt(session, primaryPrompt, deps); - } deps.spawn(["tmux", "select-pane", "-t", primaryPane]); return session; }; diff --git a/tests/loop/tmux.test.ts b/tests/loop/tmux.test.ts index 9637430..4e57d6c 100644 --- a/tests/loop/tmux.test.ts +++ b/tests/loop/tmux.test.ts @@ -312,7 +312,8 @@ test("runInTmux starts paired tmux panes for Claude and Codex", async () => { ...tmuxInternals.buildCodexCommand( codexProxyUrl, "test-model", - codexMcpConfigArgs + codexMcpConfigArgs, + tmuxInternals.buildPrimaryPrompt("Ship feature", opts, "1") ), ]); @@ -375,16 +376,7 @@ test("runInTmux starts paired tmux panes for Claude and Codex", async () => { ], ["tmux", "has-session", "-t", "repo-loop-1"], ]); - const typedByPane = new Map(); - for (const entry of typed) { - const values = typedByPane.get(entry.pane) ?? []; - values.push(entry.text); - typedByPane.set(entry.pane, values); - } - expect(typedByPane.get("repo-loop-1:0.0")).toBeUndefined(); - expect(typedByPane.get("repo-loop-1:0.1")?.join("\n")).toBe( - tmuxInternals.buildPrimaryPrompt("Ship feature", opts, "1") - ); + expect(typed).toEqual([]); expect(logs[0]).toBe( "[loop] starting paired tmux workspace. This can take a few seconds..." ); @@ -667,19 +659,97 @@ test("runInTmux starts paired interactive tmux panes without a task", async () = "/repo", claudeCommand, ]); - const typedByPane = new Map(); - for (const entry of typed) { - const values = typedByPane.get(entry.pane) ?? []; - values.push(entry.text); - typedByPane.set(entry.pane, values); - } - expect(typedByPane.get("repo-loop-1:0.0")).toBeUndefined(); - expect(typedByPane.get("repo-loop-1:0.1")?.join("\n")).toBe( - tmuxInternals.buildInteractivePrimaryPrompt(opts, "1") - ); + const codexCommand = tmuxInternals.buildShellCommand([ + "env", + ...env, + ...tmuxInternals.buildCodexCommand( + "ws://127.0.0.1:4600/", + "test-model", + ["-c", 'mcp_servers.loop-bridge.command="loop"'], + tmuxInternals.buildInteractivePrimaryPrompt(opts, "1") + ), + ]); + expect(calls[3]).toEqual([ + "tmux", + "split-window", + "-h", + "-t", + "repo-loop-1:0", + "-c", + "/repo", + codexCommand, + ]); + expect(typed).toEqual([]); expect(manifest.tmuxSession).toBe("repo-loop-1"); }); +test("runInTmux keeps the no-prompt Claude startup wait short", async () => { + const sleeps: number[] = []; + let sessionStarted = false; + const manifest = createRunManifest({ + cwd: "/repo", + mode: "paired", + pid: 1234, + repoId: "repo-123", + runId: "1", + status: "running", + }); + const storage = { + manifestPath: "/repo/.loop/runs/1/manifest.json", + repoId: "repo-123", + runDir: "/repo/.loop/runs/1", + runId: "1", + storageRoot: "/repo/.loop/runs", + transcriptPath: "/repo/.loop/runs/1/transcript.jsonl", + }; + + await runInTmux( + ["--tmux"], + { + capturePane: () => "", + cwd: "/repo", + env: {}, + findBinary: () => true, + getCodexAppServerUrl: () => "ws://127.0.0.1:4500", + getLastCodexThreadId: () => "codex-thread-1", + isInteractive: () => false, + launchArgv: ["bun", "/repo/src/cli.ts"], + log: (): void => undefined, + makeClaudeSessionId: () => "claude-session-1", + preparePairedRun: (nextOpts) => { + nextOpts.codexMcpConfigArgs = [ + "-c", + 'mcp_servers.loop-bridge.command="loop"', + ]; + return { manifest, storage }; + }, + sendKeys: (): void => undefined, + sendText: (): void => undefined, + sleep: (ms: number) => { + sleeps.push(ms); + return Promise.resolve(); + }, + startCodexProxy: () => Promise.resolve("ws://127.0.0.1:4600/"), + startPersistentAgentSession: () => Promise.resolve(undefined), + spawn: (args: string[]) => { + if (args[0] === "tmux" && args[1] === "has-session") { + return sessionStarted + ? { exitCode: 0, stderr: "" } + : { exitCode: 1, stderr: "" }; + } + if (args[0] === "tmux" && args[1] === "new-session") { + sessionStarted = true; + } + return { exitCode: 0, stderr: "" }; + }, + updateRunManifest: (_path, update) => update(manifest), + }, + { opts: makePairedOptions({ proof: "" }) } + ); + + expect(sleeps).toEqual([250, 250, 250]); +}); + test("tmux prompts keep the paired review workflow explicit", () => { const opts = makePairedOptions(); const primaryPrompt = tmuxInternals.buildPrimaryPrompt( @@ -860,17 +930,8 @@ test("runInTmux auto-confirms Claude startup prompts in paired mode", async () = call.keys.length === 1 && call.keys[0] === "Enter" ) - ).toBe(true); - const typedByPane = new Map(); - for (const entry of typed) { - const values = typedByPane.get(entry.pane) ?? []; - values.push(entry.text); - typedByPane.set(entry.pane, values); - } - expect(typedByPane.get("repo-loop-1:0.0")).toBeUndefined(); - expect(typedByPane.get("repo-loop-1:0.1")?.join("\n")).toBe( - tmuxInternals.buildPrimaryPrompt("Ship feature", opts, "1") - ); + ).toBe(false); + expect(typed).toEqual([]); }); test("runInTmux confirms wrapped Claude dev-channel prompts", async () => { @@ -1119,6 +1180,81 @@ test("runInTmux still confirms Claude trust prompts in paired mode", async () => }); }); +test("runInTmux still catches a delayed Claude trust prompt", async () => { + const keyCalls: Array<{ keys: string[]; pane: string }> = []; + let sessionStarted = false; + let pollCount = 0; + const manifest = createRunManifest({ + cwd: "/repo", + mode: "paired", + pid: 1234, + repoId: "repo-123", + runId: "1", + status: "running", + }); + const storage = { + manifestPath: "/repo/.loop/runs/1/manifest.json", + repoId: "repo-123", + runDir: "/repo/.loop/runs/1", + runId: "1", + storageRoot: "/repo/.loop/runs", + transcriptPath: "/repo/.loop/runs/1/transcript.jsonl", + }; + + await runInTmux( + ["--tmux", "--proof", "verify with tests"], + { + capturePane: () => { + pollCount += 1; + return pollCount === 4 + ? "Is this a project you created or one you trust?" + : ""; + }, + cwd: "/repo", + env: {}, + findBinary: () => true, + getCodexAppServerUrl: () => "ws://127.0.0.1:4500", + getLastCodexThreadId: () => "codex-thread-1", + isInteractive: () => false, + launchArgv: ["bun", "/repo/src/cli.ts"], + log: (): void => undefined, + makeClaudeSessionId: () => "claude-session-1", + preparePairedRun: (nextOpts) => { + nextOpts.codexMcpConfigArgs = [ + "-c", + 'mcp_servers.loop-bridge.command="loop"', + ]; + return { manifest, storage }; + }, + sendKeys: (pane: string, keys: string[]) => { + keyCalls.push({ keys, pane }); + }, + sendText: (): void => undefined, + sleep: () => Promise.resolve(), + startCodexProxy: () => Promise.resolve("ws://127.0.0.1:4600/"), + startPersistentAgentSession: () => Promise.resolve(undefined), + spawn: (args: string[]) => { + if (args[0] === "tmux" && args[1] === "has-session") { + return sessionStarted + ? { exitCode: 0, stderr: "" } + : { exitCode: 1, stderr: "" }; + } + if (args[0] === "tmux" && args[1] === "new-session") { + sessionStarted = true; + } + return { exitCode: 0, stderr: "" }; + }, + updateRunManifest: (_path, update) => update(manifest), + }, + { opts: makePairedOptions(), task: "Ship feature" } + ); + + expect(keyCalls).toContainEqual({ + keys: ["Enter"], + pane: "repo-loop-1:0.0", + }); +}); + test("runInTmux reopens paired tmux panes without replaying the task", async () => { const calls: string[][] = []; const typed: Array<{ pane: string; text: string }> = []; From 1d7398369fa832c5167dd3548ff369543e06a459 Mon Sep 17 00:00:00 2001 From: Axel Delafosse Date: Wed, 25 Mar 2026 14:08:14 -0700 Subject: [PATCH 2/2] Shorten tmux startup message --- src/loop/tmux.ts | 4 ++-- tests/loop/tmux.test.ts | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/loop/tmux.ts b/src/loop/tmux.ts index 5899c6a..b4dcc45 100644 --- a/src/loop/tmux.ts +++ b/src/loop/tmux.ts @@ -609,8 +609,8 @@ const buildSessionCommand = ( const tmuxStartupMessage = (paired: boolean): string => paired - ? "[loop] starting paired tmux workspace. This can take a few seconds..." - : "[loop] starting tmux session. This can take a few seconds..."; + ? "[loop] starting paired tmux workspace..." + : "[loop] starting tmux session..."; const updatePairedManifest = ( deps: TmuxDeps, diff --git a/tests/loop/tmux.test.ts b/tests/loop/tmux.test.ts index 4e57d6c..ac61a5b 100644 --- a/tests/loop/tmux.test.ts +++ b/tests/loop/tmux.test.ts @@ -131,9 +131,7 @@ test("runInTmux starts detached session and strips --tmux", async () => { "remain-on-exit", "on", ]); - expect(logs[0]).toBe( - "[loop] starting tmux session. This can take a few seconds..." - ); + expect(logs[0]).toBe("[loop] starting tmux session..."); expect(logs).toContain('[loop] started tmux session "repo-loop-1"'); expect(logs).toContain("[loop] attach with: tmux attach -t repo-loop-1"); expect(attaches).toEqual(["repo-loop-1"]); @@ -377,9 +375,7 @@ test("runInTmux starts paired tmux panes for Claude and Codex", async () => { ["tmux", "has-session", "-t", "repo-loop-1"], ]); expect(typed).toEqual([]); - expect(logs[0]).toBe( - "[loop] starting paired tmux workspace. This can take a few seconds..." - ); + expect(logs[0]).toBe("[loop] starting paired tmux workspace..."); expect(logs).toContain('[loop] started tmux session "repo-loop-1"'); expect(logs).toContain("[loop] attach with: tmux attach -t repo-loop-1"); expect(manifest.claudeSessionId).toBe("claude-session-1");