fix: restore manual context compaction for direct models
Local direct-model text conversations could not use manual context compaction, and disabling automatic compression prevented saved summaries from being reused. Restore the composer action regardless of the automatic setting or threshold, use the selected summary connection, and persist summaries for subsequent requests while retaining recent turns and full conversation history. Image connections, remote channel conversations, and SSH projects remain outside the local direct-model compaction workflow. Add focused regressions, a local Electron end-to-end test, and an opt-in two-request live probe. Validation: staged snapshot passed typecheck, changed-file ESLint, 13 focused tests, and the Electron end-to-end test with two local HTTP fixture requests. No additional real-model calls were made during commit verification. Release note: 修复本地直连文本模型无法手动压缩上下文的问题。现在关闭自动压缩或尚未达到触发阈值时,也可点击“压缩上下文”;摘要会随会话保存并用于后续对话,同时保留最近轮次和完整历史。
This commit is contained in:
@@ -4523,6 +4523,75 @@ describe('registerIpcHandlers local conversation persistence', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it.each(['current', 'profile', 'image', 'missing'] as const)(
|
||||
'routes direct model manual compaction using %s selection', async (source) => {
|
||||
const profile = {
|
||||
id: crypto.randomUUID(), name: 'Selected', baseUrl: 'https://models.example',
|
||||
modelName: 'selected-text', protocol: 'anthropic-messages', authentication: 'none'
|
||||
}
|
||||
const fallback = { ...profile, id: crypto.randomUUID(), modelName: 'default-text' }
|
||||
const summary = { ...profile, id: crypto.randomUUID(), modelName: 'summary-text' }
|
||||
const selection = { provider: 'model' as const, profileId: source === 'missing' ? crypto.randomUUID() : profile.id }
|
||||
const settings = {
|
||||
provider: 'model', workspacePath: 'C:\\Workspace', defaultModelProfileId: fallback.id,
|
||||
modelProfiles: [fallback, { ...profile, protocol: source === 'image' ? 'openai-images-generations' : profile.protocol }, summary],
|
||||
continueModelProfile: fallback,
|
||||
contextCompression: { enabled: false, modelSource: source === 'profile' ? { kind: 'profile', profileId: summary.id } : { kind: 'current' } }
|
||||
}
|
||||
const messages = [
|
||||
{ id: crypto.randomUUID(), role: 'user', content: 'earlier question', state: 'complete' },
|
||||
{ id: crypto.randomUUID(), role: 'assistant', content: 'earlier answer', state: 'complete' }
|
||||
]
|
||||
const conversationId = crypto.randomUUID()
|
||||
const database = {
|
||||
queueDueSchedules: vi.fn(() => []), listConversationQueueItems: vi.fn(() => []),
|
||||
listPendingConversationQueueIds: vi.fn(() => []),
|
||||
getConversation: vi.fn(() => ({ id: conversationId, runtimeSelection: selection, messages })),
|
||||
createTask: vi.fn(), updateTaskStatus: vi.fn()
|
||||
}
|
||||
const compactor = {
|
||||
compactConversation: vi.fn(async () => ({ result: {
|
||||
provider: 'model', strategy: 'goodbuddy-summary', compacted: true, detail: 'Compacted'
|
||||
} })),
|
||||
dispose: vi.fn(async () => {})
|
||||
}
|
||||
runtimeFactoryMocks.createModelProfileRuntime.mockReturnValue(compactor)
|
||||
const webContents = { on: vi.fn(), removeListener: vi.fn(),
|
||||
mainFrame: { url: 'file:///goodbuddy/index.html' },
|
||||
getURL: vi.fn(() => 'file:///goodbuddy/index.html'), send: vi.fn()
|
||||
}
|
||||
const window = { webContents, isDestroyed: vi.fn(() => false), on: vi.fn(), removeListener: vi.fn() }
|
||||
const dispose = registerIpcHandlers(
|
||||
window as never, {} as never, 'CommandOrControl+Shift+Space',
|
||||
{ getResolvedSettings: vi.fn(async () => settings) } as never,
|
||||
{} as never, { clear: vi.fn() } as never, {} as never, database as never,
|
||||
{ clear: vi.fn() } as never, {} as never, vi.fn(async () => {})
|
||||
)
|
||||
try {
|
||||
const result = electronMocks.handlers.get(ipcChannels.agentCompactConversation)?.(
|
||||
{ sender: webContents, senderFrame: webContents.mainFrame },
|
||||
{ requestId: crypto.randomUUID(), conversationId, runtimeSelection: selection,
|
||||
history: messages.map(({ role, content }) => ({ role, content })),
|
||||
historyMessageIds: messages.map(({ id }) => id) }
|
||||
)
|
||||
if (source === 'image' || source === 'missing') {
|
||||
await expect(result).rejects.toThrow()
|
||||
expect(runtimeFactoryMocks.createModelProfileRuntime).not.toHaveBeenCalled()
|
||||
} else {
|
||||
await expect(result).resolves.toMatchObject({ provider: 'model', compacted: true })
|
||||
expect(runtimeFactoryMocks.createModelProfileRuntime).toHaveBeenCalledWith(
|
||||
'C:\\Workspace', expect.objectContaining({ defaultModelProfileId: profile.id }),
|
||||
source === 'profile' ? summary : profile
|
||||
)
|
||||
expect(compactor.dispose).toHaveBeenCalledOnce()
|
||||
expect(database.updateTaskStatus).toHaveBeenCalledWith(expect.any(String), 'completed')
|
||||
}
|
||||
} finally {
|
||||
await dispose()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('pins and unpins without attachment reconciliation or import checks', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'goodbuddy-pin-ipc-'))
|
||||
const database = new AssistantDatabase(join(root, 'assistant.sqlite'))
|
||||
|
||||
Reference in New Issue
Block a user