Skip to content

fix(session): scrub TMUX vars + PTY-exit circuit breaker (COD-115/COD-118)#147

Open
aakhter wants to merge 3 commits into
Ark0N:masterfrom
aakhter:cod-168-pty-exit-breaker
Open

fix(session): scrub TMUX vars + PTY-exit circuit breaker (COD-115/COD-118)#147
aakhter wants to merge 3 commits into
Ark0N:masterfrom
aakhter:cod-168-pty-exit-breaker

Conversation

@aakhter

@aakhter aakhter commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Two related fixes for tmux-backed session stability:

COD-115 — Delete TMUX/TMUX_PANE (don't set-to-undefined)

When the Codeman server is launched from inside a tmux session, process.env.TMUX is set. The mux-attach PTY inherits that env, and tmux detects the nesting via TMUX/TMUX_PANE → exits 1 → Codeman's respawn kicks in → crash-loop.

Root cause: buildMuxAttachEnv() was spreading {...process.env, TMUX: undefined}. node-pty serializes that as the literal string "TMUX=undefined" (the key is present with a non-empty value), so the guard still fires. Fix: delete env.TMUX and delete env.TMUX_PANE — key absent from the environment entirely. Same fix applied at web bootstrap in src/index.ts for belt-and-suspenders coverage.

COD-118 — PTY-exit circuit breaker

Introduces InteractivePtyExitBreaker (pure class, injectable time for tests): a sliding-window breaker that trips after 5 non-zero PTY exits within 10 seconds, sets _respawnBlocked, and emits respawnBreakerTripped. SSE event + push notification tell the user. Explicit user restart (POST /api/sessions/:id/start) resets the breaker before relaunching.

Prevents a misconfigured case (bad shell, bad Claude install, permission denied) from hammering tmux/PTY in an infinite respawn loop and burning system resources.

Files changed

  • src/index.ts — delete TMUX/TMUX_PANE at web bootstrap
  • src/session-cli-builder.tsbuildMuxAttachEnv() uses delete for TMUX, TMUX_PANE, CLAUDECODE, COLORTERM
  • test/session-cli-builder.test.ts — new; COD-115 absence assertions ('TMUX' in env === false)
  • src/session-pty-exit-breaker.ts — new; InteractivePtyExitBreaker (sliding window, threshold 5/10s)
  • src/session.ts_ptyExitBreaker + _respawnBlocked, resetRespawnBreaker(), PTY exit instrumentation
  • src/web/session-listener-wiring.tsrespawnBreakerTripped handler → SSE + push + run-summary
  • src/web/sse-events.tsSessionRespawnBreakerTripped event
  • src/web/public/constants.jsSESSION_RESPAWN_BREAKER_TRIPPED constant
  • src/web/public/app.js — diagnostic toast on respawnBreakerTripped
  • src/web/routes/session-routes.tssession.resetRespawnBreaker() before startInteractive()
  • test/respawn-pty-breaker.test.ts — new; 13 tests (pure breaker + session integration)

Test plan

  • npm test -- test/respawn-pty-breaker.test.ts test/session-cli-builder.test.ts → 15/15 pass
  • tsc --noEmit → clean
  • npm run build → clean

aakhter and others added 3 commits July 9, 2026 11:47
…-loop

When the web server is launched from inside a tmux pane it inherits TMUX/
TMUX_PANE. tmux's nesting guard then makes every new attach-bridge PTY
(`tmux attach-session`, used by codex/opencode/gemini and mux-wrapped claude)
exit code 1; the respawn controller recreates the dead bridge → infinite loop.

The existing guard in buildMuxAttachEnv() used `TMUX: undefined` on a
{...process.env} spread, which leaves the KEY present with value undefined —
node-pty serializes it as the literal string "TMUX=undefined", still tripping
the guard. (The working create path in tmux-manager.ts uses `delete`.)

Fix:
- Primary: delete process.env.TMUX / TMUX_PANE at web bootstrap (src/index.ts)
  so every downstream {...process.env} spread is clean regardless of launch
  context. `delete`, not `= undefined`.
- buildMuxAttachEnv(): build a copy and `delete` TMUX/TMUX_PANE/CLAUDECODE
  (and COLORTERM when not truecolor) instead of `: undefined` — same node-pty
  quirk affected all of them.
- Test: assert the keys are genuinely ABSENT (`'TMUX' in env === false`), not
  merely undefined — the prior test only checked `toBeUndefined()`, which is
  why the bug slipped through. Red→green confirmed.

Verified on isolated beta launched from inside tmux (inherited the poisonous
TMUX=codeman,980,7): created a codex session + triggered interactive attach —
the bridge `tmux -L codeman-beta attach-session` spawned with NO TMUX in its
env, attached successfully, zero "exited with code: 1", server healthy.

Circuit-breaker for repeated non-zero bridge exits (AC bullet 4, optional)
split to a follow-up. Deploy-pending (substrate): never auto-deployed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…PTY exits

Defense-in-depth after COD-115. If the interactive PTY exits non-zero
repeatedly within a short window, recovery/reconnect paths recreate it
indefinitely (COD-115 saw 114 'exited with code: 1' events + orphans).

- New pure InteractivePtyExitBreaker (session-pty-exit-breaker.ts):
  injectable time, sliding window, clean-exit resets counter, stays
  tripped until reset(). Defaults: threshold 5, window 10s.
- Session records each interactive PTY exit in the breaker; on trip it
  flips _status to 'error', sets _respawnBlocked, emits
  respawnBreakerTripped. startInteractive() refuses to respawn while
  blocked, so all recovery/reconnect callers stop looping uniformly.
- Explicit user restart (POST /api/sessions/:id/interactive) calls
  resetRespawnBreaker() so intentional restarts are never blocked.
- New SSE event session:respawnBreakerTripped wired in sse-events.ts +
  constants.js (registries in sync) + session-listener-wiring.ts;
  minimal diagnostic toast in app.js.
- Tests: test/respawn-pty-breaker.test.ts (pure trip/reset/window +
  MockSession session-level trip/reset).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POST /api/sessions/:id/interactive calls session.resetRespawnBreaker()
before startInteractive(); mock missing the stub → route threw → 422.
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