DSHarness Systems Dissection Pinned baseline 47f943859b · 36 verified / 0 in progress / 36 chapters
中文
Execution Environment·Chapter 21

Shell, PTY, Terminal, and Background Jobs

Short-lived processes, persistent sessions, retained output, and ownership

VerifiedUpstream 47f943859bScope: Trace subprocesses, shell executors, terminal ownership, job registries, spill, signals, and kill escalation.

Conclusion: This Is Not One Shell but Four Orthogonal Lifecycle Systems

DeepSeek Harness splits “run a command” into four layers: ctx.subprocess owns the execution world, process trees, and byte streams; ctx.shell normalizes a one-shot command into a foreground result or background process; ctx.jobs gives heterogeneous producers one background identity, ownership, and completion plane; and ctx.terminals manages PTY sessions that survive across calls. Tools are model-facing adapters over those seams; they do not own the underlying resources.

The split solves lifecycle rather than command syntax: who may see a resource, when cancellation counts as complete, how output loss remains explicit, and who reaps work when an Agent disappears. The cost is that deployment composition, owner scope, and provider capability all matter. A tool named bash alone does not reveal whether it starts a fresh process or reuses a persistent shell.

1. Four-Layer Topology: Execution, Commands, Background Control, and Interactive Sessions

model tools
  bash / pwsh ───────────────→ ctx.shell ──────→ ctx.subprocess.spawn
       └─ run_in_background ─→ ctx.jobs ───────┘

  terminal_* ────────────────→ ctx.terminals ─→ PTY backend
       └─ background send ───→ ctx.jobs             └→ ctx.subprocess.spawnTerminal

  persistent bash wrapper ───→ ctx.terminals (one reused PTY per Agent)
Substrate contract

SubprocessRuntime is the shared substrate for ordinary processes and the shipped Bash terminal backend. A provider must resolve executables in the same execution world, return live handles immediately, define tree-scoped termination contracts, and reap live resources when the service is disposed. TerminalBackend remains replaceable, so another backend may bring a different substrate. SHELL-SUBPROCESS-SEAM

ctx.shell intentionally knows nothing about Sessions or Job IDs; background registration, polling, and notices belong to the Job layer. Conversely, the Job Registry knows nothing about commands, PTYs, or subagent implementations. It accepts producer-supplied cancel, done, and readOutput hooks.

2. Shipped Compositions Select Two Completely Different bash Contracts

PresetModel-facing BashStateBackground control
Standarddsh-tool-bash; pwsh on WindowsFresh process per callrun_in_background plus three job_* tools
Minimaldsh-tool-bash-persistentOne reused PTY per exact AgentCommands serialize; long work backgrounds inside the shell
Terminal tool packageSix terminal_* toolsExplicit create, send, read, signal, list, and closeterminal_send can join the Job Registry
Actual assembly

The Standard preset registers one-shot Bash off Windows and PowerShell on Windows, plus Job controls. Minimal isolates its own Terminal service/backend and exposes only persistent Bash and an editor. Standard does not expose the six Terminal tools, and Minimal does not hand raw PTY lifecycle control to the model. SHELL-STANDARD-COMPOSITIONSHELL-MINIMAL-COMPOSITION

3. The Subprocess Seam Rejects Hidden Defaults: the Caller Specifies Every Execution Condition

SubprocessSpawnSpec explicitly carries argv, cwd, all three stdio dispositions, cleanup grace, an AbortSignal, and an environment overlay. The provider does not guess a shell, directory, buffer limit, or deadline. argv[0] is the executable; the substrate performs no shell interpretation.

Outcome semantics

done rejects only for a spawn-level failure; exit code and signal after a successful spawn are ordinary outcomes. Collected streams use caller-owned byte offsets, so independent readers do not consume one another. A raw pipe belongs completely to the upper layer. SHELL-SPAWN-CONTRACT

This allows local and remote execution to be swapped without changing the tool contract. A provider can change executable lookup, process/PTY transport, and filesystem world while callers continue to own policy and presentation.

4. Child Environments Are Scrubbed First, Then the Current Harness Identity Is Rebuilt Explicitly

process.env
  ├─ remove names matching KEY|PASSWORD|SECRET|TOKEN
  ├─ remove every DSH_* name, case-insensitively
  └─ retain PATH / HOME / locale / proxy and ordinary values
        ↓
