mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-01 22:48:16 +00:00
207 lines
8.6 KiB
Markdown
207 lines
8.6 KiB
Markdown
## Payload limits
|
||
|
||
Prevent blocking storage writes and runaway persisted size
|
||
|
||
---
|
||
|
||
### Summary
|
||
|
||
Large payloads (base64 images, terminal buffers) are currently persisted inside key-value stores:
|
||
|
||
- web: `localStorage` (sync, blocks the main thread)
|
||
- desktop: Tauri Store-backed async storage files (still expensive when values are huge)
|
||
|
||
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.
|
||
|
||
---
|
||
|
||
### Goals
|
||
|
||
- Stop persisting image `dataUrl` blobs inside web `localStorage`
|
||
- Stop persisting image `dataUrl` blobs inside desktop store `.dat` files
|
||
- Store image payloads out-of-band (blob store) and load lazily when needed (e.g. when restoring a history item)
|
||
- Prevent terminal buffer persistence from exceeding safe size limits
|
||
- Keep persistence behavior predictable across web (sync) and desktop (async)
|
||
- Provide escape hatches via flags and per-key size caps
|
||
|
||
---
|
||
|
||
### Non-goals
|
||
|
||
- Cross-device sync of images or terminal buffers
|
||
- Lossless persistence of full terminal scrollback on web
|
||
- Perfect blob deduplication or a complex reference-counting system on day one
|
||
|
||
---
|
||
|
||
### Current state
|
||
|
||
- `packages/app/src/utils/persist.ts` uses `localStorage` (sync) on web and async storage only on desktop.
|
||
- 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.
|
||
- Prompt history persists under `Persist.global("prompt-history")` (`packages/app/src/components/prompt-input.tsx`) and can include image parts (`dataUrl`).
|
||
- Prompt draft persistence uses `packages/app/src/context/prompt.tsx` and can also include image parts (`dataUrl`).
|
||
- Terminal buffer is serialized in `packages/app/src/components/terminal.tsx` and persisted in `packages/app/src/context/terminal.tsx`.
|
||
|
||
---
|
||
|
||
### Proposed approach
|
||
|
||
#### 1) Add per-key persistence policies (KV store guardrails)
|
||
|
||
In `packages/app/src/utils/persist.ts`, add policy hooks for each persisted key:
|
||
|
||
- `warnBytes` (soft warning threshold)
|
||
- `maxBytes` (hard cap)
|
||
- `transformIn` / `transformOut` for lossy persistence (e.g. strip or refactor fields)
|
||
- `onOversize` strategy: `drop`, `truncate`, or `migrateToBlobRef`
|
||
|
||
This protects both:
|
||
|
||
- web (`localStorage` is sync)
|
||
- desktop (async, but still expensive to store/flush giant values)
|
||
|
||
#### 2) Add a dedicated blob store for large data
|
||
|
||
Introduce a small blob-store abstraction used by the app layer:
|
||
|
||
- web backend: IndexedDB (store `Blob` values keyed by `id`)
|
||
- desktop backend: filesystem directory under the app data directory (store one file per blob)
|
||
|
||
Store _references_ to blobs inside the persisted JSON instead of the blob contents.
|
||
|
||
#### 3) Persist image parts as references (not base64 payloads)
|
||
|
||
Update the prompt image model so the in-memory shape can still use a `dataUrl` for UI, but the persisted representation is reference-based.
|
||
|
||
Suggested approach:
|
||
|
||
- Keep `ImageAttachmentPart` with:
|
||
- required: `id`, `filename`, `mime`
|
||
- optional/ephemeral: `dataUrl?: string`
|
||
- new: `blobID?: string` (or `ref: string`)
|
||
|
||
Persistence rules:
|
||
|
||
- When writing persisted prompt/history state:
|
||
- ensure each image part is stored in blob store (`blobID`)
|
||
- persist only metadata + `blobID` (no `dataUrl`)
|
||
- When reading persisted prompt/history state:
|
||
- do not eagerly load blob payloads
|
||
- hydrate `dataUrl` only when needed:
|
||
- when applying a history entry into the editor
|
||
- before submission (ensure all image parts have usable `dataUrl`)
|
||
- when rendering an attachment preview, if required
|
||
|
||
---
|
||
|
||
### Phased implementation steps
|
||
|
||
1. Add guardrails in `persist.ts`
|
||
|
||
- Implement size estimation in `packages/app/src/utils/persist.ts` using `TextEncoder` byte length on JSON strings.
|
||
- Add a policy registry keyed by persist name (e.g. `"prompt-history"`, `"prompt"`, `"terminal"`).
|
||
- Add a feature flag (e.g. `persist.payloadLimits`) to enable enforcement gradually.
|
||
|
||
2. Add blob-store abstraction + platform hooks
|
||
|
||
- Add a new app-level module (e.g. `packages/app/src/utils/blob.ts`) defining:
|
||
- `put(id, bytes|Blob)`
|
||
- `get(id)`
|
||
- `remove(id)`
|
||
- Extend the `Platform` interface (`packages/app/src/context/platform.tsx`) with optional blob methods, or provide a default web implementation and override on desktop:
|
||
- web: implement via IndexedDB
|
||
- desktop: implement via filesystem files (requires adding a Tauri fs plugin or `invoke` wrappers)
|
||
|
||
3. Update prompt history + prompt draft persistence to use blob refs
|
||
|
||
- Update prompt/history serialization paths to ensure image parts are stored as blob refs:
|
||
- Prompt history: `packages/app/src/components/prompt-input.tsx`
|
||
- Prompt draft: `packages/app/src/context/prompt.tsx`
|
||
- Ensure “apply history prompt” hydrates image blobs only when applying the prompt (not during background load).
|
||
|
||
4. One-time migration for existing persisted base64 images
|
||
|
||
- On read, detect legacy persisted image parts that include `dataUrl`.
|
||
- If a `dataUrl` is found:
|
||
- write it into the blob store (convert dataUrl → bytes)
|
||
- replace persisted payload with `{ blobID, filename, mime, id }` only
|
||
- re-save the reduced version
|
||
- If migration fails (missing permissions, quota, etc.), fall back to:
|
||
- keep the prompt entry but drop the image payload and mark as unavailable
|
||
|
||
5. Fix terminal persistence (bounded snapshot)
|
||
|
||
- In `packages/app/src/context/terminal.tsx`, persist only:
|
||
- last `maxLines` and/or
|
||
- last `maxBytes` of combined text
|
||
- In `packages/app/src/components/terminal.tsx`, keep the full in-memory buffer unchanged.
|
||
|
||
6. Add basic blob lifecycle cleanup
|
||
To avoid “blob directory grows forever”, add one of:
|
||
|
||
- TTL-based cleanup: store `lastAccessed` per blob and delete blobs older than N days
|
||
- Reference scan cleanup: periodically scan prompt-history + prompt drafts, build a set of referenced `blobID`s, and delete unreferenced blobs
|
||
|
||
Start with TTL-based cleanup (simpler, fewer cross-store dependencies), then consider scan-based cleanup if needed.
|
||
|
||
---
|
||
|
||
### Data migration / backward compatibility
|
||
|
||
- KV store data:
|
||
- policies should be tolerant of missing fields (e.g. `dataUrl` missing)
|
||
- Image parts:
|
||
- treat missing `dataUrl` as “not hydrated yet”
|
||
- treat missing `blobID` (legacy) as “not persisted” or “needs migration”
|
||
- Desktop:
|
||
- blob files should be namespaced (e.g. `opencode/blobs/<blobID>`) to avoid collisions
|
||
|
||
---
|
||
|
||
### Risk + mitigations
|
||
|
||
- Risk: blob store is unavailable (IndexedDB disabled, desktop fs permissions).
|
||
- Mitigation: keep base state functional; persist prompts without image payloads and show a clear placeholder.
|
||
- Risk: lazy hydration introduces edge cases when submitting.
|
||
- Mitigation: add a pre-submit “ensure images hydrated” step; if hydration fails, block submission with a clear error or submit without images.
|
||
- Risk: dataUrl→bytes conversion cost during migration.
|
||
- Mitigation: migrate incrementally (only when reading an entry) and/or use `requestIdleCallback` on web.
|
||
- Risk: blob cleanup deletes blobs still needed.
|
||
- Mitigation: TTL default should be conservative; scan-based cleanup should only delete blobs unreferenced by current persisted state.
|
||
|
||
---
|
||
|
||
### Validation plan
|
||
|
||
- Unit-level:
|
||
- size estimation + policy enforcement in `persist.ts`
|
||
- blob store put/get/remove round trips (web + desktop backends)
|
||
- Manual scenarios:
|
||
- attach multiple images, reload, and confirm:
|
||
- KV store files do not balloon
|
||
- images can be restored when selecting history items
|
||
- open terminal with large output and confirm reload restores bounded snapshot quickly
|
||
- confirm prompt draft persistence still works in `packages/app/src/context/prompt.tsx`
|
||
|
||
---
|
||
|
||
### Rollout plan
|
||
|
||
- Phase 1: ship with `persist.payloadLimits` off; log oversize detections in dev.
|
||
- Phase 2: enable image blob refs behind `persist.imageBlobs` (web + desktop).
|
||
- Phase 3: enable terminal truncation and enforce hard caps for known hot keys.
|
||
- Phase 4: enable blob cleanup behind `persist.blobGc` (TTL first).
|
||
- Provide quick kill switches by disabling each flag independently.
|
||
|
||
---
|
||
|
||
### Open questions
|
||
|
||
- What should the canonical persisted image schema be (`blobID` field name, placeholder shape, etc.)?
|
||
- Desktop implementation detail:
|
||
- add `@tauri-apps/plugin-fs` vs custom `invoke()` commands for blob read/write?
|
||
- where should blob files live (appDataDir) and what retention policy is acceptable?
|
||
- Web implementation detail:
|
||
- do we store `Blob` directly in IndexedDB, or store base64 strings?
|
||
- Should prompt-history images be retained indefinitely, or only for the last `MAX_HISTORY` entries?
|