fix(fs): harden directory listing and glob sampling

This commit is contained in:
NI0317
2026-07-28 13:01:26 +08:00
parent a9c0e00620
commit 717852423f
46 changed files with 480 additions and 219 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-27-directory-listing-tool.md
2026-07-27-directory-listing-tool.md: 4292c173f07064ff8825462e08096780c20ad9d6
2026-07-27-directory-listing-tool.zh.md: 4033254b08a77ba7ed9b1f3e638213f019abb692
2026-07-27-directory-listing-tool.md: a23cdb0090f1a88b783d9717aa3f0434b6c2782e
2026-07-27-directory-listing-tool.zh.md: bab0138a79ae770ae7e841392b879940e8ac2dc4

View File

@@ -16,13 +16,13 @@ Three properties of `glob` compose into that page:
- **`--sort=modified` orders oldest first.** Unpacking an archive restores the timestamps stored inside it, which predate everything the user wrote, so a freshly unpacked subtree lands at the very front of any broad match. (Ripgrep sorts ascending; `--sortr` is the descending form and is not used here.)
- **The inline page was the head of that order.** `globMaxResults` (100) paths were kept from the front. Under the first two properties, one subtree takes every slot.
Each property is defensible alone. Together they make the most ordinary request an agent receives — "what is in this directory" — reliably produce a confident wrong answer, because nothing in the result distinguishes "the 100 newest files in this workspace" from "this workspace".
Each property is defensible alone. Together they make the most ordinary request an agent receives — "what is in this directory" — reliably produce a confident wrong answer, because nothing in the result distinguishes "the 100 oldest files in this workspace" from "this workspace".
### What ordering can and cannot fix
A directory's name reaches the model only as the prefix of one of its files' paths, since `rg --files` emits files and never directory entries. That is enough for ordering to matter a great deal, and not enough for `glob` to answer the question.
Measured on a reproduction of the failure's shape — 24 top-level entries, 716 files, one recently-written subtree:
Measured on a reproduction of the failure's shape — 24 top-level entries, 716 files, one old-timestamped subtree:
| First 100 paths chosen by | Distinct top-level names visible |
| --- | --- |
@@ -39,31 +39,30 @@ Two changes, in the two packages that own the two halves of the failure.
`@deepseek-ai/dsh-tool-fs-search`. A result within `globMaxResults` is unchanged: shown whole, in modification-time order. Only when the result is larger does the page change — and there, taking the head is what fails.
`sampleAcrossTopLevel` groups the complete result by leading path segment and fills the page round-robin: every top-level entry gets a slot before any entry gets a second, and an entry that runs out of paths drops out so its remaining slots go to the rest. Sort order survives where it still carries meaning — groups are visited in the order ripgrep first emits them, and each group's own paths keep their relative order. The page is emitted grouped by entry rather than interleaved, so the breadth is legible at a glance.
`sampleAcrossTopLevel` removes the displayed search-root prefix, groups the complete result by the next path segment, and fills the page round-robin: every entry immediately beneath the actual relative or absolute root gets a slot before any entry gets a second, and an entry that runs out of paths drops out so its remaining slots go to the rest. Sort order survives where it still carries meaning — groups are visited in the order ripgrep first emits them, and each group's own paths keep their relative order. The page is emitted grouped by entry rather than interleaved, so the breadth is legible at a glance.
The footer states the basis, because a page that silently stopped being "the newest N" would be a second, quieter version of the same lie:
The footer states the basis, because a page that silently stopped being "the first N in modification-time order" would be a second, quieter version of the same lie:
```
(Showing 100 of 10030 paths, sampled across 22 of the 22 top-level entries this pattern matched
instead of taken in modification-time order. Full sorted result stored at: …)
```
When the page cannot reach every top-level entry — more entries than slots — the footer says so with the shown/total spread and points at `list`. A flat result, where every path is its own top-level entry, keeps the plain `(Showing k of n paths. …)` footer: there the round-robin *is* the sorted head, and naming a spread would only restate the counts. The spill artifact always holds the complete list in modification-time order, so the sorted view is never lost.
When the page cannot reach every top-level entry — more entries than slots — the footer says so with the shown/total spread and tells the model to narrow `path`. A flat result, where every path is its own top-level entry, keeps the plain `(Showing k of n paths. …)` footer: there the round-robin *is* the sorted head, and naming a spread would only restate the counts. The spill artifact always holds the complete list in modification-time order, so the sorted view is never lost.
The guidance and schema stop misleading in the same change: "not shell find or ls" becomes "not shell find"; both now state that a pattern without `/` matches basenames at any depth, that results are files and never directories, that a fitting result is modification-time ordered while a larger one is sampled, and that `list` is the tool for a directory's contents.
The guidance and schema stop misleading in the same change: "not shell find or ls" becomes "not shell find"; both now state that a pattern without `/` matches basenames at any depth, that results are files and never directories, and that a fitting result is modification-time ordered while a larger one is sampled across top-level entries. They do not recommend sibling-package tools that may be absent from the current composition.
### `list`, in `@deepseek-ai/dsh-tool-fs`
A fourth model-facing filesystem tool over the existing `ctx.fs.listDir` primitive, which until now shipped with skill discovery as its only consumer; [the seam Agent Note](../../archived/architecture/2026-07-03-filesystem-directory-listing-seam.md) deferred the model-facing consumer to a separate decision, which this is.
It takes an optional `path` defaulting to `.`, the calling agent's session workspace, so the common question needs no argument — and returns the direct children of one directory as `{ path, entries: [{ name, type }] }`, where `type` is `file`, `directory`, or `other`. It calls `listDir` and nothing else: the seam already reports absence as `FS_NOT_FOUND` and a non-directory target as `FS_NOT_DIRECTORY`, so a preceding `stat` would add a round-trip and a second source of truth.
It takes optional `path` and 1-based `offset` arguments, defaulting to the calling agent's session workspace and entry 1, and returns one bounded page as `{ path, offset, entries: [{ name, type }], totalEntries, counts }`, where `type` is `file`, `directory`, or `other`. It calls `listDir` and nothing else: the seam already reports absence as `FS_NOT_FOUND` and a non-directory target as `FS_NOT_DIRECTORY`, so a preceding `stat` would add a round-trip and a second source of truth.
Two presentation rules carry the decision:
Three presentation rules carry the decision:
- **Directories sort first, then files, then non-regular children, each alphabetically** — in the canonical value as well as the rendered text, so a Code Mode caller and the model see one ordering contract. The seam returns stable name order, which scatters subdirectories through the alphabet; capping such a list can drop every subdirectory and reproduce, inside `list`, the same blindness. Directory-first ordering makes truncation lose leaves, never structure.
- **The footer always states the complete listing's size and composition** — `(22 entries: 18 directories, 4 files)`, and when the view is capped at `listMaxEntries` (default 200, configurable), `(Showing 200 of 5000 entries: 12 directories, 4988 files. …)`. A partial listing therefore cannot read as a whole directory.
Directory entries render with a trailing `/` and non-regular children with `@`, so the model can tell what it may descend into without a second call.
- **Directories sort first, then files, then non-regular children, each alphabetically** before paging, so every offset traverses one stable order and the first page keeps navigable structure.
- **The canonical value and Native result carry one recoverable page** of at most `listMaxEntries` (default 200, configurable). The footer states the complete size and composition and gives `offset=<next>` until the final page, so omitted sibling names remain reachable.
- **Filesystem text cannot forge presentation structure.** The path and entry names render as JSON strings with envelope-significant characters escaped; directory `/` and non-regular `@` markers sit outside the quoted name, so a regular filename ending in `@` remains distinguishable.
`list` emits no `fs/observed`. Seeing a filename is not reading a file, and a listing must never satisfy the read-before-write gate that `@deepseek-ai/dsh-fs-policy` enforces. It declares `isConcurrencySafe`, because it mutates nothing at all.
@@ -73,7 +72,7 @@ They answer different questions and neither substitutes for the other. `list` an
## Alternatives considered
**Leave `glob` ordering alone and only warn in the footer.** This is what the first implementation did: keep the recency head and add a clause saying the head covered 1 of 22 top-level entries. Rejected once measured. A warning asks the model to distrust the only data it has and go elsewhere; a better page just is not wrong. The warning also does nothing for a model that stops reading at the paths, which is the failure being fixed.
**Leave `glob` ordering alone and only warn in the footer.** This is what the first implementation did: keep the oldest-first head and add a clause saying the head covered 1 of 22 top-level entries. Rejected once measured. A warning asks the model to distrust the only data it has and go elsewhere; a better page just is not wrong. The warning also does nothing for a model that stops reading at the paths, which is the failure being fixed.
**Sample always, replacing modification-time order outright.** Rejected. Over a complete result the order answers age questions — what is stale, what was touched last — a genuinely useful and separate purpose, and a complete result is the case where the order costs nothing and means everything. Sampling only past the cap is the point where the order has already stopped describing the result: the head of a 10,030-path list is not "the oldest files worth knowing about", it is an arbitrary 1% of them.
@@ -91,11 +90,11 @@ They answer different questions and neither substitutes for the other. `list` an
**Make `list` recursive with a depth argument.** Rejected for now. One level composes: the model lists what it needs to descend into. Recursion reintroduces the size and truncation problems this note exists to fix, and the provider primitive is deliberately one-level.
**Spill a capped listing through `ctx.spillStore`, as `glob` does.** Rejected for v1. `glob` needs spill because a truncated path list has no cheap successor call; a truncated listing does, and the footer states the complete size and composition, so the model knows both that it is looking at part of a directory and what to do about it.
**Spill a capped listing through `ctx.spillStore`, as `glob` does.** Rejected for v1. `glob` needs spill because its schema has no offset; `list` has a cheap successor call through the exact next offset in the footer, while every page repeats the complete size and composition.
## Consequences
An over-cap `glob` result no longer returns the most recently modified paths. That is a real contract change on a hot path, and it is why the footer, the prompt section, and the schema all state the sampled basis rather than leaving the model to infer it. A result within the cap is byte-identical to before, so age-ordered reading keeps working wherever it was working.
An over-cap `glob` result no longer returns the oldest paths at the head of modification-time order. That is a real contract change on a hot path, and it is why the footer, the prompt section, and the schema all state the sampled basis rather than leaving the model to infer it. A result within the cap is byte-identical to before, so age-ordered reading keeps working wherever it was working.
Balancing is by first path segment only, so a result concentrated deeper — one enormous directory inside an otherwise even tree — is still shown unevenly below the top level. Recorded in the package's Known Limitations.
@@ -105,6 +104,6 @@ The shipped tool surface grows by one tool in every deployment that loads `@deep
## Testing
Package tests pin the model-visible text of both surfaces. For `glob`: `sampleAcrossTopLevel` over a concentrated result, an exhausted group handing its slots on, a page smaller than the top level, absolute paths outside the workdir, and a flat result that must reproduce the sorted head; plus end-to-end assertions that a fitting result is untouched, that an over-cap result returns the sampled page with the sampled-basis footer, that the list hint disappears once the page reaches every entry, and that a flat over-cap result keeps the plain footer. For `list`: the envelope, type markers, singular and plural footers, the empty-directory footer, and the capped footer that keeps the sole directory visible. A registry-level test asserts that a listing emits no `fs/observed` and that a following `edit` still fails `FS_NOT_OBSERVED`, so the tool cannot become an accidental read-before-write bypass. Prompt-section registration, schema registration, and HMR disposal cover the fourth tool alongside the existing three.
Package tests pin the model-visible text of both surfaces. For `glob`: sampling over a concentrated result, an explicit relative root, more top-level groups than JavaScript's argument limit, an exhausted group handing its slots on, a page smaller than the top level, absolute paths outside the workdir, and a flat result that must reproduce the sorted head. For `list`: ordering, complete composition, offset continuation and rejection, empty directories, and filesystem names containing newlines, tag text, or marker suffixes. A registry-level test asserts that a listing emits no `fs/observed` and that a following `edit` still fails `FS_NOT_OBSERVED`, so the tool cannot become an accidental read-before-write bypass. Prompt-section registration, schema registration, and HMR disposal cover the fourth tool alongside the existing three.
The assembled transcript is the `fs-list` ACP scenario: a workspace whose answer is its subdirectories, where the model calls `list` with no arguments and the pinned tool result carries the directory-first envelope and its composition footer — an answer `glob` could not have produced at all.

View File

@@ -16,13 +16,13 @@ Status: implemented
- **`--sort=modified` 按从旧到新排序。** 解包压缩档会还原档案内部保存的时间戳它们早于用户自己写下的一切因此新近解包的子树会落在任何宽泛匹配的最前面。ripgrep 按升序排序;降序是 `--sortr`,此处未使用。)
- **内联页面取的是该顺序的头部。** 从最前面保留 `globMaxResults`100条路径。在前两项性质之下一个子树吃掉全部位置。
单看每一项都站得住。合在一起,它们让 agent 收到的最普通的请求——「这个目录里有什么」——稳定地产出一个笃定的错误答案,因为结果里没有任何信息能区分「本工作区最新的 100 个文件」与「本工作区」。
单看每一项都站得住。合在一起,它们让 agent 收到的最普通的请求——「这个目录里有什么」——稳定地产出一个笃定的错误答案,因为结果里没有任何信息能区分「本工作区按从旧到新顺序排在最前的 100 个文件」与「本工作区」。
### 排序能修什么,不能修什么
由于 `rg --files` 输出文件、从不输出目录条目,一个目录的名字只能作为其下某个文件路径的前缀抵达模型。这既足以让排序变得非常重要,也不足以让 `glob` 回答那个问题。
在一份复现该失败形态的目录上实测——24 个顶层条目、716 个文件、一个新近写入的子树:
在一份复现该失败形态的目录上实测——24 个顶层条目、716 个文件、一个时间戳较旧的子树:
| 前 100 条路径的挑选方式 | 可见的顶层名个数 |
| --- | --- |
@@ -39,31 +39,30 @@ Status: implemented
`@deepseek-ai/dsh-tool-fs-search`。未超过 `globMaxResults` 的结果完全不变:整体展示,按修改时间排序。只有结果更大时页面才改变——而恰恰在这种情况下,取头部是会失败的做法。
`sampleAcrossTopLevel` 路径段对完整结果分组,并以轮转方式填充页面:每个顶层条目都先拿到一个位置,任何条目才可能拿到第二个;某个条目的路径用尽后退出,其剩余份额交给其余条目。排序在仍有意义的地方被保留下来——分组按 ripgrep 首次输出它们的顺序访问,而每个分组内部的路径保持原有相对顺序。页面按条目分组输出而非交错输出,使覆盖广度一眼可辨。
`sampleAcrossTopLevel` 移除所显示的搜索根前缀,再按下一个路径段对完整结果分组,并以轮转方式填充页面:实际相对或绝对搜索根正下方的每个条目都先拿到一个位置,任何条目才可能拿到第二个;某个条目的路径用尽后退出,其剩余份额交给其余条目。排序在仍有意义的地方被保留下来——分组按 ripgrep 首次输出它们的顺序访问,而每个分组内部的路径保持原有相对顺序。页面按条目分组输出而非交错输出,使覆盖广度一眼可辨。
footer 会说明取用依据,因为一个悄悄不再是「最新 N 条」的页面,只会成为同一个谎言更安静的版本:
footer 会说明取用依据,因为一个悄悄不再是「按修改时间排序的前 N 条」的页面,只会成为同一个谎言更安静的版本:
```
(Showing 100 of 10030 paths, sampled across 22 of the 22 top-level entries this pattern matched
instead of taken in modification-time order. Full sorted result stored at: …)
```
当页面无法触达全部顶层条目时——条目数多于位置数——footer 会给出已触达/总计的分布并指向 `list`。扁平结果(每个路径各自构成一个顶层条目)保留朴素的 `(Showing k of n paths. …)` footer此时轮转结果**就是**排序后的头部说明分布只会重复计数。spill 产物始终保存按修改时间排序的完整列表,排序视图不会丢失。
当页面无法触达全部顶层条目时——条目数多于位置数——footer 会给出已触达/总计的分布,并要求模型缩小 `path`。扁平结果(每个路径各自构成一个顶层条目)保留朴素的 `(Showing k of n paths. …)` footer此时轮转结果**就是**排序后的头部说明分布只会重复计数。spill 产物始终保存按修改时间排序的完整列表,排序视图不会丢失。
同一次改动里,指导与 schema 也不再误导「not shell find or ls」改为「not shell find」两处现在都写明不含 `/` 的 pattern 匹配任意深度的基名、结果只有文件从不包含目录、未超上限的结果按修改时间排序而更大的结果为取样所得、目录内容请用 `list`
同一次改动里,指导与 schema 也不再误导「not shell find or ls」改为「not shell find」两处现在都写明不含 `/` 的 pattern 匹配任意深度的基名、结果只有文件从不包含目录、未超上限的结果按修改时间排序而更大的结果跨顶层条目取样。它们不会推荐当前组合中可能不存在的兄弟包工具
### `list`,位于 `@deepseek-ai/dsh-tool-fs`
在既有的 `ctx.fs.listDir` 原语之上新增第四个面向模型的文件系统工具;该原语此前交付时唯一的消费方是 skill技能发现[seam Agent Note](../../archived/architecture/2026-07-03-filesystem-directory-listing-seam.md) 把面向模型的消费方推迟为一项单独的决策,也就是本文。
它接受可选的 `path`,默认为 `.`,即调用 agent 的会话工作区,因此那个最常见的问题不需要任何参数;返回单个目录的直接子项,形如 `{ path, entries: [{ name, type }] }`,其中 `type``file``directory``other`。它只调用 `listDir`seam 已经把不存在报告为 `FS_NOT_FOUND`、把非目录目标报告为 `FS_NOT_DIRECTORY`,前置 `stat` 只会多一次往返并制造第二个真源。
它接受可选的 `path` 和从 1 开始的 `offset` 参数,默认取调用 agent 的会话工作区和第 1 个条目,并返回一个有界页面,形如 `{ path, offset, entries: [{ name, type }], totalEntries, counts }`,其中 `type``file``directory``other`。它只调用 `listDir`seam 已经把不存在报告为 `FS_NOT_FOUND`、把非目录目标报告为 `FS_NOT_DIRECTORY`,前置 `stat` 只会多一次往返并制造第二个真源。
条展示规则承载了这个决策:
条展示规则承载了这个决策:
- **先目录、再文件、最后非常规子项,各组内按字母序** —— 规范值与渲染文本采用同一顺序,使 Code Mode 调用方和模型看到同一份顺序契约。seam 返回的是稳定名称序,会把子目录散落在字母表各处;对这样的列表设上限可能丢掉全部子目录,在 `list` 内部重演同一种盲区。目录优先的顺序让截断只丢叶子,绝不丢结构。
- **footer 始终说明完整列表的规模与构成** —— 例如 `(22 entries: 18 directories, 4 files)`;视图受 `listMaxEntries`(默认 200可配置截断时则为 `(Showing 200 of 5000 entries: 12 directories, 4988 files. …)`因此部分列出结果不可能被读成整个目录
目录条目渲染带尾部 `/`,非常规子项带 `@`,使模型无需第二次调用就能判断哪些可以继续进入。
- **先目录、再文件、最后非常规子项,各组内按字母序**,然后再分页,使每个 offset 都遍历同一稳定顺序,且第一页保留可导航的结构。
- **规范值和 Native 结果携带一个可继续取回的页面**,最多包含 `listMaxEntries` 个条目(默认 200可配置。footer 会说明完整规模与构成,并在最后一页之前给出 `offset=<next>`因此被省略的同级名称仍可取回
- **文件系统文本无法伪造展示结构。** 路径和条目名渲染为 JSON 字符串,并转义对包络有意义的字符;目录 `/` 与非常规子项 `@` 标记位于带引号名称之外,因此以 `@` 结尾的常规文件名仍可区分。
`list` 不发出 `fs/observed`。看到文件名不等于读过文件,列出绝不能满足 `@deepseek-ai/dsh-fs-policy` 施加的编辑前读取门禁。它声明 `isConcurrencySafe`,因为它完全不做任何变更。
@@ -73,7 +72,7 @@ instead of taken in modification-time order. Full sorted result stored at: …)
## Alternatives considered
**不动 `glob` 排序,只在 footer 里警告。** 这正是第一版实现的做法:保留新近序头部,加一句「该头部只覆盖 22 个顶层条目中的 1 个」。实测之后否决。一句警告是在要求模型不信任它手上唯一的数据并另寻他处;而一个更好的页面本身就不是错的。对于读到路径就停下的模型,警告也毫无作用——而那正是本次要修的失败。
**不动 `glob` 排序,只在 footer 里警告。** 这正是第一版实现的做法:保留从旧到新的头部,加一句「该头部只覆盖 22 个顶层条目中的 1 个」。实测之后否决。一句警告是在要求模型不信任它手上唯一的数据并另寻他处;而一个更好的页面本身就不是错的。对于读到路径就停下的模型,警告也毫无作用——而那正是本次要修的失败。
**一律取样,彻底取消按修改时间排序。** 已否决。在完整结果上,该顺序回答的是与新旧有关的问题——哪些已经陈旧、哪些最后被动过——这是一个确实有用且独立的用途;而未超上限的完整结果恰恰是该顺序毫无代价、意义最大的场景。只在超过上限后取样,正好落在该顺序已经不再描述结果的那个点上:一份 10030 条列表的头部不是「最值得知道的最旧文件」,而是其中任意的 1%。
@@ -91,11 +90,11 @@ instead of taken in modification-time order. Full sorted result stored at: …)
**让 `list` 支持递归和深度参数。** 暂时否决。单层是可组合的:模型列出它需要进入的那一层即可。递归会重新引入本 Agent Note 要解决的规模与截断问题,而且提供方原语本身就有意只做一层。
**像 `glob` 那样,把达到上限的列出结果通过 `ctx.spillStore` 落盘。** v1 已否决。`glob` 需要 spill是因为被截断的路径列表没有廉价的后继调用;被截断的列出结果有,而且 footer 已说明完整规模与构成,模型既知道自己只看到目录的一部分,也知道该怎么办
**像 `glob` 那样,把达到上限的列出结果通过 `ctx.spillStore` 落盘。** v1 已否决。`glob` 需要 spill是因为其 schema 没有 offset`list` 可以通过 footer 中精确的下一 offset 廉价地继续调用,而且每一页都会重复完整规模与构成
## Consequences
超过上限的 `glob` 结果不再返回修改时间最新的那些路径。这是热路径上一项真实的契约变更也正因如此footer、提示词段和 schema 都写明了取样这一依据,而不是留给模型去推断。未超上限的结果与此前逐字节相同,因此按新旧顺序阅读结果的用法在原本能用的地方继续能用。
超过上限的 `glob` 结果不再返回修改时间从旧到新排序时位于头部的那些路径。这是热路径上一项真实的契约变更也正因如此footer、提示词段和 schema 都写明了取样这一依据,而不是留给模型去推断。未超上限的结果与此前逐字节相同,因此按新旧顺序阅读结果的用法在原本能用的地方继续能用。
均衡只按路径首段进行,因此集中在更深层的结果——一棵总体均匀的树里某个特别庞大的目录——在顶层以下仍然分布不均。已记入该包的已知限制。
@@ -105,6 +104,6 @@ instead of taken in modification-time order. Full sorted result stored at: …)
## Testing
包测试钉住两个接口面向模型的文本。`glob` 方面:`sampleAcrossTopLevel` 在集中结果、某分组用尽后交出份额、页面小于顶层条目数、工作目录之外的绝对路径,以及必须复现排序头部的扁平结果上的行为;另有端到端断言——未超上限的结果原样不动、超上限结果返回取样页面并带取样依据 footer、页面覆盖全部条目后 list 提示消失、扁平的超上限结果保留朴素 footer。`list` 方面:包络、类型标记、单复数 footer、空目录 footer以及让唯一那个目录留在视野内的截断 footer。一项注册表层级的测试断言列出不会发出 `fs/observed`,且随后的 `edit` 仍以 `FS_NOT_OBSERVED` 失败使该工具不会变成意外的编辑前读取绕过口。提示词段注册、schema 注册与 HMR热模块替换dispose资源释放覆盖了这第四个工具与既有三个工具。
包测试钉住两个接口面向模型的文本。`glob` 方面:对集中结果取样、显式相对根、顶层分组数量超过 JavaScript 参数上限、某分组用尽后交出份额、页面小于顶层条目数、工作目录之外的绝对路径,以及必须复现排序头部的扁平结果`list` 方面顺序、完整构成、offset 续页与拒绝、空目录,以及包含换行、标签文本或标记后缀的文件系统名称。一项注册表层级的测试断言列出不会发出 `fs/observed`,且随后的 `edit` 仍以 `FS_NOT_OBSERVED` 失败使该工具不会变成意外的编辑前读取绕过口。提示词段注册、schema 注册与 HMR热模块替换dispose资源释放覆盖了这第四个工具与既有三个工具。
组装后的 transcript文本记录`fs-list` ACP 场景承担:该工作区的答案就是它的子目录,模型不带任何参数调用 `list`,被钉住的工具结果携带目录优先的包络及其构成 footer —— 这个答案 `glob` 根本无法给出。