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( function commandOutput(
snapshot: RetainedOutput, snapshot: RetainedOutput,
marker: CommandMarkers, marker: CommandMarkers,
): CapturedOutput { ): CapturedOutput | undefined {
const text = snapshot.text const text = snapshot.text
const end = text.lastIndexOf(marker.end) 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 startMarker = text.lastIndexOf(marker.start, end)
const start = startMarker < 0 ? 0 : startMarker + marker.start.length const start = startMarker < 0 ? 0 : startMarker + marker.start.length
return { return {
text: stripPrompt(text.slice(start, end).replace(/^\r?\n/, '')), text: stripPrompt(text.slice(start, end).replace(/^\r?\n/, '')),
incomplete: startMarker < 0, incomplete: startMarker < 0,
exitCode, exitCode: Number(status),
} }
} }
@@ -317,7 +319,7 @@ async function executeCommand(
} }
if (latest.text.includes(marker.end)) { if (latest.text.includes(marker.end)) {
const complete = commandOutput(retainedScrollback(ctx, owner, id, latest), marker) 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') { if (result.sessionStatus.kind === 'exited') {
const snapshot = retainedScrollback(ctx, owner, id, latest) const snapshot = retainedScrollback(ctx, owner, id, latest)

View File

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