A filename whose extension is an Object.prototype key (foo.constructor,
foo.__proto__) resolved to the inherited member through the plain-object index,
so a function reached the read card's lang hint and failed the tool-output JSON
validation, failing an otherwise successful read. Look the extension up as an
own property only. Added rejection tests, converted the zh Note headings to the
all-English sibling convention, and named the parallel-file-reads terminal
golden as the TUI-unchanged evidence in the Testing section (both languages).
readMetaFromMeta narrows the opaque persisted meta boundary, so beyond
shape it now rejects replayed JSON that is well-typed but semantically
invalid: line numbers must be 1-based integers, totalLines a non-negative
integer, and line numbers must strictly increase without exceeding
totalLines. Any violation declines to the generic fallback.
Sync the public ToolResultView contract across the core/tools and
tool-fs READMEs and docs/core-data-structures/tools for the fourth
result-view member and the ReadFileLine vocabulary, and expand the
Agent Note Testing section with the new rejection paths and the snapshot
evidence this PR carries.
grep and glob returned only model-facing text; the structured matches/paths
never reached the client. Add a card:'search' result view with a kind
discriminant ('matches' grouped by file for grep, 'paths' for glob), projected
through each tool's output.presentationMeta and read back in presentResult. The
projections re-apply the same inline cap and per-line budget as the render text
and report total + truncated, so a UI never presents a capped page as complete.
A UI without the search card falls back to content; the TUI is unchanged. The
web consumer is a follow-up.
The read tool's result carries structured numbered lines, but only the
model-facing envelope text reached the client. Add a card:'read' result view
(ReadResultView) projecting {path, lines, totalLines, lang} through the tool's
output.presentationMeta so presentResult reproduces it on live and replay
paths; the pending call stays a generic read card. A UI without the read
capability falls back to the envelope-stripped content, so the TUI is
unchanged. The web consumer that renders the line-numbered view is a follow-up.
The native adapter's route was named deepseek, colliding with pi-ai's
catalog provider of the same name, so the two DeepSeek paths could never
be mounted side by side. The web settings page needs both configurable at
once. Compositions, fixtures, goldens, scaffolding defaults, and docs all
move together (pre-release, no shim); TUI/session-query-spill/
missing-credential goldens re-recorded through their keyless refresh
modes because provider-name length shifts box padding and spill
truncation points.
Boot the glob sampling scenario from a standalone ACP composition that exposes only bash, glob, and grep. Regenerate the smaller header fixtures and trim implementation narration already owned by the Agent Note.
Remove the duplicate model-facing list tool from this branch; directory orientation remains available through bash ls. Keep the glob sampling fix, add a real ACP composition snapshot, and narrow the decision record to the shipped bug fix.
Master had advanced 392 commits. Every conflict was a derived artifact both
sides had regenerated — `docs/tool-catalog.md`, four recorded headless
session logs, and one TUI terminal expectation — so each was resolved by
taking master's version and re-running the generator and the keyless
snapshot refresh on the merged tree. No source file conflicted.
The new `examples/jsonrpc-agent` fixtures keep master's scrubbed
`{{system}}` / `{{tools}}` headers: that suite scrubs request headers, so a
new tool changes nothing there and the refresh output was discarded.
The review made every name a JSON string with `<`, `>`, and `&` escaped. The
hazards behind that are real and each is now covered: a control character
splits one entry across lines, `</` closes the envelope, and a regular file
named `x@` reads as a socket named `x` under the non-regular marker.
Quote those, and only those. `list` is the tool an agent reaches for first
and its output sits in every transcript, so `"archive"/` on every ordinary
line is a permanent cost for a case that almost never occurs. A name is now
emitted verbatim unless it matches a control character, a leading quote, a
backslash, `</`, or a trailing `@`, and is otherwise a JSON string with `</`
neutralized — the delimiter treatment `dsh-workspace-context` already applies
to instruction text, extended to an interpolated path as its
`instruction-frame-paths` TODO asks.
Asked what a workspace contained, an agent described one subfolder as the
whole project. `glob {"pattern": "*"}` matched 10030 paths across 22
top-level entries and the inline page was the first 100 of them, all under a
single unpacked archive.
Three properties compose into that page: a pattern with no `/` matches
basenames at any depth, so `*` means the whole tree rather than its top
level; `--sort=modified` orders oldest first, and unpacking an archive
restores timestamps that predate everything the user wrote; and the page was
the head of that order. Each is defensible alone, and together they make the
most ordinary request an agent receives produce a confident wrong answer.
A result within `globMaxResults` is unchanged — shown whole, in
modification-time order. Beyond it the page is filled round-robin across the
complete result's top-level entries, so one subtree cannot own every slot,
and the footer states that the page was sampled rather than taken in
modification-time order. Measured on a 24-entry, 716-file reproduction, the
head of 100 reaches 7 top-level names and the sampled page reaches 21. The
spill artifact still holds the complete sorted list.
The guidance and schema stop steering away from `ls`, state the any-depth
pattern rule, say results are files and never directories, and point at
`list` for a directory's contents.
`ctx.fs.listDir` has shipped since the filesystem seam gained it, with skill
discovery as its only consumer; the model-facing tool was deferred to a
separate decision. Nothing else could answer "what is in this directory":
`rg --files` backs glob and grep and never emits a directory entry, so an
empty directory is invisible, no output says which names are directories,
and no output gives an entry count.
`list` takes an optional `path`, defaulting to the session workspace so the
common question needs no argument, and returns the direct children of one
directory with their type. Two presentation rules carry it: directories sort
first, then files, then non-regular children, each alphabetically — so
truncation loses leaves rather than the tree — and the footer always states
the complete listing's size and composition, so a capped view can never read
as a whole directory.
It emits no `fs/observed`: seeing a filename is not reading a file, and a
listing must never satisfy the read-before-write gate.
Review feedback (tianyicui): 'process' is a poor service name. The family is
now packages/subprocess/ — @deepseek-ai/dsh-subprocess (ctx.subprocess,
abstract SubprocessService, Subprocess* vocabulary) and
@deepseek-ai/dsh-subprocess-local (LocalSubprocessService) — renamed
throughout code, compositions, docs (en+zh, pairs re-recorded), catalogs,
and gates. 'subprocess' is the precise term for managed OS children (the
Python-stdlib sense), avoids colliding with Node's global process object,
and reads as one system beside dsh-subagent-subprocess.
ds-review-bot findings addressed:
- kill() on a settled handle is now a no-op (no signal to a possibly-reused
pgid, no referenced grace timer delaying exit); pinned by a spy test.
- The moved DshEnvironmentKey/DshEnvironment/CollectedOutput types get
drift-checked type-equiv blocks on the new subprocess.md page, restoring
their manifest registration.
- subprocess.md is registered in the core.md sub-page index (en+zh).
New process/ capability family: @deepseek-ai/dsh-process owns ctx.processes —
abstract ProcessManager.spawn(spec) over a fully-explicit ProcessSpawnSpec —
plus the shared DSH_* managed-environment and CollectedOutput vocabulary;
@deepseek-ai/dsh-process-local carries the former bash-local run.ts plumbing
(detached groups, tail-keep spill-backed output, credential scrub, kill
escalation, kill-and-join disposal) with no config of its own.
dsh-bash-local becomes a consumer: it keeps command defaulting, the fused
deadline timedOut/aborted classification, the model-friendly terminal env
(now merged through the ordinary env channel), and the [stderr]-marked
background read merge, and spawns through ctx.processes. Background-process
lifetime moves to the manager, so an executor reload no longer kills live
background work; a background spawn failure is injected once into the read
path instead of being buffered as fake stderr. dsh-bash re-exports the moved
vocabulary so bash consumers keep one import root; dsh-bash-sandbox only
redeclares the inherited inject.
Every composition loading a bash executor now loads dsh-process-local (CLI,
examples, python bundled runtime, create-sdk bash feature, inline test
configs).