fix(remote): harden parking review edge cases

This commit is contained in:
Brennan Benson
2026-07-30 20:46:56 -07:00
parent 0d45a4cb39
commit 1f078cb9e6
10 changed files with 274 additions and 107 deletions
+9 -6
View File
@@ -3105,7 +3105,7 @@
"ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-remote-terminal-probe-gap-recovery.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/headless-paired-remote-terminal-stall-recovery.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/paired-remote-terminal-retention-memory.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"SKIP_BUILD=1 pnpm exec playwright test tests/e2e/terminal-parked-memory.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1"
],
"testFiles": [
@@ -3167,7 +3167,10 @@
},
{
"file": "tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts",
"assertions": ["ordinary-parks paired terminals against an isolated headless Orca host"]
"assertions": [
"ordinary-parks paired terminals against an isolated headless Orca host",
"cold-activates only visible paired terminals against an isolated headless host"
]
},
{
"file": "tests/e2e/terminal-parked-memory.spec.ts",
@@ -3310,13 +3313,13 @@
"summary": "An isolated `orca serve` host and paired web renderer exhausted one terminal stream, recovered it, and preserved the original PTY and tab without exposing readiness or pairing material."
},
{
"date": "2026-07-29",
"date": "2026-07-30",
"runner": "local",
"platform": "macos",
"command": "ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1",
"command": "ORCA_E2E_WEB_CLIENT=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/headless-paired-remote-terminal-retention-memory.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1",
"result": "passed",
"durationSeconds": 14.2,
"summary": "The isolated headless `orca serve` host passed the byte-identical six-PTY ordinary-parking, memory/lag, deep-history, same-PTY, parked-output, and post-reveal I/O oracle."
"durationSeconds": 23,
"summary": "The isolated headless `orca serve` host passed ordinary parking plus cold activation: deferred tabs stayed unmounted until activation, preserved PTY identity and parked output, and resumed live input."
},
{
"date": "2026-07-29",
@@ -33,63 +33,66 @@ async function main(): Promise<void> {
}
originalWarn(message, details)
}
const startedAt = Date.now()
const before = await requestMemorySnapshot(pairing, environment.id)
let ok = 0
let subscriptionResponses = 0
let runtimeStatus: RuntimeStatus | null = null
const cleanupDurationsMs: number[] = []
for (let cycle = 0; cycle < cycles; cycle += 1) {
const result = await runCycle({
pairing,
environmentId: environment.id,
concurrency,
settleMs,
cleanupTimeoutMs
})
ok += result.ok
subscriptionResponses += result.subscriptionResponses
runtimeStatus ??= result.runtimeStatus
cleanupDurationsMs.push(result.cleanupDurationMs)
}
await wait(settleMs)
const after = await requestMemorySnapshot(pairing, environment.id)
console.warn = originalWarn
console.log(
JSON.stringify({
environment: { id: environment.id, name: environment.name },
runtime: runtimeStatus
? {
appVersion: runtimeStatus.appVersion ?? null,
capabilities: runtimeStatus.capabilities ?? [],
hostPlatform: runtimeStatus.hostPlatform ?? null
}
: null,
cycles,
concurrency,
requests: cycles * concurrency,
ok,
subscriptionResponses,
cleanupDurationMs: {
average: Math.round(
cleanupDurationsMs.reduce((total, duration) => total + duration, 0) /
cleanupDurationsMs.length
try {
const startedAt = Date.now()
const before = await requestMemorySnapshot(pairing, environment.id)
let ok = 0
let subscriptionResponses = 0
let runtimeStatus: RuntimeStatus | null = null
const cleanupDurationsMs: number[] = []
for (let cycle = 0; cycle < cycles; cycle += 1) {
const result = await runCycle({
pairing,
environmentId: environment.id,
concurrency,
settleMs,
cleanupTimeoutMs
})
ok += result.ok
subscriptionResponses += result.subscriptionResponses
runtimeStatus ??= result.runtimeStatus
cleanupDurationsMs.push(result.cleanupDurationMs)
}
await wait(settleMs)
const after = await requestMemorySnapshot(pairing, environment.id)
console.log(
JSON.stringify({
environment: { id: environment.id, name: environment.name },
runtime: runtimeStatus
? {
appVersion: runtimeStatus.appVersion ?? null,
capabilities: runtimeStatus.capabilities ?? [],
hostPlatform: runtimeStatus.hostPlatform ?? null
}
: null,
cycles,
concurrency,
requests: cycles * concurrency,
ok,
subscriptionResponses,
cleanupDurationMs: {
average: Math.round(
cleanupDurationsMs.reduce((total, duration) => total + duration, 0) /
cleanupDurationsMs.length
),
maximum: Math.max(...cleanupDurationsMs)
},
unknownResponseFrames: Array.from(unknownResponses.values()).reduce(
(total, count) => total + count,
0
),
maximum: Math.max(...cleanupDurationsMs)
},
unknownResponseFrames: Array.from(unknownResponses.values()).reduce(
(total, count) => total + count,
0
),
unknownResponseIds: unknownResponses.size,
memory: {
before: summarizeMemory(before),
after: summarizeMemory(after),
appDelta: after.app.memory - before.app.memory
},
elapsedMs: Date.now() - startedAt
})
)
unknownResponseIds: unknownResponses.size,
memory: {
before: summarizeMemory(before),
after: summarizeMemory(after),
appDelta: after.app.memory - before.app.memory
},
elapsedMs: Date.now() - startedAt
})
)
} finally {
console.warn = originalWarn
}
}
async function runCycle(args: {
@@ -134,6 +137,8 @@ async function runCycle(args: {
subscription.close()
}
const cleanupDurationMs = await waitForConnectionIdle(connection, args.cleanupTimeoutMs)
// Let cleanup replies reach the retirement cache before closing the socket.
await wait(args.settleMs)
return {
ok: responses.filter((response) => response.ok).length,
subscriptionResponses,
@@ -165,12 +170,19 @@ async function requestMemorySnapshot(
environmentId: string
): Promise<MemorySnapshot> {
const connection = new RemoteRuntimeSharedControlConnection(pairing, { environmentId })
const response = await connection.request<MemorySnapshot>('diagnostics.memory', undefined, 20_000)
connection.close()
if (!response.ok) {
throw new Error(`Memory snapshot failed: ${response.error.message}`)
try {
const response = await connection.request<MemorySnapshot>(
'diagnostics.memory',
undefined,
20_000
)
if (!response.ok) {
throw new Error(`Memory snapshot failed: ${response.error.message}`)
}
return response.result
} finally {
connection.close()
}
return response.result
}
function summarizeMemory(snapshot: MemorySnapshot): {
@@ -137,6 +137,7 @@ const mocks = vi.hoisted(() => ({
setTabColor: vi.fn(),
setTabCustomTitle: vi.fn(),
setTabPaneExpanded: vi.fn(),
shouldDeferParkedPtyExitTabClose: vi.fn(),
useContextualTour: vi.fn()
}))
@@ -213,6 +214,10 @@ vi.mock('@/components/terminal-pane/use-terminal-tab-cold-parking', () => ({
useTerminalTabColdParking: () => parkingBox.parkedTabIds
}))
vi.mock('@/components/terminal-pane/terminal-parked-tab-watchers', () => ({
shouldDeferParkedPtyExitTabClose: mocks.shouldDeferParkedPtyExitTabClose
}))
vi.mock('@/components/terminal-pane/terminal-ime-input-context-refresh', () => ({
isTerminalImeInputContextRefreshing: mocks.isTerminalImeInputContextRefreshing
}))
@@ -825,6 +830,7 @@ describe('FloatingTerminalPanel close behavior', () => {
mocks.isTerminalImeInputContextRefreshing.mockReturnValue(false)
mocks.isWebRuntimeSessionActive.mockReturnValue(false)
mocks.pickFloatingMarkdownDocument.mockResolvedValue(null)
mocks.shouldDeferParkedPtyExitTabClose.mockReturnValue(false)
const localStorage = {
clear: vi.fn(),
getItem: vi.fn(() => null),
@@ -2522,11 +2528,16 @@ describe('FloatingTerminalPanel close behavior', () => {
const element = await renderPanel(true, onOpenChange)
const terminalPane = findByTypeName(element, 'TerminalPane')
;(terminalPane.props.onPtyExit as () => void)()
expect(mocks.closeTab).toHaveBeenCalledWith('tab-1', { reason: 'pty-exit' })
;(terminalPane.props.onPtyExit as (ptyId: string) => void)('pty-1')
expect(mocks.shouldDeferParkedPtyExitTabClose).toHaveBeenCalledWith('tab-1', 'pty-1')
expect(mocks.closeTerminalTab).toHaveBeenCalledWith('tab-1', {
lifecyclePtyId: 'pty-1',
reason: 'pty-exit'
})
expect(mocks.closeTab).not.toHaveBeenCalled()
expect(onOpenChange).not.toHaveBeenCalled()
mocks.closeTab.mockClear()
mocks.closeTerminalTab.mockClear()
;(terminalPane.props.onCloseTab as () => void)()
// Explicit pane close routes through the confirmed-close authority, not a raw pty-exit prune.
expect(mocks.closeTerminalTab).toHaveBeenCalledWith(
@@ -2537,6 +2548,23 @@ describe('FloatingTerminalPanel close behavior', () => {
expect(onOpenChange).not.toHaveBeenCalled()
})
it('preserves split siblings when a parked PTY exits during reveal', async () => {
setFloatingTabs([makeTab({ id: 'tab-1' })])
mocks.shouldDeferParkedPtyExitTabClose.mockReturnValueOnce(true)
await renderPanel(true)
runEffects()
await Promise.resolve()
const element = await renderPanel(true)
const terminalPane = findByTypeName(element, 'TerminalPane')
;(terminalPane.props.onPtyExit as (ptyId: string) => void)('split-pty')
expect(mocks.shouldDeferParkedPtyExitTabClose).toHaveBeenCalledWith('tab-1', 'split-pty')
expect(mocks.closeTerminalTab).not.toHaveBeenCalled()
expect(mocks.closeTab).not.toHaveBeenCalled()
})
it('renders and closes simulator tabs in the floating workspace', async () => {
const tab = setFloatingSimulatorTab()
@@ -12,6 +12,7 @@ import { useContextualTour } from '@/components/contextual-tours/use-contextual-
import TabBar from '@/components/tab-bar/TabBar'
import { resolveGroupTabFromVisibleId } from '@/components/tab-group/tab-group-visible-id'
import TerminalPane, { type TerminalPaneHandle } from '@/components/terminal-pane/TerminalPane'
import { shouldDeferParkedPtyExitTabClose } from '@/components/terminal-pane/terminal-parked-tab-watchers'
import { useTerminalTabColdParking } from '@/components/terminal-pane/use-terminal-tab-cold-parking'
import { isTerminalPaneCloseChord } from '@/components/terminal-pane/terminal-shortcut-policy'
import { isTerminalImeInputContextRefreshing } from '@/components/terminal-pane/terminal-ime-input-context-refresh'
@@ -1863,7 +1864,15 @@ export function FloatingTerminalPanel({
// atlas to corrupt) while hidden, and the resume on
// reopen rebuilds the renderer from scratch.
isVisible={isActive && open}
onPtyExit={() => closeTab(tab.id, { reason: 'pty-exit' })}
onPtyExit={(ptyId) => {
if (shouldDeferParkedPtyExitTabClose(tab.id, ptyId)) {
return
}
closeTerminalTab(tab.id, {
reason: 'pty-exit',
lifecyclePtyId: ptyId
})
}}
onCloseTab={() => closeFloatingItemConfirmed(tab.id)}
/>
</div>
@@ -30,4 +30,33 @@ describe('SharedControlRetiredRequestIds', () => {
expect(ids.has('request-2')).toBe(true)
expect(ids.has('request-3')).toBe(true)
})
it('refreshes an existing id to the newest eviction rank', () => {
const ids = new SharedControlRetiredRequestIds({ maxIds: 2 })
ids.retire('request-1')
ids.retire('request-2')
ids.retire('request-1')
ids.retire('request-3')
expect(ids.has('request-1')).toBe(true)
expect(ids.has('request-2')).toBe(false)
expect(ids.has('request-3')).toBe(true)
})
it('expires ids correctly after the clock moves backward', () => {
let now = 1_000
const ids = new SharedControlRetiredRequestIds({
ttlMs: 100,
now: () => now
})
ids.retire('request-1')
now = 900
ids.retire('request-2')
now = 1_001
expect(ids.has('request-1')).toBe(true)
expect(ids.has('request-2')).toBe(false)
})
})
@@ -45,10 +45,9 @@ export class SharedControlRetiredRequestIds {
private pruneExpired(now: number): void {
for (const [requestId, expiresAt] of this.ids) {
if (expiresAt > now) {
return
if (expiresAt <= now) {
this.ids.delete(requestId)
}
this.ids.delete(requestId)
}
}
}
@@ -4,7 +4,7 @@ import { launchPairedWebClient, type PairedWebClient } from './helpers/paired-el
import { runPairedTerminalColdActivationOracle } from './helpers/paired-terminal-cold-activation-oracle'
import { runPairedTerminalParkingOracle } from './helpers/paired-terminal-parking-oracle'
test('ordinary-parks paired terminals against an isolated headless Orca host @headful', async ({
test('ordinary-parks paired terminals against an isolated headless Orca host', async ({
testRepoPath
}) => {
test.setTimeout(240_000)
@@ -40,7 +40,7 @@ test('ordinary-parks paired terminals against an isolated headless Orca host @he
}
})
test('cold-activates only visible paired terminals against an isolated headless host @headful', async ({
test('cold-activates only visible paired terminals against an isolated headless host', async ({
testRepoPath
}) => {
test.setTimeout(240_000)
@@ -22,6 +22,8 @@ type ServeReady = {
}
const STARTUP_DIAGNOSTIC_LIMIT = 8_000
const PAIRING_URL_PATTERN = /orca:\/\/[^\s"\\]+/g
const WEB_CLIENT_PAIRING_PATTERN = /([#&]pairing=)[^&\s"\\]+/g
export type HeadlessPairedRuntimeHost = {
app: ElectronApplication
@@ -30,21 +32,62 @@ export type HeadlessPairedRuntimeHost = {
offer: RuntimeDesktopPairingOffer
}
function appendStartupDiagnostic(current: string, chunk: Buffer): string {
return `${current}${chunk.toString()}`.slice(-STARTUP_DIAGNOSTIC_LIMIT)
export class HeadlessPairedRuntimeStartupDiagnosticBuffer {
private completed = ''
private discardingOversizedLine = false
private pending = ''
append(chunk: Buffer): void {
let value = chunk.toString()
if (this.discardingOversizedLine) {
const newlineIndex = value.indexOf('\n')
if (newlineIndex === -1) {
return
}
value = value.slice(newlineIndex + 1)
this.discardingOversizedLine = false
}
const combined = `${this.pending}${value}`
const newlineIndex = combined.lastIndexOf('\n')
if (newlineIndex !== -1) {
this.completed = `${this.completed}${redactPairingMaterial(
combined.slice(0, newlineIndex + 1)
)}`.slice(-STARTUP_DIAGNOSTIC_LIMIT)
this.pending = combined.slice(newlineIndex + 1)
} else {
this.pending = combined
}
if (this.pending.length > STARTUP_DIAGNOSTIC_LIMIT) {
this.pending = ''
this.discardingOversizedLine = true
}
}
read(): string {
const pending = this.discardingOversizedLine ? '' : redactPairingMaterial(this.pending)
return `${this.completed}${pending}`.slice(-STARTUP_DIAGNOSTIC_LIMIT)
}
}
function formatStartupDiagnostics(stdout: string, stderr: string): string {
const redactPairingCode = (value: string): string =>
value.replace(/orca:\/\/[^\s"\\]+/g, 'orca://[redacted]')
export function formatHeadlessPairedRuntimeStartupDiagnostics(
stdout: string,
stderr: string
): string {
return [
stdout ? `stdout:\n${redactPairingCode(stdout)}` : '',
stderr ? `stderr:\n${redactPairingCode(stderr)}` : ''
stdout ? `stdout:\n${redactPairingMaterial(stdout)}` : '',
stderr ? `stderr:\n${redactPairingMaterial(stderr)}` : ''
]
.filter(Boolean)
.join('\n')
}
function redactPairingMaterial(value: string): string {
return value
.replace(PAIRING_URL_PATTERN, 'orca://[redacted]')
.replace(WEB_CLIENT_PAIRING_PATTERN, '$1[redacted]')
}
async function readPairingOffer(app: ElectronApplication): Promise<RuntimeDesktopPairingOffer> {
const child = app.process()
const stdout = child.stdout
@@ -53,12 +96,15 @@ async function readPairingOffer(app: ElectronApplication): Promise<RuntimeDeskto
}
return new Promise((resolve, reject) => {
let buffered = ''
let stdoutDiagnostic = ''
let stderrDiagnostic = ''
const stdoutDiagnostic = new HeadlessPairedRuntimeStartupDiagnosticBuffer()
const stderrDiagnostic = new HeadlessPairedRuntimeStartupDiagnosticBuffer()
const stderr = child.stderr
const timeout = setTimeout(() => {
cleanup()
const diagnostics = formatStartupDiagnostics(stdoutDiagnostic, stderrDiagnostic)
const diagnostics = formatHeadlessPairedRuntimeStartupDiagnostics(
stdoutDiagnostic.read(),
stderrDiagnostic.read()
)
reject(
new Error(
`Headless runtime did not publish pairing readiness${diagnostics ? `\n${diagnostics}` : ''}`
@@ -73,7 +119,10 @@ async function readPairingOffer(app: ElectronApplication): Promise<RuntimeDeskto
}
const onClose = (code: number | null, signal: NodeJS.Signals | null): void => {
cleanup()
const diagnostics = formatStartupDiagnostics(stdoutDiagnostic, stderrDiagnostic)
const diagnostics = formatHeadlessPairedRuntimeStartupDiagnostics(
stdoutDiagnostic.read(),
stderrDiagnostic.read()
)
reject(
new Error(
`Headless runtime exited before pairing readiness (code=${code ?? 'none'}, signal=${signal ?? 'none'})${diagnostics ? `\n${diagnostics}` : ''}`
@@ -81,10 +130,10 @@ async function readPairingOffer(app: ElectronApplication): Promise<RuntimeDeskto
)
}
const onStderr = (chunk: Buffer): void => {
stderrDiagnostic = appendStartupDiagnostic(stderrDiagnostic, chunk)
stderrDiagnostic.append(chunk)
}
const onData = (chunk: Buffer): void => {
stdoutDiagnostic = appendStartupDiagnostic(stdoutDiagnostic, chunk)
stdoutDiagnostic.append(chunk)
buffered += chunk.toString()
const lines = buffered.split(/\r?\n/)
buffered = lines.pop() ?? ''
@@ -0,0 +1,45 @@
import { describe, expect, it } from 'vitest'
import {
HeadlessPairedRuntimeStartupDiagnosticBuffer,
formatHeadlessPairedRuntimeStartupDiagnostics
} from './headless-paired-runtime-host'
describe('headless paired runtime startup diagnostics', () => {
it('redacts pairing URLs before truncation can remove their prefix', () => {
const pairingUrl = `orca://${'secret'.repeat(1_500)}`
const diagnostic = new HeadlessPairedRuntimeStartupDiagnosticBuffer()
diagnostic.append(Buffer.from(`prefix${pairingUrl}\n`))
expect(diagnostic.read()).toBe('prefixorca://[redacted]\n')
expect(diagnostic.read()).not.toContain('secret')
})
it('redacts pairing URLs split across chunks', () => {
const diagnostic = new HeadlessPairedRuntimeStartupDiagnosticBuffer()
diagnostic.append(Buffer.from('orca://p'))
diagnostic.append(Buffer.from('airing-secret\nready'))
expect(formatHeadlessPairedRuntimeStartupDiagnostics(diagnostic.read(), '')).toBe(
'stdout:\norca://[redacted]\nready'
)
})
it('redacts encoded pairing material from web-client URLs', () => {
const pairingUrl = encodeURIComponent('orca://pairing-secret')
const diagnostic = new HeadlessPairedRuntimeStartupDiagnosticBuffer()
diagnostic.append(Buffer.from(`https://host/web-index.html#pairing=${pairingUrl}\n`))
expect(diagnostic.read()).toBe('https://host/web-index.html#pairing=[redacted]\n')
expect(diagnostic.read()).not.toContain('pairing-secret')
})
it('drops oversized unfinished lines instead of retaining a pairing fragment', () => {
const diagnostic = new HeadlessPairedRuntimeStartupDiagnosticBuffer()
diagnostic.append(Buffer.from(`orca://${'secret'.repeat(1_500)}`))
diagnostic.append(Buffer.from('still-secret\nsafe'))
expect(diagnostic.read()).toBe('safe')
})
})
@@ -132,37 +132,30 @@ export async function runPairedTerminalColdActivationOracle(
})
}
const originalPtyIds = await expect
let originalPtyIds: string[] | null = null
await expect
.poll(
() =>
page.evaluate(
async () => {
originalPtyIds = await page.evaluate(
({ tabIds, targetWorktreeId }) => {
const tabs = window.__store?.getState().tabsByWorktree[targetWorktreeId] ?? []
const byId = new Map(tabs.map((tab) => [tab.id, tab.ptyId]))
const ids = tabIds.map((id) => byId.get(id) ?? null)
return ids.every((id) => typeof id === 'string') ? ids : null
return ids.every((id): id is string => typeof id === 'string') ? ids : null
},
{
tabIds: pendingTabs.map((tab) => tab.tabId),
targetWorktreeId: worktreeId
}
),
)
return originalPtyIds
},
{ timeout: 30_000 }
)
.not.toBeNull()
.then(() =>
page.evaluate(
({ tabIds, targetWorktreeId }) => {
const tabs = window.__store?.getState().tabsByWorktree[targetWorktreeId] ?? []
const byId = new Map(tabs.map((tab) => [tab.id, tab.ptyId]))
return tabIds.map((id) => byId.get(id)!)
},
{
tabIds: pendingTabs.map((tab) => tab.tabId),
targetWorktreeId: worktreeId
}
)
)
if (originalPtyIds === null) {
throw new Error('Paired cold-activation PTY ids were not captured')
}
const tabs: ColdTab[] = pendingTabs.map((tab, index) => ({
...tab,
originalPtyId: originalPtyIds[index]!