From 7a1d6b8912e2b7098772f16fd58fbbdfcc08c8ef Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Wed, 1 Apr 2026 17:12:33 +0200 Subject: [PATCH 1/3] fix(cloud-agent): restore canSend/canInterrupt on interrupt failure A failed interrupt() HTTP call left canSendAtom and canInterruptAtom stuck at false, making the session unusable until page refresh. Now the catch block restores both atoms from the session's actual state. --- src/lib/cloud-agent-sdk/session-manager.test.ts | 16 ++++++++++++++++ src/lib/cloud-agent-sdk/session-manager.ts | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/src/lib/cloud-agent-sdk/session-manager.test.ts b/src/lib/cloud-agent-sdk/session-manager.test.ts index 89dc7ec754..e5ddab24a2 100644 --- a/src/lib/cloud-agent-sdk/session-manager.test.ts +++ b/src/lib/cloud-agent-sdk/session-manager.test.ts @@ -608,6 +608,22 @@ describe('createSessionManager', () => { ); }); + it('restores canSend and canInterrupt on interrupt failure', async () => { + const config = createMockConfig(); + const mgr = createSessionManager(config); + + await mgr.switchSession(kiloId('ses-1')); + expect(atomValue(config.store, mgr.atoms.canSend)).toBe(true); + expect(atomValue(config.store, mgr.atoms.canInterrupt)).toBe(true); + + mockSession.interrupt.mockRejectedValueOnce(new Error('transient failure')); + await mgr.interrupt(); + + // After a failed interrupt, atoms should be restored from session state + expect(atomValue(config.store, mgr.atoms.canSend)).toBe(true); + expect(atomValue(config.store, mgr.atoms.canInterrupt)).toBe(true); + }); + it('is a no-op without active session', async () => { const config = createMockConfig(); const mgr = createSessionManager(config); diff --git a/src/lib/cloud-agent-sdk/session-manager.ts b/src/lib/cloud-agent-sdk/session-manager.ts index 7d7ea6ef9f..f18c31280d 100644 --- a/src/lib/cloud-agent-sdk/session-manager.ts +++ b/src/lib/cloud-agent-sdk/session-manager.ts @@ -613,6 +613,11 @@ function createSessionManager(config: SessionManagerConfig): SessionManager { } setIndicator({ type: 'info', message: 'Session stopped', timestamp: Date.now() }); } catch { + // Restore atoms from the session's actual state so the UI isn't stuck. + store.set(canInterruptAtom, currentSession.canInterrupt); + const cs = store.get(cloudStatusAtom); + const cloudReady = cs === null || cs.type === 'ready'; + store.set(canSendAtom, currentSession.canSend && cloudReady); store.set(errorAtom, 'Failed to stop execution'); } } From 78aea3089c28598d9dbdd0fb6e1dedb069362c82 Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Wed, 1 Apr 2026 20:18:13 +0200 Subject: [PATCH 2/3] fix(cloud-agent): snapshot session before await in interrupt() currentSession can be nulled by destroy() or swapped by switchSession() during the await. Snapshot the reference before the async call and only restore atoms if the session wasn't swapped. --- src/lib/cloud-agent-sdk/session-manager.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/lib/cloud-agent-sdk/session-manager.ts b/src/lib/cloud-agent-sdk/session-manager.ts index f18c31280d..56e59358fc 100644 --- a/src/lib/cloud-agent-sdk/session-manager.ts +++ b/src/lib/cloud-agent-sdk/session-manager.ts @@ -601,6 +601,8 @@ function createSessionManager(config: SessionManagerConfig): SessionManager { async function interrupt(): Promise { if (!currentSession) return; + // Snapshot before await — switchSession()/destroy() can swap currentSession while in flight. + const session = currentSession; // Eagerly disable send/interrupt to prevent the user from sending a // message while the async interrupt HTTP call is in flight. We do NOT // call disconnect() — interrupt stops the agent but keeps the transport @@ -608,16 +610,18 @@ function createSessionManager(config: SessionManagerConfig): SessionManager { store.set(canSendAtom, false); store.set(canInterruptAtom, false); try { - if (currentSession.canInterrupt) { - await currentSession.interrupt(); + if (session.canInterrupt) { + await session.interrupt(); } setIndicator({ type: 'info', message: 'Session stopped', timestamp: Date.now() }); } catch { - // Restore atoms from the session's actual state so the UI isn't stuck. - store.set(canInterruptAtom, currentSession.canInterrupt); - const cs = store.get(cloudStatusAtom); - const cloudReady = cs === null || cs.type === 'ready'; - store.set(canSendAtom, currentSession.canSend && cloudReady); + // Only restore atoms if the session wasn't swapped during the await. + if (currentSession === session) { + store.set(canInterruptAtom, session.canInterrupt); + const cs = store.get(cloudStatusAtom); + const cloudReady = cs === null || cs.type === 'ready'; + store.set(canSendAtom, session.canSend && cloudReady); + } store.set(errorAtom, 'Failed to stop execution'); } } From d9736f99f69d4b34b005c39ab2d4864c481dca5d Mon Sep 17 00:00:00 2001 From: Evgeny Shurakov Date: Wed, 1 Apr 2026 21:06:29 +0200 Subject: [PATCH 3/3] fix(cloud-agent): gate all post-await interrupt updates on session identity Move setIndicator and errorAtom writes inside the currentSession===session guard so stale interrupt completions don't surface banners on a replacement session. --- src/lib/cloud-agent-sdk/session-manager.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib/cloud-agent-sdk/session-manager.ts b/src/lib/cloud-agent-sdk/session-manager.ts index 56e59358fc..4c73a770fc 100644 --- a/src/lib/cloud-agent-sdk/session-manager.ts +++ b/src/lib/cloud-agent-sdk/session-manager.ts @@ -613,16 +613,17 @@ function createSessionManager(config: SessionManagerConfig): SessionManager { if (session.canInterrupt) { await session.interrupt(); } - setIndicator({ type: 'info', message: 'Session stopped', timestamp: Date.now() }); + if (currentSession === session) { + setIndicator({ type: 'info', message: 'Session stopped', timestamp: Date.now() }); + } } catch { - // Only restore atoms if the session wasn't swapped during the await. if (currentSession === session) { store.set(canInterruptAtom, session.canInterrupt); const cs = store.get(cloudStatusAtom); const cloudReady = cs === null || cs.type === 'ready'; store.set(canSendAtom, session.canSend && cloudReady); + store.set(errorAtom, 'Failed to stop execution'); } - store.set(errorAtom, 'Failed to stop execution'); } }