mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 01:57:57 +00:00
fix(remote): reopening a slept tab resumes when the other holder's agent has proven exited (#29588)
A pane whose shell outlived its agent kept counting as the session's live holder because its PTY was alive, so a paired client got a plain shell. Ask the hook store: a holder it proves exited no longer blocks the resume; unverifiable still does.
This commit is contained in:
@@ -301,11 +301,12 @@ describe('OrcaRuntimeService', () => {
|
||||
expect(tap.spawn.mock.calls[0]?.[0]).toMatchObject({ spawnIntent: 'user-resume' })
|
||||
})
|
||||
|
||||
function makeRuntimeWithOtherPaneHolder(livePtyIds: string[]) {
|
||||
function makeRuntimeWithOtherPaneHolder(ptyIds: string[], verdict?: 'exited' | 'unverifiable') {
|
||||
const { runtimeStore } = makeParkedSessionStore('worktree-sleep')
|
||||
const parked = makeParkedRuntime(runtimeStore, {
|
||||
livePtyIds,
|
||||
livePtyIds: ptyIds,
|
||||
deps: {
|
||||
checkHookAgentPresence: async () => verdict ?? null,
|
||||
getAgentProviderSessionSnapshot: () => [
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the resume guard reads only paneKey and providerSession.
|
||||
{
|
||||
@@ -330,6 +331,16 @@ describe('OrcaRuntimeService', () => {
|
||||
expect(firstSpawnArgs(spawn).command ?? '').not.toContain('provider-session-1')
|
||||
})
|
||||
|
||||
// Why: the agent can exit to a shell that keeps the PTY alive; only the hook store proves that.
|
||||
it.each([
|
||||
['exited', true],
|
||||
['unverifiable', false]
|
||||
] as const)('live-PTY holder agent %s: resumes %s', async (verdict, resumes) => {
|
||||
const { runtime, spawn } = makeRuntimeWithOtherPaneHolder(['other-pty'], verdict)
|
||||
await userActivate(runtime)
|
||||
expect((firstSpawnArgs(spawn).command ?? '').includes('provider-session-1')).toBe(resumes)
|
||||
})
|
||||
|
||||
// Why: losing sight of the holder (a dropped SSH relay) is not evidence it stopped writing.
|
||||
it('does not resume while the other pane holding the session is unverifiable', async () => {
|
||||
const { runtime, spawn } = makeRuntimeWithOtherPaneHolder([])
|
||||
|
||||
+20
-14
@@ -177,7 +177,7 @@ export abstract class OrcaRuntimeWithApplyMobileSessionTabNavigation extends Orc
|
||||
)
|
||||
if (
|
||||
sharedWithAnotherPane ||
|
||||
this.isProviderSessionLiveInAnotherPane(record) ||
|
||||
(await this.isProviderSessionLiveInAnotherPane(record)) ||
|
||||
this.hookStoreNamesAnotherSessionForPane(record)
|
||||
) {
|
||||
return {}
|
||||
@@ -191,7 +191,9 @@ export abstract class OrcaRuntimeWithApplyMobileSessionTabNavigation extends Orc
|
||||
}
|
||||
|
||||
/** The host's hook store places this conversation in another pane not proven exited. */
|
||||
private isProviderSessionLiveInAnotherPane(record: SleepingAgentSessionRecord): boolean {
|
||||
private async isProviderSessionLiveInAnotherPane(
|
||||
record: SleepingAgentSessionRecord
|
||||
): Promise<boolean> {
|
||||
const otherPaneKeys = new Set(
|
||||
(this.getAgentProviderSessionSnapshotFn?.() ?? [])
|
||||
.filter(
|
||||
@@ -201,21 +203,25 @@ export abstract class OrcaRuntimeWithApplyMobileSessionTabNavigation extends Orc
|
||||
)
|
||||
.map((row) => row.paneKey)
|
||||
)
|
||||
if (otherPaneKeys.size === 0) {
|
||||
return false
|
||||
}
|
||||
// Why: only a proven exit clears a holder; an unverifiable PTY (dropped SSH relay) or one not
|
||||
// yet hydrated after a restart may still be writing the transcript.
|
||||
const provenExitedPaneKeys = new Set<string>()
|
||||
for (const pty of this.ptysById.values()) {
|
||||
if (pty.paneKey && otherPaneKeys.has(pty.paneKey)) {
|
||||
if (!this.isPtyKnownExited(pty.ptyId)) {
|
||||
return true
|
||||
}
|
||||
provenExitedPaneKeys.add(pty.paneKey)
|
||||
}
|
||||
const ptys = [...this.ptysById.values()]
|
||||
const holderPaneKeys = [...otherPaneKeys].filter((paneKey) => {
|
||||
const panePtys = ptys.filter((pty) => pty.paneKey === paneKey)
|
||||
return panePtys.length === 0 || panePtys.some((pty) => !this.isPtyKnownExited(pty.ptyId))
|
||||
})
|
||||
if (holderPaneKeys.length === 0) {
|
||||
return false
|
||||
}
|
||||
return [...otherPaneKeys].some((paneKey) => !provenExitedPaneKeys.has(paneKey))
|
||||
// Why: a shell can outlive the agent; the hook store's proven exit releases a pane whose PTY lives.
|
||||
const verdicts = await Promise.all(
|
||||
holderPaneKeys.map((paneKey) =>
|
||||
this.checkHookAgentPresenceFn
|
||||
? this.checkHookAgentPresenceFn(paneKey, 0).catch(() => null)
|
||||
: Promise.resolve(null)
|
||||
)
|
||||
)
|
||||
return verdicts.some((verdict) => verdict !== 'exited')
|
||||
}
|
||||
|
||||
/** The pane moved on to another conversation after the record was saved; the hook store is authoritative. */
|
||||
|
||||
Reference in New Issue
Block a user