test(snapshot): bind sibling roles to catalog publication order

This commit is contained in:
Dudu-0223
2026-09-08 23:28:50 +08:00
parent 9f69a7fb16
commit e2fa69f5e2
4 changed files with 39 additions and 8 deletions
@@ -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/architecture/2026-09-01-parent-owned-subagent-catalog.md
2026-09-01-parent-owned-subagent-catalog.md: b0eee83499f8f9f3b3e2be652037dc882e005c94
2026-09-01-parent-owned-subagent-catalog.zh.md: 8985eb291c5dbfadb53eb3576dcd9306496aa983
2026-09-01-parent-owned-subagent-catalog.md: 24036545d72070e0c46dd4d6ac4ac30728140afd
2026-09-01-parent-owned-subagent-catalog.zh.md: 416fba9fc75842b3b4a2a46c67568d232135cae2
@@ -24,6 +24,8 @@ The utility owns chunk layout and its shared capacity constant; the catalog owns
Fork isolation uses the exact `Session.inheritedEventCount` supplied to projection initialization. The fold ignores `subagent/catalog` events below that offset. The state stores the inherited offset but not each event seq because acceptance is decided during folding.
Headless snapshot collection assigns sibling fixture roles by their parent catalog order, regardless of child creation timestamps: provider startup can publish an older Session after a newer one. The collection preserves each log verbatim.
Snapshot normalizers zero `childCreatedAt` because it originates from the process clock. Event order and source-event references remain intact: adjacent facts can come from sequential creation, so adjacency does not establish commutativity.
## Alternatives considered
@@ -24,6 +24,8 @@ child header 与 `subagent/descriptor` 继续拥有恢复与 composition 权威
fork 隔离使用 projection 初始化时提供的精确 `Session.inheritedEventCount`。fold 忽略该 offset 之前的 `subagent/catalog` 事件。state 保存 inherited offset,但不保存每条 event seq,因为接受判定已在 fold 时完成。
Headless 快照采集按父目录顺序分配同父子级的 fixture 角色,不依赖子级创建时间戳:provider 启动可能在较新的 Session 之后发布较旧的 Session。采集过程原样保留每份日志。
snapshot normalizer 会把 `childCreatedAt` 归零,因为它来自 process clock。事件顺序与来源事件引用保持不变:相邻 fact 也可能来自顺序创建,因此相邻关系不能证明可交换性。
## 考虑过的替代方案
+33 -6
View File
@@ -176,8 +176,8 @@ async function persistedSessions(cwd: string): Promise<SessionLog[]> {
expect(assertPersistedSessionVersion(basename(file), content), `${file}: current writer`).toBe(SESSION_FORMAT_VERSION)
return { content, header: headerOf(content) }
}))
// Same-millisecond siblings bind to fixture roles by their parent's recorded
// discovery order; random persistence filenames do not identify child roles.
// Siblings bind to fixture roles by catalog publication order; concurrent
// provider startup can publish an older Session after a newer one.
const catalogOrders = new Map(logs.map(log => [log.header.id, new Map(
records(log.content)
.filter(event => event.type === 'subagent/catalog')
@@ -187,10 +187,12 @@ async function persistedSessions(cwd: string): Promise<SessionLog[]> {
const leftChild = typeof left.header.parentSession === 'string'
const rightChild = typeof right.header.parentSession === 'string'
if (leftChild !== rightChild) return leftChild ? 1 : -1
const timeOrder = Number(left.header.createdAt) - Number(right.header.createdAt)
if (timeOrder !== 0 || left.header.parentSession !== right.header.parentSession) return timeOrder
const catalogOrder = catalogOrders.get(left.header.parentSession)
return (catalogOrder?.get(left.header.id) ?? Infinity) - (catalogOrder?.get(right.header.id) ?? Infinity)
if (left.header.parentSession === right.header.parentSession) {
const catalogOrder = catalogOrders.get(left.header.parentSession)
const childOrder = (catalogOrder?.get(left.header.id) ?? Infinity) - (catalogOrder?.get(right.header.id) ?? Infinity)
if (childOrder) return childOrder
}
return Number(left.header.createdAt) - Number(right.header.createdAt)
})
}
@@ -782,6 +784,31 @@ describe('headless recorded-session snapshots', () => {
expect(stderrFromSession(log)).toBe('dsh: reasoning:\nfirst thought\ndsh: reasoning:\nsecond\n')
})
it.each([10, 20])('assigns sibling roles by catalog order when the first child timestamp is %i', async (firstCreatedAt) => {
const cwd = await mkdtemp(join(tmpdir(), 'dsh-headless-catalog-order-'))
try {
const logs = [
[
{ type: 'session', version: 2, id: 'parent', createdAt: 1 },
{ type: 'subagent/catalog', data: { childId: 'child-z' } },
{ type: 'subagent/catalog', data: { childId: 'child-a' } },
],
[{ type: 'session', version: 2, id: 'child-z', createdAt: firstCreatedAt, parentSession: 'parent' }],
[{ type: 'session', version: 2, id: 'child-a', createdAt: 10, parentSession: 'parent' }],
].map(rows => rows.map(row => JSON.stringify(row)).join('\n') + '\n')
for (const content of logs) {
const directory = join(cwd, '.dsh', 'sessions', String(headerOf(content).id))
await mkdir(directory, { recursive: true })
await writeFile(join(directory, 'session.v2.jsonl'), content)
}
const actual = await persistedSessions(cwd)
expect(actual.map(log => log.header.id)).toEqual(['parent', 'child-z', 'child-a'])
expect(actual.map(log => log.content)).toEqual(logs)
} finally {
await rm(cwd, { recursive: true, force: true })
}
})
it('writes header sidecars without replacing a retained Session generation', async () => {
const directory = await mkdtemp(join(tmpdir(), 'dsh-headless-sidecars-'))
try {