Files
deepseek-harness/packages/terminal/terminal-bash
Tianyi Cui 4f3a47d792 fix(terminal-bash): handle terminal protocol replies
Unix PowerShell emits cursor-position requests while PSReadLine starts and
redraws prompts. The subprocess PTY is only a transport, so those requests
went unanswered. Startup could then accept the dsh> literal echoed from its
setup source as a rendered prompt, and later sends were lost or clipped.

Feed raw PTY output into a zero-scrollback @xterm/headless state machine and
write generated replies through the provider-owned terminal handle. Drain
replies before caller input, repeat foreground inspection when terminal
activity races the sample, and retain send ownership until parser and reply
work quiesce. Coalesce raw chunks behind one active parser write so large
Windows output cannot create thousands of queued parse callbacks.

Publish pwsh only from backend stdin_read evidence and start one timeoutMs
deadline before the complete startup retry loop, so inferred-idle follow-ups
cannot reset the bound. Dispose the emulator when the
terminal or cleanup fails. Document the fail-loud ConstrainedLanguage path
and add focused coverage for split queries, reply ordering, foreground
resampling, failure containment, batching, timeout, and disposal.
2026-08-23 19:24:50 +08:00
..

@deepseek-ai/dsh-terminal-bash

English | 中文

Persistent shell backend for ctx.terminals over ctx.subprocess.spawnTerminal. It starts an interactive shell under the shared ctx.sandboxPolicy, retains bounded line-oriented output, and detects readiness while the subprocess provider owns PTY allocation, environment scrubbing, foreground process groups, signalling, and complete terminal-session cleanup. The same PTY backend therefore composes with local or remote execution-world providers.

Plugin (terminal-bash)

The plugin injects pty, sandboxPolicy, and subprocess, then registers the configured backend type (shell). danger-full-access starts the shell directly without requiring a sandbox provider; confined modes require a same-world ctx.sandbox and wrap the exact shell argv through it, failing before spawn when none is mounted. At spawn, one ctx.sandboxPolicy.resolve({ session }) call supplies both the effective mode and the session workspace root; the same root is the default shell cwd when the caller omits one. A change to a different effective mode is rejected before its sandbox/mode event commits while that owner has an open PTY or a spawn in progress; the fence is attached to the exact owner and therefore outlives a provider reload that retains existing sessions. Wait for creation to settle and close the sessions before changing modes, so a terminal opened with wider access cannot survive a downgrade.

shellDialect selects the shell stack (bash default, pwsh): it picks the default shellPath/shellArgs (bash --noprofile --norc -i; pwsh -NoLogo -NoProfile through the shared dsh-pwsh-local resolver) and the startup contract. The bash dialect installs its prompt through the environment (PS1 plus an OSC 133;D;-terminated PROMPT_COMMAND). pwsh cannot install a prompt from the environment, so the backend writes a prompt function through the session and accepts startup only after the backend reports stdin_read; echoed setup text cannot publish the shell. One timeoutMs deadline starts before the complete pwsh startup loop, so an inferred_idle follow-up send does not restart the bound. Its environment drops the bash-only markers and adds NO_COLOR, while the first send prefixes the shared dsh-pwsh-local encoding preamble, pinning [Console]::OutputEncoding and $OutputEncoding to UTF-8 before anything runs. A zero-scrollback @xterm/headless instance consumes the raw PTY stream and writes terminal-protocol replies, including cursor-position reports required by Unix pwsh, through the same terminal handle. The backend drains those replies before every caller input and samples foreground state only after the protocol state stayed quiet throughout inspection. It keeps one parser write active and coalesces later raw chunks into the next batch, so high-volume output does not schedule one parser task per chunk. The line-oriented sanitizer and bounded buffers remain the only output projection. Both dialects emit the same BEL-terminated OSC marker, so the readiness machinery and consumers are dialect-agnostic.

