Summary
src/lib/enbox/storage-adapter.ts tracks the set of stored keys in a single JSON-encoded list under `enbox:keys`. Every `set()` and `remove()` does a read-modify-write on that list (trackKey / untrackKey), so concurrent in-process writes race on the index — entries that landed between a read and the matching write are lost.
The header comment of the file already documents the caveat and proposes a per-key marker pattern as the long-term fix. This issue tracks that work.
Current Impact
clear() iterates the (possibly drifted) index, so dropped index entries leak past a `clear()` call.
- Individual
get / set / remove on the actual key path are unaffected.
useAgentStore.reset() works around the race by serializing all SecureStorage writes (sequential for loop, never Promise.all). Every other caller that does parallel writes is at risk.
Proposed Direction
- Replace the JSON list with per-key marker keys (e.g.
enbox:__index__:<key> → '1').
set() becomes two single-key writes; remove() becomes two single-key deletes; both are idempotent and free of read-modify-write.
clear() either:
- Enumerates marker keys via a native enumeration API where available, or
- Reads each marker by deterministic prefix (requires native support) — failing that, keep a separate enumeration channel that only the cleanup path consumes.
Acceptance Criteria
- No path in
SecureStorageAdapter performs a read-modify-write on a shared list.
clear() reliably removes every key written via set(), even under concurrent set / remove workloads.
- Migration path for existing installs that already wrote a JSON
KEY_INDEX (read once on first run, convert, delete the legacy entry).
- Tests cover concurrent writes against a fake native backend.
Out of Scope
- Changing the
enbox: key prefix.
- Replacing
SecureStorageAdapter with a different abstraction.
Summary
src/lib/enbox/storage-adapter.tstracks the set of stored keys in a single JSON-encoded list under `enbox:keys`. Every `set()` and `remove()` does a read-modify-write on that list (trackKey/untrackKey), so concurrent in-process writes race on the index — entries that landed between a read and the matching write are lost.The header comment of the file already documents the caveat and proposes a per-key marker pattern as the long-term fix. This issue tracks that work.
Current Impact
clear()iterates the (possibly drifted) index, so dropped index entries leak past a `clear()` call.get/set/removeon the actual key path are unaffected.useAgentStore.reset()works around the race by serializing all SecureStorage writes (sequentialforloop, neverPromise.all). Every other caller that does parallel writes is at risk.Proposed Direction
enbox:__index__:<key>→'1').set()becomes two single-key writes;remove()becomes two single-key deletes; both are idempotent and free of read-modify-write.clear()either:Acceptance Criteria
SecureStorageAdapterperforms a read-modify-write on a shared list.clear()reliably removes every key written viaset(), even under concurrentset/removeworkloads.KEY_INDEX(read once on first run, convert, delete the legacy entry).Out of Scope
enbox:key prefix.SecureStorageAdapterwith a different abstraction.