Skip to content

Clean up stale tmux MCP bridge state#38

Merged
axeldelafosse merged 6 commits intomainfrom
fix/tmux-bridge-cleanup
Mar 25, 2026
Merged

Clean up stale tmux MCP bridge state#38
axeldelafosse merged 6 commits intomainfrom
fix/tmux-bridge-cleanup

Conversation

@axeldelafosse
Copy link
Owner

Summary

  • clear stale tmux bridge routing state from the run manifest when the tmux session is gone
  • remove the per-run Claude local MCP server during stale-session cleanup
  • cover stale-session cleanup and the no-op cleanup path in bridge tests

Testing

  • bun run check
  • bun test tests/loop/bridge.test.ts tests/loop/codex-tmux-proxy.test.ts

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly improves the stability and resource management of the tmux bridge by introducing robust mechanisms to automatically detect and clean up stale tmux session data and associated Claude MCP servers. It also refines the interaction with Claude's startup prompts within tmux, ensuring smoother and more reliable automated operations.

Highlights

  • Stale Tmux Bridge State Cleanup: Implemented automatic detection and cleanup of stale tmux bridge routing state from the run manifest, ensuring that inactive tmux sessions no longer leave lingering data.
  • Claude MCP Server Removal: Added functionality to remove the per-run Claude local Message Channel Protocol (MCP) server when its associated tmux session is no longer active, preventing resource leaks.
  • Improved Claude Prompt Handling: Enhanced the detection and interaction logic for Claude's startup prompts within tmux, specifically for bypass and development channel confirmations, to ensure smoother automated responses.
  • Comprehensive Test Coverage: Expanded unit tests to cover the new stale-session cleanup mechanisms and the updated Claude prompt handling, including no-op scenarios for cleanup.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces functionality to automatically clean up stale tmux sessions and associated Claude channel servers, improving the robustness of the bridge. It also enhances the handling of Claude startup prompts within tmux, specifically for 'Bypass Permissions mode' and 'development channels', by making prompt detection more robust and key sending more reliable. New tests have been added to cover these cleanup and prompt handling scenarios. Feedback suggests logging errors during Claude channel server removal for better debugging and refactoring duplicated test setup code in tmux.test.ts for improved readability and maintainability.

Comment on lines +391 to +393
} catch {
// Cleanup should not fail the bridge flow.
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Swallowing errors completely can make debugging difficult if an unexpected issue occurs during cleanup (e.g., the claude command is not found). While the cleanup shouldn't block the main flow, logging the error would be beneficial for observability.

Consider adding a log statement to record the error.

  } catch (error) {
    // Cleanup should not fail the bridge flow, but we should log the error for debugging.
    console.error(`[loop] Failed to remove Claude channel server for run ${runId}:`, error);
  }

Comment on lines +703 to +871
test("runInTmux confirms wrapped Claude dev-channel prompts", async () => {
const keyCalls: Array<{ keys: string[]; pane: string }> = [];
let sessionStarted = false;
let pollCount = 0;
const devChannelsPrompt = [
"WARNING: Loading development channels",
"",
"--dangerously-load-development-channels is for local channel development only.",
"",
"1. I am using this for local",
"development",
].join("\n");
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;
if (pollCount === 1) {
return devChannelsPrompt;
}
return "";
},
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 confirms the current Claude bypass prompt wording", async () => {
const keyCalls: Array<{ keys: string[]; pane: string }> = [];
let sessionStarted = false;
let pollCount = 0;
const bypassPrompt = [
"Bypass Permissions mode",
"",
"1. No, exit",
"2. Yes, I accept",
].join("\n");
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;
if (pollCount === 1) {
return bypassPrompt;
}
return "";
},
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: ["Down"],
pane: "repo-loop-1:0.0",
});
expect(keyCalls).toContainEqual({
keys: ["Enter"],
pane: "repo-loop-1:0.0",
});
});

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's significant boilerplate duplication across the new tests (runInTmux confirms wrapped Claude dev-channel prompts and runInTmux confirms the current Claude bypass prompt wording) and other tests in this file. This harms readability and maintainability.

Consider extracting the common test setup into a helper function. This function could accept parameters for test-specific logic, like the capturePane mock's return value and the assertions on keyCalls.

For example: const testTmuxPromptHandling = async (promptText, expectedKeyCalls) => { ... };

@axeldelafosse axeldelafosse marked this pull request as ready for review March 25, 2026 19:43
@axeldelafosse axeldelafosse merged commit 7e62b15 into main Mar 25, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant