diff --git a/src/index.ts b/src/index.ts index 494d6944..9f94d378 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,18 @@ import { program } from './cli.js'; // In web mode, we should NOT exit on transient errors — log and continue const isWebMode = process.argv.includes('web'); +// COD-115: Codeman IS a tmux controller; it must never present as a tmux *client*. +// If the web server is launched from inside a tmux pane it inherits TMUX/TMUX_PANE, +// and tmux's nesting guard then kills every new attach-bridge PTY (exit 1 → respawn +// loop, crash-looping any new tmux-backed session). Scrub at the root so every +// downstream `{...process.env}` spread (attach, send-keys, create) is clean regardless +// of launch context. `delete` (not `= undefined`, which node-pty serializes as the +// literal string "undefined" and fails to clear). +if (isWebMode) { + delete process.env.TMUX; + delete process.env.TMUX_PANE; +} + import { MAX_CONSECUTIVE_ERRORS, ERROR_RESET_MS } from './config/server-timing.js'; // Track consecutive unhandled errors in web mode — restart after too many diff --git a/src/session-cli-builder.ts b/src/session-cli-builder.ts index ad6ed85f..72ee3f96 100644 --- a/src/session-cli-builder.ts +++ b/src/session-cli-builder.ts @@ -124,17 +124,32 @@ export function buildClaudeEnv(sessionId: string): Record { - return { +export function buildMuxAttachEnv(truecolorEnabled?: boolean): Record { + const env: Record = { ...process.env, LANG: 'en_US.UTF-8', LC_ALL: 'en_US.UTF-8', TERM: 'xterm-256color', - COLORTERM: undefined, - CLAUDECODE: undefined, }; + // COD-115: keys to UNSET must be `delete`d, NOT set to `undefined`. On a + // `{...process.env}` spread the key stays present with value undefined, and node-pty + // serializes it as the literal string "TMUX=undefined" — a non-empty value that still + // trips tmux's nesting guard, killing the attach-bridge PTY (exit 1 → respawn loop). + // The server can be launched from inside tmux; attach clients must never inherit that + // parent tmux context. (Same fix the working create path uses in tmux-manager.ts.) + delete env.TMUX; + delete env.TMUX_PANE; + delete env.CLAUDECODE; + if (truecolorEnabled) { + env.COLORTERM = 'truecolor'; + } else { + delete env.COLORTERM; // COD-75: unset for non-truecolor (was `: undefined`, same node-pty quirk) + } + return env; } /** diff --git a/test/session-cli-builder.test.ts b/test/session-cli-builder.test.ts new file mode 100644 index 00000000..f853d0ee --- /dev/null +++ b/test/session-cli-builder.test.ts @@ -0,0 +1,66 @@ +/** + * @fileoverview Tests for CLI environment builders. + * + * Port: N/A (no server needed) + */ + +import { describe, it, expect } from 'vitest'; +import { buildMuxAttachEnv } from '../src/session-cli-builder.js'; + +describe('buildMuxAttachEnv', () => { + it('does not pass an inherited tmux context into tmux attach clients', () => { + const originalTmux = process.env.TMUX; + const originalTmuxPane = process.env.TMUX_PANE; + process.env.TMUX = '/tmp/tmux-1000/codeman,1169416,9'; + process.env.TMUX_PANE = '%9'; + + try { + const env = buildMuxAttachEnv(); + + expect(env.TMUX).toBeUndefined(); + expect(env.TMUX_PANE).toBeUndefined(); + } finally { + if (originalTmux === undefined) { + delete process.env.TMUX; + } else { + process.env.TMUX = originalTmux; + } + if (originalTmuxPane === undefined) { + delete process.env.TMUX_PANE; + } else { + process.env.TMUX_PANE = originalTmuxPane; + } + } + }); + + // COD-115: `{...process.env, TMUX: undefined}` leaves the KEY present with value + // undefined; node-pty serializes that as the literal string "TMUX=undefined", which + // still trips tmux's nesting guard and kills the attach-bridge PTY (exit 1 → respawn + // loop). The keys must be genuinely ABSENT, which only `delete` achieves. + it('deletes tmux/claude context keys entirely (absent, not present-with-undefined) (COD-115)', () => { + const saved = { + TMUX: process.env.TMUX, + TMUX_PANE: process.env.TMUX_PANE, + CLAUDECODE: process.env.CLAUDECODE, + }; + process.env.TMUX = '/tmp/tmux-1000/codeman,1169416,9'; + process.env.TMUX_PANE = '%9'; + process.env.CLAUDECODE = '1'; + + try { + const env = buildMuxAttachEnv(); + + expect('TMUX' in env).toBe(false); + expect('TMUX_PANE' in env).toBe(false); + expect('CLAUDECODE' in env).toBe(false); + } finally { + for (const [k, v] of Object.entries(saved)) { + if (v === undefined) { + delete process.env[k]; + } else { + process.env[k] = v; + } + } + } + }); +});