LearnNewsExamplesServices
Frontmatter
id13722
titleMake repairUnprojectedSessions dry-run truly read-only
stateClosed
labels
bugaiarchitecturemodel-experience
assigneesneo-gpt
createdAtJun 21, 2026, 8:02 AM
updatedAtJun 21, 2026, 11:41 AM
githubUrlhttps://github.com/neomjs/neo/issues/13722
authorneo-gpt
commentsCount0
parentIssue13624
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJun 21, 2026, 11:41 AM

Make repairUnprojectedSessions dry-run truly read-only

Closed v13.1.0/archive-v13-1-0-chunk-5 bugaiarchitecturemodel-experience
neo-gpt
neo-gpt commented on Jun 21, 2026, 8:02 AM

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:

  1. Split runtime creation into dry-run and apply paths.
  2. For apply === false, open aiConfig.storagePaths.graph through better-sqlite3 with {readonly: true, fileMustExist: true} and use the summary collection directly.
  3. For apply === true, keep the existing writable runtime and MemorySessionIngestor path.
  4. Ensure any opened readonly handle is closed after the scan.
  5. Preserve the current JSON result shape for dry-run and apply.
  6. 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

  • --dry-run --limit all can reach candidate scanning with a readonly SQLite graph handle and without initializing writable GraphService storage.
  • --apply still uses the writable runtime and requires MemorySessionIngestor.ingestSingleRow().
  • The JSON output shape stays compatible for both modes.
  • Focused unit tests cover the dry-run/read-only split and apply path guard.
  • Manual evidence includes the previously failing dry-run scenario or an equivalent injected-readonly test path.

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.

tobiu referenced in commit ddf02da - "fix(ai): make repair dry-run runtime readonly (#13722) (#13723)" on Jun 21, 2026, 11:41 AM
tobiu closed this issue on Jun 21, 2026, 11:41 AM