test(agent-loop): reproduce series-start prompt rewrite on unchanged text

A compaction replacement after an appended in-history prompt starts a new request series, and the projection folds the prompt back into node 0 although the effective text is unchanged. The request moves the system segment to wire message 0, so the provider prefix cache misses from the first token (#6973).

The regression fails before the fix: the third request carries one system text ("prompt two") instead of keeping the appended node behind the cached prefix.
This commit is contained in:
mektpoy committed 2026-10-22 21:19:24 +08:00
1 parent d10f417f13
commit 71d5454904
1 file changed
+25
@@ -240,6 +240,31 @@ describe('prepared-route prompt admission', () => {
if (reason === 'tools') expect(header?.data.startsSeries).toBe(true)
})
it('keeps the cached prefix at a compaction series start when the prompt text is unchanged', async () => {
const h = await harness()
await send(h.agent, 'first')
h.setPrompt('prompt two')
await send(h.agent, 'second')
const previous = h.capable.requests[1]!
// A prune/compaction replaces the last admitted user message: the surface changes, so the
// next step starts a new request series while the appended prompt node still holds the
// rendered text.
const seq = h.agent.session.surface.nodes.findLast(node => h.agent.session.eventAt(node)?.type === 'user/message')!
h.agent.session.append('user/message', createUserMessage({
content: [{ type: 'text', text: 'compacted history' }], source: { kind: 'test-compaction' },
}), { surfaceOp: { op: 'replace', startSeq: seq, endSeq: seq }, sourceEventSeqs: [seq] })
await send(h.agent, 'third')
// Folding the prompt back into node 0 would move the system segment to wire message 0 and
// miss the provider prefix cache from the first token, although no prompt text changed.
const request = h.capable.requests[2]!
expect(systemTexts(request)).toEqual([
[{ type: 'text', text: 'prompt one' }], [{ type: 'text', text: 'prompt two' }],
])
expect(request.messages.slice(0, previous.messages.length - 1)).toEqual(previous.messages.slice(0, -1))
})
it.each([false, true])('reconciles compaction retries without replaying admission, older tail survives=%s', async (retainOlder) => {
const overflow = () => { throw new LlmError('context window exceeded', 'CONTEXT_LENGTH') }
const adapter = new MockAdapter([textResponse('one'), textResponse('two'), overflow, overflow, textResponse('done')])