fix(maestro): three defects the review flagged and left open
ci / web (pull_request) Successful in 28s
ci / rust (pull_request) Successful in 1m58s
ci / web (push) Successful in 34s
ci / rust (push) Successful in 2m44s

SPA attach had no reconnect at all. Any dropped frame closed the socket,
`onClosed` fired, and the pane was destroyed permanently — the TUI
reconnects forever on the same endpoint. Session death also reached the UI
on two uncoordinated channels: the row from the events socket, the pane
from `ws.onclose`, with nothing reconciling them, so either could happen
without the other.

The catalog is now the authority. `shouldRetryClose` decides from session
status: Running means the link dropped, so retry with 500ms-to-10s
backoff; Dead means the PTY stream was sealed and nothing more is coming;
absent means it is gone. Application closes (4000+, a relayed daemon
rejection) and clean closes are never retried. The shell also closes any
pane whose session has left the catalog. The policy lives in reconnect.ts
so it is tested without a DOM.

kill_session checked `has()` before `kill()`, which only opened a window
for the liveness watcher to reap in between — turning a 404 into a 502.
`kill` reports the miss itself.

with_spawn_history logged and carried on when `Arc::get_mut` failed,
producing a daemon whose recent-dirs picker is permanently empty with
nothing to show for it. It panics now: that is a construction-order
mistake, at startup, in code that runs once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit was merged in pull request #50.
This commit is contained in:
Miguel Palhas
2026-08-22 16:31:54 +01:00
co-authored by Claude Opus 5
parent 812861f883
commit f31377c34f
9 changed files with 234 additions and 58 deletions
+32
View File
@@ -0,0 +1,32 @@
// Attach-socket reconnect policy. Split out of attach.ts so it can be tested
// without a DOM or an xterm instance.
import type { Session, Uuid } from "./types.js";
/** First retry delay after an unexpected close, doubling up to the cap. */
export const INITIAL_RECONNECT_MS = 500;
export const MAX_RECONNECT_MS = 10_000;
/** Close codes at or above this are application-defined; `maestro-web` uses
* them to relay a daemon rejection, which retrying would only loop on. */
const APPLICATION_CLOSE = 4000;
/** Normal Closure — the peer ended the stream deliberately. */
const CLEAN_CLOSE = 1000;
/**
* Should a closed attach socket be retried?
*
* The catalog is the authority: a session still `Running` lost its link, so
* retry; one that is `Dead` has had its PTY stream sealed and will never send
* again, and one absent from the catalog is gone. Before this existed, any
* dropped frame destroyed the pane permanently.
*/
export function shouldRetryClose(
sessionId: Uuid,
code: number,
sessions: ReadonlyMap<Uuid, Session>,
): boolean {
if (code >= APPLICATION_CLOSE || code === CLEAN_CLOSE) return false;
return sessions.get(sessionId)?.status === "Running";
}