refactor(terminal): make wedge probes explicit

This commit is contained in:
Jinjing
2026-07-25 15:01:17 -07:00
parent 51fa7e55ae
commit 2cfb7ac411
3 changed files with 35 additions and 20 deletions
@@ -75,9 +75,9 @@ import {
waitForTerminalReplayWritesParsed
} from './replay-guard'
import {
armTerminalWriteStallWatch,
isTerminalWritePipelineCertifiedDead,
registerUndeliverableWriteHandler
registerUndeliverableWriteHandler,
requestTerminalWritePipelineProbe
} from '@/lib/pane-manager/terminal-write-pipeline-health'
import {
captureTerminalPaneRecoveryGeneration,
@@ -3358,6 +3358,8 @@ export function connectPanePty(
const MAX_DEFERRED_REATTACH_LIVE_CHUNKS = 1_024
const markTerminalInputSent = (): void => {
lastTerminalInputAt = performance.now()
// Why: input must probe a wedged xterm even when the PTY produces no renderer output.
requestTerminalWritePipelineProbe(pane.terminal)
}
const recordTerminalInputForHibernation = (): void => {
useAppStore.getState().recordTerminalInput(cacheKey)
@@ -3605,10 +3607,6 @@ export function connectPanePty(
// pre-existing session when a late reattach resolves, so a remount racing
// a slow-but-alive connect costs a wasted view rebuild, not a shell.
const TRANSPORT_CONNECT_SETTLE_GRACE_MS = 60_000
// Why: user input must probe a wedged xterm even when no PTY output reaches the renderer.
const armWritePipelineWatchForUserInput = (): void => {
armTerminalWriteStallWatch(pane.terminal, { requireProbe: true })
}
const requestRecoveryForUndeliverableInput = (): void => {
if (transport.isConnected?.() && transport.getPtyId() !== null) {
return
@@ -3734,7 +3732,6 @@ export function connectPanePty(
}
clearPendingTerminalInputIntent()
markTerminalInputSent()
armWritePipelineWatchForUserInput()
const writePromise = transport
.sendInputAccepted(data)
.then((accepted) => {
@@ -3760,7 +3757,6 @@ export function connectPanePty(
claimViewportForUserActivity()
if (transport.sendInput(data)) {
markAcceptedTerminalInputSent()
armWritePipelineWatchForUserInput()
observeAcceptedShellCommandInput(data)
observeAcceptedTerminalInput(data, intent)
} else {
@@ -3772,7 +3768,6 @@ export function connectPanePty(
claimViewportForUserActivity()
if (transport.sendInput(data)) {
markAcceptedTerminalInputSent()
armWritePipelineWatchForUserInput()
observeAcceptedShellCommandInput(data)
observeAcceptedTerminalInput(data)
observeSentTerminalInputIntent(data)
@@ -9,6 +9,7 @@ import {
isTerminalWritePipelineCertifiedDead,
notifyUndeliverableWrite,
registerUndeliverableWriteHandler,
requestTerminalWritePipelineProbe,
settleTerminalWriteStallWatch,
WRITE_PIPELINE_STALL_CHECK_MS
} from './terminal-write-pipeline-health'
@@ -113,14 +114,15 @@ describe('terminal write pipeline health', () => {
_resetWritePipelineHealthForTests(terminal)
})
it('keeps a required probe armed through unrelated parse progress', () => {
it('keeps a requested probe armed through unrelated parse progress', () => {
vi.useFakeTimers()
const terminal = makeTerminal()
terminal.dropCallbacks = true
const handler = vi.fn()
registerUndeliverableWriteHandler(terminal, handler)
armTerminalWriteStallWatch(terminal, { requireProbe: true })
armTerminalWriteStallWatch(terminal)
requestTerminalWritePipelineProbe(terminal)
settleTerminalWriteStallWatch(terminal)
vi.advanceTimersByTime(WRITE_PIPELINE_STALL_CHECK_MS * 2)
@@ -42,10 +42,12 @@ export function hasTerminalParseProgressSince(terminal: object, generation: numb
return captureTerminalParseProgressGeneration(terminal) !== generation
}
type StallWatchMode = 'output-completion' | 'fifo-probe'
type StallWatch = {
timer: ReturnType<typeof setTimeout>
onCertifiedDead?: () => void
requireProbe: boolean
mode: StallWatchMode
}
const stallWatchByTerminal = new WeakMap<object, StallWatch>()
@@ -111,27 +113,28 @@ export function isTerminalWritePipelineCertifiedDead(terminal: object): boolean
* alive (slow parse), re-arm and keep waiting; probe silent for another
* interval → dead, notify.
*/
export function armTerminalWriteStallWatch(
function armTerminalWritePipelineWatch(
terminal: WriteTarget,
options: {
mode: StallWatchMode
onCertifiedDead?: () => void
/** Ignore unrelated write completions so user-input health checks reach their FIFO probe. */
requireProbe?: boolean
stallCheckMs?: number
} = {}
}
): void {
if (certifiedDeadTerminals.has(terminal)) {
return
}
const existingWatch = stallWatchByTerminal.get(terminal)
if (existingWatch) {
existingWatch.requireProbe ||= options.requireProbe === true
if (options.mode === 'fifo-probe') {
existingWatch.mode = 'fifo-probe'
}
return
}
const stallCheckMs = options.stallCheckMs ?? WRITE_PIPELINE_STALL_CHECK_MS
const watch: StallWatch = {
mode: options.mode,
onCertifiedDead: options.onCertifiedDead,
requireProbe: options.requireProbe === true,
timer: setTimeout(probeForStall, stallCheckMs)
}
const certifyDead = (): void => certifyTerminalWritePipelineDead(terminal, watch)
@@ -167,6 +170,21 @@ export function armTerminalWriteStallWatch(
stallWatchByTerminal.set(terminal, watch)
}
export function armTerminalWriteStallWatch(
terminal: WriteTarget,
options: { onCertifiedDead?: () => void; stallCheckMs?: number } = {}
): void {
armTerminalWritePipelineWatch(terminal, { ...options, mode: 'output-completion' })
}
/** Require the active watch to reach its FIFO probe before it may settle. */
export function requestTerminalWritePipelineProbe(
terminal: WriteTarget,
options: { stallCheckMs?: number } = {}
): void {
armTerminalWritePipelineWatch(terminal, { ...options, mode: 'fifo-probe' })
}
/** Cancel a pending watch without claiming that any bytes parsed. */
export function cancelTerminalWriteStallWatch(terminal: object): void {
const watch = stallWatchByTerminal.get(terminal)
@@ -177,10 +195,10 @@ export function cancelTerminalWriteStallWatch(terminal: object): void {
stallWatchByTerminal.delete(terminal)
}
/** Write completed normally — record progress and drop ordinary write watches. */
/** Write completed normally — the pipeline is healthy; drop any pending watch. */
export function settleTerminalWriteStallWatch(terminal: object): void {
recordTerminalParseProgress(terminal)
if (stallWatchByTerminal.get(terminal)?.requireProbe) {
if (stallWatchByTerminal.get(terminal)?.mode === 'fifo-probe') {
return
}
cancelTerminalWriteStallWatch(terminal)