Skip to content

fix(tui-agents): distinguish missing tmux from dead sessions in sweep and reconcile - #3169

Open
Tyagiquamar wants to merge 2 commits into
generalaction:mainfrom
Tyagiquamar:fix/3109-tui-idle-reconcile
Open

fix(tui-agents): distinguish missing tmux from dead sessions in sweep and reconcile#3169
Tyagiquamar wants to merge 2 commits into
generalaction:mainfrom
Tyagiquamar:fix/3109-tui-idle-reconcile

Conversation

@Tyagiquamar

Copy link
Copy Markdown
Contributor

Fixes #3109.

Problem

On machines without tmux, \listTmuxSessions\ collapsed capability-unavailable (ENOENT, exit 127) into an empty activity map. Consequences in \TuiAgentsRuntime:

  • sweep treated tmux-backed sessions as idle-dead and evicted them (scrollback reseeded, terminal blank on reopen),
  • the reconcile gate suspended every active intent as \process-lost, including non-tmux sessions whose liveness never depended on tmux, so nothing auto-resumed after a worker restart.

Change

  • pty service boundary (\ mux-commands.ts): new \TmuxUnavailableError\ + reusable \isTmuxMissingError. Successful empty enumeration (no-server exit 1) still means zero live sessions; a missing executable is unknown liveness and now throws typed.

  • esolveTmuxSession/\ indTmuxSessionNamesByIdentity\ (creation/discovery): treat unknown as not-found, so spawn surfaces the honest failure downstream. Only \listTmuxSessionActivity\ propagates for sweep/reconcile judgments.
  • \TuiAgentsRuntime: tracks \ muxUnknown. Sweep keeps tmux-backed sessions while unknown (reaped once tmux is queryable again); reconcile proceeds instead of vetoing, defers tmux-backed intents via a new gate \defer\ verdict, and always resumes non-tmux intents.
  • session-lifecycle: gate gains { defer: true }\ (intent left untouched).

Verification

  • 3 new deterministic RED tests (failed on \810e17f3, green after): non-tmux intent resumes on reconcile, tmux intent stays active when the binary is missing, sweep keeps tmux sessions when the binary is missing.
  • Updated \ mux.test.ts\ missing-binary cases to the new contract; added resolve-missing and gate-defer tests.
  • \pnpm --filter @emdash/core\ targeted suites green (
    untime.test.ts\ 38/38; pty, session-lifecycle green); \pnpm run typecheck\ green for all 9 projects.
  • Pre-existing Windows-host failures (prompt-spill, workspace-trust file test, terminals tmux tests) verified identical on clean HEAD; tmux-assuming TUI tests pinned to \platform: linux\ for hermeticity (test-only).
  • Not run per author request: \pnpm run lint, full \pnpm run test.

… and reconcile

On machines without tmux, listTmuxSessions collapsed capability-unavailable
(ENOENT, exit 127) into an empty activity map. The sweep then treated
tmux-backed sessions as idle-dead and evicted them, and the reconcile gate
suspended both tmux-backed and non-tmux active intents as process-lost, so
reopened TUI conversations came back blank.

Represent the distinction at the pty service boundary with
TmuxUnavailableError/isTmuxMissingError: successful empty enumeration still
means zero live sessions, while an unavailable executable is unknown
liveness. The runtime keeps (sweep) or defers (reconcile gate) tmux-backed
sessions while unknown, and non-tmux sessions resume without consulting
tmux at all.
@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR distinguishes an unavailable tmux executable from an empty tmux server and adds deferred lifecycle reconciliation for unknown liveness. However, the newly introduced typed error is not recognized at the runtime boundary, so missing tmux still globally vetoes reconciliation.

  • Adds TmuxUnavailableError and tmux failure classification.
  • Adds a per-intent defer lifecycle verdict.
  • Updates TUI sweep and reconcile behavior for unknown tmux liveness.
  • Adds focused tmux, runtime, and lifecycle tests.

Confidence Score: 4/5

This PR is not safe to merge until the runtime can recognize the wrapped unavailable-tmux error and resume non-tmux intents when tmux is missing.

The central reconcile fix remains ineffective because the classifier rejects the TmuxUnavailableError emitted by listTmuxSessions, sending missing-tmux failures through the global veto path.

Files Needing Attention: packages/core/src/services/pty/api/tmux-commands.ts, packages/core/src/runtimes/tui-agents/node/runtime/runtime.ts, packages/core/src/runtimes/tui-agents/node/runtime/runtime.test.ts

Important Files Changed

