Context
During post-merge validation for the unprojected-session repair lane, I attempted the advertised safe validation path for the merged repair script:
node ai/scripts/maintenance/repairUnprojectedSessions.mjs --dry-run --limit all
The direct command did not reach the scan in the Codex harness. First it hit a sandbox-only log-path EPERM because .neo-ai-data/logs is a symlink outside the writable root. After redirecting Memory Core and Knowledge Base logPath process-locally to a writable temp directory, the same nominal dry-run still failed before scanning with SQLITE_READONLY because the CLI createRuntime() initializes GraphService, whose SQLite storage startup stamps schema metadata.
A pure readonly scan using the exported findUnprojectedSessions(...) helper, ChromaManager.getSummaryCollection(), and a better-sqlite3 readonly/fileMustExist graph handle did succeed and found 416 candidates.
Post-merge validation ledger: https://github.com/neomjs/neo/issues/13697#issuecomment-4761068136
Release classification: boardless operational hardening; not a release-blocker.
Live latest-open sweep: checked the latest 20 open issues at 2026-06-21T06:00Z; no equivalent dry-run-readonly ticket found. The closest open items were hook/workflow lanes (#13720, #13717, #13711, #13710) and the parent epic #13624, none covering this maintenance CLI contract.
A2A in-flight sweep: checked latest 30 A2A messages at 2026-06-21T06:00Z; no overlapping [lane-claim] for repairUnprojectedSessions dry-run/read-only runtime. Ada's #13720 claim is unrelated pre-commit hook work.
The Problem
--dry-run is the operator-facing safe validation mode for repairUnprojectedSessions.mjs, but the current runtime path still performs writable graph initialization before it can read candidates. That weakens the safety contract in exactly the environment where operators and agents need dry-run first:
- readonly/sandboxed local harnesses;
- post-merge validation before destructive
--apply;
- deployments where graph schema mutation is not allowed from a diagnostic command;
- repair workflows that need a candidate count before a human/operator decides whether to apply.
The dry-run scan itself does not need writable graph access. It only needs:
- a Chroma summary collection with
count() and get();
- a SQLite handle that supports
prepare(...).get(...) against Nodes.
The Architectural Reality
The relevant current surfaces are:
ai/scripts/maintenance/repairUnprojectedSessions.mjs
findUnprojectedSessions(...) is already pure enough to work with an injected summary collection and graph DB handle.
repairUnprojectedSessions(...) only needs MemorySessionIngestor.ingestSingleRow() when apply === true.
createRuntime() always initializes LifecycleService, GraphService, and StorageRouter, then returns the writable graph handle and ingestor.
ai/services/memory-core/GraphService.mjs
initAsync() creates the SQLite storage and calls the storage init path.
ai/graph/storage/SQLite.mjs
- startup uses a writable
better-sqlite3 connection and stamps schema metadata as part of initialization.
ai/services/memory-core/managers/ChromaManager.mjs
- can resolve the summary collection without requiring
GraphService initialization when imported directly.
better-sqlite3 readonly usage already has repo precedent in maintenance scripts such as ai/scripts/maintenance/restore.mjs and examples such as ai/examples/inspectGraph.mjs.
The Fix
Refactor the CLI runtime path so dry-run avoids writable graph initialization:
- Split runtime creation into dry-run and apply paths.
- For
apply === false, open aiConfig.storagePaths.graph through better-sqlite3 with {readonly: true, fileMustExist: true} and use the summary collection directly.
- For
apply === true, keep the existing writable runtime and MemorySessionIngestor path.
- Ensure any opened readonly handle is closed after the scan.
- Preserve the current JSON result shape for dry-run and apply.
- Add focused unit coverage that proves dry-run does not call writable
GraphService initialization and that apply still requires MemorySessionIngestor.ingestSingleRow().
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
repairUnprojectedSessions.mjs --dry-run CLI |
The script usage text says dry-run is the default safe mode; findUnprojectedSessions(...) only needs read operations |
Dry-run opens the graph through a readonly better-sqlite3 handle and scans candidates without GraphService writable startup |
Missing graph file returns a clear CLI error; malformed/old schema errors remain visible |
Usage text remains accurate; no public docs change required unless implementation adds a new option |
Unit test with injected runtime factory / readonly open seam; local dry-run reaches scan under readonly graph access |
repairUnprojectedSessions.mjs --apply CLI |
The script usage text says --apply mutates the Native Edge Graph |
Apply keeps the existing writable graph + MemorySessionIngestor path |
--apply still fails if MemorySessionIngestor.ingestSingleRow() is unavailable |
Usage text already warns that apply mutates |
Unit test asserts apply path requires ingestor and does not silently use readonly mode |
| Result JSON shape |
Existing CLI output consumed by operator validation comments and future automation |
Preserve {mode, digestedOnly, candidates, repaired, failed, stats, results} |
Dry-run results remains candidate rows; apply results remains repair results |
No schema docs today |
Existing and new unit coverage over runCli/repairUnprojectedSessions |
Decision Record impact
None. This is a maintenance CLI contract repair aligned with the existing Memory Core repair lane and the broader #13624 orchestrator-repair objective.
Acceptance Criteria
Out of Scope
- Running
--apply against the live graph.
- Changing
findUnprojectedSessions(...) candidate semantics.
- Changing log-path defaults or the shared MCP logger policy.
- Implementing the actual 416-candidate graph backfill.
Avoided Traps
- Do not make dry-run require write permission just to reuse the apply runtime.
- Do not hide
SQLITE_READONLY; it is useful evidence that the startup path is wrong for dry-run.
- Do not route this through
GraphService unless GraphService gains an explicit readonly mode; this ticket should stay scoped to the maintenance CLI.
Related
Parent: #13624
Related: #13697, #13701
Handoff Retrieval Hints: repairUnprojectedSessions dry-run SQLITE_READONLY GraphService stampSchemaVersion readonly better-sqlite3.
Context
During post-merge validation for the unprojected-session repair lane, I attempted the advertised safe validation path for the merged repair script:
node ai/scripts/maintenance/repairUnprojectedSessions.mjs --dry-run --limit allThe direct command did not reach the scan in the Codex harness. First it hit a sandbox-only log-path
EPERMbecause.neo-ai-data/logsis a symlink outside the writable root. After redirecting Memory Core and Knowledge BaselogPathprocess-locally to a writable temp directory, the same nominal dry-run still failed before scanning withSQLITE_READONLYbecause the CLIcreateRuntime()initializesGraphService, whose SQLite storage startup stamps schema metadata.A pure readonly scan using the exported
findUnprojectedSessions(...)helper,ChromaManager.getSummaryCollection(), and abetter-sqlite3readonly/fileMustExist graph handle did succeed and found 416 candidates.Post-merge validation ledger: https://github.com/neomjs/neo/issues/13697#issuecomment-4761068136
Release classification: boardless operational hardening; not a release-blocker.
Live latest-open sweep: checked the latest 20 open issues at 2026-06-21T06:00Z; no equivalent dry-run-readonly ticket found. The closest open items were hook/workflow lanes (
#13720,#13717,#13711,#13710) and the parent epic#13624, none covering this maintenance CLI contract.A2A in-flight sweep: checked latest 30 A2A messages at 2026-06-21T06:00Z; no overlapping
[lane-claim]forrepairUnprojectedSessionsdry-run/read-only runtime. Ada's#13720claim is unrelated pre-commit hook work.The Problem
--dry-runis the operator-facing safe validation mode forrepairUnprojectedSessions.mjs, but the current runtime path still performs writable graph initialization before it can read candidates. That weakens the safety contract in exactly the environment where operators and agents need dry-run first:--apply;The dry-run scan itself does not need writable graph access. It only needs:
count()andget();prepare(...).get(...)againstNodes.The Architectural Reality
The relevant current surfaces are:
ai/scripts/maintenance/repairUnprojectedSessions.mjsfindUnprojectedSessions(...)is already pure enough to work with an injected summary collection and graph DB handle.repairUnprojectedSessions(...)only needsMemorySessionIngestor.ingestSingleRow()whenapply === true.createRuntime()always initializesLifecycleService,GraphService, andStorageRouter, then returns the writable graph handle and ingestor.ai/services/memory-core/GraphService.mjsinitAsync()creates the SQLite storage and calls the storage init path.ai/graph/storage/SQLite.mjsbetter-sqlite3connection and stamps schema metadata as part of initialization.ai/services/memory-core/managers/ChromaManager.mjsGraphServiceinitialization when imported directly.better-sqlite3readonly usage already has repo precedent in maintenance scripts such asai/scripts/maintenance/restore.mjsand examples such asai/examples/inspectGraph.mjs.The Fix
Refactor the CLI runtime path so dry-run avoids writable graph initialization:
apply === false, openaiConfig.storagePaths.graphthroughbetter-sqlite3with{readonly: true, fileMustExist: true}and use the summary collection directly.apply === true, keep the existing writable runtime andMemorySessionIngestorpath.GraphServiceinitialization and that apply still requiresMemorySessionIngestor.ingestSingleRow().Contract Ledger Matrix
repairUnprojectedSessions.mjs --dry-runCLIfindUnprojectedSessions(...)only needs read operationsbetter-sqlite3handle and scans candidates withoutGraphServicewritable startuprepairUnprojectedSessions.mjs --applyCLI--applymutates the Native Edge GraphMemorySessionIngestorpath--applystill fails ifMemorySessionIngestor.ingestSingleRow()is unavailable{mode, digestedOnly, candidates, repaired, failed, stats, results}resultsremains candidate rows; applyresultsremains repair resultsrunCli/repairUnprojectedSessionsDecision Record impact
None. This is a maintenance CLI contract repair aligned with the existing Memory Core repair lane and the broader #13624 orchestrator-repair objective.
Acceptance Criteria
--dry-run --limit allcan reach candidate scanning with a readonly SQLite graph handle and without initializing writableGraphServicestorage.--applystill uses the writable runtime and requiresMemorySessionIngestor.ingestSingleRow().Out of Scope
--applyagainst the live graph.findUnprojectedSessions(...)candidate semantics.Avoided Traps
SQLITE_READONLY; it is useful evidence that the startup path is wrong for dry-run.GraphServiceunlessGraphServicegains an explicit readonly mode; this ticket should stay scoped to the maintenance CLI.Related
Parent: #13624 Related: #13697, #13701
Handoff Retrieval Hints:
repairUnprojectedSessions dry-run SQLITE_READONLY GraphService stampSchemaVersion readonly better-sqlite3.