summaryrefslogtreecommitdiffhomepage
path: root/packages/conversation-store/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-27 20:48:24 +0900
committerAdam Malczewski <[email protected]>2026-06-27 20:48:24 +0900
commit04356c8678ae8dd1d7ddca2d0460b514116adc2e (patch)
tree6c81894ef02d062570b12f4d3a871e58600dcb9c /packages/conversation-store/src
parent3184b10e614ce6249c83aa111368e98f6689f456 (diff)
parentb24ed99e89bc657e8c98c7cef8608e0c0b7594da (diff)
downloaddispatch-04356c8678ae8dd1d7ddca2d0460b514116adc2e.tar.gz
dispatch-04356c8678ae8dd1d7ddca2d0460b514116adc2e.zip
Merge branch 'feature/vision-handoff' into dev
# Conflicts: # packages/session-orchestrator/src/extension.ts # packages/session-orchestrator/src/orchestrator.ts
Diffstat (limited to 'packages/conversation-store/src')
-rw-r--r--packages/conversation-store/src/keys.ts8
-rw-r--r--packages/conversation-store/src/store.ts77
2 files changed, 85 insertions, 0 deletions
diff --git a/packages/conversation-store/src/keys.ts b/packages/conversation-store/src/keys.ts
index b2c635d..6ec2bc5 100644
--- a/packages/conversation-store/src/keys.ts
+++ b/packages/conversation-store/src/keys.ts
@@ -66,6 +66,14 @@ export function compactThresholdKey(conversationId: string): string {
return `conv:${conversationId}:compact-percent`;
}
+/** Per-conversation image transcription cache (JSON map of imageUrl → transcription). */
+export function imageTranscriptionsKey(conversationId: string): string {
+ return `conv:${conversationId}:image-transcriptions`;
+}
+
+/** Global vision settings (image compaction limit + compaction model). */
+export const VISION_SETTINGS_KEY = "vision-settings";
+
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 f90e809..69334e6 100644
--- a/packages/conversation-store/src/store.ts
+++ b/packages/conversation-store/src/store.ts
@@ -20,6 +20,7 @@ import {
compactThresholdKey,
computerKey,
cwdKey,
+ imageTranscriptionsKey,
metaKey,
metricsKey,
metricsPrefix,
@@ -28,6 +29,7 @@ import {
parseSeq,
reasoningEffortKey,
seqKey,
+ VISION_SETTINGS_KEY,
workspaceKey,
} from "./keys.js";
import { reconcileWithReport } from "./reconcile.js";
@@ -141,6 +143,35 @@ export interface ConversationStore {
/** Set the compact percent (0-100, 0 = manual only). */
readonly setCompactPercent: (conversationId: string, percent: number) => Promise<void>;
/**
+ * Get the per-conversation image transcription cache: a map of image URL →
+ * transcription text. Used by the vision handoff to avoid re-transcribing
+ * old images that were compacted to text on a previous turn. Returns an
+ * empty map when none are cached.
+ */
+ readonly getImageTranscriptions: (conversationId: string) => Promise<ReadonlyMap<string, string>>;
+ /**
+ * Upsert a single image transcription into the per-conversation cache.
+ * Merges with any existing transcriptions (does NOT replace the whole map).
+ */
+ readonly setImageTranscription: (
+ conversationId: string,
+ imageUrl: string,
+ transcription: string,
+ ) => Promise<void>;
+ /**
+ * Get the global vision settings (image compaction limit + compaction model).
+ * The limit defaults to 10 when never set; the compaction model defaults to
+ * null (auto-select). Shared across ALL conversations and vision models.
+ */
+ readonly getVisionSettings: () => Promise<{
+ readonly imageLimit: number;
+ readonly compactionModel: string | null;
+ }>;
+ /** Set the global vision image compaction limit (0 = disabled). */
+ readonly setVisionImageLimit: (limit: number) => Promise<void>;
+ /** Set the global vision compaction model (null = auto-select). */
+ readonly setVisionCompactionModel: (model: string | null) => Promise<void>;
+ /**
* Set the `compactedFrom` field on a conversation's metadata, pointing to
* the archive conversation that holds the pre-compaction history.
*/
@@ -1004,6 +1035,52 @@ export function createConversationStore(
}
},
+ async getImageTranscriptions(conversationId) {
+ const raw = await storage.get(imageTranscriptionsKey(conversationId));
+ if (raw === null) return new Map();
+ try {
+ const obj = JSON.parse(raw) as Record<string, string>;
+ return new Map(Object.entries(obj));
+ } catch {
+ return new Map();
+ }
+ },
+
+ async setImageTranscription(conversationId, imageUrl, transcription) {
+ const existing = await this.getImageTranscriptions(conversationId);
+ const merged = new Map(existing);
+ merged.set(imageUrl, transcription);
+ const obj: Record<string, string> = {};
+ for (const [k, v] of merged) obj[k] = v;
+ await storage.set(imageTranscriptionsKey(conversationId), JSON.stringify(obj));
+ },
+
+ async getVisionSettings() {
+ const raw = await storage.get(VISION_SETTINGS_KEY);
+ if (raw === null) return { imageLimit: 10, compactionModel: null };
+ try {
+ const obj = JSON.parse(raw) as { imageLimit?: number; compactionModel?: string | null };
+ return {
+ imageLimit: typeof obj.imageLimit === "number" ? obj.imageLimit : 10,
+ compactionModel: obj.compactionModel ?? null,
+ };
+ } catch {
+ return { imageLimit: 10, compactionModel: null };
+ }
+ },
+
+ async setVisionImageLimit(limit) {
+ const current = await this.getVisionSettings();
+ const obj = { imageLimit: limit, compactionModel: current.compactionModel };
+ await storage.set(VISION_SETTINGS_KEY, JSON.stringify(obj));
+ },
+
+ async setVisionCompactionModel(model) {
+ const current = await this.getVisionSettings();
+ const obj = { imageLimit: current.imageLimit, compactionModel: model };
+ await storage.set(VISION_SETTINGS_KEY, JSON.stringify(obj));
+ },
+
async setCompactedFrom(conversationId, newConversationId) {
const raw = await storage.get(metaKey(conversationId));
const existing = raw !== null ? parseMetaRow(raw) : null;