diff --git a/.agents/notes/implemented/bug-fix/2026-09-19-v3-incomplete-child-catalog-evidence.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-09-19-v3-incomplete-child-catalog-evidence.i18n.yaml index 7b9cc481fe..4ede5bd457 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-19-v3-incomplete-child-catalog-evidence.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-09-19-v3-incomplete-child-catalog-evidence.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-09-19-v3-incomplete-child-catalog-evidence.md -2026-09-19-v3-incomplete-child-catalog-evidence.md: ec6bb9c0c4bed133a758b528c579285caebbdcf0 -2026-09-19-v3-incomplete-child-catalog-evidence.zh.md: 8382be6ed28801e2b9ab925740262b4bdcf314a8 +2026-09-19-v3-incomplete-child-catalog-evidence.md: 22ca91502425257ed30aa4962f5a38497b8a8a92 +2026-09-19-v3-incomplete-child-catalog-evidence.zh.md: c11e6de2c27b9892a72c28f327b6d06834d84038 diff --git a/.agents/notes/implemented/bug-fix/2026-09-19-v3-incomplete-child-catalog-evidence.md b/.agents/notes/implemented/bug-fix/2026-09-19-v3-incomplete-child-catalog-evidence.md index ec6bb9c0c4..22ca915024 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-19-v3-incomplete-child-catalog-evidence.md +++ b/.agents/notes/implemented/bug-fix/2026-09-19-v3-incomplete-child-catalog-evidence.md @@ -12,13 +12,15 @@ A valid V3 child log can have no own descriptor, an unsupported descriptor versi The V3→V4 migration appends a missing parent catalog entry only when exactly one supported own child descriptor supplies its discovery fields. Other descriptor counts and unsupported versions contribute no new catalog fact. Existing parent entries and child events remain intact. This replaces only the missing-evidence refusal in the [adjacent migration decision](../architecture/2026-08-31-released-session-format-migrations.md). -Known child creation times must still match existing parent entries. Available unambiguous mode and label fields must also agree. Invalid known descriptor fields, corrupt logs, unreadable membership, unsupported selected generations, and changed source revisions retain their existing failures. +Known child creation times must still match existing parent entries. Mode/label and descriptor fields are checked only for exactly one supported own descriptor. Invalid fields in that descriptor, corrupt logs, unreadable membership, unsupported selected generations, and changed source revisions retain their existing failures. ## Alternatives considered **Refuse the whole parent.** Discovery metadata is insufficient to justify making valid parent history unavailable. -**Choose a descriptor or infer defaults.** Multiple descriptors do not establish which identity was admitted. Invented mode or label fields would become durable catalog facts without evidence. +**Choose the first or last descriptor.** [`foldSubagentDescriptor()`](../../../../packages/subagent/subagent/src/descriptor.ts) takes the first under the establishing provider’s exactly-once rule; the [identity projection](../../../../packages/subagent/subagent/src/projection.ts) takes the last to override inherited identities. Multiple own records violate that rule. Backfill requires one own record rather than choosing between these consumer policies. + +**Infer missing fields.** Invented mode or label fields would become durable catalog facts without evidence. ## Consequences diff --git a/.agents/notes/implemented/bug-fix/2026-09-19-v3-incomplete-child-catalog-evidence.zh.md b/.agents/notes/implemented/bug-fix/2026-09-19-v3-incomplete-child-catalog-evidence.zh.md index 8382be6ed2..c11e6de2c2 100644 --- a/.agents/notes/implemented/bug-fix/2026-09-19-v3-incomplete-child-catalog-evidence.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-09-19-v3-incomplete-child-catalog-evidence.zh.md @@ -12,13 +12,15 @@ Status: implemented V3→V4 迁移只在恰好一个受支持的自身子 descriptor 提供发现字段时,追加缺失的父目录项。其他 descriptor 数量及不受支持的版本不贡献新的目录事实。已有父目录项和子事件保持完整。这只替代[相邻迁移决策](../architecture/2026-08-31-released-session-format-migrations.zh.md)中的证据缺失拒绝规则。 -已知子创建时间仍必须与已有父目录项一致。可获得且明确的 mode 和 label 字段也必须一致。已知 descriptor 字段无效、日志损坏、成员关系不可读、所选代际不受支持以及源修订变化仍保留原有失败行为。 +已知子创建时间仍必须与已有父目录项一致。只有恰好一个受支持的自身 descriptor 时,才校验其字段并比较 mode/label。该 descriptor 字段无效、日志损坏、成员关系不可读、所选代际不受支持以及源修订变化仍保留原有失败行为。 ## Alternatives considered **拒绝整个父 Session。** 发现元数据不足以成为禁止读取有效父历史的理由。 -**选择一个 descriptor 或推断默认值。** 多个 descriptor 无法确定哪个身份已被准入。推断的 mode 或 label 会在没有证据的情况下成为持久目录事实。 +**选择首个或末个 descriptor。** [`foldSubagentDescriptor()`](../../../../packages/subagent/subagent/src/descriptor.ts) 依据建立提供方恰好写入一次的规则取首个;[身份投影](../../../../packages/subagent/subagent/src/projection.ts)取末个以覆盖继承身份。多个自身记录违反该规则。补填要求一个自身记录,而不在这两种消费者策略之间做选择。 + +**推断缺失字段。** 推断的 mode 或 label 会在没有证据的情况下成为持久目录事实。 ## Consequences diff --git a/packages/session/session-format-v3-to-v4/README.i18n.yaml b/packages/session/session-format-v3-to-v4/README.i18n.yaml index 14e8ceb3ce..1a211e967a 100644 --- a/packages/session/session-format-v3-to-v4/README.i18n.yaml +++ b/packages/session/session-format-v3-to-v4/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-format-v3-to-v4/README.md -README.md: c067355a8189cf3a23d79c5d59577434b3aa1c92 -README.zh.md: 4279967f2cb196dd8de40d6689536d3f51098673 +README.md: 5a251c8430e358f16216a0fdd09497bd29b463d8 +README.zh.md: c706e7f99d097e1ca6c1b4db2653ed9b7a6f1955 diff --git a/packages/session/session-format-v3-to-v4/README.md b/packages/session/session-format-v3-to-v4/README.md index c067355a81..5a251c8430 100644 --- a/packages/session/session-format-v3-to-v4/README.md +++ b/packages/session/session-format-v3-to-v4/README.md @@ -145,8 +145,8 @@ There is no recursive source search. Captured request text, assistant replay sta | One descriptor with version 1 | Require string provider and label; derive `mode: 'continuable'`. | | One descriptor with version 2 or 3 | Require string provider; use its mode and optional label under catalog rules. | | Zero descriptors, or an unsupported descriptor version | May retain an existing parent entry; cannot create a missing entry. | -| More than one own descriptor | Preserve the child; no unambiguous discovery fact is available for backfill. | -| Existing own parent entry | Retain it and its extensions; require matching child creation time and any available supported mode/label. | +| More than one own descriptor | Retain an existing parent entry without mode/label comparison; do not create a missing entry. | +| Existing own parent entry | Retain it and its extensions; require matching child creation time and mode/label from exactly one supported own descriptor, when available. | | Missing own parent entry with complete supported evidence | Append a version-0 catalog fact with child id, creation time, mode, and optional label. | | Missing own parent entry without complete evidence | Preserve the parent without inventing a catalog entry. | @@ -334,6 +334,7 @@ The edge preserves the recorded request prefix. Provider cache availability and - **Nested historical tool results** — migration refuses results containing another tool-result wrapper because flattening loses its call identity and error status. The original generation remains intact and no V4 successor is published; those histories need a preserving conversion before they can resume. - **Historical tool-result extensions** — outer message JSON properties remain own data properties, including `__proto__` and `constructor`. Unknown wrapper fields have no defined V4 destination and refuse migration. Existing outer `toolCallId` or `isError` fields must agree with the lifted result; conflicts refuse without publishing a successor. - **Retained child logs required** — a parent alone cannot recover unrecorded child ids, creation times, or descriptors. Deleted children cannot be reconstructed from tool arguments; existing parent catalog records remain. +- **Missing historical catalog entries** — without exactly one supported own descriptor, an absent parent entry is not backfilled. The child log remains readable by id; current V4 reads do not rescan children to repair that omission. - **Storage scope** — facts cover recognizable children within the same persistence root. Cross-root import and corrupt-log repair are outside this migration. diff --git a/packages/session/session-format-v3-to-v4/README.zh.md b/packages/session/session-format-v3-to-v4/README.zh.md index 4279967f2c..c706e7f99d 100644 --- a/packages/session/session-format-v3-to-v4/README.zh.md +++ b/packages/session/session-format-v3-to-v4/README.zh.md @@ -145,8 +145,8 @@ V3 未知内容标签变为 `plugin:`,其他字段原样保留 | 一个 version 1 descriptor | 要求字符串 provider 与 label;导出 `mode: 'continuable'`。 | | 一个 version 2 或 3 descriptor | 要求字符串 provider;按目录规则使用其 mode 和可选 label。 | | 没有 descriptor,或 descriptor 版本不受支持 | 可以保留已有父目录项;不能创建缺失项。 | -| 多个自身 descriptor | 保留子 Session;没有可用于补填的明确发现事实。 | -| 已有自身父目录项 | 保留条目及其扩展;要求子创建时间以及可用且受支持的 mode/label 一致。 | +| 多个自身 descriptor | 保留已有父目录项,不比较 mode/label;不创建缺失项。 | +| 已有自身父目录项 | 保留条目及其扩展;要求子创建时间一致,并在恰好一个受支持的自身 descriptor 可用时比较其 mode/label。 | | 缺少自身父目录项,且受支持证据完整 | 追加带 child id、创建时间、mode 和可选 label 的 version-0 目录事实。 | | 缺少自身父目录项,且证据不完整 | 保留父 Session,不凭空创建目录项。 | @@ -334,6 +334,7 @@ Fork 种子构造归核心 Session 所有,不属于此迁移。原生 V4 接 - **历史嵌套工具结果**——迁移会拒绝包含另一层 tool-result 包装的结果,因为展平会丢失其调用身份和错误状态。原始代际保持不变,也不会发布 V4 后继;这些历史需要能保留信息的转换才能继续运行。 - **历史工具结果扩展**——外层消息的 JSON 属性仍保留为自有数据属性,包括 `__proto__` 和 `constructor`。未知 wrapper 字段没有已定义的 V4 存放位置,因此拒绝迁移。已有外层 `toolCallId` 或 `isError` 字段必须与提升后的结果一致;冲突时拒绝且不发布 successor。 - **依赖保留的子日志**——仅凭父日志无法恢复未记录的子 id、创建时间或 descriptor。删除的子 Session 无法从工具参数恢复;已存在的父目录记录仍保留。 +- **历史目录项缺失**——没有恰好一个受支持的自身 descriptor 时,不补填父目录缺失项。子日志仍可按 id 读取;当前 V4 读取不会重新扫描子日志来补齐该条目。 - **存储范围**——事实只覆盖同一持久化根目录内可识别的子 Session。跨根目录导入和损坏日志修复不属于此迁移。 diff --git a/packages/session/session-persistence-jsonl/README.i18n.yaml b/packages/session/session-persistence-jsonl/README.i18n.yaml index 62795c5287..1589a1d5c7 100644 --- a/packages/session/session-persistence-jsonl/README.i18n.yaml +++ b/packages/session/session-persistence-jsonl/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/session/session-persistence-jsonl/README.md -README.md: 5907a09a109f2703f67817a20608499e38840dfd -README.zh.md: 0720388f29934579efa42a9a9b85e74f80651176 +README.md: 96c333d9f845943ff44c8d5d539c40d07b9dc0f6 +README.zh.md: 71126dd909c92aabdee5c07d23505f708dc5e038 diff --git a/packages/session/session-persistence-jsonl/README.md b/packages/session/session-persistence-jsonl/README.md index 5907a09a10..96c333d9f8 100644 --- a/packages/session/session-persistence-jsonl/README.md +++ b/packages/session/session-persistence-jsonl/README.md @@ -81,7 +81,7 @@ The current-generation scanner applies the current codec owner’s structural ad `open(id, 'read'|'write')` selects the highest canonical generation. Current input follows the ordinary fast path. For historical input, a read open decodes and migrates the source once, validates the current logical result, and returns it without publishing a successor. A write open reuses that revision-keyed preparation when available, or performs the same preparation, then encodes a same-directory temporary file in bounded chunks, verifies it in a Worker Thread, rechecks the source revision, and publishes the current successor without overwrite before returning. The source remains byte-identical. Source drift after preparation rejects that write open without replacing the logical history already returned to readers; a later write open prepares the new revision. The backend marks decoded event graphs `shared-frozen` when it freezes them before memoization; every nested object and array is frozen, and handle reads and slices preserve that state, including empty slices. Only an unmaterialized pending log reports `detached`. `stat(id)` and `list()` select and translate only the highest generation header without reading event rows or starting migration; snapshots carry the selected file’s `sizeBytes` and a best-effort revision. Current revisions identify that file; historical revisions also fingerprint the selected files across the persistence root, so child changes invalidate cached logical events. Fingerprinting reads filesystem metadata only; unrelated changes conservatively invalidate historical revisions. One `list()` call shares a corpus fingerprint across its historical entries. With `compression: 'none'`, the log is newline-delimited text an external reader can consume directly; the compressed default must be read through the backend. -Historical body preparation completes the parent catalog through [V3→V4](../session-format-v3-to-v4/README.md). It scans headers, decodes only direct children one at a time, and retains compact descriptor evidence plus source revisions. It rechecks membership and revisions before returning preparation, reusing it, and publishing. Source drift invalidates preparation: read open retries preparation once automatically, while write open refuses publication. Diagnostics name the changed, added, removed, or unreadable child path. An unsupported selected generation or unreadable header anywhere else in the scan refuses migration because it cannot establish complete membership. Ordinary listing still skips those files. Missing, unsupported, or multiple child descriptors prevent only that child’s catalog backfill; existing parent entries and child logs remain available. Invalid known descriptor fields still report an unsupported format; malformed child events, JSON records, and compressed data report corruption at the child path. Filesystem errors and cancellation retain their original errors. Current V4 opens bypass this scan and validate catalog fields, uniqueness, and current delivery ownership before exposing events. +Historical body preparation completes the parent catalog through [V3→V4](../session-format-v3-to-v4/README.md). It scans headers, decodes only direct children one at a time, and retains compact descriptor evidence plus source revisions. It rechecks membership and revisions before returning preparation, reusing it, and publishing. Source drift invalidates preparation: read open retries preparation once automatically, while write open refuses publication. Diagnostics name the changed, added, removed, or unreadable child path. An unsupported selected generation or unreadable header anywhere else in the scan refuses migration because it cannot establish complete membership. Ordinary listing still skips those files. Missing, unsupported, or multiple child descriptors prevent only that child’s catalog backfill; existing parent entries and child logs remain available. Invalid fields in exactly one supported own descriptor still report an unsupported format; malformed child events, JSON records, and compressed data report corruption at the child path. Filesystem errors and cancellation retain their original errors. Current V4 opens bypass this scan and validate catalog fields, uniqueness, and current delivery ownership before exposing events. Historical `stat` and `list` revisions require metadata work proportional to the root’s Session count. Fresh body preparation scans all selected headers and decodes direct-child bodies; memo reuse still scans membership and checks revisions. Read-only access never publishes an upgrade, so cold processes and evicted preparations repeat that work. Current V4 body reads and revisions avoid the historical corpus scan. See the [measured costs and diagnostic command](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md#catalog-scan-measurements). diff --git a/packages/session/session-persistence-jsonl/README.zh.md b/packages/session/session-persistence-jsonl/README.zh.md index 0720388f29..71126dd909 100644 --- a/packages/session/session-persistence-jsonl/README.zh.md +++ b/packages/session/session-persistence-jsonl/README.zh.md @@ -81,7 +81,7 @@ kind: "package-reference" `open(id, 'read'|'write')` 选择最高规范 generation。当前格式输入走普通快速路径。对于历史输入,只读 open 会单遍解码并迁移源、校验当前逻辑结果,然后在不发布后继的情况下返回。写 open 会在可用时复用按 revision 为键的 preparation,否则执行同一套 preparation,再按有界分片编码同目录临时文件、在 Worker Thread 中校验、复查源修订,并在返回前以不覆盖方式发布当前后继。源保持逐字节不变。如果源在 preparation 后发生变化,该次写 open 会失败,已经返回给读方的逻辑历史不会被替换;后续写 open 会针对新的 revision 重新执行 preparation。后端在 memo 化前冻结已解码的 event graph,并在此时将其标记为 `shared-frozen`;每个嵌套对象和数组都会冻结,句柄读取和 slice 即使为空也保留该状态。只有尚未实体化的 pending 空日志报告 `detached`。`stat(id)` 与 `list()` 只选择并转换最高 generation 的 header,不读取事件行,也不启动迁移;快照携带所选文件的 `sizeBytes` 与尽力而为的修订号。当前格式修订号标识该文件;历史格式修订号还包含持久化根目录中所选文件的指纹,因此子日志变化会使缓存的逻辑事件失效。指纹只读取文件系统元数据;无关变化也会保守地使历史修订号失效。一次 `list()` 为其历史条目共用一个全库指纹。选择 `compression: 'none'` 后,日志是外部读取方可直接消费的换行分隔文本;压缩默认值必须经后端读取。 -历史正文准备通过 [V3→V4](../session-format-v3-to-v4/README.zh.md) 补齐父目录:扫描 header,仅逐个解码直属子 Session,保留紧凑的 descriptor 证据及来源修订。在准备返回、复用和发布前重新检查成员集合与修订。来源变化会使准备缓存失效:只读打开自动重新准备一次,写打开则拒绝发布。诊断指出发生变化、新增、删除或无法读取的子日志路径。扫描中其他任何被选中的不支持代际或不可读 header 都会拒绝迁移,因为无法确认成员集合完整。普通列举仍跳过这些文件。子 descriptor 缺失、版本不受支持或存在多个时,只跳过该子 Session 的目录补填;已有父目录项和子日志仍可读取。已知 descriptor 字段无效仍会报告格式不支持;子事件、JSON 记录或压缩数据畸形会在子日志路径报告损坏。文件系统错误与取消保留原始错误。当前 V4 打开跳过此扫描,并在暴露事件之前检查目录字段、唯一性及当前投递归属。 +历史正文准备通过 [V3→V4](../session-format-v3-to-v4/README.zh.md) 补齐父目录:扫描 header,仅逐个解码直属子 Session,保留紧凑的 descriptor 证据及来源修订。在准备返回、复用和发布前重新检查成员集合与修订。来源变化会使准备缓存失效:只读打开自动重新准备一次,写打开则拒绝发布。诊断指出发生变化、新增、删除或无法读取的子日志路径。扫描中其他任何被选中的不支持代际或不可读 header 都会拒绝迁移,因为无法确认成员集合完整。普通列举仍跳过这些文件。子 descriptor 缺失、版本不受支持或存在多个时,只跳过该子 Session 的目录补填;已有父目录项和子日志仍可读取。恰好一个受支持的自身 descriptor 字段无效时,仍会报告格式不支持;子事件、JSON 记录或压缩数据畸形会在子日志路径报告损坏。文件系统错误与取消保留原始错误。当前 V4 打开跳过此扫描,并在暴露事件之前检查目录字段、唯一性及当前投递归属。 历史格式的 `stat` 与 `list` 修订号需要与根目录 Session 数量成正比的元数据工作。首次正文准备扫描所有所选 header 并解码直属子正文;复用准备缓存仍扫描成员集合并检查修订。只读访问从不发布升级,因此冷进程与被淘汰的准备缓存会重复这些工作。当前 V4 正文读取和修订号避开历史全库扫描。见[实测成本与诊断命令](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md#catalog-scan-measurements)。