fix(persistent-bash): wait for complete status markers

This commit is contained in:
Tianyi Cui
2026-07-29 23:50:12 +08:00
parent fbfe520471
commit 7f5aa2d053
2 changed files with 28 additions and 4 deletions

View File

@@ -93,16 +93,18 @@ function stripPrompt(text: string): string {
function commandOutput(
snapshot: RetainedOutput,
marker: CommandMarkers,
): CapturedOutput {
): CapturedOutput | undefined {
const text = snapshot.text
const end = text.lastIndexOf(marker.end)
const exitCode = Number.parseInt(text.slice(end + marker.end.length), 10)
if (end < 0) return undefined
const status = /^(\d+)\r?\n/.exec(text.slice(end + marker.end.length))?.[1]
if (status === undefined) return undefined
const startMarker = text.lastIndexOf(marker.start, end)
const start = startMarker < 0 ? 0 : startMarker + marker.start.length
return {
text: stripPrompt(text.slice(start, end).replace(/^\r?\n/, '')),
incomplete: startMarker < 0,
exitCode,
exitCode: Number(status),
}
}
@@ -317,7 +319,7 @@ async function executeCommand(
}
if (latest.text.includes(marker.end)) {
const complete = commandOutput(retainedScrollback(ctx, owner, id, latest), marker)
return renderCaptured(complete, config.maxOutputChars)
if (complete !== undefined) return renderCaptured(complete, config.maxOutputChars)
}
if (result.sessionStatus.kind === 'exited') {
const snapshot = retainedScrollback(ctx, owner, id, latest)

View File

@@ -84,6 +84,8 @@ type StubMode =
| 'idle-then-normal'
| 'large'
| 'nonzero'
| 'torn-status'
| 'finish-torn-status'
| 'end-only'
| 'init-exit'
| 'init-timeout'
@@ -160,6 +162,17 @@ class StubPtySession implements PtyBackendSession {
this.pendingText = ''
const start = /__DSH_PERSISTENT_BASH_START_[^_]+(?:-[^_]+)*__/.exec(sent)?.[0]
const end = /__DSH_PERSISTENT_BASH_END_[^:]+:/.exec(sent)?.[0]
if (this.mode === 'torn-status') {
const output = `${start ?? ''}\nhello from stub\n${end ?? ''}`
this.scrollback += output
this.mode = 'finish-torn-status'
return this.operation(Promise.resolve(this.result(output, 'inferred_idle')))
}
if (this.mode === 'finish-torn-status') {
const output = `7\n${this.motd}`
this.scrollback += output
return this.operation(Promise.resolve(this.result(output, 'stdin_read')))
}
if (this.mode === 'end-only') {
const output = `recovered output\n${end ?? ''}0\n${this.motd}`
this.scrollback += output
@@ -345,6 +358,15 @@ describe('tool-bash-persistent', () => {
expect(stub.sessions[2]?.closed).toEqual(['external cleanup'])
})
it('waits for status digits after a torn completion marker', async () => {
const { ctx, owner, stub } = await setup({ backendType: 'stub', maxOutputChars: 1_000 })
await call(ctx, owner, 'warm up')
stub.sessions[0]!.mode = 'torn-status'
stub.sessions[0]!.scrollback = ''
expect(text(await call(ctx, owner, 'torn status'))).toBe('hello from stub\n[exit code: 7]')
})
it('marks a short missing-prefix result and tolerates exhausted scrollback pages', async () => {
const { ctx, owner, stub } = await setup({ backendType: 'stub', maxOutputChars: 1_000 })
await call(ctx, owner, 'warm up')