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

@@ -59,12 +59,7 @@ export class LocalSubprocessService extends SubprocessService {
pending.push(handle.done.catch(() => {}).then(() => handle.waitForExit()))
}
for (const terminal of this.terminals) {
terminal.terminate()
// Cleanup may reject before the top-level process exits (for example,
// an identity-fenced descendant survives escalation). Await the cleanup
// transaction directly so disposal reports that failure rather than
// waiting forever on `done`.
pending.push(terminal.waitForExit().then(() => { this.terminals.delete(terminal) }))
pending.push(terminal.terminate().then(() => { this.terminals.delete(terminal) }))
}
this.live.clear()
const outcomes = [
@@ -136,11 +131,6 @@ export class LocalSubprocessService extends SubprocessService {
if (file === undefined || file.length === 0) {
throw new Error('subprocess-local: terminal argv must contain a program')
}
for (const [name, value] of [['rows', spec.rows], ['cols', spec.cols], ['graceMs', spec.graceMs]] as const) {
if (!Number.isSafeInteger(value) || value <= 0) {
throw new Error(`subprocess-local: terminal ${name} must be a positive safe integer`)
}
}
spec.signal?.throwIfAborted()
const options: IPtyForkOptions = {
name: 'dumb',
@@ -151,10 +141,10 @@ export class LocalSubprocessService extends SubprocessService {
}
const inspector = this.terminalInspector ?? createProcessInspector()
const terminal = nodePty.spawn(file, [...spec.argv.slice(1)], options)
const handle = new LocalTerminalHandle(terminal, inspector, spec.graceMs, spec.signal)
const handle = new LocalTerminalHandle(terminal, inspector, spec.graceMs)
this.terminals.add(handle)
const release = async (): Promise<void> => {
await handle.waitForExit()
await handle.terminate()
this.terminals.delete(handle)
}
void handle.done.then(release, release).catch(() => {})

View File

@@ -4,7 +4,6 @@ import { Buffer } from 'node:buffer'
import { constants } from 'node:os'
import { PassThrough } from 'node:stream'
import type { IDisposable, IPty } from 'node-pty'
import { SubprocessTerminalLifecycle } from '@deepseek-ai/dsh-subprocess'
import type {
SubprocessOutcome,
SubprocessTerminalForeground,
@@ -34,7 +33,7 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
private readonly outcome = Promise.withResolvers<SubprocessOutcome>()
private readonly dataDisposable: IDisposable
private readonly exitDisposable: IDisposable
private readonly lifecycle: SubprocessTerminalLifecycle
private cleanup: Promise<void> | undefined
private exited = false
private trackedDescendants: ProcessIdentity[] = []
@@ -42,13 +41,11 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
* @param terminal - allocated node-pty process.
* @param inspector - platform process/session operations.
* @param graceMs - TERM-to-KILL and exit-wait grace.
* @param signal - optional lifetime cancellation.
*/
constructor(
private readonly terminal: IPty,
private readonly inspector: ProcessInspector,
private readonly graceMs: number,
signal?: AbortSignal,
) {
this.pid = terminal.pid
this.done = this.outcome.promise
@@ -61,26 +58,15 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
exitCode: exitSignal === undefined || exitSignal === 0 ? exitCode : null,
signal: signalName(exitSignal),
})
this.terminate()
})
this.lifecycle = new SubprocessTerminalLifecycle({
done: this.done,
cleanup: () => this.closeOnce(),
signal,
void this.terminate().catch(() => {})
})
}
// node-pty writes synchronously; the seam returns a promise for remote transports.
// eslint-disable-next-line @typescript-eslint/require-await
async write(data: Uint8Array): Promise<void> {
async write(data: string): Promise<void> {
if (this.exited) throw new Error('terminal process has exited')
let text: string
try {
text = new TextDecoder('utf-8', { fatal: true }).decode(data)
} catch (error: unknown) {
throw new Error('terminal input must be valid UTF-8', { cause: error })
}
this.terminal.write(text)
this.terminal.write(data)
}
// Local inspection is synchronous; the seam returns a promise for remote transports.
@@ -107,12 +93,12 @@ export class LocalTerminalHandle implements SubprocessTerminalHandle {
return foreground.processGroupId
}
terminate(): void {
this.lifecycle.terminate()
}
async waitForExit(signal?: AbortSignal): Promise<boolean> {
return await this.lifecycle.waitForExit(signal)
terminate(): Promise<void> {
if (this.cleanup !== undefined) return this.cleanup
const cleanup = this.closeOnce()
this.cleanup = cleanup
void cleanup.catch(() => { this.cleanup = undefined })
return cleanup
}
private survivors(members: ProcessIdentity[]): ProcessIdentity[] {