Merge branch 'master' into hero-fish-hover-swim

This commit is contained in:
Yifffan 2026-08-28 11:51:06 +02:00 • committed by GitHub
commit 5e610e1a74
7 changed files with 120 additions and 15 deletions

View file

@ -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

View file

@ -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.

View file

@ -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 映射和这条守卫且无需缓存,但实测并不比本次改动更快,所以更进一步的收益必然要引入记忆化及其失效处理。

View file

@ -54,7 +54,7 @@ function SettingsPanel({ rows, renderSlot, activeId, onSelect, onClose }: PanelP
return () => { document.removeEventListener('keydown', onKeyDown) }
}, [onClose])
// Baseline focus management: entering the dialog lands on the close button.
// Entering the dialog focuses the close button; the root restores its trigger on close.
const closeButton = useRef<HTMLButtonElement | null>(null)
useEffect(() => { closeButton.current?.focus() }, [])
@ -106,10 +106,17 @@ export function SettingsRoot(props: SettingsRootComponentProps) {
const [open, setOpen] = useState(false)
const [activeId, setActiveId] = useState<string | undefined>(undefined)
const [completedOnboarding, setCompletedOnboarding] = useState<ReadonlySet<string>>(() => new Set())
const triggerButton = useRef<HTMLButtonElement | null>(null)
const wasOpen = useRef(open)
const close = useCallback(() => {
setOpen(false)
setActiveId(undefined)
}, [])
// Restore after the close commit, when the dialog can no longer own focus.
useEffect(() => {
if (wasOpen.current && !open) triggerButton.current?.focus()
wasOpen.current = open
}, [open])
const openSection = useCallback((id: string) => {
setActiveId(id)
setOpen(true)
@ -142,6 +149,7 @@ export function SettingsRoot(props: SettingsRootComponentProps) {
return (
<>
<button
ref={triggerButton}
type="button"
className={clsx(css.trigger, !wide && css.rail)}
aria-haspopup="dialog"

View file

@ -81,7 +81,10 @@ function mount({
}
function openPanel() {
fireEvent.click(screen.getByRole('button', { name: 'Settings' }))
const trigger = screen.getByRole('button', { name: 'Settings' })
trigger.focus()
fireEvent.click(trigger)
return trigger
}
describe('SettingsRoot trigger', () => {
@ -131,26 +134,29 @@ describe('SettingsPanel chrome seats', () => {
})
describe('SettingsPanel close paths', () => {
it('closes via the header button', () => {
it('closes via the header button and restores trigger focus', async () => {
mount()
openPanel()
const trigger = openPanel()
fireEvent.click(screen.getByRole('button', { name: 'Close' }))
expect(screen.queryByRole('dialog')).toBeNull()
await vi.waitFor(() => { expect(document.activeElement).toBe(trigger) })
})
it('closes via a mask click', () => {
it('closes via a mask click and restores trigger focus', async () => {
mount()
openPanel()
const trigger = openPanel()
const dialog = screen.getByRole('dialog')
fireEvent.click(dialog.parentElement!.firstElementChild!)
expect(screen.queryByRole('dialog')).toBeNull()
await vi.waitFor(() => { expect(document.activeElement).toBe(trigger) })
})
it('closes via document-level Escape and unhooks the listener with the panel', () => {
it('closes via document-level Escape, restores trigger focus, and unhooks the listener', async () => {
mount()
openPanel()
const trigger = openPanel()
fireEvent.keyDown(document, { key: 'Escape' })
expect(screen.queryByRole('dialog')).toBeNull()
await vi.waitFor(() => { expect(document.activeElement).toBe(trigger) })
// Ignored while closed (listener removed with the panel) and non-Escape
// keys are ignored while open.
fireEvent.keyDown(document, { key: 'Escape' })

View file

@ -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. */

View file

@ -442,15 +442,17 @@ describe('SessionProjectionCache cold-read seeding', () => {
// Host-only unit: folded but not served; the refreshed row is written
// back (fail-soft, fire-and-forget) once the write lands.
expect(Object.keys(snapshot.values)).not.toContain('cache-test/count')
await settle()
expect((await storedRows(root, meta.id))?.['cache-test/count']?.seq).toBe(4)
await vi.waitFor(async () => {
expect((await storedRows(root, meta.id))?.['cache-test/count']?.seq).toBe(4)
})
// No cached row yet: the first cold read folds from init over the full
// log and creates the cache row (the `?? {}` seed path).
const fresh = headerOf(SessionId('cold-fresh'), 10)
cache.coldSnapshot(fresh, events)
expect(apply).toHaveBeenCalledTimes(7) // 2 tail + 5 full
await settle()
expect((await storedRows(root, fresh.id))?.['cache-test/count']?.seq).toBe(4)
await vi.waitFor(async () => {
expect((await storedRows(root, fresh.id))?.['cache-test/count']?.seq).toBe(4)
})
})
it('coldSnapshot write-back is fail-soft: a failed durable write logs and never throws', async () => {