diff --git a/.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.i18n.yaml b/.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.i18n.yaml index 2c4f111759..ec7312d865 100644 --- a/.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.i18n.yaml @@ -9,17 +9,17 @@ en: de9b76d89795ccec zh: 588d45b7d9ca9582 /agent-note-apply-patch-file-editing/decision: - en: 2319310b963e152f - zh: 8bfaa674dc4eae13 + en: f3ea29e495abb268 + zh: d1f71d692801ffd4 /agent-note-apply-patch-file-editing/alternatives-considered: - en: 8807fd9b985dad25 - zh: 2f0c415e8d8e2f7f + en: 448fc0db36ad219e + zh: e7effa6079b24b13 /agent-note-apply-patch-file-editing/consequences: - en: 6b1b596aa0cebdb6 - zh: 5e6ab9bb40e81b78 + en: 9c7453a5cc53e75e + zh: e51ee8e776592b01 /agent-note-apply-patch-file-editing/testing: en: 82ae0a96fbae53bc zh: 9a8194bdb134ae8c /agent-note-apply-patch-file-editing/deferred: - en: de13171524d3e173 - zh: 09cf8aa7cdb14606 + en: b783059f9706a718 + zh: 84d9dc6ea5f01bca diff --git a/.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.md b/.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.md index 1975946c97..d81e1922dc 100644 --- a/.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.md +++ b/.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.md @@ -10,17 +10,17 @@ A model editing several files or separated regions needs multiple literal-edit c ## Decision -`apply_patch` accepts one Codex-style patch for add, update, delete, and move operations, including multiple files and hunks. The pure [dsh-fs-patch library](../../../../packages/fs/fs-patch/README.md) follows Codex `rust-v0.158.0` parsing, sequential matching, whitespace tolerance, and `NormalizeToLf` reconstruction. The matcher receives LF text only; Codex `PreserveLineEndings` is not implemented. The pinned reference and compatibility fixtures distinguish syntax and matching behavior from Harness observation and sandbox policy. +`apply_patch` accepts one Codex-style patch for add, update, delete, and move operations, including multiple files and hunks. The pure [dsh-fs-patch library](../../../../packages/fs/fs-patch/README.md) follows Codex `rust-v0.158.0` parsing, sequential matching, whitespace tolerance, and `NormalizeToLf` reconstruction; the matcher receives LF text only, so Codex `PreserveLineEndings` is not implemented. -The [filesystem service](../../../../packages/fs/fs/README.md) exposes `patchText`, `deleteFile`, and `moveFile`; local, sandboxed, and SSH providers implement them. `fs/delete-intent` requires the same present observation as `fs/edit-intent`, while move destinations use `fs/write-intent`. Internal reads do not grant model observations. Version guards and sandbox enforcement remain with the provider. There is no transaction across paths: a move publishes its destination before deleting its source, and `sourceDeleted` distinguishes a completed move from a published destination whose source could not be removed. +The [filesystem service](../../../../packages/fs/fs/README.md) exposes `patchText`, `deleteFile`, and `moveFile`; local, sandboxed, and SSH providers implement them. `fs/delete-intent` requires the same present observation as `fs/edit-intent`, while move destinations use `fs/write-intent`. Version guards and sandbox enforcement remain with the provider. A move publishes its destination before deleting its source, and `sourceDeleted` distinguishes a completed move from a published destination whose source could not be removed. -The [tool executor](../../../../packages/fs/tool-fs/README.md) resolves paths and simulates expected state during whole-patch preflight, including repeated paths and moves, without writing files. A rejected preflight commits nothing. Commit reobtains observation guards for each operation and stops at the first failure or cancellation without rollback. After an I/O or cancellation failure with uncertain effects, an independent `stat` comparison checks whether every affected target retains its prior version; changed state or a failed comparison produces `unknown` and requires rereading before retry. Reports retain completed, failed, unknown, and skipped operations, including a partially published move. +The [tool executor](../../../../packages/fs/tool-fs/README.md) resolves paths and simulates expected state during whole-patch preflight, including repeated paths and moves, without writing files; a refused preflight commits nothing. Commit reobtains observation guards for each operation and stops at the first failure or cancellation without rollback. After an I/O or cancellation failure, a `stat` comparison checks whether every affected target retains its prior version; changed state or a failed comparison produces `unknown`, which requires rereading before retry. Reports retain completed, failed, unknown, and skipped operations, including a partially published move. -Commit reports return `isError: false` so completed diff metadata survives a stopped patch; their operation statuses and stopped flag still expose failure. Syntax and preflight rejection produce tool errors. Successful move summaries use `M `. Applied diff paths use canonical absolute `processPath` for client navigation, while operation records retain display paths and unresolved failures retain original patch paths. +Commit reports return `isError: false` so completed diff metadata survives a stopped patch; their operation statuses and stopped flag expose the failure. Syntax and preflight rejection produce tool errors. Successful move summaries use `M `. Applied diff paths use canonical absolute `processPath` for client navigation, while operation records retain display paths and unresolved paths keep their patch spelling. -[Review capture](../../../../packages/deliverables/workspace-changes/README.md) uses filesystem intent listeners, not argument parsing. `tools/pre-execute` associates the execution with its turn recorder; prepended write, edit, and delete listeners first await the policy decision, then await capture before provider mutation. Moves capture both endpoints. Each turn retains its first admitted baseline and compares actual final bytes, so repeated edits yield one net diff and reverted changes disappear. Denied intents create no capture or observation. Existing size, count, time, location, and Session-lifetime limits remain; a commit failure does not discard earlier real changes. +[Review capture](../../../../packages/deliverables/workspace-changes/README.md) uses filesystem intent listeners, not argument parsing. `tools/pre-execute` associates the execution with its turn recorder; prepended write, edit, and delete listeners first await the policy decision, then await capture before provider mutation. Moves capture both endpoints. Each turn retains its first baseline and compares actual final bytes, so repeated edits yield one net diff and reverted changes disappear. Existing size, count, time, location, and Session-lifetime limits remain. -`tool-fs.enableApplyPatch` defaults to `true` under the [default-activation decision](2026-10-16-default-apply-patch.md). The tool registers its schema, guidance, native/PTC execution, and tool presentation while retaining existing file tools; explicit `false` omits it. Registration uses plugin effects, and the agent loop is unchanged. +`tool-fs.applyPatch` defaults to `true`, so ordinary coding sessions get the tool alongside read, write, and edit; explicit `false` omits the tool and its guidance, and the minimal SDK and Web presets, which exclude text file tools, set it to `false`. The tool registers its schema, guidance, native/PTC execution, and tool presentation through plugin effects; the agent loop is unchanged. ## Alternatives considered @@ -30,11 +30,13 @@ Commit reports return `isError: false` so completed diff metadata survives a sto **Make the whole patch transactional.** Cross-file rollback adds recovery state and can overwrite concurrent external edits. Preflight and ordered commits reject known failures early and expose partial results without promising rollback. -**Expand Review while adding the tool.** Rename presentation, persistent storage, SSH capture, and capture-failure policy are independent product decisions. Existing whole-file capture and rendering cover this integration. +**Expand Review while adding the tool.** Rename presentation, persistent storage, SSH capture, and capture-failure policy are independent product decisions; existing whole-file capture and rendering cover this integration. + +**Gate default availability on a controlled evaluation.** This would keep an activation overlay between ordinary sessions and the tool; the explicit off switch already supports comparison runs and deployments that do not want it. ## Consequences -One call can express related edits, but lower tool-call counts do not establish better task outcomes or lower total token cost. Preflight cannot prevent a later conflict, cancellation, or storage failure; callers must inspect operation statuses and actual files before retrying. Permissive upstream matching remains pinned and tested rather than silently tightened. Provider implementations and external subclasses must support three additional operations, and partial moves require handling both endpoints. +One call can express related edits. Default model requests include another tool schema and its guidance, so recorded request expectations change. Preflight cannot prevent a later conflict, cancellation, or storage failure; callers inspect operation statuses before retrying. Permissive upstream matching remains pinned and tested rather than tightened. Provider implementations and external subclasses must support three additional operations, and partial moves require handling both endpoints. ## Testing @@ -42,4 +44,4 @@ Parser and matcher fixtures cover the pinned syntax, whitespace, repeated contex ## Deferred -The controlled editing-task evaluation in step eight remains pending: compare final-file correctness, task success, prompt and completion tokens, model steps, tool calls, retries, latency, and Review coverage on identical initial states. SSH Review, closing-prose file references for `apply_patch`, and Codex `PreserveLineEndings` remain outside this implementation. +A controlled editing-task evaluation comparing final-file correctness, task success, tokens, tool calls, retries, latency, and Review coverage against `edit` on identical initial states. SSH Review, closing-prose file references for `apply_patch`, and Codex `PreserveLineEndings`. diff --git a/.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.zh.md b/.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.zh.md index dadef29d02..b6e8629d2f 100644 --- a/.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.zh.md +++ b/.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.zh.md @@ -10,17 +10,17 @@ Status: implemented ## 决策 -`apply_patch` 接收一份 Codex 风格补丁,支持新增、更新、删除和移动,以及多个文件和 hunk。[dsh-fs-patch 纯库](../../../../packages/fs/fs-patch/README.zh.md)遵循 Codex `rust-v0.158.0` 的解析、顺序匹配、空白容忍和 `NormalizeToLf` 重建。匹配器只接收 LF 文本;尚未实现 Codex `PreserveLineEndings`。固定版本参考与兼容性 fixture(测试前置数据)将语法和匹配行为同 Harness 观察及沙箱策略区分开来。 +`apply_patch` 接收一份 Codex 风格补丁,支持新增、更新、删除和移动,以及多个文件和 hunk。[dsh-fs-patch 纯库](../../../../packages/fs/fs-patch/README.zh.md)遵循 Codex `rust-v0.158.0` 的解析、顺序匹配、空白容忍和 `NormalizeToLf` 重建。匹配器只接收 LF 文本,因此不实现 Codex `PreserveLineEndings`。 -[文件系统服务](../../../../packages/fs/fs/README.zh.md)提供 `patchText`、`deleteFile` 和 `moveFile`;本地、沙箱和 SSH 提供方均实现它们。`fs/delete-intent` 与 `fs/edit-intent` 要求相同的存在观察,移动目标使用 `fs/write-intent`。内部读取不授予模型观察。版本防护和沙箱约束仍由提供方负责。不存在跨路径事务:移动先发布目标,再删除源;`sourceDeleted` 区分已完成的移动与目标已发布但源未能删除的情况。 +[文件系统服务](../../../../packages/fs/fs/README.zh.md)提供 `patchText`、`deleteFile` 和 `moveFile`;本地、沙箱和 SSH 提供方均实现它们。`fs/delete-intent` 与 `fs/edit-intent` 要求相同的存在观察,移动目标使用 `fs/write-intent`。版本防护和沙箱约束仍由提供方负责。移动先发布目标,再删除源;`sourceDeleted` 区分已完成的移动与目标已发布但源未能删除的情况。 -[工具执行器](../../../../packages/fs/tool-fs/README.zh.md)在整份补丁预检时解析路径并模拟预期状态,包括重复路径和移动,不写入文件。预检拒绝时没有提交。提交时为每条操作重新获取观察防护,在首次失败或取消时停止,不回滚。I/O 或取消失败导致效果不确定时,独立的 `stat` 比较检查所有受影响目标是否仍保持先前版本;状态变化或比较失败会产生 `unknown`,要求重试前重新读取。报告保留已完成、失败、未知和跳过的操作,包括部分发布的移动。 +[工具执行器](../../../../packages/fs/tool-fs/README.zh.md)在整份补丁预检时解析路径并模拟预期状态,包括重复路径和移动,不写入文件;预检拒绝时没有提交。提交时为每条操作重新获取观察防护,在首次失败或取消时停止,不回滚。I/O 或取消失败后,`stat` 比较检查所有受影响目标是否仍保持先前版本;状态变化或比较失败会产生 `unknown`,要求重试前重新读取。报告保留已完成、失败、未知和跳过的操作,包括部分发布的移动。 -提交报告返回 `isError: false`,让已完成的 diff 元数据在补丁中止后仍被保留;操作状态和 stopped 标记继续暴露失败。语法和预检拒绝产生工具错误。成功移动的摘要使用 `M `。已应用 diff 的路径采用规范绝对 `processPath`,供客户端导航使用;操作记录保留显示路径,解析失败则保留补丁原始路径。 +提交报告返回 `isError: false`,让已完成的 diff 元数据在补丁中止后仍被保留;操作状态和 stopped 标记暴露失败。语法和预检拒绝产生工具错误。成功移动的摘要使用 `M `。已应用 diff 的路径采用规范绝对 `processPath`,供客户端导航使用;操作记录保留显示路径,未解析的路径保留补丁原始写法。 -[Review 采集](../../../../packages/deliverables/workspace-changes/README.zh.md)使用文件系统 intent 监听器,不解析参数。`tools/pre-execute` 将执行关联到轮次记录器;前置的 write、edit 和 delete 监听器先等待策略决策,再等待捕获完成,然后提供方才修改文件。移动捕获两端。每轮保留首次准入时的基线,比较实际最终字节,因此重复编辑形成一份净 diff,还原的变化不再显示。被拒绝的 intent 不产生副本或观察。现有大小、数量、时间、位置和 Session 生命周期限制保持适用;提交失败不会丢弃先前真实变化。 +[Review 采集](../../../../packages/deliverables/workspace-changes/README.zh.md)使用文件系统 intent 监听器,不解析参数。`tools/pre-execute` 将执行关联到轮次记录器;前置的 write、edit 和 delete 监听器先等待策略决策,再等待捕获完成,然后提供方才修改文件。移动捕获两端。每轮保留首次基线,比较实际最终字节,因此重复编辑形成一份净 diff,还原的变化不再显示。现有大小、数量、时间、位置和 Session 生命周期限制保持适用。 -根据[默认启用决定](2026-10-16-default-apply-patch.zh.md),`tool-fs.enableApplyPatch` 默认为 `true`。工具注册 schema、指导文本、native/PTC 执行与调用展示,同时保留已有文件工具;显式设为 `false` 可省略它。注册使用插件 effect,不修改 agent loop(智能体循环)。 +`tool-fs.applyPatch` 默认为 `true`,普通编程会话与 read、write、edit 一同获得该工具;显式设为 `false` 可省略工具及其指导文本,有意排除文本文件工具的最小 SDK 和 Web 预设将其设为 `false`。工具通过插件 effect 注册 schema、指导文本、native/PTC 执行与调用展示;不修改 agent loop(智能体循环)。 ## 考虑过的替代方案 @@ -30,11 +30,13 @@ Status: implemented **将整份补丁做成事务。** 跨文件回滚增加恢复状态,还可能覆盖并发外部修改。预检和顺序提交可以提早拒绝已知失败,并公开部分结果,而不承诺回滚。 -**加入工具时同时扩展 Review。** 重命名展示、持久存储、SSH 采集和捕获失败策略是独立产品决策。既有整文件采集和展示覆盖本次接入。 +**加入工具时同时扩展 Review。** 重命名展示、持久存储、SSH 采集和捕获失败策略是独立产品决策;既有整文件采集和展示覆盖本次接入。 + +**以受控评测作为默认启用的前提。** 这会在普通会话与工具之间保留一层启用 overlay;显式关闭开关已经支持对比评测和不需要该工具的部署。 ## 后果 -一次调用可以表达相关编辑,但工具调用次数减少并不证明任务效果更好或总 token 成本更低。预检不能阻止之后的冲突、取消或存储失败;调用方必须在重试前检查操作状态和实际文件。上游宽松匹配保持固定版本并受到测试约束,不会被悄悄收紧。提供方实现和外部子类必须支持三个新增操作,部分移动要求处理两端状态。 +一次调用可以表达相关编辑。默认模型请求包含额外的工具 schema 及其指导文本,因此录制请求的预期会变化。预检不能阻止之后的冲突、取消或存储失败;调用方在重试前检查操作状态。上游宽松匹配保持固定版本并受到测试约束,不会被收紧。提供方实现和外部子类必须支持三个新增操作,部分移动要求处理两端状态。 ## 测试 @@ -42,4 +44,4 @@ Status: implemented ## 延期工作 -第八步受控编辑任务评测仍待开展:在相同初始状态下比较最终文件正确性、任务成功率、提示词及完成 token、模型步数、工具调用、重试、延迟和 Review 覆盖。SSH Review、`apply_patch` 的收尾文案文件引用,以及 Codex `PreserveLineEndings` 不在本次实现内。 +受控编辑任务评测:在相同初始状态下与 `edit` 比较最终文件正确性、任务成功率、token、工具调用、重试、延迟和 Review 覆盖。SSH Review、`apply_patch` 的收尾文案文件引用,以及 Codex `PreserveLineEndings`。 diff --git a/.agents/notes/implemented/feature/2026-10-16-default-apply-patch.i18n.yaml b/.agents/notes/implemented/feature/2026-10-16-default-apply-patch.i18n.yaml deleted file mode 100644 index 1bda5a5dc4..0000000000 --- a/.agents/notes/implemented/feature/2026-10-16-default-apply-patch.i18n.yaml +++ /dev/null @@ -1,19 +0,0 @@ -# Bilingual-pair consistency record for 2026-10-16-default-apply-patch.md (docs/i18n/README.md): per heading -# section, a hash of its English and Chinese blocks outside code blocks and generated regions. -# After editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-10-16-default-apply-patch.md -/agent-note-default-apply-patch-availability: - en: b2ced9277c80779e - zh: 9322d51aceeb60af -/agent-note-default-apply-patch-availability/problem: - en: 306cd5111592054b - zh: 6eb160fe2c203ef4 -/agent-note-default-apply-patch-availability/decision: - en: 16c45444fd57f3d9 - zh: c4dd315e107339a5 -/agent-note-default-apply-patch-availability/alternatives-considered: - en: 9f0d54b891b53a91 - zh: bbd996b37b894736 -/agent-note-default-apply-patch-availability/consequences: - en: 70451da3d003b88a - zh: e8e6697e4b66ced5 diff --git a/.agents/notes/implemented/feature/2026-10-16-default-apply-patch.md b/.agents/notes/implemented/feature/2026-10-16-default-apply-patch.md deleted file mode 100644 index b84320590d..0000000000 --- a/.agents/notes/implemented/feature/2026-10-16-default-apply-patch.md +++ /dev/null @@ -1,23 +0,0 @@ -# Agent Note: Default apply_patch availability - -Status: implemented - -English | [中文](2026-10-16-default-apply-patch.zh.md) - -## Problem - -A separate activation overlay prevents ordinary coding sessions from using the multi-file patch capability in the default filesystem tool set. - -## Decision - -`tool-fs.enableApplyPatch` defaults to `true`, alongside read, write, and edit. Explicit `false` removes the tool and its guidance. Minimal SDK and Web presets, which intentionally exclude text file tools, set it to `false`. - -This supersedes the evaluation-gated activation decision in [apply_patch file editing](2026-09-28-apply-patch-file-editing.md). Availability does not assert an improvement in task success or token cost; controlled evaluation remains separate. Observation, version, and sandbox requirements are unchanged. - -## Alternatives considered - -**Keep activation gated on evaluation.** This retains the extra configuration requirement that default availability removes. The explicit off switch supports comparison runs and deployments that do not want the tool. - -## Consequences - -Default model requests include another tool schema and its guidance, so recorded request expectations change. Existing write and edit tools remain available. Minimal presets retain their small tool sets, and deployments can opt out without removing filesystem access. diff --git a/.agents/notes/implemented/feature/2026-10-16-default-apply-patch.zh.md b/.agents/notes/implemented/feature/2026-10-16-default-apply-patch.zh.md deleted file mode 100644 index 457efd2759..0000000000 --- a/.agents/notes/implemented/feature/2026-10-16-default-apply-patch.zh.md +++ /dev/null @@ -1,23 +0,0 @@ -# Agent Note: 默认提供 apply_patch - -Status: implemented - -[English](2026-10-16-default-apply-patch.md) | 中文 - -## Problem - -单独的启用 overlay 使普通编程会话无法从默认文件系统工具集中使用多文件补丁能力。 - -## Decision - -`tool-fs.enableApplyPatch` 默认为 `true`,与 read、write 和 edit 一同提供。显式设为 `false` 可移除工具及其指导文本。有意排除文本文件工具的最小 SDK 和 Web 预设将其设为 `false`。 - -本决定取代 [apply_patch 文件编辑](2026-09-28-apply-patch-file-editing.zh.md)中以评测为默认启用前提的决定。提供工具不意味着任务成功率或 token 成本已经改善;受控评测仍独立开展。观察、版本和沙箱要求保持不变。 - -## Alternatives considered - -**继续以评测作为启用前提。** 这会保留默认提供工具所要省去的额外配置。显式关闭开关支持对比评测和不需要该工具的部署。 - -## Consequences - -默认模型请求包含额外的工具 schema 及其指导文本,因此录制请求的预期会变化。已有 write 和 edit 工具继续可用。最小预设保留精简工具集,部署可单独关闭补丁工具而不移除文件系统访问。 diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 25f23fc8ba..29bc4b47f0 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -3763,8 +3763,6 @@ export interface Config { ```ts config-catalog /** Plugin config (all optional — `Config` supplies the defaults). */ export interface Config { - /** Register `apply_patch` and its guidance. Defaults to true. */ - enableApplyPatch?: boolean /** Register `read` and its guidance. Defaults to true. */ read?: boolean /** Register `read_image` while an attachment store is mounted. Defaults to true. */ @@ -3773,6 +3771,8 @@ export interface Config { write?: boolean /** Register `edit` and its guidance. Defaults to true. */ edit?: boolean + /** Register `apply_patch` and its guidance. Defaults to true. */ + applyPatch?: boolean /** Default and maximum number of lines returned by one `read` call. */ readLimit?: number /** Maximum characters returned for a single line before truncation. */ diff --git a/docs/config-catalog.zh.md b/docs/config-catalog.zh.md index 4e9823ba00..7bccdb9816 100644 --- a/docs/config-catalog.zh.md +++ b/docs/config-catalog.zh.md @@ -3765,8 +3765,6 @@ export interface Config { ```ts config-catalog /** Plugin config (all optional — `Config` supplies the defaults). */ export interface Config { - /** Register `apply_patch` and its guidance. Defaults to true. */ - enableApplyPatch?: boolean /** Register `read` and its guidance. Defaults to true. */ read?: boolean /** Register `read_image` while an attachment store is mounted. Defaults to true. */ @@ -3775,6 +3773,8 @@ export interface Config { write?: boolean /** Register `edit` and its guidance. Defaults to true. */ edit?: boolean + /** Register `apply_patch` and its guidance. Defaults to true. */ + applyPatch?: boolean /** Default and maximum number of lines returned by one `read` call. */ readLimit?: number /** Maximum characters returned for a single line before truncation. */ diff --git a/docs/subsystems/filesystem.i18n.yaml b/docs/subsystems/filesystem.i18n.yaml index 5fe6750eea..ce85a37157 100644 --- a/docs/subsystems/filesystem.i18n.yaml +++ b/docs/subsystems/filesystem.i18n.yaml @@ -9,11 +9,11 @@ en: 1e55f1c74f0ade3f zh: faecf389a2fb0a93 /filesystem/patch-syntax-records: - en: 4c087c802cd21042 - zh: 3425805843420226 + en: 3ab3a02918d41178 + zh: 78ff80be8599ba43 /filesystem/write-and-edit-guards-provider-contract: - en: ec52d132bf1b6e0e - zh: 3e76a67c30131ff1 + en: 2d5f148d79802cf9 + zh: 70fb89582a138175 /filesystem/the-fs-policy-events-provider-contract-vocabulary: en: b25951ca185a890e zh: 42a1344a2cc860a9 @@ -27,8 +27,8 @@ en: cfa56ed5d1eb2ff9 zh: 04be764f3dc8a48c /filesystem/patch-execution-consumer: - en: 1453e772c27288a1 - zh: 0612ca7c2c0409ee + en: cd796ff507f51816 + zh: 294ca04bf6553f72 /filesystem/error-taxonomy-provider-contract: en: ca206569d4e2528d zh: e36d70ad39a73570 diff --git a/docs/subsystems/filesystem.md b/docs/subsystems/filesystem.md index a81798b1f3..e7f9b2e79b 100644 --- a/docs/subsystems/filesystem.md +++ b/docs/subsystems/filesystem.md @@ -114,10 +114,10 @@ interface FsDirEntry { ## Patch syntax records -[dsh-fs-patch](../../packages/fs/fs-patch/README.md) parses complete patch text into serializable operations and applies hunks to caller-supplied LF-normalized text. Paths remain unresolved strings; syntax acceptance neither grants permission nor checks file versions. `collectMutationPaths` returns unique source and destination spellings for callers that capture files before mutation. `applyHunks` returns complete new text or a structured match failure. The package README owns the pinned parsing and matching rules and exclusions. +[dsh-fs-patch](../../packages/fs/fs-patch/README.md) parses complete patch text into serializable operations whose paths are unresolved strings, and `applyHunks` applies hunks to caller-supplied LF-normalized text, returning complete new text or a structured match failure. The package README owns the pinned parsing and matching rules. ```ts type-equiv -/** One contiguous update, with zero-based indices identifying explicit context lines. */ +/** One contiguous update. */ interface PatchHunk { /** One-based input line containing the marker or first implicit hunk line. */ lineNumber: number @@ -127,8 +127,6 @@ interface PatchHunk { oldLines: string[] /** Context and added lines, without patch prefixes or line endings. */ newLines: string[] - /** Corresponding old/new offsets for context; identical remove/add pairs are excluded. */ - contextLineIndices: [number, number][] /** Whether `*** End of File` follows this hunk. */ isEndOfFile: boolean } @@ -192,9 +190,9 @@ The same optional guard rules cover patches, deletion, and moves. Providers perf | `moveFile` source | `fs/edit-intent` | Source absent only when `sourceDeleted` is true. | | `moveFile` destination | `fs/write-intent` | Destination present at `destination.version`. | -Patch, delete, and move sources must be regular files; missing or stale sources report `FS_STALE_VERSION`. Patch matching follows LF normalization inside the mutation lock. A mismatch reports `FS_PATCH_MISMATCH` with the patch hunk line and stage. Moves reject identical target keys with `FS_SAME_TARGET`, use write guards for their destination, and preserve source line endings and mode. Deletion and overwritten-destination bases are bounded nullable LF text. Moves are not cross-path transactions: the destination may be published while source removal fails. Cancellation is checked before local destination staging; after that, the provider completes both effects and reports their actual outcome. Remote cancellation or connection loss can make that outcome unavailable to the caller. +Patch, delete, and move sources must be regular files; missing or stale sources report `FS_STALE_VERSION`. Patch matching runs on LF-normalized text inside the mutation lock, and a mismatch reports `FS_PATCH_MISMATCH` with the patch hunk line and stage. Moves reject identical target keys with `FS_SAME_TARGET`, use write guards for their destination, and preserve source line endings and mode. Deletion and overwritten-destination bases are bounded nullable LF text. A move is two effects: the destination may be published while source removal fails. Cancellation is checked before local destination staging; after that, the provider completes both effects and reports their outcome. -Targets already follow symlinks. A caller that rejects symlink sources must inspect the path with `lstat` before `resolve`. Internal preflight reads are not model observations and cannot establish a guard. +Targets already follow symlinks, so a caller that rejects symlink sources inspects the path with `lstat` before `resolve`. ```ts type-equiv /** Outcome of a guarded delete; the bounded basis follows FsWriteOutcome.before. */ @@ -351,7 +349,7 @@ Observed state is a `WeakMap>` held inside ## Patch execution (consumer) -The internal [tool-fs patch executor](../../packages/fs/tool-fs/README.md#understand-the-implementation) dispatches pure intent decisions in preflight, simulates preceding successful operations, and reacquires guards for each commit. Internal reads never establish model observation. Provider results determine which effects are recorded; executor details and failure guarantees belong to the package README. +The internal [tool-fs patch executor](../../packages/fs/tool-fs/README.md#understand-the-implementation) dispatches intent decisions in preflight without emitting observations, simulates preceding operations, and reacquires guards for each commit. Provider results determine which effects are recorded; executor details and failure guarantees belong to the package README. | Operation | Preflight intent on first target use | Commit intent | Successful observation | |---|---|---|---| @@ -360,7 +358,7 @@ The internal [tool-fs patch executor](../../packages/fs/tool-fs/README.md#unders | delete | `fs/delete-intent` | `fs/delete-intent` | target absent | | move | source `fs/edit-intent`; destination `fs/write-intent` | same two intents, freshly dispatched | destination present(version); source absent only when removed | -The opt-in `apply_patch` tool returns strict commit values with per-operation diagnostics and full-text change inputs for rendering; persisted metadata holds diff hunks and a stopped flag. Syntax and preflight refusals set `isError: true`; commit reports retain `isError: false` even when stopped so known partial diffs reach Session presentation. The [tool README](../../packages/fs/tool-fs/README.md#use-this-package) owns formatting and recovery. +The `apply_patch` tool returns strict commit values with per-operation diagnostics and full-text change inputs for rendering; persisted metadata holds diff hunks and a stopped flag. Syntax and preflight refusals set `isError: true`; commit reports retain `isError: false` even when stopped so known partial diffs reach Session presentation. The [tool README](../../packages/fs/tool-fs/README.md#use-this-package) owns formatting and recovery. ## Error taxonomy (provider contract) @@ -570,49 +568,49 @@ abstract writeText( target: FsTarget, content: string, expected?: FsWriteIntent, abstract editText( target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise /** - * Apply hunks under the version guard and mutation lock. Missing targets and - * stale versions report FS_STALE_VERSION; unmatched hunks report FS_PATCH_MISMATCH. - * Callers obtain expected through fs/edit-intent and record present(version) - * through fs/observed on success. Internal preflight reads are not model observations. - * @param target - resolved regular UTF-8 file to update. - * @param hunks - ordered parsed hunks applied to LF-normalized content. - * @param expected - observed version; omit to patch the current content unconditionally. - * @param signal - aborts before atomic publication. - * @param sandboxPolicy - per-call confinement; omit to use the provider default. - * @returns the published version and LF-normalized before/after text. + * Atomically apply parsed hunks to the LF-normalized text and restore the + * stored line-ending style. A missing or stale target reports + * `FS_STALE_VERSION`; an unmatched hunk reports `FS_PATCH_MISMATCH`. + * @param target - the resolved regular file to patch. + * @param hunks - ordered hunks from `parsePatch`. + * @param expected - the version guard; omit for an unconditional patch. + * @param signal - aborts before atomic publication takes effect. + * @param sandboxPolicy - the per-call mode and workspace root this patch runs + * under; a sandboxing backend fences the patch by it, the bare backend + * ignores it. Omit to leave the backend its own default. + * @returns the outcome, including the version the patch produced. */ abstract patchText( target: FsTarget, hunks: readonly PatchHunk[], expected?: { version: FsVersion }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise /** - * Remove a resolved regular file. Missing/stale targets report FS_STALE_VERSION; - * other types report FS_NOT_REGULAR_FILE. Obtain expected through fs/delete-intent - * and record absent through fs/observed after successful removal. - * Resolution follows symlinks: callers rejecting symlink sources must lstat before resolve. - * @param target - resolved file to remove, including the real target of a symlink. - * @param expected - observed version; omit for unconditional removal of the current file. - * @param signal - aborts before removal; successful removal is not rolled back. - * @param sandboxPolicy - per-call confinement; omit to use the provider default. - * @returns the bounded LF-normalized removed content, or null when no basis is available. + * Remove a regular file. A missing or stale target reports `FS_STALE_VERSION`; + * a directory or other non-file reports `FS_NOT_REGULAR_FILE`. Resolution has + * already followed symlinks, so the target is the real file. + * @param target - the resolved file to remove. + * @param expected - the version guard; omit for an unconditional removal. + * @param signal - aborts before removal. + * @param sandboxPolicy - the per-call mode and workspace root this removal + * runs under; a sandboxing backend fences it, the bare backend ignores it. + * Omit to leave the backend its own default. + * @returns the bounded LF-normalized removed text, or null when no basis is available. */ abstract deleteFile( target: FsTarget, expected?: { version: FsVersion }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise /** - * Publish source text at a destination, optionally patching it, then remove the source. - * Both paths remain locked in canonical order. FS_SAME_TARGET rejects identical keys; - * source guards match patchText, destination guards match writeText. No cross-path - * transaction is promised: destination publication with the source retained is a final outcome. - * Obtain source/destination guards through fs/edit-intent and fs/write-intent. On - * resolution, record destination present(destination.version); record source absent - * only when sourceDeleted is true. Providers do not emit these observations. - * Resolution follows symlinks: callers rejecting symlink sources must lstat before resolve. - * @param source - resolved regular UTF-8 source; its line-ending style and mode are preserved. - * @param destination - resolved destination in the same provider, distinct from source. - * @param options - optional hunks and source/destination guards; omitted fields mean - * no text patch and unconditional source/destination mutations respectively. - * @param signal - checked before destination staging; once staging starts, cancellation - * does not interrupt the local commit. Remote cancellation can leave the outcome unknown. - * @param sandboxPolicy - confinement for both paths; omit to use the provider default. - * @returns locked LF sourceBefore, destination outcome, and source-removal status; failures before publication reject. + * Publish the source text, optionally patched, at the destination, then remove + * the source. Both targets stay locked in canonical order. Identical targets + * report `FS_SAME_TARGET`; the source guard follows `patchText` and the + * destination guard follows `writeText`. The two effects are separate: a + * published destination with a retained source is a final outcome reported + * through `sourceDeleted: false`. + * @param source - the resolved regular source; its line-ending style and mode carry over. + * @param destination - the resolved destination within the same provider. + * @param options - optional hunks and the source/destination guards; an omitted guard is unconditional. + * @param signal - checked before destination staging; the commit then completes both effects. + * @param sandboxPolicy - the per-call mode and workspace root both paths run + * under; a sandboxing backend fences them, the bare backend ignores it. + * Omit to leave the backend its own default. + * @returns the locked LF source text, the destination outcome, and the source-removal status. */ abstract moveFile( source: FsTarget, destination: FsTarget, options: { hunks?: readonly PatchHunk[] expectedSource?: { version: FsVersion } expectedDestination?: FsWriteIntent }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise ``` diff --git a/docs/subsystems/filesystem.zh.md b/docs/subsystems/filesystem.zh.md index ec770b367d..d22ac834b9 100644 --- a/docs/subsystems/filesystem.zh.md +++ b/docs/subsystems/filesystem.zh.md @@ -114,10 +114,10 @@ interface FsDirEntry { ## 补丁语法记录 -[dsh-fs-patch](../../packages/fs/fs-patch/README.zh.md) 将完整补丁文本解析为可序列化的操作,并将 hunk 应用于调用方提供的 LF 规范化文本。路径仍是未解析的字符串;语法接受既不授予权限,也不检查文件版本。`collectMutationPaths` 返回去重的源路径和目标路径原字符串,供修改前采集文件的调用方使用。`applyHunks` 返回完整新文本或结构化匹配失败。固定的解析与匹配规则及排除项由包 README 维护。 +[dsh-fs-patch](../../packages/fs/fs-patch/README.zh.md) 将完整补丁文本解析为可序列化的操作,路径保持为未解析的字符串;`applyHunks` 将 hunk 应用于调用方提供的 LF 规范化文本,返回完整新文本或结构化匹配失败。固定的解析与匹配规则由包 README 维护。 ```ts type-equiv -/** One contiguous update, with zero-based indices identifying explicit context lines. */ +/** One contiguous update. */ interface PatchHunk { /** One-based input line containing the marker or first implicit hunk line. */ lineNumber: number @@ -127,8 +127,6 @@ interface PatchHunk { oldLines: string[] /** Context and added lines, without patch prefixes or line endings. */ newLines: string[] - /** Corresponding old/new offsets for context; identical remove/add pairs are excluded. */ - contextLineIndices: [number, number][] /** Whether `*** End of File` follows this hunk. */ isEndOfFile: boolean } @@ -192,9 +190,9 @@ type ApplyHunksResult = | `moveFile` 源 | `fs/edit-intent` | 仅在 `sourceDeleted` 为 true 时记录源缺失。 | | `moveFile` 目标 | `fs/write-intent` | 目标存在于 `destination.version`。 | -补丁、删除和移动的源必须是普通文件;源缺失或版本陈旧报告 `FS_STALE_VERSION`。补丁在变更锁内先规范化为 LF 再匹配。不匹配报告 `FS_PATCH_MISMATCH`,包含补丁 hunk 行号与阶段。移动以 `FS_SAME_TARGET` 拒绝相同目标 key,对目标使用写入防护,并保留源的行尾风格与模式。删除和被覆盖目标的基线是有界、可空的 LF 文本。移动不是跨路径事务:目标可能已发布而源删除失败。本地移动在暂存目标前检查取消;随后提供方完成两端效果并报告实际结果。远端取消或连接丢失可能使调用方无法取得该结果。 +补丁、删除和移动的源必须是普通文件;源缺失或版本陈旧报告 `FS_STALE_VERSION`。补丁在变更锁内对 LF 规范化文本匹配,不匹配报告 `FS_PATCH_MISMATCH`,包含补丁 hunk 行号与阶段。移动以 `FS_SAME_TARGET` 拒绝相同目标 key,对目标使用写入防护,并保留源的行尾风格与模式。删除和被覆盖目标的基线是有界、可空的 LF 文本。移动是两个效果:目标可能已发布而源删除失败。本地移动在暂存目标前检查取消;随后提供方完成两端效果并报告结果。 -目标已跟随符号链接。拒绝符号链接源的调用方必须在 `resolve` 前用 `lstat` 检查路径。内部预检读取不算模型观察,也不能建立防护。 +目标已跟随符号链接,因此拒绝符号链接源的调用方在 `resolve` 前用 `lstat` 检查路径。 ```ts type-equiv /** Outcome of a guarded delete; the bounded basis follows FsWriteOutcome.before. */ @@ -351,7 +349,7 @@ interface FileReadOutcome { ## 补丁执行(消费方) -内部 [tool-fs 补丁执行器](../../packages/fs/tool-fs/README.zh.md#understand-the-implementation) 在预检时分发纯意图决策、模拟前序成功操作,并在每次提交时重新取得防护。内部读取从不建立模型观察。提供方结果决定记录哪些实际效果;执行器细节与失败保证由包 README 维护。 +内部 [tool-fs 补丁执行器](../../packages/fs/tool-fs/README.zh.md#understand-the-implementation) 在预检时分发意图决策而不发出观察、模拟前序操作,并在每次提交时重新取得防护。提供方结果决定记录哪些效果;执行器细节与失败保证由包 README 维护。 | 操作 | 首次使用目标的预检意图 | 提交意图 | 成功观察 | |---|---|---|---| @@ -360,7 +358,7 @@ interface FileReadOutcome { | delete | `fs/delete-intent` | `fs/delete-intent` | 目标 absent | | move | 源 `fs/edit-intent`;目标 `fs/write-intent` | 重新分发同一组意图 | 目标 present(version);仅在源删除成功时记录 absent | -按需启用的 `apply_patch` 工具返回严格的提交值,包含各操作诊断及用于渲染的完整文本变更输入;持久化元数据只保留 diff hunk 与停止标志。语法和预检拒绝为 `isError: true`;停止的提交报告仍为 `isError: false`,确保已知部分 diff 进入 Session 展示。[工具 README](../../packages/fs/tool-fs/README.zh.md#use-this-package) 维护格式与恢复规则。 +`apply_patch` 工具返回严格的提交值,包含各操作诊断及用于渲染的完整文本变更输入;持久化元数据只保留 diff hunk 与停止标志。语法和预检拒绝为 `isError: true`;停止的提交报告仍为 `isError: false`,确保已知部分 diff 进入 Session 展示。[工具 README](../../packages/fs/tool-fs/README.zh.md#use-this-package) 维护格式与恢复规则。 ## 错误分类体系(提供方约定) @@ -570,49 +568,49 @@ abstract writeText( target: FsTarget, content: string, expected?: FsWriteIntent, abstract editText( target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise /** - * Apply hunks under the version guard and mutation lock. Missing targets and - * stale versions report FS_STALE_VERSION; unmatched hunks report FS_PATCH_MISMATCH. - * Callers obtain expected through fs/edit-intent and record present(version) - * through fs/observed on success. Internal preflight reads are not model observations. - * @param target - resolved regular UTF-8 file to update. - * @param hunks - ordered parsed hunks applied to LF-normalized content. - * @param expected - observed version; omit to patch the current content unconditionally. - * @param signal - aborts before atomic publication. - * @param sandboxPolicy - per-call confinement; omit to use the provider default. - * @returns the published version and LF-normalized before/after text. + * Atomically apply parsed hunks to the LF-normalized text and restore the + * stored line-ending style. A missing or stale target reports + * `FS_STALE_VERSION`; an unmatched hunk reports `FS_PATCH_MISMATCH`. + * @param target - the resolved regular file to patch. + * @param hunks - ordered hunks from `parsePatch`. + * @param expected - the version guard; omit for an unconditional patch. + * @param signal - aborts before atomic publication takes effect. + * @param sandboxPolicy - the per-call mode and workspace root this patch runs + * under; a sandboxing backend fences the patch by it, the bare backend + * ignores it. Omit to leave the backend its own default. + * @returns the outcome, including the version the patch produced. */ abstract patchText( target: FsTarget, hunks: readonly PatchHunk[], expected?: { version: FsVersion }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise /** - * Remove a resolved regular file. Missing/stale targets report FS_STALE_VERSION; - * other types report FS_NOT_REGULAR_FILE. Obtain expected through fs/delete-intent - * and record absent through fs/observed after successful removal. - * Resolution follows symlinks: callers rejecting symlink sources must lstat before resolve. - * @param target - resolved file to remove, including the real target of a symlink. - * @param expected - observed version; omit for unconditional removal of the current file. - * @param signal - aborts before removal; successful removal is not rolled back. - * @param sandboxPolicy - per-call confinement; omit to use the provider default. - * @returns the bounded LF-normalized removed content, or null when no basis is available. + * Remove a regular file. A missing or stale target reports `FS_STALE_VERSION`; + * a directory or other non-file reports `FS_NOT_REGULAR_FILE`. Resolution has + * already followed symlinks, so the target is the real file. + * @param target - the resolved file to remove. + * @param expected - the version guard; omit for an unconditional removal. + * @param signal - aborts before removal. + * @param sandboxPolicy - the per-call mode and workspace root this removal + * runs under; a sandboxing backend fences it, the bare backend ignores it. + * Omit to leave the backend its own default. + * @returns the bounded LF-normalized removed text, or null when no basis is available. */ abstract deleteFile( target: FsTarget, expected?: { version: FsVersion }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise /** - * Publish source text at a destination, optionally patching it, then remove the source. - * Both paths remain locked in canonical order. FS_SAME_TARGET rejects identical keys; - * source guards match patchText, destination guards match writeText. No cross-path - * transaction is promised: destination publication with the source retained is a final outcome. - * Obtain source/destination guards through fs/edit-intent and fs/write-intent. On - * resolution, record destination present(destination.version); record source absent - * only when sourceDeleted is true. Providers do not emit these observations. - * Resolution follows symlinks: callers rejecting symlink sources must lstat before resolve. - * @param source - resolved regular UTF-8 source; its line-ending style and mode are preserved. - * @param destination - resolved destination in the same provider, distinct from source. - * @param options - optional hunks and source/destination guards; omitted fields mean - * no text patch and unconditional source/destination mutations respectively. - * @param signal - checked before destination staging; once staging starts, cancellation - * does not interrupt the local commit. Remote cancellation can leave the outcome unknown. - * @param sandboxPolicy - confinement for both paths; omit to use the provider default. - * @returns locked LF sourceBefore, destination outcome, and source-removal status; failures before publication reject. + * Publish the source text, optionally patched, at the destination, then remove + * the source. Both targets stay locked in canonical order. Identical targets + * report `FS_SAME_TARGET`; the source guard follows `patchText` and the + * destination guard follows `writeText`. The two effects are separate: a + * published destination with a retained source is a final outcome reported + * through `sourceDeleted: false`. + * @param source - the resolved regular source; its line-ending style and mode carry over. + * @param destination - the resolved destination within the same provider. + * @param options - optional hunks and the source/destination guards; an omitted guard is unconditional. + * @param signal - checked before destination staging; the commit then completes both effects. + * @param sandboxPolicy - the per-call mode and workspace root both paths run + * under; a sandboxing backend fences them, the bare backend ignores it. + * Omit to leave the backend its own default. + * @returns the locked LF source text, the destination outcome, and the source-removal status. */ abstract moveFile( source: FsTarget, destination: FsTarget, options: { hunks?: readonly PatchHunk[] expectedSource?: { version: FsVersion } expectedDestination?: FsWriteIntent }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise ``` diff --git a/docs/subsystems/system-prompt.i18n.yaml b/docs/subsystems/system-prompt.i18n.yaml index b7671bae6a..9e4e3362df 100644 --- a/docs/subsystems/system-prompt.i18n.yaml +++ b/docs/subsystems/system-prompt.i18n.yaml @@ -6,8 +6,8 @@ en: 7ad02b2146520e7b zh: 93cb93c7aa12ceb4 /system-prompt-assembly/assembly-context: - en: d76d9f9f22614d4b - zh: 990c3b88ece1b24f + en: 5c99927ab1441bb1 + zh: 36f119ff30307f62 /system-prompt-assembly/tool-provider-result: en: 89cb015bf2e3e7a5 zh: b4340e853b34c777 diff --git a/docs/subsystems/system-prompt.md b/docs/subsystems/system-prompt.md index ae10db31e3..c56b8bbd6b 100644 --- a/docs/subsystems/system-prompt.md +++ b/docs/subsystems/system-prompt.md @@ -23,7 +23,7 @@ interface AssembleContext { } ``` -`TOOL_APPLY_PATCH` allocates order 1350 between `TOOL_EDIT` (1300) and `TOOL_GLOB` (1400); its guidance is present only when the evaluation tool is enabled and visible. +`TOOL_APPLY_PATCH` takes order 1350 between `TOOL_EDIT` (1300) and `TOOL_GLOB` (1400). ## Tool-provider result diff --git a/docs/subsystems/system-prompt.zh.md b/docs/subsystems/system-prompt.zh.md index c01c5bb426..c57b17cd29 100644 --- a/docs/subsystems/system-prompt.zh.md +++ b/docs/subsystems/system-prompt.zh.md @@ -23,7 +23,7 @@ interface AssembleContext { } ``` -`TOOL_APPLY_PATCH` 的顺序为 1350,位于 `TOOL_EDIT`(1300)与 `TOOL_GLOB`(1400)之间;只有评测工具已启用且可见时才显示其提示。 +`TOOL_APPLY_PATCH` 的顺序为 1350,位于 `TOOL_EDIT`(1300)与 `TOOL_GLOB`(1400)之间。 ## 工具提供方结果 diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/guarded-filesystem-mutations/guide.i18n.yaml b/docs/upgrade-guide/v0.2.1-alpha.4/guarded-filesystem-mutations/guide.i18n.yaml deleted file mode 100644 index bf740eef58..0000000000 --- a/docs/upgrade-guide/v0.2.1-alpha.4/guarded-filesystem-mutations/guide.i18n.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# Bilingual-pair consistency record for guide.md (docs/i18n/README.md): per heading -# section, a hash of its English and Chinese blocks outside code blocks and generated regions. -# After editing either side, bring the other along and re-record with: -# pnpm run verify-translation-pairing --write docs/upgrade-guide/v0.2.1-alpha.4/guarded-filesystem-mutations/guide.md -/: - en: d2b160c7f3d0ba78 - zh: aa0b2cd228bdbcda -/guarded-filesystem-patch-delete-and-move-operations: - en: 25c6a54f9edc6017 - zh: 2fc1a21e58dc8a77 -/guarded-filesystem-patch-delete-and-move-operations/change: - en: d276665c66c59fcf - zh: b6edecdcd3b54f78 -/guarded-filesystem-patch-delete-and-move-operations/migration: - en: d19d02313a567275 - zh: 2262adb2a8ec3f48 diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/guarded-filesystem-mutations/guide.md b/docs/upgrade-guide/v0.2.1-alpha.4/guarded-filesystem-mutations/guide.md deleted file mode 100644 index 5469ace8b3..0000000000 --- a/docs/upgrade-guide/v0.2.1-alpha.4/guarded-filesystem-mutations/guide.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -kind: upgrade-guide -description: "Filesystem providers must implement guarded patch, delete, and move operations and handle two new filesystem error codes." ---- - -# Guarded filesystem patch, delete, and move operations - -English | [中文](guide.zh.md) - -## Change - -The next release adds three abstract methods to `FileSystem`: `patchText`, `deleteFile`, and `moveFile`. External subclasses that implemented the earlier API must implement them. `FsErrorCode` adds `FS_PATCH_MISMATCH` and `FS_SAME_TARGET`; exhaustive consumers must handle both. The bundled local, sandboxed, and SSH providers implement these operations. - -`@deepseek-ai/dsh-fs-patch` provides pure `parsePatch`, `applyHunks`, and `collectMutationPaths` functions. The SSH helper adds `fs.patch`, `fs.delete`, and `fs.move` RPC methods. `dsh-tool-fs` adds `enableApplyPatch`, defaulting to `true`; profiles that mount the text tools advertise `apply_patch` and its guidance alongside existing tools. Minimal presets explicitly disable it. No Session-data migration is required. - -## Migration - -1. Update external `FileSystem` subclasses to the [method signatures and failure guarantees](../../../../packages/fs/fs/src/index.ts). Preserve version guards, cancellation, text handling, and sandbox enforcement. `patchText` publishes a complete replacement only after matching succeeds; `deleteFile` reports the removed text; `moveFile` reports destination publication and `sourceDeleted` separately. A failed source deletion must retain the published destination in its result. Do not promise a transaction across paths. -2. Handle `FS_PATCH_MISMATCH` and `FS_SAME_TARGET` in exhaustive error-code dispatch. Use [the pure patch library](../../../../packages/fs/fs-patch/README.md) if the provider needs shared syntax and LF matching. Mutation callers obtain guards from `fs/edit-intent`, `fs/delete-intent`, or destination `fs/write-intent`, and record successful observations; internal reads do not authorize mutations. -3. Deploy the SSH host and helper from matching package versions before invoking the three new RPC methods. Custom helpers must implement the [declared helper protocol](../../../../packages/ssh/ssh/src/helper.ts). -4. To omit the tool and its guidance, add `enableApplyPatch: false` to the existing `tool-fs` row's config in a `--patch` overlay or `cordis.patch.yml`, preserving other configured fields. Follow [tool configuration](../../../../packages/fs/tool-fs/README.md#use-this-package). Compile external providers and exercise add, update, delete, move, stale-version refusal, and partial source-deletion failure through their ordinary tool composition; compare final files with reported effects. diff --git a/docs/upgrade-guide/v0.2.1-alpha.4/guarded-filesystem-mutations/guide.zh.md b/docs/upgrade-guide/v0.2.1-alpha.4/guarded-filesystem-mutations/guide.zh.md deleted file mode 100644 index a5b3aca6a8..0000000000 --- a/docs/upgrade-guide/v0.2.1-alpha.4/guarded-filesystem-mutations/guide.zh.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -kind: upgrade-guide -description: "文件系统提供方必须实现受保护的补丁、删除和移动操作,并处理两个新增文件系统错误码。" ---- - -# 受保护的文件系统补丁、删除和移动操作 - -[English](guide.md) | 中文 - -## 变更 - -下一版本为 `FileSystem` 新增三个抽象方法:`patchText`、`deleteFile` 和 `moveFile`。实现旧 API 的外部子类必须实现这些方法。`FsErrorCode` 新增 `FS_PATCH_MISMATCH` 和 `FS_SAME_TARGET`;穷尽分派的消费方必须处理这两个值。随发行版提供的本地、沙箱和 SSH 提供方均实现这些操作。 - -`@deepseek-ai/dsh-fs-patch` 提供纯函数 `parsePatch`、`applyHunks` 和 `collectMutationPaths`。SSH helper 新增 `fs.patch`、`fs.delete` 和 `fs.move` RPC 方法。`dsh-tool-fs` 新增默认值为 `true` 的 `enableApplyPatch`;挂载文本工具的 profile 会在已有工具之外提供 `apply_patch` 及其指导文本。最小预设显式关闭它。无需迁移 Session 数据。 - -## 迁移 - -1. 按[方法签名和失败保证](../../../../packages/fs/fs/src/index.ts)更新外部 `FileSystem` 子类。保留版本防护、取消、文本处理和沙箱约束。`patchText` 仅在匹配成功后发布完整替换;`deleteFile` 报告被删除的文本;`moveFile` 分别报告目标发布与 `sourceDeleted`。删除源失败时,结果必须保留已发布目标。不要承诺跨路径事务。 -2. 在错误码穷尽分派中处理 `FS_PATCH_MISMATCH` 和 `FS_SAME_TARGET`。提供方需要共享语法和 LF 匹配时,使用[纯补丁库](../../../../packages/fs/fs-patch/README.zh.md)。修改调用方从 `fs/edit-intent`、`fs/delete-intent` 或目标的 `fs/write-intent` 获取防护,并记录成功观察;内部读取不能授权修改。 -3. 在调用三个新 RPC 方法前,部署版本一致的 SSH Host 和 helper。自定义 helper 必须实现[声明的 helper 协议](../../../../packages/ssh/ssh/src/helper.ts)。 -4. 若要省略工具及其指导文本,在 `--patch` overlay 或 `cordis.patch.yml` 的已有 `tool-fs` 行 config 中加入 `enableApplyPatch: false`,保留其他配置字段。遵循[工具配置](../../../../packages/fs/tool-fs/README.zh.md#use-this-package)。编译外部提供方,通过其正常工具组合验证新增、更新、删除、移动、过期版本拒绝和部分删除源失败,并比较最终文件与报告效果。 diff --git a/packages/acp/acp/README.i18n.yaml b/packages/acp/acp/README.i18n.yaml index 6c78c472ad..68c18de182 100644 --- a/packages/acp/acp/README.i18n.yaml +++ b/packages/acp/acp/README.i18n.yaml @@ -21,8 +21,8 @@ en: c3f5f144daf83157 zh: a65a64842fd35bef /deepseek-ai-dsh-acp/use-this-package/minimal-configuration: - en: 1cc88bb03fabb004 - zh: 8cecdee1085aa6a5 + en: 320139cc5ebcf34a + zh: 89beb58d9a0511ba /deepseek-ai-dsh-acp/use-this-package/start-a-server: en: 4ed8e37834267edb zh: a32d5dedb4e8e919 diff --git a/packages/acp/acp/README.md b/packages/acp/acp/README.md index 71fa1a5e21..fa713b872f 100644 --- a/packages/acp/acp/README.md +++ b/packages/acp/acp/README.md @@ -33,8 +33,6 @@ Choose it when automation should own the interaction: an out-of-process subagent ### Minimal configuration -Image capability discovery returns false for unknown providers or models; other metadata lookup failures propagate to initialization. - Every session the server creates uses the provider and model configured here. Both fields are optional so another agent or request listener can supply them; the runnable demo composition sets both. Stdout carries only protocol traffic, so keep logging off it. ```yaml diff --git a/packages/acp/acp/README.zh.md b/packages/acp/acp/README.zh.md index 760da1ff7f..e62e154191 100644 --- a/packages/acp/acp/README.zh.md +++ b/packages/acp/acp/README.zh.md @@ -33,8 +33,6 @@ kind: "package-reference" ### 最小配置 -图片能力探测对未知提供方或模型返回 false;其他元数据查询失败会传播到初始化调用。 - 服务器创建的每个会话都使用此处配置的提供方与模型。两个字段都是可选的,以便由另一个 agent/request 监听器提供;可运行的演示组合会同时设置两者。Stdout 只承载协议流量,因此请让日志远离它。 ```yaml diff --git a/packages/acp/acp/src/content.ts b/packages/acp/acp/src/content.ts index c73d45f5e6..e31807b276 100644 --- a/packages/acp/acp/src/content.ts +++ b/packages/acp/acp/src/content.ts @@ -5,7 +5,7 @@ import type { Context } from '@deepseek-ai/cordis' import { isImageAdmissionError } from '@deepseek-ai/dsh-attachment' import type { ImageAttachmentRef, ImageMediaType, SaveImageAttachment } from '@deepseek-ai/dsh-attachment' import type { ModelSelection } from '@deepseek-ai/dsh-agent' -import { LlmError, type ContentBlock } from '@deepseek-ai/dsh-llm' +import type { ContentBlock } from '@deepseek-ai/dsh-llm' /** Raster formats shared by ACP image blocks and the core attachment vocabulary. */ const IMAGE_MEDIA_TYPES: readonly ImageMediaType[] = [ @@ -85,7 +85,6 @@ async function assertImageRoute(ctx: Context, route: ModelSelection | undefined, * @param provider - configured provider route used for newly created sessions. * @param model - configured exact model id used for newly created sessions. * @returns whether this bridge can admit images at initialization time. - * @throws metadata lookup failures other than an unknown provider or model. */ export async function supportsAcpImagePrompts( ctx: Context, @@ -96,12 +95,12 @@ export async function supportsAcpImagePrompts( const llm = ctx.get('llm') if (attachments === undefined || llm === undefined || provider === undefined || model === undefined) return false if (!attachments.imageLimits.mediaTypes.some(mediaType => IMAGE_MEDIA_TYPES.includes(mediaType))) return false - let info - try { info = await llm.resolveModelInfo(provider, model) } catch (error) { - if (error instanceof LlmError && (error.code === 'NO_ADAPTER' || error.code === 'UNKNOWN_MODEL')) return false - throw error + try { + const info = await llm.resolveModelInfo(provider, model) + return info.inputModalities?.includes('image') === true + } catch { + return false } - return info.inputModalities?.includes('image') === true } /** Render one baseline resource link into the core's current text vocabulary. */ diff --git a/packages/acp/acp/tests/content.spec.ts b/packages/acp/acp/tests/content.spec.ts index 7c36cd4a9f..144559b6e0 100644 --- a/packages/acp/acp/tests/content.spec.ts +++ b/packages/acp/acp/tests/content.spec.ts @@ -3,7 +3,6 @@ import type { Context } from '@deepseek-ai/cordis' import { AttachmentError, AttachmentId } from '@deepseek-ai/dsh-attachment' import type { ImageAttachmentRef, SaveImageAttachment } from '@deepseek-ai/dsh-attachment' import type { ModelSelection } from '@deepseek-ai/dsh-agent' -import { LlmError } from '@deepseek-ai/dsh-llm' import { AcpContentError, admitAcpPrompt, @@ -80,14 +79,7 @@ describe('ACP rich content codec', () => { await expect(supportsAcpImagePrompts(absent(store, imageLlm), undefined, 'm')).resolves.toBe(false) await expect(supportsAcpImagePrompts(absent(store, imageLlm), 'p', undefined)).resolves.toBe(false) await expect(supportsAcpImagePrompts(absent(noMediaStore, imageLlm), 'p', 'm')).resolves.toBe(false) - await expect(supportsAcpImagePrompts(absent(store, brokenLlm), 'p', 'm')).rejects.toThrow('catalog down') - for (const code of ['NO_ADAPTER', 'UNKNOWN_MODEL']) { - const missing = { resolveModelInfo: vi.fn().mockRejectedValue(new LlmError('missing route', code)) } - await expect(supportsAcpImagePrompts(absent(store, missing), 'p', 'm')).resolves.toBe(false) - } - const invalid = new LlmError('invalid metadata', 'INVALID_MODEL_INFO') - brokenLlm.resolveModelInfo.mockRejectedValueOnce(invalid) - await expect(supportsAcpImagePrompts(absent(store, brokenLlm), 'p', 'm')).rejects.toBe(invalid) + await expect(supportsAcpImagePrompts(absent(store, brokenLlm), 'p', 'm')).resolves.toBe(false) await expect(supportsAcpImagePrompts(absent(store, unknownLlm), 'p', 'm')).resolves.toBe(false) await expect(supportsAcpImagePrompts(absent(store, textLlm), 'p', 'm')).resolves.toBe(false) await expect(supportsAcpImagePrompts(absent(store, imageLlm), 'p', 'm')).resolves.toBe(true) diff --git a/packages/api/gateway/tests/gateway.host.spec.ts b/packages/api/gateway/tests/gateway.host.spec.ts index 0ec1e61908..6ec0e64bbb 100644 --- a/packages/api/gateway/tests/gateway.host.spec.ts +++ b/packages/api/gateway/tests/gateway.host.spec.ts @@ -61,7 +61,7 @@ class GoalService extends Service { lastSignal: AbortSignal | undefined lastPeer: PeerScope | undefined nextResult: unknown = undefined - businessError: unknown + businessError: Error | undefined constructor(ctx: Context) { super(ctx, 'goals') @@ -1121,7 +1121,7 @@ describe('TypertGatewayService', () => { expect(result.error.message).toContain('plain-object args field') } - service.businessError = 'non-error failure' + service.businessError = 'non-error failure' as unknown as Error await expect(handler( 'goals/fail', { args: { request: null } }, diff --git a/packages/api/session-controller/README.i18n.yaml b/packages/api/session-controller/README.i18n.yaml index 58c0b11451..46f670802e 100644 --- a/packages/api/session-controller/README.i18n.yaml +++ b/packages/api/session-controller/README.i18n.yaml @@ -9,8 +9,8 @@ en: c7a4c58ea6d3437b zh: 45c2b472a074bb1b /session-controller/summary: - en: 30b14b61e6a00185 - zh: 2b9cece7756eb361 + en: 4914824c347fdab5 + zh: 684a7b7fa970c39f /session-controller/table-of-contents: en: 1fdf707322fb6996 zh: 0204f7e9d4aa3cce diff --git a/packages/api/session-controller/README.md b/packages/api/session-controller/README.md index d9e0fb58da..02dff176ba 100644 --- a/packages/api/session-controller/README.md +++ b/packages/api/session-controller/README.md @@ -12,8 +12,6 @@ Desktop analytics follows the [product collection policy](../../client/product-a `@deepseek-ai/dsh-api-session-controller` owns the Host `ctx.sessionController` service and the generated Client `session`, `skills`, and `fileReferences` Remote namespaces. It serves Session lifecycle and history, the Host-generation model catalog, human background-job kill, workspace-path opening, user-invocable skill discovery, and Agent-scoped file references. Use it through API Gateway when a Client needs operations addressed by a Session. -Cold skill listing falls back to the global registry when preset scope acquisition fails. Missing or invalid presets fall back silently; other failures log the preset id and error. - ## Table of Contents - [Use this package](#use-this-package) diff --git a/packages/api/session-controller/README.zh.md b/packages/api/session-controller/README.zh.md index 6ba37fb44f..b01a99cc1f 100644 --- a/packages/api/session-controller/README.zh.md +++ b/packages/api/session-controller/README.zh.md @@ -12,8 +12,6 @@ kind: "package-reference" `@deepseek-ai/dsh-api-session-controller` 拥有 Host 的 `ctx.sessionController` 服务,以及生成的 Client `session`、`skills` 和 `fileReferences` Remote namespace。它提供 Session 生命周期与历史、Host generation 模型目录、人工后台任务终止、工作区路径打开、用户可调用 skill(技能)发现和 Agent(智能体)范围的文件引用。当 Client 需要按 Session 寻址的操作时,请通过 API Gateway 使用它。 -冷 Session 的技能列表在 preset 作用域获取失败时回退到全局注册表。preset 缺失或无效时静默回退,其他失败会记录 preset id 和错误。 - ## 目录 - [使用本包](#use-this-package) diff --git a/packages/api/session-controller/src/skill-catalog.ts b/packages/api/session-controller/src/skill-catalog.ts index 9fed066469..c9865b0e6b 100644 --- a/packages/api/session-controller/src/skill-catalog.ts +++ b/packages/api/session-controller/src/skill-catalog.ts @@ -98,9 +98,8 @@ export class SessionSkillCatalog extends TypertRemoteService { if (presets === undefined) return undefined try { return await presets.acquireScope(agentPreset) - } catch (error) { - if (error instanceof RemoteError && (error.code === 'agent-preset/not-found' || error.code === 'agent-preset/invalid')) return undefined - this.ctx.logger.warn(`skill catalog could not acquire preset ${String(agentPreset)}; using global skills: ${String(error)}`) + } catch { + // An unknown or unusable recorded preset falls back to the global registry. return undefined } } diff --git a/packages/api/session-controller/tests/session-skills.host.spec.ts b/packages/api/session-controller/tests/session-skills.host.spec.ts index 156f3b1a66..939ef7a269 100644 --- a/packages/api/session-controller/tests/session-skills.host.spec.ts +++ b/packages/api/session-controller/tests/session-skills.host.spec.ts @@ -1,4 +1,3 @@ -import { RemoteError } from '@deepseek-ai/dsh-typert-protocol' import { Context } from '@deepseek-ai/cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' import { provideWorkingDirectoryFixture } from '@deepseek-ai/dsh-agent-loop-testkit' @@ -177,7 +176,7 @@ describe('SessionSkillCatalog', () => { expect(ctx.agents.list()).toEqual([]) }) - it.each(['agent-preset/not-found', 'agent-preset/invalid'] as const)('falls back to the global registry for %s', async (code) => { + it('falls back to the global registry when the recorded preset is unavailable', async () => { const ctx = await context() const sessionId = SessionId('gone-preset') ctx.provide('sessionQuery', { @@ -187,7 +186,7 @@ describe('SessionSkillCatalog', () => { })), } as never) ctx.provide('agentPresets', { - acquireScope: () => Promise.reject(new RemoteError(code, 'unavailable preset', { agentPreset: 'gone', available: [], reason: 'unavailable' })), + acquireScope: () => Promise.reject(new Error('unknown preset')), } as never) const list = vi.fn(() => Promise.resolve([])) ctx.provide('skills', { list } as never) @@ -197,29 +196,6 @@ describe('SessionSkillCatalog', () => { expect(list).toHaveBeenCalledWith({ cwd: '/cold/project', scope: undefined }) }) - it.each([new Error('preset defect'), new RemoteError('gateway/internal', 'preset defect', {})])('warns and falls back to global skills for unexpected preset failures: %s', async (failure) => { - const ctx = await context() - const sessionId = SessionId('gone-preset') - ctx.provide('sessionQuery', { - observeSession: () => Promise.resolve(observation(sessionId, { - cwd: '/cold/project', - agentPreset: 'gone', - })), - } as never) - ctx.provide('agentPresets', { - acquireScope: () => Promise.reject(failure), - } as never) - const list = vi.fn(() => Promise.resolve([])) - ctx.provide('skills', { list } as never) - const catalog = new SessionSkillCatalog(ctx) - - const warn = vi.spyOn(ctx.logger, 'warn').mockImplementation(() => {}) - await expect(catalog.list({ sessionId }, new AbortController().signal)).resolves.toEqual({ skills: [] }) - expect(list).toHaveBeenCalledWith({ cwd: '/cold/project', scope: undefined }) - expect(warn).toHaveBeenCalledWith(expect.stringContaining('preset gone')) - expect(warn).toHaveBeenCalledWith(expect.stringContaining('preset defect')) - }) - it.each([ { error: new SessionQueryError( diff --git a/packages/bundle/sdk-minimal/cordis.patch.yml b/packages/bundle/sdk-minimal/cordis.patch.yml index 2fb021e480..3547d65e81 100644 --- a/packages/bundle/sdk-minimal/cordis.patch.yml +++ b/packages/bundle/sdk-minimal/cordis.patch.yml @@ -48,7 +48,7 @@ readImage: true write: false edit: false - enableApplyPatch: false + applyPatch: false - id: working-directory name: '@deepseek-ai/dsh-working-directory' diff --git a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts index f45a8d7192..09b5a997ce 100644 --- a/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts +++ b/packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts @@ -78,7 +78,7 @@ describe('dsh-sdk-minimal bundle', () => { personaPrefix: { __jsExpr: "process.env.DSH_SYSTEM_PROMPT ?? 'You are a helpful software engineer assistant.'" }, }) expect(rows.find(row => row.id === 'agent-loop')?.config).toEqual({ agents: [] }) - expect(rows.find(row => row.id === 'tool-fs')?.config).toEqual({ read: false, readImage: true, write: false, edit: false, enableApplyPatch: false }) + expect(rows.find(row => row.id === 'tool-fs')?.config).toEqual({ read: false, readImage: true, write: false, edit: false, applyPatch: false }) expect(rows.find(row => row.id === 'compaction-basic')?.config).toBeUndefined() expect(rows.find(row => row.id === 'terminal-bash')).toMatchObject({ disabled: { __jsExpr: "process.platform === 'win32'" }, diff --git a/packages/bundle/web-app/presets/minimal.patch.yml b/packages/bundle/web-app/presets/minimal.patch.yml index fc731fc2de..332e69884e 100644 --- a/packages/bundle/web-app/presets/minimal.patch.yml +++ b/packages/bundle/web-app/presets/minimal.patch.yml @@ -21,7 +21,7 @@ readImage: true write: false edit: false - enableApplyPatch: false + applyPatch: false - id: compaction name: cordis:group group: true diff --git a/packages/client/locale/src/client/index.ts b/packages/client/locale/src/client/index.ts index a149395c95..744a5f54e6 100644 --- a/packages/client/locale/src/client/index.ts +++ b/packages/client/locale/src/client/index.ts @@ -419,8 +419,11 @@ export class LocaleRuntime { } for (const [locale, entries] of pairs) locales.set(localeKey(locale), entries) this.publish(this.snapshot.active, false) - const owner = locales return () => { + const owner = this.dicts.get(ns) + /* v8 ignore next -- defensive: a namespace's locales map is created on + * first register and never removed, so the disposer always finds it. */ + if (!owner) return let removed = false for (const [locale, entries] of pairs) { const key = localeKey(locale) diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx index 89d398a3ff..ef95796577 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx @@ -337,7 +337,8 @@ export const InputBar = memo(function InputBar({ return } if (keyboard === undefined) return // absent machine: the button is disabled - keyboard.submit(primarySubmitMode, 'click') + /* v8 ignore next -- defensive: the primary button is disabled for empty, disabled, and pending-upload states. */ + if (!empty && !disabled && !machineBusy && !uploadsPending) keyboard.submit(primarySubmitMode, 'click') } // Claim ghost hint: rendered by CSS as generated content after the last diff --git a/packages/client/ui-renderer/src/client/registry.ts b/packages/client/ui-renderer/src/client/registry.ts index 4da639d888..4c3f520c8a 100644 --- a/packages/client/ui-renderer/src/client/registry.ts +++ b/packages/client/ui-renderer/src/client/registry.ts @@ -488,14 +488,16 @@ export class SlotRegistry extends Service { // duplicate declaration, kind conflicts, cross-scope handle) throws // there before this layer commits anything. const dispose = (this._core as ErasedCore).register(erased, component) - const releaseStore = store === undefined ? undefined - : this._acquire(store, (this._core.specDynamic(options.name) as SlotSpec).scope) + if (store !== undefined) { + const scope = (this._core.specDynamic(options.name) as SlotSpec).scope + this._acquire(store, scope) + } let disposed = false return () => { if (disposed) return disposed = true dispose() - releaseStore?.() + if (store !== undefined) this._release(store) } } @@ -508,9 +510,9 @@ export class SlotRegistry extends Service { const dispose = (this._core as ErasedFactoryCore).registerFactory(erased, component) const definition = this._core.factory(options.name) if (definition === undefined) throw new Error(`slot factory "${options.name}" disappeared during registration`) - const releaseStore = definition.store !== undefined && typeof definition.store !== 'function' - ? this._acquire(definition.store, definition.scope) : undefined - if (typeof definition.store === 'function') { + if (definition.store !== undefined && typeof definition.store !== 'function') { + this._acquire(definition.store, definition.scope) + } else if (typeof definition.store === 'function') { this._factoryStores.set(definition, { occurrences: new WeakMap(), mounted: new Map(), @@ -522,7 +524,9 @@ export class SlotRegistry extends Service { disposed = true dispose() this._factoryStores.delete(definition) - releaseStore?.() + if (definition.store !== undefined && typeof definition.store !== 'function') { + this._release(definition.store) + } } } @@ -688,19 +692,26 @@ export class SlotRegistry extends Service { } } - /** Retain a handle and return the release of that exact record. */ - private _acquire(handle: EngineStoreHandle, scope: SlotScope): () => void { - let record = this._stores.get(handle) + /** Bind (or re-reference) a handle on the axis; cross-scope conflicts already threw in the core. */ + private _acquire(handle: EngineStoreHandle, scope: SlotScope): void { + const record = this._stores.get(handle) if (record === undefined) { - record = { scope, refs: 0, instances: new Map() } - this._stores.set(handle, record) + this._stores.set(handle, { scope, refs: 1, instances: new Map() }) + return } record.refs += 1 - const retained = record - return () => { - retained.refs -= 1 - if (retained.refs === 0) this._stores.delete(handle) - } + } + + /** Drop one reference; the last holder's unload drops the record (instances go with it — engine stores need no explicit dispose). */ + private _release(handle: EngineStoreHandle): void { + const record = this._stores.get(handle) + /* v8 ignore next -- defensive: release only runs from a disposer whose + * register acquired the same handle, so the record must exist; kept so a + * future call site cannot underflow the axis. */ + if (record === undefined) return + record.refs -= 1 + if (record.refs !== 0) return + this._stores.delete(handle) } } diff --git a/packages/client/ui-schedule/src/client/task-cron.ts b/packages/client/ui-schedule/src/client/task-cron.ts index c95631bdd7..abe0412ec0 100644 --- a/packages/client/ui-schedule/src/client/task-cron.ts +++ b/packages/client/ui-schedule/src/client/task-cron.ts @@ -196,6 +196,21 @@ function cronFieldValues(raw: string, spec: CronFieldSpec): number[] | undefined return [...matched].sort((left, right) => left - right) } +/** + * Failure reading a value the cron parser's own matching already proved present. + */ +/* v8 ignore next -- constructed only by the unreachable bounds guard below. */ +class CronInvariantError extends Error { + /** + * Construct an invariant failure. + * @param message - violated invariant. + */ + constructor(message: string) { + super(message) + this.name = 'CronInvariantError' + } +} + /** * Read one array element the caller's own length guard already bounds. * @param values - array whose length the caller checked. @@ -203,7 +218,10 @@ function cronFieldValues(raw: string, spec: CronFieldSpec): number[] | undefined * @returns the element at that index. */ function cronElementAt(values: readonly T[], index: number): T { - return values[index] as T + const value = values[index] + /* v8 ignore next -- every caller reads an index inside an array length it already proved. */ + if (value === undefined) throw new CronInvariantError('cron description read an index outside its array') + return value } /** diff --git a/packages/client/ui-slots/src/index.ts b/packages/client/ui-slots/src/index.ts index dcefe6317d..85c7d676cc 100644 --- a/packages/client/ui-slots/src/index.ts +++ b/packages/client/ui-slots/src/index.ts @@ -1598,7 +1598,9 @@ export class SlotCore { private releaseChildren(children: Readonly>> | undefined): void { if (children === undefined) return for (const childKey of Object.keys(children)) { - const childRec = this.record(childKey) + const childRec = this.records.get(childKey) + /* v8 ignore next -- defensive: declaring always creates the record */ + if (!childRec) continue const doomed = childRec.entries childRec.spec = undefined childRec.declaredBy = undefined diff --git a/packages/compaction/compaction-basic/src/index.ts b/packages/compaction/compaction-basic/src/index.ts index 7dd346b080..163cec3810 100644 --- a/packages/compaction/compaction-basic/src/index.ts +++ b/packages/compaction/compaction-basic/src/index.ts @@ -326,13 +326,16 @@ export class BasicCompactionEngine extends CompactionEngine { } if (measurement.totalTokens < spec.thresholdTokens) return null + let result: CompactionResult | null = null for (let attempt = 0; attempt <= spec.compactionRetries; attempt += 1) { const range = selectCompactableRange(agent.session, measurement, spec.retainTokens) if (range === null) { - if (attempt === 0) return null + /* v8 ignore else -- concrete replacement preserves a compactable checkpoint; subclass hooks cannot mutate it. */ + if (result === null) return null + /* v8 ignore next -- paired with the defensive post-success branch above. */ break } - const result = await this.compactRegion(range.start, range.end, agent, signal) + result = await this.compactRegion(range.start, range.end, agent, signal) measurement = meter.measure(agent.session) if (measurement.totalTokens < spec.thresholdTokens) return result } diff --git a/packages/core/agent-loop/src/tool-calls.ts b/packages/core/agent-loop/src/tool-calls.ts index dbeea913ee..c12de68d19 100644 --- a/packages/core/agent-loop/src/tool-calls.ts +++ b/packages/core/agent-loop/src/tool-calls.ts @@ -241,6 +241,8 @@ async function runGroup( for (const call of group.slice(started)) appendSkippedToolCall(session, turn, step, call.block) return { consumed: group.length, aborted: true, concluded } } + /* v8 ignore next -- unreachable: a non-aborted group commits every started call */ + if (committed !== started) throw new Error('tool-call scheduler: uncommitted settled calls') return { consumed: started, aborted: false, concluded } } diff --git a/packages/core/agent/src/consumed-work.ts b/packages/core/agent/src/consumed-work.ts index 5e19927b6c..55da0dbb5e 100644 --- a/packages/core/agent/src/consumed-work.ts +++ b/packages/core/agent/src/consumed-work.ts @@ -40,7 +40,22 @@ export interface ConsumedWork { * @returns whether the ending accounts for the input the turn took. */ function accountsForClaim(reason: TurnEndReason): boolean { - return reason.kind !== 'completed' + switch (reason.kind) { + case 'completed': + return false + case 'blocked': + case 'aborted': + case 'interrupted': + case 'error': + return true + /* v8 ignore next 5 -- unreachable: `max-tokens` requires a step and short-circuits + * before this call; `forked` exists only in constructor seed history, while production + * callers fold an operation-owned suffix. `TurnEndReasonMap` is merge-extensible, so a + * backend-added variant cannot be listed; an unnameable ending over consumed input must + * not read as success. */ + default: + return true + } } /** diff --git a/packages/core/tools/tests/gen-tool-catalog.spec.ts b/packages/core/tools/tests/gen-tool-catalog.spec.ts index bc0b2ba97d..03fe0ab9ce 100644 --- a/packages/core/tools/tests/gen-tool-catalog.spec.ts +++ b/packages/core/tools/tests/gen-tool-catalog.spec.ts @@ -26,7 +26,7 @@ describe('gen-tool-catalog collectToolCatalog', () => { const catalog = await collectToolCatalog() const names = catalog.flatMap(entry => entry.schemas.map(s => s.name)).sort() expect(names).toEqual([ - 'ask_user_question', 'bash', 'bash', 'cordis_inspect_list', + 'apply_patch', 'ask_user_question', 'bash', 'bash', 'cordis_inspect_list', 'cordis_inspect_query', 'create_goal', 'create_worktree', 'edit', 'exit_plan_mode', 'get_goal', 'glob', 'grep', 'interrupt_agent', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', diff --git a/packages/deliverables/workspace-changes/README.i18n.yaml b/packages/deliverables/workspace-changes/README.i18n.yaml index d1737ad4a3..b3cbe0d1af 100644 --- a/packages/deliverables/workspace-changes/README.i18n.yaml +++ b/packages/deliverables/workspace-changes/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-workspace-changes/use-this-package: - en: 3561aec982acc1e3 - zh: a1ae5f6aed527797 + en: 0ab7680d831598aa + zh: 2594f3c8c99cef24 /deepseek-ai-dsh-workspace-changes/understand-the-implementation: en: 9bfc66bea548bda2 zh: 7023c42cdb44569b diff --git a/packages/deliverables/workspace-changes/README.md b/packages/deliverables/workspace-changes/README.md index 5f1197743a..6cad61cf3d 100644 --- a/packages/deliverables/workspace-changes/README.md +++ b/packages/deliverables/workspace-changes/README.md @@ -47,7 +47,7 @@ After a `write`, `edit`, or mutating `str_replace_editor` call resolves its targ Each file carries a durable `path` — relative to the working directory inside it, absolute elsewhere — and a `display` path used for ordering and labels: the relative path, a `../` path for repository files above the working directory, a `~` path under the home directory, otherwise the absolute path. Files sort by `display` in code-unit order, which lists parent and absolute paths before the working directory's own files. The `workspace/changes` event carries only the turn number; `ctx.workspaceChanges.summary(sessionId, seq)` returns the summary the event with that sequence announced, or undefined once the Session is disposed or when this Host process never recorded it. `ctx.workspaceChanges.diff(sessionId, seq, index, signal)` compares the listed file at that index: hunks with three context lines from the two snapshot trees or the two copies, `binary` for a side git reported as binary or that holds a NUL byte, `oversized` for a side larger than `maxFileBytes`. A line comparison that runs longer than `diffTimeoutMs` degrades to one hunk replacing every line, marked `coarse`. A conversation reopened after a Host restart therefore has no card, and no comparison, for its earlier turns. -The intent listeners capture all add, update, delete, and move targets of `apply_patch` after policy admission and before provider mutation. Moves capture both ends; denied intents grant no observations and create no captures. Review compares each path against its first admitted baseline and lists only net changes, including completed effects of a stopped patch. +The intent listeners capture every add, update, delete, and move target of `apply_patch` after policy admission and before provider mutation; a move captures both ends. Review compares each path against its first baseline and lists net changes, including the completed effects of a stopped patch. ----- diff --git a/packages/deliverables/workspace-changes/README.zh.md b/packages/deliverables/workspace-changes/README.zh.md index 36687ebc00..a36f75dea4 100644 --- a/packages/deliverables/workspace-changes/README.zh.md +++ b/packages/deliverables/workspace-changes/README.zh.md @@ -47,7 +47,7 @@ kind: "package-reference" 每个文件携带持久的 `path`——位于工作目录内时为相对路径,否则为绝对路径——以及用于排序和标签的 `display` 路径:相对路径,仓库内位于工作目录之上的文件为 `../` 路径,家目录下的文件为 `~` 路径,其余为绝对路径。文件按 `display` 的码元顺序排序,因此上级路径和绝对路径排在工作目录自身文件之前。`workspace/changes` 事件只携带轮号;`ctx.workspaceChanges.summary(sessionId, seq)` 返回该序号的事件宣告的摘要,Session 已释放或本 Host 进程从未记录时返回 undefined。`ctx.workspaceChanges.diff(sessionId, seq, index, signal)` 对比该下标所列的文件:从两棵快照树或两份副本得出带三行上下文的 hunk;git 报告为二进制或某一侧含 NUL 字节时返回 `binary`;某一侧超过 `maxFileBytes` 时返回 `oversized`。逐行对比运行超过 `diffTimeoutMs` 时退化为一个替换全部行的 hunk,并标记 `coarse`。因此 Host 重启后重新打开的对话,先前轮次既没有卡片也没有对比。 -intent 监听器在策略准入后、提供方修改前捕获 `apply_patch` 的新增、更新、删除和移动目标。移动捕获两端;被拒绝的 intent 不授予观察,也不创建副本。Review 将每个路径与首次准入时的基线比较,只列出净变化,包括中止补丁已经完成的效果。 +intent 监听器在策略准入后、提供方修改前捕获 `apply_patch` 的每个新增、更新、删除和移动目标;移动捕获两端。Review 将每个路径与首次基线比较并列出净变化,包括中止补丁已经完成的效果。 ----- diff --git a/packages/deliverables/workspace-changes/tests/apply-patch.spec.ts b/packages/deliverables/workspace-changes/tests/apply-patch.spec.ts index 39a3e354c8..527ccc4889 100644 --- a/packages/deliverables/workspace-changes/tests/apply-patch.spec.ts +++ b/packages/deliverables/workspace-changes/tests/apply-patch.spec.ts @@ -49,7 +49,7 @@ async function boot(environment: string) { await ctx.plugin(ToolRuntime, { mode: 'native' }) await ctx.plugin(LocalFileSystem, { cwd }) await ctx.plugin(FsPolicy) - await ctx.plugin(ToolFs, { enableApplyPatch: true }) + await ctx.plugin(ToolFs, { applyPatch: true }) await ctx.plugin(WorkspaceChanges) const session = ctx.sessions.create(SessionId('patch-review'), { meta: { cwd } }) const signal = new AbortController().signal diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 6abd7d6bf2..6c6b2e5091 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -1205,21 +1205,21 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, { signature: 'abstract patchText( target: FsTarget, hunks: readonly PatchHunk[], expected?: { version: FsVersion }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise', - description: 'Apply hunks under the version guard and mutation lock. Missing targets and stale versions report FS_STALE_VERSION; unmatched hunks report FS_PATCH_MISMATCH. Callers obtain expected through fs/edit-intent and record present(version) through fs/observed on success. Internal preflight reads are not model observations.', - parameters: [{ name: 'target', description: 'resolved regular UTF-8 file to update.' }, { name: 'hunks', description: 'ordered parsed hunks applied to LF-normalized content.' }, { name: 'expected', description: 'observed version; omit to patch the current content unconditionally.' }, { name: 'signal', description: 'aborts before atomic publication.' }, { name: 'sandboxPolicy', description: 'per-call confinement; omit to use the provider default.' }], - returns: 'the published version and LF-normalized before/after text.', + description: 'Atomically apply parsed hunks to the LF-normalized text and restore the stored line-ending style. A missing or stale target reports `FS_STALE_VERSION`; an unmatched hunk reports `FS_PATCH_MISMATCH`.', + parameters: [{ name: 'target', description: 'the resolved regular file to patch.' }, { name: 'hunks', description: 'ordered hunks from `parsePatch`.' }, { name: 'expected', description: 'the version guard; omit for an unconditional patch.' }, { name: 'signal', description: 'aborts before atomic publication takes effect.' }, { name: 'sandboxPolicy', description: 'the per-call mode and workspace root this patch runs under; a sandboxing backend fences the patch by it, the bare backend ignores it. Omit to leave the backend its own default.' }], + returns: 'the outcome, including the version the patch produced.', }, { signature: 'abstract deleteFile( target: FsTarget, expected?: { version: FsVersion }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise', - description: 'Remove a resolved regular file. Missing/stale targets report FS_STALE_VERSION; other types report FS_NOT_REGULAR_FILE. Obtain expected through fs/delete-intent and record absent through fs/observed after successful removal. Resolution follows symlinks: callers rejecting symlink sources must lstat before resolve.', - parameters: [{ name: 'target', description: 'resolved file to remove, including the real target of a symlink.' }, { name: 'expected', description: 'observed version; omit for unconditional removal of the current file.' }, { name: 'signal', description: 'aborts before removal; successful removal is not rolled back.' }, { name: 'sandboxPolicy', description: 'per-call confinement; omit to use the provider default.' }], - returns: 'the bounded LF-normalized removed content, or null when no basis is available.', + description: 'Remove a regular file. A missing or stale target reports `FS_STALE_VERSION`; a directory or other non-file reports `FS_NOT_REGULAR_FILE`. Resolution has already followed symlinks, so the target is the real file.', + parameters: [{ name: 'target', description: 'the resolved file to remove.' }, { name: 'expected', description: 'the version guard; omit for an unconditional removal.' }, { name: 'signal', description: 'aborts before removal.' }, { name: 'sandboxPolicy', description: 'the per-call mode and workspace root this removal runs under; a sandboxing backend fences it, the bare backend ignores it. Omit to leave the backend its own default.' }], + returns: 'the bounded LF-normalized removed text, or null when no basis is available.', }, { signature: 'abstract moveFile( source: FsTarget, destination: FsTarget, options: { hunks?: readonly PatchHunk[] expectedSource?: { version: FsVersion } expectedDestination?: FsWriteIntent }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise', - description: 'Publish source text at a destination, optionally patching it, then remove the source. Both paths remain locked in canonical order. FS_SAME_TARGET rejects identical keys; source guards match patchText, destination guards match writeText. No cross-path transaction is promised: destination publication with the source retained is a final outcome. Obtain source/destination guards through fs/edit-intent and fs/write-intent. On resolution, record destination present(destination.version); record source absent only when sourceDeleted is true. Providers do not emit these observations. Resolution follows symlinks: callers rejecting symlink sources must lstat before resolve.', - parameters: [{ name: 'source', description: 'resolved regular UTF-8 source; its line-ending style and mode are preserved.' }, { name: 'destination', description: 'resolved destination in the same provider, distinct from source.' }, { name: 'options', description: 'optional hunks and source/destination guards; omitted fields mean no text patch and unconditional source/destination mutations respectively.' }, { name: 'signal', description: 'checked before destination staging; once staging starts, cancellation does not interrupt the local commit. Remote cancellation can leave the outcome unknown.' }, { name: 'sandboxPolicy', description: 'confinement for both paths; omit to use the provider default.' }], - returns: 'locked LF sourceBefore, destination outcome, and source-removal status; failures before publication reject.', + description: 'Publish the source text, optionally patched, at the destination, then remove the source. Both targets stay locked in canonical order. Identical targets report `FS_SAME_TARGET`; the source guard follows `patchText` and the destination guard follows `writeText`. The two effects are separate: a published destination with a retained source is a final outcome reported through `sourceDeleted: false`.', + parameters: [{ name: 'source', description: 'the resolved regular source; its line-ending style and mode carry over.' }, { name: 'destination', description: 'the resolved destination within the same provider.' }, { name: 'options', description: 'optional hunks and the source/destination guards; an omitted guard is unconditional.' }, { name: 'signal', description: 'checked before destination staging; the commit then completes both effects.' }, { name: 'sandboxPolicy', description: 'the per-call mode and workspace root both paths run under; a sandboxing backend fences them, the bare backend ignores it. Omit to leave the backend its own default.' }], + returns: 'the locked LF source text, the destination outcome, and the source-removal status.', }, ], }, @@ -6401,7 +6401,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [ }, { name: 'PatchHunk', - declaration: 'export interface PatchHunk {\n lineNumber: number;\n context?: string;\n oldLines: string[];\n newLines: string[];\n contextLineIndices: [\n number,\n number\n ][];\n isEndOfFile: boolean;\n}', + declaration: 'export interface PatchHunk {\n lineNumber: number;\n context?: string;\n oldLines: string[];\n newLines: string[];\n isEndOfFile: boolean;\n}', }, { name: 'PeerAdmission', diff --git a/packages/fs/README.i18n.yaml b/packages/fs/README.i18n.yaml index 7e7eae12d9..71bfe0ceb3 100644 --- a/packages/fs/README.i18n.yaml +++ b/packages/fs/README.i18n.yaml @@ -15,8 +15,8 @@ en: 1eb35b015a6c423f zh: a8a3dceee2c9e943 /packages-fs/packages: - en: 65da8f7d3942553e - zh: 0e1bfee110d5b5ea + en: 67ec32541e82a3f9 + zh: ef2b04f25e2ef634 /packages-fs/related-documentation: en: 2682ce38d0a99122 zh: 6e794fcfd13c3ef3 diff --git a/packages/fs/README.md b/packages/fs/README.md index cfd0debb7e..c9b9e157ba 100644 --- a/packages/fs/README.md +++ b/packages/fs/README.md @@ -27,7 +27,7 @@ The packages below provide filesystem access and patch parsing; the subsystem re | Package | Role | ctx key | |---|---|---| | [`fs/`](fs/README.md) | `ctx.fs` service contract: execution-world paths, bounded text I/O, and atomic mutations with an optional version guard | `ctx.fs` | -| [`fs-patch/`](fs-patch/README.md) | Pure Codex-style patch parsing and affected-path collection | none | +| [`fs-patch/`](fs-patch/README.md) | Pure Codex-style patch parsing and hunk application | none | | [`fs-local/`](fs-local/README.md) | Host-filesystem backend: reads, writes, and edits real files on the local machine | registers on `ctx.fs` | | [`fs-sandbox/`](fs-sandbox/README.md) | Sandbox-enforcing backend: fences writes and edits by the per-call sandbox mode while reads pass through | registers on `ctx.fs` | | [`fs-observation-policy/`](fs-observation-policy/README.md) | Read-before-edit policy: records observed presence or absence and guards write/edit through the `fs/*` events | `fs/*` listeners | diff --git a/packages/fs/README.zh.md b/packages/fs/README.zh.md index e8aa228534..aa62e09ace 100644 --- a/packages/fs/README.zh.md +++ b/packages/fs/README.zh.md @@ -27,7 +27,7 @@ kind: "package-group" | 包 | 职责 | ctx 键 | |---|---|---| | [`fs/`](fs/README.zh.md) | `ctx.fs` 服务约定:执行世界路径、有界文本 I/O,以及带可选版本防护的原子变更 | `ctx.fs` | -| [`fs-patch/`](fs-patch/README.zh.md) | Codex 风格补丁的纯解析与受影响路径收集 | 无 | +| [`fs-patch/`](fs-patch/README.zh.md) | Codex 风格补丁的纯解析与 hunk 应用 | 无 | | [`fs-local/`](fs-local/README.zh.md) | 宿主文件系统后端:读取、写入并编辑本机上的真实文件 | 注册到 `ctx.fs` | | [`fs-sandbox/`](fs-sandbox/README.zh.md) | 沙箱强制后端:按每次调用的沙箱模式约束写入与编辑,读取直接通过 | 注册到 `ctx.fs` | | [`fs-observation-policy/`](fs-observation-policy/README.zh.md) | 编辑前读取策略:记录观测到的存在或缺失,并通过 `fs/*` 事件防护写入/编辑 | `fs/*` 监听器 | diff --git a/packages/fs/fs-local/README.i18n.yaml b/packages/fs/fs-local/README.i18n.yaml index ad5b2c7475..c6fee0428e 100644 --- a/packages/fs/fs-local/README.i18n.yaml +++ b/packages/fs/fs-local/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-fs-local/use-this-package: - en: 6935a731cb5fee6a - zh: 7b17dab693b7c4b7 + en: c49313aeb4737eca + zh: 7b80272bbf8404e3 /deepseek-ai-dsh-fs-local/use-this-package/when-to-choose-it: en: 1bceab0cdb2e8f8e zh: 7d14d0c1ad90e9e2 @@ -24,8 +24,8 @@ en: 60678d0c5fbf3c56 zh: 1698a9645c12578b /deepseek-ai-dsh-fs-local/use-this-package/what-you-can-do: - en: 1a58fc2541567a12 - zh: beaea8d99c9d5171 + en: 04b6889eec28bd96 + zh: e94e2787de0ba704 /deepseek-ai-dsh-fs-local/understand-the-implementation: en: 6ffc93e09381bd3d zh: 57c3192c26a080ef diff --git a/packages/fs/fs-local/README.md b/packages/fs/fs-local/README.md index eb3a5fbe5c..0b6908f046 100644 --- a/packages/fs/fs-local/README.md +++ b/packages/fs/fs-local/README.md @@ -27,8 +27,6 @@ Use `dsh-fs-local` to read, list, watch, write, edit, patch, delete, and move fi Mount this backend when a composition needs `ctx.fs` backed by the real host filesystem and accepts a process-local implementation. The common path is explicit: load the backend, give it a base directory, and the model-facing tools (`dsh-tool-fs`) or your own plugins can read, write, and edit files. -Atomic write failures preserve filesystem error codes: denied parent or staging creation is `FS_PERMISSION_DENIED`; other Node I/O failures are `FS_IO_ERROR`. Unexpected non-I/O defects still reject unchanged. - ### When to choose it Choose `fs-local` for ordinary host-file access in a single process. Choose [`fs-sandbox`](../fs-sandbox/README.md) when a session's writes and edits must be confined to its workspace and temp roots — it extends this backend and adds only the mode fence. `config.cwd` is a resolution default, not a containment boundary: absolute paths and `..` escape it. @@ -54,14 +52,12 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a Read regular UTF-8 files whole or as streams, read bounded bytes, and list one directory level. Writes, literal edits, patches, and deletions serialize per target. Moves lock both targets in lexical order. `patchText` checks the version before calling `applyHunks`; `deleteFile` removes only a regular file. Missing sources use `FS_STALE_VERSION`, unmatched hunks use `FS_PATCH_MISMATCH`, and identical move targets use `FS_SAME_TARGET`. -Patch and move text is LF-normalized internally and restored using the source line-ending style. Outcome text uses LF. Delete bases and overwritten destination bases follow `diffBasisMaxBytes`; unavailable or oversized bases are null. Move checks cancellation before staging the destination, then completes without further cancellation checks. A source-removal failure returns the published destination with `sourceDeleted: false`; it does not roll back or hide that write. +Patch and move text is LF-normalized internally and restored using the source line-ending style; outcome text uses LF. Delete bases and overwritten destination bases follow `diffBasisMaxBytes`. A move checks cancellation before staging the destination, then completes both effects; a source-removal failure returns the published destination with `sourceDeleted: false`. Atomic write failures map denied directory or staging creation to `FS_PERMISSION_DENIED` and other Node I/O errors to `FS_IO_ERROR`. -Read, listing, and mutation failures are typed `FsError`s with stable codes — `FS_NOT_FOUND`, `FS_NOT_TEXT` (binary content), `FS_STALE_VERSION` (changed since observation), `FS_EDIT_NOT_FOUND` or `FS_AMBIGUOUS_EDIT` (no unique literal match), and others — so callers branch on the code, never on message text. A missing target on an edit reports `FS_STALE_VERSION` whether or not the version guard is supplied. A move returns `sourceFailure` only for a typed `FsError` from source removal; unexpected programming errors reject the call. +Read, listing, and mutation failures are typed `FsError`s with stable codes — `FS_NOT_FOUND`, `FS_NOT_TEXT` (binary content), `FS_STALE_VERSION` (changed since observation), `FS_EDIT_NOT_FOUND` or `FS_AMBIGUOUS_EDIT` (no unique literal match), and others — so callers branch on the code, never on message text. A missing target on an edit reports `FS_STALE_VERSION` whether or not the version guard is supplied. Chokidar observes one file or a directory's direct entries through OS events, without polling or recursive watching. Files use a filtered parent-directory watch, so readiness also covers creation of an initially missing file. File watches cover in-place writes, atomic replacement, deletion, and same-path recreation while the parent directory remains. -Move results include `sourceBefore`, the LF source text captured while both paths are locked. Complete and partial moves return it so consumers can record the source basis. - ----- diff --git a/packages/fs/fs-local/README.zh.md b/packages/fs/fs-local/README.zh.md index afcc0d9d50..a2ce35ad06 100644 --- a/packages/fs/fs-local/README.zh.md +++ b/packages/fs/fs-local/README.zh.md @@ -27,8 +27,6 @@ kind: "package-reference" 当组合需要由真实宿主文件系统支撑的 `ctx.fs`、且可以接受进程本地实现时,挂载此后端。常用路径是显式的:加载后端、给出基准目录,然后面向模型的工具(`dsh-tool-fs`)或你自己的插件即可读取、写入和编辑文件。 -原子写入失败保留文件系统错误码:父目录或临时目录创建被拒绝时为 `FS_PERMISSION_DENIED`,其他 Node I/O 失败为 `FS_IO_ERROR`。非 I/O 的意外缺陷仍原样抛出。 - ### 何时选择 在单个进程中进行普通宿主文件访问时,请选择 `fs-local`。会话的写入与编辑必须限制在工作区与临时根目录内时,选择 [`fs-sandbox`](../fs-sandbox/README.zh.md)——它扩展此后端,只增加模式围栏。`config.cwd` 只是解析默认值,不是约束边界:绝对路径与 `..` 都可以逃逸它。 @@ -54,14 +52,12 @@ kind: "package-reference" 完整或流式读取普通 UTF-8 文件、读取有界字节,并列出一层目录。写入、字面量编辑、补丁和删除按目标串行化。移动按字典序锁定两端。`patchText` 在调用 `applyHunks` 前检查版本;`deleteFile` 只删除普通文件。源缺失使用 `FS_STALE_VERSION`,hunk 不匹配使用 `FS_PATCH_MISMATCH`,移动两端相同使用 `FS_SAME_TARGET`。 -补丁与移动的文本在内存中规范化为 LF,写回时还原源文件的行尾风格。结果文本使用 LF。删除基线和被覆盖目标的基线遵循 `diffBasisMaxBytes`;不可用或过大的基线为 null。移动在暂存目标前检查取消,随后完成提交且不再检查取消。源删除失败返回已发布目标及 `sourceDeleted: false`,不会回滚或隐去该写入。 +补丁与移动的文本在内存中规范化为 LF,写回时还原源文件的行尾风格;结果文本使用 LF。删除基线和被覆盖目标的基线遵循 `diffBasisMaxBytes`。移动在暂存目标前检查取消,随后完成两个效果;源删除失败返回已发布目标及 `sourceDeleted: false`。原子写入失败时,目录或临时目录创建被拒绝映射为 `FS_PERMISSION_DENIED`,其他 Node I/O 错误映射为 `FS_IO_ERROR`。 -读取、列出与变更操作的失败是携带稳定错误码的类型化 `FsError`——`FS_NOT_FOUND`、`FS_NOT_TEXT`(二进制内容)、`FS_STALE_VERSION`(自观察以来已变化)、`FS_EDIT_NOT_FOUND` 或 `FS_AMBIGUOUS_EDIT`(无唯一字面量匹配)等——因此调用方依据错误码分支,绝不解析消息文本。编辑遇到缺失目标时,无论是否提供版本防护,都报告 `FS_STALE_VERSION`。 移动仅在源删除抛出类型化 `FsError` 时返回 `sourceFailure`;意外的编程错误使调用拒绝。 +读取、列出与变更操作的失败是携带稳定错误码的类型化 `FsError`——`FS_NOT_FOUND`、`FS_NOT_TEXT`(二进制内容)、`FS_STALE_VERSION`(自观察以来已变化)、`FS_EDIT_NOT_FOUND` 或 `FS_AMBIGUOUS_EDIT`(无唯一字面量匹配)等——因此调用方依据错误码分支,绝不解析消息文本。编辑遇到缺失目标时,无论是否提供版本防护,都报告 `FS_STALE_VERSION`。 Chokidar 通过 OS 事件观察单个文件或目录的直接子项,不使用轮询或递归监听。文件使用筛选到目标的父目录监听,因此就绪时也能接收起初缺失文件的创建事件。父目录保持存在时,文件监听覆盖原地写入、原子替换、删除和同路径重建。 -移动结果的 `sourceBefore` 是提供方在两端锁内取得的 LF 源文本;完整与部分移动均返回它,调用方据此记录源基线。 - ----- diff --git a/packages/fs/fs-observation-policy/README.i18n.yaml b/packages/fs/fs-observation-policy/README.i18n.yaml index 341bef5d98..52458e6c35 100644 --- a/packages/fs/fs-observation-policy/README.i18n.yaml +++ b/packages/fs/fs-observation-policy/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-fs-observation-policy/use-this-package: - en: 0e25541c0231af8e - zh: 4fafb4b32cbc4690 + en: e45098c59fc9b3ba + zh: 2a12cf800bca1ef6 /deepseek-ai-dsh-fs-observation-policy/use-this-package/minimal-composition: en: d85e3b814b43768e zh: 89d3672dbb2f6d30 diff --git a/packages/fs/fs-observation-policy/README.md b/packages/fs/fs-observation-policy/README.md index 9acfc756bc..7f76f910b9 100644 --- a/packages/fs/fs-observation-policy/README.md +++ b/packages/fs/fs-observation-policy/README.md @@ -25,7 +25,7 @@ English | [中文](README.zh.md) ## Use this package -`fs/delete-intent` requires the same present observation as `fs/edit-intent`: unseen sources fail with `FS_NOT_OBSERVED`, confirmed-absent sources fail with `FS_NOT_FOUND`, and present sources supply their observed version. Patches and move sources reuse `fs/edit-intent`; move destinations reuse `fs/write-intent`. Providers do not record observations, and internal preflight reads cannot authorize later mutations. +`fs/delete-intent` requires the same present observation as `fs/edit-intent`: unseen sources fail with `FS_NOT_OBSERVED`, confirmed-absent sources fail with `FS_NOT_FOUND`, and present sources supply their observed version. Patches and move sources reuse `fs/edit-intent`; move destinations reuse `fs/write-intent`. Load this plugin alongside a `ctx.fs` backend and the `dsh-tool-fs` tools when a deployment wants the model to read a file before it can overwrite or edit it. The plugin needs no configuration and injects no service; it only listens for the `fs/*` events the tools dispatch. diff --git a/packages/fs/fs-observation-policy/README.zh.md b/packages/fs/fs-observation-policy/README.zh.md index 383b3a52d5..7c06af8beb 100644 --- a/packages/fs/fs-observation-policy/README.zh.md +++ b/packages/fs/fs-observation-policy/README.zh.md @@ -25,7 +25,7 @@ kind: "package-reference" ## 使用本包 -`fs/delete-intent` 与 `fs/edit-intent` 要求相同的存在观察:未见源以 `FS_NOT_OBSERVED` 失败,确认缺失的源以 `FS_NOT_FOUND` 失败,存在源提供观察到的版本。补丁和移动源复用 `fs/edit-intent`;移动目标复用 `fs/write-intent`。提供方不记录观察,内部预检读取也不能授权后续变更。 +`fs/delete-intent` 与 `fs/edit-intent` 要求相同的存在观察:未见源以 `FS_NOT_OBSERVED` 失败,确认缺失的源以 `FS_NOT_FOUND` 失败,存在源提供观察到的版本。补丁和移动源复用 `fs/edit-intent`;移动目标复用 `fs/write-intent`。 当部署希望模型在覆盖或编辑文件之前先读取该文件时,把本插件与 `ctx.fs` 后端及 `dsh-tool-fs` 工具一起加载。插件无需配置,也不注入任何服务;它只监听工具分派的 `fs/*` 事件。 diff --git a/packages/fs/fs-patch/README.i18n.yaml b/packages/fs/fs-patch/README.i18n.yaml index 3d99efd4cb..ea6734ab28 100644 --- a/packages/fs/fs-patch/README.i18n.yaml +++ b/packages/fs/fs-patch/README.i18n.yaml @@ -3,38 +3,38 @@ # After editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/fs/fs-patch/README.md /: - en: b1423ce8f8e485a4 - zh: 3aa0c738a9e9e644 + en: 80da396fec6cc3d9 + zh: 35d4d8c713d9450c /dsh-fs-patch: en: ad2970c594c3b669 zh: fee247cad74b6191 /dsh-fs-patch/summary: - en: 4df049dd40249119 - zh: 452de8411dbf87ef + en: 614121759671f168 + zh: 7cea460bb8940793 /dsh-fs-patch/table-of-contents: en: 170f124543e5647a zh: c30de483a11a1c09 /dsh-fs-patch/use-this-package: - en: 90b982536638c6e3 - zh: 27ff79a787e43e06 + en: 142b3b023512fc72 + zh: bcfa70b710947eaf /dsh-fs-patch/compatibility: en: 70da6aede0649b32 zh: 2e3ed2964ae0461a /dsh-fs-patch/compatibility/parsing: - en: 3e84b50d523a539b - zh: 38ceb4216753ac79 + en: 06644a9c38f7d569 + zh: 91ca563d4e789087 /dsh-fs-patch/compatibility/matching-and-text-updates: - en: 12cd4980041ddf4e - zh: b5559a4316309ea0 + en: fcd630c121472565 + zh: 35cc062e2f76b195 /dsh-fs-patch/understand-the-implementation: - en: c843cf32d16ab46f - zh: a2cabd9293705cc6 + en: 2ded4719a85a20bc + zh: e04f2614167a6c1c /dsh-fs-patch/further-exploration: - en: 95d956173414da4e - zh: 29c6d0306a4af646 + en: d7d750bd71805956 + zh: 8d33c76f5895d286 /dsh-fs-patch/known-limitations-and-deferred-work: - en: fcc76944c497c7c7 - zh: 307ad5bd3afe8e6f + en: b18ead2791224bc2 + zh: a4a8ab319c072445 /dsh-fs-patch/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: e88aeb617ffb74ea diff --git a/packages/fs/fs-patch/README.md b/packages/fs/fs-patch/README.md index b8c7f7ea10..19e196cc89 100644 --- a/packages/fs/fs-patch/README.md +++ b/packages/fs/fs-patch/README.md @@ -1,5 +1,5 @@ --- -description: "Parse Codex-style patches, apply hunks to text, and collect affected paths without filesystem I/O." +description: "Parse Codex-style patches and apply hunks to text without filesystem I/O." kind: "package-library" --- @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Parse a complete Codex-style patch into ordered file operations with `parsePatch`, then apply update hunks to LF-normalized text with `applyHunks`. Use `collectMutationPaths` to list each source and move destination before capturing file contents. These functions use caller-supplied values without filesystem access. Syntax errors throw with the original input line; matching returns new text or a structured failure. Neither operation authorizes or commits a file change. +Parse a complete Codex-style patch into ordered file operations with `parsePatch`, then apply update hunks to LF-normalized text with `applyHunks`. Both functions work on caller-supplied strings without filesystem access. Syntax errors throw with the original input line; matching returns new text or a structured failure. ## Table of Contents @@ -23,22 +23,21 @@ Parse a complete Codex-style patch into ordered file operations with `parsePatch ## Use this package -Import the library when execution and file capture need the same interpretation of a patch. Operations and hunks contain only JSON-serializable values. Parsing retains repeated operations in input order; path collection deduplicates exact strings in first-seen order, with the source before a move destination. It does not resolve `a` and `./a` to one target. +Operations and hunks contain only JSON-serializable values, and parsing retains repeated paths in input order as unresolved strings. ```ts -import { applyHunks, collectMutationPaths, parsePatch } from '@deepseek-ai/dsh-fs-patch' +import { applyHunks, parsePatch } from '@deepseek-ai/dsh-fs-patch' const operations = parsePatch('*** Begin Patch\n*** Update File: hello.txt\n-hello\n+hi\n*** End Patch') -const paths = collectMutationPaths(operations) // ['hello.txt'] const operation = operations[0] if (operation?.kind === 'update') { const result = applyHunks('hello\n', operation.hunks) // { ok: true, after: 'hi\n' } } ``` -Malformed input throws `PatchParseError` with a one-based `lineNumber` and a corrective `reason`. Locations count leading blank lines and heredoc wrappers in the original input. No partial operation list is returned on failure. +Malformed input throws `PatchParseError` with a one-based `lineNumber` and a corrective `reason`. Locations count leading blank lines and heredoc wrappers in the original input. -`applyHunks` returns `{ ok: true, after }` or `{ ok: false, failure }` without mutating its inputs. A match failure reports the hunk's patch line, `context` or `oldLines` stage, sequential search cursor, and original unmatched pattern. The cursor is the `start` argument; EOF matching overrides it with the tail candidate. A trailing-empty-line retry still reports the original pattern. Failure contains no partial text and does not throw, allowing callers to collect preflight failures across file operations. All result fields are JSON-serializable. +`applyHunks` returns `{ ok: true, after }` or `{ ok: false, failure }` without mutating its inputs. A match failure reports the hunk's patch line, the `context` or `oldLines` stage, the sequential search cursor, and the original unmatched pattern; a trailing-empty-line retry still reports the original pattern. Returning rather than throwing lets callers collect match failures across file operations. ## Compatibility @@ -58,10 +57,10 @@ The reference is [OpenAI Codex rust-v0.158.0](https://github.com/openai/codex/tr | LF/CRLF, outer Unicode whitespace, indented headers | Codex trimming rules use Unicode `White_Space`, including U+0085 and excluding BOM (U+FEFF). Indented markers inside updates remain context. Content whitespace is retained. | | Literal `< ## Understand the implementation @@ -85,9 +84,7 @@ The intentional differences from Codex are ownership and result reporting: line-
Implementation internals — click to expand -The batch parser builds syntax records without a Session or provider dependency. The [text updater](src/apply.ts) locates hunks against the original text before constructing a complete result. The [types](src/types.ts) carry syntax locations and match failures; [path collection](src/index.ts) reads syntax records without reparsing text. The [filesystem reference](../../../docs/subsystems/filesystem.md#patch-syntax-records) owns the public type definitions. - -**Runtime invariant:** No companion is published. This pure library owns no mutable runtime relationship or independent observations that can diverge. +The [parser](src/parse.ts) builds syntax records without a Session or provider dependency. The [text updater](src/apply.ts) locates every hunk against the original text before constructing a complete result. The [types](src/types.ts) carry syntax locations and match failures; the [filesystem reference](../../../docs/subsystems/filesystem.md#patch-syntax-records) owns the public type definitions.
@@ -95,15 +92,14 @@ The batch parser builds syntax records without a Session or provider dependency. ## Further Exploration - [Filesystem package group](../README.md) — providers, policies, and file tools. -- [Apply patch proposal](../../../.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.md) — implementation sequence and Review scope. +- [Apply patch file editing](../../../.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.md) — decision record. ## Known Limitations and Deferred Work -- Text updates implement only `NormalizeToLf`; callers own line-ending conversion. Parsing alone says nothing about whether a hunk matches a file. -- [dsh-fs](../fs/README.md) provides guarded `patchText`, `deleteFile`, and `moveFile`; providers own permissions, freshness, path safety, and line-ending conversion. Tool registration and Review capture belong to consumers. -- The public API accepts complete strings, not streaming deltas. It exposes no Codex workdir or environment-routing metadata. +- Text updates implement only `NormalizeToLf`; callers own line-ending conversion. +- The public API accepts complete strings, not streaming deltas. ### Dev Note diff --git a/packages/fs/fs-patch/README.zh.md b/packages/fs/fs-patch/README.zh.md index c4bd2e2258..adce0354cd 100644 --- a/packages/fs/fs-patch/README.zh.md +++ b/packages/fs/fs-patch/README.zh.md @@ -1,5 +1,5 @@ --- -description: "解析 Codex 风格补丁、将 hunk 应用于文本并收集受影响路径,不执行文件系统 I/O。" +description: "解析 Codex 风格补丁并将 hunk 应用于文本,不执行文件系统 I/O。" kind: "package-library" --- @@ -9,7 +9,7 @@ kind: "package-library" ## 概述 -使用 `parsePatch` 将完整的 Codex 风格补丁解析为有序文件操作,再用 `applyHunks` 将更新 hunk 应用于 LF 规范化文本。使用 `collectMutationPaths` 在采集文件内容前列出每个源路径和移动目标路径。这些函数只处理调用方传入的值,不访问文件系统。语法错误抛出并标明原始输入行号;匹配返回新文本或结构化失败。两类操作都不授权或提交文件修改。 +使用 `parsePatch` 将完整的 Codex 风格补丁解析为有序文件操作,再用 `applyHunks` 将更新 hunk 应用于 LF 规范化文本。两个函数只处理调用方传入的字符串,不访问文件系统。语法错误抛出并标明原始输入行号;匹配返回新文本或结构化失败。 ## 目录 @@ -23,22 +23,21 @@ kind: "package-library" ## 使用此包 -执行与文件采集需要以相同规则解释补丁时,导入此库。操作和 hunk(修改块)只包含可 JSON 序列化的值。解析保留重复操作及其输入顺序;路径收集按首次出现顺序对原字符串去重,移动时先列源路径再列目标路径。它不会将 `a` 和 `./a` 解析为同一目标。 +操作和 hunk(修改块)只包含可 JSON 序列化的值;解析按输入顺序保留重复路径,路径保持为未解析的字符串。 ```ts -import { applyHunks, collectMutationPaths, parsePatch } from '@deepseek-ai/dsh-fs-patch' +import { applyHunks, parsePatch } from '@deepseek-ai/dsh-fs-patch' const operations = parsePatch('*** Begin Patch\n*** Update File: hello.txt\n-hello\n+hi\n*** End Patch') -const paths = collectMutationPaths(operations) // ['hello.txt'] const operation = operations[0] if (operation?.kind === 'update') { const result = applyHunks('hello\n', operation.hunks) // { ok: true, after: 'hi\n' } } ``` -非法输入抛出 `PatchParseError`,包含从 1 开始的 `lineNumber` 和说明如何修正的 `reason`。位置计入原始输入中的前导空行和 heredoc 包装。失败时不会返回部分操作列表。 +非法输入抛出 `PatchParseError`,包含从 1 开始的 `lineNumber` 和说明如何修正的 `reason`。位置计入原始输入中的前导空行和 heredoc 包装。 -`applyHunks` 返回 `{ ok: true, after }` 或 `{ ok: false, failure }`,不修改输入。匹配失败报告 hunk 在补丁中的行号、`context` 或 `oldLines` 阶段、顺序搜索游标和原始未匹配模式。游标是传入的 `start`;EOF 匹配将其替换为尾部候选位置。移除末尾空行重试后仍报告原始模式。失败不包含部分文本,也不抛异常,调用方可据此收集多个文件操作的预检失败。所有结果字段都可 JSON 序列化。 +`applyHunks` 返回 `{ ok: true, after }` 或 `{ ok: false, failure }`,不修改输入。匹配失败报告 hunk 在补丁中的行号、`context` 或 `oldLines` 阶段、顺序搜索游标和原始未匹配模式;移除末尾空行重试后仍报告原始模式。以返回值而非异常报告失败,便于调用方收集多个文件操作的匹配失败。 ## 兼容性 @@ -58,10 +57,10 @@ if (operation?.kind === 'update') { | LF/CRLF、外围 Unicode 空白、缩进的文件头 | Codex 裁剪规则使用 Unicode `White_Space`,包含 U+0085,不包含 BOM(U+FEFF)。更新内部缩进的标记仍作为上下文。保留内容空白。 | | 字面量 `< ## 理解实现 @@ -85,9 +84,7 @@ if (operation?.kind === 'update') {
实现内部机制——点击展开 -批量解析器构建语法记录,不依赖 Session 或提供方。[文本更新器](src/apply.ts) 先在原文本中定位 hunk,再构建完整结果。[类型](src/types.ts) 携带语法位置与匹配失败;[路径收集](src/index.ts) 读取语法记录,不重新解析文本。[文件系统参考](../../../docs/subsystems/filesystem.zh.md#patch-syntax-records) 维护公开类型定义。 - -**运行时不变量:** 不发布 companion。此纯库不拥有可变运行时关系或可能发生偏离的独立观测。 +[解析器](src/parse.ts) 构建语法记录,不依赖 Session 或提供方。[文本更新器](src/apply.ts) 先在原文本中定位每个 hunk,再构建完整结果。[类型](src/types.ts) 携带语法位置与匹配失败;[文件系统参考](../../../docs/subsystems/filesystem.zh.md#patch-syntax-records) 维护公开类型定义。
@@ -95,15 +92,14 @@ if (operation?.kind === 'update') { ## 进一步探索 - [文件系统包组](../README.zh.md)——提供方、策略和文件工具。 -- [Apply patch 提案](../../../.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.zh.md)——实现顺序与 Review 范围。 +- [Apply patch 文件编辑](../../../.agents/notes/implemented/feature/2026-09-28-apply-patch-file-editing.zh.md)——决策记录。 ## 已知限制与后续工作 -- 文本更新只实现 `NormalizeToLf`;调用方负责行尾转换。仅解析成功不能说明 hunk 能否匹配文件。 -- [dsh-fs](../fs/README.zh.md) 提供受防护的 `patchText`、`deleteFile` 和 `moveFile`;提供方负责权限、新鲜度、路径安全和行尾转换。工具注册与 Review 采集属于消费方。 -- 公开 API 接受完整字符串,不接收流式增量。不暴露 Codex workdir 或环境路由元数据。 +- 文本更新只实现 `NormalizeToLf`;调用方负责行尾转换。 +- 公开 API 接受完整字符串,不接收流式增量。 ### 开发备注 diff --git a/packages/fs/fs-patch/src/index.ts b/packages/fs/fs-patch/src/index.ts index d341770051..8fe23a4d05 100644 --- a/packages/fs/fs-patch/src/index.ts +++ b/packages/fs/fs-patch/src/index.ts @@ -1,21 +1,5 @@ -/** Pure patch parsing, text application, and path enumeration, independent of filesystem providers. */ - -import type { PatchOperation } from './types.ts' +/** Pure patch parsing and text application, independent of filesystem providers. */ export { parsePatch, PatchParseError } from './parse.ts' export { applyHunks } from './apply.ts' export type { ApplyHunksResult, HunkApplyFailure, PatchHunk, PatchOperation } from './types.ts' - -/** - * Collect source paths and move destinations in first-seen order for pre-mutation capture. - * @param operations - parsed operations; repeated operations remain intact. - * @returns unique path strings, without resolving aliases or applying filesystem policy. - */ -export function collectMutationPaths(operations: readonly PatchOperation[]): string[] { - const paths = new Set() - for (const operation of operations) { - paths.add(operation.path) - if (operation.kind === 'update' && operation.moveTo !== undefined) paths.add(operation.moveTo) - } - return [...paths] -} diff --git a/packages/fs/fs-patch/src/parse.ts b/packages/fs/fs-patch/src/parse.ts index 5f28453a6e..8aa513289e 100644 --- a/packages/fs/fs-patch/src/parse.ts +++ b/packages/fs/fs-patch/src/parse.ts @@ -83,7 +83,7 @@ function ensureUpdate(operation: PatchOperation | undefined, lineNumber: number) } function newHunk(lineNumber: number): PatchHunk { - return { lineNumber, oldLines: [], newLines: [], contextLineIndices: [], isEndOfFile: false } + return { lineNumber, oldLines: [], newLines: [], isEndOfFile: false } } function readUpdate(operation: Extract, line: InputLine): void { @@ -126,7 +126,6 @@ function readUpdate(operation: Extract, line if (prefix === '+') hunk.newLines.push(content) else if (prefix === '-') hunk.oldLines.push(content) else { - hunk.contextLineIndices.push([hunk.oldLines.length, hunk.newLines.length]) hunk.oldLines.push(content) hunk.newLines.push(content) } diff --git a/packages/fs/fs-patch/src/types.ts b/packages/fs/fs-patch/src/types.ts index 68b2120fe2..bc58ac8418 100644 --- a/packages/fs/fs-patch/src/types.ts +++ b/packages/fs/fs-patch/src/types.ts @@ -1,6 +1,6 @@ /** Serializable patch syntax and text-application results; paths remain unresolved. */ -/** One contiguous update, with zero-based indices identifying explicit context lines. */ +/** One contiguous update. */ export interface PatchHunk { /** One-based input line containing the marker or first implicit hunk line. */ lineNumber: number @@ -10,8 +10,6 @@ export interface PatchHunk { oldLines: string[] /** Context and added lines, without patch prefixes or line endings. */ newLines: string[] - /** Corresponding old/new offsets for context; identical remove/add pairs are excluded. */ - contextLineIndices: [number, number][] /** Whether `*** End of File` follows this hunk. */ isEndOfFile: boolean } diff --git a/packages/fs/fs-patch/tests/parse.spec.ts b/packages/fs/fs-patch/tests/parse.spec.ts index 5aeb1d1374..5e10ffbf7e 100644 --- a/packages/fs/fs-patch/tests/parse.spec.ts +++ b/packages/fs/fs-patch/tests/parse.spec.ts @@ -1,14 +1,14 @@ /** Parser compatibility cases adapted from OpenAI Codex rust-v0.158.0 (Apache-2.0; see ../LICENSE). */ import { describe, expect, it } from 'vitest' -import { collectMutationPaths, parsePatch, PatchParseError } from '../src/index.ts' +import { parsePatch, PatchParseError } from '../src/index.ts' function patch(...lines: string[]): string { return ['*** Begin Patch', ...lines, '*** End Patch'].join('\n') } describe('parsePatch', () => { - it('preserves file order, repeated paths, move endpoints, and explicit context indices', () => { + it('preserves file order, repeated paths, and move endpoints', () => { const input = patch( '*** Add File: dir/a.txt', '+hello', '+', '*** Update File: dir/a.txt', '*** Move to: dir/b.txt', @@ -25,11 +25,10 @@ describe('parsePatch', () => { hunks: [ { lineNumber: 7, context: 'hello', oldLines: ['hello', 'same', 'same'], - newLines: ['hi', 'there', 'same', 'same'], contextLineIndices: [[1, 2]], isEndOfFile: false, + newLines: ['hi', 'there', 'same', 'same'], isEndOfFile: false, }, { - lineNumber: 14, oldLines: ['last'], newLines: ['last', 'tail'], - contextLineIndices: [[0, 0]], isEndOfFile: true, + lineNumber: 14, oldLines: ['last'], newLines: ['last', 'tail'], isEndOfFile: true, }, ], }, @@ -37,8 +36,6 @@ describe('parsePatch', () => { { kind: 'add', path: 'dir/a.txt', lineNumber: 19, content: '' }, ]) expect(JSON.parse(JSON.stringify(operations))).toEqual(operations) - expect(collectMutationPaths(operations)).toEqual(['dir/a.txt', 'dir/b.txt', '/tmp/obsolete']) - expect(operations).toHaveLength(4) }) it('accepts an empty patch and empty creation', () => { @@ -70,12 +67,10 @@ describe('parsePatch', () => { kind: 'update', path: 'a', lineNumber: 2, hunks: [ { - lineNumber: 3, oldLines: ['a\t ', '', '\t'], newLines: ['b ', '', '\t'], - contextLineIndices: [[1, 1], [2, 2]], isEndOfFile: false, + lineNumber: 3, oldLines: ['a\t ', '', '\t'], newLines: ['b ', '', '\t'], isEndOfFile: false, }, { - lineNumber: 7, context: 'anchor', oldLines: [], newLines: ['c'], - contextLineIndices: [], isEndOfFile: false, + lineNumber: 7, context: 'anchor', oldLines: [], newLines: ['c'], isEndOfFile: false, }, ], }, @@ -108,8 +103,7 @@ describe('parsePatch', () => { { kind: 'update', path: 'a', lineNumber: 2, moveTo: 'b', hunks: [{ - lineNumber: 4, context: 'anchor\uFEFF', oldLines: [], newLines: [content], - contextLineIndices: [], isEndOfFile: false, + lineNumber: 4, context: 'anchor\uFEFF', oldLines: [], newLines: [content], isEndOfFile: false, }], }, ]) @@ -125,8 +119,7 @@ describe('parsePatch', () => { { kind: 'update', path: 'b', lineNumber: 4, hunks: [{ - lineNumber: 5, oldLines: [content], newLines: [`${content}y`], - contextLineIndices: [], isEndOfFile: false, + lineNumber: 5, oldLines: [content], newLines: [`${content}y`], isEndOfFile: false, }], }, ]) @@ -210,22 +203,3 @@ describe('parsePatch', () => { ) }) }) - -describe('collectMutationPaths', () => { - it('returns an empty list for no operations', () => { - expect(collectMutationPaths([])).toEqual([]) - }) - - it('deduplicates exact spellings without resolving aliases or changing operations', () => { - const operations = parsePatch(patch( - '*** Update File: a', '*** Move to: b', '+x', - '*** Update File: b', '*** Move to: b', '+y', - '*** Update File: ./a', '+z', - '*** Delete File: a', '*** Add File: ../a', - '*** Add File: C:\\dir\\a', '*** Delete File: /a', - )) - const before = JSON.stringify(operations) - expect(collectMutationPaths(operations)).toEqual(['a', 'b', './a', '../a', 'C:\\dir\\a', '/a']) - expect(JSON.stringify(operations)).toBe(before) - }) -}) diff --git a/packages/fs/fs-sandbox/README.i18n.yaml b/packages/fs/fs-sandbox/README.i18n.yaml index 1dd0aa4905..cc7c3fb713 100644 --- a/packages/fs/fs-sandbox/README.i18n.yaml +++ b/packages/fs/fs-sandbox/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 09388d293f9be9cb /deepseek-ai-dsh-fs-sandbox/use-this-package: - en: f80886b50bd466e2 - zh: a8b0e685f603610a + en: c159e3686e5b036d + zh: 2ff0d0e7ce62cb30 /deepseek-ai-dsh-fs-sandbox/use-this-package/minimal-composition: en: a954957753339bbf zh: c6633e86de8b2707 diff --git a/packages/fs/fs-sandbox/README.md b/packages/fs/fs-sandbox/README.md index 8eb8038fd8..457d8f60a5 100644 --- a/packages/fs/fs-sandbox/README.md +++ b/packages/fs/fs-sandbox/README.md @@ -25,7 +25,7 @@ English | [中文](README.zh.md) ## Use this package -The fence covers `writeText`, `editText`, `patchText`, `deleteFile`, and `moveFile`. A move checks both source and destination before either effect and delegates with both freshly checked targets. Any denied path leaves both files untouched. +The fence covers `writeText`, `editText`, `patchText`, `deleteFile`, and `moveFile`. A move checks both source and destination before either effect, so a denied path leaves both files untouched. Mount this backend instead of `fs-local` when the model's file writes and edits must be confined by the session's sandbox mode, while reads stay unconfined. The fence applies per call: the tool layer resolves the calling session's mode and workspace root into the same policy the bash runner receives, so the filesystem and shell families never confine to different roots. diff --git a/packages/fs/fs-sandbox/README.zh.md b/packages/fs/fs-sandbox/README.zh.md index 832940dcf4..ca5c6c9bc7 100644 --- a/packages/fs/fs-sandbox/README.zh.md +++ b/packages/fs/fs-sandbox/README.zh.md @@ -25,7 +25,7 @@ kind: "package-reference" ## 使用本包 -围栏覆盖 `writeText`、`editText`、`patchText`、`deleteFile` 和 `moveFile`。移动在任何效果发生前检查源与目标,并用两个重新检查的目标委托执行。任一路径被拒都会使两端文件保持不变。 +围栏覆盖 `writeText`、`editText`、`patchText`、`deleteFile` 和 `moveFile`。移动在任何效果发生前检查源与目标,因此任一路径被拒都会使两端文件保持不变。 当模型的文件写入与编辑必须受会话沙箱模式约束、而读取保持不受约束时,挂载此后端以替代 `fs-local`。围栏按调用生效:工具层把调用会话的模式与工作区根目录解析为与 bash runner 收到的相同策略,因此文件系统与 shell 两个能力族绝不会约束到不同根目录。 diff --git a/packages/fs/fs-sandbox/tests/fs-sandbox.spec.ts b/packages/fs/fs-sandbox/tests/fs-sandbox.spec.ts index d64d75d297..75c58621df 100644 --- a/packages/fs/fs-sandbox/tests/fs-sandbox.spec.ts +++ b/packages/fs/fs-sandbox/tests/fs-sandbox.spec.ts @@ -251,7 +251,7 @@ describe('patch, delete, and move fences', () => { await writeFile(outsidePath, 'outside\n') const source = await target(sourcePath) const out = await target(outsidePath) - const patch = [{ lineNumber: 3, oldLines: ['source'], newLines: ['patched'], contextLineIndices: [], isEndOfFile: false }] + const patch = [{ lineNumber: 3, oldLines: ['source'], newLines: ['patched'], isEndOfFile: false }] if (mode === 'read-only') { await expect(fs.patchText(source, patch)).rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' }) await expect(fs.deleteFile(source)).rejects.toMatchObject({ code: 'FS_SANDBOX_DENIED' }) diff --git a/packages/fs/fs/README.i18n.yaml b/packages/fs/fs/README.i18n.yaml index 64e709114e..7e3bfe3472 100644 --- a/packages/fs/fs/README.i18n.yaml +++ b/packages/fs/fs/README.i18n.yaml @@ -21,8 +21,8 @@ en: 8e2f904a21ba4dfb zh: 76d9ce7503c84a5c /deepseek-ai-dsh-fs/use-this-package/what-the-service-lets-you-do: - en: 6b5f12bf92454cbc - zh: ee75b60c29302062 + en: 637fc69ab6f6d19f + zh: 0fceafe5b38a06a1 /deepseek-ai-dsh-fs/understand-the-implementation: en: 7e13d669ecc96d59 zh: b5fe7ae65812d926 @@ -36,8 +36,8 @@ en: 3cefede64907731c zh: 529ef3851c2fb5c1 /deepseek-ai-dsh-fs/understand-the-implementation/the-fs-policy-events: - en: b85d03be3b666d7a - zh: 4c1f2c102cdc49a5 + en: 9addf6fbfab04db1 + zh: b29942be20c13558 /deepseek-ai-dsh-fs/understand-the-implementation/invariants: en: 15b98bc9800b6eae zh: 434d9dd05fa1a332 @@ -51,8 +51,8 @@ en: 40294c1c31b74824 zh: 0857e9b4dd363e0a /deepseek-ai-dsh-fs/known-limitations-and-deferred-work: - en: 18948b9e176668ca - zh: 0ab4fb934c8c6945 + en: 3e0736adfd6f54a5 + zh: 6b043891432eed82 /deepseek-ai-dsh-fs/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/fs/fs/README.md b/packages/fs/fs/README.md index 9ff9a6a663..949a453d2c 100644 --- a/packages/fs/fs/README.md +++ b/packages/fs/fs/README.md @@ -33,14 +33,10 @@ Pick [`fs-local`](../fs-local/README.md) for ordinary host files or [`fs-sandbox ### What the service lets you do -Through `ctx.fs`, resolve paths, read text or bounded bytes, list direct children, and perform guarded mutations. `patchText` applies parsed hunks under the same lock as its version check. `deleteFile` removes a regular file. `moveFile` locks both targets, optionally patches the source text, publishes the destination, then removes the source. Guards are optional: omission uses the current state unconditionally. Failures carry stable `FsError` codes; branch on the code instead of parsing messages. - -`FS_ERROR_CODES` is the runtime list shared by `FsErrorCode` and transport validators. Import it when constructing an error-code schema. +Through `ctx.fs`, resolve paths, read text or bounded bytes, list direct children, and perform guarded mutations. `patchText` applies parsed hunks under the same lock as its version check. `deleteFile` removes a regular file. `moveFile` locks both targets, optionally patches the source text, publishes the destination, then removes the source. Guards are optional: omission mutates the current state unconditionally. Failures carry stable `FsError` codes from the runtime `FS_ERROR_CODES` list, which transport validators reuse. `watch(target, changed, signal)` reports invalidations for one file or a directory's direct entries. It resolves once observation is ready with an asynchronous close function that the caller must await. The signal cancels initialization; unsupported providers reject without polling. -Move results include `sourceBefore`, the LF source text captured while both paths are locked. Complete and partial moves return it so consumers can record the source basis. - ----- @@ -73,7 +69,7 @@ Every operation uses a target from `resolve(path, { cwd })`; aliases share an op ### The `fs/*` policy events -The package declares four events shared by consumers and observation policy. Intent waterfalls have one decision owner; observation recording is synchronous and carries an opaque actor. Callers dispatch the applicable intent before a mutation and record only completed effects afterwards. Internal preflight reads do not count as model observations. +The package declares four events shared by consumers and observation policy. Intent waterfalls have one decision owner; observation recording is synchronous and carries an opaque actor. Callers dispatch the applicable intent before a mutation and record completed effects afterwards. | Event | Use | |---|---| @@ -122,10 +118,9 @@ No direct invalidation; the named consumer owns any request-prefix changes. These limits define when the contract is a poor fit or needs special operational care. They are current package constraints, not a general filesystem comparison or a task backlog. -- Literal edits, patches, and moves require UTF-8 text; deletion also accepts binary regular files and may return `before: null`. Whole-file writes accept text and may replace a prior binary file. -- Moves are not cross-path transactions. A resolved `sourceDeleted: false` result means the destination is published and the source remains; callers must report both facts. -- Resolved targets follow symlinks. Callers that reject symlink sources must use `lstat` before `resolve`; copying and recursive directory operations are outside this API. -- External `FileSystem` providers follow the [guarded-mutation migration guide](../../../docs/upgrade-guide/v0.2.1-alpha.4/guarded-filesystem-mutations/guide.md). +- Literal edits, patches, and moves require UTF-8 text; deletion also accepts binary regular files and returns `before: null` for them. +- Moves are two effects, not a transaction: `sourceDeleted: false` means the destination is published and the source remains. +- Resolved targets follow symlinks; callers that reject symlink sources use `lstat` before `resolve`. Copying and recursive directory operations are outside this API. - **No I/O deadline** — the seam arms no timeout; cancellation is a best-effort optional `AbortSignal` per primitive ([fs family stance](../README.md)). - **Resolve-then-operate costs a remote backend two round-trips per tool call** — folding or caching resolution is left to such a backend. diff --git a/packages/fs/fs/README.zh.md b/packages/fs/fs/README.zh.md index 9141629c75..90118d2ecf 100644 --- a/packages/fs/fs/README.zh.md +++ b/packages/fs/fs/README.zh.md @@ -33,14 +33,10 @@ kind: "package-reference" ### 服务能做什么 -通过 `ctx.fs` 可解析路径、读取文本或有界字节、列出直接子项并执行受防护变更。`patchText` 在版本检查的同一把锁内应用解析后的 hunk。`deleteFile` 删除普通文件。`moveFile` 锁定两端,按需修改源文本,发布目标后再删除源。防护是可选的:省略时无条件使用当前状态。失败携带稳定的 `FsError` 错误码;应按代码分支,不解析消息。 - -`FS_ERROR_CODES` 是 `FsErrorCode` 与传输校验器共用的运行时列表。构造错误码 schema 时应导入该常量。 +通过 `ctx.fs` 可解析路径、读取文本或有界字节、列出直接子项并执行受防护变更。`patchText` 在版本检查的同一把锁内应用解析后的 hunk。`deleteFile` 删除普通文件。`moveFile` 锁定两端,按需修改源文本,发布目标后再删除源。防护是可选的:省略时无条件修改当前状态。失败携带运行时列表 `FS_ERROR_CODES` 中的稳定 `FsError` 错误码,传输校验器复用该列表。 `watch(target, changed, signal)` 报告单个文件或目录直接子项的失效通知。观察就绪后,它返回调用方必须等待完成的异步关闭函数。signal 取消初始化;不支持监听的提供方直接拒绝,不使用轮询。 -移动结果的 `sourceBefore` 是提供方在两端锁内取得的 LF 源文本;完整与部分移动均返回它,调用方据此记录源基线。 - ----- @@ -73,7 +69,7 @@ kind: "package-reference" ### `fs/*` 策略事件 -本包声明四个由消费方与观察策略共享的事件。意图 waterfall 由一个决策方负责;观察记录同步执行并携带不透明参与者。调用方在变更前分派对应意图,之后只记录已完成的效果。内部预检读取不算模型观察。 +本包声明四个由消费方与观察策略共享的事件。意图 waterfall 由一个决策方负责;观察记录同步执行并携带不透明参与者。调用方在变更前分派对应意图,之后记录已完成的效果。 | 事件 | 用途 | |---|---| @@ -122,10 +118,9 @@ kind: "package-reference" 这些限制说明该约定何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是通用文件系统对比或任务积压。 -- 字面量编辑、补丁与移动要求 UTF-8 文本;删除也接受二进制普通文件,此时可返回 `before: null`。整文件写入接受文本,也可替换原有二进制文件。 -- 移动不是跨路径事务。返回 `sourceDeleted: false` 表示目标已发布、源仍存在;调用方必须报告这两个事实。 -- 已解析目标会跟随符号链接。拒绝符号链接源的调用方必须在 `resolve` 前使用 `lstat`;复制和递归目录操作不在此 API 范围内。 -- 外部 `FileSystem` 提供方遵循[受保护修改迁移指南](../../../docs/upgrade-guide/v0.2.1-alpha.4/guarded-filesystem-mutations/guide.zh.md)。 +- 字面量编辑、补丁与移动要求 UTF-8 文本;删除也接受二进制普通文件,并对其返回 `before: null`。 +- 移动是两个效果而非一个事务:`sourceDeleted: false` 表示目标已发布、源仍存在。 +- 已解析目标会跟随符号链接;拒绝符号链接源的调用方在 `resolve` 前使用 `lstat`。复制和递归目录操作不在此 API 范围内。 - **没有 I/O deadline**:该 seam 不启动超时;取消只是每个原语上尽力而为的可选 `AbortSignal`(见[fs 能力族立场](../README.zh.md))。 - **先解析后操作使远程后端每次工具调用需要两次往返**:折叠或缓存解析由这种后端自行决定。 diff --git a/packages/fs/fs/src/index.ts b/packages/fs/fs/src/index.ts index 2eda70a2bd..3c0566251b 100644 --- a/packages/fs/fs/src/index.ts +++ b/packages/fs/fs/src/index.ts @@ -309,16 +309,17 @@ export abstract class FileSystem extends Service { ): Promise /** - * Apply hunks under the version guard and mutation lock. Missing targets and - * stale versions report FS_STALE_VERSION; unmatched hunks report FS_PATCH_MISMATCH. - * Callers obtain expected through fs/edit-intent and record present(version) - * through fs/observed on success. Internal preflight reads are not model observations. - * @param target - resolved regular UTF-8 file to update. - * @param hunks - ordered parsed hunks applied to LF-normalized content. - * @param expected - observed version; omit to patch the current content unconditionally. - * @param signal - aborts before atomic publication. - * @param sandboxPolicy - per-call confinement; omit to use the provider default. - * @returns the published version and LF-normalized before/after text. + * Atomically apply parsed hunks to the LF-normalized text and restore the + * stored line-ending style. A missing or stale target reports + * `FS_STALE_VERSION`; an unmatched hunk reports `FS_PATCH_MISMATCH`. + * @param target - the resolved regular file to patch. + * @param hunks - ordered hunks from `parsePatch`. + * @param expected - the version guard; omit for an unconditional patch. + * @param signal - aborts before atomic publication takes effect. + * @param sandboxPolicy - the per-call mode and workspace root this patch runs + * under; a sandboxing backend fences the patch by it, the bare backend + * ignores it. Omit to leave the backend its own default. + * @returns the outcome, including the version the patch produced. */ abstract patchText( target: FsTarget, @@ -329,15 +330,16 @@ export abstract class FileSystem extends Service { ): Promise /** - * Remove a resolved regular file. Missing/stale targets report FS_STALE_VERSION; - * other types report FS_NOT_REGULAR_FILE. Obtain expected through fs/delete-intent - * and record absent through fs/observed after successful removal. - * Resolution follows symlinks: callers rejecting symlink sources must lstat before resolve. - * @param target - resolved file to remove, including the real target of a symlink. - * @param expected - observed version; omit for unconditional removal of the current file. - * @param signal - aborts before removal; successful removal is not rolled back. - * @param sandboxPolicy - per-call confinement; omit to use the provider default. - * @returns the bounded LF-normalized removed content, or null when no basis is available. + * Remove a regular file. A missing or stale target reports `FS_STALE_VERSION`; + * a directory or other non-file reports `FS_NOT_REGULAR_FILE`. Resolution has + * already followed symlinks, so the target is the real file. + * @param target - the resolved file to remove. + * @param expected - the version guard; omit for an unconditional removal. + * @param signal - aborts before removal. + * @param sandboxPolicy - the per-call mode and workspace root this removal + * runs under; a sandboxing backend fences it, the bare backend ignores it. + * Omit to leave the backend its own default. + * @returns the bounded LF-normalized removed text, or null when no basis is available. */ abstract deleteFile( target: FsTarget, @@ -347,22 +349,20 @@ export abstract class FileSystem extends Service { ): Promise /** - * Publish source text at a destination, optionally patching it, then remove the source. - * Both paths remain locked in canonical order. FS_SAME_TARGET rejects identical keys; - * source guards match patchText, destination guards match writeText. No cross-path - * transaction is promised: destination publication with the source retained is a final outcome. - * Obtain source/destination guards through fs/edit-intent and fs/write-intent. On - * resolution, record destination present(destination.version); record source absent - * only when sourceDeleted is true. Providers do not emit these observations. - * Resolution follows symlinks: callers rejecting symlink sources must lstat before resolve. - * @param source - resolved regular UTF-8 source; its line-ending style and mode are preserved. - * @param destination - resolved destination in the same provider, distinct from source. - * @param options - optional hunks and source/destination guards; omitted fields mean - * no text patch and unconditional source/destination mutations respectively. - * @param signal - checked before destination staging; once staging starts, cancellation - * does not interrupt the local commit. Remote cancellation can leave the outcome unknown. - * @param sandboxPolicy - confinement for both paths; omit to use the provider default. - * @returns locked LF sourceBefore, destination outcome, and source-removal status; failures before publication reject. + * Publish the source text, optionally patched, at the destination, then remove + * the source. Both targets stay locked in canonical order. Identical targets + * report `FS_SAME_TARGET`; the source guard follows `patchText` and the + * destination guard follows `writeText`. The two effects are separate: a + * published destination with a retained source is a final outcome reported + * through `sourceDeleted: false`. + * @param source - the resolved regular source; its line-ending style and mode carry over. + * @param destination - the resolved destination within the same provider. + * @param options - optional hunks and the source/destination guards; an omitted guard is unconditional. + * @param signal - checked before destination staging; the commit then completes both effects. + * @param sandboxPolicy - the per-call mode and workspace root both paths run + * under; a sandboxing backend fences them, the bare backend ignores it. + * Omit to leave the backend its own default. + * @returns the locked LF source text, the destination outcome, and the source-removal status. */ abstract moveFile( source: FsTarget, @@ -375,7 +375,6 @@ export abstract class FileSystem extends Service { signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy, ): Promise - } export default FileSystem diff --git a/packages/fs/fs/tests/service.spec.ts b/packages/fs/fs/tests/service.spec.ts index 3030ae95ca..0fccd4edc6 100644 --- a/packages/fs/fs/tests/service.spec.ts +++ b/packages/fs/fs/tests/service.spec.ts @@ -7,7 +7,7 @@ import { describe, expect, it } from 'vitest' import { Context } from '@deepseek-ai/cordis' -import { FS_ERROR_CODES, FileSystem, FsError, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs' +import { FileSystem, FsError, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs' import type { FsDirEntry, FsEditOutcome, @@ -222,10 +222,3 @@ it('exposes guarded patch, delete, and move on the provider interface', async () expect(new FsError('same target', 'FS_SAME_TARGET').code).toBe('FS_SAME_TARGET') } finally { await fiber.dispose() } }) - -it('exports the shared filesystem error-code vocabulary', () => { - expect(FS_ERROR_CODES).toHaveLength(15) - expect(new Set(FS_ERROR_CODES).size).toBe(FS_ERROR_CODES.length) - expect(FS_ERROR_CODES).toContain('FS_PATCH_MISMATCH') - expect(FS_ERROR_CODES).toContain('FS_SAME_TARGET') -}) diff --git a/packages/fs/tool-fs/README.i18n.yaml b/packages/fs/tool-fs/README.i18n.yaml index 8996e94289..17660d520b 100644 --- a/packages/fs/tool-fs/README.i18n.yaml +++ b/packages/fs/tool-fs/README.i18n.yaml @@ -9,8 +9,8 @@ en: fc8f28deebe5fa6b zh: 738c6ad45034da88 /deepseek-ai-dsh-tool-fs/summary: - en: 3294a0c897099b57 - zh: e04fcb03a1def45b + en: c324e1f6369b38b9 + zh: 5e4fd0d5f64b0746 /deepseek-ai-dsh-tool-fs/table-of-contents: en: d152484eb41ac6b4 zh: 09388d293f9be9cb @@ -24,14 +24,14 @@ en: 0e0cb1337b2a8466 zh: 99be61e9f7124182 /deepseek-ai-dsh-tool-fs/use-this-package/configuration: - en: 98979987316862c6 - zh: 3bda0bcda2862eff + en: c4817cc85c49154f + zh: 52ef75e02f244a15 /deepseek-ai-dsh-tool-fs/use-this-package/policy-and-sandbox-behavior: en: 53d4ac2db089ef39 zh: 214ea051ed56f500 /deepseek-ai-dsh-tool-fs/use-this-package/failures-and-recovery: - en: e80b0f62e95b4379 - zh: 4f3cfa15d20d80b7 + en: 448f8afbbf3a90d1 + zh: e87db5c1868cc94e /deepseek-ai-dsh-tool-fs/understand-the-implementation: en: e30f7038c28e93f8 zh: a3868dbd90367b13 @@ -39,14 +39,14 @@ en: 1f4d7213aa3d0d60 zh: aa6573f96c8b5220 /deepseek-ai-dsh-tool-fs/understand-the-implementation/source-map: - en: 50fc5ec596af7755 - zh: d3b2ea5a73b329a9 + en: 7cda5167ece32686 + zh: 30a00318f70bf5d5 /deepseek-ai-dsh-tool-fs/understand-the-implementation/per-tool-flow: - en: 868fe62516f8c412 - zh: 6026edeb33542eb9 + en: 9e75dd547b41ce02 + zh: 62184c725aaf862c /deepseek-ai-dsh-tool-fs/understand-the-implementation/observation-and-concurrency: - en: 1184cf39ebb68529 - zh: 4a15bf9e3a9dd1e9 + en: 6a54598ec69f6f20 + zh: 27d31d22ee926622 /deepseek-ai-dsh-tool-fs/further-exploration: en: 485bc55482f576bc zh: fc5fa55b41098bd0 @@ -138,8 +138,8 @@ en: e24083366d909456 zh: 003e7533d417bfe5 /deepseek-ai-dsh-tool-fs/model-experience/apply-patch/what-the-model-sees: - en: b808c52fd766ef36 - zh: 6f8df91309989fa0 + en: 4370cdafba12b67e + zh: 684e3237f081c1f1 /deepseek-ai-dsh-tool-fs/model-experience/apply-patch/what-the-model-sees/system-guidance: en: 3e353846d8e13cdf zh: 3d93eaadc65898b3 @@ -147,14 +147,14 @@ en: 37d9e89be68e7725 zh: fd09eca8f63c7c58 /deepseek-ai-dsh-tool-fs/model-experience/apply-patch/token-effect: - en: f8345fdddccd10ec - zh: 252b5d4cb20a723b + en: 886e012dd66f2548 + zh: a22ea38a243f82db /deepseek-ai-dsh-tool-fs/model-experience/apply-patch/kv-cache-effect: - en: f50c2c9526598702 - zh: bf5011dc7c4b1fd6 + en: d3a98306460d7f9f + zh: 59836b4f59b8214a /deepseek-ai-dsh-tool-fs/known-limitations-and-deferred-work: - en: b7c956a5f22ed9e3 - zh: bd68af719e4bf154 + en: 61600821252cf29c + zh: 65af2a8ca0515906 /deepseek-ai-dsh-tool-fs/known-limitations-and-deferred-work/dev-note: en: e67f633dc6fe7773 zh: 3bb0c20c93f16f98 diff --git a/packages/fs/tool-fs/README.md b/packages/fs/tool-fs/README.md index 48853b2af1..d2a4f90d4e 100644 --- a/packages/fs/tool-fs/README.md +++ b/packages/fs/tool-fs/README.md @@ -9,7 +9,7 @@ English | [中文](README.zh.md) ## Summary -Use `dsh-tool-fs` to let a model read UTF-8 files with line numbers, read supported images, create or atomically replace files, apply targeted literal edits, and optionally apply Codex-style multi-file patches. Results are capped, and failures provide stable error codes and recovery instructions. Add `dsh-fs-observation-policy` when writes and edits must follow a successful read; without it, mutations remain atomic but are unconditional. Image reads require durable attachment storage and an image-capable routed model. Choose the sibling discovery package for glob or grep searches. +Use `dsh-tool-fs` to let a model read UTF-8 files with line numbers, read supported images, create or atomically replace files, apply targeted literal edits, and apply Codex-style multi-file patches. Results are capped, and failures provide stable error codes and recovery instructions. Add `dsh-fs-observation-policy` when writes and edits must follow a successful read; without it, mutations remain atomic but are unconditional. Image reads require durable attachment storage and an image-capable routed model. Choose the sibling discovery package for glob or grep searches. ## Table of Contents @@ -55,7 +55,7 @@ Field names are snake_case to match Claude Code and existing harness tool schema ### Configuration -All keys are optional. The `read`, `readImage`, `write`, `edit`, and `enableApplyPatch` switches default to `true`; set a switch to `false` to omit that tool and its guidance. All five may be disabled. Minimal presets disable text tools explicitly. Enabling `readImage` still requires an attachment store and an image-capable model. Read caps remain validated when `read` is disabled. +All keys are optional. The `read`, `readImage`, `write`, `edit`, and `applyPatch` switches default to `true`; set a switch to `false` to omit that tool and its guidance. All five may be disabled. Enabling `readImage` still requires an attachment store and an image-capable model. Read caps remain validated when `read` is disabled. | Key | Default | Meaning | |---|---|---| @@ -63,20 +63,12 @@ All keys are optional. The `read`, `readImage`, `write`, `edit`, and `enableAppl | `readImage` | `true` | Register `read_image` while an attachment store is mounted | | `write` | `true` | Register `write` and its guidance | | `edit` | `true` | Register `edit` and its guidance | -| `enableApplyPatch` | `true` | Register `apply_patch` and its guidance | +| `applyPatch` | `true` | Register `apply_patch` and its guidance | | `readLimit` | `2000` | Default and maximum lines returned by one `read` call | | `readMaxLineLength` | `2000` | Characters kept per line before truncation | | `readMaxBytes` | `51200` | Byte cap on one `read` call's selected lines; overflow ends the window with a capped footer | | `readStreamMinSize` | `10485760` | Files at or above this size (or of unknown size) stream instead of loading whole into memory | -Disable the patch tool in home-level `cordis.patch.yml` or a `--patch` overlay: - -```yaml -- id: tool-fs - config: - enableApplyPatch: false -``` - The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-tool-fs) is the exhaustive source for every accepted field and its JSDoc. With `fs-observation-policy` mounted and `read` disabled, `write` can still create files, and successful writes or edits record the versions needed for later mutations in that session. Existing unobserved files remain protected; disabling `read` does not relax the policy. Another filesystem reader must emit `fs/observed` for an existing file to become observed. @@ -89,7 +81,7 @@ With the policy plugin mounted, `write` and `edit` obtain their guard from the ` ### Failures and recovery -For `apply_patch`, syntax and preflight refusals throw with `isError: true` before any disk change. `FsError` preserves filesystem codes; `PatchToolError extends HarnessError` carries `PATCH_SYNTAX` and `PATCH_SYMLINK_SOURCE`. Once commit starts, the tool returns a value with `isError: false` even on failure or uncertainty, so known partial effects retain their diff metadata. `phase`, `cancelled`, ordered `operations` and `affectedPaths` describe the commit; each operation retains its index, kind, display paths, patch line, status, diagnostic and known changes. Full `before`/`after` text is execution-local canonical data for rendering and PTC; Session stores the rendered result and `meta.diffs` plus `meta.stopped`, not whole-file change records. Deletions and move sources use `newText: ''` in diff metadata because `FileDiff.newText` cannot be null. +For `apply_patch`, syntax and preflight refusals throw with `isError: true` before any disk change. `FsError` preserves filesystem codes; `PatchToolError extends HarnessError` carries `PATCH_SYNTAX` and `PATCH_SYMLINK_SOURCE`. Once commit starts, the tool returns a value with `isError: false` even on failure or uncertainty, so known partial effects retain their diff metadata. `phase`, `cancelled`, ordered `operations` and `affectedPaths` describe the commit; each operation retains its index, kind, display paths, patch line, status, diagnostic and known changes. Full `before`/`after` text serves rendering and PTC; the Session stores the rendered result plus `meta.diffs` and `meta.stopped`. Deletions and move sources use `newText: ''` in diff metadata because `FileDiff.newText` cannot be null. Failures are normalized as `Error: ` with a structured code preserved for callers. Stable messages include `file_path must be a non-empty string`, `limit must be less than or equal to `, `cannot read "": not found`, `cannot read "": not a regular file`, and the image-route refusal `cannot read "" as an image: model "" does not declare image input; switch to an image-capable model to read images`. `FS_NOT_OBSERVED` is normalized to `cannot modify "": file has not been read — read the file, then retry`, independent of whether the policy or provider rejected the operation; `FS_STALE_VERSION` retains the provider's reason and appends `— re-read the file, then retry`. After the reread confirms absence, `edit` reports `FS_NOT_FOUND` instead of repeating a stale remedy, while `write` uses guarded creation. @@ -115,15 +107,15 @@ The tools are the executor; policy is an event gate. The tools inject no policy | [`src/read.ts`](src/read.ts) | `read` executor: one stat, streaming decision, window build, observation | | [`src/read-image.ts`](src/read-image.ts) | `read_image` executor: route and media-type gates, bounded bytes, attachment save | | [`src/write.ts`](src/write.ts) | `write` executor: intent waterfall, atomic write, observation | -| [`src/apply-patch.ts`](src/apply-patch.ts), [`apply-patch-output.ts`](src/apply-patch-output.ts) | Opt-in registration, strict commit values and compact diagnostics | +| [`src/apply-patch.ts`](src/apply-patch.ts), [`apply-patch-output.ts`](src/apply-patch-output.ts) | `apply_patch` registration, strict commit values and compact diagnostics | | [`src/apply-patch-presentation.ts`](src/apply-patch-presentation.ts) | Pure call previews and known result diffs | | [`src/edit.ts`](src/edit.ts) | `edit` executor: intent waterfall, literal edit, observation | -| [`src/patch/index.ts`](src/patch/index.ts), [`types.ts`](src/patch/types.ts) | Internal `executePatch` entry and serializable per-operation reports; not exported from the package entry | +| [`src/patch/index.ts`](src/patch/index.ts), [`types.ts`](src/patch/types.ts) | Internal `executePatch` entry and serializable per-operation reports | | [`src/patch/plan.ts`](src/patch/plan.ts) | Parse and resolve Session-relative paths; preserve ordered operations sharing a canonical target | | [`src/patch/preflight.ts`](src/patch/preflight.ts) | Simulate preceding successes and collect observation, sandbox, type, and hunk failures before any write | | [`src/patch/execute.ts`](src/patch/execute.ts) | Reacquire guards, commit sequentially, record actual effects, and classify uncertain failures | | [`src/read-render.ts`](src/read-render.ts) | Cordis-free windowing and envelope formatting | -| [`src/sandbox.ts`](src/sandbox.ts) | Escalation API shared by `write`/`edit`: policy resolution and denial-marker mapping | +| [`src/sandbox.ts`](src/sandbox.ts) | Escalation API shared by the mutating tools: policy resolution and denial-marker mapping | | [`src/error.ts`](src/error.ts) | Stable model-facing diagnostics for guarded-mutation failures | | [`src/mutation-result.ts`](src/mutation-result.ts) | Successful mutation observation and canonical result paths | @@ -131,17 +123,15 @@ The tools are the executor; policy is an event gate. The tools inject no policy All four tools share one flow shape: resolve the path with the calling session's cwd, run the applicable gate, perform exactly one provider operation, and emit `fs/observed` only after success. `read` and `read_image` pay one `stat` for type and size routing; `write` and `edit` pay none because their guard comes from the intent slot, and provider failures surface as typed `FsError` results. The per-tool executors live in `src/read.ts`, `src/read-image.ts`, `src/write.ts`, and `src/edit.ts`. -The internal patch executor accepts a raw patch and an already resolved sandbox policy. It builds an ordered plan, preflights all operations against simulated state, then commits through `writeText`, `patchText`, `deleteFile`, or one `moveFile` call. A failed preflight operation has no simulated effect; any preflight failure prevents every write. Reports retain one entry per operation with `completed`, `failed`, `unknown`, or `skipped` status and known LF text changes. Syntax reports carry a required failure and no outcomes; preflight and commit reports carry ordered operation outcomes. Resolution failures retain the raw patch spelling only for unresolved paths; successful resolutions use provider display paths. Programming errors reject the call. `enableApplyPatch` connects this executor to the native and PTC registry through `src/apply-patch.ts`. +The internal patch executor accepts a raw patch and an already resolved sandbox policy. It builds an ordered plan, preflights all operations against simulated state, then commits through `writeText`, `patchText`, `deleteFile`, or one `moveFile` call. A refused preflight operation has no simulated effect, and any refusal prevents every write. A syntax report carries the parser failure; a preflight report lists the refused operations; a commit report keeps one entry per operation with `completed`, `failed`, `unknown`, or `skipped` status and known LF text changes. Unresolved paths retain their raw patch spelling; resolved paths use provider display paths. Programming errors reject the call. ### Observation and concurrency -When mounted, `workspace-changes` completes Review captures in `tools/pre-execute` before file mutations; these reads do not establish model observation. - `fs/observed` fires after the operation succeeded via a plain `ctx.emit`; a listener is contractually a synchronous, side-effect-only recorder, so async or fallible observation does not belong on this event. `read` opts into concurrent scheduling because its only mutation is the synchronous version recorder; recorder races fail closed when a later `write` or `edit` re-checks the version under its target lock, and both mutation tools remain exclusive. -Patch preflight reads never emit `fs/observed`. The first operation on a target dispatches its intent; later preflight operations use the expected state after preceding successes. Each commit dispatches its intents again to obtain current observation guards. Add/update records present, delete records absent, and move records its destination plus source absence only after successful removal. A partial move is failed with a destination-only change and preserves the source observation. The source baseline comes from `moveFile` under its lock, without an extra executor read. +Patch preflight reads never emit `fs/observed`. The first preflight operation on a target dispatches its intent; later operations on that target use the simulated state. Each commit dispatches its intents again to obtain current guards. Add and update record presence, delete records absence, and move records its destination plus source absence only after successful removal; a partial move is `failed` with a destination-only change. The move source baseline comes from `moveFile` under its lock. -After a mutation rejects with `FS_IO_ERROR` or `FS_ABORTED`, the executor compares each affected target’s existence and version with its metadata before the call, using a fresh `stat` without the cancelled signal. Unchanged targets mean a known failure; a changed target or failed verification means `unknown` and records no fabricated observation or change. `affectedPaths` contains only known effects. Execution stops at the first failed or unknown commit, or before an operation whose signal is aborted. +After a mutation rejects with `FS_IO_ERROR` or `FS_ABORTED`, the executor compares each affected target's version with a fresh `stat` taken without the cancelled signal. Unchanged targets mean a known failure; a changed target or failed comparison means `unknown`, which records no observation or change. Execution stops at the first failed or unknown commit, or before an operation whose signal is aborted. @@ -270,7 +260,7 @@ Append-only; newly visible content follows the reusable request prefix and does #### What the model sees -The `apply_patch` schema requires one `patch` string containing `*** Begin Patch` / `*** End Patch`, Add/Update/Delete headers, optional Move to, and context/add/remove hunk lines. Paths are Session-relative. A confining backend adds the same escalation fields as write/edit. Its scope-aware system guidance is shown below. Successful moves use `M ` and success lines group A/M/D, matching Codex rust-v0.158.0. An empty patch renders `Success. No operations in patch.`. A stopped patch includes only applicable Completed, Failed, Outcome unknown and Not executed sections; unknown outcomes end with the executor’s re-read remedy. Syntax errors keep the parser message. Examples: +The `apply_patch` schema requires one `patch` string containing `*** Begin Patch` / `*** End Patch`, Add/Update/Delete headers, optional Move to, and context/add/remove hunk lines. Paths are Session-relative. A confining backend adds the same escalation fields as write/edit. Success lines group A/M/D and a completed move reports `M `, matching Codex rust-v0.158.0. An empty patch renders `Success. No operations in patch.`. A stopped patch includes only the applicable Completed, Failed, Outcome unknown and Not executed sections; unknown outcomes end with the re-read remedy. Syntax errors keep the parser message. ##### System guidance @@ -301,11 +291,11 @@ Patch rejected before any change (1 problems): #### Token effect -The default tool set includes one patch tool schema and three guidance sentences to the first request. Commit output scales with operation count and diagnostics, not full-file size; canonical before/after values do not enter Native model content. +The default tool set adds one patch tool schema and three guidance sentences to the first request. Commit output scales with operation count and diagnostics, not file size. #### KV Cache effect -Stable while activation and visible tools are unchanged; call/result text appends to history. +Stable while the visible tools are unchanged; call/result text appends to history. ## Known Limitations and Deferred Work @@ -314,7 +304,7 @@ Stable while activation and visible tools are unchanged; call/result text append These limits define when the tool suite is a poor fit or needs special operational care. They are current package constraints, not a general filesystem comparison or a task backlog. -- **Patch commits do not roll back** — earlier successes and a published move destination remain on disk after later failure. Sandbox preflight is best effort; providers enforce permissions and version guards again. An `unknown` result requires re-reading the affected files before retrying. +- **Patch commits do not roll back** — earlier successes and a published move destination remain on disk after a later failure, and an `unknown` result requires re-reading the affected files before retrying. - **No model-facing directory listing ships** — `ctx.fs.listDir` serves provider code such as skill discovery, while the sibling `dsh-tool-fs-search` package supplies ripgrep-backed `glob` and `grep` rather than extending the filesystem seam. - **`read` handles UTF-8 text files only** — images use the separate `read_image` tool; PDF, audio, and video remain deferred. A directory target is `FS_NOT_REGULAR_FILE`. - **Extension-declared media type** — an extension selects the declared type and the attachment store's magic-byte validation stays authoritative; a correctly formatted image under a wrong extension is refused with the rename remedy rather than sniffed. Only a path with no extension is identified from its file signature. diff --git a/packages/fs/tool-fs/README.zh.md b/packages/fs/tool-fs/README.zh.md index 893bf48c4a..2fcb70fba8 100644 --- a/packages/fs/tool-fs/README.zh.md +++ b/packages/fs/tool-fs/README.zh.md @@ -9,7 +9,7 @@ kind: "package-reference" ## 概述 -使用 `dsh-tool-fs` 可让模型带行号读取 UTF-8 文件、读取受支持的图片、创建或原子地替换文件,执行有针对性的字面量编辑,并可选地应用 Codex 风格的多文件补丁。结果都有上限,失败会提供稳定错误码与恢复指令。当写入和编辑必须在成功读取后执行时,请添加 `dsh-fs-observation-policy`;省略它时,变更仍是原子的,但不受此条件约束。图片读取需要持久附件存储和支持图片输入的路由模型。glob 或 grep 搜索请选择同级的发现工具包。 +使用 `dsh-tool-fs` 可让模型带行号读取 UTF-8 文件、读取受支持的图片、创建或原子地替换文件,执行有针对性的字面量编辑,以及应用 Codex 风格的多文件补丁。结果都有上限,失败会提供稳定错误码与恢复指令。当写入和编辑必须在成功读取后执行时,请添加 `dsh-fs-observation-policy`;省略它时,变更仍是原子的,但不受此条件约束。图片读取需要持久附件存储和支持图片输入的路由模型。glob 或 grep 搜索请选择同级的发现工具包。 ## 目录 @@ -55,7 +55,7 @@ kind: "package-reference" ### 配置 -所有键均为可选。`read`、`readImage`、`write`、`edit` 和 `enableApplyPatch` 开关默认为 `true`;将开关设为 `false` 可省略对应工具及其指导文本。允许关闭全部五个工具。最小预设显式关闭文本工具。启用 `readImage` 仍需要附件存储和支持图片输入的模型。关闭 `read` 时仍会校验读取上限。 +所有键均为可选。`read`、`readImage`、`write`、`edit` 和 `applyPatch` 开关默认为 `true`;将开关设为 `false` 可省略对应工具及其指导文本。允许关闭全部五个工具。启用 `readImage` 仍需要附件存储和支持图片输入的模型。关闭 `read` 时仍会校验读取上限。 | 键 | 默认值 | 含义 | |---|---|---| @@ -63,20 +63,12 @@ kind: "package-reference" | `readImage` | `true` | 附件存储挂载时注册 `read_image` | | `write` | `true` | 注册 `write` 及其指导文本 | | `edit` | `true` | 注册 `edit` 及其指导文本 | -| `enableApplyPatch` | `true` | 注册 `apply_patch` 及其提示 | +| `applyPatch` | `true` | 注册 `apply_patch` 及其提示 | | `readLimit` | `2000` | 一次 `read` 调用返回的默认和最大行数 | | `readMaxLineLength` | `2000` | 每行截断前保留的字符数 | | `readMaxBytes` | `51200` | 一次 `read` 调用所选行的字节上限;溢出时以「已达上限」footer 结束窗口 | | `readStreamMinSize` | `10485760` | 大于等于该大小或大小未知的文件采用流式读取,而不是整体加载到内存 | -在 home 级 `cordis.patch.yml` 或 `--patch` overlay 中关闭补丁工具: - -```yaml -- id: tool-fs - config: - enableApplyPatch: false -``` - 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-tool-fs)是每个受支持字段及其 JSDoc 的穷尽式真源。 挂载 `fs-observation-policy` 且关闭 `read` 时,`write` 仍可创建文件,成功写入或编辑会记录本会话后续修改所需的版本。未观察过的已有文件仍受保护;关闭 `read` 不会放宽策略。其他文件系统读取方必须发出 `fs/observed`,已有文件才会成为已观察状态。 @@ -89,7 +81,7 @@ kind: "package-reference" ### 失败与恢复 -`apply_patch` 的语法与预检拒绝在磁盘变更前抛错,结果为 `isError: true`。`FsError` 保留文件系统错误码;`PatchToolError extends HarnessError` 携带 `PATCH_SYNTAX` 和 `PATCH_SYMLINK_SOURCE`。进入提交后,即使失败或结果不确定也返回 `isError: false` 的值,确保已知部分效果的 diff 元数据得以保留。`phase`、`cancelled`、有序 `operations` 与 `affectedPaths` 描述提交;每条操作保留索引、种类、展示路径、补丁行号、状态、诊断与已知变更。完整 `before`/`after` 文本仅作为执行期规范值供渲染和 PTC 使用;Session 保存渲染文本、`meta.diffs` 与 `meta.stopped`,不保存整文件变更记录。删除与移动源在 diff 元数据中使用 `newText: ''`,因为 `FileDiff.newText` 不可为 null。 +`apply_patch` 的语法与预检拒绝在磁盘变更前抛错,结果为 `isError: true`。`FsError` 保留文件系统错误码;`PatchToolError extends HarnessError` 携带 `PATCH_SYNTAX` 和 `PATCH_SYMLINK_SOURCE`。进入提交后,即使失败或结果不确定也返回 `isError: false` 的值,确保已知部分效果的 diff 元数据得以保留。`phase`、`cancelled`、有序 `operations` 与 `affectedPaths` 描述提交;每条操作保留索引、种类、展示路径、补丁行号、状态、诊断与已知变更。完整 `before`/`after` 文本供渲染和 PTC 使用;Session 保存渲染文本以及 `meta.diffs` 与 `meta.stopped`。删除与移动源在 diff 元数据中使用 `newText: ''`,因为 `FileDiff.newText` 不可为 null。 失败被规范化为 `Error: `,并为调用方保留结构化错误码。稳定消息包括 `file_path must be a non-empty string`、`limit must be less than or equal to `、`cannot read "": not found`、`cannot read "": not a regular file`,以及图像路由拒绝 `cannot read "" as an image: model "" does not declare image input; switch to an image-capable model to read images`。无论拒绝来自策略还是提供方,`FS_NOT_OBSERVED` 都规范化为 `cannot modify "": file has not been read — read the file, then retry`;`FS_STALE_VERSION` 保留提供方原因并追加 `— re-read the file, then retry`。该次重新读取确认缺失后,`edit` 报告 `FS_NOT_FOUND` 而不会重复陈旧恢复指令,`write` 则使用防护创建。 @@ -115,15 +107,15 @@ kind: "package-reference" | [`src/read.ts`](src/read.ts) | `read` 执行器:一次 stat、流式决策、窗口构建、观察 | | [`src/read-image.ts`](src/read-image.ts) | `read_image` 执行器:路由与媒体类型门禁、有界字节、附件保存 | | [`src/write.ts`](src/write.ts) | `write` 执行器:意图 waterfall、原子写入、观察 | -| [`src/apply-patch.ts`](src/apply-patch.ts)、[`apply-patch-output.ts`](src/apply-patch-output.ts) | 按需注册、严格提交值与精简诊断 | +| [`src/apply-patch.ts`](src/apply-patch.ts)、[`apply-patch-output.ts`](src/apply-patch-output.ts) | `apply_patch` 注册、严格提交值与精简诊断 | | [`src/apply-patch-presentation.ts`](src/apply-patch-presentation.ts) | 纯调用预览与已知结果 diff | | [`src/edit.ts`](src/edit.ts) | `edit` 执行器:意图 waterfall、字面量编辑、观察 | -| [`src/patch/index.ts`](src/patch/index.ts)、[`types.ts`](src/patch/types.ts) | 内部 `executePatch` 入口与可序列化的逐操作报告;不从包入口导出 | +| [`src/patch/index.ts`](src/patch/index.ts)、[`types.ts`](src/patch/types.ts) | 内部 `executePatch` 入口与可序列化的逐操作报告 | | [`src/patch/plan.ts`](src/patch/plan.ts) | 解析补丁并按 Session 工作目录解析路径;保留共享规范化目标的有序操作 | | [`src/patch/preflight.ts`](src/patch/preflight.ts) | 模拟前序成功操作,在任何写入前收集观察、沙箱、类型与 hunk 失败 | | [`src/patch/execute.ts`](src/patch/execute.ts) | 重新获取防护、顺序提交、记录实际效果并判断不确定失败 | | [`src/read-render.ts`](src/read-render.ts) | 不依赖 Cordis 的窗口构建与信封格式化 | -| [`src/sandbox.ts`](src/sandbox.ts) | `write`/`edit` 共享的升权 API:策略解析与拒绝标记映射 | +| [`src/sandbox.ts`](src/sandbox.ts) | 各变更工具共享的升权 API:策略解析与拒绝标记映射 | | [`src/error.ts`](src/error.ts) | 防护变更失败的稳定模型侧诊断 | | [`src/mutation-result.ts`](src/mutation-result.ts) | 成功变更的观察记录与规范化结果路径 | @@ -131,17 +123,15 @@ kind: "package-reference" 四个工具共享同一种流程形态:用调用会话的 cwd 解析路径、运行适用的门禁、恰好执行一次提供方操作,并且只在成功后发出 `fs/observed`。`read` 与 `read_image` 为类型与大小路由付出一次 `stat`;`write` 与 `edit` 不执行 stat,因为防护来自意图槽位,提供方失败以类型化 `FsError` 结果呈现。各工具执行器位于 `src/read.ts`、`src/read-image.ts`、`src/write.ts` 与 `src/edit.ts`。 -内部补丁执行器接收原始补丁和已解析的沙箱策略。它建立有序计划,针对模拟状态预检全部操作,再通过 `writeText`、`patchText`、`deleteFile` 或一次 `moveFile` 调用提交。预检失败的操作不产生模拟效果;任何预检失败都会阻止全部写入。报告为每个操作保留 `completed`、`failed`、`unknown` 或 `skipped` 状态及已知的 LF 文本变化。语法报告必须包含失败诊断且没有操作结果;预检和提交报告包含有序操作结果。路径解析失败时,仅未解析的路径保留补丁原始写法;成功解析的路径使用提供方显示路径。编程错误使调用拒绝。`enableApplyPatch` 通过 `src/apply-patch.ts` 将执行器接入原生与 PTC 注册入口。 +内部补丁执行器接收原始补丁和已解析的沙箱策略。它建立有序计划,针对模拟状态预检全部操作,再通过 `writeText`、`patchText`、`deleteFile` 或一次 `moveFile` 调用提交。被预检拒绝的操作不产生模拟效果,任何拒绝都会阻止全部写入。语法报告携带解析器失败;预检报告列出被拒绝的操作;提交报告为每个操作保留 `completed`、`failed`、`unknown` 或 `skipped` 状态及已知的 LF 文本变化。未解析的路径保留补丁原始写法;已解析的路径使用提供方显示路径。编程错误使调用拒绝。 ### 观察与并发 -挂载 `workspace-changes` 后,它在 `tools/pre-execute` 中完成 Review 采集,再执行文件变更;这些读取不建立模型观察。 - `fs/observed` 在操作成功之后通过普通 `ctx.emit` 发出;监听器的约定是同步且只有副作用的记录器,因此异步或可能失败的观察不属于该事件。`read` 允许并发调度,因为它唯一改变状态的操作是同步记录版本;稍后的 `write` 或 `edit` 会在目标锁内重新检查版本,因此记录器竞态会安全地失败,两个变更工具仍保持互斥。 -补丁预检读取从不发出 `fs/observed`。对目标的首次操作分发意图;后续预检操作使用前序成功操作后的预期状态。每次提交重新分发意图以取得当前观察防护。新增/更新记录存在,删除记录缺失,移动记录目标存在,且仅在源删除成功后记录源缺失。部分移动报告失败且只包含目标变化,保留源观察。源内容基线由 `moveFile` 在锁内返回,执行器无需额外读取。 +补丁预检读取从不发出 `fs/observed`。对目标的首次预检操作分发意图;同一目标的后续操作使用模拟状态。每次提交重新分发意图以取得当前防护。新增与更新记录存在,删除记录缺失,移动记录目标存在,且仅在源删除成功后记录源缺失;部分移动为 `failed`,只包含目标变化。移动源基线由 `moveFile` 在锁内返回。 -变更以 `FS_IO_ERROR` 或 `FS_ABORTED` 拒绝后,执行器使用不携带已取消信号的新 `stat`,将每个受影响目标的存在性与版本和调用前元数据比较。全部未变表示已知失败;任何目标已变或验证失败均为 `unknown`,不伪造观察或变化记录。`affectedPaths` 仅包含已知效果。执行在首个失败或未知提交后停止,或在信号已取消的操作开始前停止。 +变更以 `FS_IO_ERROR` 或 `FS_ABORTED` 拒绝后,执行器使用不携带已取消信号的新 `stat` 比较每个受影响目标的版本。全部未变表示已知失败;任何目标已变或比较失败均为 `unknown`,不记录观察或变化。执行在首个失败或未知提交后停止,或在信号已取消的操作开始前停止。 @@ -270,7 +260,7 @@ Read a file before editing it (the default fs-observation-policy requires it), u #### 模型看到的内容 -`apply_patch` schema 要求一个 `patch` 字符串,包含 `*** Begin Patch` / `*** End Patch`、Add/Update/Delete 文件头、可选 Move to,以及上下文/新增/删除 hunk 行。路径相对于 Session 工作目录。有围栏后端追加与 write/edit 相同的提权字段。按作用域显示的系统提示如下。成功移动显示 `M `,成功行按 A/M/D 分组,与 Codex rust-v0.158.0 一致。空补丁显示 `Success. No operations in patch.`。停止的补丁只包含适用的 Completed、Failed、Outcome unknown 与 Not executed 小节;结果不确定时以执行器的重读建议结束。语法错误保留解析器消息。示例: +`apply_patch` schema 要求一个 `patch` 字符串,包含 `*** Begin Patch` / `*** End Patch`、Add/Update/Delete 文件头、可选 Move to,以及上下文/新增/删除 hunk 行。路径相对于 Session 工作目录。有围栏后端追加与 write/edit 相同的提权字段。成功行按 A/M/D 分组,完成的移动显示 `M `,与 Codex rust-v0.158.0 一致。空补丁显示 `Success. No operations in patch.`。停止的补丁只包含适用的 Completed、Failed、Outcome unknown 与 Not executed 小节;结果不确定时以重读建议结束。语法错误保留解析器消息。 ##### 系统提示 @@ -301,11 +291,11 @@ Patch rejected before any change (1 problems): #### Token 影响 -默认工具集在首轮请求包含一份补丁工具 schema 与三句提示。提交输出随操作数量与诊断增长,不随整文件大小增长;规范值中的 before/after 不进入原生模型内容。 +默认工具集在首轮请求新增一份补丁工具 schema 与三句提示。提交输出随操作数量与诊断增长,不随文件大小增长。 #### KV Cache 影响 -启用状态与可见工具不变时保持稳定;调用和结果文本追加到历史。 +可见工具不变时保持稳定;调用和结果文本追加到历史。 ## 已知限制与延期工作 @@ -314,7 +304,7 @@ Patch rejected before any change (1 problems): 这些限制说明工具套件何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是通用文件系统对比或任务积压。 -- **补丁提交不回滚**——后续失败后,先前成功操作和已发布的移动目标仍保留在磁盘上。沙箱预检尽力提前检查;提供方仍会再次执行权限与版本防护。`unknown` 结果要求重读受影响文件后再重试。 +- **补丁提交不回滚**——后续失败后,先前成功操作和已发布的移动目标仍保留在磁盘上,`unknown` 结果要求重读受影响文件后再重试。 - **未交付面向模型的目录列表工具**:`ctx.fs.listDir` 服务于 skill(技能)发现等提供方代码,同级 `dsh-tool-fs-search` 包则提供基于 ripgrep 的 `glob` 与 `grep`,而不是扩展文件系统 seam。 - **`read` 只处理 UTF-8 文本文件**:图像使用独立的 `read_image` 工具;PDF、音频和视频仍延期处理。目录目标为 `FS_NOT_REGULAR_FILE`。 - **媒体类型按扩展名声明**:扩展名选择声明类型,附件存储的魔数校验保持权威;扩展名错误但格式正确的图像会得到改名修复提示,而不是被嗅探接受。只有没有扩展名的路径按文件签名识别格式。 diff --git a/packages/fs/tool-fs/src/apply-patch-output.ts b/packages/fs/tool-fs/src/apply-patch-output.ts index 102a0e116d..cd33d2cf64 100644 --- a/packages/fs/tool-fs/src/apply-patch-output.ts +++ b/packages/fs/tool-fs/src/apply-patch-output.ts @@ -50,7 +50,7 @@ function rejection(code: PatchFailureCode, message: string): HarnessError { /** * Convert a report into a commit value, or reject before any disk change. - * @param report - executor report with complete operation outcomes. + * @param report - executor report. * @returns a flat, lossless JSON commit value, including partial failures. * @throws FsError or PatchToolError for syntax or preflight refusals. */ @@ -59,13 +59,9 @@ export function patchToolValue(report: PatchExecutionReport): PatchToolValue { throw rejection(report.failure.code, report.failure.message) } if (report.phase === 'preflight') { - const failures = report.outcomes.filter(outcome => outcome.status === 'failed') - const first = failures[0] - if (first !== undefined) { - const lines = failures.map(({ operation, failure }) => - `- operation ${operation.index + 1} (${operation.kind} ${operation.path}, patch line ${operation.patchLine}): ${failure.message}`) - throw rejection(first.failure.code, `Patch rejected before any change (${failures.length} problems):\n${lines.join('\n')}`) - } + const lines = report.refusals.map(({ operation, failure }) => + `- operation ${operation.index + 1} (${operation.kind} ${operation.path}, patch line ${operation.patchLine}): ${failure.message}`) + throw rejection(report.refusals[0].failure.code, `Patch rejected before any change (${report.refusals.length} problems):\n${lines.join('\n')}`) } return { phase: 'commit', cancelled: report.cancelled, affectedPaths: report.affectedPaths, operations: report.outcomes.map(outcome => ({ diff --git a/packages/fs/tool-fs/src/apply-patch.ts b/packages/fs/tool-fs/src/apply-patch.ts index bcfeb81f4b..d405024ad5 100644 --- a/packages/fs/tool-fs/src/apply-patch.ts +++ b/packages/fs/tool-fs/src/apply-patch.ts @@ -1,4 +1,4 @@ -/** Opt-in Codex-style multi-file patch tool over the guarded patch executor. */ +/** Codex-style multi-file patch tool over the guarded patch executor. */ import type { Context } from '@deepseek-ai/cordis' import { defineTool } from '@deepseek-ai/dsh-tools' import { executePatch } from './patch/index.ts' diff --git a/packages/fs/tool-fs/src/index.ts b/packages/fs/tool-fs/src/index.ts index 6d4c254226..c03e7d4cfa 100644 --- a/packages/fs/tool-fs/src/index.ts +++ b/packages/fs/tool-fs/src/index.ts @@ -25,8 +25,6 @@ export const inject = ['tools', 'fs', 'systemPrompt', 'workingDirectory'] /** Plugin config (all optional — `Config` supplies the defaults). */ export interface Config { - /** Register `apply_patch` and its guidance. Defaults to true. */ - enableApplyPatch?: boolean /** Register `read` and its guidance. Defaults to true. */ read?: boolean /** Register `read_image` while an attachment store is mounted. Defaults to true. */ @@ -35,6 +33,8 @@ export interface Config { write?: boolean /** Register `edit` and its guidance. Defaults to true. */ edit?: boolean + /** Register `apply_patch` and its guidance. Defaults to true. */ + applyPatch?: boolean /** Default and maximum number of lines returned by one `read` call. */ readLimit?: number /** Maximum characters returned for a single line before truncation. */ @@ -46,11 +46,11 @@ export interface Config { } export const Config: z = z.object({ - enableApplyPatch: z.boolean().default(true), read: z.boolean().default(true), readImage: z.boolean().default(true), write: z.boolean().default(true), edit: z.boolean().default(true), + applyPatch: z.boolean().default(true), readLimit: z.number().default(READ_LIMIT), readMaxLineLength: z.number().default(READ_MAX_LINE_LENGTH), readMaxBytes: z.number().default(READ_MAX_BYTES), @@ -94,10 +94,10 @@ export function apply(ctx: Context, config: Config): void { // One escalation API shared by the mutating tools: advertisement gating, // per-call policy resolution, and denial-marker mapping, all keyed off whether // the mounted ctx.fs confines (ctx.fs.sandboxMode). - if (resolved.write || resolved.edit || resolved.enableApplyPatch) { + if (resolved.write || resolved.edit || resolved.applyPatch) { const sandbox = new FsSandboxController(ctx) if (resolved.write) applyWriteTool(ctx, sandbox) if (resolved.edit) applyEditTool(ctx, sandbox) - if (resolved.enableApplyPatch) applyApplyPatchTool(ctx, sandbox) + if (resolved.applyPatch) applyApplyPatchTool(ctx, sandbox) } } diff --git a/packages/fs/tool-fs/src/patch/execute.ts b/packages/fs/tool-fs/src/patch/execute.ts index d407bb5b1a..37cd5f3bda 100644 --- a/packages/fs/tool-fs/src/patch/execute.ts +++ b/packages/fs/tool-fs/src/patch/execute.ts @@ -110,7 +110,6 @@ export async function commitPatch( outcome = uncertain ? { status: 'unknown', operation: record, failure: { ...failure, message: `${failure.message} — re-read the affected files before retrying` } } : { status: 'failed', operation: record, failure, changes: [] } - cancelled = exec.signal.aborted } outcomes[index] = outcome if (outcome.status !== 'completed') { cancelled = exec.signal.aborted; break } diff --git a/packages/fs/tool-fs/src/patch/index.ts b/packages/fs/tool-fs/src/patch/index.ts index 3da4641bf5..7854c4837a 100644 --- a/packages/fs/tool-fs/src/patch/index.ts +++ b/packages/fs/tool-fs/src/patch/index.ts @@ -8,7 +8,7 @@ import { planPatch } from './plan.ts' import { preflightPatch } from './preflight.ts' import type { PatchExecutionReport, PatchExecutionRequest } from './types.ts' -export type { PatchExecutionReport, PatchExecutionRequest, PatchFailure, PatchFailureCode, PatchFileChange, PatchOperationOutcome, PatchOperationRecord } from './types.ts' +export type { PatchExecutionReport, PatchExecutionRequest, PatchFailure, PatchFailureCode, PatchFileChange, PatchOperationOutcome, PatchOperationRecord, PatchRefusal } from './types.ts' /** * Preflight a whole patch before sequential guarded mutations; business failures become records. @@ -16,7 +16,7 @@ export type { PatchExecutionReport, PatchExecutionRequest, PatchFailure, PatchFa * @param sandbox - denial mapper shared with existing filesystem tools. * @param exec - session cwd, observation actor, and cancellation signal. * @param request - raw patch and policy already resolved by the caller. - * @returns ordered results retaining completed, partial, unknown, and skipped effects. + * @returns the syntax failure, the preflight refusals, or ordered commit outcomes. * @throws unexpected programming defects without starting further operations. */ export async function executePatch( @@ -25,14 +25,12 @@ export async function executePatch( let plan try { plan = await planPatch(ctx, sandbox, exec, request) } catch (error) { if (!(error instanceof PatchParseError)) throw error - return { phase: 'syntax', failure: { code: 'PATCH_SYNTAX', message: error.message }, outcomes: [], affectedPaths: [], cancelled: false } + return { phase: 'syntax', failure: { code: 'PATCH_SYNTAX', message: error.message } } } - const outcomes = [ - ...plan.failures, + const [first, ...rest] = [ + ...plan.refusals, ...await preflightPatch(ctx, sandbox, exec, request.sandboxPolicy, plan.operations), ].sort((left, right) => left.operation.index - right.operation.index) - if (outcomes.some(outcome => outcome.status === 'failed')) { - return { phase: 'preflight', outcomes, affectedPaths: [], cancelled: exec.signal.aborted } - } + if (first !== undefined) return { phase: 'preflight', refusals: [first, ...rest] } return commitPatch(ctx, sandbox, exec, request.sandboxPolicy, plan.operations) } diff --git a/packages/fs/tool-fs/src/patch/plan.ts b/packages/fs/tool-fs/src/patch/plan.ts index 629cd83155..195947c230 100644 --- a/packages/fs/tool-fs/src/patch/plan.ts +++ b/packages/fs/tool-fs/src/patch/plan.ts @@ -17,7 +17,7 @@ import type { PatchExecutionRequest, PatchFailure, PatchOperationRecord, PatchPl * @param sandbox - filesystem diagnostic mapper. * @param exec - session cwd and execution signal. * @param request - raw patch and resolved per-call policy. - * @returns resolved operations sharing one target per canonical key, plus resolution failures. + * @returns resolved operations sharing one target per canonical key, plus resolution refusals. */ export async function planPatch( ctx: Context, sandbox: FsSandboxController, exec: ToolExecution, request: PatchExecutionRequest, @@ -31,7 +31,7 @@ export async function planPatch( targets.set(target.targetKey, target) return target } - const result: PatchPlan = { operations: [], failures: [] } + const result: PatchPlan = { operations: [], refusals: [] } for (const [index, operation] of parsed.entries()) { const moveTo = operation.kind === 'update' ? operation.moveTo : undefined const record: PatchOperationRecord = { @@ -61,8 +61,7 @@ export async function planPatch( } } catch (error) { if (!(error instanceof FsError)) throw error - result.failures.push({ status: 'failed', operation: record, - failure: patchFailure(sandbox, request.sandboxPolicy, error, failurePath), changes: [] }) + result.refusals.push({ operation: record, failure: patchFailure(sandbox, request.sandboxPolicy, error, failurePath) }) } } return result diff --git a/packages/fs/tool-fs/src/patch/preflight.ts b/packages/fs/tool-fs/src/patch/preflight.ts index ac2ab9a145..1c80cca5ba 100644 --- a/packages/fs/tool-fs/src/patch/preflight.ts +++ b/packages/fs/tool-fs/src/patch/preflight.ts @@ -10,7 +10,7 @@ import { assertNever } from '@deepseek-ai/dsh-util-values' import type { FsSandboxController } from '../sandbox.ts' import { sessionResolveOptions } from '../session-cwd.ts' import { destinationIntent, mutationTargets, patchFailure, sourceIntent } from './plan.ts' -import type { PatchFailure, PatchOperationOutcome, PlannedOperation } from './types.ts' +import type { PatchFailure, PatchRefusal, PlannedOperation } from './types.ts' interface SimulatedFile { exists: boolean @@ -134,28 +134,27 @@ class Preflight { } /** - * Collect all independent preflight failures while simulating only successful operations. + * Collect every independent preflight refusal while simulating only admitted operations. * @param ctx - filesystem service owner; reads never emit observations. * @param sandbox - model-facing denial mapper. * @param exec - observation actor and cancellation signal. * @param policy - resolved confinement policy. * @param operations - complete resolved operation plan. - * @returns failed/skipped entries; any failure forbids the entire commit phase. + * @returns refused operations in patch order; any refusal forbids the commit phase. */ export async function preflightPatch( ctx: Context, sandbox: FsSandboxController, exec: ToolExecution, policy: SandboxExecutionPolicy | undefined, operations: readonly PlannedOperation[], -): Promise { +): Promise { const preflight = new Preflight(ctx, exec, policy) - const outcomes: PatchOperationOutcome[] = [] + const refusals: PatchRefusal[] = [] for (const operation of operations) { let failure: PatchFailure | undefined try { failure = await preflight.check(operation) } catch (error) { if (!(error instanceof FsError)) throw error failure = patchFailure(sandbox, policy, error, preflight.failurePath) } - outcomes.push(failure === undefined ? { status: 'skipped', operation: operation.record } - : { status: 'failed', operation: operation.record, failure, changes: [] }) + if (failure !== undefined) refusals.push({ operation: operation.record, failure }) } - return outcomes + return refusals } diff --git a/packages/fs/tool-fs/src/patch/types.ts b/packages/fs/tool-fs/src/patch/types.ts index 89689f1087..c17bb946fd 100644 --- a/packages/fs/tool-fs/src/patch/types.ts +++ b/packages/fs/tool-fs/src/patch/types.ts @@ -48,6 +48,12 @@ export interface PatchFailure { hunkLine?: number } +/** One operation refused before any write. */ +export interface PatchRefusal { + operation: PatchOperationRecord + failure: PatchFailure +} + /** Each operation retains completed effects even when a move fails halfway. */ export type PatchOperationOutcome = | { status: 'completed'; operation: PatchOperationRecord; changes: PatchFileChange[] } @@ -55,10 +61,10 @@ export type PatchOperationOutcome = | { status: 'unknown'; operation: PatchOperationRecord; failure: PatchFailure } | { status: 'skipped'; operation: PatchOperationRecord } -/** Ordered report; preflight failures prevent all writes, commit failures stop later operations. */ +/** Syntax and preflight refusals prevent all writes; a commit failure stops later operations. */ export type PatchExecutionReport = - | { phase: 'syntax'; failure: PatchFailure; outcomes: []; affectedPaths: []; cancelled: false } - | { phase: 'preflight'; outcomes: PatchOperationOutcome[]; affectedPaths: []; cancelled: boolean } + | { phase: 'syntax'; failure: PatchFailure } + | { phase: 'preflight'; refusals: [PatchRefusal, ...PatchRefusal[]] } | { phase: 'commit'; outcomes: PatchOperationOutcome[]; affectedPaths: string[]; cancelled: boolean } /** Resolved operation with raw source spelling retained for no-follow inspection. */ @@ -73,10 +79,10 @@ export type PlannedOperation = { | { kind: 'move'; destination: FsTarget; hunks: readonly PatchHunk[] } ) -/** Resolved operations and per-operation path-resolution failures in one parsed patch. */ +/** Resolved operations and path-resolution refusals in one parsed patch. */ export interface PatchPlan { - /** Successfully resolved operations, retaining their original indexes and order. */ + /** Resolved operations in patch order. */ operations: PlannedOperation[] - /** Resolution failures; unresolvable paths retain their raw patch spelling. */ - failures: Extract[] + /** Resolution refusals; unresolvable paths retain their raw patch spelling. */ + refusals: PatchRefusal[] } diff --git a/packages/fs/tool-fs/tests/apply-patch.spec.ts b/packages/fs/tool-fs/tests/apply-patch.spec.ts index 11a1829bdf..4d6bbc2b66 100644 --- a/packages/fs/tool-fs/tests/apply-patch.spec.ts +++ b/packages/fs/tool-fs/tests/apply-patch.spec.ts @@ -8,7 +8,6 @@ import { FsError } from '@deepseek-ai/dsh-fs' import { PtcRuntime } from '@deepseek-ai/dsh-ptc-runtime' import type { PtcRunRequest, PtcRunSpec } from '@deepseek-ai/dsh-ptc-runtime' import { add, move, patch, remove, setup, update } from './patch/harness.ts' -import { patchToolValue } from '../src/apply-patch-output.ts' const guidance = 'Prefer apply_patch over separate edit calls for changes across files or separated regions. ' + 'Read files before updating or deleting them when the observation policy is enabled. ' @@ -20,7 +19,7 @@ function call(t: Fixture, text: string) { } it('omits patch registration and guidance when explicitly disabled', async () => { - const t = await setup({ enableApplyPatch: false }) + const t = await setup({ applyPatch: false }) expect(t.ctx.tools.get('apply_patch')).toBeUndefined() expect((await t.ctx.systemPrompt.assemble()).sections.find(section => section.name === 'tool:apply_patch')).toBeUndefined() }) @@ -185,7 +184,6 @@ it('dispatches native and PTC patch calls with identical values', async () => { expect(await t.bytes('a')).toBe('new\n') }) - it.skipIf(process.platform === 'win32' || process.getuid?.() === 0)('retains completed diffs after a real write permission failure', async () => { const t = await setup() const locked = resolve(t.workspace, 'locked') @@ -203,8 +201,3 @@ it.skipIf(process.platform === 'win32' || process.getuid?.() === 0)('retains com await expect(t.bytes('last')).rejects.toMatchObject({ code: 'ENOENT' }) } finally { await chmod(locked, 0o755) } }) - -it('returns a commit value for a preflight report without failures', () => { - expect(patchToolValue({ phase: 'preflight', outcomes: [], affectedPaths: [], cancelled: false })) - .toEqual({ phase: 'commit', operations: [], affectedPaths: [], cancelled: false }) -}) diff --git a/packages/fs/tool-fs/tests/patch/commit.spec.ts b/packages/fs/tool-fs/tests/patch/commit.spec.ts index 9d20690168..6a4c52dc23 100644 --- a/packages/fs/tool-fs/tests/patch/commit.spec.ts +++ b/packages/fs/tool-fs/tests/patch/commit.spec.ts @@ -172,7 +172,6 @@ it('propagates programmer defects from mutation and preserves observed state', a expect(await t.editIntent('a')).toEqual({ version: await t.version('a') }) }) - it('reports the locked move source after an external edit following preflight', async () => { const t = await setup({ policy: false }) await t.file('a', 'old\n') @@ -182,11 +181,10 @@ it('reports the locked move source after an external edit following preflight', return originalMove(...args) }) const reads = vi.spyOn(t.fs, 'readText') - const result = await t.run(patch(move('a', 'b'))) - expect(result.outcomes).toEqual([{ status: 'completed', operation: t.record('move', 'a', 0, 2, 'b'), changes: [ + expect(await t.run(patch(move('a', 'b')))).toMatchObject({ phase: 'commit', outcomes: [{ status: 'completed', operation: t.record('move', 'a', 0, 2, 'b'), changes: [ { path: resolve(t.workspace, 'b'), before: null, after: 'new\nexternal\n' }, { path: resolve(t.workspace, 'a'), before: 'old\nexternal\n', after: null }, - ] }]) + ] }] }) expect(reads).toHaveBeenCalledTimes(1) expect(await t.bytes('b')).toBe('new\nexternal\n') }) diff --git a/packages/fs/tool-fs/tests/patch/harness.ts b/packages/fs/tool-fs/tests/patch/harness.ts index 30fd7ec86f..9f697da53d 100644 --- a/packages/fs/tool-fs/tests/patch/harness.ts +++ b/packages/fs/tool-fs/tests/patch/harness.ts @@ -29,7 +29,7 @@ export const remove = (path: string) => `*** Delete File: ${path}` export const move = (source: string, destination: string, before = 'old', after = 'new') => `*** Update File: ${source}\n*** Move to: ${destination}\n@@\n-${before}\n+${after}` -export async function setup(options: { policy?: boolean; mode?: SandboxMode; provider?: typeof LocalFileSystem; enableApplyPatch?: boolean; toolMode?: 'native' | 'ptc' } = {}) { +export async function setup(options: { policy?: boolean; mode?: SandboxMode; provider?: typeof LocalFileSystem; applyPatch?: boolean; toolMode?: 'native' | 'ptc' } = {}) { const root = await realpath(await mkdtemp(join(tmpdir(), 'dsh-patch-executor-'))) onTestFinished(() => rm(root, { recursive: true, force: true })) const workspace = join(root, 'workspace') @@ -48,7 +48,7 @@ export async function setup(options: { policy?: boolean; mode?: SandboxMode; pro await ctx.plugin(SandboxedFileSystem, { cwd: providerRoot }) } else await ctx.plugin(options.provider ?? LocalFileSystem, { cwd: providerRoot }) if (options.policy !== false) await ctx.plugin(FsPolicy) - const toolFiber = await ctx.plugin(ToolFs, options.enableApplyPatch === undefined ? {} : { enableApplyPatch: options.enableApplyPatch }) + const toolFiber = await ctx.plugin(ToolFs, options.applyPatch === undefined ? {} : { applyPatch: options.applyPatch }) const fs = ctx.fs as LocalFileSystem const controller = new AbortController() const exec: ToolExecution = { diff --git a/packages/fs/tool-fs/tests/patch/preflight.spec.ts b/packages/fs/tool-fs/tests/patch/preflight.spec.ts index d1184de7ae..4060deb1f7 100644 --- a/packages/fs/tool-fs/tests/patch/preflight.spec.ts +++ b/packages/fs/tool-fs/tests/patch/preflight.spec.ts @@ -11,7 +11,7 @@ it('reports syntax errors before resolving paths', async () => { const resolve = vi.spyOn(t.fs, 'resolve') expect(await t.run('invalid')).toEqual({ phase: 'syntax', failure: { code: 'PATCH_SYNTAX', message: "Invalid patch at line 1: Expected '*** Begin Patch'", - }, outcomes: [], affectedPaths: [], cancelled: false }) + } }) expect(resolve).not.toHaveBeenCalled() expect(t.observations).toEqual([]) await expect(t.bytes('a')).rejects.toMatchObject({ code: 'ENOENT' }) @@ -24,11 +24,10 @@ it('collects independent unread and confirmed-absent failures and skips valid wr await t.absent('b') const a = t.record('update', 'a') const b = t.record('update', 'b', 1, 6) - expect(await t.run(patch(update('a'), update('b'), add('c')))).toEqual({ phase: 'preflight', outcomes: [ - { status: 'failed', operation: a, failure: { code: 'FS_NOT_OBSERVED', message: `cannot modify "${a.path}": file has not been read — read the file, then retry` }, changes: [] }, - { status: 'failed', operation: b, failure: { code: 'FS_NOT_FOUND', message: `cannot edit "${b.path}": not found` }, changes: [] }, - { status: 'skipped', operation: t.record('add', 'c', 2, 10) }, - ], affectedPaths: [], cancelled: false }) + expect(await t.run(patch(update('a'), update('b'), add('c')))).toEqual({ phase: 'preflight', refusals: [ + { operation: a, failure: { code: 'FS_NOT_OBSERVED', message: `cannot modify "${a.path}": file has not been read — read the file, then retry` } }, + { operation: b, failure: { code: 'FS_NOT_FOUND', message: `cannot edit "${b.path}": not found` } }, + ] }) expect(await t.bytes('a')).toBe('old\n') await expect(t.bytes('c')).rejects.toMatchObject({ code: 'ENOENT' }) expect(t.observations).toEqual([]) @@ -44,11 +43,10 @@ it.each([ const t = await setup() await t.file('a', 'old\n') const a = t.record('update', 'a') - expect(await t.run(patch(`*** Update File: a\n${marker}\n-${pattern}\n+new`, update('a')))).toEqual({ phase: 'preflight', outcomes: [ - { status: 'failed', operation: a, failure: { code: 'FS_PATCH_MISMATCH', hunkLine: 3, - message: `cannot patch "${a.path}": hunk at patch line 3 (${stage}) does not match — ${preview}` }, changes: [] }, - { status: 'skipped', operation: t.record('update', 'a', 1, 6) }, - ], affectedPaths: [], cancelled: false }) + expect(await t.run(patch(`*** Update File: a\n${marker}\n-${pattern}\n+new`, update('a')))).toEqual({ phase: 'preflight', refusals: [ + { operation: a, failure: { code: 'FS_PATCH_MISMATCH', hunkLine: 3, + message: `cannot patch "${a.path}": hunk at patch line 3 (${stage}) does not match — ${preview}` } }, + ] }) expect(await t.bytes('a')).toBe('old\n') expect(t.observations).toEqual([]) expect(await t.editIntent('a')).toEqual({ version: await t.version('a') }) @@ -59,10 +57,10 @@ it.skipIf(process.platform === 'win32').each(['delete', 'move'] as const)('rejec await t.file('a', 'old\n') await symlink('a', resolve(t.workspace, 'link')) const a = t.record(kind, 'link', 0, 2, kind === 'move' ? 'b' : undefined) - expect(await t.run(patch(kind === 'delete' ? remove('link') : move('link', 'b')))).toEqual({ phase: 'preflight', outcomes: [ - { status: 'failed', operation: a, failure: { code: 'PATCH_SYMLINK_SOURCE', - message: `cannot ${kind} "${a.path}": path is a symbolic link — operate on its target path instead` }, changes: [] }, - ], affectedPaths: [], cancelled: false }) + expect(await t.run(patch(kind === 'delete' ? remove('link') : move('link', 'b')))).toEqual({ phase: 'preflight', refusals: [ + { operation: a, failure: { code: 'PATCH_SYMLINK_SOURCE', + message: `cannot ${kind} "${a.path}": path is a symbolic link — operate on its target path instead` } }, + ] }) expect(await t.bytes('link')).toBe('old\n') expect(t.observations).toEqual([]) expect(await t.editIntent('a')).toEqual({ version: await t.version('a') }) @@ -72,9 +70,9 @@ it('rejects a move to an alias of its source', async () => { const t = await setup() await t.file('a', 'old\n') const a = t.record('move', 'a', 0, 2, 'a') - expect(await t.run(patch(move('a', './a')))).toEqual({ phase: 'preflight', outcomes: [ - { status: 'failed', operation: a, failure: { code: 'FS_SAME_TARGET', message: `cannot move "${a.path}" to itself` }, changes: [] }, - ], affectedPaths: [], cancelled: false }) + expect(await t.run(patch(move('a', './a')))).toEqual({ phase: 'preflight', refusals: [ + { operation: a, failure: { code: 'FS_SAME_TARGET', message: `cannot move "${a.path}" to itself` } }, + ] }) expect(await t.bytes('a')).toBe('old\n') expect(t.observations).toEqual([]) expect(await t.editIntent('a')).toEqual({ version: await t.version('a') }) @@ -86,9 +84,9 @@ it.each(['add', 'move'] as const)('attributes an unread %s destination to its re await t.file('b', 'existing\n', false) const operation = kind === 'add' ? t.record('add', 'b') : t.record('move', 'a', 0, 2, 'b') const b = t.record('add', 'b') - expect(await t.run(patch(kind === 'add' ? add('b') : move('a', 'b')))).toEqual({ phase: 'preflight', outcomes: [ - { status: 'failed', operation, failure: { code: 'FS_NOT_OBSERVED', message: `cannot modify "${b.path}": file has not been read — read the file, then retry` }, changes: [] }, - ], affectedPaths: [], cancelled: false }) + expect(await t.run(patch(kind === 'add' ? add('b') : move('a', 'b')))).toEqual({ phase: 'preflight', refusals: [ + { operation, failure: { code: 'FS_NOT_OBSERVED', message: `cannot modify "${b.path}": file has not been read — read the file, then retry` } }, + ] }) expect(await t.bytes('a')).toBe('old\n') expect(await t.bytes('b')).toBe('existing\n') expect(t.observations).toEqual([]) @@ -99,9 +97,9 @@ it('rejects every operation under read-only policy', async () => { const t = await setup({ mode: 'read-only' }) await t.file('a', 'old\n') const records = [t.record('add', 'b'), t.record('update', 'a', 1, 4), t.record('delete', 'a', 2, 8), t.record('move', 'a', 3, 9, 'c')] - expect(await t.run(patch(add('b'), update('a'), remove('a'), move('a', 'c')))).toEqual({ phase: 'preflight', outcomes: records.map(operation => ({ - status: 'failed', operation, failure: { code: 'FS_SANDBOX_DENIED', message: expect.stringContaining('[sandbox: file access denied under read-only mode]') as string }, changes: [], - })), affectedPaths: [], cancelled: false }) + expect(await t.run(patch(add('b'), update('a'), remove('a'), move('a', 'c')))).toEqual({ phase: 'preflight', refusals: records.map(operation => ({ + operation, failure: { code: 'FS_SANDBOX_DENIED', message: expect.stringContaining('[sandbox: file access denied under read-only mode]') as string }, + })) }) expect(await t.bytes('a')).toBe('old\n') await expect(t.bytes('b')).rejects.toMatchObject({ code: 'ENOENT' }) expect(t.observations).toEqual([]) @@ -113,10 +111,9 @@ it('prevents all writes when a move destination is outside writable roots', asyn await t.file('a', 'old\n') const outside = resolve(process.cwd(), 'patch-preflight-outside-root') const a = t.record('move', 'a', 1, 4, outside) - expect(await t.run(patch(add('b'), move('a', outside)))).toEqual({ phase: 'preflight', outcomes: [ - { status: 'skipped', operation: t.record('add', 'b') }, - { status: 'failed', operation: a, failure: { code: 'FS_SANDBOX_DENIED', message: expect.stringContaining('[sandbox: file access denied under workspace-write mode]') as string }, changes: [] }, - ], affectedPaths: [], cancelled: false }) + expect(await t.run(patch(add('b'), move('a', outside)))).toEqual({ phase: 'preflight', refusals: [ + { operation: a, failure: { code: 'FS_SANDBOX_DENIED', message: expect.stringContaining('[sandbox: file access denied under workspace-write mode]') as string } }, + ] }) expect(await t.bytes('a')).toBe('old\n') await expect(t.bytes('b')).rejects.toMatchObject({ code: 'ENOENT' }) expect(t.observations).toEqual([]) @@ -129,9 +126,9 @@ it.each(['update', 'delete', 'add', 'move'] as const)('rejects non-regular %s ta await t.file('a', 'old\n') const operation = kind === 'move' ? t.record(kind, 'a', 0, 2, 'directory') : t.record(kind, 'directory') const text = kind === 'add' ? add('directory') : kind === 'delete' ? remove('directory') : kind === 'move' ? move('a', 'directory') : update('directory') - expect(await t.run(patch(text))).toEqual({ phase: 'preflight', outcomes: [ - { status: 'failed', operation, failure: { code: 'FS_NOT_REGULAR_FILE', message: `cannot modify "${resolve(t.workspace, 'directory')}": not a regular file` }, changes: [] }, - ], affectedPaths: [], cancelled: false }) + expect(await t.run(patch(text))).toEqual({ phase: 'preflight', refusals: [ + { operation, failure: { code: 'FS_NOT_REGULAR_FILE', message: `cannot modify "${resolve(t.workspace, 'directory')}": not a regular file` } }, + ] }) expect(await t.bytes('a')).toBe('old\n') expect((await t.fs.stat(await t.target('directory')))?.type).toBe('directory') expect(t.observations).toEqual([]) @@ -144,9 +141,9 @@ it.each(['update', 'add'] as const)('rejects a stale observed version before %s' const observed = await t.editIntent('a') await writeFile(resolve(t.workspace, 'a'), 'external edit\n') const a = t.record(kind, 'a') - expect(await t.run(patch(kind === 'add' ? add('a') : update('a')))).toEqual({ phase: 'preflight', outcomes: [ - { status: 'failed', operation: a, failure: { code: 'FS_STALE_VERSION', message: `cannot ${kind === 'add' ? 'write' : 'edit'} "${a.path}": file changed since it was read — re-read the file, then retry` }, changes: [] }, - ], affectedPaths: [], cancelled: false }) + expect(await t.run(patch(kind === 'add' ? add('a') : update('a')))).toEqual({ phase: 'preflight', refusals: [ + { operation: a, failure: { code: 'FS_STALE_VERSION', message: `cannot ${kind === 'add' ? 'write' : 'edit'} "${a.path}": file changed since it was read — re-read the file, then retry` } }, + ] }) expect(await t.bytes('a')).toBe('external edit\n') expect(t.observations).toEqual([]) expect(await t.editIntent('a')).toEqual(observed) @@ -156,9 +153,9 @@ it.each(['FS_NOT_TEXT', 'FS_TOO_LARGE'] satisfies FsErrorCode[])('preserves a pr const t = await setup() await t.file('a', 'old\n') vi.spyOn(t.fs, 'readText').mockRejectedValue(new FsError('cannot read fixture', code)) - expect(await t.run(patch(update('a')))).toEqual({ phase: 'preflight', outcomes: [ - { status: 'failed', operation: t.record('update', 'a'), failure: { code, message: 'cannot read fixture' }, changes: [] }, - ], affectedPaths: [], cancelled: false }) + expect(await t.run(patch(update('a')))).toEqual({ phase: 'preflight', refusals: [ + { operation: t.record('update', 'a'), failure: { code, message: 'cannot read fixture' } }, + ] }) expect(await t.bytes('a')).toBe('old\n') expect(t.observations).toEqual([]) expect(await t.editIntent('a')).toEqual({ version: await t.version('a') }) @@ -167,12 +164,10 @@ it.each(['FS_NOT_TEXT', 'FS_TOO_LARGE'] satisfies FsErrorCode[])('preserves a pr it.each(['delete', 'move'] as const)('simulates the missing source after %s without performing it', async (kind) => { const t = await setup() await t.file('a', 'old\n') - const first = t.record(kind, 'a', 0, 2, kind === 'move' ? 'b' : undefined) const a = t.record('update', 'a', 1, kind === 'move' ? 7 : 3) - expect(await t.run(patch(kind === 'delete' ? remove('a') : move('a', 'b'), update('a')))).toEqual({ phase: 'preflight', outcomes: [ - { status: 'skipped', operation: first }, - { status: 'failed', operation: a, failure: { code: 'FS_NOT_FOUND', message: `cannot edit "${a.path}": not found` }, changes: [] }, - ], affectedPaths: [], cancelled: false }) + expect(await t.run(patch(kind === 'delete' ? remove('a') : move('a', 'b'), update('a')))).toEqual({ phase: 'preflight', refusals: [ + { operation: a, failure: { code: 'FS_NOT_FOUND', message: `cannot edit "${a.path}": not found` } }, + ] }) expect(await t.bytes('a')).toBe('old\n') expect(t.observations).toEqual([]) expect(await t.editIntent('a')).toEqual({ version: await t.version('a') }) @@ -204,10 +199,9 @@ it('rejects repeated deletion against simulated absence', async () => { const t = await setup() await t.file('a', 'old\n') const a = t.record('delete', 'a') - expect(await t.run(patch(remove('a'), remove('a')))).toEqual({ phase: 'preflight', outcomes: [ - { status: 'skipped', operation: a }, - { status: 'failed', operation: t.record('delete', 'a', 1, 3), failure: { code: 'FS_NOT_FOUND', message: `cannot delete "${a.path}": not found` }, changes: [] }, - ], affectedPaths: [], cancelled: false }) + expect(await t.run(patch(remove('a'), remove('a')))).toEqual({ phase: 'preflight', refusals: [ + { operation: t.record('delete', 'a', 1, 3), failure: { code: 'FS_NOT_FOUND', message: `cannot delete "${a.path}": not found` } }, + ] }) expect(await t.bytes('a')).toBe('old\n') expect(t.observations).toEqual([]) expect(await t.editIntent('a')).toEqual({ version: await t.version('a') }) @@ -218,10 +212,9 @@ it('preserves source simulation after a move destination fails admission', async await t.file('a', 'old\n') await t.file('b', 'unread\n', false) const a = t.record('move', 'a', 0, 2, 'b') - expect(await t.run(patch(move('a', 'b'), update('a')))).toEqual({ phase: 'preflight', outcomes: [ - { status: 'failed', operation: a, failure: { code: 'FS_NOT_OBSERVED', message: `cannot modify "${a.destination}": file has not been read — read the file, then retry` }, changes: [] }, - { status: 'skipped', operation: t.record('update', 'a', 1, 7) }, - ], affectedPaths: [], cancelled: false }) + expect(await t.run(patch(move('a', 'b'), update('a')))).toEqual({ phase: 'preflight', refusals: [ + { operation: a, failure: { code: 'FS_NOT_OBSERVED', message: `cannot modify "${a.destination}": file has not been read — read the file, then retry` } }, + ] }) expect(await t.bytes('a')).toBe('old\n') expect(await t.bytes('b')).toBe('unread\n') expect(t.observations).toEqual([]) @@ -232,9 +225,8 @@ it('preserves source simulation after a move destination fails admission', async it('names the missing move source operation', async () => { const t = await setup({ policy: false }) expect(await t.run(patch(move('missing', 'destination')))).toEqual({ - phase: 'preflight', outcomes: [{ status: 'failed', operation: t.record('move', 'missing', 0, 2, 'destination'), - failure: { code: 'FS_NOT_FOUND', message: `cannot move "${resolve(t.workspace, 'missing')}": not found` }, changes: [] }], - affectedPaths: [], cancelled: false, + phase: 'preflight', refusals: [{ operation: t.record('move', 'missing', 0, 2, 'destination'), + failure: { code: 'FS_NOT_FOUND', message: `cannot move "${resolve(t.workspace, 'missing')}": not found` } }], }) }) diff --git a/packages/fs/tool-fs/tests/patch/resolution.spec.ts b/packages/fs/tool-fs/tests/patch/resolution.spec.ts index a1cfea1d32..9803e00fac 100644 --- a/packages/fs/tool-fs/tests/patch/resolution.spec.ts +++ b/packages/fs/tool-fs/tests/patch/resolution.spec.ts @@ -11,12 +11,9 @@ it('collects path resolution failures while retaining resolved paths and checkin if (path === './denied') throw new FsError('cannot resolve denied path', 'FS_PERMISSION_DENIED') return resolve(path, options) }) - expect(await t.run(patch(update('a'), add('./denied'), add('c')))).toEqual({ phase: 'preflight', outcomes: [ - { status: 'skipped', operation: t.record('update', 'a') }, - { status: 'failed', operation: { kind: 'add', path: './denied', index: 1, patchLine: 6 }, - failure: { code: 'FS_PERMISSION_DENIED', message: 'cannot resolve denied path' }, changes: [] }, - { status: 'skipped', operation: t.record('add', 'c', 2, 8) }, - ], affectedPaths: [], cancelled: false }) + expect(await t.run(patch(update('a'), add('./denied'), add('c')))).toEqual({ phase: 'preflight', refusals: [ + { operation: { kind: 'add', path: './denied', index: 1, patchLine: 6 }, failure: { code: 'FS_PERMISSION_DENIED', message: 'cannot resolve denied path' } }, + ] }) expect(await t.bytes('a')).toBe('old\n') await expect(t.bytes('c')).rejects.toMatchObject({ code: 'ENOENT' }) expect(t.observations).toEqual([]) @@ -31,25 +28,22 @@ it('retains a resolved move source when only its destination cannot be resolved' if (path === './denied') throw new FsError('destination unread', 'FS_NOT_OBSERVED') return resolve(path, options) }) - expect(await t.run(patch(move('a', './denied'), update('a')))).toEqual({ phase: 'preflight', outcomes: [ - { status: 'failed', operation: { ...t.record('move', 'a'), destination: './denied' }, - failure: { code: 'FS_NOT_OBSERVED', message: 'cannot modify "./denied": file has not been read — read the file, then retry' }, changes: [] }, - { status: 'skipped', operation: t.record('update', 'a', 1, 7) }, - ], affectedPaths: [], cancelled: false }) + expect(await t.run(patch(move('a', './denied'), update('a')))).toEqual({ phase: 'preflight', refusals: [ + { operation: { ...t.record('move', 'a'), destination: './denied' }, + failure: { code: 'FS_NOT_OBSERVED', message: 'cannot modify "./denied": file has not been read — read the file, then retry' } }, + ] }) expect(await t.bytes('a')).toBe('old\n') expect(t.observations).toEqual([]) expect(await t.editIntent('a')).toEqual({ version: await t.version('a') }) }) -it('returns cancelled resolution records for a signal aborted before entry', async () => { +it('returns aborted resolution refusals for a signal aborted before entry', async () => { const t = await setup() t.controller.abort() - expect(await t.run(patch(add('a'), move('b', 'c')))).toEqual({ phase: 'preflight', outcomes: [ - { status: 'failed', operation: { kind: 'add', path: 'a', index: 0, patchLine: 2 }, - failure: { code: 'FS_ABORTED', message: 'resolve aborted' }, changes: [] }, - { status: 'failed', operation: { kind: 'move', path: 'b', destination: 'c', index: 1, patchLine: 4 }, - failure: { code: 'FS_ABORTED', message: 'resolve aborted' }, changes: [] }, - ], affectedPaths: [], cancelled: true }) + expect(await t.run(patch(add('a'), move('b', 'c')))).toEqual({ phase: 'preflight', refusals: [ + { operation: { kind: 'add', path: 'a', index: 0, patchLine: 2 }, failure: { code: 'FS_ABORTED', message: 'resolve aborted' } }, + { operation: { kind: 'move', path: 'b', destination: 'c', index: 1, patchLine: 4 }, failure: { code: 'FS_ABORTED', message: 'resolve aborted' } }, + ] }) await expect(t.bytes('a')).rejects.toMatchObject({ code: 'ENOENT' }) expect(t.observations).toEqual([]) await expect(t.editIntent('a')).rejects.toMatchObject({ code: 'FS_NOT_OBSERVED' }) diff --git a/packages/fs/tool-fs/tests/read-image.spec.ts b/packages/fs/tool-fs/tests/read-image.spec.ts index 3657dbda8c..a6f57263d1 100644 --- a/packages/fs/tool-fs/tests/read-image.spec.ts +++ b/packages/fs/tool-fs/tests/read-image.spec.ts @@ -514,7 +514,7 @@ describe('argument and service preconditions', () => { expect(text(nonImage)).toContain('the .txt extension does not declare a supported image format') }) - it.each([{}, { toolFs: { read: false, write: false, edit: false, enableApplyPatch: false } }] satisfies SetupOptions[]) + it.each([{}, { toolFs: { read: false, write: false, edit: false, applyPatch: false } }] satisfies SetupOptions[]) ('refuses when no attachment service is mounted: %j', async (config) => { await writeFile(join(dir, 'red.png'), PNG_1X1) const ctx = await setup({ ...config, attachments: false }) @@ -762,7 +762,7 @@ describe('image admission failures', () => { describe('registration surface', () => { it('registers only read_image without text guidance', async () => { - const ctx = await setup({ toolFs: { read: false, write: false, edit: false, enableApplyPatch: false } }) + const ctx = await setup({ toolFs: { read: false, write: false, edit: false, applyPatch: false } }) try { expect(ctx.tools.schemas().map(tool => tool.name)).toEqual(['read_image']) expect((await ctx.systemPrompt.assemble()).sections.some(section => section.name.startsWith('tool:'))).toBe(false) @@ -790,7 +790,7 @@ describe('registration surface', () => { await ctx.plugin(ToolRuntime) await ctx.plugin(LocalFileSystem, { cwd: dir }) const attachment = await ctx.plugin(LocalAttachmentStore, { dshHome: home }) - const plugin = await ctx.plugin(ToolFs, { read: false, write: false, edit: false, enableApplyPatch: false }) + const plugin = await ctx.plugin(ToolFs, { read: false, write: false, edit: false, applyPatch: false }) const names = () => ctx.tools.schemas().map(tool => tool.name).sort() expect(names()).toEqual(['read_image']) await attachment.dispose() diff --git a/packages/fs/tool-fs/tests/tools.spec.ts b/packages/fs/tool-fs/tests/tools.spec.ts index 5b4f5d445e..e038fe169e 100644 --- a/packages/fs/tool-fs/tests/tools.spec.ts +++ b/packages/fs/tool-fs/tests/tools.spec.ts @@ -156,8 +156,8 @@ describe('registration', () => { { config: { read: false }, tools: ['apply_patch', 'edit', 'write'], sections: ['tool:apply_patch', 'tool:edit', 'tool:write'] }, { config: { write: false }, tools: ['apply_patch', 'edit', 'read'], sections: ['tool:apply_patch', 'tool:edit', 'tool:read'] }, { config: { edit: false }, tools: ['apply_patch', 'read', 'write'], sections: ['tool:apply_patch', 'tool:read', 'tool:write'] }, - { config: { enableApplyPatch: false }, tools: ['edit', 'read', 'write'], sections: ['tool:edit', 'tool:read', 'tool:write'] }, - { config: { read: false, readImage: false, write: false, edit: false, enableApplyPatch: false }, tools: [], sections: [] }, + { config: { applyPatch: false }, tools: ['edit', 'read', 'write'], sections: ['tool:edit', 'tool:read', 'tool:write'] }, + { config: { read: false, readImage: false, write: false, edit: false, applyPatch: false }, tools: [], sections: [] }, ])('omits disabled tools and their guidance: $config', async ({ config, tools, sections }) => { const { ctx } = await setup(config) try { @@ -171,7 +171,7 @@ describe('registration', () => { } }) - it.each([{ read: 'false' }, { readImage: 1 }, { write: [] }, { edit: {} }, { enableApplyPatch: 'true' }])('rejects non-boolean switches at configuration parsing: %j', async (config) => { + it.each([{ read: 'false' }, { readImage: 1 }, { write: [] }, { edit: {} }, { applyPatch: 'true' }])('rejects non-boolean switches at configuration parsing: %j', async (config) => { expect((await ToolFs.Config['~standard'].validate(config)).issues).toBeDefined() }) @@ -1078,7 +1078,7 @@ const originalGuidance = { describe('scope-aware filesystem guidance', () => { it.each(Array.from({ length: 8 }, (_, mask) => mask))('preserves exact text for visible tools (mask %i)', async (mask) => { - const { ctx } = await setup({ enableApplyPatch: false }) + const { ctx } = await setup({ applyPatch: false }) const { key, scope } = await guidanceScope(ctx) const names = ['read', 'write', 'edit'] as const const allow = names.filter((_, index) => (mask & (1 << index)) !== 0) @@ -1101,7 +1101,7 @@ describe('scope-aware filesystem guidance', () => { }) it('honors deny filters and the existing exemption for own-scope tools', async () => { - const { ctx } = await setup({ enableApplyPatch: false }) + const { ctx } = await setup({ applyPatch: false }) const { key, scope } = await guidanceScope(ctx) const write = ctx.tools.get('write')! scope.ctx.tools.restrict({ deny: ['write', 'edit'] }) diff --git a/packages/session/session-title/src/index.ts b/packages/session/session-title/src/index.ts index 83b30ada7f..6a7e9acdde 100644 --- a/packages/session/session-title/src/index.ts +++ b/packages/session/session-title/src/index.ts @@ -415,12 +415,15 @@ export class SessionTitleService extends Service { } const state = this.stateFor(session) this.supersede(state, 'user rename superseded automatic title generation') - const event = session.append('session/title', { + session.append('session/title', { title: normalized, messageSeqs: [], source: { kind: 'user' }, }) - return titleSnapshotFromState({ ...event.data, eventSeq: event.seq, updatedAt: event.time }) + const snapshot = this.get(session) + /* v8 ignore next -- unreachable: the append above just committed a session/title event. */ + if (snapshot === undefined) throw new Error('renamed title failed to fold') + return snapshot } /** diff --git a/packages/ssh/fs-ssh/README.i18n.yaml b/packages/ssh/fs-ssh/README.i18n.yaml index ac017d90a6..3ddc2ae46e 100644 --- a/packages/ssh/fs-ssh/README.i18n.yaml +++ b/packages/ssh/fs-ssh/README.i18n.yaml @@ -15,8 +15,8 @@ en: d152484eb41ac6b4 zh: 725b47765ee8f30b /deepseek-ai-dsh-fs-ssh/use-this-package: - en: 2636cc38510bff3c - zh: f350936e43675c04 + en: 33fa4c10b96c7ade + zh: 4fd3db455c3c14cf /deepseek-ai-dsh-fs-ssh/understand-the-implementation: en: df9be336b11850b2 zh: 6748e9993fa1e2ac diff --git a/packages/ssh/fs-ssh/README.md b/packages/ssh/fs-ssh/README.md index dfdefe18d3..b59bed0e25 100644 --- a/packages/ssh/fs-ssh/README.md +++ b/packages/ssh/fs-ssh/README.md @@ -29,7 +29,7 @@ Mount this provider with [`dsh-ssh`](../ssh/README.md) and `sandboxPolicy`; use `resolve()` canonicalizes paths on the remote host. `processPath()` and `fileUrl()` name files in that same remote namespace; they do not grant host-side access. File URLs encode literal percent signs, backslashes and newlines without changing the filename. `processPathFromHostPath()` returns `undefined`, so consumers requiring an installed executable or bootstrap must supply a remote artifact explicitly. -Reads preserve shared filesystem error codes. All five mutations send per-call policy to the helper. `patchText`, `deleteFile`, and `moveFile` use `fs.patch`, `fs.delete`, and `fs.move`; their guards and results follow the local provider, including a published destination with failed source removal. Responses are strictly validated. Cancellation or transport loss can leave the remote outcome unknown; no request is replayed and no committed effect is rolled back. +Reads preserve the shared filesystem error codes. All five mutations send the resolved per-call policy to the helper; `patchText`, `deleteFile`, and `moveFile` use `fs.patch`, `fs.delete`, and `fs.move`, with guards and results matching the local provider. Lost transport reports an I/O failure; a mutation may already have committed and is not retried automatically. ----- diff --git a/packages/ssh/fs-ssh/README.zh.md b/packages/ssh/fs-ssh/README.zh.md index 71ffc8f6e4..1e5f66b18b 100644 --- a/packages/ssh/fs-ssh/README.zh.md +++ b/packages/ssh/fs-ssh/README.zh.md @@ -29,7 +29,7 @@ kind: "package-reference" `resolve()` 在远端主机上规范化路径。`processPath()` 与 `fileUrl()` 在同一个远端命名空间中标识文件,并不授予主机侧访问能力。文件 URL 对字面的百分号、反斜杠和换行进行编码,保留原文件名。`processPathFromHostPath()` 返回 `undefined`,因此需要已安装可执行文件或引导程序的消费方必须显式提供远端产物。 -读取保留共享文件系统错误码。五个变更操作都会向辅助进程发送逐次调用策略。`patchText`、`deleteFile` 和 `moveFile` 使用 `fs.patch`、`fs.delete` 和 `fs.move`;其防护和结果遵循本地提供方,包括目标已发布但源删除失败的结果。响应经过严格校验。取消或传输丢失可能使远端结果未知;请求不会重放,已提交效果不会回滚。 +读取保留共享文件系统错误码。五个变更操作都会向辅助进程发送已解析的逐次调用策略;`patchText`、`deleteFile` 和 `moveFile` 使用 `fs.patch`、`fs.delete` 和 `fs.move`,防护和结果与本地提供方一致。传输丢失报告为 I/O 失败;变更可能已经提交,且不会自动重试。 ----- diff --git a/packages/ssh/fs-ssh/tests/provider.spec.ts b/packages/ssh/fs-ssh/tests/provider.spec.ts index ce9bdfa6a1..ee2c740f14 100644 --- a/packages/ssh/fs-ssh/tests/provider.spec.ts +++ b/packages/ssh/fs-ssh/tests/provider.spec.ts @@ -210,7 +210,7 @@ it.each([false, true])('forwards patch/delete/move with explicit policy %s and p const selected = explicit ? policy : undefined const resolved = explicit ? policy : { mode: 'read-only', workspaceRoot: '/remote/work' } const expected = { version: FsVersion('v1') } - const hunks = [{ lineNumber: 3, context: 'anchor', oldLines: ['old'], newLines: ['new'], contextLineIndices: [], isEndOfFile: false }] + const hunks = [{ lineNumber: 3, context: 'anchor', oldLines: ['old'], newLines: ['new'], isEndOfFile: false }] const edited = { version: 'v2', before: 'old', after: 'new' } const deleted = { before: 'old' } const destination = { targetKey: FsTargetKey('/remote/work/new.txt'), displayPath: 'new.txt' } diff --git a/packages/ssh/ssh/README.i18n.yaml b/packages/ssh/ssh/README.i18n.yaml index 7b7bdb9ea7..c5f21fe720 100644 --- a/packages/ssh/ssh/README.i18n.yaml +++ b/packages/ssh/ssh/README.i18n.yaml @@ -21,8 +21,8 @@ en: 9be90e9dad103726 zh: af9e050b0ab50d04 /deepseek-ai-dsh-ssh/understand-the-implementation: - en: 1dde8ea95db9fdc1 - zh: 98b91c4d6ddca232 + en: 6156b76f56e241c3 + zh: 6688f94f6db930e4 /deepseek-ai-dsh-ssh/further-exploration: en: 1d179b90a32af26b zh: 683cf3c0085147e1 diff --git a/packages/ssh/ssh/README.md b/packages/ssh/ssh/README.md index 997d9ec7ef..efa3539c42 100644 --- a/packages/ssh/ssh/README.md +++ b/packages/ssh/ssh/README.md @@ -52,7 +52,7 @@ Pass the verified `ctx.ssh.ptcLaunch` as the `launch` configuration of [`NodePtc ## Understand the implementation -The helper dispatches `fs.patch`, `fs.delete`, and `fs.move` through its sandboxed filesystem provider. Strict schemas validate hunk positions, line arrays, context index pairs, source/destination guards, and complete or partial outcomes. Hunk payloads share the connection frame limit. Protocol framing remains version 1; deployment must install the matching helper artifact and update `helperHash`. +The helper dispatches `fs.patch`, `fs.delete`, and `fs.move` through its sandboxed filesystem provider; strict schemas validate hunks, source/destination guards, and complete or partial move outcomes. Hunk payloads share the connection frame limit.
Implementation internals — click to expand diff --git a/packages/ssh/ssh/README.zh.md b/packages/ssh/ssh/README.zh.md index 9a8c677fba..022395d817 100644 --- a/packages/ssh/ssh/README.zh.md +++ b/packages/ssh/ssh/README.zh.md @@ -52,7 +52,7 @@ kind: "package-reference" ## 理解实现 -辅助进程通过其沙箱文件系统提供方分派 `fs.patch`、`fs.delete` 和 `fs.move`。严格 schema 校验 hunk 位置、行数组、上下文索引对、源/目标防护,以及完整或部分结果。hunk 负载使用连接帧上限。协议帧格式保持版本 1;部署必须安装匹配的辅助进程产物并更新 `helperHash`。 +辅助进程通过其沙箱文件系统提供方分派 `fs.patch`、`fs.delete` 和 `fs.move`;严格 schema 校验 hunk、源/目标防护,以及完整或部分的移动结果。hunk 负载使用连接帧上限。
实现细节 — 点击展开 diff --git a/packages/ssh/ssh/src/schemas.ts b/packages/ssh/ssh/src/schemas.ts index 50ec39ddf5..aaa88e1603 100644 --- a/packages/ssh/ssh/src/schemas.ts +++ b/packages/ssh/ssh/src/schemas.ts @@ -37,7 +37,6 @@ export const editResultSchema = z.object({ version: z.string(), before: z.string export const hunkSchema = z.object({ lineNumber: z.number().int().positive(), context: z.string().optional(), oldLines: z.array(z.string()), newLines: z.array(z.string()), - contextLineIndices: z.array(z.tuple([z.number().int().nonnegative(), z.number().int().nonnegative()])), isEndOfFile: z.boolean(), }).strict().transform(({ context, ...hunk }) => context === undefined ? hunk : { ...hunk, context }) /** Source version guard used by patch, delete, and move. */ diff --git a/packages/ssh/ssh/tests/helper-runtime.spec.ts b/packages/ssh/ssh/tests/helper-runtime.spec.ts index 6c4e7e16c3..c41e459aa5 100644 --- a/packages/ssh/ssh/tests/helper-runtime.spec.ts +++ b/packages/ssh/ssh/tests/helper-runtime.spec.ts @@ -119,7 +119,7 @@ describe.skipIf(process.platform === 'win32')('guarded patch helper dispatch', ( const target = await test.client.request('fs.resolve', { path: 'a' }, targetSchema) const destination = await test.client.request('fs.resolve', { path: 'b' }, targetSchema) const info = await test.client.request('fs.stat', { target }, infoSchema) - const hunks = [{ lineNumber: 3, oldLines: ['old'], newLines: ['new'], contextLineIndices: [], isEndOfFile: false }] + const hunks = [{ lineNumber: 3, oldLines: ['old'], newLines: ['new'], isEndOfFile: false }] const patched = await test.client.request('fs.patch', { target, hunks, expected: guarded ? { version: info.version } : undefined, policy: policy(test.root) }, editResultSchema) expect(patched).toMatchObject({ before: 'old\n', after: 'new\n' }) expect(await readFile(`${test.root}/a`, 'utf8')).toBe('new\r\n') @@ -152,10 +152,9 @@ describe.skipIf(process.platform === 'win32')('guarded patch helper dispatch', ( it.each([ { lineNumber: 0 }, { lineNumber: 1.5 }, { context: 1 }, { oldLines: [1] }, { newLines: [false] }, - { contextLineIndices: [[-1, 0]] }, { contextLineIndices: [[0, 0.5]] }, { contextLineIndices: [[0]] }, { isEndOfFile: 'true' }, { extra: true }, ])('rejects invalid hunk fields %j', (fields) => { - const valid = { lineNumber: 3, oldLines: ['old'], newLines: ['new'], contextLineIndices: [[0, 0]], isEndOfFile: false } + const valid = { lineNumber: 3, oldLines: ['old'], newLines: ['new'], isEndOfFile: false } expect(hunkSchema.safeParse({ ...valid, ...fields }).success).toBe(false) expect(moveOptionsSchema.safeParse({ hunks: [{ ...valid, ...fields }] }).success).toBe(false) }) diff --git a/packages/subprocess/subprocess-local/src/spawn-runner.ts b/packages/subprocess/subprocess-local/src/spawn-runner.ts index 6239f38c65..b0d7681c5f 100644 --- a/packages/subprocess/subprocess-local/src/spawn-runner.ts +++ b/packages/subprocess/subprocess-local/src/spawn-runner.ts @@ -306,7 +306,7 @@ class WindowsJobRunner { }, 0) return } - const api = this.api = this.internals.loadWin32ProcessBindings() + this.api = this.internals.loadWin32ProcessBindings() const spawned = this.internals.spawnCurrentTokenJobProcess(this.api, { command: command as string, applicationName, @@ -320,7 +320,7 @@ class WindowsJobRunner { for (const fileDescriptor of request.control === 'pipe' ? [4, 5, 6, SUBPROCESS_CONTROL_FD] : [4, 5, 6]) { this.internals.closeFileDescriptor(fileDescriptor) } - this.pollTimer = setInterval(() => { this.poll(api) }, 10) + this.pollTimer = setInterval(() => { this.poll() }, 10) } catch (error) { if (this.jobHandle === undefined && error instanceof Win32Error && error.api === 'CreateProcessW') { await this.publishTerminalResult({ @@ -352,19 +352,21 @@ class WindowsJobRunner { this.internals.terminateJob(this.api, job, 1) } - private poll(api: CurrentTokenProcessBindings): void { + private poll(): void { if (this.finished) return + /* v8 ignore next -- poll is installed only after start() stores the bindings; retained as a defensive invariant guard. */ + if (this.api === undefined) return try { if (this.processHandle !== undefined) { - const exitCode = this.internals.pollProcessExit(api, this.processHandle) + const exitCode = this.internals.pollProcessExit(this.api, this.processHandle) if (exitCode !== undefined) { - this.internals.closeHandleChecked(api, this.processHandle, 'ordinary direct process') + this.internals.closeHandleChecked(this.api, this.processHandle, 'ordinary direct process') this.processHandle = undefined void this.publishTerminalResult({ type: 'target-exit', exitCode }) } } - if (this.jobHandle !== undefined && this.internals.isJobEmpty(api, this.jobHandle)) { - this.internals.closeHandleChecked(api, this.jobHandle, 'ordinary process Job') + if (this.jobHandle !== undefined && this.internals.isJobEmpty(this.api, this.jobHandle)) { + this.internals.closeHandleChecked(this.api, this.jobHandle, 'ordinary process Job') this.jobHandle = undefined if (this.resultDelivered) this.finish(0) } diff --git a/packages/subprocess/subprocess-local/tests/spawn-runner.spec.ts b/packages/subprocess/subprocess-local/tests/spawn-runner.spec.ts index 3942c40ab5..e2df9d213a 100644 --- a/packages/subprocess/subprocess-local/tests/spawn-runner.spec.ts +++ b/packages/subprocess/subprocess-local/tests/spawn-runner.spec.ts @@ -395,7 +395,7 @@ describe('runner launch inputs', () => { } expect(resolveWindowsExecutable('bash', 'C:\\target', { Path: 'relative;"C:\\semi;colon";"C:\\tools\\git\\bin";C:\\later', - }, exists, {})).toBe('C:\\tools\\git\\bin\\bash.exe') + }, exists)).toBe('C:\\tools\\git\\bin\\bash.exe') expect(probed).toEqual([ 'C:\\target\\bash.com', 'C:\\target\\bash.exe', @@ -408,7 +408,7 @@ describe('runner launch inputs', () => { ]) expect(resolveWindowsExecutable('local.exe', 'C:\\target', {}, candidate => - candidate === 'C:\\target\\local.exe', {})).toBe('C:\\target\\local.exe') + candidate === 'C:\\target\\local.exe')).toBe('C:\\target\\local.exe') expect(resolveWindowsExecutable('tool', 'C:\\target', { PATH: 'C:\\bin', }, candidate => candidate === 'C:\\bin\\tool.com', { @@ -416,28 +416,28 @@ describe('runner launch inputs', () => { })).toBe('C:\\bin\\tool.com') expect(resolveWindowsExecutable('tool', 'C:\\target', { PATH: 'D:relative', - }, candidate => candidate === 'D:relative\\tool.exe', {})).toBe('D:relative\\tool.exe') + }, candidate => candidate === 'D:relative\\tool.exe')).toBe('D:relative\\tool.exe') expect(resolveWindowsExecutable('tool.', 'C:\\target', {}, candidate => - candidate === 'C:\\target\\tool.exe', {})).toBe('C:\\target\\tool.exe') - expect(resolveWindowsExecutable('.\\missing', 'C:\\target', {}, () => false, {})) + candidate === 'C:\\target\\tool.exe')).toBe('C:\\target\\tool.exe') + expect(resolveWindowsExecutable('.\\missing', 'C:\\target', {}, () => false)) .toBeUndefined() expect(resolveWindowsExecutable('tool', 'C:\\target', { PATH: ';;C:\\bin', - }, candidate => candidate === 'C:\\bin\\tool.exe', {})).toBe('C:\\bin\\tool.exe') + }, candidate => candidate === 'C:\\bin\\tool.exe')).toBe('C:\\bin\\tool.exe') expect(resolveWindowsExecutable('tool', 'C:\\target', { PATH: '"";C:\\bin', - }, candidate => candidate === 'C:\\bin\\tool.exe', {})).toBe('C:\\bin\\tool.exe') + }, candidate => candidate === 'C:\\bin\\tool.exe')).toBe('C:\\bin\\tool.exe') expect(resolveWindowsExecutable('tool', 'C:\\target', { PATH: '"unterminated', - }, candidate => candidate === 'C:\\target\\unterminated\\tool.exe', {})) + }, candidate => candidate === 'C:\\target\\unterminated\\tool.exe')) .toBe('C:\\target\\unterminated\\tool.exe') expect(resolveWindowsExecutable('\\\\server\\share\\tool', 'C:\\target', {}, candidate => - candidate === '\\\\server\\share\\tool.exe', {})).toBe('\\\\server\\share\\tool.exe') + candidate === '\\\\server\\share\\tool.exe')).toBe('\\\\server\\share\\tool.exe') expect(resolveWindowsExecutable('\\tools\\tool', 'C:\\target', {}, candidate => - candidate === 'C:\\tools\\tool.exe', {})).toBe('C:\\tools\\tool.exe') + candidate === 'C:\\tools\\tool.exe')).toBe('C:\\tools\\tool.exe') expect(resolveWindowsExecutable('C:tools\\tool', 'C:\\target', {}, candidate => - candidate === 'C:\\target\\tools\\tool.exe', {})).toBe('C:\\target\\tools\\tool.exe') + candidate === 'C:\\target\\tools\\tool.exe')).toBe('C:\\target\\tools\\tool.exe') const noSearchEnvironment = { NoDefaultCurrentDirectoryInExePath: '1' } expect(resolveWindowsExecutable('missing', 'C:\\target', {}, () => false, noSearchEnvironment)) diff --git a/scripts/no-unknown-casts.baseline.json b/scripts/no-unknown-casts.baseline.json index 5e2d80be2d..41520f4e27 100644 --- a/scripts/no-unknown-casts.baseline.json +++ b/scripts/no-unknown-casts.baseline.json @@ -174,6 +174,7 @@ "packages/api/gateway/tests/gateway.host.spec.ts": { "344e2cbbed2a8ef8d5356a24def92ac3453ffaa2efa58f5481d59206f5b79cbc": 1, "529f85662621d1be427dd178aee815ab0595dc05bc2324545d4914e17515dace": 1, + "52f51bf0e468505650c8d072d3f1d77b1d2e16492a6c7b5082e7e6c15240d7be": 1, "7fedb21312a9c9efcaa4f269ad32fb590191f31e6ca49b000abcda55e9dac795": 1, "b1a7aab550fc7e44d0e10a7926466553a9ca5735ae32debf5834d14854eb2b27": 1 }, diff --git a/scripts/rescope-vendor.ts b/scripts/rescope-vendor.ts index d262f128b2..e6292fb2a5 100644 --- a/scripts/rescope-vendor.ts +++ b/scripts/rescope-vendor.ts @@ -76,26 +76,6 @@ interface GenericSkip { } const GENERIC_SKIPS: readonly GenericSkip[] = [ - // Preset ids and extensions event names retain their literal cordis spelling. - { file: 'apps/cli/tests/optional-bundle-transitions.e2e.ts', upstream: ['cordis'] }, - { file: 'apps/web/tests/agent-preset-selection.e2e.ts', upstream: ['cordis'] }, - { file: 'apps/web/tests/developer-tools-settings.e2e.ts', upstream: ['cordis'] }, - { file: 'docs/subsystems/schedule.md', upstream: ['cordis'] }, - { file: 'docs/subsystems/schedule.zh.md', upstream: ['cordis'] }, - { file: 'docs/upgrade-guide/v0.2.0-rc.2/schedule-bundle-retired/guide.md', upstream: ['cordis'] }, - { file: 'docs/upgrade-guide/v0.2.0-rc.2/schedule-bundle-retired/guide.zh.md', upstream: ['cordis'] }, - { file: 'docs/upgrade-guide/v0.2.1-alpha.1/subagent-preset-composition/guide.md', upstream: ['cordis'] }, - { file: 'docs/upgrade-guide/v0.2.1-alpha.1/subagent-preset-composition/guide.zh.md', upstream: ['cordis'] }, - { file: 'docs/user/guide/schedule.md', upstream: ['cordis'] }, - { file: 'docs/user/guide/schedule.zh.md', upstream: ['cordis'] }, - { file: 'packages/bundle/web-app/cordis.patch.yml', upstream: ['cordis'] }, - { file: 'packages/client/ui-agent-preset/src/client/CreatePluginMenuItem.tsx', upstream: ['cordis'] }, - { file: 'packages/client/ui-agent-preset/tests/components.client.spec.tsx', upstream: ['cordis'] }, - { file: 'packages/client/ui-agent-preset/tests/create-plugin-menu-item.client.spec.tsx', upstream: ['cordis'] }, - { file: 'packages/client/ui-agent-preset/tests/section-store.client.spec.ts', upstream: ['cordis'] }, - { file: 'packages/extensions/cordis-host-runner/tests/inspect-registry.spec.ts', upstream: ['cordis'] }, - { file: 'snapshots/session/cordis-inspect-liveness/client-fixture.mjs', upstream: ['cordis'] }, - { file: 'snapshots/session/cordis-inspect-timeout/client-fixture.mjs', upstream: ['cordis'] }, // `Symbol.for('schemastery')` and the `vendor:` metadata field are upstream identifiers. { file: 'vendor/schemastery/src/index.ts', upstream: ['schemastery'] }, // Narrows a Standard Schema by the same upstream `vendor:` identifier. diff --git a/snapshots/session/compaction-output-reserve/cordis.snapshot.yml b/snapshots/session/compaction-output-reserve/cordis.snapshot.yml index 243aeaf10a..98ee410af1 100644 --- a/snapshots/session/compaction-output-reserve/cordis.snapshot.yml +++ b/snapshots/session/compaction-output-reserve/cordis.snapshot.yml @@ -4,7 +4,7 @@ # Keep the recorded tool set for the fixed compaction thresholds. - id: tool-fs config: - enableApplyPatch: false + applyPatch: false - id: agent-instructions disabled: true diff --git a/snapshots/session/compaction-recovery/cordis.snapshot.yml b/snapshots/session/compaction-recovery/cordis.snapshot.yml index 77595e514f..dccb8a0242 100644 --- a/snapshots/session/compaction-recovery/cordis.snapshot.yml +++ b/snapshots/session/compaction-recovery/cordis.snapshot.yml @@ -31,4 +31,4 @@ # Keep the recorded tool set shared by the fixed-budget compaction scenarios. - id: tool-fs config: - enableApplyPatch: false + applyPatch: false diff --git a/snapshots/session/compaction-summary-headroom/cordis.snapshot.yml b/snapshots/session/compaction-summary-headroom/cordis.snapshot.yml index 1eeab2fe42..4593e4f50a 100644 --- a/snapshots/session/compaction-summary-headroom/cordis.snapshot.yml +++ b/snapshots/session/compaction-summary-headroom/cordis.snapshot.yml @@ -4,7 +4,7 @@ # Keep the recorded tool set for the fixed compaction thresholds. - id: tool-fs config: - enableApplyPatch: false + applyPatch: false - id: agent-instructions disabled: true