fix(subagent): store unknown mode in the existing catalog event

This commit is contained in:
Dudu-0223 committed 2026-09-20 14:46:54 +08:00
1 parent 6f3140d7cf
commit f984683956
43 files changed
+703 -764

No files matched your search

@@ -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-session-local-subagent-migration.md
2026-09-19-session-local-subagent-migration.md: fcdbb7830a12bca645b2a20c68178fff97d74626
2026-09-19-session-local-subagent-migration.zh.md: bd6aaf5c434ec109cae2d3c68ddd29249debabf2
2026-09-19-session-local-subagent-migration.md: a44b6cdfe37656fc112f06187ef0c884e8b2fae1
2026-09-19-session-local-subagent-migration.zh.md: e1c790d4e19dbc0bab962592d9ff14b2e94fbdb0
@@ -12,7 +12,7 @@ Opening a historical parent requires discovery facts from its direct children. A
Opening A discovers candidate B Sessions through headers, reads their own descriptors, and completes A's catalog. Historical child reads use the V0–V3 catalog; current child reads use native validation. These reads neither prepare B's own catalog nor publish B's successor. Opening B separately performs B's migration and catalog preparation. Frontend discovery retains its parent-catalog source. Unknown-mode rows remain browsable; `list_agents` continues to select only known continuable children.
JSONL omits unreadable or unsupported headers from discovery. A child-body or descriptor-field failure produces a warning with the child path and retains the child's header identity. If the parent lacks a complete entry, migration appends `subagent/catalog-unknown`, projected as `mode: 'unknown'` alongside healthy siblings. Existing complete entries remain authoritative. Cancellation and source-consistency failures still stop the operation. Inspected child revisions, including failed decodes, are rechecked before reuse and publication so repairs cannot silently reuse stale evidence. Released predecessors remain byte-identical.
JSONL omits unreadable or unsupported headers from discovery. A child-body or descriptor-field failure produces a warning with the child path and retains the child's header identity. If the parent lacks a complete entry, migration appends `subagent/catalog`, projected as `mode: 'unknown'` alongside healthy siblings. Existing complete entries remain authoritative. Cancellation and source-consistency failures still stop the operation. Inspected child revisions, including failed decodes, are rechecked before reuse and publication so repairs cannot silently reuse stale evidence. Released predecessors remain byte-identical.
This partially supersedes child-failure propagation in [released migrations](../architecture/2026-08-31-released-session-format-migrations.md) and [incomplete child evidence](2026-09-19-v3-incomplete-child-catalog-evidence.md). Their format conversion, descriptor cardinality, conflict validation, and immutable publication rules remain active.
@@ -28,6 +28,6 @@ This partially supersedes child-failure propagation in [released migrations](../
## Consequences
Initial parent preparation still scans headers and reads direct-child bodies one at a time. It does not claim to eliminate this I/O. Published V4 parents use their catalog without rescanning children. Unknown entries are not automatically rewritten after publication. Opening a child retries its log and determines browsing capabilities from its actual descriptor, or reports the child-local error. A separate required-on-read event preserves the finalized V4 catalog payload while making unknown membership durable; older readers that lack this event refuse the log.
Initial parent preparation still scans headers and reads direct-child bodies one at a time. It does not claim to eliminate this I/O. Published V4 parents use their catalog without rescanning children. Unknown entries are not automatically rewritten after publication. Opening a child retries its log and determines browsing capabilities from its actual descriptor, or reports the child-local error. Catalog mode adds `unknown` without changing existing alternatives or the V4 header. The persistence checker admits this scoped extension; older readers may reject records using the new mode.
Raw and compressed tests cover catalog completion, child-local errors, source changes and repairs, unchanged child generations, and deferred child-catalog preparation. The shipped Web migration snapshot checks the parent catalog with a corrupt sibling, then opens the child independently. Its additional historical child raises the corpus budget from ten to eleven roles; the guard rejects a twelfth role.
@@ -12,7 +12,7 @@ Status: implemented
打开 A 时通过 header 发现候选 B Session,读取其自身 descriptor,并补齐 A 的 catalog。历史子日志使用 V0–V3 catalog 读取,当前子日志使用原生校验。这些读取不准备 B 自己的 catalog,也不发布 B 的后继。单独打开 B 时才执行 B 的迁移与目录准备。前端发现继续读取父 catalog。模式未知的条目仍可浏览;`list_agents` 继续只选择已知可继续的子会话。
JSONL 在发现时跳过不可读或不支持的 header。子正文或 descriptor 字段失败会产生带子路径的警告,并保留该子的 header 身份。父目录没有完整条目时,迁移追加 `subagent/catalog-unknown`,与健康兄弟项一同投影为目录条目,其模式为 `mode: 'unknown'`。已有完整条目仍具有权威性。取消与来源一致性失败仍中止操作。复用和发布前会重新检查已检查子日志的修订,包括解码失败的日志,使修复不能静默复用旧证据。已发布前驱保持逐字节不变。
JSONL 在发现时跳过不可读或不支持的 header。子正文或 descriptor 字段失败会产生带子路径的警告,并保留该子的 header 身份。父目录没有完整条目时,迁移追加 `subagent/catalog`,与健康兄弟项一同投影为目录条目,其模式为 `mode: 'unknown'`。已有完整条目仍具有权威性。取消与来源一致性失败仍中止操作。复用和发布前会重新检查已检查子日志的修订,包括解码失败的日志,使修复不能静默复用旧证据。已发布前驱保持逐字节不变。
本决策部分取代[已发布迁移](../architecture/2026-08-31-released-session-format-migrations.zh.md)和[不完整子证据](2026-09-19-v3-incomplete-child-catalog-evidence.zh.md)中的子错误传播规则。其格式转换、descriptor 数量、冲突校验和不可变发布规则仍有效。
@@ -28,6 +28,6 @@ JSONL 在发现时跳过不可读或不支持的 header。子正文或 descripto
## 后果
首次父准备仍扫描 header 并逐个读取直属子正文,不宣称消除此 I/O。已发布 V4 父会话使用其 catalog,不重新扫描子日志。发布后不自动重写未知条目。打开子会话会重试其日志,根据实际 descriptor 确定浏览能力,或报告该子会话的错误。独立的必读事件保留已定稿 V4 目录载荷,同时持久记录未知成员;不认识该事件的旧读取器拒绝日志。
首次父准备仍扫描 header 并逐个读取直属子正文,不宣称消除此 I/O。已发布 V4 父会话使用其 catalog,不重新扫描子日志。发布后不自动重写未知条目。打开子会话会重试其日志,根据实际 descriptor 确定浏览能力,或报告该子会话的错误。Catalog mode 增加 `unknown`,既有分支和 V4 header 保持不变。持久化检查允许这项限定扩展;旧读取器可能拒绝使用新模式的记录。
原始和压缩测试覆盖目录补齐、子会话错误隔离、来源变化与修复、子代际不变和延后的子目录准备。随产品 Web 迁移快照在损坏兄弟存在时检查父目录,再独立打开子会话。额外的历史子角色使语料预算由十个增加到十一个;守卫拒绝第十二个角色。
@@ -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/process/2026-09-11-persistence-type-history.md
2026-09-11-persistence-type-history.md: c7cddea5ecc479a64ad5f24f5447e869779b7f9e
2026-09-11-persistence-type-history.zh.md: 001ce197c4cd12dc64a5e2bd710e39cb2b61f23a
2026-09-11-persistence-type-history.md: a5c50a871ebdb583c7e955944b03f4e082c69fe2
2026-09-11-persistence-type-history.zh.md: 8fc42ccd9964d7eab254da8bb8c40ae055ff1152
@@ -14,7 +14,7 @@ One normalized type model supplies the readable persistence catalog, the complet
The dedicated [persistence-change records](../../../../docs/persistence-changes/README.md) bind acknowledgement to the after digest of each affected root. A generated companion stores complete after schemas. The initial record covers all roots; later records name each root's predecessor. A predecessor's after schema supplies the next before schema. Verification rejects ambiguous history and requires current source to match the terminal recorded state without consulting Git history or remote services.
Automatic classification permits optional body additions, required-to-optional body changes, and ordinary event additions within one version. The scoped [source-attribution policy](2026-09-17-persistence-schema-review.md) also permits qualified new producer kinds when both saved schemas opt in. Other structural changes require a version-bump decision that includes an increasing header version in that record. The [format-version procedure](../../../../docs/cookbook/adding-a-session-format-version.md) continues to own adjacent migration work. Every structural difference requires an explicit record, including additions allowed within the same version.
Automatic classification permits optional body additions, required-to-optional body changes, and ordinary event additions within one version. The scoped [source-attribution policy](2026-09-17-persistence-schema-review.md) also permits qualified new producer kinds when both saved schemas opt in. The [catalog exception](../../../../docs/persistence-changes/README.md#compatibility-rules) admits unknown child mode with unchanged identity fields and records the older-reader limitation. Other structural changes require a version-bump decision that includes an increasing header version in that record. The [format-version procedure](../../../../docs/cookbook/adding-a-session-format-version.md) continues to own adjacent migration work. Every structural difference requires an explicit record, including additions allowed within the same version.
The new document kind preserves the compatibility reasoning and evidence for a historical type transition. Agent Notes retain mechanism-level decisions; they do not become a growing inventory of individual acknowledgements. Machine declarations remain identical across the bilingual pair and are parsed once.
@@ -14,7 +14,7 @@ Status: implemented
专用的[持久化变更记录](../../../../docs/persistence-changes/README.zh.md)将确认绑定到每个受影响根的变更后摘要。生成的伴随文件保存完整的变更后 schema。初始记录覆盖所有根;后续记录为每个根指定前驱。前驱的变更后 schema 提供下一次的变更前 schema。验证拒绝有歧义的历史,并要求当前源码匹配最终记录状态,无需查询 Git 历史或远端服务。
自动分类允许可选事件体新增、事件体属性从必选改为可选,以及普通事件新增保持同一版本。当双方保存的 schema 均声明支持时,受限的[消息源归属策略](2026-09-17-persistence-schema-review.zh.md)也允许添加符合条件的生产者 kind。其他结构变更需要升版本决策,并在该记录中包含递增的头部版本。[格式版本流程](../../../../docs/cookbook/adding-a-session-format-version.zh.md)继续负责相邻迁移工作。每个结构差异都需要显式记录,包括允许保持同一版本的新增。
自动分类允许可选事件体新增、事件体属性从必选改为可选,以及普通事件新增保持同一版本。当双方保存的 schema 均声明支持时,受限的[消息源归属策略](2026-09-17-persistence-schema-review.zh.md)也允许添加符合条件的生产者 kind。[Catalog 例外](../../../../docs/persistence-changes/README.zh.md#compatibility-rules) 允许增加身份字段不变的未知子模式,并记录旧读取器限制。其他结构变更需要升版本决策,并在该记录中包含递增的头部版本。[格式版本流程](../../../../docs/cookbook/adding-a-session-format-version.zh.md)继续负责相邻迁移工作。每个结构差异都需要显式记录,包括允许保持同一版本的新增。
新文档类型保留历史类型转换的兼容性说明和证据。Agent Note 保留机制层面的决策,不积累单次确认清单。双语对中的机器声明保持相同,且只解析一次。
+1 -1
View File
@@ -108,7 +108,7 @@ describe.skipIf(webSnapshotMode() === 'record')('historical preset restoration t
&& (row['data'] as { agentPreset: string }).agentPreset === 'code'
? { ...row, data: { agentPreset: 'ptc' } }
: row),
{ type: 'subagent/catalog-unknown', seq: rows.length, time: rows.at(-1)?.['time'], data: {
{ type: 'subagent/catalog', seq: rows.length, time: rows.at(-1)?.['time'], data: {
version: 0, childId: brokenId, childCreatedAt: childFixture[0]?.['createdAt'], mode: 'unknown',
} },
{ type: 'subagent/catalog', seq: rows.length + 1, time: rows.at(-1)?.['time'], data: {
+2 -2
View File
@@ -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 docs/persistence-catalog.md
persistence-catalog.md: 2c64c088a70a9a80430c2af377f09ddaedc356dc
persistence-catalog.zh.md: 9ea121c475aed4f12c164aeae1d6f26cb1679ede
persistence-catalog.md: 0733c6880cf65e445b35c2dea0589b7ce93a96b8
persistence-catalog.zh.md: c4f4870fb8d712b43502579ecfd7adae8d74a535
+23 -67
View File
@@ -57,8 +57,7 @@ The [format references](persistence-changes/historical-formats/README.md) cover
| `event:session/title-llm-request` | event | `fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d) |
| `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) |
| `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) |
| `event:subagent/catalog` | event | `ae1f7110feeec697b8cab42b68f7709aa7b3279764099dfb53c25890d2e5c871` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-ae1f7110feeec697b8cab42b68f7709aa7b3279764099dfb53c25890d2e5c871) |
| `event:subagent/catalog-unknown` | event | `d21dd6c60a43d9cb0d93da371e482f66ffd55daa36544e80978a0e7b32737b21` | [`{ type: "subagent/catalog-unknown" }`](#persistence-type-sha256-d21dd6c60a43d9cb0d93da371e482f66ffd55daa36544e80978a0e7b32737b21) |
| `event:subagent/catalog` | event | `4d6002c7eec8d76bbb6e531a35a55bc66dfb87621bc2e89e3af0641acf947bb3` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-4d6002c7eec8d76bbb6e531a35a55bc66dfb87621bc2e89e3af0641acf947bb3) |
| `event:subagent/descriptor` | event | `b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5` | [`{ type: "subagent/descriptor" }`](#persistence-type-sha256-b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5) |
| `event:subagent/model-selection-policy` | event | `a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc` | [`{ type: "subagent/model-selection-policy" }`](#persistence-type-sha256-a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc) |
| `event:system/message` | event | `69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1` | [`{ type: "system/message" }`](#persistence-type-sha256-69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1) |
@@ -895,27 +894,13 @@ Source: [`packages/core/session/src/types.ts:299`](../packages/core/session/src/
```ts persistence-catalog
/**
* A direct child's complete discovery fact.
* A direct child's identity and available discovery fields.
* @param data - versioned parent-owned catalog entry.
*/
'subagent/catalog': SubagentCatalogEvent
```
Source: [`packages/subagent/subagent/src/catalog.ts:48`](../packages/subagent/subagent/src/catalog.ts)
<a id="subagentcatalog-unknown--log-only"></a>
#### `subagent/catalog-unknown` — log-only
```ts persistence-catalog
/**
* A historical child's header identity when its descriptor cannot establish a mode.
* @param data - child identity retained for discovery and later history reads.
*/
'subagent/catalog-unknown': UnknownCatalogEvent
```
Source: [`packages/subagent/subagent/src/catalog.ts:53`](../packages/subagent/subagent/src/catalog.ts)
Source: [`packages/subagent/subagent/src/catalog.ts:41`](../packages/subagent/subagent/src/catalog.ts)
<a id="subagentdescriptor--log-only"></a>
@@ -2490,14 +2475,6 @@ SHA-256: `44c8c77aa9aa100b0d74d2deee85474b29faa6eee0ac0f662f7abe9931ddd7f2`
`"subagent/catalog"`
<a id="persistence-type-sha256-d437ebdbc1839ffcf3f6fa201a003126ec63945186b0c0792f1db466bf8ebd89"></a>
### `"subagent/catalog-unknown"`
SHA-256: `d437ebdbc1839ffcf3f6fa201a003126ec63945186b0c0792f1db466bf8ebd89`
`"subagent/catalog-unknown"`
<a id="persistence-type-sha256-5ebce1317eacd2c148456627a09668e927ee15c204f0643f5ea76b46b54a2647"></a>
### `"subagent/descriptor"`
@@ -4578,7 +4555,7 @@ One of:
- [`{ type: "finish" }`](#persistence-type-sha256-a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1)
- [`{ type: "usage" }`](#persistence-type-sha256-f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515)
<a id="persistence-type-sha256-3ac1196f59d5e9e0e9345122c8c403b102478b526404a2e591a38f613483321e"></a>
<a id="persistence-type-sha256-30a49d1a89bd03b8503fbdab94d4883e4ce839d931f29672ac070007cc0016ce"></a>
<a id="persistence-type-packagessubagentsubagentsrccatalogtssubagentcatalogevent"></a>
@@ -4586,7 +4563,7 @@ One of:
### `SubagentCatalogEvent`
SHA-256: `3ac1196f59d5e9e0e9345122c8c403b102478b526404a2e591a38f613483321e`
SHA-256: `30a49d1a89bd03b8503fbdab94d4883e4ce839d931f29672ac070007cc0016ce`
Sources: [`packages/subagent/subagent/src/catalog.ts:24`](../packages/subagent/subagent/src/catalog.ts)
@@ -4594,6 +4571,7 @@ One of:
- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd)
- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-5d542ac7bdf13310c46e1a884dec9fb18ad8c6e153cff207e639cd7bf9d7e694)
- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-c8d70349934748fe4cf79c49654c72459f4073c57b65e01ff1e8f49dfcce49ca)
<a id="persistence-type-sha256-86d1d0e9cfebb240421187e3dc37fb71cc38a1501885cf32f8f9adef7b0a2bde"></a>
@@ -5118,26 +5096,6 @@ Sources: [`packages/core/session/src/types.ts:232`](../packages/core/session/src
| `interrupted` | not declared | none | [`{ kind: "interrupted" }`](#persistence-type-sha256-56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23) |
| `max-tokens` | not declared | none | [`{ kind: "max-tokens" }`](#persistence-type-sha256-ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7) |
<a id="persistence-type-sha256-c8d70349934748fe4cf79c49654c72459f4073c57b65e01ff1e8f49dfcce49ca"></a>
<a id="persistence-type-packagessubagentsubagentsrccatalogtsunknowncatalogevent"></a>
<a id="persistence-type-unknowncatalogevent"></a>
### `UnknownCatalogEvent`
SHA-256: `c8d70349934748fe4cf79c49654c72459f4073c57b65e01ff1e8f49dfcce49ca`
Sources: [`packages/subagent/subagent/src/catalog.ts:34`](../packages/subagent/subagent/src/catalog.ts)
| Property | Presence | Type |
|---|---|---|
| `childCreatedAt` | required | `number` |
| `childId` | required | `string` |
| `label` | optional | `string` |
| `mode` | required | `"unknown"` |
| `version` | required | `0` |
<a id="persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586"></a>
<a id="persistence-type-packagesllmllmsrcmessagetsusermessage"></a>
@@ -5677,6 +5635,20 @@ SHA-256: `5d542ac7bdf13310c46e1a884dec9fb18ad8c6e153cff207e639cd7bf9d7e694`
| `mode` | required | `"one-shot"` |
| `version` | required | `0` |
<a id="persistence-type-sha256-c8d70349934748fe4cf79c49654c72459f4073c57b65e01ff1e8f49dfcce49ca"></a>
### `{ childCreatedAt, childId, label?, mode, … }`
SHA-256: `c8d70349934748fe4cf79c49654c72459f4073c57b65e01ff1e8f49dfcce49ca`
| Property | Presence | Type |
|---|---|---|
| `childCreatedAt` | required | `number` |
| `childId` | required | `string` |
| `label` | optional | `string` |
| `mode` | required | `"unknown"` |
| `version` | required | `0` |
<a id="persistence-type-sha256-a81059ca06999da1e712414520c2c65e491d052fcf9640fae20aa38f0a5dc735"></a>
### `{ code, name, reason? }`
@@ -8272,38 +8244,22 @@ SHA-256: `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986`
| `time` | required | `number` |
| `type` | required | `"step/start"` |
<a id="persistence-type-sha256-ae1f7110feeec697b8cab42b68f7709aa7b3279764099dfb53c25890d2e5c871"></a>
<a id="persistence-type-sha256-4d6002c7eec8d76bbb6e531a35a55bc66dfb87621bc2e89e3af0641acf947bb3"></a>
<a id="persistence-type-eventsubagentcatalog"></a>
### `{ type: "subagent/catalog" }`
SHA-256: `ae1f7110feeec697b8cab42b68f7709aa7b3279764099dfb53c25890d2e5c871`
SHA-256: `4d6002c7eec8d76bbb6e531a35a55bc66dfb87621bc2e89e3af0641acf947bb3`
| Property | Presence | Type |
|---|---|---|
| `data` | required | [`SubagentCatalogEvent`](#persistence-type-sha256-3ac1196f59d5e9e0e9345122c8c403b102478b526404a2e591a38f613483321e) |
| `data` | required | [`SubagentCatalogEvent`](#persistence-type-sha256-30a49d1a89bd03b8503fbdab94d4883e4ce839d931f29672ac070007cc0016ce) |
| `ignorable` | optional | `true` |
| `seq` | required | `number` |
| `time` | required | `number` |
| `type` | required | `"subagent/catalog"` |
<a id="persistence-type-sha256-d21dd6c60a43d9cb0d93da371e482f66ffd55daa36544e80978a0e7b32737b21"></a>
<a id="persistence-type-eventsubagentcatalog-unknown"></a>
### `{ type: "subagent/catalog-unknown" }`
SHA-256: `d21dd6c60a43d9cb0d93da371e482f66ffd55daa36544e80978a0e7b32737b21`
| Property | Presence | Type |
|---|---|---|
| `data` | required | [`UnknownCatalogEvent`](#persistence-type-sha256-c8d70349934748fe4cf79c49654c72459f4073c57b65e01ff1e8f49dfcce49ca) |
| `ignorable` | optional | `true` |
| `seq` | required | `number` |
| `time` | required | `number` |
| `type` | required | `"subagent/catalog-unknown"` |
<a id="persistence-type-sha256-b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5"></a>
<a id="persistence-type-eventsubagentdescriptor"></a>
+23 -67
View File
@@ -59,8 +59,7 @@
| `event:session/title-llm-request` | event | `fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d` | [`{ type: "session/title-llm-request" }`](#persistence-type-sha256-fa8f7d3ebf08a76c7f7a8b0781873c4d819b964da5dbb52cd3cdfa5da34f452d) |
| `event:step/end` | event | `e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05` | [`{ type: "step/end" }`](#persistence-type-sha256-e0a787e6ec76c7c94fecbc501b489164ab0293db05bc947914077ad01e674f05) |
| `event:step/start` | event | `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986` | [`{ type: "step/start" }`](#persistence-type-sha256-4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986) |
| `event:subagent/catalog` | event | `ae1f7110feeec697b8cab42b68f7709aa7b3279764099dfb53c25890d2e5c871` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-ae1f7110feeec697b8cab42b68f7709aa7b3279764099dfb53c25890d2e5c871) |
| `event:subagent/catalog-unknown` | event | `d21dd6c60a43d9cb0d93da371e482f66ffd55daa36544e80978a0e7b32737b21` | [`{ type: "subagent/catalog-unknown" }`](#persistence-type-sha256-d21dd6c60a43d9cb0d93da371e482f66ffd55daa36544e80978a0e7b32737b21) |
| `event:subagent/catalog` | event | `4d6002c7eec8d76bbb6e531a35a55bc66dfb87621bc2e89e3af0641acf947bb3` | [`{ type: "subagent/catalog" }`](#persistence-type-sha256-4d6002c7eec8d76bbb6e531a35a55bc66dfb87621bc2e89e3af0641acf947bb3) |
| `event:subagent/descriptor` | event | `b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5` | [`{ type: "subagent/descriptor" }`](#persistence-type-sha256-b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5) |
| `event:subagent/model-selection-policy` | event | `a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc` | [`{ type: "subagent/model-selection-policy" }`](#persistence-type-sha256-a6567ccb2e530606b775371eb4fa31468d72084339968e8b0440a516a23b39dc) |
| `event:system/message` | event | `69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1` | [`{ type: "system/message" }`](#persistence-type-sha256-69081694be231d56fd9580ba14645fd5e35373202605d5c5c841a9435b5fa3b1) |
@@ -897,27 +896,13 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
```ts persistence-catalog
/**
* A direct child's complete discovery fact.
* A direct child's identity and available discovery fields.
* @param data - versioned parent-owned catalog entry.
*/
'subagent/catalog': SubagentCatalogEvent
```
来源:[`packages/subagent/subagent/src/catalog.ts:48`](../packages/subagent/subagent/src/catalog.ts)
<a id="subagentcatalog-unknown--log-only"></a>
#### `subagent/catalog-unknown` — log-only
```ts persistence-catalog
/**
* A historical child's header identity when its descriptor cannot establish a mode.
* @param data - child identity retained for discovery and later history reads.
*/
'subagent/catalog-unknown': UnknownCatalogEvent
```
来源:[`packages/subagent/subagent/src/catalog.ts:53`](../packages/subagent/subagent/src/catalog.ts)
来源:[`packages/subagent/subagent/src/catalog.ts:41`](../packages/subagent/subagent/src/catalog.ts)
<a id="subagentdescriptor--log-only"></a>
@@ -2492,14 +2477,6 @@ SHA-256: `44c8c77aa9aa100b0d74d2deee85474b29faa6eee0ac0f662f7abe9931ddd7f2`
`"subagent/catalog"`
<a id="persistence-type-sha256-d437ebdbc1839ffcf3f6fa201a003126ec63945186b0c0792f1db466bf8ebd89"></a>
### `"subagent/catalog-unknown"`
SHA-256: `d437ebdbc1839ffcf3f6fa201a003126ec63945186b0c0792f1db466bf8ebd89`
`"subagent/catalog-unknown"`
<a id="persistence-type-sha256-5ebce1317eacd2c148456627a09668e927ee15c204f0643f5ea76b46b54a2647"></a>
### `"subagent/descriptor"`
@@ -4580,7 +4557,7 @@ SHA-256: `47e9095591ae36e7c39ee62553097772f2bfa16304536a72dbbc3f691f605180`
- [`{ type: "finish" }`](#persistence-type-sha256-a2b49331716ba0a7078b026d145306a70eae64b7313c3e6204bd931e75ec3bb1)
- [`{ type: "usage" }`](#persistence-type-sha256-f1a96afe7503ac1817de769cd0ba13d9b80111c7034c50106b3344ecc14cc515)
<a id="persistence-type-sha256-3ac1196f59d5e9e0e9345122c8c403b102478b526404a2e591a38f613483321e"></a>
<a id="persistence-type-sha256-30a49d1a89bd03b8503fbdab94d4883e4ce839d931f29672ac070007cc0016ce"></a>
<a id="persistence-type-packagessubagentsubagentsrccatalogtssubagentcatalogevent"></a>
@@ -4588,7 +4565,7 @@ SHA-256: `47e9095591ae36e7c39ee62553097772f2bfa16304536a72dbbc3f691f605180`
### `SubagentCatalogEvent`
SHA-256: `3ac1196f59d5e9e0e9345122c8c403b102478b526404a2e591a38f613483321e`
SHA-256: `30a49d1a89bd03b8503fbdab94d4883e4ce839d931f29672ac070007cc0016ce`
来源:[`packages/subagent/subagent/src/catalog.ts:24`](../packages/subagent/subagent/src/catalog.ts)
@@ -4596,6 +4573,7 @@ SHA-256: `3ac1196f59d5e9e0e9345122c8c403b102478b526404a2e591a38f613483321e`
- [`{ childCreatedAt, childId, label, mode, … }`](#persistence-type-sha256-0e10d7c096b47e61defe372a5fc8bd37d835199410eac44367f95c45082498bd)
- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-5d542ac7bdf13310c46e1a884dec9fb18ad8c6e153cff207e639cd7bf9d7e694)
- [`{ childCreatedAt, childId, label?, mode, … }`](#persistence-type-sha256-c8d70349934748fe4cf79c49654c72459f4073c57b65e01ff1e8f49dfcce49ca)
<a id="persistence-type-sha256-86d1d0e9cfebb240421187e3dc37fb71cc38a1501885cf32f8f9adef7b0a2bde"></a>
@@ -5120,26 +5098,6 @@ SHA-256: `f1687f982fa3d41ef29e159feed154ecd13af4dd209acaee16038f223f2e9402`
| `interrupted` | 未声明 | 无 | [`{ kind: "interrupted" }`](#persistence-type-sha256-56b7c047694875a843ba020dfea3c8380d54ae4ff88ab349e72004dc21e07c23) |
| `max-tokens` | 未声明 | 无 | [`{ kind: "max-tokens" }`](#persistence-type-sha256-ab7ec5f9eb2c6dfacca5da4c37cf7ba85d04172053ee6b1ebf1709ffcd99c1a7) |
<a id="persistence-type-sha256-c8d70349934748fe4cf79c49654c72459f4073c57b65e01ff1e8f49dfcce49ca"></a>
<a id="persistence-type-packagessubagentsubagentsrccatalogtsunknowncatalogevent"></a>
<a id="persistence-type-unknowncatalogevent"></a>
### `UnknownCatalogEvent`
SHA-256: `c8d70349934748fe4cf79c49654c72459f4073c57b65e01ff1e8f49dfcce49ca`
来源:[`packages/subagent/subagent/src/catalog.ts:34`](../packages/subagent/subagent/src/catalog.ts)
| 属性 | 存在性 | 类型 |
|---|---|---|
| `childCreatedAt` | 必需 | `number` |
| `childId` | 必需 | `string` |
| `label` | 可选 | `string` |
| `mode` | 必需 | `"unknown"` |
| `version` | 必需 | `0` |
<a id="persistence-type-sha256-64ea5653002834c44f29bea4620e9c42aeb6024ae5b1d58d69f95158006b2586"></a>
<a id="persistence-type-packagesllmllmsrcmessagetsusermessage"></a>
@@ -5679,6 +5637,20 @@ SHA-256: `5d542ac7bdf13310c46e1a884dec9fb18ad8c6e153cff207e639cd7bf9d7e694`
| `mode` | 必需 | `"one-shot"` |
| `version` | 必需 | `0` |
<a id="persistence-type-sha256-c8d70349934748fe4cf79c49654c72459f4073c57b65e01ff1e8f49dfcce49ca"></a>
### `{ childCreatedAt, childId, label?, mode, … }`
SHA-256: `c8d70349934748fe4cf79c49654c72459f4073c57b65e01ff1e8f49dfcce49ca`
| 属性 | 存在性 | 类型 |
|---|---|---|
| `childCreatedAt` | 必需 | `number` |
| `childId` | 必需 | `string` |
| `label` | 可选 | `string` |
| `mode` | 必需 | `"unknown"` |
| `version` | 必需 | `0` |
<a id="persistence-type-sha256-a81059ca06999da1e712414520c2c65e491d052fcf9640fae20aa38f0a5dc735"></a>
### `{ code, name, reason? }`
@@ -8274,38 +8246,22 @@ SHA-256: `4513e088d43e6c68425be30451b9f961cc264fe7318ca62681c41d4d78615986`
| `time` | 必需 | `number` |
| `type` | 必需 | `"step/start"` |
<a id="persistence-type-sha256-ae1f7110feeec697b8cab42b68f7709aa7b3279764099dfb53c25890d2e5c871"></a>
<a id="persistence-type-sha256-4d6002c7eec8d76bbb6e531a35a55bc66dfb87621bc2e89e3af0641acf947bb3"></a>
<a id="persistence-type-eventsubagentcatalog"></a>
### `{ type: "subagent/catalog" }`
SHA-256: `ae1f7110feeec697b8cab42b68f7709aa7b3279764099dfb53c25890d2e5c871`
SHA-256: `4d6002c7eec8d76bbb6e531a35a55bc66dfb87621bc2e89e3af0641acf947bb3`
| 属性 | 存在性 | 类型 |
|---|---|---|
| `data` | 必需 | [`SubagentCatalogEvent`](#persistence-type-sha256-3ac1196f59d5e9e0e9345122c8c403b102478b526404a2e591a38f613483321e) |
| `data` | 必需 | [`SubagentCatalogEvent`](#persistence-type-sha256-30a49d1a89bd03b8503fbdab94d4883e4ce839d931f29672ac070007cc0016ce) |
| `ignorable` | 可选 | `true` |
| `seq` | 必需 | `number` |
| `time` | 必需 | `number` |
| `type` | 必需 | `"subagent/catalog"` |
<a id="persistence-type-sha256-d21dd6c60a43d9cb0d93da371e482f66ffd55daa36544e80978a0e7b32737b21"></a>
<a id="persistence-type-eventsubagentcatalog-unknown"></a>
### `{ type: "subagent/catalog-unknown" }`
SHA-256: `d21dd6c60a43d9cb0d93da371e482f66ffd55daa36544e80978a0e7b32737b21`
| 属性 | 存在性 | 类型 |
|---|---|---|
| `data` | 必需 | [`UnknownCatalogEvent`](#persistence-type-sha256-c8d70349934748fe4cf79c49654c72459f4073c57b65e01ff1e8f49dfcce49ca) |
| `ignorable` | 可选 | `true` |
| `seq` | 必需 | `number` |
| `time` | 必需 | `number` |
| `type` | 必需 | `"subagent/catalog-unknown"` |
<a id="persistence-type-sha256-b79ada42962cad0190a9d465805260567621fa3a4abd757eb31e6016b52d5ab5"></a>
<a id="persistence-type-eventsubagentdescriptor"></a>
@@ -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 docs/persistence-changes/2026-09-20-unknown-child-catalog.md
2026-09-20-unknown-child-catalog.md: 80de55e616d63730bc1d0bbc0ffa43923f833923
2026-09-20-unknown-child-catalog.zh.md: 1ee8a04e7318013ec27a7c736fadd540a96cfc0d
2026-09-20-unknown-child-catalog.md: 58e8a8ffedb7b3b913c9e04590ce5bbabd148ba6
2026-09-20-unknown-child-catalog.zh.md: 488899ecadcdc93910927c80b22b8fcf0d319224
@@ -9,7 +9,7 @@ English | [中文](2026-09-20-unknown-child-catalog.zh.md)
## Summary
Add subagent/catalog-unknown to retain historical direct-child header identity when a descriptor cannot establish the mode.
Retain unreadable historical children in subagent/catalog with unknown mode.
## Table of Contents
@@ -26,21 +26,21 @@ schemaVersion: 1
id: 2026-09-20-unknown-child-catalog
baseline: false
changes:
- root: "event:subagent/catalog-unknown"
previous: null
after: "d21dd6c60a43d9cb0d93da371e482f66ffd55daa36544e80978a0e7b32737b21"
- root: "event:subagent/catalog"
previous: "2026-09-11-initial"
after: "4d6002c7eec8d76bbb6e531a35a55bc66dfb87621bc2e89e3af0641acf947bb3"
decision: same-version
```
<a id="compatibility"></a>
## Compatibility
This adds an ordinary required-on-read event without changing the finalized subagent/catalog payload, Session header, or envelope. Existing logs remain valid. Older readers that do not know the new event refuse logs containing it; current readers project it into the existing catalog with unknown mode and keep continuation requests restricted to supported child descriptors.
The catalog adds one unknown-mode alternative with the same identity and optional label fields as one-shot mode. Existing alternatives and the V4 header remain unchanged. New readers accept old records; older readers may reject records with unknown mode. The persistence classifier has a scoped catalog exception for this addition, while other union additions and modifications remain strict. Unknown membership grants no continuation capability.
<a id="verification"></a>
## Verification
pnpm exec vitest run packages/session/session-format-v3-to-v4/tests/validation.spec.ts packages/subagent/subagent/tests/catalog.spec.ts: 16 tests passed, covering unknown identity restoration, malformed payload rejection, duplicate child rejection, and shared catalog projection.
Catalog migration, restoration, projection, and Web regressions cover retained unknown membership and child-local errors. Persistence classifier tests accept the exact catalog extension and reject altered existing fields, other new modes, extra unknown fields, removals, and unrelated union additions.
<a id="dev-note"></a>
## Dev Note
@@ -2,11 +2,11 @@
"formatVersion": 2,
"roots": [
{
"key": "event:subagent/catalog-unknown",
"key": "event:subagent/catalog",
"kind": "event",
"event": "subagent/catalog-unknown",
"event": "subagent/catalog",
"surface": false,
"digest": "d21dd6c60a43d9cb0d93da371e482f66ffd55daa36544e80978a0e7b32737b21",
"digest": "4d6002c7eec8d76bbb6e531a35a55bc66dfb87621bc2e89e3af0641acf947bb3",
"schema": {
"root": 0,
"nodes": [
@@ -20,54 +20,62 @@
},
{
"name": "ignorable",
"type": 6,
"type": 11,
"optional": true
},
{
"name": "seq",
"type": 2,
"type": 3,
"optional": false
},
{
"name": "time",
"type": 2,
"type": 3,
"optional": false
},
{
"name": "type",
"type": 7,
"type": 12,
"optional": false
}
],
"indices": []
},
{
"kind": "union",
"types": [
2,
7,
9
]
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 2,
"type": 3,
"optional": false
},
{
"name": "childId",
"type": 3,
"optional": false
},
{
"name": "label",
"type": 3,
"optional": true
},
{
"name": "mode",
"type": 4,
"optional": false
},
{
"name": "version",
"name": "label",
"type": 4,
"optional": true
},
{
"name": "mode",
"type": 5,
"optional": false
},
{
"name": "version",
"type": 6,
"optional": false
}
],
"indices": []
@@ -88,13 +96,83 @@
"kind": "literal",
"value": 0
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 3,
"optional": false
},
{
"name": "childId",
"type": 4,
"optional": false
},
{
"name": "label",
"type": 4,
"optional": false
},
{
"name": "mode",
"type": 8,
"optional": false
},
{
"name": "version",
"type": 6,
"optional": false
}
],
"indices": []
},
{
"kind": "literal",
"value": "continuable"
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 3,
"optional": false
},
{
"name": "childId",
"type": 4,
"optional": false
},
{
"name": "label",
"type": 4,
"optional": true
},
{
"name": "mode",
"type": 10,
"optional": false
},
{
"name": "version",
"type": 6,
"optional": false
}
],
"indices": []
},
{
"kind": "literal",
"value": "one-shot"
},
{
"kind": "literal",
"value": true
},
{
"kind": "literal",
"value": "subagent/catalog-unknown"
"value": "subagent/catalog"
}
]
}
@@ -9,7 +9,7 @@ kind: persistence-change
## 概述
新增 subagent/catalog-unknown,在 descriptor 无法确定模式时保留历史直属子会话的 header 身份。
在 subagent/catalog 中使用 unknown 模式保留不可读的历史子会话。
## 目录
@@ -26,21 +26,21 @@ schemaVersion: 1
id: 2026-09-20-unknown-child-catalog
baseline: false
changes:
- root: "event:subagent/catalog-unknown"
previous: null
after: "d21dd6c60a43d9cb0d93da371e482f66ffd55daa36544e80978a0e7b32737b21"
- root: "event:subagent/catalog"
previous: "2026-09-11-initial"
after: "4d6002c7eec8d76bbb6e531a35a55bc66dfb87621bc2e89e3af0641acf947bb3"
decision: same-version
```
<a id="compatibility"></a>
## 兼容性
新增普通的必读事件,不修改已定稿的 subagent/catalog 载荷、Session header 或事件信封。已有日志仍有效。不认识新事件的旧读取器拒绝包含它的日志;当前读取器将其投影为现有目录中的未知模式项,继续执行请求仍受支持的子 descriptor 约束。
Catalog 增加一个 unknown 模式分支,身份与可选 label 字段和 one-shot 模式相同。已有分支及 V4 header 保持不变。新读取器接受旧记录;旧读取器可能拒绝 unknown 模式记录。持久化分类器仅为该 catalog 扩展设置限定例外,其他联合类型新增与修改仍严格检查。未知成员关系不授予继续执行能力。
<a id="verification"></a>
## 验证
pnpm exec vitest run packages/session/session-format-v3-to-v4/tests/validation.spec.ts packages/subagent/subagent/tests/catalog.spec.ts:16 个测试通过,覆盖未知身份恢复、畸形载荷拒绝、重复 child 拒绝及共享目录投影。
Catalog 迁移、恢复、投影和 Web 回归覆盖未知成员保留与子会话局部报错。持久化分类器测试接受此 catalog 扩展,并拒绝已有字段修改、其他新增模式、未知分支额外字段、删除及无关联合类型新增。
<a id="dev-note"></a>
## 开发备注
+2 -2
View File
@@ -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 docs/persistence-changes/README.md
README.md: 3fd95e48548fdb9c1d04330c2925096d072850d1
README.zh.md: f4ab3b3d22b1aa2f7f5923e84fd4effc339874db
README.md: 1e7c7b7aec8ecf6687b37f36544dccb4c8e4c4f5
README.zh.md: d6a315acb42a82a926fc323370b52c1a1b61065e
+3
View File
@@ -53,12 +53,15 @@ Every detected structural change requires an acknowledgement. Record creation an
| Add an optional event-body property, including its complete subtree | `same-version` |
| Make a required event-body property optional | `same-version` |
| Add an ordinary event type | `same-version` |
| Add `unknown` mode to `subagent/catalog` using the one-shot fields, while preserving existing alternatives | `same-version` |
| Add an explicitly qualified attribution kind to a user/developer source slot whose before and after schemas carry the same supported policy | `same-version` |
| Make an optional property required, add a required property, change an existing type, or remove/rename a property or event | `version-bump` |
| Change the Session header or event envelope | `version-bump` |
A finalized checkpoint protects the accepted baseline without replacing these compatibility rules. At V4, optional additions, ordinary events, and qualified attribution additions can receive new same-version records. Breaking differences require a higher writer version and an acknowledgement containing its own header increase. The accepted V4 record cannot be updated to reuse its original 3→4 transition.
The catalog exception retains a historical child's identity when its descriptor is unavailable. It admits only an `unknown` alternative with the same fields as the one-shot alternative; existing alternatives still receive ordinary comparison. It does not permit other mode additions, removals, header changes, or arbitrary union extensions. Existing logs remain readable by the updated reader; older readers may reject the new mode. The [acknowledgement](2026-09-20-unknown-child-catalog.md) records this forward-read limitation.
The extractor accepts an explicit `@persistenceSource` binding for a core-owned source property on a literal user or developer role; it does not infer a binding from an unannotated type. A producer qualifies its `MessageSourceMap` entry with `@persistenceAttribution`. Qualification promises that an unknown kind and its JSON metadata survive reading without the producer, and that the kind imposes no validation, replay, or authority requirement. A producer may inspect its own kind to resume duplicate suppression; other readers must preserve and derive the recorded messages without that projection. The recorded schema retains the binding, policy version, literal `kind` discriminator, preservation promise, and qualified kind set. Inventory format 2 stores those promises; extraction without a bound policy retains format 1. Session format versions are independent. Both compared snapshots must carry compatible policy state. Existing kind groups still receive ordinary structural comparison; removals, unmarked additions, policy changes, and unrelated breaking changes remain strict. Multiple context-form alternatives with the same wire kind form one group.
A same-version explanation states why old records can omit the addition and why older readers can ignore it without changing replay. For required-to-optional changes, it explains how readers handle an absent value. The checker validates the type classification; reviewers assess the explanation. A version-bump record includes the increasing header version in the same transition and follows the [Session-format procedure](../cookbook/adding-a-session-format-version.md).
+3
View File
@@ -53,12 +53,15 @@ description: "审阅和维护已记录的会话持久化类型变更、对应 sc
| 添加可选事件体属性,包括其完整子树 | `same-version` |
| 将必选事件体属性改为可选 | `same-version` |
| 添加普通事件类型 | `same-version` |
| 为 `subagent/catalog` 增加使用 one-shot 字段的 `unknown` 模式,并保留既有分支 | `same-version` |
| 在前后 schema 均带有相同受支持策略的 user/developer 消息源字段中,添加显式声明为归属信息的 kind | `same-version` |
| 将可选属性改为必选、添加必选属性、更改已有类型,或删除/重命名属性或事件 | `version-bump` |
| 更改会话头或事件封装 | `version-bump` |
定稿检查点保护已接受基线,不替换这些兼容性规则。在 V4 中,可选新增、普通事件及符合条件的归属 kind 新增可以使用新的同版本记录。破坏性差异要求更高的写入器版本,以及包含自身头版本递增的确认记录。不能更新已接受 V4 记录以复用其原有 3→4 转换。
Catalog 例外在 descriptor 不可用时保留历史子会话身份。它仅允许增加字段与 one-shot 分支相同的 `unknown` 分支;已有分支仍执行常规比较。它不允许其他模式新增、删除、header 变更或任意联合类型扩展。新版读取器仍可读取已有日志;旧版读取器可能拒绝新模式。[确认记录](2026-09-20-unknown-child-catalog.zh.md) 说明这一向前读取限制。
提取器接受核心拥有的 source 属性上针对 user 或 developer 字面量角色的显式 `@persistenceSource` 绑定,不会从未标记类型中推断绑定。生产者用 `@persistenceAttribution` 标记其 `MessageSourceMap` 成员。该标记承诺:读取器无需生产者即可保留未知 kind 及其 JSON 元数据,且该 kind 不引入校验、回放或权限要求。生产者可以检查自身 kind 来恢复去重状态;其他读取器必须无需该投影也能保留并派生已记录的消息。记录的 schema 保存绑定、策略版本、字面量 `kind` 判别字段、保留承诺及符合条件的 kind 集合。Inventory format 2 保存这些承诺;没有绑定策略的提取仍使用 format 1。Session 格式版本独立于此。比较双方的快照必须带有兼容的策略状态。已有 kind 分组仍进行常规结构比较;删除、未标记的添加、策略更改及无关破坏性变更继续采用严格规则。同一 wire kind 的多个上下文形式分支归为一组。
同版本说明须解释旧记录为何可以缺少新增内容,以及旧读取器为何可以忽略它而不改变回放。对于必选改可选的变更,说明须解释读取器如何处理缺失值。检查器验证类型分类;审阅者判断说明是否成立。升版本记录在同一转换中包含递增的头部版本,并遵循[会话格式流程](../cookbook/adding-a-session-format-version.zh.md)。
@@ -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 docs/persistence-changes/historical-formats/README.md
README.md: 386b0bd9091cbd509843bd6464c8a95650accb99
README.zh.md: e93e8033daa639eee3a5035efc2df8ef123f563e
README.md: f4e721bbef9d442021226c573280edc193792662
README.zh.md: be4b7c86b68fa682e7f8275f85a2548f39f30df9
@@ -32,7 +32,7 @@ The index is generated from validated snapshots and the current writer constant.
| 1 | PR #3349 | [V1](v1.md) | [JSON](v1.schema.json) | 54 / 415 |
| 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.md) | [JSON](v2.schema.json) | 56 / 435 |
| 3 | PR #4320 | [V3](v3.md) | [JSON](v3.schema.json) | 60 / 467 |
| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 63 / 587 |
| 4 | Current checkout | [Current catalog](../../persistence-catalog.md) | [JSON](../../persistence-schema.json) | 62 / 585 |
<!-- persistence-format-index:end -->
@@ -32,7 +32,7 @@ description: "查找从 V0 到工作区写入器版本的每个 Session 格式
| 1 | PR #3349 | [V1](v1.zh.md) | [JSON](v1.schema.json) | 54 / 415 |
| 2 | `dsh-v0.1.3-alpha.2` | [V2](v2.zh.md) | [JSON](v2.schema.json) | 56 / 435 |
| 3 | PR #4320 | [V3](v3.zh.md) | [JSON](v3.schema.json) | 60 / 467 |
| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 63 / 587 |
| 4 | 当前工作树 | [当前目录](../../persistence-catalog.zh.md) | [JSON](../../persistence-schema.json) | 62 / 585 |
<!-- persistence-format-index:end -->
+392 -496
View File
@@ -14850,7 +14850,7 @@
"kind": "event",
"event": "subagent/catalog",
"surface": false,
"digest": "ae1f7110feeec697b8cab42b68f7709aa7b3279764099dfb53c25890d2e5c871",
"digest": "4d6002c7eec8d76bbb6e531a35a55bc66dfb87621bc2e89e3af0641acf947bb3",
"schema": {
"root": 0,
"nodes": [
@@ -14864,7 +14864,7 @@
},
{
"name": "ignorable",
"type": 9,
"type": 11,
"optional": true
},
{
@@ -14879,7 +14879,7 @@
},
{
"name": "type",
"type": 10,
"type": 12,
"optional": false
}
],
@@ -14889,7 +14889,8 @@
"kind": "union",
"types": [
2,
7
7,
9
]
},
{
@@ -14908,7 +14909,7 @@
{
"name": "label",
"type": 4,
"optional": false
"optional": true
},
{
"name": "mode",
@@ -14931,139 +14932,6 @@
"kind": "primitive",
"type": "string"
},
{
"kind": "literal",
"value": "continuable"
},
{
"kind": "literal",
"value": 0
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 3,
"optional": false
},
{
"name": "childId",
"type": 4,
"optional": false
},
{
"name": "label",
"type": 4,
"optional": true
},
{
"name": "mode",
"type": 8,
"optional": false
},
{
"name": "version",
"type": 6,
"optional": false
}
],
"indices": []
},
{
"kind": "literal",
"value": "one-shot"
},
{
"kind": "literal",
"value": true
},
{
"kind": "literal",
"value": "subagent/catalog"
}
]
}
},
{
"key": "event:subagent/catalog-unknown",
"kind": "event",
"event": "subagent/catalog-unknown",
"surface": false,
"digest": "d21dd6c60a43d9cb0d93da371e482f66ffd55daa36544e80978a0e7b32737b21",
"schema": {
"root": 0,
"nodes": [
{
"kind": "object",
"properties": [
{
"name": "data",
"type": 1,
"optional": false
},
{
"name": "ignorable",
"type": 6,
"optional": true
},
{
"name": "seq",
"type": 2,
"optional": false
},
{
"name": "time",
"type": 2,
"optional": false
},
{
"name": "type",
"type": 7,
"optional": false
}
],
"indices": []
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 2,
"optional": false
},
{
"name": "childId",
"type": 3,
"optional": false
},
{
"name": "label",
"type": 3,
"optional": true
},
{
"name": "mode",
"type": 4,
"optional": false
},
{
"name": "version",
"type": 5,
"optional": false
}
],
"indices": []
},
{
"kind": "primitive",
"type": "number"
},
{
"kind": "primitive",
"type": "string"
},
{
"kind": "literal",
"value": "unknown"
@@ -15072,13 +14940,83 @@
"kind": "literal",
"value": 0
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 3,
"optional": false
},
{
"name": "childId",
"type": 4,
"optional": false
},
{
"name": "label",
"type": 4,
"optional": false
},
{
"name": "mode",
"type": 8,
"optional": false
},
{
"name": "version",
"type": 6,
"optional": false
}
],
"indices": []
},
{
"kind": "literal",
"value": "continuable"
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 3,
"optional": false
},
{
"name": "childId",
"type": 4,
"optional": false
},
{
"name": "label",
"type": 4,
"optional": true
},
{
"name": "mode",
"type": 10,
"optional": false
},
{
"name": "version",
"type": 6,
"optional": false
}
],
"indices": []
},
{
"kind": "literal",
"value": "one-shot"
},
{
"kind": "literal",
"value": true
},
{
"kind": "literal",
"value": "subagent/catalog-unknown"
"value": "subagent/catalog"
}
]
}
@@ -33635,6 +33573,145 @@
"packages/core/tools/src/types.ts:11"
]
},
{
"digest": "30a49d1a89bd03b8503fbdab94d4883e4ce839d931f29672ac070007cc0016ce",
"schema": {
"root": 0,
"nodes": [
{
"kind": "union",
"types": [
1,
6,
8
]
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 2,
"optional": false
},
{
"name": "childId",
"type": 3,
"optional": false
},
{
"name": "label",
"type": 3,
"optional": false
},
{
"name": "mode",
"type": 4,
"optional": false
},
{
"name": "version",
"type": 5,
"optional": false
}
],
"indices": []
},
{
"kind": "primitive",
"type": "number"
},
{
"kind": "primitive",
"type": "string"
},
{
"kind": "literal",
"value": "continuable"
},
{
"kind": "literal",
"value": 0
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 2,
"optional": false
},
{
"name": "childId",
"type": 3,
"optional": false
},
{
"name": "label",
"type": 3,
"optional": true
},
{
"name": "mode",
"type": 7,
"optional": false
},
{
"name": "version",
"type": 5,
"optional": false
}
],
"indices": []
},
{
"kind": "literal",
"value": "one-shot"
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 2,
"optional": false
},
{
"name": "childId",
"type": 3,
"optional": false
},
{
"name": "label",
"type": 3,
"optional": true
},
{
"name": "mode",
"type": 9,
"optional": false
},
{
"name": "version",
"type": 5,
"optional": false
}
],
"indices": []
},
{
"kind": "literal",
"value": "unknown"
}
]
},
"names": [
"packages/subagent/subagent/src/catalog.ts#SubagentCatalogEvent"
],
"sources": [
"packages/subagent/subagent/src/catalog.ts:24"
]
},
{
"digest": "30d5a537b296685770b1241c4b4bbde19d5a31ff81aa23df73fab5e90b8fc65f",
"schema": {
@@ -35345,109 +35422,6 @@
"packages/feedback/message-feedback/src/types.ts:17"
]
},
{
"digest": "3ac1196f59d5e9e0e9345122c8c403b102478b526404a2e591a38f613483321e",
"schema": {
"root": 0,
"nodes": [
{
"kind": "union",
"types": [
1,
6
]
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 2,
"optional": false
},
{
"name": "childId",
"type": 3,
"optional": false
},
{
"name": "label",
"type": 3,
"optional": false
},
{
"name": "mode",
"type": 4,
"optional": false
},
{
"name": "version",
"type": 5,
"optional": false
}
],
"indices": []
},
{
"kind": "primitive",
"type": "number"
},
{
"kind": "primitive",
"type": "string"
},
{
"kind": "literal",
"value": "continuable"
},
{
"kind": "literal",
"value": 0
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 2,
"optional": false
},
{
"name": "childId",
"type": 3,
"optional": false
},
{
"name": "label",
"type": 3,
"optional": true
},
{
"name": "mode",
"type": 7,
"optional": false
},
{
"name": "version",
"type": 5,
"optional": false
}
],
"indices": []
},
{
"kind": "literal",
"value": "one-shot"
}
]
},
"names": [
"packages/subagent/subagent/src/catalog.ts#SubagentCatalogEvent"
],
"sources": [
"packages/subagent/subagent/src/catalog.ts:24"
]
},
{
"digest": "3b04790e08b35241ffa0d38f6d7e4f8788aa921341f6c0d7574a329397c59408",
"schema": {
@@ -44281,6 +44255,180 @@
"packages/workflow/workflow/src/types.ts:13"
]
},
{
"digest": "4d6002c7eec8d76bbb6e531a35a55bc66dfb87621bc2e89e3af0641acf947bb3",
"schema": {
"root": 0,
"nodes": [
{
"kind": "object",
"properties": [
{
"name": "data",
"type": 1,
"optional": false
},
{
"name": "ignorable",
"type": 11,
"optional": true
},
{
"name": "seq",
"type": 3,
"optional": false
},
{
"name": "time",
"type": 3,
"optional": false
},
{
"name": "type",
"type": 12,
"optional": false
}
],
"indices": []
},
{
"kind": "union",
"types": [
2,
7,
9
]
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 3,
"optional": false
},
{
"name": "childId",
"type": 4,
"optional": false
},
{
"name": "label",
"type": 4,
"optional": true
},
{
"name": "mode",
"type": 5,
"optional": false
},
{
"name": "version",
"type": 6,
"optional": false
}
],
"indices": []
},
{
"kind": "primitive",
"type": "number"
},
{
"kind": "primitive",
"type": "string"
},
{
"kind": "literal",
"value": "unknown"
},
{
"kind": "literal",
"value": 0
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 3,
"optional": false
},
{
"name": "childId",
"type": 4,
"optional": false
},
{
"name": "label",
"type": 4,
"optional": false
},
{
"name": "mode",
"type": 8,
"optional": false
},
{
"name": "version",
"type": 6,
"optional": false
}
],
"indices": []
},
{
"kind": "literal",
"value": "continuable"
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 3,
"optional": false
},
{
"name": "childId",
"type": 4,
"optional": false
},
{
"name": "label",
"type": 4,
"optional": true
},
{
"name": "mode",
"type": 10,
"optional": false
},
{
"name": "version",
"type": 6,
"optional": false
}
],
"indices": []
},
{
"kind": "literal",
"value": "one-shot"
},
{
"kind": "literal",
"value": true
},
{
"kind": "literal",
"value": "subagent/catalog"
}
]
},
"names": [],
"sources": []
},
{
"digest": "4e011753f8263f436efaabfbb3b4db53a98fc55342c5ded2daf7208b217be159",
"schema": {
@@ -70277,144 +70425,6 @@
"names": [],
"sources": []
},
{
"digest": "ae1f7110feeec697b8cab42b68f7709aa7b3279764099dfb53c25890d2e5c871",
"schema": {
"root": 0,
"nodes": [
{
"kind": "object",
"properties": [
{
"name": "data",
"type": 1,
"optional": false
},
{
"name": "ignorable",
"type": 9,
"optional": true
},
{
"name": "seq",
"type": 3,
"optional": false
},
{
"name": "time",
"type": 3,
"optional": false
},
{
"name": "type",
"type": 10,
"optional": false
}
],
"indices": []
},
{
"kind": "union",
"types": [
2,
7
]
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 3,
"optional": false
},
{
"name": "childId",
"type": 4,
"optional": false
},
{
"name": "label",
"type": 4,
"optional": false
},
{
"name": "mode",
"type": 5,
"optional": false
},
{
"name": "version",
"type": 6,
"optional": false
}
],
"indices": []
},
{
"kind": "primitive",
"type": "number"
},
{
"kind": "primitive",
"type": "string"
},
{
"kind": "literal",
"value": "continuable"
},
{
"kind": "literal",
"value": 0
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 3,
"optional": false
},
{
"name": "childId",
"type": 4,
"optional": false
},
{
"name": "label",
"type": 4,
"optional": true
},
{
"name": "mode",
"type": 8,
"optional": false
},
{
"name": "version",
"type": 6,
"optional": false
}
],
"indices": []
},
{
"kind": "literal",
"value": "one-shot"
},
{
"kind": "literal",
"value": true
},
{
"kind": "literal",
"value": "subagent/catalog"
}
]
},
"names": [],
"sources": []
},
{
"digest": "ae63d9d1080ed8945031594d93f8d96651d7101b848719cc3e1475875cd3d649",
"schema": {
@@ -73643,12 +73653,8 @@
}
]
},
"names": [
"packages/subagent/subagent/src/catalog.ts#UnknownCatalogEvent"
],
"sources": [
"packages/subagent/subagent/src/catalog.ts:34"
]
"names": [],
"sources": []
},
{
"digest": "c8e6f48878cc2a2f0dc59682280d81f407943fb503f32c64bf9ff3d9393c3eff",
@@ -77131,102 +77137,6 @@
"names": [],
"sources": []
},
{
"digest": "d21dd6c60a43d9cb0d93da371e482f66ffd55daa36544e80978a0e7b32737b21",
"schema": {
"root": 0,
"nodes": [
{
"kind": "object",
"properties": [
{
"name": "data",
"type": 1,
"optional": false
},
{
"name": "ignorable",
"type": 6,
"optional": true
},
{
"name": "seq",
"type": 2,
"optional": false
},
{
"name": "time",
"type": 2,
"optional": false
},
{
"name": "type",
"type": 7,
"optional": false
}
],
"indices": []
},
{
"kind": "object",
"properties": [
{
"name": "childCreatedAt",
"type": 2,
"optional": false
},
{
"name": "childId",
"type": 3,
"optional": false
},
{
"name": "label",
"type": 3,
"optional": true
},
{
"name": "mode",
"type": 4,
"optional": false
},
{
"name": "version",
"type": 5,
"optional": false
}
],
"indices": []
},
{
"kind": "primitive",
"type": "number"
},
{
"kind": "primitive",
"type": "string"
},
{
"kind": "literal",
"value": "unknown"
},
{
"kind": "literal",
"value": 0
},
{
"kind": "literal",
"value": true
},
{
"kind": "literal",
"value": "subagent/catalog-unknown"
}
]
},
"names": [],
"sources": []
},
{
"digest": "d28765f87d6b6ab408e0b2f3e7e297791b004b806d8e302c49903ebae815feb2",
"schema": {
@@ -77241,20 +77151,6 @@
"names": [],
"sources": []
},
{
"digest": "d437ebdbc1839ffcf3f6fa201a003126ec63945186b0c0792f1db466bf8ebd89",
"schema": {
"root": 0,
"nodes": [
{
"kind": "literal",
"value": "subagent/catalog-unknown"
}
]
},
"names": [],
"sources": []
},
{
"digest": "d4b739491689cc6bb13d07ffac1a32239875abda364c0d5edd8f74faf5e9c00d",
"schema": {
@@ -58,7 +58,6 @@ export const KNOWN_SESSION_EVENT_TYPES: ReadonlySet<string> = new Set([
'step/end',
'step/start',
'subagent/catalog',
'subagent/catalog-unknown',
'subagent/descriptor',
'subagent/model-selection-policy',
'system/message',
+1 -1
View File
@@ -647,7 +647,7 @@ describe('HarnessClient', () => {
const inject = (method: string, params: Record<string, unknown>): void => {
(client as unknown as { dispatchNotification(n: HarnessNotification): void }).dispatchNotification({ method, params })
}
const unknownChild = { type: 'subagent/catalog-unknown', seq: 0, time: 1,
const unknownChild = { type: 'subagent/catalog', seq: 0, time: 1,
data: { version: 0, childId: 'unreadable-child', childCreatedAt: 1, mode: 'unknown' } }
inject('session.event', { sessionId: 'root', event: unknownChild })
inject('subagent.started', { parentSessionId: 'root', childSessionId: 'child' })
@@ -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: 6721d43e7653942b055443a2da99d7fca1d6fd57
README.zh.md: 48b7e5cbd00a19a81730cbf1310e462196cd8ca1
README.md: 1c558d34cc67fc94aafcaf23405b0b1f2a0efa85
README.zh.md: 6c079269503d5e11a0bbb03e3460d5a5432fa9e5
@@ -148,9 +148,9 @@ There is no recursive source search. Captured request text, assistant replay sta
| More than one own descriptor | Retain an existing parent entry without mode/label comparison; otherwise append unknown-mode membership. |
| 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 | Append `subagent/catalog-unknown` with header identity and unknown mode, without inventing a label. |
| Missing own parent entry without complete evidence | Append `subagent/catalog` with header identity and unknown mode, without inventing a label. |
Catalog version 0 requires string `childId`, nonnegative safe-integer `childCreatedAt`, mode `continuable` or `one-shot`, and a string label for continuable mode; a present one-shot label must also be a string. Duplicate own child ids are refused. Existing entries without a corresponding retained child remain in the parent. Descriptor collection does not restore a child's old continuation composition or recover deleted children from tool arguments. `subagent/catalog-unknown` separately records version 0, child id, creation time, and `mode: 'unknown'`; both event types contribute to the same unique-child catalog. Existing `subagent/catalog` payloads retain their mode requirements.
Catalog version 0 requires string `childId`, nonnegative safe-integer `childCreatedAt`, mode `continuable`, `one-shot`, or `unknown`, and a string label for continuable mode; a present label in any mode must also be a string. Duplicate own child ids are refused. Existing entries without a corresponding retained child remain in the parent. Descriptor collection does not restore a child's old continuation composition or recover deleted children from tool arguments. Unknown-mode entries retain header identity without asserting a supported child descriptor.
The stage considers parent catalog records only after the final inherited cut. Every inherited marker discards earlier catalog candidates without interpreting their payloads. Missing entries append after all source events, sorted by creation time then child id, with dense new sequences. Their time is the final source event's time, or header creation time for an empty log. They neither enter the model surface nor change the inherited count.
@@ -243,7 +243,7 @@ Current common admission does not validate each user/tool/developer content bloc
| `compaction/start`, `compaction/summary`, `compaction/end` | Match compaction id, source command, and the active turn context. Summary spans name exact current-surface nodes and exclude the protected head; successful completion has one summary. Inherited unfinished compactions expire at the end-seed marker. |
| `compaction/prune` | Its span names exact current-surface nodes and excludes the protected head; it does not require a compaction transaction or its owner fields. |
| Compact checkpoint replacement | Its `compact-checkpoint` source identifies the active compaction. |
| Native `subagent/catalog` and `subagent/catalog-unknown` | Check own version-0 payload fields and unique child ids after the inherited cut. Native reads neither collect child logs nor compare their physical facts; inherited entries do not establish own membership. |
| Native `subagent/catalog` | Check own version-0 payload fields and unique child ids after the inherited cut. Native reads neither collect child logs nor compare their physical facts; inherited entries do not establish own membership. |
| Inherited cut and delivery | Apply the marker, coordinate, and generation-ownership rules stated above. |
These checks are generation-owned in [relationships.ts](src/relationships.ts). Full common message/envelope acceptance and plugin-owned message projections additionally use the installed Session; the exported V4 restorer alone is not a replacement for complete catalog restoration.
@@ -148,9 +148,9 @@ V3 未知内容标签变为 `plugin:<original-type>`,其他字段原样保留
| 多个自身 descriptor | 保留已有父目录项,不比较 mode/label;否则追加模式未知的目录项。 |
| 已有自身父目录项 | 保留条目及其扩展;要求子创建时间一致,并在恰好一个受支持的自身 descriptor 可用时比较其 mode/label。 |
| 缺少自身父目录项,且受支持证据完整 | 追加带 child id、创建时间、mode 和可选 label 的 version-0 目录事实。 |
| 缺少自身父目录项,且证据不完整 | 追加 `subagent/catalog-unknown`,记录 header 身份和未知模式,不编造标签。 |
| 缺少自身父目录项,且证据不完整 | 追加 `subagent/catalog`,记录 header 身份和未知模式,不编造标签。 |
Catalog version 0 要求字符串 `childId`、非负安全整数 `childCreatedAt`、`continuable` 或 `one-shot` mode,以及 continuable mode 下的字符串 label;存在的 one-shot label 也必须是字符串。重复的自身 child id 被拒绝。没有对应保留子日志的已有条目仍保留在父日志中。Descriptor 收集不恢复子级的旧 continuation composition,也不从工具参数恢复已删除子级。 `subagent/catalog-unknown` 单独记录 version 0、child id、创建时间和 `mode: 'unknown'`;两种事件共同组成 child id 唯一的目录。已有 `subagent/catalog` 载荷保留其 mode 要求。
Catalog version 0 要求字符串 `childId`、非负安全整数 `childCreatedAt`、`continuable`、`one-shot` 或 `unknown` mode,以及 continuable mode 下的字符串 label;任何 mode 下存在的 label 都必须是字符串。重复的自身 child id 被拒绝。没有对应保留子日志的已有条目仍保留在父日志中。Descriptor 收集不恢复子级的旧 continuation composition,也不从工具参数恢复已删除子级。未知模式条目保留 header 身份信息,不声明子 descriptor 受支持。
Stage 只把最终继承截点之后的父目录记录作为候选。每个 inherited marker 都会丢弃更早的目录候选,不解释其载荷。缺失项追加在所有源事件之后,按创建时间、child id 排序,并使用连续的新序号。时间取最后一个源事件的 time;空日志则取 header 创建时间。这些记录既不进入模型表面,也不改变继承计数。
@@ -243,7 +243,7 @@ System image 接纳要求非空 attachment id、PNG/JPEG/WebP/GIF MIME 类
| `compaction/start`、`compaction/summary`、`compaction/end` | 匹配 compaction id、源 command 和活动 turn 上下文。Summary 区间引用精确的当前表面节点且排除 protected head;成功完成需要一个 summary。继承的未完成 compaction 在 end-seed marker 处过期。 |
| `compaction/prune` | 其区间引用精确的当前表面节点且排除 protected head;它不要求存在 compaction 事务或其所有者字段。 |
| Compact checkpoint 替换 | 其 `compact-checkpoint` 来源标识活动 compaction。 |
| 原生 `subagent/catalog` 与 `subagent/catalog-unknown` | 检查继承截点之后的自身 version-0 载荷字段与 child id 唯一性。原生读取既不收集子日志,也不比较其物理事实;继承项不建立自身成员关系。 |
| 原生 `subagent/catalog` | 检查继承截点之后的自身 version-0 载荷字段与 child id 唯一性。原生读取既不收集子日志,也不比较其物理事实;继承项不建立自身成员关系。 |
| 继承截点与 delivery | 应用上文的 marker、坐标及代际归属规则。 |
这些检查由 [relationships.ts](src/relationships.ts) 按代际拥有。完整的通用消息/信封接纳与插件拥有的消息投影还使用已安装 Session;单独的导出 V4 恢复器不能替代完整 catalog 恢复。
@@ -52,6 +52,9 @@ export function childCatalogFact(source: SessionFormatJsonObject): SessionFormat
if (typeof descriptor['provider'] !== 'string') {
throw new SessionFormatUnsupportedMigrationError(`${childCatalogSubject(source)} has an invalid subagent descriptor provider`)
}
if (descriptor['version'] !== 1 && descriptor['mode'] !== 'continuable' && descriptor['mode'] !== 'one-shot') {
throw new SessionFormatUnsupportedMigrationError(`${childCatalogSubject(source)} has an invalid subagent descriptor mode`)
}
return catalogFact({
version: 0, childId: id, childCreatedAt: source['childCreatedAt'] as number,
// V1 described only continuable children and carried no mode field.
@@ -69,25 +72,10 @@ export function childCatalogFact(source: SessionFormatJsonObject): SessionFormat
export function catalogFact(value: SessionFormatJsonValue, subject = 'subagent/catalog'): SessionFormatJsonObject {
if (!isSessionFormatJsonObject(value) || value['version'] !== 0
|| typeof value['childId'] !== 'string'
|| (value['mode'] !== 'continuable' && value['mode'] !== 'one-shot')
|| (value['mode'] !== 'continuable' && value['mode'] !== 'one-shot' && value['mode'] !== 'unknown')
|| (value['mode'] === 'continuable' && typeof value['label'] !== 'string')
|| (value['label'] !== undefined && typeof value['label'] !== 'string')) {
throw new SessionFormatError(`${subject} requires a complete version 0 catalog fact`)
}
sessionFormatCount(value['childCreatedAt'], 'catalog child creation time')
return value
}
/**
* Validate a current catalog entry retained without a supported child descriptor.
* @param value - decoded unknown-child payload.
* @returns the validated identity with unknown mode.
*/
export function unknownCatalogFact(value: SessionFormatJsonValue): SessionFormatJsonObject {
if (!isSessionFormatJsonObject(value) || value['version'] !== 0
|| typeof value['childId'] !== 'string' || value['mode'] !== 'unknown'
|| (value['label'] !== undefined && typeof value['label'] !== 'string')) {
throw new SessionFormatError('subagent/catalog-unknown requires a version 0 identity with unknown mode')
throw new SessionFormatError(`${subject} requires a version 0 catalog fact`)
}
sessionFormatCount(value['childCreatedAt'], 'catalog child creation time')
return value
@@ -124,7 +124,7 @@ class ReleasedV3ToV4Stage implements SessionFormatMigrationStage {
}
const entry = fact ?? { version: 0, childId: id, childCreatedAt: source['childCreatedAt'] as number, mode: 'unknown' }
existingCatalogs.set(id, entry)
context.emitEvent({ type: fact === undefined ? 'subagent/catalog-unknown' : 'subagent/catalog',
context.emitEvent({ type: 'subagent/catalog',
seq: this.nextSeq++, time: this.time, data: entry })
}
return cut
@@ -6,7 +6,7 @@ import type { SessionFormatArtifact, SessionFormatEvent } from '@deepseek-ai/dsh
import { assertV4DeveloperData } from './developer.ts'
import { assertV4LifecycleRelationships } from './relationships.ts'
import { assertV4MessageSources } from './message-sources.ts'
import { catalogFact, unknownCatalogFact } from './facts.ts'
import { catalogFact } from './facts.ts'
import { assertV4RetiredSyntax } from './retired-syntax.ts'
import { assertV4SystemMessageFields } from './system-message.ts'
import { assertV4ForkResult } from './fork-result.ts'
@@ -119,8 +119,8 @@ export function assertReleasedV4Relationships(artifact: SessionFormatArtifact, k
&& deliveryId !== artifact.header.id) {
throw new SessionFormatError('current-generation delivery marker names the wrong Session')
}
if ((event.type === 'subagent/catalog' || event.type === 'subagent/catalog-unknown') && event.seq >= artifact.inheritedEventCount) {
const fact = event.type === 'subagent/catalog' ? catalogFact(event.data) : unknownCatalogFact(event.data)
if (event.type === 'subagent/catalog' && event.seq >= artifact.inheritedEventCount) {
const fact = catalogFact(event.data)
const id = fact['childId'] as string
if (ids.has(id)) throw new SessionFormatError(`duplicate catalog child ${id}`)
ids.add(id)
@@ -67,14 +67,14 @@ describe('V3 parent catalog completion', () => {
expect(migrate(source, [child], true).events).toEqual([
...source, { type: 'subagent/catalog', seq: 4, time: 2, data: child },
])
expect(() => migrate([...source, { ...opaque, seq: 4 }], [], true)).toThrow('complete version 0')
expect(() => migrate([opaque], [])).toThrow('complete version 0')
expect(() => migrate([...source, { ...opaque, seq: 4 }], [], true)).toThrow('version 0')
expect(() => migrate([opaque], [])).toThrow('version 0')
})
it('requires explicit corpus facts and refuses conflicting identities', () => {
expect(() => sessionFormatV3ToV4.createStage({ sourceHeader: header, targetHeader: { ...header, version: 4 },
sourceKind: 'decoded', sourceInheritedEventCount: 0 })).toThrow('explicit historical child facts')
expect(() => migrate([], [{ ...child, mode: 'invalid' }])).toThrow('complete version 0')
expect(() => migrate([], [{ ...child, mode: 'invalid' }])).toThrow('version 0')
expect(() => migrate([{ type: 'subagent/catalog', seq: 0, time: 2, data: child }], [{ ...child, childCreatedAt: 3 }])).toThrow('conflicts')
})
@@ -148,7 +148,7 @@ describe('V3 parent catalog completion', () => {
const unavailable = { childId: 'unavailable', childCreatedAt: 1,
descriptorCount: kind === 'missing' ? 0 : kind === 'multiple' ? 2 : 1, descriptor }
expect(migrate([], [child, unavailable]).events).toEqual([
{ type: 'subagent/catalog-unknown', seq: 0, time: 1, data: { version: 0, childId: 'unavailable', childCreatedAt: 1, mode: 'unknown' } },
{ type: 'subagent/catalog', seq: 0, time: 1, data: { version: 0, childId: 'unavailable', childCreatedAt: 1, mode: 'unknown' } },
{ type: 'subagent/catalog', seq: 1, time: 1, data: child },
])
})
@@ -114,20 +114,19 @@ describe('restoreReleasedV4Artifact', () => {
})
it('retains unknown child membership and rejects invalid identities or duplicate child ids', () => {
const known = new Set([...KNOWN, 'subagent/catalog', 'subagent/catalog-unknown'])
const known = new Set([...KNOWN, 'subagent/catalog'])
const data = { version: 0, childId: 'unreadable', childCreatedAt: 2, mode: 'unknown' }
const event = { type: 'subagent/catalog-unknown', seq: 0, time: 1, data }
const event = { type: 'subagent/catalog', seq: 0, time: 1, data }
const valid = artifact([event])
expect(restoreReleasedV4Artifact(valid, known)).toBe(valid)
expect(() => restoreReleasedV4Artifact(artifact([{ ...event, data: { ...data, label: 'retained' } }]), known)).not.toThrow()
for (const invalid of [null, { ...data, version: 1 }, { ...data, childId: 1 },
{ ...data, childCreatedAt: -1 }, { ...data, mode: 'one-shot' }, { ...data, label: null }]) {
{ ...data, childCreatedAt: -1 }, { ...data, mode: 'invalid' }, { ...data, label: null }]) {
expect(() => restoreReleasedV4Artifact(artifact([{ ...event, data: invalid }]), known)).toThrow()
}
expect(() => restoreReleasedV4Artifact(artifact([event, { ...event, seq: 1 }]), known)).toThrow('duplicate catalog child')
expect(() => restoreReleasedV4Artifact(artifact([event, { type: 'subagent/catalog', seq: 1, time: 1,
data: { ...data, mode: 'one-shot' } }]), known)).toThrow('duplicate catalog child')
expect(() => restoreReleasedV4Artifact(artifact([{ ...event, type: 'subagent/catalog' }]), known)).toThrow('complete version 0')
})
it('validates only the envelope in physical mode', () => {
@@ -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: 066b43d6a0f7c0e29b8c2ebbba17fa460103187a
README.zh.md: 61693b96d904b8d2c3d679c065f3667eb3bdc46e
README.md: 609368aa42f1ba75d818a74877fcd574bf9d5ddf
README.zh.md: 07c0dc94270ddb69812ec8443688a505269eba59
@@ -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 finds candidate direct children from headers, reads each child's own descriptor through historical codecs, and retains compact evidence plus source revisions. It neither prepares child catalogs nor publishes child successors. Unreadable or unsupported headers are omitted from discovery. Child decoding and descriptor-field failures produce warnings naming the child path and retain header identity through `subagent/catalog-unknown` when the parent has no complete entry; healthy children and existing parent catalog entries remain available. Opening a damaged child still reports that child's error. Missing, unsupported, or multiple descriptors likewise produce unknown-mode membership without inventing a label. Published unknown entries remain browsable; child history reads retry the actual log and resolve its mode from a valid descriptor. Preparation rechecks membership and inspected source revisions before returning, reuse, and publication, including failed child reads so a repaired child invalidates stale preparation. Source drift retries a read open once and refuses a write open. Cancellation still aborts the operation. Current V4 opens bypass discovery 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 finds candidate direct children from headers, reads each child's own descriptor through historical codecs, and retains compact evidence plus source revisions. It neither prepares child catalogs nor publishes child successors. Unreadable or unsupported headers are omitted from discovery. Child decoding and descriptor-field failures produce warnings naming the child path and retain header identity through `subagent/catalog` when the parent has no complete entry; healthy children and existing parent catalog entries remain available. Opening a damaged child still reports that child's error. Missing, unsupported, or multiple descriptors likewise produce unknown-mode membership without inventing a label. Published unknown entries remain browsable; child history reads retry the actual log and resolve its mode from a valid descriptor. Preparation rechecks membership and inspected source revisions before returning, reuse, and publication, including failed child reads so a repaired child invalidates stale preparation. Source drift retries a read open once and refuses a write open. Cancellation still aborts the operation. Current V4 opens bypass discovery 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).
@@ -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 字段失败会产生带子路径的警告;父目录没有完整条目时,通过 `subagent/catalog-unknown` 保留 header 身份信息。健康子项和已有父目录项仍可使用。打开损坏子 Session 时仍报告该子会话的错误。缺失、不支持或多个 descriptor 同样生成模式未知的目录项,不编造标签。已发布的未知条目仍可浏览;读取子历史时会重试实际日志,并从有效 descriptor 确定模式。准备返回、复用与发布前会重新检查成员集合及已检查来源的修订,也包括读取失败的子日志,使修复后的子日志能够使旧准备缓存失效。来源变化时只读打开重试一次,写打开拒绝发布。取消仍会中止操作。当前 V4 打开跳过发现,并在暴露事件前校验目录字段、唯一性及当前投递归属。
历史正文准备通过 [V3→V4](../session-format-v3-to-v4/README.zh.md) 补齐父目录:从 header 找到候选直属子 Session,通过历史编解码器逐个读取其自身 descriptor,保留紧凑证据与来源修订。此过程不准备子目录,也不发布子后继。不可读或不支持的 header 不参与发现。子日志解码或 descriptor 字段失败会产生带子路径的警告;父目录没有完整条目时,通过 `subagent/catalog` 保留 header 身份信息。健康子项和已有父目录项仍可使用。打开损坏子 Session 时仍报告该子会话的错误。缺失、不支持或多个 descriptor 同样生成模式未知的目录项,不编造标签。已发布的未知条目仍可浏览;读取子历史时会重试实际日志,并从有效 descriptor 确定模式。准备返回、复用与发布前会重新检查成员集合及已检查来源的修订,也包括读取失败的子日志,使修复后的子日志能够使旧准备缓存失效。来源变化时只读打开重试一次,写打开拒绝发布。取消仍会中止操作。当前 V4 打开跳过发现,并在暴露事件前校验目录字段、唯一性及当前投递归属。
历史格式的 `stat` 与 `list` 修订号需要与根目录 Session 数量成正比的元数据工作。首次正文准备扫描所有所选 header 并解码直属子正文;复用准备缓存仍扫描成员集合并检查修订。只读访问从不发布升级,因此冷进程与被淘汰的准备缓存会重复这些工作。当前 V4 正文读取和修订号避开历史全库扫描。见[实测成本与诊断命令](../../../.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.zh.md#catalog-scan-measurements)。
@@ -120,7 +120,7 @@ describe.each(['none', 'zstd'] as const)('historical catalog publication (%s)',
const catalog = { type: 'subagent/catalog', seq: 0, time: 1,
data: { version: 0, childId: 'grandchild', childCreatedAt: 3, mode: 'one-shot' } }
const path = await f.write('child', [catalog, { ...catalog, seq: 1 }], true, 4)
expect(await f.read()).toMatchObject([{ type: 'subagent/catalog-unknown', data: { childId: 'child', mode: 'unknown' } }])
expect(await f.read()).toMatchObject([{ type: 'subagent/catalog', data: { childId: 'child', mode: 'unknown' } }])
expect(warn).toHaveBeenCalledWith(expect.stringContaining(path))
await expect(f.ctx.sessionPersistence.open(SessionId('child'), 'read')).rejects.toBeInstanceOf(SessionPersistenceCorruptionError)
})
@@ -137,7 +137,7 @@ describe.each(['none', 'zstd'] as const)('historical catalog publication (%s)',
await f.write('healthy', [f.descriptor], true)
const parent = await f.ctx.sessionPersistence.open(f.parent, access)
try { expect((await parent.read()).events).toMatchObject([
{ type: 'subagent/catalog-unknown', data: { childId: 'child', mode: 'unknown' } },
{ type: 'subagent/catalog', data: { childId: 'child', mode: 'unknown' } },
{ type: 'subagent/catalog', data: { childId: 'healthy', mode: 'continuable' } },
]) } finally { await parent.close() }
await expect(f.ctx.sessionPersistence.open(SessionId('child'), access)).rejects.toBeInstanceOf(SessionPersistenceCorruptionError)
@@ -170,7 +170,7 @@ describe.each(['none', 'zstd'] as const)('historical catalog publication (%s)',
const f = await fixture()
const childPath = await f.write('child', [], true)
await writeFile(childPath, Buffer.concat([await readFile(childPath), Buffer.alloc(8)]))
expect(await f.read()).toMatchObject([{ type: 'subagent/catalog-unknown', data: { childId: 'child', mode: 'unknown' } }])
expect(await f.read()).toMatchObject([{ type: 'subagent/catalog', data: { childId: 'child', mode: 'unknown' } }])
await expect(f.ctx.sessionPersistence.open(SessionId('child'), 'read')).rejects.toBeInstanceOf(SessionPersistenceCorruptionError)
})
@@ -208,10 +208,10 @@ describe.each(['none', 'zstd'] as const)('historical catalog publication (%s)',
const childPath = await f.write('child', events, true)
const parentPath = generationLogPath(f.root, undefined, f.parent, 3, compression)
const original = await Promise.all([readFile(parentPath), readFile(childPath)])
expect(await f.read()).toMatchObject([{ type: 'subagent/catalog-unknown', data: { childId: 'child', mode: 'unknown' } }])
expect(await f.read()).toMatchObject([{ type: 'subagent/catalog', data: { childId: 'child', mode: 'unknown' } }])
const writer = await f.ctx.sessionPersistence.open(f.parent, 'write')
try { expect((await writer.read()).events).toMatchObject([{ type: 'subagent/catalog-unknown', data: { childId: 'child', mode: 'unknown' } }]) } finally { await writer.close() }
expect(await f.read()).toMatchObject([{ type: 'subagent/catalog-unknown', data: { childId: 'child', mode: 'unknown' } }])
try { expect((await writer.read()).events).toMatchObject([{ type: 'subagent/catalog', data: { childId: 'child', mode: 'unknown' } }]) } finally { await writer.close() }
expect(await f.read()).toMatchObject([{ type: 'subagent/catalog', data: { childId: 'child', mode: 'unknown' } }])
const child = await f.ctx.sessionPersistence.open(SessionId('child'), 'read')
try { expect((await child.read()).events).toEqual(events) } finally { await child.close() }
expect(await Promise.all([readFile(parentPath), readFile(childPath)])).toEqual(original)
@@ -240,7 +240,7 @@ describe.each(['none', 'zstd'] as const)('historical catalog publication (%s)',
it('retains unknown child identity without migrating unsupported contents', async () => {
const f = await fixture()
await f.write('child', [{ type: 'future/required', seq: 0, time: 2, data: {} }], true)
expect(await f.read()).toMatchObject([{ type: 'subagent/catalog-unknown', data: { childId: 'child', mode: 'unknown' } }])
expect(await f.read()).toMatchObject([{ type: 'subagent/catalog', data: { childId: 'child', mode: 'unknown' } }])
await expect(f.ctx.sessionPersistence.open(SessionId('child'), 'read')).rejects.toBeInstanceOf(SessionFormatUnsupportedError)
})
@@ -248,7 +248,7 @@ describe.each(['none', 'zstd'] as const)('historical catalog publication (%s)',
const f = await fixture()
const warn = vi.spyOn(f.ctx.logger, 'warn')
const path = await f.write('child', [{ ...f.descriptor, data: { version: 2, provider: 'spawn', mode: 'unknown' } }], true)
expect(await f.read()).toMatchObject([{ type: 'subagent/catalog-unknown', data: { childId: 'child', mode: 'unknown' } }])
expect(await f.read()).toMatchObject([{ type: 'subagent/catalog', data: { childId: 'child', mode: 'unknown' } }])
expect(warn).toHaveBeenCalledWith(expect.stringContaining(path))
})
@@ -283,7 +283,7 @@ describe.each(['none', 'zstd'] as const)('historical catalog publication (%s)',
it('recollects newly available discovery fields before publishing a prepared parent', async () => {
const f = await fixture()
const childPath = await f.write('child', [], true)
expect(await f.read()).toMatchObject([{ type: 'subagent/catalog-unknown', data: { childId: 'child', mode: 'unknown' } }])
expect(await f.read()).toMatchObject([{ type: 'subagent/catalog', data: { childId: 'child', mode: 'unknown' } }])
await f.write('child', [f.descriptor], true)
await expect(f.ctx.sessionPersistence.open(f.parent, 'write')).rejects.toThrow(childPath)
const parentPath = generationLogPath(f.root, undefined, f.parent, 3, compression)
@@ -383,7 +383,7 @@ describe.each(['none', 'zstd'] as const)('historical catalog publication (%s)',
it('recollects a repaired child before publishing a prepared parent', async () => {
const f = await fixture()
await f.write('child', [{ type: 'future/required', seq: 0, time: 2, data: {} }], true)
expect(await f.read()).toMatchObject([{ type: 'subagent/catalog-unknown', data: { childId: 'child', mode: 'unknown' } }])
expect(await f.read()).toMatchObject([{ type: 'subagent/catalog', data: { childId: 'child', mode: 'unknown' } }])
await f.write('child', [f.descriptor], true)
expect(await f.read()).toMatchObject([{ type: 'subagent/catalog', data: { childId: 'child' } }])
const writer = await f.ctx.sessionPersistence.open(f.parent, 'write')
+2 -2
View File
@@ -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/subagent/subagent/README.md
README.md: 94da1be172b441aec1fd16d3853596eaee74ca1b
README.zh.md: 462bc3aff2155b96f87d9f69c5aae1aa5002a9ca
README.md: 75237c6eda835459015d5b2f64402190d27d72af
README.zh.md: 187d26ea3a4261c4ddeed96cd6c41f50fbd48c91
+1 -1
View File
@@ -109,7 +109,7 @@ A request is validated against the provider's advertised capabilities, a durable
The manager reserves a child identity, resolves the durable descriptor, creates (or cold-resumes) the child Agent, installs it in an Activation, and submits the prompt. Model-authored messages cross one parent/child edge through fixed Steer scheduling; browser human prompts choose Queue or best-effort Steer through an internal adapter, while other host protocols may retain Queue for distinct turns. A Session queue command admits a live subagent-owned Agent only from its own continuable descriptor. Settlement waits for Agent activity to finish, an empty Inbox, and no owned children, then flushes final Session state with admission open. Under the child lock, the manager revalidates the wake generation, Session sequence, Inbox, and owned children; the synchronous task entry of `Agent.runMaintenance()` claims the idle phase and closes the private subagent Inbox in the same JavaScript turn before handle disposal. An absent direct-child Activation cold-resumes from the persisted session. When a resident Activation settles, the manager tells the child's direct parent in the parent's own turn stream.
Successful local child creation appends a `subagent/catalog` fact to the parent Session. One-shot creation records it after the provider returns; continuable creation records it after initial inbox admission and before returning the child id. Failure releases the child without publishing a compensating catalog event. A one-shot catalog append failure handles the run’s result rejection and preserves the catalog error; disposal failures are logged separately. The `subagentCatalog` projection excludes fork-inherited facts and exposes a direct-child list through `projections.values.subagentCatalog` in Session observations and client snapshots. Each child's `subagentTiming` projection accumulates post-descriptor duration and records whether its latest closed turn ended with `completed`, clearing that completion when another turn opens. Invalid own catalog payloads, including unsupported versions, reject projection restoration. Projection state-version changes refold cached rows from the durable log. The catalog view preserves parent event order in O(D) time for D facts, and its immutable storage and checkpoint validation use [`dsh-chunked-list`](../../util/chunked-list/README.md). [The parent-catalog decision](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns ordering, persistence costs, and alternatives. Historical migration may instead append `subagent/catalog-unknown` from a readable child header. Its `mode: 'unknown'` projection keeps the child visible without claiming continuation support; an existing complete entry remains authoritative.
Successful local child creation appends a `subagent/catalog` fact to the parent Session. One-shot creation records it after the provider returns; continuable creation records it after initial inbox admission and before returning the child id. Failure releases the child without publishing a compensating catalog event. A one-shot catalog append failure handles the run’s result rejection and preserves the catalog error; disposal failures are logged separately. The `subagentCatalog` projection excludes fork-inherited facts and exposes a direct-child list through `projections.values.subagentCatalog` in Session observations and client snapshots. Each child's `subagentTiming` projection accumulates post-descriptor duration and records whether its latest closed turn ended with `completed`, clearing that completion when another turn opens. Invalid own catalog payloads, including unsupported versions, reject projection restoration. Projection state-version changes refold cached rows from the durable log. The catalog view preserves parent event order in O(D) time for D facts, and its immutable storage and checkpoint validation use [`dsh-chunked-list`](../../util/chunked-list/README.md). [The parent-catalog decision](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.md) owns ordering, persistence costs, and alternatives. Historical migration may instead append `subagent/catalog` from a readable child header. Its `mode: 'unknown'` projection keeps the child visible without claiming continuation support; an existing complete entry remains authoritative.
### Ownership and invariants
+1 -1
View File
@@ -109,7 +109,7 @@ kind: "package-reference"
管理器预留 child 身份、解析持久化描述符、创建(或冷恢复)child、把它安装进 Activation 并提交提示词。模型编写的消息通过固定 Steer 调度跨一条 parent/child 边;浏览器人类 prompt 通过内部适配器选择 Queue 或 best-effort Steer,其他 host 协议仍可保留 Queue 以创建独立轮次。Session queue command 仅根据 child 自身的 continuable descriptor 准入在线 subagent-owned Agent。Settlement 会等待 Agent 活动结束、Inbox 为空且没有所拥有子级,再在准入开放时 flush 最终 Session 状态。管理器随后在 child lock 内重新验证 wake generation、Session 序号、Inbox 与所拥有子级;`Agent.runMaintenance()` 的同步 task 入口会占用 idle 阶段,并在同一个 JavaScript turn 内关闭私有 subagent Inbox,然后才释放句柄。直接 child 不存在 Activation 时会从持久化会话冷恢复。当驻留 Activation 结算时,管理器会在 parent 自身的轮次流中告知该 child 的直接 parent。
本地子级创建成功时,父 Session 追加一条 `subagent/catalog` 事实。一次性创建在提供方返回后记录;可继续创建在初始 inbox 准入后、返回子级 id 前记录。失败会释放子级,不发布补偿性目录事件。一次性目录追加失败时会处理 run 的结果拒绝,并保留目录错误;资源释放失败会单独记录。`subagentCatalog` projection 排除 fork 继承的事实,通过 Session 观察和客户端快照中的 `projections.values.subagentCatalog` 暴露直接子级列表。每个 child 的 `subagentTiming` projection 会累加 descriptor 之后的耗时,并记录最近一个已结束轮次是否以 `completed` 结束;新轮次打开时会清除该完成状态。无效的自身 catalog payload(包括不支持的版本)会使 projection 恢复失败。projection 状态版本变更会从持久日志重新折叠缓存行。目录视图对 D 条事实以 O(D) 时间保留父目录事件顺序,其不可变存储和检查点校验使用 [`dsh-chunked-list`](../../util/chunked-list/README.zh.md)。[父目录决策](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明排序、持久化成本和替代方案。 历史迁移可根据可读子 header 追加 `subagent/catalog-unknown`。其 `mode: 'unknown'` 投影让子会话保持可见,但不表示支持继续执行;已有完整条目仍具有权威性。
本地子级创建成功时,父 Session 追加一条 `subagent/catalog` 事实。一次性创建在提供方返回后记录;可继续创建在初始 inbox 准入后、返回子级 id 前记录。失败会释放子级,不发布补偿性目录事件。一次性目录追加失败时会处理 run 的结果拒绝,并保留目录错误;资源释放失败会单独记录。`subagentCatalog` projection 排除 fork 继承的事实,通过 Session 观察和客户端快照中的 `projections.values.subagentCatalog` 暴露直接子级列表。每个 child 的 `subagentTiming` projection 会累加 descriptor 之后的耗时,并记录最近一个已结束轮次是否以 `completed` 结束;新轮次打开时会清除该完成状态。无效的自身 catalog payload(包括不支持的版本)会使 projection 恢复失败。projection 状态版本变更会从持久日志重新折叠缓存行。目录视图对 D 条事实以 O(D) 时间保留父目录事件顺序,其不可变存储和检查点校验使用 [`dsh-chunked-list`](../../util/chunked-list/README.zh.md)。[父目录决策](../../../.agents/notes/implemented/architecture/2026-09-01-parent-owned-subagent-catalog.zh.md) 说明排序、持久化成本和替代方案。 历史迁移可根据可读子 header 追加 `subagent/catalog`。其 `mode: 'unknown'` 投影让子会话保持可见,但不表示支持继续执行;已有完整条目仍具有权威性。
### 所有权与不变式
+7 -22
View File
@@ -20,7 +20,7 @@ import type { SubagentCatalogEntry } from './projection-types.ts'
/** Current payload version for `subagent/catalog` events. */
export const SUBAGENT_CATALOG_VERSION = 0
/** One complete parent-owned catalog fact. */
/** One parent-owned catalog fact with known or unknown child mode. */
export type SubagentCatalogEvent =
& {
readonly version: 0
@@ -29,35 +29,23 @@ export type SubagentCatalogEvent =
} & (
| { readonly mode: 'one-shot'; readonly label?: string }
| { readonly mode: 'continuable'; readonly label: string }
| { readonly mode: 'unknown'; readonly label?: string }
)
type UnknownCatalogEvent = {
readonly version: 0
readonly childId: SessionId
readonly childCreatedAt: number
readonly mode: 'unknown'
readonly label?: string
}
declare module '@deepseek-ai/dsh-session/types' {
interface SessionEventMap {
/**
* A direct child's complete discovery fact.
* A direct child's identity and available discovery fields.
* @param data - versioned parent-owned catalog entry.
*/
'subagent/catalog': SubagentCatalogEvent
/**
* A historical child's header identity when its descriptor cannot establish a mode.
* @param data - child identity retained for discovery and later history reads.
*/
'subagent/catalog-unknown': UnknownCatalogEvent
}
}
/** Host fold state for one parent catalog. */
export interface SubagentCatalogState {
readonly inheritedEventCount: SessionLogOffset
readonly head?: ChunkedList<SubagentCatalogEvent | UnknownCatalogEvent> | undefined
readonly head?: ChunkedList<SubagentCatalogEvent> | undefined
}
const sessionIdSchema = z.string() as unknown as z.ZodType<SessionId>
@@ -76,12 +64,11 @@ const continuableCatalogSchema = z.object({
label: z.string(),
}).strict()
const unknownCatalogSchema = oneShotCatalogSchema.extend({ mode: z.literal('unknown') })
const completeCatalogSchema = z.union([oneShotCatalogSchema, continuableCatalogSchema])
const eventDataSchema = z.union([
oneShotCatalogSchema,
continuableCatalogSchema,
unknownCatalogSchema,
]) as unknown as z.ZodType<SubagentCatalogEvent | UnknownCatalogEvent>
]) as unknown as z.ZodType<SubagentCatalogEvent>
const viewSchema = z.array(z.union([
oneShotCatalogSchema.omit({ version: true, childId: true, childCreatedAt: true }).extend({
id: sessionIdSchema,
@@ -138,10 +125,8 @@ export const subagentCatalogProjectionDefinition = {
stateSchema,
init: (_header: SessionHeader, inheritedEventCount: SessionLogOffset) => ({ inheritedEventCount }),
apply: (state, event: SessionEvent) => {
if ((event.type !== 'subagent/catalog' && event.type !== 'subagent/catalog-unknown') || event.seq < state.inheritedEventCount) return state
const data = (event.type === 'subagent/catalog-unknown' ? unknownCatalogSchema : completeCatalogSchema)
.parse(event.data) as SubagentCatalogEvent | UnknownCatalogEvent
return { ...state, head: appendChunkedList(state.head, data) }
if (event.type !== 'subagent/catalog' || event.seq < state.inheritedEventCount) return state
return { ...state, head: appendChunkedList(state.head, eventDataSchema.parse(event.data)) }
},
stateVersion: 3,
wire: { viewSchema, view: subagentCatalogEntries },
@@ -107,7 +107,7 @@ describe('subagent catalog projection', () => {
fact(0, 'child-b', 1, { mode: 'one-shot' }),
fact(1, 'child-a', 1, { mode: 'one-shot', label: 'once' }),
fact(2, 'child-d', 3, { mode: 'continuable', label: 'later' }),
{ type: 'subagent/catalog-unknown', seq: SessionSeq(3), time: 0,
{ type: 'subagent/catalog', seq: SessionSeq(3), time: 0,
data: { version: 0, childId: SessionId('unreadable'), childCreatedAt: 4, mode: 'unknown' } },
]
const state = fold(events)
+1 -1
View File
@@ -596,7 +596,7 @@ def test_session_subscription_keeps_descendant_relationships_across_subscription
client = HarnessClient()
with client.subscribe_session_notifications("main") as first:
unknown_child = {
"type": "subagent/catalog-unknown", "seq": 0, "time": 1,
"type": "subagent/catalog", "seq": 0, "time": 1,
"data": {"version": 0, "childId": "unreadable-child", "childCreatedAt": 1, "mode": "unknown"},
}
client._handle_message({
+53
View File
@@ -397,7 +397,60 @@ function unionBody(arms: readonly (readonly SchemaProperty[])[]): PersistenceRoo
return { ...typeRoot('event:example/value', {}), schema, digest: schemaDigest(schema) }
}
function catalogModeRoot(unknownMode?: string, changeLabel = false, extraField = false): PersistenceRoot {
const snapshot = parsePersistenceSnapshot(JSON.parse(readFileSync(
new URL('../docs/persistence-changes/2026-09-11-initial.schema.json', import.meta.url), 'utf8',
)))
const root = snapshot.roots.find(root => root.key === 'event:subagent/catalog')!
const nodes: SchemaNode[] = [...structuredClone(root.schema.nodes)]
const union = nodes.find(node => node.kind === 'union')!
if (union.kind !== 'union') throw new Error('catalog fixture requires mode alternatives')
const oneShot = union.types.find((index) => {
const node = nodes[index]
if (node?.kind !== 'object') return false
const mode = nodes[node.properties.find(property => property.name === 'mode')!.type]
return mode?.kind === 'literal' && mode.value === 'one-shot'
})!
const variant = nodes[oneShot]!
if (variant.kind !== 'object') throw new Error('catalog fixture requires object alternatives')
if (unknownMode !== undefined) {
const index = nodes.length
nodes.push({ ...variant, properties: variant.properties.map(property => property.name === 'mode'
? { ...property, type: index + 1 } : property) }, { kind: 'literal', value: unknownMode })
const added = nodes[index]!
if (extraField && added.kind === 'object') nodes[index] = { ...added,
properties: [...added.properties, { name: 'extra', type: variant.properties[0]!.type, optional: false }] }
nodes[nodes.indexOf(union)] = { ...union, types: [...union.types, index] }
}
if (changeLabel) nodes[oneShot] = { ...variant, properties: variant.properties.filter(property => property.name !== 'label') }
const schema = canonicalizeSchema(nodes, 0)
return { ...root, schema, digest: schemaDigest(schema) }
}
describe('persistence change classification', () => {
it('acknowledges unknown catalog mode at V4 while retaining the accepted checkpoint', () => {
const root = fixture()
const before = { ...inventory({}, 4), roots: [...inventory({}, 4).roots.filter(root => root.kind !== 'event'), catalogModeRoot()] }
finalize(root, before)
const after = { ...before, roots: before.roots.map(root => root.kind === 'event' ? catalogModeRoot('unknown') : root) }
expect(classifyPersistenceChange(catalogModeRoot(), catalogModeRoot('unknown')))
.toEqual([expect.objectContaining({ path: 'event:subagent/catalog.data', requiresVersionBump: false })])
runPersistenceChanges(['--record', COMPATIBLE_ID, '--prose', proseFile(root)], root, () => after)
verifyPersistenceChanges(root, after)
})
it('keeps other catalog modes, altered fields, classification changes, and removals breaking', () => {
const before = catalogModeRoot()
for (const after of [catalogModeRoot('other'), catalogModeRoot('unknown', true), catalogModeRoot('unknown', false, true)]) {
expect(classifyPersistenceChange(before, after).some(change => change.requiresVersionBump)).toBe(true)
}
const expanded = catalogModeRoot('unknown')
expect(classifyPersistenceChange(expanded, before).some(change => change.requiresVersionBump)).toBe(true)
expect(classifyPersistenceChange(before, { ...expanded, surface: true }).some(change => change.requiresVersionBump)).toBe(true)
expect(classifyPersistenceChange({ ...before, key: 'event:other' }, { ...expanded, key: 'event:other' })
.some(change => change.requiresVersionBump)).toBe(true)
})
it('treats a new optional payload subtree as one additive change even with required descendants', () => {
const before = typeRoot('event:example/value', { value: 'string' })
const after = typeRoot('event:example/value', { value: 'string', 'details?': { name: 'string', count: 'number' } })
+23
View File
@@ -471,6 +471,29 @@ export function classifyPersistenceChange(before: PersistenceRoot | null, after:
function compareAlternatives(
oldTypes: readonly number[], newTypes: readonly number[], path: string, scope: Scope, active: ReadonlySet<string>,
): PersistenceTypeChange[] {
if (key === 'event:subagent/catalog' && path === `${key}.data` && scope === 'body'
&& oldTypes.length === 2 && newTypes.length === 3) {
const modeNode = (index: number): number | undefined => {
const node = newRoot.schema.nodes[index]
return node?.kind === 'object' ? node.properties.find(property => property.name === 'mode' && !property.optional)?.type : undefined
}
const mode = (index: number): string | number | boolean | null | undefined => {
const node = newRoot.schema.nodes[modeNode(index) ?? -1]
return node?.kind === 'literal' ? node.value : undefined
}
const unknown = newTypes.find(index => mode(index) === 'unknown')
const oneShot = newTypes.find(index => mode(index) === 'one-shot')
if (unknown !== undefined && oneShot !== undefined) {
// The catalog exception admits only the one-shot fields with unknown mode;
// existing branches still pass the ordinary structural comparison below.
const normalized = { ...newRoot.schema, nodes: newRoot.schema.nodes.map((node, index) =>
index === modeNode(unknown) ? { kind: 'literal' as const, value: 'one-shot' } : node) }
if (subDigest(normalized, unknown) === fingerprint(newRoot.schema, oneShot, 1)) {
return [describe(path, 'union-variants-changed', false),
...compareAlternatives(oldTypes, newTypes.filter(index => index !== unknown), path, scope, active)]
}
}
}
if (oldTypes.length !== newTypes.length) return [describe(path, 'union-variants-changed')]
const candidates = oldTypes.map(oldType => newTypes.map(newType => compare(oldType, newType, path, scope, active)))
const matching = matchUnionVariants(candidates.map(row => row.flatMap((candidate, index) =>