caller env overlay (string restores; undefined deletes)
        ↓
shell tool injects the current ctx.shellEnv DSH_* snapshot per call
Preventing stale identity leaks

Ordinary-process and PTY backends share one scrub function. The explicit overlay merges afterward, so a deployment can still deliberately forward a credential-shaped or DSH_* value. SHELL-ENV-SCRUB

Managed variables

ctx.shellEnv rebuilds, sorts, and freezes DSH_HOME, DSH_SHELL=1, an Agent's DSH_SESSION_ID, and DSH_SESSION_JSONL when the backend exposes a location. Contributors must predeclare keys; duplicate ownership or an undeclared runtime key fails loudly. SHELL-MANAGED-ENV

5. Long Output Prefers the Tail; a Complete Spill Is Published Only While It Remains Trustworthy

1

Bounded memory

The collector counts bytes and drops from the head to retain exactly the last maxBytes.

2

Lazy spill

First overflow creates a random, exclusive, mode-0600 file in a private temporary directory and backfills prior chunks.

3

Completeness has a cap

Once the total stream exceeds the spill cap, the spill is removed because it can no longer represent the complete output.

4

Readable while running, final after settlement

The current spill path may be returned while complete; it represents final complete output only after process settlement and a successful seal.

Observable loss

An offset read returns lossy, the next offset, and the spill path while complete. During execution that path represents the complete stream so far; only successful settlement sealing makes it the final file. If the requested offset has fallen out of the memory window, the reader returns the entire retained tail instead of pretending it is a contiguous delta. SHELL-OUTPUT-SPILL

Tail retention is optimized for diagnostics: errors and summaries often appear at the end, while the spill preserves complete retrieval. Tool rendering applies another result cap, so recoverable substrate output does not mean every byte enters that model request.

6. One-Shot Shell Treats Nonzero Exit, Timeout, and Cancellation as Facts, Not Infrastructure Failures

Casectx.shell.runModel tool
Exit 0 / nonzeroResolves a ShellRunResultReturns output; nonzero adds an exit marker
Executor timeoutResolves with timedOut:trueShows timeout and partial output
Tool AbortSignal wins firstResolves with aborted:trueConverts it to a stable tool-aborted error
Spawn/provider failureRejects in foregroundNormalizes an execution error
Service contract

The executor's resolve owns defaults and caps; foreground run rejects only for infrastructure failure. Background start applies no timeout, and even a spawn failure becomes a killed handle with stderr so Job cleanup cannot hang on a rejected done. SHELL-EXECUTOR-CONTRACT

7. Switching to Background Is an Atomic Ownership Transfer, Not Merely “Do Not Await”

bash(args)
  → resolve arguments / escalation / workdir / shellEnv
  → verify the tool-call signal is not already aborted
  → jobs.start({ owner, kind, label, run })
       ├─ controller / owner / capacity preflight
       ├─ run() actually spawns
       ├─ register record and JobId
       └─ Job runtime owns cancel / done / output
  → return job id
Detached cancellation boundary

The Bash tool retains tool-call cancellation until jobs.start. After Job ID publication it no longer passes exec.signal to the process; only job_kill or owner/service teardown stops it. A nonzero background command is completed with an exit-code detail, not registry-level failed. SHELL-BASH-TOOLSHELL-BACKGROUND-OUTCOME

8. The Job Registry Is a Producer-Neutral, Process-Local Control Plane

A Job carries a kind, label, optional exact-Agent owner, output cap, and three hooks. The producer returns cancel, non-rejecting done, and optional consuming readOutput from run(); the registry owns IDs, status, access control, waiters, notices, and teardown.

Check before starting work

The local registry validates a reachable scoped controller, kind, label, output limit, live registered owner, and the per-owner active cap—10 by default—before invoking the producer. There is no queue. Only after all preflight succeeds does it call run() and mint <kind>-N. SHELL-JOBS-START

Isolation and first-wins

An owned Job can be read or cancelled only by a caller with the same Session ID; listeners are routed through the exact owner's scope chain. Settlement is first-wins: commit terminal state, release waiters, notify UI observers, then invoke completion listeners, so a reporter cannot open a turn before other observers see the committed record. SHELL-JOBS-SETTLEMENT

