diff --git a/bun.lock b/bun.lock index fae6c5a2..0909270f 100644 --- a/bun.lock +++ b/bun.lock @@ -52,10 +52,10 @@ "vite-plugin-pwa": "^1.2.0", }, "optionalDependencies": { - "@gbasin/agentboard-darwin-arm64": "0.4.3", - "@gbasin/agentboard-darwin-x64": "0.4.3", - "@gbasin/agentboard-linux-arm64": "0.4.3", - "@gbasin/agentboard-linux-x64": "0.4.3", + "@gbasin/agentboard-darwin-arm64": "0.4.4", + "@gbasin/agentboard-darwin-x64": "0.4.4", + "@gbasin/agentboard-linux-arm64": "0.4.4", + "@gbasin/agentboard-linux-x64": "0.4.4", }, }, }, diff --git a/src/server/__tests__/isolated/terminalProxy.test.ts b/src/server/__tests__/isolated/terminalProxy.test.ts index dbeb1d71..1aa2a9e4 100644 --- a/src/server/__tests__/isolated/terminalProxy.test.ts +++ b/src/server/__tests__/isolated/terminalProxy.test.ts @@ -647,4 +647,49 @@ describe('TerminalProxy', () => { options: expect.objectContaining({ timeout: 15000 }), }) }) + + test('disposes the grouped session when tmux attach spawn fails', async () => { + const harness = createSpawnHarness() + const failingSpawn = ( + args: string[], + options: Parameters[1] + ) => { + harness.spawnCalls.push({ args, options }) + throw new Error('pty exhausted') + } + + const proxy = new TerminalProxy({ + connectionId: 'attach-fail', + sessionName: 'agentboard-ws-attach-fail', + baseSession: 'agentboard', + onData: () => {}, + spawn: failingSpawn, + spawnSync: harness.spawnSync, + wait: async () => {}, + }) + + await expect(proxy.start()).rejects.toMatchObject({ + code: 'ERR_TMUX_ATTACH_FAILED', + }) + + // new-session created the grouped session before attach; dispose must + // kill it so a failed spawn cannot orphan a pty-holding session. + expect(harness.spawnSyncCalls).toContainEqual({ + args: [ + 'tmux', + 'new-session', + '-d', + '-t', + 'agentboard', + '-s', + 'agentboard-ws-attach-fail', + ], + options: expect.objectContaining({ timeout: 15000 }), + }) + expect(harness.spawnSyncCalls).toContainEqual({ + args: ['tmux', 'kill-session', '-t', 'agentboard-ws-attach-fail'], + options: expect.objectContaining({ timeout: 15000 }), + }) + expect(proxy.isReady()).toBe(false) + }) }) diff --git a/src/server/terminal/PtyTerminalProxy.ts b/src/server/terminal/PtyTerminalProxy.ts index bed7eb7a..be6bdb14 100644 --- a/src/server/terminal/PtyTerminalProxy.ts +++ b/src/server/terminal/PtyTerminalProxy.ts @@ -240,7 +240,10 @@ class PtyTerminalProxy extends TerminalProxyBase { }, }) } catch (error) { - this.state = TerminalState.DEAD + // Tear down the grouped session created just above; otherwise a failed + // attach orphans a `…-ws-` session that holds a pty until the next + // restart's reaper. + await this.dispose() throw new TerminalProxyError( 'ERR_TMUX_ATTACH_FAILED', error instanceof Error ? error.message : 'Failed to attach tmux client',