[Bug] persistent bash each command waits 3.5s: CONTROLLED_PROMPT mismatch SHELL_PROMPT #3657
rhetthsu0701
started this conversation in
Ideas
Replies: 1 comment
完整补丁(git apply dsh-prompt-fix.patch)diff --git a/packages/terminal/terminal-bash/src/sanitize.ts b/packages/terminal/terminal-bash/src/sanitize.ts
index 1f28315..b0da3e4 100644
--- a/packages/terminal/terminal-bash/src/sanitize.ts
+++ b/packages/terminal/terminal-bash/src/sanitize.ts
@@ -5,8 +5,13 @@ import { Buffer } from 'node:buffer'
/** OSC marker emitted by the controlled bash before each prompt. */
export const PROMPT_MARKER_PREFIX = '133;D;'
-/** Exact printable prompt emitted after the private marker. */
-export const CONTROLLED_PROMPT = 'dsh> '
+/**
+ * Exact printable prompt emitted after the private marker.
+ * Must equal the persistent-bash tool's `SHELL_PROMPT`
+ * (`packages/shell/tool-bash-persistent/src/index.ts`) so the fast
+ * stdin_read readiness path fires instead of the silence timeout fallback.
+ */
+export const CONTROLLED_PROMPT = '__DSH_PERSISTENT_BASH_PROMPT__ '
/** One sanitized chunk plus whether it contained the owned prompt marker. */
export interface SanitizedChunk {
diff --git a/packages/terminal/terminal-bash/tests/index.spec.ts b/packages/terminal/terminal-bash/tests/index.spec.ts
index 9e2a352..2ae18a3 100644
--- a/packages/terminal/terminal-bash/tests/index.spec.ts
+++ b/packages/terminal/terminal-bash/tests/index.spec.ts
@@ -207,7 +207,7 @@ describe('BashTerminalBackend startup rollback', () => {
cwd: '/work',
graceMs: 10,
env: {
- TERM: 'dumb', PAGER: 'cat', GIT_PAGER: 'cat', PS1: 'dsh> ', BASH_SILENCE_DEPRECATION_WARNING: '1',
+ TERM: 'dumb', PAGER: 'cat', GIT_PAGER: 'cat', PS1: '__DSH_PERSISTENT_BASH_PROMPT__ ', BASH_SILENCE_DEPRECATION_WARNING: '1',
DSH_SHELL: '1', DSH_SESSION_ID: 'agent', DSH_PTY_SESSION_ID: 'pty-1',
},
})
@@ -327,14 +327,14 @@ describe('BashTerminalBackend startup rollback', () => {
outcome.resolve({ exitCode: null, signal: 'SIGTERM' })
},
}
- queueMicrotask(() => { output.write(Buffer.from('\x1b]133;D;0\x07dsh> ')) })
+ queueMicrotask(() => { output.write(Buffer.from('\x1b]133;D;0\x07__DSH_PERSISTENT_BASH_PROMPT__ ')) })
const backend = new BashTerminalBackend(
ctx,
config(),
async () => terminal,
)
const session = await backend.spawn(spec(agent(ctx)))
- expect(session.motd).toBe('dsh> ')
+ expect(session.motd).toBe('__DSH_PERSISTENT_BASH_PROMPT__ ')
await session.close('test complete')
})
})
diff --git a/packages/terminal/terminal-bash/tests/local.spec.ts b/packages/terminal/terminal-bash/tests/local.spec.ts
index d8c8fe6..9b9b664 100644
--- a/packages/terminal/terminal-bash/tests/local.spec.ts
+++ b/packages/terminal/terminal-bash/tests/local.spec.ts
@@ -121,7 +121,7 @@ describe('terminal-bash real shell', () => {
try {
const { ctx, root, agent } = await harness('danger-full-access')
const created = await ctx.terminals.spawn(agent, { type: 'shell', name: 'main', cwd: root })
- expect(created.motd).toContain('dsh> ')
+ expect(created.motd).toContain('__DSH_PERSISTENT_BASH_PROMPT__ ')
const first = ctx.terminals.startSend(agent, created.sessionId, { text: 'export KEEP=ok; cd /', submit: true })
expect((await first.done).waitReason).toBe('stdin_read')
diff --git a/packages/terminal/terminal-bash/tests/sanitize.spec.ts b/packages/terminal/terminal-bash/tests/sanitize.spec.ts
index 7cb2315..8279692 100644
--- a/packages/terminal/terminal-bash/tests/sanitize.spec.ts
+++ b/packages/terminal/terminal-bash/tests/sanitize.spec.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { normalizeTerminalText, TerminalSanitizer } from '@deepseek-ai/dsh-terminal-bash/src/sanitize.ts'
+import { CONTROLLED_PROMPT, normalizeTerminalText, TerminalSanitizer } from '@deepseek-ai/dsh-terminal-bash/src/sanitize.ts'
describe('TerminalSanitizer', () => {
it('removes split CSI and owned OSC prompt markers', () => {
@@ -7,7 +7,7 @@ describe('TerminalSanitizer', () => {
expect(sanitizer.push('red\x1b[3')).toEqual({ text: 'red', prompt: false })
expect(sanitizer.push('1m text\x1b[0m\r\n')).toEqual({ text: ' text\n', prompt: false })
expect(sanitizer.push('\x1b]133;')).toEqual({ text: '', prompt: false })
- expect(sanitizer.push('D;0\x07dsh> ')).toEqual({ text: 'dsh> ', prompt: true, promptTail: 'dsh> ' })
+ expect(sanitizer.push(`D;0\x07${CONTROLLED_PROMPT}`)).toEqual({ text: CONTROLLED_PROMPT, prompt: true, promptTail: CONTROLLED_PROMPT })
})
it('drops unrelated OSC, short escapes, BEL, and incomplete trailing escape', () => {
@@ -36,7 +36,7 @@ describe('TerminalSanitizer', () => {
it('reports printable prompt text that follows a marker in a later chunk', () => {
const sanitizer = new TerminalSanitizer(64)
expect(sanitizer.push('\x1b]133;D;0\x07')).toEqual({ text: '', prompt: true, promptTail: '' })
- expect(sanitizer.push('dsh> ')).toEqual({ text: 'dsh> ', prompt: false, promptTail: 'dsh> ' })
+ expect(sanitizer.push(CONTROLLED_PROMPT)).toEqual({ text: CONTROLLED_PROMPT, prompt: false, promptTail: CONTROLLED_PROMPT })
})
it('bounds and discards unterminated control sequences through their terminators', () => {
diff --git a/packages/terminal/terminal-bash/tests/session.spec.ts b/packages/terminal/terminal-bash/tests/session.spec.ts
index ef9fd21..751954c 100644
--- a/packages/terminal/terminal-bash/tests/session.spec.ts
+++ b/packages/terminal/terminal-bash/tests/session.spec.ts
@@ -142,7 +142,7 @@ afterEach(() => { vi.useRealTimers() })
async function initialize(session: LocalPtySession, terminal: FakeTerminal): Promise<void> {
const pending = session.initialize()
- terminal.emitData('\x1b]133;D;0\x07dsh> ')
+ terminal.emitData('\x1b]133;D;0\x07__DSH_PERSISTENT_BASH_PROMPT__ ')
await vi.advanceTimersByTimeAsync(10)
await pending
}
@@ -168,7 +168,7 @@ describe('LocalPtySession readiness and output', () => {
await vi.advanceTimersByTimeAsync(0)
expect(inspections).toBe(1)
- terminal.emitData('\x1b]133;D;0\x07dsh> ')
+ terminal.emitData('\x1b]133;D;0\x07__DSH_PERSISTENT_BASH_PROMPT__ ')
await vi.advanceTimersByTimeAsync(10)
expect((await operation.done).waitReason).toBe('stdin_read')
})
@@ -185,13 +185,13 @@ describe('LocalPtySession readiness and output', () => {
let settled = false
void operation.done.then(() => { settled = true })
- terminal.emitData('\x1b]133;D;0\x07dsh> ')
+ terminal.emitData('\x1b]133;D;0\x07__DSH_PERSISTENT_BASH_PROMPT__ ')
inspection.resolve({ processGroupId: 456, inputWaiting: true })
await vi.advanceTimersByTimeAsync(20)
expect(terminal.writes).toEqual(['long-running-command\r'])
expect(settled).toBe(false)
- terminal.emitData('\x1b]133;D;0\x07dsh> ')
+ terminal.emitData('\x1b]133;D;0\x07__DSH_PERSISTENT_BASH_PROMPT__ ')
await vi.advanceTimersByTimeAsync(10)
expect((await operation.done).waitReason).toBe('stdin_read')
})
@@ -202,7 +202,7 @@ describe('LocalPtySession readiness and output', () => {
const inspector = new FakeInspector()
const session = makeSession(terminal, inspector, config())
await initialize(session, terminal)
- expect(session.motd).toBe('dsh> ')
+ expect(session.motd).toBe('__DSH_PERSISTENT_BASH_PROMPT__ ')
inspector.waiting = true
const operation = session.startSend({ text: 'python3', submit: true })
@@ -306,7 +306,7 @@ describe('LocalPtySession readiness and output', () => {
await Promise.resolve()
expect(inspector.groups).toContainEqual([456, 'SIGINT'])
expect(terminal.writes).not.toContain('\x03')
- terminal.emitData('\x1b]133;D;130\x07dsh> ')
+ terminal.emitData('\x1b]133;D;130\x07__DSH_PERSISTENT_BASH_PROMPT__ ')
await vi.advanceTimersByTimeAsync(10)
await operation.done
@@ -340,7 +340,7 @@ describe('LocalPtySession readiness and output', () => {
expect(terminal.writes).toEqual([])
expect(inspector.groups).toContainEqual([456, 'SIGINT'])
- terminal.emitData('\x1b]133;D;130\x07dsh> ')
+ terminal.emitData('\x1b]133;D;130\x07__DSH_PERSISTENT_BASH_PROMPT__ ')
await vi.advanceTimersByTimeAsync(10)
await operation.done
})
@@ -381,7 +381,7 @@ describe('LocalPtySession readiness and output', () => {
await vi.advanceTimersByTimeAsync(0)
expect(inspector.groups).toContainEqual([456, 'SIGINT'])
- terminal.emitData('\x1b]133;D;130\x07dsh> ')
+ terminal.emitData('\x1b]133;D;130\x07__DSH_PERSISTENT_BASH_PROMPT__ ')
await vi.advanceTimersByTimeAsync(10)
await operation.done
})
@@ -406,7 +406,7 @@ describe('LocalPtySession readiness and output', () => {
await Promise.resolve()
expect(operation.cancel()).toBe(true)
- terminal.emitData('\x1b]133;D;130\x07dsh> ')
+ terminal.emitData('\x1b]133;D;130\x07__DSH_PERSISTENT_BASH_PROMPT__ ')
await vi.advanceTimersByTimeAsync(100)
expect((await operation.done).waitReason).toBe('timeout')
expect(() => session.startSend({ text: 'successor', submit: true })).toThrow('active send')
@@ -532,7 +532,7 @@ describe('LocalPtySession readiness and output', () => {
writeGate.resolve(undefined)
await vi.advanceTimersByTimeAsync(0)
expect(inspector.groups).toContainEqual([456, 'SIGINT'])
- terminal.emitData('\x1b]133;D;130\x07dsh> ')
+ terminal.emitData('\x1b]133;D;130\x07__DSH_PERSISTENT_BASH_PROMPT__ ')
await vi.advanceTimersByTimeAsync(10)
await operation.done
})
@@ -769,7 +769,7 @@ describe('LocalPtySession readiness and output', () => {
const initializing = session.initialize().then(() => { settled = true })
await vi.advanceTimersByTimeAsync(60)
expect(settled).toBe(false)
- terminal.emitData('\x1b]133;D;0\x07dsh> ')
+ terminal.emitData('\x1b]133;D;0\x07__DSH_PERSISTENT_BASH_PROMPT__ ')
await vi.advanceTimersByTimeAsync(10)
await initializing
@@ -806,10 +806,10 @@ describe('LocalPtySession readiness and output', () => {
await vi.advanceTimersByTimeAsync(20)
expect(settled).toBe(false)
- terminal.emitData('dsh> ')
+ terminal.emitData('__DSH_PERSISTENT_BASH_PROMPT__ ')
await vi.advanceTimersByTimeAsync(10)
await initializing
- expect(session.motd).toBe('dsh> ')
+ expect(session.motd).toBe('__DSH_PERSISTENT_BASH_PROMPT__ ')
})
it('does not attribute a delayed prior prompt to the current send', async () => {
@@ -824,11 +824,11 @@ describe('LocalPtySession readiness and output', () => {
await Promise.resolve()
await Promise.resolve()
- terminal.emitData('\x1b]133;D;0\x07dsh> printf \'PID=%s\\n\' "$!"\r\n')
+ terminal.emitData('\x1b]133;D;0\x07__DSH_PERSISTENT_BASH_PROMPT__ printf \'PID=%s\\n\' "$!"\r\n')
await vi.advanceTimersByTimeAsync(20)
expect(settled).toBe(false)
- terminal.emitData('PID=123\r\n\x1b]133;D;0\x07dsh> ')
+ terminal.emitData('PID=123\r\n\x1b]133;D;0\x07__DSH_PERSISTENT_BASH_PROMPT__ ')
await vi.advanceTimersByTimeAsync(10)
expect(await operation.done).toMatchObject({ waitReason: 'stdin_read' })
})
@@ -846,7 +846,7 @@ describe('LocalPtySession readiness and output', () => {
await Promise.resolve()
await Promise.resolve()
inspector.pgid = 789
- terminal.emitData('\x1b]133;D;0\x07dsh> ')
+ terminal.emitData('\x1b]133;D;0\x07__DSH_PERSISTENT_BASH_PROMPT__ ')
await vi.advanceTimersByTimeAsync(50)
expect(settled).toBe(false)
@@ -869,7 +869,7 @@ describe('LocalPtySession readiness and output', () => {
await Promise.resolve()
await Promise.resolve()
inspector.pgid = 789
- terminal.emitData('\x1b]133;D;0\x07dsh> ')
+ terminal.emitData('\x1b]133;D;0\x07__DSH_PERSISTENT_BASH_PROMPT__ ')
// One poll past the silence bound would already have settled inferred_idle.
await vi.advanceTimersByTimeAsync(70)
expect(settled).toBe(false)
@@ -920,7 +920,7 @@ describe('LocalPtySession readiness and output', () => {
const chunkSession = new LocalPtySession(chunkTerminal, config())
const chunkOperation = chunkSession.startSend({ text: '', submit: false })
chunkTerminal.emitBytes(Uint8Array.from([0xff]))
- expect(chunkOperation.readOutput()).toEqual({ delta: '�', truncated: false })
+ expect(chunkOperation.readOutput()).toEqual({ delta: '锟?, truncated: false })
chunkTerminal.emitExit()
await chunkOperation.done
@@ -929,7 +929,7 @@ describe('LocalPtySession readiness and output', () => {
const endOperation = endSession.startSend({ text: '', submit: false })
endTerminal.emitBytes(Uint8Array.from([0xe2]))
endTerminal.emitExit()
- expect((await endOperation.done).viewport).toBe('�')
+ expect((await endOperation.done).viewport).toBe('锟?)
})
it('contains readiness inspection failure and a stale inspection result', async () => {
@@ -991,7 +991,7 @@ describe('LocalPtySession readiness and output', () => {
block = false
const current = session.startSend({ text: '', submit: false })
- terminal.emitData('\x1b]133;D;0\x07dsh> ')
+ terminal.emitData('\x1b]133;D;0\x07__DSH_PERSISTENT_BASH_PROMPT__ ')
await Promise.resolve()
await Promise.resolve()
inspection.resolve({ processGroupId: 456, inputWaiting: false })
@@ -1038,7 +1038,7 @@ describe('LocalPtySession readiness and output', () => {
successorInspection.resolve({ processGroupId: 456, inputWaiting: false })
await Promise.resolve()
await Promise.resolve()
- terminal.emitData('\x1b]133;D;0\x07dsh> ')
+ terminal.emitData('\x1b]133;D;0\x07__DSH_PERSISTENT_BASH_PROMPT__ ')
await vi.advanceTimersByTimeAsync(10)
expect(terminal.writes).toEqual(['successor\r'])
expect((await current.done).waitReason).toBe('stdin_read')
@@ -1138,7 +1138,7 @@ describe('LocalPtySession bounds, signals, and teardown', () => {
expect(session.read({})).toMatchObject({ text: '' })
await initialize(session, terminal)
const operation = session.startSend({ text: '', submit: false })
- terminal.emitData('一\n二\n三\n四')
+ terminal.emitData('涓€\n浜孿n涓塡n鍥?)
await vi.advanceTimersByTimeAsync(60)
expect((await operation.done).truncated).toBe(true)
const page = session.read({ offset: 0, count: 3 })
@@ -1152,7 +1152,7 @@ describe('LocalPtySession bounds, signals, and teardown', () => {
const tiny = new LocalPtySession(tinyTerminal, config({ maxReadBytes: 1 }))
await initialize(tiny, tinyTerminal)
const tinyOperation = tiny.startSend({ text: '', submit: false })
- tinyTerminal.emitData('一')
+ tinyTerminal.emitData('涓€')
await vi.advanceTimersByTimeAsync(60)
await tinyOperation.done
expect(tiny.read({ offset: 0, count: 1 }).text).toBe('')
@@ -1207,7 +1207,7 @@ describe('LocalPtySession bounds, signals, and teardown', () => {
// The shell returns to its prompt while the send is active; a running
// readiness poll would otherwise mis-settle this as stdin_read once close
// begins, so teardown must stop polling before its grace period.
- terminal.emitData('\x1b]133;D;0\x07dsh> ')
+ terminal.emitData('\x1b]133;D;0\x07__DSH_PERSISTENT_BASH_PROMPT__ ')
terminal.autoExitOnKill = false
const closing = session.close('mid-send')
await vi.advanceTimersByTimeAsync(20)
@@ -1270,7 +1270,7 @@ describe('LocalPtySession bounds, signals, and teardown', () => {
const operation = session.startSend({ text: 'pending readiness', submit: true })
await Promise.resolve()
await Promise.resolve()
- terminal.emitData('\x1b]133;D;0\x07dsh> ')
+ terminal.emitData('\x1b]133;D;0\x07__DSH_PERSISTENT_BASH_PROMPT__ ')
await vi.advanceTimersByTimeAsync(10)
expect(inspections).toBe(2)
|
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
问题
使用持久化 bash 工具(tool-bash-persistent)执行命令时,1ms 的命令也要等 3.5 秒才返回。
根因
两个提示符常量不一致,终端层永远匹配不到"命令完成":
上层工具用 stty -echo; PS1=DSH_PERSISTENT_BASH_PROMPT 覆盖 PS1 后,bash 打印的提示符不再是 dsh> ,promptTextSeen 永远为 false,快速通道(stdin_read,50ms)永不触发,只能落入 idleSilenceMs(3000) + handoffGraceMs(500) = 3.5s 的静默兜底。
修复
把 CONTROLLED_PROMPT 改为与 SHELL_PROMPT 一致(含尾部空格):
`diff
`
session.ts:394 已用 CONTROLLED_PROMPT.length + 1 适配,无需再改。同步更新了 4 个测试文件的期望值(sanitize.spec.ts 改为引用常量,防再失配)。
验证
修复后命令完成即走 50ms 快速通道返回。补丁在下方评论中。
All reactions