mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-01 22:48:16 +00:00
docs: perf plans
This commit is contained in:
206
specs/01-persist-payload-limits.md
Normal file
206
specs/01-persist-payload-limits.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
## 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?
|
||||||
141
specs/02-cache-eviction.md
Normal file
141
specs/02-cache-eviction.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
## Cache eviction
|
||||||
|
|
||||||
|
Add explicit bounds for long-lived in-memory state
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Several in-memory caches grow without limits during long sessions. We’ll introduce explicit eviction (LRU + TTL + size caps) for sessions/messages/file contents and global per-directory sync stores.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
- Prevent unbounded memory growth from caches that survive navigation
|
||||||
|
- Add consistent eviction primitives shared across contexts
|
||||||
|
- Keep UI responsive under heavy usage (many sessions, large files)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- Perfect cache hit rates or prefetch strategies
|
||||||
|
- Changing server APIs or adding background jobs
|
||||||
|
- Persisting caches for offline use
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Current state
|
||||||
|
|
||||||
|
- Global sync uses per-directory child stores without eviction in `packages/app/src/context/global-sync.tsx`.
|
||||||
|
- File contents cached in `packages/app/src/context/file.tsx` with no cap.
|
||||||
|
- Session-heavy pages include `packages/app/src/pages/session.tsx` and `packages/app/src/pages/layout.tsx`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Proposed approach
|
||||||
|
|
||||||
|
- Introduce a shared cache utility that supports:
|
||||||
|
- `maxEntries`, `maxBytes` (approx), and `ttlMs`
|
||||||
|
- LRU ordering with explicit `touch(key)` on access
|
||||||
|
- deterministic `evict()` and `clear()` APIs
|
||||||
|
- Apply the utility to:
|
||||||
|
- global-sync per-directory child stores (cap number of directories kept “hot”)
|
||||||
|
- file contents cache (cap by entries + bytes, with TTL)
|
||||||
|
- session/message caches (cap by session count, and optionally message count)
|
||||||
|
- Add feature flags per cache domain to allow partial rollout (e.g. `cache.eviction.files`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phased implementation steps
|
||||||
|
|
||||||
|
1. Add a generic cache helper
|
||||||
|
|
||||||
|
- Create `packages/app/src/utils/cache.ts` with a small, dependency-free LRU+TTL.
|
||||||
|
- Keep it framework-agnostic and usable from Solid contexts.
|
||||||
|
|
||||||
|
Sketch:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type CacheOpts = {
|
||||||
|
maxEntries: number
|
||||||
|
ttlMs?: number
|
||||||
|
maxBytes?: number
|
||||||
|
sizeOf?: (value: unknown) => number
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLruCache<T>(opts: CacheOpts) {
|
||||||
|
// get, set, delete, clear, evictExpired, stats
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Apply eviction to file contents
|
||||||
|
|
||||||
|
- In `packages/app/src/context/file.tsx`:
|
||||||
|
- wrap the existing file-content map in the LRU helper
|
||||||
|
- approximate size via `TextEncoder` length of content strings
|
||||||
|
- evict on `set` and periodically via `requestIdleCallback` when available
|
||||||
|
- Add a small TTL (e.g. 10–30 minutes) to discard stale contents.
|
||||||
|
|
||||||
|
3. Apply eviction to global-sync child stores
|
||||||
|
|
||||||
|
- In `packages/app/src/context/global-sync.tsx`:
|
||||||
|
- track child stores by directory key in an LRU with `maxEntries`
|
||||||
|
- call a `dispose()` hook on eviction to release subscriptions and listeners
|
||||||
|
- Ensure “currently active directory” is always `touch()`’d to avoid surprise evictions.
|
||||||
|
|
||||||
|
4. Apply eviction to session/message caches
|
||||||
|
|
||||||
|
- Identify the session/message caching touchpoints used by `packages/app/src/pages/session.tsx`.
|
||||||
|
- Add caps that reflect UI needs (e.g. last 10–20 sessions kept, last N messages per session if cached).
|
||||||
|
|
||||||
|
5. Add developer tooling
|
||||||
|
|
||||||
|
- Add a debug-only stats readout (console or dev panel) for cache sizes and eviction counts.
|
||||||
|
- Add a one-click “clear caches” action for troubleshooting.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Data migration / backward compatibility
|
||||||
|
|
||||||
|
- No persisted schema changes are required since this targets in-memory caches.
|
||||||
|
- If any cache is currently mirrored into persistence, keep keys stable and only change in-memory retention.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Risk + mitigations
|
||||||
|
|
||||||
|
- Risk: evicting content still needed causes extra refetches and flicker.
|
||||||
|
- Mitigation: always pin “active” entities and evict least-recently-used first.
|
||||||
|
- Risk: disposing global-sync child stores could leak listeners if not cleaned up correctly.
|
||||||
|
- Mitigation: require an explicit `dispose()` contract and add dev assertions for listener counts.
|
||||||
|
- Risk: approximate byte sizing is imprecise.
|
||||||
|
- Mitigation: combine entry caps with byte caps and keep thresholds conservative.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Validation plan
|
||||||
|
|
||||||
|
- Add tests for `createLruCache` covering TTL expiry, LRU ordering, and eviction triggers.
|
||||||
|
- Manual scenarios:
|
||||||
|
- open many files and confirm memory stabilizes and UI remains responsive
|
||||||
|
- switch across many directories and confirm global-sync does not continuously grow
|
||||||
|
- long session navigation loop and confirm caches plateau
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Rollout plan
|
||||||
|
|
||||||
|
- Land cache utility first with flags default off.
|
||||||
|
- Enable file cache eviction first (lowest behavioral risk).
|
||||||
|
- Enable global-sync eviction next with conservative caps and strong logging in dev.
|
||||||
|
- Enable session/message eviction last after observing real usage patterns.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Open questions
|
||||||
|
|
||||||
|
- What are the current session/message cache structures and their ownership boundaries?
|
||||||
|
- Which child stores in `global-sync.tsx` have resources that must be disposed explicitly?
|
||||||
|
- What caps are acceptable for typical workflows (files open, directories visited, sessions viewed)?
|
||||||
145
specs/03-request-throttling.md
Normal file
145
specs/03-request-throttling.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
## Request throttling
|
||||||
|
|
||||||
|
Debounce and cancel high-frequency server calls
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Some user interactions trigger bursts of server requests that can overlap and return out of order. We’ll debounce frequent triggers and cancel in-flight requests (or ignore stale results) for file search and LSP refresh.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
- Reduce redundant calls from file search and LSP refresh
|
||||||
|
- Prevent stale responses from overwriting newer UI state
|
||||||
|
- Preserve responsive typing and scrolling during high activity
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- Changing server-side behavior or adding new endpoints
|
||||||
|
- Implementing global request queues for all SDK calls
|
||||||
|
- Persisting search results across reloads
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Current state
|
||||||
|
|
||||||
|
- File search calls `sdk.client.find.files` via `files.searchFilesAndDirectories`.
|
||||||
|
- LSP refresh is triggered frequently (exact call sites vary, but the refresh behavior is high-frequency).
|
||||||
|
- Large UI modules involved include `packages/app/src/pages/layout.tsx` and `packages/app/src/components/prompt-input.tsx`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Proposed approach
|
||||||
|
|
||||||
|
- Add a small request coordinator utility:
|
||||||
|
- debounced triggering (leading/trailing configurable)
|
||||||
|
- cancellation via `AbortController` when supported
|
||||||
|
- stale-result protection via monotonic request ids when abort is not supported
|
||||||
|
- Integrate coordinator into:
|
||||||
|
- `files.searchFilesAndDirectories` (wrap `sdk.client.find.files`)
|
||||||
|
- LSP refresh call path (wrap refresh invocation and ensure only latest applies)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phased implementation steps
|
||||||
|
|
||||||
|
1. Add a debounced + cancellable helper
|
||||||
|
|
||||||
|
- Create `packages/app/src/utils/requests.ts` with:
|
||||||
|
- `createDebouncedAsync(fn, delayMs)`
|
||||||
|
- `createLatestOnlyAsync(fn)` that drops stale responses
|
||||||
|
- Prefer explicit, readable primitives over a single complex abstraction.
|
||||||
|
|
||||||
|
Sketch:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
function createLatestOnlyAsync<TArgs extends unknown[], TResult>(
|
||||||
|
fn: (args: { input: TArgs; signal?: AbortSignal }) => Promise<TResult>,
|
||||||
|
) {
|
||||||
|
let id = 0
|
||||||
|
let controller: AbortController | undefined
|
||||||
|
|
||||||
|
return async (...input: TArgs) => {
|
||||||
|
id += 1
|
||||||
|
const current = id
|
||||||
|
controller?.abort()
|
||||||
|
controller = new AbortController()
|
||||||
|
|
||||||
|
const result = await fn({ input, signal: controller.signal })
|
||||||
|
if (current !== id) return
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Apply to file search
|
||||||
|
|
||||||
|
- Update `files.searchFilesAndDirectories` to:
|
||||||
|
- debounce input changes (e.g. 150–300 ms)
|
||||||
|
- abort prior request when a new query begins
|
||||||
|
- ignore results if they are stale
|
||||||
|
- Ensure “empty query” is handled locally without calling the server.
|
||||||
|
|
||||||
|
3. Apply to LSP refresh
|
||||||
|
|
||||||
|
- Identify the refresh trigger points used during typing and file switching.
|
||||||
|
- Add:
|
||||||
|
- debounce for rapid triggers (e.g. 250–500 ms)
|
||||||
|
- cancellation for in-flight refresh if supported
|
||||||
|
- last-write-wins behavior for applying diagnostics/results
|
||||||
|
|
||||||
|
4. Add feature flags and metrics
|
||||||
|
|
||||||
|
- Add flags:
|
||||||
|
- `requests.debounce.fileSearch`
|
||||||
|
- `requests.latestOnly.lspRefresh`
|
||||||
|
- Add simple dev-only counters for “requests started / aborted / applied”.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Data migration / backward compatibility
|
||||||
|
|
||||||
|
- No persisted data changes.
|
||||||
|
- Behavior is compatible as long as UI state updates only when the “latest” request resolves.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Risk + mitigations
|
||||||
|
|
||||||
|
- Risk: aggressive debounce makes UI feel laggy.
|
||||||
|
- Mitigation: keep delays small and tune separately for search vs refresh.
|
||||||
|
- Risk: aborting requests may surface as errors in logs.
|
||||||
|
- Mitigation: treat `AbortError` as expected and do not log it as a failure.
|
||||||
|
- Risk: SDK method may not accept `AbortSignal`.
|
||||||
|
- Mitigation: use request-id stale protection even without true cancellation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Validation plan
|
||||||
|
|
||||||
|
- Manual scenarios:
|
||||||
|
- type quickly in file search and confirm requests collapse and results stay correct
|
||||||
|
- trigger LSP refresh repeatedly and confirm diagnostics do not flicker backward
|
||||||
|
- Add a small unit test for latest-only behavior (stale results are ignored).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Rollout plan
|
||||||
|
|
||||||
|
- Ship helpers behind flags default off.
|
||||||
|
- Enable file search debounce first (high impact, easy to validate).
|
||||||
|
- Enable LSP latest-only next, then add cancellation if SDK supports signals.
|
||||||
|
- Keep a quick rollback by disabling the flags.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Open questions
|
||||||
|
|
||||||
|
- Does `sdk.client.find.files` accept an abort signal today, or do we need stale-result protection only?
|
||||||
|
- Where is LSP refresh initiated, and does it have a single chokepoint we can wrap?
|
||||||
|
- What debounce values feel best for common repos and slower machines?
|
||||||
125
specs/04-scroll-spy-optimization.md
Normal file
125
specs/04-scroll-spy-optimization.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
## Spy acceleration
|
||||||
|
|
||||||
|
Replace O(N) DOM scans in session view
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
The session scroll-spy currently scans the DOM with `querySelectorAll` and walks message nodes, which becomes expensive as message count grows. We’ll replace the scan with an observer-based or indexed approach that scales smoothly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
- Remove repeated full DOM scans during scroll in the session view
|
||||||
|
- Keep “current message” tracking accurate during streaming and layout shifts
|
||||||
|
- Provide a safe fallback path for older browsers and edge cases
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- Visual redesign of the session page
|
||||||
|
- Changing message rendering structure or IDs
|
||||||
|
- Perfect accuracy during extreme layout thrash
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Current state
|
||||||
|
|
||||||
|
- `packages/app/src/pages/session.tsx` uses `querySelectorAll('[data-message-id]')` for scroll-spy.
|
||||||
|
- The page is large and handles many responsibilities, increasing the chance of perf regressions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Proposed approach
|
||||||
|
|
||||||
|
Implement a two-tier scroll-spy:
|
||||||
|
|
||||||
|
- Primary: `IntersectionObserver` to track which message elements are visible, updated incrementally.
|
||||||
|
- Secondary: binary search over precomputed offsets when observer is unavailable or insufficient.
|
||||||
|
- Use `ResizeObserver` (and a lightweight “dirty” flag) to refresh offsets only when layout changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phased implementation steps
|
||||||
|
|
||||||
|
1. Extract a dedicated scroll-spy module
|
||||||
|
|
||||||
|
- Create `packages/app/src/pages/session/scroll-spy.ts` (or similar) that exposes:
|
||||||
|
- `register(el, id)` and `unregister(id)`
|
||||||
|
- `getActiveId()` signal/store
|
||||||
|
- Keep DOM operations centralized and easy to profile.
|
||||||
|
|
||||||
|
2. Add IntersectionObserver tracking
|
||||||
|
|
||||||
|
- Observe each `[data-message-id]` element once, on mount.
|
||||||
|
- Maintain a small map of `id -> intersectionRatio` (or visible boolean).
|
||||||
|
- Pick the active id by:
|
||||||
|
- highest intersection ratio, then
|
||||||
|
- nearest to top of viewport as a tiebreaker
|
||||||
|
|
||||||
|
3. Add binary search fallback
|
||||||
|
|
||||||
|
- Maintain an ordered list of `{ id, top }` positions.
|
||||||
|
- On scroll (throttled via `requestAnimationFrame`), compute target Y and binary search to find nearest message.
|
||||||
|
- Refresh the positions list on:
|
||||||
|
- message list mutations (new messages)
|
||||||
|
- container resize events (ResizeObserver)
|
||||||
|
- explicit “layout changed” events after streaming completes
|
||||||
|
|
||||||
|
4. Remove `querySelectorAll` hot path
|
||||||
|
|
||||||
|
- Keep a one-time initial query only as a bridge during rollout, then remove it.
|
||||||
|
- Ensure newly rendered messages are registered via refs rather than scanning the whole DOM.
|
||||||
|
|
||||||
|
5. Add a feature flag and fallback
|
||||||
|
|
||||||
|
- Add `session.scrollSpyOptimized` flag.
|
||||||
|
- If observer setup fails, fall back to the existing scan behavior temporarily.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Data migration / backward compatibility
|
||||||
|
|
||||||
|
- No persisted data changes.
|
||||||
|
- IDs remain sourced from existing `data-message-id` attributes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Risk + mitigations
|
||||||
|
|
||||||
|
- Risk: observer ordering differs from previous “active message” logic.
|
||||||
|
- Mitigation: keep selection rules simple, document them, and add a small tolerance for tie cases.
|
||||||
|
- Risk: layout shifts cause incorrect offset indexing.
|
||||||
|
- Mitigation: refresh offsets with ResizeObserver and after message streaming batches.
|
||||||
|
- Risk: performance regressions from observing too many nodes.
|
||||||
|
- Mitigation: prefer one observer instance and avoid per-node observers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Validation plan
|
||||||
|
|
||||||
|
- Manual scenarios:
|
||||||
|
- very long sessions (hundreds of messages) and continuous scrolling
|
||||||
|
- streaming responses that append content and change heights
|
||||||
|
- resizing the window and toggling side panels
|
||||||
|
- Add a dev-only profiler hook to log time spent in scroll-spy updates per second.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Rollout plan
|
||||||
|
|
||||||
|
- Land extracted module first, still using the old scan internally.
|
||||||
|
- Add observer implementation behind `session.scrollSpyOptimized` off by default.
|
||||||
|
- Enable flag for internal testing, then default on after stability.
|
||||||
|
- Keep fallback code for one release cycle, then remove scan path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Open questions
|
||||||
|
|
||||||
|
- What is the exact definition of “active” used elsewhere (URL hash, sidebar highlight, breadcrumb)?
|
||||||
|
- Are messages virtualized today, or are all DOM nodes mounted at once?
|
||||||
|
- Which container is the scroll root (window vs an inner div), and does it change by layout mode?
|
||||||
153
specs/05-modularize-and-dedupe.md
Normal file
153
specs/05-modularize-and-dedupe.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
## Component modularity
|
||||||
|
|
||||||
|
Split mega-components and dedupe scoped caches
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Several large UI files combine rendering, state, persistence, and caching patterns, including repeated “scoped session cache” infrastructure. We’ll extract reusable primitives and break large components into smaller units without changing user-facing behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Goals
|
||||||
|
|
||||||
|
- Reduce complexity in:
|
||||||
|
- `packages/app/src/pages/session.tsx`
|
||||||
|
- `packages/app/src/pages/layout.tsx`
|
||||||
|
- `packages/app/src/components/prompt-input.tsx`
|
||||||
|
- Deduplicate “scoped session cache” logic into a shared utility
|
||||||
|
- Make performance fixes (eviction, throttling) easier to implement safely
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Non-goals
|
||||||
|
|
||||||
|
- Large redesign of routing or page structure
|
||||||
|
- Moving to a different state management approach
|
||||||
|
- Rewriting all contexts in one pass
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Current state
|
||||||
|
|
||||||
|
- Session page is large and mixes concerns (`packages/app/src/pages/session.tsx`).
|
||||||
|
- Layout is also large and likely coordinates multiple global concerns (`packages/app/src/pages/layout.tsx`).
|
||||||
|
- Prompt input is large and includes persistence and interaction logic (`packages/app/src/components/prompt-input.tsx`).
|
||||||
|
- Similar “scoped cache” patterns appear in multiple places (session-bound maps, per-session stores, ad hoc memoization).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Proposed approach
|
||||||
|
|
||||||
|
- Introduce a shared “scoped store” utility to standardize session-bound caches:
|
||||||
|
- keyed by `sessionId`
|
||||||
|
- automatic cleanup via TTL or explicit `dispose(sessionId)`
|
||||||
|
- optional LRU cap for many sessions
|
||||||
|
- Break mega-components into focused modules with clear boundaries:
|
||||||
|
- “view” components (pure rendering)
|
||||||
|
- “controller” hooks (state + effects)
|
||||||
|
- “services” (SDK calls, persistence adapters)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phased implementation steps
|
||||||
|
|
||||||
|
1. Inventory and name the repeated pattern
|
||||||
|
|
||||||
|
- Identify the repeated “scoped session cache” usage sites in:
|
||||||
|
- `packages/app/src/pages/session.tsx`
|
||||||
|
- `packages/app/src/pages/layout.tsx`
|
||||||
|
- `packages/app/src/components/prompt-input.tsx`
|
||||||
|
- Write down the common operations (get-or-create, clear-on-session-change, dispose).
|
||||||
|
|
||||||
|
2. Add a shared scoped-cache utility
|
||||||
|
|
||||||
|
- Create `packages/app/src/utils/scoped-cache.ts`:
|
||||||
|
- `createScopedCache(createValue, opts)` returning `get(key)`, `peek(key)`, `delete(key)`, `clear()`
|
||||||
|
- optional TTL + LRU caps to avoid leak-by-design
|
||||||
|
- Keep the API tiny and explicit so call sites stay readable.
|
||||||
|
|
||||||
|
Sketch:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type ScopedOpts = { maxEntries?: number; ttlMs?: number }
|
||||||
|
|
||||||
|
function createScopedCache<T>(createValue: (key: string) => T, opts: ScopedOpts) {
|
||||||
|
// store + eviction + dispose hooks
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Extract session page submodules
|
||||||
|
|
||||||
|
- Split `packages/app/src/pages/session.tsx` into:
|
||||||
|
- `session/view.tsx` for rendering layout
|
||||||
|
- `session/messages.tsx` for message list
|
||||||
|
- `session/composer.tsx` for input wiring
|
||||||
|
- `session/scroll-spy.ts` for active message tracking
|
||||||
|
- Keep exports stable so routing code changes minimally.
|
||||||
|
|
||||||
|
4. Extract layout coordination logic
|
||||||
|
|
||||||
|
- Split `packages/app/src/pages/layout.tsx` into:
|
||||||
|
- shell layout view
|
||||||
|
- navigation/controller logic
|
||||||
|
- global keyboard shortcuts (if present)
|
||||||
|
- Ensure each extracted piece has a narrow prop surface and no hidden globals.
|
||||||
|
|
||||||
|
5. Extract prompt-input state machine
|
||||||
|
|
||||||
|
- Split `packages/app/src/components/prompt-input.tsx` into:
|
||||||
|
- `usePromptComposer()` hook (draft, submission, attachments)
|
||||||
|
- presentational input component
|
||||||
|
- Route persistence through existing `packages/app/src/context/prompt.tsx`, but isolate wiring code.
|
||||||
|
|
||||||
|
6. Replace ad hoc scoped caches with the shared utility
|
||||||
|
|
||||||
|
- Swap one call site at a time and keep behavior identical.
|
||||||
|
- Add a flag `scopedCache.shared` to fall back to the old implementation if needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Data migration / backward compatibility
|
||||||
|
|
||||||
|
- No persisted schema changes are required by modularization alone.
|
||||||
|
- If any cache keys change due to refactors, keep a compatibility reader for one release cycle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Risk + mitigations
|
||||||
|
|
||||||
|
- Risk: refactors cause subtle behavior changes (focus, keyboard shortcuts, scroll position).
|
||||||
|
- Mitigation: extract without logic changes first, then improve behavior in later diffs.
|
||||||
|
- Risk: new shared cache introduces lifecycle bugs.
|
||||||
|
- Mitigation: require explicit cleanup hooks and add dev assertions for retained keys.
|
||||||
|
- Risk: increased file count makes navigation harder temporarily.
|
||||||
|
- Mitigation: use consistent naming and keep the folder structure shallow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Validation plan
|
||||||
|
|
||||||
|
- Manual regression checklist:
|
||||||
|
- compose, attach images, submit, and reload draft
|
||||||
|
- navigate between sessions and confirm caches don’t bleed across IDs
|
||||||
|
- verify terminal, file search, and scroll-spy still behave normally
|
||||||
|
- Add lightweight unit tests for `createScopedCache` eviction and disposal behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Rollout plan
|
||||||
|
|
||||||
|
- Phase 1: introduce `createScopedCache` unused, then adopt in one low-risk area.
|
||||||
|
- Phase 2: extract session submodules with no behavior changes.
|
||||||
|
- Phase 3: flip remaining scoped caches to shared utility behind `scopedCache.shared`.
|
||||||
|
- Phase 4: remove old duplicated implementations after confidence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Open questions
|
||||||
|
|
||||||
|
- Where exactly is “scoped session cache” duplicated today, and what are the differing lifecycle rules?
|
||||||
|
- Which extracted modules must remain synchronous for Solid reactivity to behave correctly?
|
||||||
|
- Are there implicit dependencies in the large files (module-level state) that need special handling?
|
||||||
196
specs/perf-roadmap.md
Normal file
196
specs/perf-roadmap.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
## Performance roadmap
|
||||||
|
|
||||||
|
Sequenced delivery plan for app scalability + maintainability
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Objective
|
||||||
|
|
||||||
|
Deliver the top 5 app improvements (performance + long-term flexibility) in a safe, incremental sequence that:
|
||||||
|
|
||||||
|
- minimizes regression risk
|
||||||
|
- keeps changes reviewable (small PRs)
|
||||||
|
- provides escape hatches (flags / caps)
|
||||||
|
- validates improvements with targeted measurements
|
||||||
|
|
||||||
|
This roadmap ties together:
|
||||||
|
|
||||||
|
- `specs/01-persist-payload-limits.md`
|
||||||
|
- `specs/02-cache-eviction.md`
|
||||||
|
- `specs/03-request-throttling.md`
|
||||||
|
- `specs/04-scroll-spy-optimization.md`
|
||||||
|
- `specs/05-modularize-and-dedupe.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Guiding principles
|
||||||
|
|
||||||
|
- Prefer “guardrails first”: add caps/limits and do no harm, then optimize.
|
||||||
|
- Always ship behind flags if behavior changes (especially persistence and eviction).
|
||||||
|
- Optimize at chokepoints (SDK call wrappers, storage wrappers, scroll-spy module) instead of fixing symptoms at every call site.
|
||||||
|
- Make “hot paths” explicitly measurable in dev (e.g. via `packages/app/src/utils/perf.ts`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 0 — Baseline + flags (prep)
|
||||||
|
|
||||||
|
**Goal:** make later changes safe to land and easy to revert.
|
||||||
|
|
||||||
|
**Deliverables**
|
||||||
|
|
||||||
|
- Feature-flag plumbing for:
|
||||||
|
- persistence payload limits (`persist.payloadLimits`)
|
||||||
|
- request debouncing/latest-only (`requests.*`)
|
||||||
|
- cache eviction (`cache.eviction.*`)
|
||||||
|
- optimized scroll spy (`session.scrollSpyOptimized`)
|
||||||
|
- shared scoped cache (`scopedCache.shared`)
|
||||||
|
- Dev-only counters/logs for:
|
||||||
|
- persist oversize detections
|
||||||
|
- request aborts/stale drops
|
||||||
|
- eviction counts and retained sizes
|
||||||
|
- scroll-spy compute time per second
|
||||||
|
|
||||||
|
**Exit criteria**
|
||||||
|
|
||||||
|
- Flags exist but default “off” for behavior changes.
|
||||||
|
- No user-visible behavior changes.
|
||||||
|
|
||||||
|
**Effort / risk**: `S–M` / low
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 1 — Stop the worst “jank generators” (storage + request storms)
|
||||||
|
|
||||||
|
**Goal:** remove the highest-frequency sources of main-thread blocking and redundant work.
|
||||||
|
|
||||||
|
**Work items**
|
||||||
|
|
||||||
|
- Implement file search debounce + stale-result protection
|
||||||
|
- Spec: `specs/03-request-throttling.md`
|
||||||
|
- Start with file search only (lowest risk, easy to observe).
|
||||||
|
- Add persistence payload size checks + warnings (no enforcement yet)
|
||||||
|
- Spec: `specs/01-persist-payload-limits.md`
|
||||||
|
- Focus on detecting oversized keys and preventing repeated write attempts.
|
||||||
|
- Ship prompt-history “strip image dataUrl” behind a flag
|
||||||
|
- Spec: `specs/01-persist-payload-limits.md`
|
||||||
|
- Keep image metadata placeholders so UI remains coherent.
|
||||||
|
|
||||||
|
**Exit criteria**
|
||||||
|
|
||||||
|
- Fast typing in file search generates at most 1 request per debounce window.
|
||||||
|
- Oversize persisted keys are detected and do not cause repeated blocking writes.
|
||||||
|
- Prompt history reload does not attempt to restore base64 `dataUrl` on web when flag enabled.
|
||||||
|
|
||||||
|
**Effort / risk**: `M` / low–med
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2 — Bound memory growth (in-memory eviction)
|
||||||
|
|
||||||
|
**Goal:** stabilize memory footprint for long-running sessions and “project hopping”.
|
||||||
|
|
||||||
|
**Work items**
|
||||||
|
|
||||||
|
- Introduce shared LRU/TTL cache helper
|
||||||
|
- Spec: `specs/02-cache-eviction.md`
|
||||||
|
- Apply eviction to file contents cache first
|
||||||
|
- Spec: `specs/02-cache-eviction.md`
|
||||||
|
- Pin open tabs / active file to prevent flicker.
|
||||||
|
- Add conservative eviction for global-sync per-directory child stores
|
||||||
|
- Spec: `specs/02-cache-eviction.md`
|
||||||
|
- Ensure evicted children are fully disposed.
|
||||||
|
- (Optional) session/message eviction if memory growth persists after the above
|
||||||
|
- Spec: `specs/02-cache-eviction.md`
|
||||||
|
|
||||||
|
**Exit criteria**
|
||||||
|
|
||||||
|
- Opening many files does not continuously increase JS heap without bound.
|
||||||
|
- Switching across many directories does not keep all directory stores alive indefinitely.
|
||||||
|
- Eviction never removes currently active session/file content.
|
||||||
|
|
||||||
|
**Effort / risk**: `M–L` / med
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3 — Large session scroll scalability (scroll spy)
|
||||||
|
|
||||||
|
**Goal:** keep scrolling smooth as message count increases.
|
||||||
|
|
||||||
|
**Work items**
|
||||||
|
|
||||||
|
- Extract scroll-spy logic into a dedicated module (no behavior change)
|
||||||
|
- Spec: `specs/04-scroll-spy-optimization.md`
|
||||||
|
- Implement IntersectionObserver tracking behind flag
|
||||||
|
- Spec: `specs/04-scroll-spy-optimization.md`
|
||||||
|
- Add binary search fallback for non-observer environments
|
||||||
|
- Spec: `specs/04-scroll-spy-optimization.md`
|
||||||
|
|
||||||
|
**Exit criteria**
|
||||||
|
|
||||||
|
- Scroll handler no longer calls `querySelectorAll('[data-message-id]')` on every scroll tick.
|
||||||
|
- Long sessions (hundreds of messages) maintain smooth scrolling.
|
||||||
|
- Active message selection remains stable during streaming/layout shifts.
|
||||||
|
|
||||||
|
**Effort / risk**: `M` / med
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4 — “Make it easy to keep fast” (modularity + dedupe)
|
||||||
|
|
||||||
|
**Goal:** reduce maintenance cost and make future perf work cheaper.
|
||||||
|
|
||||||
|
**Work items**
|
||||||
|
|
||||||
|
- Introduce shared scoped-cache utility and adopt in one low-risk area
|
||||||
|
- Spec: `specs/05-modularize-and-dedupe.md`
|
||||||
|
- Incrementally split mega-components (one PR per extraction)
|
||||||
|
- Spec: `specs/05-modularize-and-dedupe.md`
|
||||||
|
- Prioritize extracting:
|
||||||
|
- session scroll/backfill logic
|
||||||
|
- prompt editor model/history
|
||||||
|
- layout event/shortcut wiring
|
||||||
|
- Remove duplicated patterns after confidence + one release cycle
|
||||||
|
|
||||||
|
**Exit criteria**
|
||||||
|
|
||||||
|
- Each mega-file drops below a target size (suggestion):
|
||||||
|
- `session.tsx` < ~800 LOC
|
||||||
|
- `prompt-input.tsx` < ~900 LOC
|
||||||
|
- “Scoped cache” has a single implementation used across contexts.
|
||||||
|
- Future perf fixes land in isolated modules with minimal cross-cutting change.
|
||||||
|
|
||||||
|
**Effort / risk**: `L` / med–high
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Recommended PR slicing (keeps reviews safe)
|
||||||
|
|
||||||
|
- PR A: add request helpers + file search debounce (flagged)
|
||||||
|
- PR B: persist size detection + logs (no behavior change)
|
||||||
|
- PR C: prompt history strip images (flagged)
|
||||||
|
- PR D: cache helper + file content eviction (flagged)
|
||||||
|
- PR E: global-sync child eviction (flagged)
|
||||||
|
- PR F: scroll-spy extraction (no behavior change)
|
||||||
|
- PR G: optimized scroll-spy implementation (flagged)
|
||||||
|
- PR H+: modularization PRs (small, mechanical refactors)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Rollout strategy
|
||||||
|
|
||||||
|
- Keep defaults conservative and ship flags “off” first.
|
||||||
|
- Enable flags internally (dev builds) to gather confidence.
|
||||||
|
- Flip defaults in this order:
|
||||||
|
1. file search debounce
|
||||||
|
2. prompt-history image stripping
|
||||||
|
3. file-content eviction
|
||||||
|
4. global-sync child eviction
|
||||||
|
5. optimized scroll-spy
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Open questions
|
||||||
|
|
||||||
|
- What are acceptable defaults for storage caps and cache sizes for typical OpenCode usage?
|
||||||
|
- Does the SDK support `AbortSignal` end-to-end for cancellation, or do we rely on stale-result dropping?
|
||||||
|
- Should web and desktop persistence semantics be aligned (even if desktop has async storage available)?
|
||||||
Reference in New Issue
Block a user