diff --git a/.agents/notes/implemented/simplification/2026-08-28-windows-only-absent-probe-repair.i18n.yaml b/.agents/notes/implemented/simplification/2026-08-28-windows-only-absent-probe-repair.i18n.yaml new file mode 100644 index 0000000000..e4d02c9697 --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-08-28-windows-only-absent-probe-repair.i18n.yaml @@ -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/simplification/2026-08-28-windows-only-absent-probe-repair.md +2026-08-28-windows-only-absent-probe-repair.md: 55fa7bee8d6e16eb96223fa9745f95891cfdf89c +2026-08-28-windows-only-absent-probe-repair.zh.md: 5a459cab1f3d914934945465be1d1f08e0c4530b diff --git a/.agents/notes/implemented/simplification/2026-08-28-windows-only-absent-probe-repair.md b/.agents/notes/implemented/simplification/2026-08-28-windows-only-absent-probe-repair.md new file mode 100644 index 0000000000..55fa7bee8d --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-08-28-windows-only-absent-probe-repair.md @@ -0,0 +1,40 @@ +# Agent Note: The absent-probe parent repair runs on Windows only + +Status: implemented + +English | [中文](2026-08-28-windows-only-absent-probe-repair.zh.md) + +## Problem + +`JsonlSessionPersistence.exists` treats ENOENT as absence, and before returning false it stats the path's parent so a session directory blocked by a regular file surfaces as a storage fault rather than as a missing session. Windows needs that: it reports ENOENT, not ENOTDIR, for `regular-file/child`. POSIX does not — `open(2)` specifies ENOTDIR when a component of the path prefix is not a directory, which the repair's own guard comment already recorded. + +The stat ran on every platform and on every absent probe. `findLog` issues four probes per project directory — two rejecting the legacy flat-file layout, one for the opposite encoding, one for the log itself — and the coordinator resolves an id twice per `inspect`: once in `prepareCore`, once in `isPreparedSourceCurrent`, which runs even when the prepared source is cached. All but one directory answers absent, so nearly every probe paid the extra stat. + +## Decision + +The repair is reached only under `process.platform === 'win32'`, matching the platform dispatch `materialize` already uses. POSIX keeps the ENOTDIR that `open` itself reports. + +## Testing + +Counting `node:fs/promises` calls through the suite's existing module mock, against a store with five project directories — the layout of a real `~/.dsh/sessions`: + +| Operation | Before | After | +|---|---|---| +| `load` an existing session | 40 open + 41 stat | 40 open + 3 stat | +| `load` an absent id | 20 open + 20 stat | 20 open + 0 stat | + +The package's 242 tests pass unchanged and per-file coverage is identical; the new branch carries the same `v8 ignore` marker as the sibling platform dispatch. + +That POSIX never reaches the repair was confirmed directly rather than taken from the comment: opening `regular-file/child` and `regular-file/child/deeper` reports ENOTDIR on macOS, while only a genuinely missing directory reports ENOENT. + +## Alternatives considered + +**Keep the stat on every platform as defense in depth.** Rejected: on POSIX it can only confirm what `open` already reported, so it detects no fault the caller would otherwise miss — it doubles the syscalls of every absent probe to re-derive a known answer. + +**Remove the repair outright and let Windows report absence.** Rejected: it exists so a session directory blocked by a regular file stays a storage fault instead of reading as a missing session, which is the fail-loud stance the backend takes elsewhere. + +## Consequences + +Path resolution keeps its full cost in `open` calls: four per project directory per lookup, twice per `inspect`. This removes the stat half only, and the remaining cost still grows with the number of project directories rather than with the number of sessions. + +Collapsing the scan needs an id-to-path index, which first requires deciding what happens to the per-lookup legacy-artifact guard that `rejects a compressed obsolete flat-file artifact during targeted lookup` pins: that test writes its artifact after `list()` has memoized the root encoding check, so the guard covers store mutation after memoization, and an index hit would return before reaching it. One `readdir` per project directory, which would supply both the id-to-path map and that guard without a cache, measured no faster than this change, so any further win requires memoization and its invalidation. diff --git a/.agents/notes/implemented/simplification/2026-08-28-windows-only-absent-probe-repair.zh.md b/.agents/notes/implemented/simplification/2026-08-28-windows-only-absent-probe-repair.zh.md new file mode 100644 index 0000000000..5a459cab1f --- /dev/null +++ b/.agents/notes/implemented/simplification/2026-08-28-windows-only-absent-probe-repair.zh.md @@ -0,0 +1,40 @@ +# Agent Note: 缺失探测的父目录修复只在 Windows 上执行 + +Status: implemented + +[English](2026-08-28-windows-only-absent-probe-repair.md) | 中文 + +## Problem + +`JsonlSessionPersistence.exists` 把 ENOENT 视为「不存在」,并在返回 false 之前 stat 一次路径的父目录,好让「被普通文件挡住的会话目录」暴露成存储故障,而不是一个不存在的会话。Windows 需要这一步:它对 `regular-file/child` 报的是 ENOENT 而不是 ENOTDIR。POSIX 不需要——`open(2)` 规定路径前缀中有非目录组件时报 ENOTDIR,这一点该修复自己的守卫注释早已写明。 + +而这次 stat 在所有平台、每一次缺失探测上都会执行。`findLog` 对每个 project 目录发四次探测——两次拒绝 legacy 扁平文件布局、一次查相反编码、一次查日志本身——而 coordinator 每次 `inspect` 要解析两遍 id:一遍在 `prepareCore`,一遍在 `isPreparedSourceCurrent`,后者即使 prepared source 已缓存也照跑。除拥有该会话的那个目录外,其余全部回答「不存在」,所以几乎每次探测都付了这次多余的 stat。 + +## Decision + +该修复现在只在 `process.platform === 'win32'` 下可达,与 `materialize` 既有的平台分派写法一致。POSIX 保留 `open` 自己报出的 ENOTDIR。 + +## Testing + +借用测试套件既有的模块 mock 统计 `node:fs/promises` 调用,对照一个五 project 目录的存储——即真实 `~/.dsh/sessions` 的布局: + +| 操作 | 改动前 | 改动后 | +|---|---|---| +| `load` 一个已存在的会话 | 40 open + 41 stat | 40 open + 3 stat | +| `load` 一个不存在的 id | 20 open + 20 stat | 20 open + 0 stat | + +该包的 242 个测试原样通过,单文件覆盖率不变;新增分支带的 `v8 ignore` 标记与相邻的平台分派一致。 + +「POSIX 永远到不了这处修复」是直接验证的,而非采信注释:在 macOS 上打开 `regular-file/child` 与 `regular-file/child/deeper` 都报 ENOTDIR,只有真正缺失的目录才报 ENOENT。 + +## Alternatives considered + +**所有平台都保留这次 stat,作为纵深防御。** 否决:在 POSIX 上它只能确认 `open` 已经报出的结论,检测不到任何调用方本会漏掉的故障——代价是把每次缺失探测的系统调用翻倍,只为重新推导一个已知答案。 + +**直接删掉该修复,让 Windows 报「不存在」。** 否决:它的存在正是为了让「被普通文件挡住的会话目录」保持为存储故障,而不是读成一个不存在的会话,这与该 backend 别处一贯的 fail-loud 立场一致。 + +## Consequences + +路径解析在 `open` 调用上的开销原样保留:每次查找每个 project 目录四次,每次 `inspect` 两轮。本次只削掉了 stat 那一半,剩余开销仍随 project 目录数增长,而不随会话数增长。 + +要压掉这轮扫描需要一份 id→path 索引,而它首先要求决定:`rejects a compressed obsolete flat-file artifact during targeted lookup` 钉住的那条「每次查找都做的 legacy 产物守卫」该如何安置。该测试是在 `list()` 已经记忆化 root 编码检查之后才写入产物的,所以这条守卫覆盖的是记忆化之后的存储变动,而索引命中会在到达它之前就返回。「每个 project 目录一次 `readdir`」本可以同时提供 id→path 映射和这条守卫且无需缓存,但实测并不比本次改动更快,所以更进一步的收益必然要引入记忆化及其失效处理。 diff --git a/packages/session/session-persistence-jsonl/src/index.ts b/packages/session/session-persistence-jsonl/src/index.ts index 4bed7aefb9..111af6dc08 100644 --- a/packages/session/session-persistence-jsonl/src/index.ts +++ b/packages/session/session-persistence-jsonl/src/index.ts @@ -953,11 +953,14 @@ export class JsonlSessionPersistence extends SessionPersistence implements Persi } catch (error) { // Only ENOENT means absent. A permission/I/O error must surface rather // than letting load or collision checks proceed under false absence. - // Windows reports ENOENT, not ENOTDIR, for `regular-file/child`; verify - // the immediate parent so a blocked session directory remains a storage fault. /* v8 ignore else -- Windows reports file-valued parents as ENOENT; POSIX covers direct ENOTDIR. */ if (isENOENT(error)) { - await this.assertLogParentAllowsAbsence(path) + // Windows reports ENOENT, not ENOTDIR, for `regular-file/child`, so it + // alone verifies the immediate parent to keep a blocked session + // directory a storage fault. POSIX open already reported ENOTDIR before + // this point, where the extra stat would only cost a syscall per probe. + /* v8 ignore next -- native Windows coverage exercises this platform dispatch; POSIX reports ENOTDIR from open */ + if (process.platform === 'win32') await this.assertLogParentAllowsAbsence(path) return false } /* v8 ignore next -- Windows repairs ENOTDIR from ENOENT above; POSIX covers direct ENOTDIR. */