fix(pty): preserve fence refusals across providers and UI

This commit is contained in:
Merge Sim
2026-08-31 12:51:07 -07:00
parent 3cf6937dc1
commit cb2c5bec77
7 changed files with 38 additions and 4 deletions
@@ -19,7 +19,7 @@ import {
describe('daemon protocol version', () => {
it('ships bounded history transfer after the 2031-unsubscribe fact', () => {
expect(PROTOCOL_VERSION).toBe(36)
expect(PROTOCOL_VERSION).toBe(37)
expect(CONTENT_ADDRESSED_SHELL_WRAPPER_DAEMON_PROTOCOL_VERSION).toBe(36)
expect(ASYNC_CWD_VALIDATION_DAEMON_PROTOCOL_VERSION).toBe(35)
expect(CODEX_SHELL_LAUNCH_PREFLIGHT_DAEMON_PROTOCOL_VERSION).toBe(34)
@@ -33,7 +33,7 @@ describe('daemon protocol version', () => {
expect(AGENT_SESSION_CLAIM_DAEMON_PROTOCOL_VERSION).toBe(26)
expect(AGENT_SESSION_CREATE_OPERATION_DAEMON_PROTOCOL_VERSION).toBe(26)
expect(PREVIOUS_DAEMON_PROTOCOL_VERSIONS).toEqual(
Array.from({ length: 35 }, (_, index) => index + 1)
Array.from({ length: 36 }, (_, index) => index + 1)
)
})
+1 -1
View File
@@ -31,7 +31,7 @@ export const CLEAN_DISCONNECT_PROTOCOL_VERSION = 24
export const MODE_2031_UNSUBSCRIBE_FACT_PROTOCOL_VERSION = 29
export const PREVIOUS_DAEMON_PROTOCOL_VERSIONS = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
28, 29, 30, 31, 32, 33, 34, 35
28, 29, 30, 31, 32, 33, 34, 35, 36
] as const
export function supportsPtyStartupIngress(protocolVersion: number): boolean {
+1
View File
@@ -72,6 +72,7 @@ export async function shutdownSinglePty(
// A pending daemon teardown can reject a stale incarnation without throwing.
// Keep client ownership intact until bulk verification classifies that refusal.
if (result?.fenceUnavailable) {
deps.runtime?.clearPtyStopRequested?.(id)
return result
}
} catch (err) {
+1 -1
View File
@@ -217,7 +217,7 @@ export async function shutdownLocalPty(
): Promise<PtyShutdownResult | void> {
const expectedIncarnationId = ptyIncarnations.get(id)
if (opts.incarnationId !== undefined && opts.incarnationId !== expectedIncarnationId) {
throw new Error(`PTY incarnation mismatch: ${id}`)
return { fenceUnavailable: true }
}
cancelPendingLocalPtySpawns(id)
const pending = ptyShutdownOperations.get(id)
+4
View File
@@ -20823,6 +20823,10 @@ export class OrcaRuntimeService {
this.stopRequestedPtyIds.add(ptyId)
}
clearPtyStopRequested(ptyId: string): void {
this.stopRequestedPtyIds.delete(ptyId)
}
isPtyStopRequested(ptyId: string): boolean {
return this.stopRequestedPtyIds.has(ptyId)
}
@@ -36,6 +36,7 @@ import type {
UnifiedWorktreeRow
} from './resource-usage-merge-types'
import { buildResourceSessionBindingIndex } from './resource-session-bindings'
import { killVerdictFields } from './resource-session-kill-verdict-fields'
// ─── Helpers ────────────────────────────────────────────────────────
@@ -134,6 +135,11 @@ function resolveDaemonSessionLabel(
export const UNATTRIBUTED_REPO_ID = '__unattributed__'
export const UNATTRIBUTED_REPO_NAME = 'Unattributed'
type SessionWithKillVerdict = DaemonSession & {
killVerdict?: UnifiedSessionRow['killVerdict']
killReason?: string
}
export function mergeSnapshotAndSessions(
snapshot: MemorySnapshot | null,
daemonSessions: readonly DaemonSession[],
@@ -152,6 +158,9 @@ export function mergeSnapshotAndSessions(
const ownershipBySessionId = new Map(
daemonSessions.map((session) => [session.id, session.agentOwnership])
)
const verdictBySessionId = new Map(
daemonSessions.map((session) => [session.id, session as SessionWithKillVerdict])
)
function isRepoRemote(repoId: string): boolean {
// Why: missing entry === we don't know about this repo (typically the
@@ -206,8 +215,10 @@ export function mergeSnapshotAndSessions(
const sessions: UnifiedSessionRow[] = wt.sessions.map((s) => {
seenSessionIds.add(s.sessionId)
const tabId = index.ptyIdToTabId.get(s.sessionId) ?? null
const verdict = verdictBySessionId.get(s.sessionId)
return {
sessionId: s.sessionId,
...(verdict ? killVerdictFields(verdict) : {}),
paneKey: s.paneKey,
pid: s.pid,
label: resolveSnapshotSessionLabel(s, wt.worktreeId, ctx),
@@ -297,6 +308,7 @@ export function mergeSnapshotAndSessions(
row.sessions.push({
sessionId: session.id,
...(session.incarnationId ? { incarnationId: session.incarnationId } : {}),
...killVerdictFields(session as SessionWithKillVerdict),
paneKey: null,
pid: 0,
label: resolveDaemonSessionLabel(session, worktreeId, tabId, ctx),
@@ -0,0 +1,17 @@
import type { UnifiedSessionRow } from './resource-usage-merge-types'
type SessionWithKillVerdict = {
killVerdict?: UnifiedSessionRow['killVerdict']
killReason?: string
}
export function killVerdictFields(
session: SessionWithKillVerdict
): Pick<UnifiedSessionRow, 'killVerdict' | 'killReason'> {
return session.killVerdict
? {
killVerdict: session.killVerdict,
...(session.killReason ? { killReason: session.killReason } : {})
}
: {}
}