mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
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:
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),
|
||||
|
||||
Reference in new issue
Block a user