|
| 1 | +## Payload limits |
| 2 | + |
| 3 | +Prevent blocking storage writes and runaway persisted size |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +### Summary |
| 8 | + |
| 9 | +Large payloads (base64 images, terminal buffers) are currently persisted inside key-value stores: |
| 10 | + |
| 11 | +- web: `localStorage` (sync, blocks the main thread) |
| 12 | +- desktop: Tauri Store-backed async storage files (still expensive when values are huge) |
| 13 | + |
| 14 | +We’ll introduce size-aware persistence policies plus a dedicated “blob store” for large/binary data (IndexedDB on web; separate files on desktop). Prompt/history state will persist only lightweight references to blobs and load them on demand. |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +### Goals |
| 19 | + |
| 20 | +- Stop persisting image `dataUrl` blobs inside web `localStorage` |
| 21 | +- Stop persisting image `dataUrl` blobs inside desktop store `.dat` files |
| 22 | +- Store image payloads out-of-band (blob store) and load lazily when needed (e.g. when restoring a history item) |
| 23 | +- Prevent terminal buffer persistence from exceeding safe size limits |
| 24 | +- Keep persistence behavior predictable across web (sync) and desktop (async) |
| 25 | +- Provide escape hatches via flags and per-key size caps |
| 26 | + |
| 27 | +--- |
| 28 | + |
| 29 | +### Non-goals |
| 30 | + |
| 31 | +- Cross-device sync of images or terminal buffers |
| 32 | +- Lossless persistence of full terminal scrollback on web |
| 33 | +- Perfect blob deduplication or a complex reference-counting system on day one |
| 34 | + |
| 35 | +--- |
| 36 | + |
| 37 | +### Current state |
| 38 | + |
| 39 | +- `packages/app/src/utils/persist.ts` uses `localStorage` (sync) on web and async storage only on desktop. |
| 40 | +- Desktop storage is implemented via `@tauri-apps/plugin-store` and writes to named `.dat` files (see `packages/desktop/src/index.tsx`). Large values bloat these files and increase flush costs. |
| 41 | +- Prompt history persists under `Persist.global("prompt-history")` (`packages/app/src/components/prompt-input.tsx`) and can include image parts (`dataUrl`). |
| 42 | +- Prompt draft persistence uses `packages/app/src/context/prompt.tsx` and can also include image parts (`dataUrl`). |
| 43 | +- Terminal buffer is serialized in `packages/app/src/components/terminal.tsx` and persisted in `packages/app/src/context/terminal.tsx`. |
| 44 | + |
| 45 | +--- |
| 46 | + |
| 47 | +### Proposed approach |
| 48 | + |
| 49 | +#### 1) Add per-key persistence policies (KV store guardrails) |
| 50 | + |
| 51 | +In `packages/app/src/utils/persist.ts`, add policy hooks for each persisted key: |
| 52 | + |
| 53 | +- `warnBytes` (soft warning threshold) |
| 54 | +- `maxBytes` (hard cap) |
| 55 | +- `transformIn` / `transformOut` for lossy persistence (e.g. strip or refactor fields) |
| 56 | +- `onOversize` strategy: `drop`, `truncate`, or `migrateToBlobRef` |
| 57 | + |
| 58 | +This protects both: |
| 59 | + |
| 60 | +- web (`localStorage` is sync) |
| 61 | +- desktop (async, but still expensive to store/flush giant values) |
| 62 | + |
| 63 | +#### 2) Add a dedicated blob store for large data |
| 64 | + |
| 65 | +Introduce a small blob-store abstraction used by the app layer: |
| 66 | + |
| 67 | +- web backend: IndexedDB (store `Blob` values keyed by `id`) |
| 68 | +- desktop backend: filesystem directory under the app data directory (store one file per blob) |
| 69 | + |
| 70 | +Store _references_ to blobs inside the persisted JSON instead of the blob contents. |
| 71 | + |
| 72 | +#### 3) Persist image parts as references (not base64 payloads) |
| 73 | + |
| 74 | +Update the prompt image model so the in-memory shape can still use a `dataUrl` for UI, but the persisted representation is reference-based. |
| 75 | + |
| 76 | +Suggested approach: |
| 77 | + |
| 78 | +- Keep `ImageAttachmentPart` with: |
| 79 | + - required: `id`, `filename`, `mime` |
| 80 | + - optional/ephemeral: `dataUrl?: string` |
| 81 | + - new: `blobID?: string` (or `ref: string`) |
| 82 | + |
| 83 | +Persistence rules: |
| 84 | + |
| 85 | +- When writing persisted prompt/history state: |
| 86 | + - ensure each image part is stored in blob store (`blobID`) |
| 87 | + - persist only metadata + `blobID` (no `dataUrl`) |
| 88 | +- When reading persisted prompt/history state: |
| 89 | + - do not eagerly load blob payloads |
| 90 | + - hydrate `dataUrl` only when needed: |
| 91 | + - when applying a history entry into the editor |
| 92 | + - before submission (ensure all image parts have usable `dataUrl`) |
| 93 | + - when rendering an attachment preview, if required |
| 94 | + |
| 95 | +--- |
| 96 | + |
| 97 | +### Phased implementation steps |
| 98 | + |
| 99 | +1. Add guardrails in `persist.ts` |
| 100 | + |
| 101 | +- Implement size estimation in `packages/app/src/utils/persist.ts` using `TextEncoder` byte length on JSON strings. |
| 102 | +- Add a policy registry keyed by persist name (e.g. `"prompt-history"`, `"prompt"`, `"terminal"`). |
| 103 | +- Add a feature flag (e.g. `persist.payloadLimits`) to enable enforcement gradually. |
| 104 | + |
| 105 | +2. Add blob-store abstraction + platform hooks |
| 106 | + |
| 107 | +- Add a new app-level module (e.g. `packages/app/src/utils/blob.ts`) defining: |
| 108 | + - `put(id, bytes|Blob)` |
| 109 | + - `get(id)` |
| 110 | + - `remove(id)` |
| 111 | +- Extend the `Platform` interface (`packages/app/src/context/platform.tsx`) with optional blob methods, or provide a default web implementation and override on desktop: |
| 112 | + - web: implement via IndexedDB |
| 113 | + - desktop: implement via filesystem files (requires adding a Tauri fs plugin or `invoke` wrappers) |
| 114 | + |
| 115 | +3. Update prompt history + prompt draft persistence to use blob refs |
| 116 | + |
| 117 | +- Update prompt/history serialization paths to ensure image parts are stored as blob refs: |
| 118 | + - Prompt history: `packages/app/src/components/prompt-input.tsx` |
| 119 | + - Prompt draft: `packages/app/src/context/prompt.tsx` |
| 120 | +- Ensure “apply history prompt” hydrates image blobs only when applying the prompt (not during background load). |
| 121 | + |
| 122 | +4. One-time migration for existing persisted base64 images |
| 123 | + |
| 124 | +- On read, detect legacy persisted image parts that include `dataUrl`. |
| 125 | +- If a `dataUrl` is found: |
| 126 | + - write it into the blob store (convert dataUrl → bytes) |
| 127 | + - replace persisted payload with `{ blobID, filename, mime, id }` only |
| 128 | + - re-save the reduced version |
| 129 | +- If migration fails (missing permissions, quota, etc.), fall back to: |
| 130 | + - keep the prompt entry but drop the image payload and mark as unavailable |
| 131 | + |
| 132 | +5. Fix terminal persistence (bounded snapshot) |
| 133 | + |
| 134 | +- In `packages/app/src/context/terminal.tsx`, persist only: |
| 135 | + - last `maxLines` and/or |
| 136 | + - last `maxBytes` of combined text |
| 137 | +- In `packages/app/src/components/terminal.tsx`, keep the full in-memory buffer unchanged. |
| 138 | + |
| 139 | +6. Add basic blob lifecycle cleanup |
| 140 | + To avoid “blob directory grows forever”, add one of: |
| 141 | + |
| 142 | +- TTL-based cleanup: store `lastAccessed` per blob and delete blobs older than N days |
| 143 | +- Reference scan cleanup: periodically scan prompt-history + prompt drafts, build a set of referenced `blobID`s, and delete unreferenced blobs |
| 144 | + |
| 145 | +Start with TTL-based cleanup (simpler, fewer cross-store dependencies), then consider scan-based cleanup if needed. |
| 146 | + |
| 147 | +--- |
| 148 | + |
| 149 | +### Data migration / backward compatibility |
| 150 | + |
| 151 | +- KV store data: |
| 152 | + - policies should be tolerant of missing fields (e.g. `dataUrl` missing) |
| 153 | +- Image parts: |
| 154 | + - treat missing `dataUrl` as “not hydrated yet” |
| 155 | + - treat missing `blobID` (legacy) as “not persisted” or “needs migration” |
| 156 | +- Desktop: |
| 157 | + - blob files should be namespaced (e.g. `opencode/blobs/<blobID>`) to avoid collisions |
| 158 | + |
| 159 | +--- |
| 160 | + |
| 161 | +### Risk + mitigations |
| 162 | + |
| 163 | +- Risk: blob store is unavailable (IndexedDB disabled, desktop fs permissions). |
| 164 | + - Mitigation: keep base state functional; persist prompts without image payloads and show a clear placeholder. |
| 165 | +- Risk: lazy hydration introduces edge cases when submitting. |
| 166 | + - Mitigation: add a pre-submit “ensure images hydrated” step; if hydration fails, block submission with a clear error or submit without images. |
| 167 | +- Risk: dataUrl→bytes conversion cost during migration. |
| 168 | + - Mitigation: migrate incrementally (only when reading an entry) and/or use `requestIdleCallback` on web. |
| 169 | +- Risk: blob cleanup deletes blobs still needed. |
| 170 | + - Mitigation: TTL default should be conservative; scan-based cleanup should only delete blobs unreferenced by current persisted state. |
| 171 | + |
| 172 | +--- |
| 173 | + |
| 174 | +### Validation plan |
| 175 | + |
| 176 | +- Unit-level: |
| 177 | + - size estimation + policy enforcement in `persist.ts` |
| 178 | + - blob store put/get/remove round trips (web + desktop backends) |
| 179 | +- Manual scenarios: |
| 180 | + - attach multiple images, reload, and confirm: |
| 181 | + - KV store files do not balloon |
| 182 | + - images can be restored when selecting history items |
| 183 | + - open terminal with large output and confirm reload restores bounded snapshot quickly |
| 184 | + - confirm prompt draft persistence still works in `packages/app/src/context/prompt.tsx` |
| 185 | + |
| 186 | +--- |
| 187 | + |
| 188 | +### Rollout plan |
| 189 | + |
| 190 | +- Phase 1: ship with `persist.payloadLimits` off; log oversize detections in dev. |
| 191 | +- Phase 2: enable image blob refs behind `persist.imageBlobs` (web + desktop). |
| 192 | +- Phase 3: enable terminal truncation and enforce hard caps for known hot keys. |
| 193 | +- Phase 4: enable blob cleanup behind `persist.blobGc` (TTL first). |
| 194 | +- Provide quick kill switches by disabling each flag independently. |
| 195 | + |
| 196 | +--- |
| 197 | + |
| 198 | +### Open questions |
| 199 | + |
| 200 | +- What should the canonical persisted image schema be (`blobID` field name, placeholder shape, etc.)? |
| 201 | +- Desktop implementation detail: |
| 202 | + - add `@tauri-apps/plugin-fs` vs custom `invoke()` commands for blob read/write? |
| 203 | + - where should blob files live (appDataDir) and what retention policy is acceptable? |
| 204 | +- Web implementation detail: |
| 205 | + - do we store `Blob` directly in IndexedDB, or store base64 strings? |
| 206 | +- Should prompt-history images be retained indefinitely, or only for the last `MAX_HISTORY` entries? |
0 commit comments