fix(runtime): close async cleanup and framing races

This commit is contained in:
Tianyi Cui
2026-07-29 01:18:01 +08:00
parent 16cac3b5af
commit ac81165662
15 changed files with 109 additions and 47 deletions

View File

@@ -20,7 +20,7 @@ import type {
PtyWaitReason,
} from '@deepseek-ai/dsh-pty'
import type { ResolvedConfig } from './config.ts'
import { TerminalSanitizer } from './sanitize.ts'
import { CONTROLLED_PROMPT, TerminalSanitizer } from './sanitize.ts'
function utf8Tail(text: string, maxBytes: number): { text: string; truncated: boolean } {
if (Buffer.byteLength(text) <= maxBytes) return { text, truncated: false }
@@ -77,6 +77,7 @@ class LocalSendOperation implements PtySendOperation {
private readonly output: BoundedTextBuffer
private readonly promise: PromiseWithResolvers<PtySendResult>
private finished = false
private cancellationRequested = false
private initialForegroundLeftWait: boolean
private initialForegroundPgid: number | undefined
@@ -98,6 +99,10 @@ class LocalSendOperation implements PtySendOperation {
return this.finished
}
get cancelRequested(): boolean {
return this.cancellationRequested
}
append(text: string): void {
if (!this.finished) this.output.append(text)
}
@@ -140,6 +145,7 @@ class LocalSendOperation implements PtySendOperation {
cancel(): boolean {
if (this.finished) return false
this.cancellationRequested = true
this.onCancel()
return true
}
@@ -164,6 +170,7 @@ export class LocalPtySession implements PtyBackendSession {
private polling = false
private promptSeen = false
private promptTextSeen = false
private promptTail = ''
private shellPgid: number | undefined
private initializing = false
private lastOutputAt = Date.now()
@@ -223,6 +230,7 @@ export class LocalPtySession implements PtyBackendSession {
this.lastOutputAt = Date.now()
this.promptSeen = false
this.promptTextSeen = false
this.promptTail = ''
if (request.signal !== undefined) {
const onAbort = (): void => { operation.cancel() }
@@ -242,7 +250,7 @@ export class LocalPtySession implements PtyBackendSession {
if (this.active !== operation || this.closing) return
operation.setInitialForeground(foreground)
const input = `${request.text}${request.submit ? '\r' : ''}`
if (input.length > 0) {
if (input.length > 0 && !operation.cancelRequested) {
this.writing = operation
try {
await this.terminal.write(Buffer.from(input, 'utf8'))
@@ -347,10 +355,14 @@ export class LocalPtySession implements PtyBackendSession {
// to the foreground process group. Retain the marker; polling below is
// the authority that accepts it only after bash owns the foreground.
this.promptSeen = true
this.promptTextSeen = sanitized.promptText === true
this.promptTail = ''
this.lastOutputAt = Date.now()
} else if (this.promptSeen && sanitized.promptText === true) {
this.promptTextSeen = true
}
if (this.promptSeen && sanitized.promptTail !== undefined) {
const remaining = Math.max(0, CONTROLLED_PROMPT.length + 1 - this.promptTail.length)
this.promptTail += sanitized.promptTail.slice(0, remaining)
if (sanitized.promptTail.length > remaining) this.promptTail = `${CONTROLLED_PROMPT}\0`
this.promptTextSeen = this.promptTail === CONTROLLED_PROMPT
}
}