Readiness combines a foreground-verified private bash prompt marker, provider-reported foreground stdin-wait facts, silence fallback, and absolute timeout. A marker is not ready until the printable tail after the latest owned marker exactly equals the controlled PS1, including when the OSC marker and prompt are split across data callbacks; echoed input or output following an earlier prompt therefore cannot settle the current send. The controlled PROMPT_COMMAND re-asserts that PS1 before every prompt, so an in-shell prompt override cannot degrade later sends to silence readiness. Prompt and silence evidence collected before the provider write, including while pre-write foreground inspection is pending, is discarded at the write boundary. When bash prints the marker before the terminal provider publishes its return to the foreground process group, polling retains the candidate for handoffGraceMs past the ordinary silence bound so a coincident handoff can win. An interactive child that inherits PROMPT_COMMAND therefore cannot suppress inferred-idle readiness until the absolute timeout. Unknown foreground state is never a positive exact-idle signal. A foreground group's stdin wait that existed before a send is likewise not post-write readiness: the same group must be observed outside that wait before a later wait can settle the send, while a changed foreground group is new evidence. During unpublished startup, a fallback requires observed output; zero-output silence cannot publish an empty session, and timeout rejects the spawn. Cancellation closes the unpublished shell and rejects with the caller's exact abort reason; TerminalBackendCleanupError separately preserves a cleanup failure. The caller's signal is forwarded for terminal allocation and readiness initialization; after publication the handle owns its lifetime. Incomplete terminal-control sequences are bounded by maxReadBytes and discarded through their terminator after crossing that limit; malformed UTF-8 terminal output uses replacement characters, and a trailing carriage return is carried across callbacks so split CRLF becomes one newline.

Send cancellation marks queued input as canceled before asking the terminal handle to signal the current foreground process group with a real SIGINT; if asynchronous pre-write inspection later settles, it cannot execute that input. If a provider write is already in flight, signalling waits for it to settle; a rejected write sends no signal. The canceled send retains its slot until the write and foreground signalling settle, so a successor cannot receive either late bytes or that signal. A provider write or signal that never settles therefore retains the slot indefinitely; closing the session (terminal_close) is the recovery. The absolute deadline remains armed while cancellation waits. A signal failure is a terminal transport failure and rejects the active send. Cancellation never emulates interruption by writing \x03, so raw-mode programs remain cancellable. Close rejects new public signals, stops readiness polling, and awaits the handle's provider-owned complete-session termination before settling the active send as session_exit.

Model Experience

Current file policy and indirect consumer

What the model sees

The policy owner contributes capability-neutral sandbox:policy context. Through @deepseek-ai/dsh-tool-terminal or another PTY consumer, the model may also receive bounded MOTD, send deltas, scrollback pages, readiness reasons, and cleanup errors.

Token effect

The current-policy clause is present while this backend is mounted. Retained PTY scrollback is not placed in model history until a consumer returns bounded output.

KV Cache effect

A standing-policy change appends an owner-rendered superseding runtime-context snapshot after retained history; consumer results remain append-only.

Known Limitations and Deferred Work

  • A headless xterm instance maintains control-sequence state only for terminal-protocol replies. Returned output remains line-oriented and normalized; full-screen alternate-buffer interaction is unsupported.
  • Exact stdin-wait detection depends on the mounted subprocess provider; providers that cannot prove it use prompt-marker and silence/timeout readiness. Windows is such a provider: the shell pid is the pseudo foreground group and there is no exact stdin-wait tier, so a marker-less child settles on the silence bound.
  • The pwsh bootstrap writes through [Console]:: (the UTF-8 encoding pin and the prompt function), which the Windows ACL sandbox's read-only mode (ConstrainedLanguage) may deny. When that prevents marker readiness, startup rejects at timeoutMs instead of publishing a shell whose setup did not complete.
  • Cleanup guarantees are those of SubprocessTerminalHandle; provider-specific gaps belong to that implementation's contract rather than this PTY consumer.
  • Sessions do not survive harness process exit.