Merge remote-tracking branch 'origin/master' into worktree/fix-minimal-preset-prompt

# Conflicts:
#	apps/cli/config/agent-presets/minimal/preset.yml
This commit is contained in:
Yichen Jiang
2026-08-10 21:05:29 +08:00
101 changed files with 1385 additions and 269 deletions
@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-30-bounded-overwrite-diff-basis.md
2026-07-30-bounded-overwrite-diff-basis.md: 7a09934bd1798059de43a092f338d37aa9ccbd9a
2026-07-30-bounded-overwrite-diff-basis.zh.md: 1d6bdd1068d119aae859132d9f8216ca29d0dc11
@@ -0,0 +1,31 @@
# Agent Note: Bound overwrite contextual-diff bases at the provider
Status: implemented
English | [中文](2026-07-30-bounded-overwrite-diff-basis.zh.md)
## Problem
`dsh-fs-local` returned the complete prior file in `FsWriteOutcome.before` so consumers could build a contextual overwrite diff. That presentation-only pre-read was unbounded: a large overwrite could allocate the entire prior file, and checking an earlier path stat alone could not enforce a limit because an external process could replace or grow the file between the stat and the read. A large replacement also made the contextual hunk approach the replacement size even when the prior file was small. This closes the deferred bound recorded by [result-time applied-hunk diffs](../../archived/architecture/2026-07-02-result-time-applied-hunk-diffs.md).
## Decision
`LocalFileSystem.Config.diffBasisMaxBytes` is a positive safe-integer deployment setting no greater than the runtime's Buffer-allocation and string-decoding limits, with a 10 MiB default. An overwrite supplies `before` only when the UTF-8 replacement is strictly below that limit and the prior file opened for the basis also ends below it. The prior read opens a descriptor, checks that descriptor, and reads at most the configured byte count in cancellation-aware chunks; reaching the boundary returns `null`. A size change after descriptor stat also returns `null`, even if the final size remains below the limit, because a partial prefix would be an incorrect diff basis. Binary or invalid UTF-8 prior content likewise returns `null`, as does any descriptor-phase errno — a prior file deleted or made unreadable between the caller's preflight and the basis open cannot fail a write the caller already committed to; only cancellation and non-errno faults propagate. These outcomes do not block the atomic write.
The local provider owns this decision because `before` is its optional, best-effort basis: it can avoid acquiring prior content that the configured pair limit has already made ineligible. `tool-fs` continues to own diff computation, retention, and presentation. The setting is independent of `tool-fs.readStreamMinSize`; read routing and overwrite presentation are different policies and need not share a value.
`before: null` asks consumers to use their existing whole-file fallback. The limit bounds only the extra prior-content acquisition and eligibility for a contextual pair. It does not bound the caller-owned replacement, the returned `after` value, or a consumer's fallback rendering.
## Alternatives considered
**Keep a hardcoded threshold equal to the read tool's streaming threshold.** Rejected because the read threshold is deployment-configurable and consumer-owned. Two same-valued constants would create an unenforced cross-package coupling, while the overwrite basis is itself a deployment memory/presentation choice.
**Gate only the prior side in the provider and cap new-content diffing in `tool-fs`.** Rejected because it would acquire prior text even when the provider's configured pair limit already excludes the replacement, and it would split one `before` eligibility rule across two plugins. Consumers remain free to impose additional output limits.
**Trust the initial `probe()` size before using an ordinary whole-file read.** Rejected because that size can become stale before the read. The descriptor reader must enforce the bound on the object it actually reads.
**Stream a contextual diff for arbitrarily large pairs.** Rejected for this bug fix because the current filesystem seam returns complete `before`/`after` strings and the current diff implementation consumes them. A streaming diff would require a separate cross-package protocol and presentation design.
## Consequences
Deployments can tune the extra overwrite-basis cost without changing read routing. At or above the exclusive limit, overwrites still succeed and remain visible through the whole-file fallback, but lose contextual hunks. Below the limit, the provider can still hold almost `diffBasisMaxBytes` of prior text in addition to the caller's replacement. The bounded descriptor read adds an open/stat/read sequence for eligible overwrites, while preventing a stale path probe from turning that sequence into an unbounded allocation.
@@ -0,0 +1,31 @@
# Agent Note: 在提供方限制覆写上下文 diff 基础
Status: implemented
[English](2026-07-30-bounded-overwrite-diff-basis.md) | 中文
## Problem
`dsh-fs-local` 会在 `FsWriteOutcome.before` 中返回完整旧文件,供消费方生成覆写上下文 diff。这个仅用于展示的预读没有上限:大文件覆写可能分配整个旧文件;而仅检查较早的路径 stat 也无法真正实施上限,因为外部进程可以在 stat 与读取之间替换文件或扩大文件。即使旧文件很小,大替换内容也会使上下文 hunk 接近替换内容本身的大小。本改动关闭了 [result-time applied-hunk diff](../../archived/architecture/2026-07-02-result-time-applied-hunk-diffs.md) 中记录的暂缓上限事项。
## Decision
`LocalFileSystem.Config.diffBasisMaxBytes` 是一个不超过运行时 Buffer 分配和字符串解码上限的正安全整数部署配置,默认 10 MiB。只有当 UTF-8 替换内容严格低于该上限,且为生成基础而打开的旧文件最终也低于该上限时,覆写才提供 `before`。旧文件读取会打开文件描述符、检查该描述符,并按可响应取消的分块最多读取配置的字节数;一旦到达边界便返回 `null`。描述符 stat 后发生大小变化时同样返回 `null`,即使最终大小仍低于上限,因为部分前缀会成为错误的 diff 基础。旧内容为二进制或无效 UTF-8 时也返回 `null`;描述符阶段的任何 errno 同样如此——旧文件在调用方预检之后、基础读取打开之前被删除或变得不可读,不能让调用方已经提交的写入失败;只有取消和非 errno 故障会继续向上传播。这些结果都不会阻止原子写入。
本地提供方拥有该决策,因为 `before` 是它提供的可选、尽力而为的基础:当配置的成对上限已使替换内容不合格时,它可以避免获取旧内容。`tool-fs` 继续拥有 diff 计算、保留与展示。该配置独立于 `tool-fs.readStreamMinSize`;读取路由与覆写展示是不同策略,无需共享数值。
`before: null` 要求消费方使用既有的整文件回退。该上限只限制额外获取旧内容的成本,以及上下文内容对是否合格;它不限制调用方持有的替换内容、返回的 `after` 值或消费方的回退渲染。
## Alternatives considered
**保留一个与读取工具流式阈值相等的硬编码阈值。** 否决,因为读取阈值可由部署配置,且归消费方所有。两个同值常量会形成无法强制的一致性耦合,而覆写基础本身也是部署层面的内存与展示选择。
**提供方只限制旧内容一侧,并在 `tool-fs` 中限制新内容 diff。** 否决,因为当提供方配置的成对上限已经排除替换内容时,这仍会获取旧文本;同时会把同一条 `before` 合格规则拆到两个插件中。消费方仍可自由施加额外的输出限制。
**信任初次 `probe()` 的大小,再执行普通整文件读取。** 否决,因为该大小可能在读取前变旧;描述符读取必须对它真正读取的对象实施上限。
**为任意大的内容对流式生成上下文 diff。** 本次缺陷修复不采用,因为当前文件系统 seam 返回完整的 `before`/`after` 字符串,当前 diff 实现也消费这两个字符串。流式 diff 需要独立的跨包协议与展示设计。
## Consequences
部署可以调整额外的覆写基础成本,而不改变读取路由。达到或超过排他上限时,覆写仍会成功,并通过整文件回退保持可见,但不再提供上下文 hunk。低于上限时,除调用方的替换内容外,提供方仍可能持有接近 `diffBasisMaxBytes` 的旧文本。对于合格覆写,有上限的描述符读取会增加一次 open/stat/read 序列,同时防止陈旧路径探测把该序列变成无上限分配。