diff options
| author | Adam Malczewski <[email protected]> | 2026-06-27 20:48:24 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-27 20:48:24 +0900 |
| commit | 04356c8678ae8dd1d7ddca2d0460b514116adc2e (patch) | |
| tree | 6c81894ef02d062570b12f4d3a871e58600dcb9c /packages/conversation-store | |
| parent | 3184b10e614ce6249c83aa111368e98f6689f456 (diff) | |
| parent | b24ed99e89bc657e8c98c7cef8608e0c0b7594da (diff) | |
| download | dispatch-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')
| -rw-r--r-- | packages/conversation-store/src/keys.ts | 8 | ||||
| -rw-r--r-- | packages/conversation-store/src/store.ts | 77 |
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; |
