Gate session ID liveness by shell foreground state

Once OSC 133;D proves a pane is back at the shell, don't return the
session ID even if a durable record survived the exit. This prevents
treating exited sessions as still active when the user is typing at
the prompt.
This commit is contained in:
Jinjing
2026-09-02 10:21:56 -07:00
parent 5153ec7a1f
commit 079ef8a9d1
2 changed files with 46 additions and 3 deletions
@@ -7,11 +7,13 @@ const PANE_KEY = 'tab-1:11111111-1111-4111-8111-111111111111'
function state(
live?: AgentStatusEntry,
sleeping?: SleepingAgentSessionRecord
sleeping?: SleepingAgentSessionRecord,
shellForeground = false
): PaneAgentSessionIdState {
return {
agentStatusByPaneKey: live ? { [PANE_KEY]: live } : {},
sleepingAgentSessionsByPaneKey: sleeping ? { [PANE_KEY]: sleeping } : {}
sleepingAgentSessionsByPaneKey: sleeping ? { [PANE_KEY]: sleeping } : {},
paneForegroundAgentByPaneKey: { [PANE_KEY]: { agent: 'claude', shellForeground } }
}
}
@@ -68,6 +70,39 @@ describe('resolvePaneAgentSessionId', () => {
).toBe('confirmed-session')
})
describe('liveness', () => {
it('is absent once the pane is proven back at the shell', () => {
expect(
resolvePaneAgentSessionId(state(live('live-session'), undefined, true), PANE_KEY)
).toBe(null)
})
it('is absent at the shell even when a durable record survives the exit', () => {
expect(
resolvePaneAgentSessionId(state(undefined, sleeping('sleeping-session'), true), PANE_KEY)
).toBeNull()
})
it('keeps a session whose foreground evidence is only that an agent runs', () => {
expect(
resolvePaneAgentSessionId(state(live('live-session'), undefined, false), PANE_KEY)
).toBe('live-session')
})
it('keeps a session for a pane with no foreground evidence at all', () => {
expect(
resolvePaneAgentSessionId(
{
agentStatusByPaneKey: { [PANE_KEY]: live('live-session') },
sleepingAgentSessionsByPaneKey: {},
paneForegroundAgentByPaneKey: {}
},
PANE_KEY
)
).toBe('live-session')
})
})
it('does not read identity from a sibling pane', () => {
const sibling = 'tab-1:22222222-2222-4222-8222-222222222222'
expect(resolvePaneAgentSessionId(state(undefined, sleeping('session-1')), sibling)).toBeNull()
@@ -1,16 +1,24 @@
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
import type { SleepingAgentSessionRecord } from '../../../../shared/agent-session-resume'
import type { PaneForegroundAgentEntry } from '../../store/slices/pane-foreground-agent'
export type PaneAgentSessionIdState = {
agentStatusByPaneKey: Record<string, AgentStatusEntry | undefined>
sleepingAgentSessionsByPaneKey: Record<string, SleepingAgentSessionRecord | undefined>
paneForegroundAgentByPaneKey: Record<string, PaneForegroundAgentEntry | undefined>
}
/** Resolves the provider session owned by one exact terminal pane. */
/** Resolves the provider session owned by one exact terminal pane, while its agent is still live. */
export function resolvePaneAgentSessionId(
state: PaneAgentSessionIdState,
paneKey: string
): string | null {
// OSC 133;D proves the pane is back at the shell. The durable record outlives that exit on
// purpose (cold restore resumes from it), so gate it here too — otherwise the gate would only
// hold for panes whose agent has no resumable record.
if (state.paneForegroundAgentByPaneKey[paneKey]?.shellForeground === true) {
return null
}
const live = state.agentStatusByPaneKey[paneKey]
if (live && live.restoredUnconfirmed !== true) {
return live.providerSession?.id ?? null