9. Job Tools Distinguish “Cancellation Requested” from “Actually Stopped”

ToolRead/mutationKey semantics
job_outputConsumes the next stream delta, or retrieves final output after settlementA timed-out wait:true returns running and leaves the Job alive
job_listLists visible live and settled recordsThe registry does not delete records on settlement
job_killCalls the cancel hook immediatelyReturns cancellation-requested; terminal state waits for producer settlement
Model control plane

All three tools honor the producer's output cap. job_output owns its wait deadline so ordinary “still running” is not mapped to a tool timeout. Kill then reads a non-consuming snapshot, avoiding accidental consumption of remaining output. SHELL-JOB-TOOLS

10. Completion Notices Choose the Current Turn's Inbox or a New Follow-up Turn

Job settles and is not reported
  ├─ owner busy                         → owner.inject(notice)
  ├─ owner idle + wake budget remains  → owner.followup(notice)
  └─ quiet mode / exhausted budget     → owner.inject(notice)
Bounding self-excitation

The default wake budget permits three consecutive Job-completion turns per exact Agent. Only claiming a genuinely user-authored message resets it. This prevents “completion starts a turn, that turn starts another job, its completion starts another turn” from buying requests forever. SHELL-JOB-NOTICES

11. Job Lifetime Is Bound to the Exact Agent Object, Not Merely a Reusable Session String

Owner cleanup attaches to the exact Agent scope. On disposal the registry requests cancellation for owned Jobs, awaits every settlement, then deletes records and updates mirrors. Whole-service disposal performs the same quiescence protocol.

Failure boundary

If a cancel hook throws, the registry force-fails the record and explicitly records that work may be orphaned. If cancel returns but the producer never settles, teardown cannot distinguish “slow stop” from “ineffective cancellation” and continues waiting. SHELL-JOBS-CLEANUP

The Web Job list is only a read-only mirror pushed by the Host. It empties on process restart while historical start cards remain in the transcript. “A start was recorded” and “a live control handle still exists” are distinct facts. SHELL-JOBS-UI

12. The Terminal Service Publishes Exact-Owner Sessions, Not Guessable Global PTYs

1

Reserve

Validate the live owner, backend, and owner-local name; reserve the name and pending spawn.

2

Initialize

The backend allocates and initializes a PTY; owner/service cancellation can abort unpublished setup.

3

Publish

Recheck the service and exact owner before inserting pty-N into the registry.

4

Roll back

Close an unpublished session on failure; retain or aggregate cleanup errors instead of silently leaking.

Access and exclusion

Read, signal, kill, and list all require the exact owner; at most one send is active per PTY. Close removes the record only after backend quiescence. A close failure clears the closing fence so a caller can retry. SHELL-TERMINAL-SERVICE

13. A terminal_send Return Means “Reasonable Time to Think Again,” Not Command Exit

waitReasonEvidenceIt does not prove
stdin_readControlled prompt plus shell foreground group, or foreground syscall probe waiting for inputThe entire Session exited
inferred_idleA configured silence window after outputThe foreground process completed
timeoutThis send's wait deadline elapsedThe process was killed or the Session exited
session_exitTop-level PTY process outcome arrivedAn earlier idle inference was equivalent to exit
Readiness algorithm

The local PTY backend combines a controlled prompt, foreground process group, Linux exact stdin-wait probing, output silence, and an absolute timeout. A prompt is accepted only after the shell regains the foreground group. Cancelling an active send delivers a real SIGINT to the foreground group and retains the exclusive slot until the signal path settles. SHELL-PTY-READINESS

14. Six Terminal Tools Expose the Resource; Persistent Bash Hides It

InterfaceModel responsibilityRuntime responsibility
terminal_open/send/read/signal/list/closeRetain Session IDs, interpret readiness, and eventually closeOwner fence, scrollback, signals, quiescent cleanup
Persistent bash(command)Submit only commandsCache a PTY per owner, serialize calls, extract marker-delimited exits, reset after timeout/abort
One-shot bash/pwshSupply a complete command/workdir each timeFresh process, timeout, bounded output, optional Job detach
Explicit Terminal tools

All six tools require a calling Agent. Background terminal_send reserves the PTY's exclusive send and exposes it as a pty-send Job. Job cancellation becomes operation cancellation/SIGINT, and output plus results share one byte cap. SHELL-TERMINAL-TOOLS

