diff options
| author | Adam Malczewski <[email protected]> | 2026-06-22 00:36:52 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-22 00:36:52 +0900 |
| commit | c1bfd62b0c734484efcff09d6cd521acdbab2640 (patch) | |
| tree | f9194ea3214d5f15be6fed659a230997a1309ab9 /packages/conversation-store/src | |
| parent | 7ff9f94c41a9870e124a50133cd74b42295ab9ac (diff) | |
| download | dispatch-c1bfd62b0c734484efcff09d6cd521acdbab2640.tar.gz dispatch-c1bfd62b0c734484efcff09d6cd521acdbab2640.zip | |
feat: conversation compacting (manual + automatic)
Implement roadmap item 10: conversation compaction to reclaim context
window without losing the thread.
Wire (0.11.0):
- Add CompactionResult type
- Add ConversationCompactedMessage WS event
Transport-contract (0.15.0):
- Add CompactResponse, CompactThresholdResponse, SetCompactThresholdRequest
- Add ConversationCompactedMessage to WsServerMessage union
- Re-export CompactionResult
Conversation-store:
- replaceHistory: delete all chunks, reset seq, append new messages
- getCompactThreshold / setCompactThreshold (per-conversation setting)
- compactThresholdKey added to keys.ts
Session-orchestrator:
- CompactionService interface + compactionHandle
- conversationCompacted hook descriptor
- createCompactionService: load history, split old/recent, call provider
to summarize, replaceHistory with [system: summary] + recent N
- Auto-trigger: resolveCompaction lazy dep, fires after turn settles
(checks threshold, non-blocking)
- Hook declared in manifest contributes.hooks + services
Transport-http:
- POST /conversations/:id/compact (manual trigger)
- GET /conversations/:id/compact-threshold (read setting)
- PUT /conversations/:id/compact-threshold (set setting)
Transport-ws:
- Subscribe to conversationCompacted hook
- Broadcast conversation.compacted WS message
CLI:
- dispatch compact <conversationId> command
FE handoff: frontend-compaction-handoff.md
Diffstat (limited to 'packages/conversation-store/src')
| -rw-r--r-- | packages/conversation-store/src/keys.ts | 4 | ||||
| -rw-r--r-- | packages/conversation-store/src/store.ts | 42 |
2 files changed, 46 insertions, 0 deletions
diff --git a/packages/conversation-store/src/keys.ts b/packages/conversation-store/src/keys.ts index 9b7c2cc..a609f82 100644 --- a/packages/conversation-store/src/keys.ts +++ b/packages/conversation-store/src/keys.ts @@ -54,6 +54,10 @@ export function reasoningEffortKey(conversationId: string): string { return `conv:${conversationId}:reasoning-effort`; } +export function compactThresholdKey(conversationId: string): string { + return `conv:${conversationId}:compact-threshold`; +} + export function metaKey(conversationId: string): string { return `conv:${conversationId}:meta`; } diff --git a/packages/conversation-store/src/store.ts b/packages/conversation-store/src/store.ts index 8d78df8..2f9f475 100644 --- a/packages/conversation-store/src/store.ts +++ b/packages/conversation-store/src/store.ts @@ -15,6 +15,7 @@ import { CONVERSATION_INDEX_KEY, chunkKey, chunkPrefix, + compactThresholdKey, cwdKey, metaKey, metricsKey, @@ -86,6 +87,20 @@ export interface ConversationStore { conversationId: string, status: ConversationStatus, ) => Promise<void>; + /** + * Replace the entire conversation history with the given messages. Deletes + * all existing chunks, resets the seq counter, and appends the new messages. + * Used by compaction to replace old history with a summary + recent messages. + * Metadata (createdAt, title, status) is preserved. + */ + readonly replaceHistory: ( + conversationId: string, + messages: readonly ChatMessage[], + ) => Promise<void>; + /** Get the compact threshold (token count, 0 = manual only), or null if unset. */ + readonly getCompactThreshold: (conversationId: string) => Promise<number | null>; + /** Set the compact threshold (token count, 0 = manual only). */ + readonly setCompactThreshold: (conversationId: string, threshold: number) => Promise<void>; } export const conversationStoreHandle = defineService<ConversationStore>("conversation-store/store"); @@ -529,5 +544,32 @@ export function createConversationStore( }; await storage.set(metaKey(conversationId), JSON.stringify(row)); }, + + async replaceHistory(conversationId, messages) { + // Delete all existing chunks. + const keys = await storage.keys(chunkPrefix(conversationId)); + for (const k of keys) { + await storage.delete(k); + } + // Reset the seq counter so the new messages start from seq 1. + await storage.set(seqKey(conversationId), "0"); + // Append the new messages (re-uses the append logic for seq + // numbering + metadata upsert). + await this.append(conversationId, messages); + }, + + async getCompactThreshold(conversationId) { + const raw = await storage.get(compactThresholdKey(conversationId)); + if (raw === null) return null; + const n = Number.parseInt(raw, 10); + return Number.isNaN(n) ? null : n; + }, + + async setCompactThreshold(conversationId, threshold) { + await storage.set(compactThresholdKey(conversationId), String(threshold)); + if (logger !== undefined) { + logger.debug("compact-threshold set", { conversationId, threshold }); + } + }, }; } |
