refactor(runtime): collapse speculative portability layers

Remove the one-consumer bounded-read primitive and shared terminal lifecycle controller, make terminal cleanup one awaited provider operation, and reuse one Code Runtime contract suite. Keep only reproduced cancellation and policy fixes; defer unproven replacement, prompt-attribution, and streaming-frame concerns to scoped markers.
This commit is contained in:
Tianyi Cui
2026-07-29 18:26:21 +08:00
parent 4fecc54998
commit c1d550de58
65 changed files with 1265 additions and 948 deletions

View File

@@ -155,7 +155,7 @@ class LocalSendOperation implements PtySendOperation {
export class LocalPtySession implements PtyBackendSession {
motd = ''
readonly pid: number
private readonly decoder = new TextDecoder('utf-8', { fatal: true })
private readonly decoder = new TextDecoder()
private readonly sanitizer: TerminalSanitizer
private readonly scrollback: BoundedTextBuffer
private readonly outputEnded = Promise.withResolvers<void>()
@@ -165,16 +165,13 @@ export class LocalPtySession implements PtyBackendSession {
private activeTimer: NodeJS.Timeout | undefined
private activeDeadlineTimer: NodeJS.Timeout | undefined
private activeAbort: (() => void) | undefined
private readonly terminalOperations = new Set<Promise<unknown>>()
private signaledOperation: LocalSendOperation | undefined
private interrupting: LocalSendOperation | undefined
private writing: LocalSendOperation | undefined
private activeWrite: { operation: LocalSendOperation; settled: Promise<boolean> } | undefined
private pollingReady: LocalSendOperation | undefined
private polling = false
private promptSeen = false
private promptTextSeen = false
private promptTail = ''
private delayedSignaledPrompt = false
private shellPgid: number | undefined
private initializing = false
private lastOutputAt = Date.now()
@@ -239,23 +236,14 @@ export class LocalPtySession implements PtyBackendSession {
this.activeAbort = () => request.signal?.removeEventListener('abort', onAbort)
}
this.activeDeadlineTimer = setTimeout(() => {
if (this.active === operation) this.settleActive('timeout', this.writing === operation)
if (this.active === operation) {
this.settleActive('timeout', this.activeWrite?.operation === operation)
}
}, this.config.timeoutMs)
this.ownTerminalOperation(this.beginSend(operation, request))
void this.beginSend(operation, request)
return operation
}
/** Retain one contained provider operation until its asynchronous work finishes. */
private ownTerminalOperation(operation: Promise<void>): void {
void this.trackTerminalOperation(operation)
}
private trackTerminalOperation<T>(operation: Promise<T>): Promise<T> {
const tracked = operation.finally(() => { this.terminalOperations.delete(tracked) })
this.terminalOperations.add(tracked)
return tracked
}
private async beginSend(operation: LocalSendOperation, request: PtySendRequest): Promise<void> {
try {
const foreground = await this.terminal.inspectForeground()
@@ -264,13 +252,20 @@ export class LocalPtySession implements PtyBackendSession {
const input = `${request.text}${request.submit ? '\r' : ''}`
if (input.length > 0 && !operation.cancelRequested) {
this.resetReadinessEvidence()
this.writing = operation
const write = this.terminal.write(input)
const activeWrite = {
operation,
settled: write.then(() => true, () => false),
}
this.activeWrite = activeWrite
try {
await this.terminal.write(Buffer.from(input, 'utf8'))
await write
} finally {
this.writing = undefined
this.activeWrite = undefined
}
}
// Cancellation owns post-write signalling and reservation release.
if (operation.cancelRequested) return
if (this.active === operation && operation.settled) {
this.clearActive()
return
@@ -282,7 +277,7 @@ export class LocalPtySession implements PtyBackendSession {
this.schedulePoll(operation)
}
} catch (error: unknown) {
if (this.active === operation) {
if (this.active === operation && !this.closing) {
if (operation.settled) this.clearActive()
else this.failActive(error)
}
@@ -323,8 +318,7 @@ export class LocalPtySession implements PtyBackendSession {
async signal(signal: PtySignal): Promise<PtySignalResult> {
if (this.closing) throw new Error('PTY session is closing')
if (this.active !== undefined) this.signaledOperation = this.active
const targetPgid = await this.trackTerminalOperation(this.terminal.signalForeground(signal))
const targetPgid = await this.terminal.signalForeground(signal)
return { delivered: true, targetPgid }
}
@@ -345,23 +339,14 @@ export class LocalPtySession implements PtyBackendSession {
}
private readonly onTerminalData = (chunk: Buffer | Uint8Array | string): void => {
try {
const bytes = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk
this.onData(this.decoder.decode(bytes, { stream: true }))
} catch (error: unknown) {
this.onTransportFailure(new Error('PTY emitted invalid UTF-8', { cause: error }))
}
const bytes = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk
this.onData(this.decoder.decode(bytes, { stream: true }))
}
private readonly onTerminalEnd = (): void => {
try {
this.onData(this.decoder.decode())
this.appendOutput(this.sanitizer.flush())
} catch (error: unknown) {
this.onTransportFailure(new Error('PTY ended with invalid UTF-8', { cause: error }))
} finally {
this.outputEnded.resolve()
}
this.onData(this.decoder.decode())
this.appendOutput(this.sanitizer.flush())
this.outputEnded.resolve()
}
private readonly onTerminalError = (error: Error): void => {
@@ -372,9 +357,9 @@ export class LocalPtySession implements PtyBackendSession {
private onData(data: string): void {
const sanitized = this.sanitizer.push(data)
this.appendOutput(sanitized.text)
if (sanitized.prompt && this.delayedSignaledPrompt) {
this.delayedSignaledPrompt = false
} else if (sanitized.prompt) {
if (sanitized.prompt) {
// TODO(pty-delayed-signal-prompt): With a reproducer, define a marker-generation boundary
// before attributing a signal-delayed prompt to a later send.
// Bash can print PROMPT_COMMAND before the kernel publishes its return
// to the foreground process group. Retain the marker; polling below is
// the authority that accepts it only after bash owns the foreground.
@@ -402,7 +387,7 @@ export class LocalPtySession implements PtyBackendSession {
this.transportFailure ??= failure
this.statusValue = { kind: 'exited', exitCode: null, signal: null }
this.failActive(failure)
this.terminal.terminate()
void this.terminal.terminate().catch(() => {})
}
private appendOutput(text: string): void {
@@ -417,7 +402,7 @@ export class LocalPtySession implements PtyBackendSession {
if (this.activeTimer !== undefined) clearTimeout(this.activeTimer)
this.activeTimer = setTimeout(() => {
this.activeTimer = undefined
this.ownTerminalOperation(this.pollReadiness(operation))
void this.pollReadiness(operation)
}, delayMs)
}
@@ -457,7 +442,7 @@ export class LocalPtySession implements PtyBackendSession {
this.settleActive('inferred_idle')
}
} catch (error: unknown) {
if (this.active === operation) this.failActive(error)
if (this.active === operation && !this.closing) this.failActive(error)
} finally {
this.polling = false
const active = this.active
@@ -470,13 +455,6 @@ export class LocalPtySession implements PtyBackendSession {
private settleActive(waitReason: PtyWaitReason, retainOwnership = false): void {
const operation = this.active
if (operation === undefined) return
// A signaled command can return by silence before bash emits its prompt.
// Reserve that marker so it cannot become successor readiness after echo.
const signaled = this.signaledOperation === operation
if (signaled) this.signaledOperation = undefined
if (waitReason === 'inferred_idle' && !this.promptSeen && signaled) {
this.delayedSignaledPrompt = true
}
const scrollbackTruncated = this.scrollback.snapshot().truncated
if (retainOwnership) {
this.stopPolling()
@@ -489,10 +467,14 @@ export class LocalPtySession implements PtyBackendSession {
}
private stopPolling(): void {
if (this.activeTimer !== undefined) clearTimeout(this.activeTimer)
this.activeTimer = undefined
this.stopReadinessPolling()
if (this.activeDeadlineTimer !== undefined) clearTimeout(this.activeDeadlineTimer)
this.activeDeadlineTimer = undefined
}
private stopReadinessPolling(): void {
if (this.activeTimer !== undefined) clearTimeout(this.activeTimer)
this.activeTimer = undefined
this.pollingReady = undefined
}
@@ -502,43 +484,38 @@ export class LocalPtySession implements PtyBackendSession {
this.activeAbort?.()
this.activeAbort = undefined
if (this.interrupting === operation) this.interrupting = undefined
if (this.signaledOperation === operation) this.signaledOperation = undefined
this.writing = undefined
this.pollingReady = undefined
this.active = undefined
}
private failActive(error: unknown, retainOwnership = false): void {
private failActive(error: unknown): void {
const operation = this.active
if (operation === undefined) return
if (retainOwnership) {
this.stopPolling()
this.activeAbort?.()
this.activeAbort = undefined
} else {
this.clearActive()
}
this.clearActive()
operation.fail(error)
}
private interrupt(operation: LocalSendOperation): void {
if (this.active !== operation) return
this.signaledOperation = operation
this.interrupting = operation
this.stopPolling()
this.ownTerminalOperation(this.interruptOnce(operation))
this.stopReadinessPolling()
void this.interruptOnce(operation)
}
private async interruptOnce(operation: LocalSendOperation): Promise<void> {
try {
const activeWrite = this.activeWrite
if (activeWrite?.operation === operation && !await activeWrite.settled) return
await this.terminal.signalForeground('SIGINT')
} catch (error: unknown) {
if (this.active === operation) this.failActive(error, this.writing === operation)
if (this.active === operation && !this.closing) this.onTransportFailure(error)
return
} finally {
if (this.interrupting === operation) this.interrupting = undefined
}
if (this.active === operation && !operation.settled && !this.closing && this.writing !== operation) {
if (this.active === operation && operation.settled) {
this.clearActive()
} else if (this.active === operation && !this.closing) {
this.pollingReady = operation
this.schedulePoll(operation, 0)
}
@@ -549,20 +526,13 @@ export class LocalPtySession implements PtyBackendSession {
// it as session_exit below, so an in-flight send is never mis-settled as
// stdin_read/inferred_idle/timeout during the grace period.
this.stopPolling()
this.terminal.terminate()
const quiescent = await this.terminal.waitForExit()
if (!quiescent) {
throw new Error(`PTY cleanup failed (${reason}); terminal session did not reach quiescence`)
try {
await this.terminal.terminate()
} catch (error: unknown) {
throw new Error(`PTY cleanup failed (${reason})`, { cause: error })
}
// Quiescence is the active send's terminal outcome. Detach its abort
// listener before snapshotting provider operations so no late interrupt
// can enter the owned set after the drain starts.
// Quiescence is the active send's terminal outcome.
this.settleActive('session_exit')
await Promise.all(this.terminalOperations)
// Whole-session cleanup can fail before the top-level process exits. Wait
// for it first so that failure is reported instead of blocking forever on
// `done`; successful quiescence guarantees `done` can now settle status and
// drain the terminal output.
await this.completion
this.terminal.output.off('data', this.onTerminalData)
this.terminal.output.off('end', this.onTerminalEnd)