From b4c22b04dcceb51303f9b4814d024167be343e1d Mon Sep 17 00:00:00 2001 From: Oliver Southgate Date: Thu, 2 Jul 2026 21:23:53 +0100 Subject: [PATCH] fix: terminal-agent respawn guard window spans guard-max ticks The crash-loop guard (3 respawns inside RESPAWN_GUARD_WINDOW_MS -> stop and require manual restart) could never trip: respawn attempts land at most once per watchdog tick, and the window (60s) equaled the tick interval (60s), so the rolling history never held more than one entry. A crash-looping agent respawned once a minute for the daemon lifetime (#2151). Size the window as max(60s, tick * guard-max) so three consecutive failed ticks trip the guard at any tick setting. Co-Authored-By: Claude Fable 5 --- browse/src/server.ts | 11 +++++++---- browse/test/terminal-agent-watchdog.test.ts | 7 ++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/browse/src/server.ts b/browse/src/server.ts index 301781acc..547995a09 100644 --- a/browse/src/server.ts +++ b/browse/src/server.ts @@ -1504,9 +1504,9 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle { // (which would create split-brain — two agents writing the port file, // tokens diverging between them, mystery PTY upgrade failures). // - // Crash-loop guard: 3 respawn attempts inside 60s → stop trying and emit - // a one-line error. Manual `forceRestart` from the sidebar clears the - // history (the user is the explicit signal to retry). + // Crash-loop guard: 3 respawn attempts inside 3 ticks → stop trying and + // emit a one-line error. Manual `forceRestart` from the sidebar clears + // the history (the user is the explicit signal to retry). // // Only active when ownsTerminalAgent === true. Embedders that pre-launch // their own PTY server (gbrowser phoenix overlay) must not be auto-respawned @@ -1517,8 +1517,11 @@ export function buildFetchHandler(cfg: ServerConfig): ServerHandle { process.env.GSTACK_AGENT_WATCHDOG_TICK_MS || '60000', 10, ); - const RESPAWN_GUARD_WINDOW_MS = 60_000; const RESPAWN_GUARD_MAX = 3; + // The guard must span at least RESPAWN_GUARD_MAX ticks or it can never + // trip: respawn attempts land at most once per tick, so a 60s window + // over a 60s tick sees a single attempt forever (see #2151). + const RESPAWN_GUARD_WINDOW_MS = Math.max(60_000, AGENT_WATCHDOG_TICK_MS * RESPAWN_GUARD_MAX); let agentRespawnGuardTripped = false; if (ownsTerminalAgent) { diff --git a/browse/test/terminal-agent-watchdog.test.ts b/browse/test/terminal-agent-watchdog.test.ts index f012dc406..c6cdfd6aa 100644 --- a/browse/test/terminal-agent-watchdog.test.ts +++ b/browse/test/terminal-agent-watchdog.test.ts @@ -50,7 +50,12 @@ describe('terminal-agent watchdog (v1.44+)', () => { test('4. crash-loop guard with rolling window', () => { const src = fs.readFileSync(SERVER_TS, 'utf-8'); const block = sliceBetween(src, '─── Terminal-Agent Watchdog', 'Factory-scoped validateAuth'); - expect(block).toContain('RESPAWN_GUARD_WINDOW_MS = 60_000'); + // The window must span at least RESPAWN_GUARD_MAX ticks. Respawn + // attempts land at most once per tick, so a fixed 60s window over a + // 60s tick would see a single attempt forever and never trip (#2151). + expect(block).toContain( + 'RESPAWN_GUARD_WINDOW_MS = Math.max(60_000, AGENT_WATCHDOG_TICK_MS * RESPAWN_GUARD_MAX)', + ); expect(block).toContain('RESPAWN_GUARD_MAX = 3'); expect(block).toContain('respawnHistory'); expect(block).toContain('agentRespawnGuardTripped');