Summary
Two concurrent open({ path }) calls on the same file produce independent in-memory stores that append to the same WAL without coordination. Updates are lost, state diverges, and the log can be corrupted. No error is raised.
Audit ID: B4 (High) — pre-announcement blocker
Comparable DBs: SQLite, LMDB, LevelDB all enforce exclusive access.
Impact
Everyday in the stated beachhead (test/dev):
- Parallel test workers on one fixture file
- Stale dev-server process + CLI write
- Hot-reload leaving old handle open
Lock machinery already exists for the CLI (src/cli/lock.ts) but the kernel does not use it.
Root cause
const opened = openLog(options.path, options.fs);
committed = opened.entries;
log = opened.log;
No exclusive lock, no in-process deduplication of open paths.
Proposed fix
1. FileSystem seam extension (minimal)
Add optional lock support or a dedicated helper on FileSystem:
lock(path): void — exclusive create <path>.lock with O_EXCL / wx semantics
unlock(path): void — release on close()
Reuse sentinel pattern from src/cli/lock.ts for consistency.
2. In-process guard
Module-level Set<string> of currently open paths (same process, two open() calls) → throw immediately without touching disk.
3. Kernel open() flow
- In-process check
- Acquire
<path>.lock via fs before openLog
- Release lock in
close() (always, idempotent close)
4. Browser / OPFS
Document that OPFS single-handle assumption remains; lock is no-op or in-process only when no cross-tab locking exists.
Tests
- Two
open() same path same process → second throws.
close() then reopen → succeeds.
- Two processes: use temp dir + fork/spawn test if feasible; else document CLI-only E2E and unit-test the lock file creation.
Related issues
Acceptance criteria
Agent constraints
- Widening
FileSystem affects node-fs, opfs, sim/simfs — update all adapters.
- Keep kernel logic in
core.ts minimal; lock acquisition can live in adapter or thin wrapper in index.ts.
Key files
src/core.ts, src/adapter/node-fs.ts, src/adapter/opfs.ts, src/sim/simfs.ts
src/cli/lock.ts — align or share sentinel constant
src/index.ts — wire default fs
Verification
Summary
Two concurrent
open({ path })calls on the same file produce independent in-memory stores that append to the same WAL without coordination. Updates are lost, state diverges, and the log can be corrupted. No error is raised.Audit ID: B4 (High) — pre-announcement blocker
Comparable DBs: SQLite, LMDB, LevelDB all enforce exclusive access.
Impact
Everyday in the stated beachhead (test/dev):
Lock machinery already exists for the CLI (
src/cli/lock.ts) but the kernel does not use it.Root cause
No exclusive lock, no in-process deduplication of open paths.
Proposed fix
1. FileSystem seam extension (minimal)
Add optional lock support or a dedicated helper on
FileSystem:lock(path): void— exclusive create<path>.lockwithO_EXCL/wxsemanticsunlock(path): void— release onclose()Reuse sentinel pattern from
src/cli/lock.tsfor consistency.2. In-process guard
Module-level
Set<string>of currently open paths (same process, twoopen()calls) → throw immediately without touching disk.3. Kernel
open()flow<path>.lockviafsbeforeopenLogclose()(always, idempotent close)4. Browser / OPFS
Document that OPFS single-handle assumption remains; lock is no-op or in-process only when no cross-tab locking exists.
Tests
open()same path same process → second throws.close()then reopen → succeeds.Related issues
--forcemust not fight kernel lock; align sentinel format.Acceptance criteria
open()on same path in one process fails loudly.close().bun run gategreen.FileSysteminterface extension).Agent constraints
FileSystemaffectsnode-fs,opfs,sim/simfs— update all adapters.core.tsminimal; lock acquisition can live in adapter or thin wrapper inindex.ts.Key files
src/core.ts,src/adapter/node-fs.ts,src/adapter/opfs.ts,src/sim/simfs.tssrc/cli/lock.ts— align or share sentinel constantsrc/index.ts— wire default fsVerification