fix(ai): keep equal-timestamp compression boundaries intact

中文:避免同秒消息跨摘要边界,防止已压缩内容重复回放或后续消息丢失。

English: Keep equal-timestamp messages on one boundary side so compressed content is not replayed and later messages remain visible.
This commit is contained in:
Sanssssssssssssssss authored and digua committed 2026-08-26 21:06:01 +08:00
1 parent 4d6acc97e5
commit 0126b56648
2 files changed
+91

No files matched your search

@@ -26,6 +26,25 @@ function cleanup(dir: string): void {
}
}
function spreadMessageTimestamps(manager: AIChatManager, chatId: string): void {
const messages = manager.getMessages(chatId)
const baseTimestamp = Math.floor(Date.now() / 1000) - messages.length
messages.forEach((message, index) => {
manager.executeAiSQL(`UPDATE ai_message SET timestamp = ${baseTimestamp + index} WHERE id = '${message.id}'`)
})
}
function addMessageAt(
manager: AIChatManager,
chatId: string,
role: 'user' | 'assistant',
content: string,
timestamp: number
): void {
const message = manager.addMessage(chatId, role, content)
manager.executeAiSQL(`UPDATE ai_message SET timestamp = ${timestamp} WHERE id = '${message.id}'`)
}
const CONFIG: CompressionConfig = {
tokenThresholdPercent: 75,
bufferSizePercent: 20,
@@ -74,10 +93,74 @@ function seedToolHeavyChat(manager: AIChatManager, withToolResults: boolean): st
manager.addMessage(chat.id, 'assistant', 'more stats', undefined, undefined, [toolBlock('query_b', result2)])
manager.addMessage(chat.id, 'user', 'thanks')
manager.addMessage(chat.id, 'assistant', 'you are welcome')
spreadMessageTimestamps(manager, chat.id)
return chat.id
}
describe('checkAndCompress tool result token accounting', () => {
it('keeps equal-timestamp messages on the buffer side of the compression boundary', async () => {
const dir = createTempDir()
const manager = createManager(dir)
try {
const baseTimestamp = 2_000_000_000
const chat = manager.createAIChat('session-1', 'Compression boundary', 'general_cn')
for (let index = 0; index < 4; index++) {
addMessageAt(
manager,
chat.id,
index % 2 === 0 ? 'user' : 'assistant',
`old-${index} ${'old '.repeat(150)}`,
baseTimestamp + index
)
}
addMessageAt(manager, chat.id, 'user', `BOUNDARY_USER ${'user '.repeat(180)}`, baseTimestamp + 10)
addMessageAt(manager, chat.id, 'assistant', `BOUNDARY_ASSISTANT ${'assistant '.repeat(60)}`, baseTimestamp + 10)
const adapter: CompressionLlmAdapter = {
contextWindow: 1000,
compress: async (prompt: string) =>
prompt.includes('BOUNDARY_USER') ? 'SUMMARY_CONTAINS_BOUNDARY_USER' : 'SUMMARY_SAFE',
}
const result = await checkAndCompress(chat.id, CONFIG, 'system', adapter, manager)
assert.equal(result.compressed, true)
assert.deepEqual(
manager.getHistoryForAgent(chat.id).map((message) => message.content.split(' ')[0]),
['SUMMARY_SAFE', 'BOUNDARY_USER', 'BOUNDARY_ASSISTANT']
)
const largeChat = manager.createAIChat('session-1', 'Large compression boundary', 'general_cn')
for (let index = 0; index < 4; index++) {
addMessageAt(
manager,
largeChat.id,
index % 2 === 0 ? 'user' : 'assistant',
`old-${index} ${'old '.repeat(180)}`,
baseTimestamp + index
)
}
addMessageAt(manager, largeChat.id, 'assistant', `LATEST_ASSISTANT ${'latest '.repeat(260)}`, baseTimestamp + 10)
const largeResult = await checkAndCompress(
largeChat.id,
CONFIG,
'system',
createAdapter({ prompt: null }),
manager
)
addMessageAt(manager, largeChat.id, 'user', 'NEXT_USER', baseTimestamp + 10)
assert.equal(largeResult.compressed, true)
assert.deepEqual(
manager.getHistoryForAgent(largeChat.id).map((message) => message.content.split(' ')[0]),
['COMPRESSED', 'LATEST_ASSISTANT', 'NEXT_USER']
)
} finally {
manager.close()
cleanup(dir)
}
})
it('counts replayed tool results toward the compression threshold', async () => {
const dir = createTempDir()
const manager = createManager(dir)
@@ -141,6 +224,7 @@ describe('checkAndCompress tool result token accounting', () => {
])
manager.addMessage(chat.id, 'user', 'thanks')
manager.addMessage(chat.id, 'assistant', 'you are welcome')
spreadMessageTimestamps(manager, chat.id)
const captured: { prompt: string | null } = { prompt: null }
const result = await checkAndCompress(chat.id, CONFIG, 'system', createAdapter(captured), manager)
@@ -177,6 +261,7 @@ describe('checkAndCompress tool result token accounting', () => {
])
manager.addMessage(chat.id, 'user', 'continue')
manager.addMessage(chat.id, 'assistant', 'done')
spreadMessageTimestamps(manager, chat.id)
const captured: { prompt: string | null } = { prompt: null }
const result = await checkAndCompress(chat.id, CONFIG, 'system', createAdapter(captured), manager)
@@ -274,6 +274,12 @@ function splitMessagesForCompression<T extends CompressibleMessage>(
}
}
// Summary boundaries have second precision, so equal-timestamp messages must stay together.
if (splitIndex > 0) {
const boundaryTimestamp = messages[Math.min(splitIndex, messages.length - 1)]!.timestamp
while (splitIndex > 0 && messages[splitIndex - 1]!.timestamp === boundaryTimestamp) splitIndex--
}
return {
bufferMessages: messages.slice(splitIndex),
messagesToCompress: messages.slice(0, splitIndex),