packages/agent/docs/work-packages/07-sqlite-host-ownership-live-forks.md
Status: implemented.
The delivered backend has no writer lease or replacement ownership primitive. It provides no-create read-write/read-only opens, queued same-repository snapshots, independent read-only WAL snapshots for live external sources, canonical physical identity, path-safe IDs, repository-local deletion reservation, and all-settled close. The tests cover both per-file and shared-container layouts, including a writer commit completed after a read snapshot boundary but before that reader closes.
This package aligns packages/session-backends/sqlite-node with the product ownership model: the server owns Session records and worker lifecycle, and exactly one host-assigned process owns writable Session authority at a time. Normally that process is the Session worker. The server may temporarily own a newly created or forked destination, but it closes that Session before handing its metadata to a worker.
Storage does not implement writer ownership. Remove the SQLite writer lease; do not repair or replace it.
A server-side fork is intentionally different from a second writer: it may open a live worker-owned source concurrently for one coherent read-only snapshot while the worker keeps committing. Shared SQLite containers remain supported.
Read completely before editing:
packages/agent/docs/plugins.md ownership, replacement, and removal sections.packages/server/README.md and relevant Session routing/removal source and tests.packages/agent/docs/harness.md §§0.6, 1.4–1.7, 2.7–2.8, 4.3, and Part 9.packages/agent/docs/post-wp05-roadmap.md.packages/session-backends/sqlite-node/src.packages/session-backends/sqlite-node.packages/session-backends/sqlite-node/README.md and CHANGELOG.md.Do not use stale files under dist/ as implementation input. Completed WP01/WP06 documents are historical; do not rewrite them to hide the earlier lease implementation.
Exactly one host-assigned process owns a writable Session. The Session worker is the normal owner. Worker replacement closes the old owner before the new worker opens the Session. Server management serializes creation, forking, removal, and attachment lifecycle around that ownership transfer.
Memory, JSONL, and SQLite do not detect a second process opening the same Session for writes. Bypassing the server/worker lifecycle is a trusted-host defect, not a storage race to repair. A repository still rejects duplicate writable handles it owns in one process.
Do not add a storage lease, filesystem lock, fencing token, heartbeat, timeout-based takeover, deletion tombstone, quarantine protocol, or generic lock manager.
The server owns repository administration and may fork a source while its Session worker continues writing it. The source side of that fork:
BEGIN transaction;COMMIT/ROLLBACK.SQLite WAL permits later worker commits while the read transaction remains open. The fork sees each source commit entirely before or entirely after its snapshot boundary, never a mixture.
A source already open in the same repository uses its active SqliteStorage.snapshot() path. That snapshot is queued on the source commitQueue, preserving WP06's admitted-commit ordering seam and the existing conformance case.
Do not replace this path with an independent connection. Instead, bind active storage lookup to exact physical identity plus Session ID so metadata for another container cannot select it accidentally.
SessionRepo.create() and fork() continue returning an open Session. A server may temporarily own that new destination, capture its metadata, and close it before launching a worker. This is a valid ownership transfer, not a reason to redesign SessionRepo into a record-only API.
Current source contains:
writer_lease schema state;SqliteOpenSession;SqliteStorage;This is a second, incomplete ownership system. Its pre-commit renewal is not atomic with the following data transaction, but the correct fix is deletion of the ownership mechanism, not transaction-local fencing.
The database factory exposes only open(path), which creates a missing SQLite file. Metadata open, listing probes, deletion, and fork-source reads must not turn a removed path into an empty database.
Fork-source reads also configure PRAGMA journal_mode = WAL, which is a write-oriented setup step and must not run on a read-only connection.
delete() checks pendingIds but does not reserve the ID. A same-repository create/open/fork destination can enter while asynchronous destructive work is in progress. Host lifecycle owns cross-process ordering; the repository still must serialize its own local operations.
openStorages is keyed only by Session ID, so the same ID at another physical path may select the wrong active source.create() makes options.directory, not the parent of an explicit databasePath./, \, .., %, and platform separators must not escape directory.repo.close() uses fail-fast Promise.all, so it may return before every open Session has attempted to drain and close.Deterministic list ordering, bind-variable limits, branch-copy cost, fork scalar filtering, prepared statements, and VACUUM policy remain separate.
Delete all runtime lease behavior:
src/sqlite/session/writer-lease.ts;writer_lease from WIP 001_initial.sql;deleteSessionRows();SqliteSessionRepo;beforeCommit from SqliteStorage;leaseError from SqliteOpenSession;Keep:
commitQueue;BEGIN IMMEDIATE transaction per commit;next_seq allocation;Format 4 remains WIP. Remove the table from new schema in place; an old file containing an unused writer_lease table remains readable and the table and stale rows are ignored forever. Post-WP07 code does not delete them because doing so serves no runtime behavior. A pre-WP07 binary cannot open a new post-WP07 database without that table; backward compatibility for this WIP format is not required. Add no migration, compatibility path, or storage-version bump.
Extend SqliteDatabaseFactory with narrow operations:
open(path) — intentional creation or create-if-missing;openExisting(path) — read-write open that fails if the file does not exist;openReadOnly(path) — read-only open that fails if the file does not exist.The Node adapter uses DatabaseSync(path, { readOnly: true }) for read-only access. Implement and test an actual no-create read-write mode for openExisting; do not rely on access() followed by a create-capable open.
Split connection setup:
busy_timeout;busy_timeout and never attempt to change journal mode.Use no-create modes for metadata open, listing probes, deletion, and fork-source reads.
Source open in this repository: retain SqliteStorage.snapshot() and queue it after prior admitted commits. Replace the ID-only active map key with canonical (containerPath, sessionId) identity and use the same helper for publish, lookup, and removal.
Source not open in this repository: this includes a closed source and a source currently owned by a worker in another process. Open the exact source through openReadOnly, then capture it in one deferred read transaction. Validate the Session row and storage version inside that transaction. Do not consult destination reservations, claim source ownership, or block the worker's later commits.
The destination remains a normal writable create/fork transaction after source capture. In shared-container mode the source worker and destination transaction may use the same file; SQLite serializes destination writes while preserving Session row isolation.
The host must close the Session worker before calling repo.delete(). Direct cross-process deletion of a live Session is unsupported.
Within one SqliteSessionRepo, deletion must reserve the Session ID from entry through completion and release it in finally:
BEGIN IMMEDIATE transaction on one connection;Do not add a lease check, tombstone, quarantine rename, or stale-deleter protocol. Cross-process removal ordering is the server's responsibility.
(canonical container path, sessionId).dirname(databasePath) when databasePath is configured.SqliteSessionRepo.close(context) must:
AggregateError containing all failures;This is backend-local resource cleanup. Do not change the shared SessionRepo interface or JSONL lifecycle in this package.
Files:
src/sqlite/session/writer-lease.ts;src/sqlite/migrations/001_initial.sql;src/sqlite/session/session-row.ts;src/sqlite/storage.ts;src/sqlite/session.ts;src/sqlite/repo.ts;Tasks:
Files:
src/index.ts;src/sqlite/types.ts;src/sqlite/repo.ts;Tasks:
openExisting and openReadOnly with tested no-create behavior.Files:
src/sqlite/repo.ts;Tasks:
Files:
src/sqlite/repo.ts;Tasks:
Files:
packages/agent/docs/harness.md;packages/agent/docs/post-wp05-roadmap.md;packages/agent/docs/values.md;packages/session-backends/sqlite-node/README.md;Document host-owned writable authority, the two fork-source paths, no-create opens, local deletion reservation, and absence of storage-layer ownership.
Use real independent node:sqlite connections. Test-only wrappers may expose deterministic transaction boundaries; production code gets no sleeps or race flags.
writer_lease table;BEGIN IMMEDIATE transaction.For both per-file and shared-container layouts:
databasePath succeeds when its parent does not exist;../, /, \, %, dots, and Unicode stay inside directory and preserve the metadata ID;BEGIN IMMEDIATE;After each code slice:
npm run check
Run each modified focused test from packages/session-backends/sqlite-node with the repository Vitest binary. Final validation:
./test.sh
Review checkpoints:
Delegated reviews use provider anthropic and model claude-fable-5.
Do not include:
SessionRepo redesign or compatibility facade;getEntries bind-limit chunking or general query-limit normalization;SessionRepo.close() contract changes;If implementation requires an excluded item, stop and revise the handoff rather than expanding silently.
WP07 is complete when:
npm run check, and ./test.sh pass;