fix(acp-snapshot): retain unchanged message ids

This commit is contained in:
Tianyi Cui
2026-07-30 15:41:16 +08:00
parent 69214b4708
commit eb51d4c669
11 changed files with 253 additions and 27 deletions
@@ -3,11 +3,13 @@
"logs": [
{ "file": "b/parent/session.jsonl", "lines": [
{ "type": "session", "id": "{{SID}}", "createdAt": 700, "cwd": "{{CWD}}", "delegationDepth": 0 },
{ "type": "request/header", "seq": 0, "time": 3, "data": { "header": { "config": { "model": "fake" }, "system": "SYS PROMPT", "tools": [{ "name": "t1", "description": "D1", "parameters": { "type": "object" } }] }, "reason": "initial" } }
{ "type": "request/header", "seq": 0, "time": 3, "data": { "header": { "config": { "model": "fake" }, "system": "SYS PROMPT", "tools": [{ "name": "t1", "description": "D1", "parameters": { "type": "object" } }] }, "reason": "initial" } },
{ "type": "user/message", "seq": 1, "time": 5, "data": { "role": "user", "content": [{ "type": "text", "text": "same inherited message" }], "source": { "kind": "user" }, "id": "11111111-1111-4111-8111-111111111111" }, "surfaceOp": "append" }
]},
{ "file": "b/child/session.jsonl", "lines": [
{ "type": "session", "id": "abababab-cdcd-4efe-8ada-badabadabada", "createdAt": 800, "cwd": "{{CWD}}", "parentSession": "{{SID}}", "delegationDepth": 1 },
{ "type": "request/header", "seq": 0, "time": 2, "data": { "header": { "config": { "model": "fake" }, "system": "SYS PROMPT", "tools": [{ "name": "t1", "description": "D1", "parameters": { "type": "object" } }] }, "reason": "initial" } }
{ "type": "request/header", "seq": 0, "time": 2, "data": { "header": { "config": { "model": "fake" }, "system": "SYS PROMPT", "tools": [{ "name": "t1", "description": "D1", "parameters": { "type": "object" } }] }, "reason": "initial" } },
{ "type": "user/message", "seq": 1, "time": 5, "data": { "role": "user", "content": [{ "type": "text", "text": "same inherited message" }], "source": { "kind": "user" }, "id": "11111111-1111-4111-8111-111111111111" }, "surfaceOp": "append" }
]}
]
}
@@ -1,2 +1,3 @@
{"type":"session","id":"abababab-cdcd-4efe-8ada-badabadabada","createdAt":800,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-KBQJbW","parentSession":"f6fa7fcf-dd9c-4b39-8815-b25ddcebfd88","delegationDepth":1}
{"type":"request/header","seq":0,"time":2,"data":{"header":{"config":{"model":"fake"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"user/message","seq":1,"time":5,"data":{"role":"user","content":[{"type":"text","text":"same inherited message"}],"source":{"kind":"user"},"id":"22222222-2222-4222-8222-222222222222"},"surfaceOp":"append"}
@@ -1,2 +1,3 @@
{"type":"session","id":"f6fa7fcf-dd9c-4b39-8815-b25ddcebfd88","createdAt":700,"cwd":"/var/folders/2g/b32ct0qn1d728l_v6tdkjytr0000gn/T/acp-snap-cwd-KBQJbW","delegationDepth":0}
{"type":"request/header","seq":0,"time":3,"data":{"header":{"config":{"model":"fake"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"user/message","seq":1,"time":5,"data":{"role":"user","content":[{"type":"text","text":"same inherited message"}],"source":{"kind":"user"},"id":"22222222-2222-4222-8222-222222222222"},"surfaceOp":"append"}
@@ -193,6 +193,18 @@ describe('defineAcpSnapshotSuite: record inventory write-back', () => {
expect(fixture).toContain('"cwd":"{{cwd}}"')
expect(() => readFileSync(join(recordDir, 'rec-child', 'session.2.jsonl'), 'utf8')).toThrow()
})
it('retains an unchanged message id across the recorded parent and child fixtures', () => {
const existingMessageId = '22222222-2222-4222-8222-222222222222'
const freshMessageId = '11111111-1111-4111-8111-111111111111'
const fixtures = ['session.jsonl', 'session.1.jsonl']
.map(file => readFileSync(join(recordDir, 'rec-child', file), 'utf8'))
for (const fixture of fixtures) {
expect(fixture).toContain(`"id":"${existingMessageId}"`)
expect(fixture).not.toContain(freshMessageId)
}
})
})
describe('defineAcpSnapshotSuite: registration contract', () => {
@@ -663,6 +675,78 @@ describe('refreshFixtureReplacements', () => {
{ from: freshBash, to: oldBash },
])
})
it('maps one inherited message id across parent and child logs', () => {
const freshMessageId = '11111111-1111-4111-8111-111111111111'
const existingMessageId = '22222222-2222-4222-8222-222222222222'
const content = [{ type: 'text', text: 'inherited' }]
const log = (sessionId: string, messageId: string): string => [
JSON.stringify({ type: 'session', id: sessionId, cwd: '/same' }),
JSON.stringify({
type: 'user/message',
data: { role: 'user', content, source: { kind: 'user' }, id: messageId },
}),
'',
].join('\n')
const harvested = (content: string): HarvestedLog => ({ id: 'diagnostic', createdAt: 1, content })
const replacements = refreshFixtureReplacements(
[harvested(log('fresh-parent', freshMessageId)), harvested(log('fresh-child', freshMessageId))],
[log('old-parent', existingMessageId), log('old-child', existingMessageId)],
)
expect(replacements.filter(replacement => replacement.from === freshMessageId)).toEqual([
{ from: freshMessageId, to: existingMessageId },
])
})
it('keeps fresh ids for new, changed, and ambiguous messages', () => {
const ids = {
new: '11111111-1111-4111-8111-111111111111',
changed: '22222222-2222-4222-8222-222222222222',
ambiguousA: '33333333-3333-4333-8333-333333333333',
ambiguousB: '44444444-4444-4444-8444-444444444444',
oldChanged: '55555555-5555-4555-8555-555555555555',
oldAmbiguous: '66666666-6666-4666-8666-666666666666',
stable: '77777777-7777-4777-8777-777777777777',
} as const
const message = (id: string, text: string): Record<string, unknown> => ({
type: 'user/message',
data: { role: 'user', content: [{ type: 'text', text }], source: { kind: 'user' }, id },
})
const log = (messages: Record<string, unknown>[]): string => [
JSON.stringify({ type: 'session', id: 'same', cwd: '/same' }),
...messages.map(record => JSON.stringify(record)),
'',
].join('\n')
const fresh = log([
message(ids.new, 'new'),
message(ids.changed, 'changed'),
message(ids.changed, 'changed again'),
message(ids.ambiguousA, 'duplicate'),
message(ids.ambiguousB, 'duplicate'),
message(ids.stable, 'stable'),
])
const existing = log([
message(ids.oldChanged, 'before'),
message(ids.oldAmbiguous, 'duplicate'),
message(ids.stable, 'stable'),
])
const replacements = refreshFixtureReplacements(
[{ id: 'diagnostic', createdAt: 1, content: fresh }],
[existing],
)
const replacedIds = replacements.map(replacement => replacement.from)
for (const id of [
ids.new,
ids.changed,
ids.ambiguousA,
ids.ambiguousB,
ids.stable,
]) expect(replacedIds).not.toContain(id)
})
})
describe('stabilizeRefreshLog', () => {
@@ -765,6 +849,50 @@ describe('stabilizeRefreshLog', () => {
].join('\n'))
})
it('retains unchanged message ids across an unrelated inserted event', () => {
const freshUserId = '11111111-1111-4111-8111-111111111111'
const existingUserId = '22222222-2222-4222-8222-222222222222'
const freshAssistantId = '33333333-3333-4333-8333-333333333333'
const existingAssistantId = '44444444-4444-4444-8444-444444444444'
const user = (id: string): Record<string, unknown> => ({
type: 'user/message',
data: { role: 'user', content: [{ type: 'text', text: 'same user' }], source: { kind: 'user' }, id },
})
const assistant = (id: string): Record<string, unknown> => ({
type: 'assistant/message',
data: {
turn: 1,
step: 1,
message: {
role: 'assistant',
content: [{ type: 'text', text: 'same assistant' }],
source: { kind: 'model', provider: 'fake', model: 'fake' },
id,
},
},
})
const lines = (records: Record<string, unknown>[]): string => [
JSON.stringify({ type: 'session', id: 'same', createdAt: 1, cwd: '/same' }),
...records.map(record => JSON.stringify(record)),
'',
].join('\n')
const fresh = lines([
user(freshUserId),
{ type: 'session/inherited', data: {} },
assistant(freshAssistantId),
])
const existing = lines([user(existingUserId), assistant(existingAssistantId)])
const replacements = refreshFixtureReplacements(
[{ id: 'diagnostic', createdAt: 1, content: fresh }],
[existing],
)
const output = stabilize(fresh, existing, replacements).trim().split('\n')
.map(line => JSON.parse(line) as Record<string, unknown>)
expect((output[1]?.data as { id: string }).id).toBe(existingUserId)
expect(((output[3]?.data as { message: { id: string } }).message).id).toBe(existingAssistantId)
})
it('keeps volatile fixture fields while preserving fresh meaningful payloads', () => {
const fresh = [
'{"type":"session","id":"new-child","createdAt":200,"cwd":"/new","parentSession":"new-parent","seedLength":1}',