mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-25 17:54:34 +08:00
fix(ai): cancel session context compression with the request
向会话压缩传递取消信号,防止停止后继续请求并写入摘要。 Forward the request signal to session compression to prevent requests and summary writes after cancellation.
This commit is contained in:
1 parent
a8e8644c7c
commit
02b75f8532
3 files changed
+84
-2
No files matched your search
@@ -0,0 +1,80 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { basename, dirname, join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import test from 'node:test'
|
||||
import { AIChatManager } from '../../../../packages/node-runtime/src/ai/chats'
|
||||
import type { AgentStreamChunk } from '@openchatlab/node-runtime'
|
||||
|
||||
test('cancelling a session agent must not persist a compression summary', async (t) => {
|
||||
let controller: AbortController
|
||||
let compressionCalls = 0
|
||||
let compressionSignal: AbortSignal | undefined
|
||||
await t.mock.module('../../../../packages/node-runtime/src/ai/compression/adapter-factory', {
|
||||
namedExports: {
|
||||
createCompressionLlmAdapter: (options: { onCompressing?: () => void }) => ({
|
||||
contextWindow: 1000,
|
||||
compress: async (_prompt: string, _maxTokens: number, signal?: AbortSignal) => {
|
||||
options.onCompressing?.()
|
||||
compressionCalls++
|
||||
compressionSignal = signal
|
||||
controller.abort()
|
||||
return 'Summary returned after cancellation'
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
const { runServerAgent } = await import('./agent')
|
||||
|
||||
for (const alreadyAborted of [true, false]) {
|
||||
await t.test(alreadyAborted ? 'cancelled before compression' : 'cancelled during compression', async () => {
|
||||
controller = new AbortController()
|
||||
compressionCalls = 0
|
||||
compressionSignal = undefined
|
||||
const dir = mkdtempSync(join(tmpdir(), 'chatlab-session-cancel-'))
|
||||
const nativeBinding = process.env.CHATLAB_TEST_SQLITE_NATIVE_BINDING
|
||||
const manager = new AIChatManager(dir, nativeBinding ? { nativeBinding } : undefined)
|
||||
try {
|
||||
const chat = manager.createAIChat('session-1', 'Cancellation', 'general_cn')
|
||||
for (let index = 0; index < 6; index++) {
|
||||
const message = manager.addMessage(chat.id, index % 2 ? 'assistant' : 'user', 'history '.repeat(200))
|
||||
manager.executeAiSQL(`UPDATE ai_message SET timestamp = ${1700000000 + index} WHERE id = '${message.id}'`)
|
||||
}
|
||||
const activeBefore = manager.getAIChat(chat.id)?.activeMessageId
|
||||
const events: AgentStreamChunk[] = []
|
||||
if (alreadyAborted) controller.abort()
|
||||
|
||||
await runServerAgent({
|
||||
userMessage: 'Continue',
|
||||
aiChatId: chat.id,
|
||||
aiChatManager: manager,
|
||||
llmConfig: { provider: 'openai-compatible', model: 'test-model', apiKey: 'test-placeholder' },
|
||||
abortSignal: controller.signal,
|
||||
onEvent: (event) => events.push(event),
|
||||
})
|
||||
|
||||
assert.equal(manager.getLatestSummary(chat.id), null)
|
||||
assert.equal(manager.getAIChat(chat.id)?.activeMessageId, activeBefore)
|
||||
assert.equal(compressionCalls, alreadyAborted ? 0 : 1)
|
||||
if (!alreadyAborted) assert.equal(compressionSignal, controller.signal)
|
||||
assert.equal(
|
||||
events.some((event) => event.type === 'compression_done'),
|
||||
false
|
||||
)
|
||||
assert.deepEqual(
|
||||
events
|
||||
.filter((event) => event.type === 'status')
|
||||
.map((event) => event.status?.phase)
|
||||
.filter((phase) => phase === 'aborted' || phase === 'completed' || phase === 'error'),
|
||||
['aborted']
|
||||
)
|
||||
assert.equal(events.filter((event) => event.type === 'done').length, 1)
|
||||
} finally {
|
||||
manager.close()
|
||||
assert.equal(dirname(dir), tmpdir())
|
||||
assert.ok(basename(dir).startsWith('chatlab-session-cancel-'))
|
||||
rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 })
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -133,7 +133,8 @@ export async function runServerAgent(options: RunAgentOptions): Promise<void> {
|
||||
systemPrompt,
|
||||
llmAdapter,
|
||||
aiChatManager,
|
||||
aiLogger ?? undefined
|
||||
aiLogger ?? undefined,
|
||||
{ signal: abortSignal }
|
||||
)
|
||||
if (compressionResult.compressed) {
|
||||
onEvent({
|
||||
|
||||
Reference in new issue
Block a user