Filename Overview
packages/core/src/services/pty/api/tmux-commands.ts Introduces the unavailable-tmux error contract, but its exported classifier cannot recognize the typed error produced by the same module.
packages/core/src/runtimes/tui-agents/node/runtime/runtime.ts Adds unknown-liveness handling for sweep and reconcile, but the reconcile path cannot reach it after the typed error is wrapped.
packages/core/src/runtimes/tui-agents/node/runtime/runtime.test.ts Adds relevant regression tests, but does not combine a missing tmux executable with a non-tmux active intent.
packages/core/src/services/session-lifecycle/node/session-lifecycle.ts Correctly adds a gate verdict that leaves deferred intents untouched.
packages/core/src/services/pty/api/tmux.ts Preserves creation and discovery behavior by treating the typed unavailable error as an empty inventory.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Execute tmux list-sessions] --> B[Raw ENOENT failure]
  B --> C[listTmuxSessions wraps TmuxUnavailableError]
  C --> D[Runtime calls isTmuxMissingError]
  D -->|Wrapper is not recognized| E[Generic failure path]
  E --> F[Reconcile vetoes all intents]
  F --> G[Non-tmux intents do not resume]
  D -.->|Intended behavior| H[Set tmuxUnknown]
  H --> I[Defer tmux intents]
  H --> J[Resume non-tmux intents]
Loading
Prompt To Fix All With AI
### Issue 1
packages/core/src/services/pty/api/tmux-commands.ts:27-32
**Wrapped error bypasses detection**

When the tmux executable is missing, `listTmuxSessions` wraps the spawn failure in `TmuxUnavailableError`. However, `isTmuxMissingError` only checks errors with an `exitCode` or `code`, so the runtime treats this wrapper as an ordinary failure and vetoes the entire reconciliation. As a result, non-tmux intents still do not resume on machines without tmux. The new tests cover non-tmux resumption and missing tmux separately, so they do not catch this combined failure path.

```suggestion
export function isTmuxMissingError(error: unknown): boolean {
  if (error instanceof TmuxUnavailableError) return true;
  const failure = readExecFailure(error);
  if (!failure) return false;
  if (failure.executableMissing) return true;
  return failure.exitCode === 127;
}
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(tui-agents): distinguish missing tmu..." | Re-trigger Greptile

Comment on lines +27 to +32
export function isTmuxMissingError(error: unknown): boolean {
const failure = readExecFailure(error);
if (!failure) return false;
if (failure.executableMissing) return true;
return failure.exitCode === 127;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Wrapped error bypasses detection

When the tmux executable is missing, listTmuxSessions wraps the spawn failure in TmuxUnavailableError. However, isTmuxMissingError only checks errors with an exitCode or code, so the runtime treats this wrapper as an ordinary failure and vetoes the entire reconciliation. As a result, non-tmux intents still do not resume on machines without tmux. The new tests cover non-tmux resumption and missing tmux separately, so they do not catch this combined failure path.

Suggested change
export function isTmuxMissingError(error: unknown): boolean {
const failure = readExecFailure(error);
if (!failure) return false;
if (failure.executableMissing) return true;
return failure.exitCode === 127;
}
export function isTmuxMissingError(error: unknown): boolean {
if (error instanceof TmuxUnavailableError) return true;
const failure = readExecFailure(error);
if (!failure) return false;
if (failure.executableMissing) return true;
return failure.exitCode === 127;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/core/src/services/pty/api/tmux-commands.ts
Line: 27-32

Comment:
**Wrapped error bypasses detection**

When the tmux executable is missing, `listTmuxSessions` wraps the spawn failure in `TmuxUnavailableError`. However, `isTmuxMissingError` only checks errors with an `exitCode` or `code`, so the runtime treats this wrapper as an ordinary failure and vetoes the entire reconciliation. As a result, non-tmux intents still do not resume on machines without tmux. The new tests cover non-tmux resumption and missing tmux separately, so they do not catch this combined failure path.

```suggestion
export function isTmuxMissingError(error: unknown): boolean {
  if (error instanceof TmuxUnavailableError) return true;
  const failure = readExecFailure(error);
  if (!failure) return false;
  if (failure.executableMissing) return true;
  return failure.exitCode === 127;
}
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@Tyagiquamar Tyagiquamar Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

listTmuxSessions wraps missing-tmux failures in TmuxUnavailableError, and isTmuxMissingError did not recognize that wrapper, so reconcile vetoed the whole run.

Confirmed. isTmuxMissingError now treats TmuxUnavailableError as missing tmux. Added a combined regression: tmux unavailable + an active non-tmux intent still resumes, while a tmux-backed intent stays deferred/active instead of being suspended.

listTmuxSessions wraps spawn failures in TmuxUnavailableError, but isTmuxMissingError only inspected raw exec shapes. Recognize the wrapper so reconcile still resumes non-tmux intents instead of vetoing the whole run.
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.

[bug]: TUI conversations go blank after about an hour idle

1 participant