Hidden Persistent Bash

The wrapper surrounds each command with random start/end markers and serially reads scrollback per owner. Timeout, abort, send failure, or shell exit closes the uncertain session and recreates it from the workspace next time rather than reusing potentially contaminated state. SHELL-PERSISTENT-BASH

15. Kill Targets a Process Tree, but the Quiescence Proof Is Platform-Specific

An ordinary local process uses a detached process group on POSIX, TERM followed by grace and KILL, with group-liveness observation. Windows requests tree termination through taskkill /T /F but treats direct-child exit as the observable completion proxy; it has no independent descendant/group liveness probe.

Ordinary process tree

On POSIX, the implementation continues probing group liveness after direct-child close so a TERM-trapping helper is not missed, and stops escalation only after group disappearance. The Windows guarantee is weaker: termination includes /T, but the wait boundary remains root-process exit and therefore does not independently prove whole-tree quiescence. SHELL-PROCESS-TREE

PTY cleanup

The PTY backend tracks descendants by PID plus start identity, clears descendants, stops the shell, then scans descendants again. SIGKILL to the current shell itself is rejected; callers should close the Terminal session. Close fails explicitly if survivors remain. SHELL-PTY-CLEANUP

16. Cross-Platform and Remote Replacement Belongs in the Substrate/Provider, Not the Tool Contract

Bash and PowerShell tools share ctx.shell, Job, approval, and managed-environment semantics; executable choice, launch flags, encoding, and sandbox runner stay provider-specific. The E2B provider replaces executable lookup, ordinary process execution, and Terminal transport behind ctx.subprocess, leaving upper Shell/PTY consumers on the same seam. SHELL-E2B-SEAM

Design assessment

This is a stronger boundary than letting every tool call child-process APIs directly: security environment, process trees, output, and PTY transport can be replaced centrally. Cross-platform parity still depends on each concrete tool provider actually reusing the shared helpers. A public Service Definition is a contract, not automatic proof of parity.

17. Three Static Audit Findings at the Pinned Baseline

FindingImpactAssessment
shellEnv.list() omits the registry's three built-insDiagnostics/UI treating it as exhaustive will under-report actual injectionExplicitly recorded in both source TODO and README
The PowerShell README still declares old inject names: tools,bash,systemPrompt,bashEnvExtension authors copying it reference obsolete or absent seamsCurrent source uses tools,shell,systemPrompt,shellEnv
PowerShell's default cwd still uses the raw Session header while Bash canonicalizes and prefers the policy workspace rootThe same Session can have non-isomorphic path identity across platformsThe README records this as a known parity gap
Reproducible drift

These are direct comparisons between current production source and adjacent official READMEs, not runtime inference. SHELL-ENV-LIST-GAPSHELL-PWSH-DOC-DRIFTSHELL-PWSH-INJECTSHELL-PWSH-CWD-GAPSHELL-BASH-CWD

18. Guarantee Matrix: Durable Facts Versus Current-Process Control State

ObjectAcross callsAcross AgentsAcross process restartOutput read
Foreground one-shot ShellNoN/ANoReplayable after its result enters the Session
Background JobYesNo for owned JobsNoStreams are single-consumer; result/notice is durable once logged
Explicit TerminalYesNo, exact ownerNoBounded paginated scrollback
Persistent BashYes, same AgentNoNoOne marker-delimited command result
Spill fileWhile process/file survivesHost path permissions decideNot Session durabilityPublished only while complete and successfully sealed
Final assessment

The strongest design choice is keeping “cancellation requested” separate from “resource quiescent,” “wait returned” separate from “command exited,” and “transcript has a start card” separate from “a live control handle still exists.” That precision is also the largest cognitive cost: Process, Job, Terminal, and Turn each have their own state machine. Operations and UI must expose those states rather than flattening all of them into running/done.

Verification status

In addition to tracing source, configuration, READMEs, and test control flow at the pinned commit, this delivery ran 15 targeted Vitest files spanning approval, presets, escalation, filesystem, process, Job, and Terminal behavior: 508 tests passed and one was skipped. Platform differences and the chapter's static findings remain separately bounded by their evidence.

My Learning Notes

Autosaved only in this browser. Nothing is uploaded or committed. Export Markdown whenever you want to keep a copy.