mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 06:59:42 +02:00
fix(remote): classify the has-session probe by exit status, and forget it once the pane is back
#355 made the remote auto-reconnect watcher revive a dead pane only when the durable remote tmux session is verifiably still alive, which is the right rule: a clean Ctrl-C / Ctrl-D / exit tears that session down and must never relaunch a fresh agent. Its probe, though, read `has-session`'s stdout and treated an empty string as "gone". `tmux has-session` prints NOTHING on success (measured on a scratch socket: exit 0, empty stdout, the failure message goes to stderr), so every live remote session classified as gone and transport-drop reconnects were silently disabled along with the clean-exit revives. The probe now goes by exit status through a pure, unit-tested mapping (`classifyRemoteAliveExit`): 0 is alive; ssh's own 255, a timeout (`killed`, no numeric code) and a spawn failure are unknown, which the watcher already treats as do-not-revive; any other status is the remote command's and means gone (tmux's 1 for a missing session, 127 when tmux is not installed there). Two smaller things in the same area: - The cached answer was never invalidated, so after one successful reattach a stale `true` would have revived the NEXT clean exit (the original bug back after the first transport drop), and a cached `false` from a clean exit would have left a manually restarted session with auto-reconnect permanently off. The tick now forgets the cache entry whenever the pane is seen alive. - The fire-and-forget probe has a 15s timeout against a 5s tick, so an unreachable host stacked up to three ssh processes per dead session. An in-flight set caps it at one. The probe command is pinned as a literal string, and the reattach-then-clean-exit sequence is driven through the watcher in the tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qg6bcATm1pNNY4kQWGwzgu
This commit is contained in:
+23
-1
@@ -1368,6 +1368,13 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
* transport drop).
|
||||
*/
|
||||
private remoteAliveCache: Map<string, boolean | undefined> = new Map();
|
||||
/**
|
||||
* Sessions with a `has-session` probe currently in flight. The probe is a
|
||||
* fire-and-forget ssh round-trip with a 15s timeout against a 5s tick, so
|
||||
* without this an unreachable host would accumulate three overlapping ssh
|
||||
* processes per dead session.
|
||||
*/
|
||||
private remoteAliveInFlight: Set<string> = new Set();
|
||||
|
||||
private trueColorConfigured = false;
|
||||
/** tmux 3.7+ can resize pane history after creation; older releases cannot. */
|
||||
@@ -2732,12 +2739,16 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
*/
|
||||
private async refreshRemoteAlive(session: MuxSession): Promise<void> {
|
||||
if (!session.remote) return;
|
||||
if (this.remoteAliveInFlight.has(session.sessionId)) return;
|
||||
this.remoteAliveInFlight.add(session.sessionId);
|
||||
const remoteName = session.remote.remoteSessionName || remoteTmuxSessionName(session.sessionId);
|
||||
try {
|
||||
const alive = await remoteTmuxSessionAlive(session.remote, remoteName);
|
||||
this.remoteAliveCache.set(session.sessionId, alive);
|
||||
} catch {
|
||||
this.remoteAliveCache.set(session.sessionId, undefined);
|
||||
} finally {
|
||||
this.remoteAliveInFlight.delete(session.sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2751,7 +2762,15 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
// refreshed lazily so a clean exit (remote tmux gone) flips it to false
|
||||
// on the next tick and stops the auto-revive.
|
||||
const paneDead = this.isPaneDead(session.muxName);
|
||||
if (paneDead && this.remoteAliveCache.get(sessionId) === undefined) {
|
||||
if (!paneDead) {
|
||||
// A live pane makes whatever the probe last said STALE, so forget it:
|
||||
// after a successful reattach (or a manual restart) the next dead pane
|
||||
// must be probed afresh. A cached `true` from the transport drop would
|
||||
// otherwise revive a later CLEAN exit, the exact bug this cache exists
|
||||
// to prevent, and a cached `false` from a clean exit would leave a
|
||||
// manually restarted session with auto-reconnect permanently off.
|
||||
this.remoteAliveCache.delete(sessionId);
|
||||
} else if (this.remoteAliveCache.get(sessionId) === undefined) {
|
||||
void this.refreshRemoteAlive(session);
|
||||
}
|
||||
const action = decideReconnect({
|
||||
@@ -2808,6 +2827,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
this.reconnectGuard.add(sessionId);
|
||||
this.reconnectState.delete(sessionId);
|
||||
this.remoteAliveCache.delete(sessionId);
|
||||
this.remoteAliveInFlight.delete(sessionId);
|
||||
}
|
||||
|
||||
/** Clear all per-session reconnect + guard state (e.g. when a session is removed). */
|
||||
@@ -2815,6 +2835,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
this.reconnectState.delete(sessionId);
|
||||
this.reconnectGuard.delete(sessionId);
|
||||
this.remoteAliveCache.delete(sessionId);
|
||||
this.remoteAliveInFlight.delete(sessionId);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
@@ -2824,6 +2845,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
this.reconnectState.clear();
|
||||
this.reconnectGuard.clear();
|
||||
this.remoteAliveCache.clear();
|
||||
this.remoteAliveInFlight.clear();
|
||||
}
|
||||
|
||||
registerSession(session: MuxSession): void {
|
||||
|
||||
Reference in New Issue
Block a user