summaryrefslogtreecommitdiffhomepage
path: root/packages/conversation-store/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-22 00:36:52 +0900
committerAdam Malczewski <[email protected]>2026-06-22 00:36:52 +0900
commitc1bfd62b0c734484efcff09d6cd521acdbab2640 (patch)
treef9194ea3214d5f15be6fed659a230997a1309ab9 /packages/conversation-store/src
parent7ff9f94c41a9870e124a50133cd74b42295ab9ac (diff)
downloaddispatch-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.ts4
-rw-r--r--packages/conversation-store/src/store.ts42
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 });
+ }
+ },
};
}