mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
fix(orcad): keep terminal models at the daemon's live depth and bound provider snapshot waits (#35679)
* fix(orcad): keep orcad's terminal models at the daemon's live depth orcad's per-PTY headless terminal model kept 5,000 rows of scrollback, but orcad has no renderer to reveal from: requested snapshots read the daemon first and subscribe snapshots read at most 1,000 rows. The model now mirrors the daemon's live window, from the same resolver the daemon uses. The execution-context reseed also awaited its provider snapshot with no bound, so a provider that never answered held every live chunk queued behind it. It now uses the authoritative snapshot timeout. Refs #34410 Refs #33870 * fix(orcad): keep provider preference when the reseed snapshot times out Refs #34410 Refs #33870 * fix(runtime): bound every provider snapshot wait in one place serializeProviderTerminalBuffer now applies the authoritative snapshot timeout when a caller passes none, so no reader can wait on a provider that never answers. The execution-context reseed relies on that bound; when its capture times out it keeps the PTY provider-preferred and reseeds again once the capture lands, instead of staying on the provider path. Refs #34410 Refs #33870 * fix(runtime): reseed once after a late provider snapshot, trust the model on none awaitProviderTerminalBuffer reports the capture still running when the bounded wait gives up, instead of callers inferring it from the acquisition map. The execution-context reseed retries once when that capture lands with a snapshot, and stops preferring the provider when it lands with none, so a slow provider can neither loop reseeds nor pin the PTY to the provider path. Refs #34410 Refs #33870 * fix(runtime): never trust a late-reseed model that found no snapshot The once-only retry disposes a model that had parsed the live bytes since the timeout. If its own capture then fails, the replacement holds less than its predecessor, so the PTY keeps preferring the provider instead of treating that model as complete. Refs #34410 Refs #33870
This commit is contained in:
@@ -90,6 +90,8 @@ async function startOrcadRuntime(
|
||||
const { buildOrcadServeReadiness } = await import('./orcad-serve-readiness')
|
||||
const { createOrcadProfileStateStartup } = await import('./orcad-profile-state-startup')
|
||||
const { daemonOwnsFreshPersistentPtys } = await import('../daemon/daemon-provider-state')
|
||||
const { resolveDaemonSessionScrollbackRows } =
|
||||
await import('../daemon/daemon-session-scrollback-window')
|
||||
const { collectOrcadHealth } = await import('./orcad-health')
|
||||
const { agentHookServer } = await import('../agent-hooks/server')
|
||||
const { isAgentStatusHooksEnabled } = await import('../agent-hooks/managed-agent-hook-controls')
|
||||
@@ -210,6 +212,8 @@ async function startOrcadRuntime(
|
||||
canRecoverPersistentLocalPtys: () => daemonOwnsFreshPersistentPtys(),
|
||||
// Node hosts cannot promote into desktop windows; override the constructor’s openable default.
|
||||
getDesktopWindowStatus: () => 'blocked',
|
||||
// No renderer reveals from this model; it keeps the daemon's live window, which deeper snapshot reads ask first.
|
||||
headlessTerminalScrollbackRows: resolveDaemonSessionScrollbackRows(),
|
||||
// Unchanged settings would reconstruct a healthy search index; deduplicate before apply.
|
||||
applySessionSearchSettings: (before, after) => {
|
||||
const next = changedAiVaultSearchSettings(before, after)
|
||||
|
||||
@@ -97,6 +97,12 @@ const DECLARED_HOST_DIFFERENCES: readonly DeclaredDifference[] = [
|
||||
reason:
|
||||
'Sleep reaches paired servers and suspends VMs from the desktop only; orcad refuses those parts.'
|
||||
},
|
||||
{
|
||||
wiring: 'ctor:headlessTerminalScrollbackRows',
|
||||
missingOn: 'electron',
|
||||
reason:
|
||||
"Desktop reveals hidden panes from its model at desktop depth; renderer-less orcad needs only the daemon's live window."
|
||||
},
|
||||
{
|
||||
wiring: 'rpc-option:exposeNetworkByDefault',
|
||||
missingOn: 'orcad',
|
||||
|
||||
@@ -72,6 +72,8 @@ export type RuntimeConstructionDependencies = {
|
||||
prepareCodexCatalogProbeHome?: (homePath: string) => void
|
||||
buildAgentHookPtyEnv?: () => Record<string, string>
|
||||
getDesktopWindowStatus?: () => RuntimeDesktopWindowStatus
|
||||
/** Live headless-model depth; a host with no renderer to reveal from needs only its PTY provider's live window. */
|
||||
headlessTerminalScrollbackRows?: number
|
||||
agentSessionClaimSigner?: AgentSessionClaimSigner
|
||||
skillTransactionRecovery?: Promise<unknown>
|
||||
applySessionSearchSettings?: SessionSearchSettingsApply
|
||||
|
||||
@@ -43,6 +43,7 @@ import { registerConptyDa1OverrideInstaller } from '../terminal-model-query-auth
|
||||
import { registerTerminalViewAttributesApplier } from '../terminal-view-attribute-store'
|
||||
import { RuntimeMachineName } from '../runtime-machine-name'
|
||||
import { setPRConflictSummaryRemoteFetches } from '../../github/conflict-summary'
|
||||
import { DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT } from '../../../shared/terminal-scrollback-policy'
|
||||
|
||||
// oxlint-disable-next-line typescript/no-unsafe-declaration-merging -- SAFETY: The constructor installs every checked canonical surface.
|
||||
export abstract class OrcaRuntimeWithStateFields extends OrcaRuntimeWithLinearCommands {
|
||||
@@ -73,6 +74,7 @@ export abstract class OrcaRuntimeWithStateFields extends OrcaRuntimeWithLinearCo
|
||||
declare protected readonly getPairedDeviceNameFn: HostServices['getPairedDeviceNameFn']
|
||||
declare protected readonly buildAgentHookPtyEnv: HostServices['buildAgentHookPtyEnv']
|
||||
declare protected readonly getDesktopWindowStatusFn: HostServices['getDesktopWindowStatusFn']
|
||||
protected readonly headlessTerminalScrollbackRows: number
|
||||
declare protected readonly prepareAiVaultSessionResumeFn: HostServices['prepareAiVaultSessionResumeFn']
|
||||
declare protected readonly prepareCodexStructuredLaunchFn: HostServices['prepareCodexStructuredLaunchFn']
|
||||
declare protected readonly resolveCodexStructuredLaunchHomeFn: HostServices['resolveCodexStructuredLaunchHomeFn']
|
||||
@@ -251,6 +253,8 @@ export abstract class OrcaRuntimeWithStateFields extends OrcaRuntimeWithLinearCo
|
||||
this.onClaudeTerminalEvidence = deps?.onClaudeTerminalEvidence ?? null
|
||||
this.buildAgentHookPtyEnv = deps?.buildAgentHookPtyEnv ?? null
|
||||
this.getDesktopWindowStatusFn = deps?.getDesktopWindowStatus ?? (() => 'openable')
|
||||
this.headlessTerminalScrollbackRows =
|
||||
deps?.headlessTerminalScrollbackRows ?? DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT
|
||||
this.prepareAiVaultSessionResumeFn = deps?.prepareAiVaultSessionResume ?? null
|
||||
this.prepareCodexStructuredLaunchFn = deps?.prepareCodexStructuredLaunch ?? null
|
||||
this.resolveCodexStructuredLaunchHomeFn = deps?.resolveCodexStructuredLaunchHome ?? null
|
||||
|
||||
@@ -56,8 +56,10 @@ export class HydrationRuntime extends OrcaRuntimeService {
|
||||
|
||||
const runtimes: HydrationRuntime[] = []
|
||||
|
||||
export function createHydrationRuntime(): HydrationRuntime {
|
||||
const runtime = new HydrationRuntime(store)
|
||||
export function createHydrationRuntime(
|
||||
deps?: ConstructorParameters<typeof OrcaRuntimeService>[2]
|
||||
): HydrationRuntime {
|
||||
const runtime = new HydrationRuntime(store, undefined, deps)
|
||||
syncSinglePty(runtime, PTY_ID)
|
||||
runtimes.push(runtime)
|
||||
return runtime
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import { DAEMON_SESSION_SCROLLBACK_ROWS } from '../daemon/daemon-session-scrollback-window'
|
||||
import { AUTHORITATIVE_TERMINAL_SNAPSHOT_TIMEOUT_MS } from './orca-runtime-postlude'
|
||||
import type { PtyProviderBufferSnapshot } from '../providers/pty-provider-contract'
|
||||
import { deferred } from './orca-runtime-test-fixtures.spec'
|
||||
import { createHydrationRuntime, PTY_ID, SIZE } from './headless-hydration-ownership-test-fixture'
|
||||
|
||||
it('retains only the configured scrollback depth', async () => {
|
||||
const runtime = createHydrationRuntime({
|
||||
headlessTerminalScrollbackRows: DAEMON_SESSION_SCROLLBACK_ROWS
|
||||
})
|
||||
const lines = Array.from({ length: 3000 }, (_, i) => `L${String(i + 1).padStart(4, '0')}`)
|
||||
runtime.onPtyData(PTY_ID, lines.join('\r\n'), 1)
|
||||
await runtime.model().writeChain
|
||||
const snapshot = await runtime.serializeMainTerminalBuffer(PTY_ID, { scrollbackRows: 5000 })
|
||||
const retained = `${snapshot?.scrollbackAnsi ?? ''}${snapshot?.data ?? ''}`.match(/L\d{4}/g)
|
||||
expect(retained).toHaveLength(DAEMON_SESSION_SCROLLBACK_ROWS + SIZE.rows)
|
||||
expect(retained?.at(-1)).toBe('L3000')
|
||||
})
|
||||
|
||||
function hangingProvider() {
|
||||
const runtime = createHydrationRuntime()
|
||||
const late = deferred<PtyProviderBufferSnapshot | null>()
|
||||
const serializeProviderBuffer = vi
|
||||
.fn<() => Promise<PtyProviderBufferSnapshot | null>>()
|
||||
.mockReturnValueOnce(late.promise)
|
||||
runtime.setPtyController({
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
getSize: () => SIZE,
|
||||
serializeProviderBuffer
|
||||
})
|
||||
return { runtime, late, serializeProviderBuffer }
|
||||
}
|
||||
|
||||
it('bounds a provider snapshot that does not answer, then reseeds once it lands', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const { runtime, late, serializeProviderBuffer } = hangingProvider()
|
||||
runtime.replaceExecutionContext()
|
||||
const stalled = runtime.model()
|
||||
runtime.onPtyData(PTY_ID, 'LIVE-AFTER-SEED', 1)
|
||||
await vi.advanceTimersByTimeAsync(AUTHORITATIVE_TERMINAL_SNAPSHOT_TIMEOUT_MS)
|
||||
await stalled.writeChain
|
||||
expect(stalled.emulator.getVisibleLines().join('\n')).toContain('LIVE-AFTER-SEED')
|
||||
// Partial until a seed lands, so readers keep asking the provider.
|
||||
expect(runtime.retainedState().providerPreferred).toBe(true)
|
||||
|
||||
serializeProviderBuffer.mockResolvedValue({
|
||||
...SIZE,
|
||||
data: 'FRESH-SEED',
|
||||
seq: 1,
|
||||
source: 'headless'
|
||||
})
|
||||
late.resolve({ ...SIZE, data: 'LATE-SEED', seq: 0, source: 'headless' })
|
||||
await vi.waitFor(() => expect(runtime.model()).not.toBe(stalled))
|
||||
await runtime.model().writeChain
|
||||
expect(runtime.model().emulator.getVisibleLines().join('\n')).toContain('FRESH-SEED')
|
||||
expect(runtime.retainedState().providerPreferred).toBe(false)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps preferring the provider when the late retry finds no snapshot', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const { runtime, late, serializeProviderBuffer } = hangingProvider()
|
||||
runtime.replaceExecutionContext()
|
||||
const stalled = runtime.model()
|
||||
await vi.advanceTimersByTimeAsync(AUTHORITATIVE_TERMINAL_SNAPSHOT_TIMEOUT_MS)
|
||||
serializeProviderBuffer.mockResolvedValue(null)
|
||||
late.resolve({ ...SIZE, data: 'LATE-SEED', seq: 0, source: 'headless' })
|
||||
await vi.waitFor(() => expect(runtime.model()).not.toBe(stalled))
|
||||
await runtime.model().writeChain
|
||||
// The retry's model lost what its predecessor held, so it must not become authoritative.
|
||||
expect(runtime.retainedState().providerPreferred).toBe(true)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('trusts the model when a timed-out capture lands with no snapshot', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const { runtime, late, serializeProviderBuffer } = hangingProvider()
|
||||
runtime.replaceExecutionContext()
|
||||
const model = runtime.model()
|
||||
await vi.advanceTimersByTimeAsync(AUTHORITATIVE_TERMINAL_SNAPSHOT_TIMEOUT_MS)
|
||||
late.resolve(null)
|
||||
await vi.waitFor(() => expect(runtime.retainedState().providerPreferred).toBe(false))
|
||||
expect(runtime.model()).toBe(model)
|
||||
expect(serializeProviderBuffer).toHaveBeenCalledOnce()
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('bounds every reader of a provider snapshot that does not answer', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const { runtime } = hangingProvider()
|
||||
runtime.preferProvider()
|
||||
const read = runtime.serializeTerminalBuffer(PTY_ID)
|
||||
// Preferred-provider read, then the no-model fallback: two bounded waits.
|
||||
await vi.advanceTimersByTimeAsync(2 * AUTHORITATIVE_TERMINAL_SNAPSHOT_TIMEOUT_MS)
|
||||
await expect(read).resolves.toBeNull()
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
@@ -175,6 +175,11 @@ export type RuntimeVisibleTerminalState = {
|
||||
generation: number
|
||||
}
|
||||
|
||||
export type ProviderTerminalBufferWait = {
|
||||
snapshot: PtyProviderBufferSnapshot | null
|
||||
late: Promise<PtyProviderBufferSnapshot | null> | null
|
||||
}
|
||||
|
||||
export type ProviderBufferAcquisition = {
|
||||
generation: number
|
||||
scrollbackRows: number
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import { TrustDialogStatusObserver } from '../../trust-dialog-status-observer'
|
||||
import type { LaunchedAgentForeground } from '../../launched-agent-foreground'
|
||||
import type { TuiAgent } from '../../../../shared/tui-agent'
|
||||
import { DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT } from '../../../../shared/terminal-scrollback-policy'
|
||||
import {
|
||||
detectAgentStatusFromTitle,
|
||||
normalizeTerminalTitle
|
||||
@@ -92,9 +91,9 @@ export abstract class OrcaRuntimeWithMaybeHydrateHeadlessFromRenderer extends Or
|
||||
}
|
||||
try {
|
||||
// Retain normal-buffer scrollback while a TUI is in alt-screen so pre-TUI history remains restorable (#6106).
|
||||
// Desktop depth (this emulator's retention): a hidden-output reveal repaints the pane from this model.
|
||||
// This emulator's retention: a hidden-output reveal repaints the pane from this model.
|
||||
const rendered = await controller.serializeBuffer!(ptyId, {
|
||||
scrollbackRows: DESKTOP_TERMINAL_SCROLLBACK_ROWS_DEFAULT
|
||||
scrollbackRows: this.headlessTerminalScrollbackRows
|
||||
})
|
||||
if (
|
||||
this.headlessTerminals.get(ptyId) !== state ||
|
||||
|
||||
@@ -41,6 +41,7 @@ export abstract class OrcaRuntimeWithCreatePtyHeadlessTerminalState extends Orca
|
||||
const emulator = new HeadlessEmulator({
|
||||
cols: dims.cols,
|
||||
rows: dims.rows,
|
||||
scrollback: this.headlessTerminalScrollbackRows,
|
||||
pathFlavor,
|
||||
remotePosixFileUriAuthority:
|
||||
!!this.ptysById.get(ptyId)?.connectionId && pathFlavor !== 'win32',
|
||||
@@ -112,19 +113,47 @@ export abstract class OrcaRuntimeWithCreatePtyHeadlessTerminalState extends Orca
|
||||
return state
|
||||
}
|
||||
|
||||
protected replaceHeadlessTerminalAfterExecutionContextChange(ptyId: string): void {
|
||||
protected replaceHeadlessTerminalAfterExecutionContextChange(
|
||||
ptyId: string,
|
||||
retryLateSeed = true
|
||||
): void {
|
||||
this.disposeHeadlessTerminal(ptyId)
|
||||
this.providerSnapshotPreferredPtys.add(ptyId)
|
||||
const dims = this.getTerminalSize(ptyId) ?? { cols: 80, rows: 24 }
|
||||
const state = this.createPtyHeadlessTerminalState(ptyId, dims)
|
||||
this.headlessTerminals.set(ptyId, state)
|
||||
const generation = this.getPtyLifecycleGeneration(ptyId)
|
||||
let seedTimedOut = false
|
||||
let seeded = false
|
||||
state.writeChain = state.writeChain
|
||||
.then(async () => {
|
||||
if (this.headlessTerminals.get(ptyId) !== state) {
|
||||
return
|
||||
}
|
||||
const snapshot = await this.serializeProviderTerminalBuffer(ptyId)
|
||||
if (this.headlessTerminals.get(ptyId) !== state || !snapshot) {
|
||||
const { snapshot, late } = await this.awaitProviderTerminalBuffer(ptyId)
|
||||
if (this.headlessTerminals.get(ptyId) !== state) {
|
||||
return
|
||||
}
|
||||
if (!snapshot) {
|
||||
seedTimedOut = late !== null
|
||||
void late?.then((landed) => {
|
||||
if (
|
||||
this.headlessTerminals.get(ptyId) !== state ||
|
||||
this.getPtyLifecycleGeneration(ptyId) !== generation
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (!retryLateSeed) {
|
||||
return
|
||||
}
|
||||
if (!landed) {
|
||||
// The provider gave nothing, so trust the live model.
|
||||
this.providerSnapshotPreferredPtys.delete(ptyId)
|
||||
} else {
|
||||
// Live bytes moved this model on, so the late image is stale; capture once more.
|
||||
this.replaceHeadlessTerminalAfterExecutionContextChange(ptyId, false)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
const data = `${snapshot.scrollbackAnsi ?? ''}${snapshot.data}`
|
||||
@@ -150,12 +179,17 @@ export abstract class OrcaRuntimeWithCreatePtyHeadlessTerminalState extends Orca
|
||||
alternateScreen: state.emulator.isAlternateScreen
|
||||
})
|
||||
state.outputSequence = snapshot.seq
|
||||
seeded = true
|
||||
})
|
||||
.catch(() => {
|
||||
// Best-effort: live bytes already chain behind this replacement state.
|
||||
})
|
||||
.finally(() => {
|
||||
if (this.headlessTerminals.get(ptyId) === state) {
|
||||
// An unseeded model stays partial, so readers keep preferring the provider; a retry also dropped the bytes its predecessor held.
|
||||
if (
|
||||
this.headlessTerminals.get(ptyId) === state &&
|
||||
(seeded || (retryLateSeed && !seedTimedOut))
|
||||
) {
|
||||
this.providerSnapshotPreferredPtys.delete(ptyId)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PtyProviderBufferSnapshot } from '../../../providers/pty-provider-contract'
|
||||
import type {
|
||||
HeadlessSeedMetadata,
|
||||
ProviderTerminalBufferWait,
|
||||
RuntimeHeadlessTerminal
|
||||
} from '../../runtime-terminal-state-records'
|
||||
import type { OrcaRuntimeWithSerializeTerminalBufferFromAvailableState } from './terminal-buffer-selection'
|
||||
@@ -59,6 +60,11 @@ export abstract class OrcaRuntimeWithSerializeMainTerminalBuffer extends OrcaRun
|
||||
opts?: { scrollbackRows?: number },
|
||||
wait?: { timeoutMs?: number; retireOnTimeout?: boolean }
|
||||
): Promise<PtyProviderBufferSnapshot | null>
|
||||
protected abstract awaitProviderTerminalBuffer(
|
||||
ptyId: string,
|
||||
opts?: { scrollbackRows?: number },
|
||||
wait?: { timeoutMs?: number; retireOnTimeout?: boolean }
|
||||
): Promise<ProviderTerminalBufferWait>
|
||||
abstract resolveLeafForHandle(handle: string): { ptyId: string | null } | null
|
||||
abstract clearHeadlessTerminalBuffer(ptyId: string): Promise<void>
|
||||
abstract resetHeadlessTerminalInputModes(ptyId: string): Promise<void>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { OrcaRuntimeWithCreatePtyHeadlessTerminalState } from './headless-terminal-state'
|
||||
import type { TerminalOscLinkRange } from '../../../../shared/terminal-osc-link-ranges'
|
||||
import type { PtyProviderBufferSnapshot } from '../../../providers/pty-provider-contract'
|
||||
import type { ProviderTerminalBufferWait } from '../../runtime-terminal-state-records'
|
||||
import { withTimeout } from '../../runtime-async-boundaries'
|
||||
import { AUTHORITATIVE_TERMINAL_SNAPSHOT_TIMEOUT_MS } from '../../orca-runtime-postlude'
|
||||
|
||||
export abstract class OrcaRuntimeWithSerializeTerminalBufferFromAvailableState extends OrcaRuntimeWithCreatePtyHeadlessTerminalState {
|
||||
protected abstract captureProviderTerminalBuffer(
|
||||
@@ -124,11 +126,20 @@ export abstract class OrcaRuntimeWithSerializeTerminalBufferFromAvailableState e
|
||||
opts: { scrollbackRows?: number } = {},
|
||||
wait: { timeoutMs?: number; retireOnTimeout?: boolean } = {}
|
||||
): Promise<PtyProviderBufferSnapshot | null> {
|
||||
return (await this.awaitProviderTerminalBuffer(ptyId, opts, wait)).snapshot
|
||||
}
|
||||
|
||||
/** `late` is the capture still running when the bounded wait gave up, so a caller can act once it lands. */
|
||||
protected async awaitProviderTerminalBuffer(
|
||||
ptyId: string,
|
||||
opts: { scrollbackRows?: number } = {},
|
||||
wait: { timeoutMs?: number; retireOnTimeout?: boolean } = {}
|
||||
): Promise<ProviderTerminalBufferWait> {
|
||||
const generation = this.getPtyLifecycleGeneration(ptyId)
|
||||
const scrollbackRows = Math.max(0, Math.floor(opts.scrollbackRows ?? 0))
|
||||
let acquisition = this.providerBufferAcquisitionsByPtyId.get(ptyId)
|
||||
if (acquisition?.generation === generation && acquisition.timedOut) {
|
||||
return null
|
||||
return { snapshot: null, late: acquisition.promise }
|
||||
}
|
||||
if (
|
||||
!acquisition ||
|
||||
@@ -145,24 +156,22 @@ export abstract class OrcaRuntimeWithSerializeTerminalBufferFromAvailableState e
|
||||
})
|
||||
}
|
||||
if (acquisition.timedOut) {
|
||||
return null
|
||||
}
|
||||
if (typeof wait.timeoutMs !== 'number') {
|
||||
return acquisition.promise
|
||||
return { snapshot: null, late: acquisition.promise }
|
||||
}
|
||||
// Every reader shares this bound: a provider that never answers must not hold its caller.
|
||||
const result = await withTimeout<
|
||||
{ settled: true; value: PtyProviderBufferSnapshot | null } | { settled: false }
|
||||
>(
|
||||
acquisition.promise.then((value) => ({ settled: true as const, value })),
|
||||
wait.timeoutMs,
|
||||
wait.timeoutMs ?? AUTHORITATIVE_TERMINAL_SNAPSHOT_TIMEOUT_MS,
|
||||
{ settled: false as const }
|
||||
)
|
||||
if (!result.settled) {
|
||||
if (wait.retireOnTimeout) {
|
||||
acquisition.timedOut = true
|
||||
}
|
||||
return null
|
||||
return { snapshot: null, late: acquisition.promise }
|
||||
}
|
||||
return result.value
|
||||
return { snapshot: result.value, late: null }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user