Refs #16513 · #16514 · surfaced by a duplication-family sweep of ai/ after the operator observed that the lock/lease family (#16514) is unlikely to be the only one.
Context
ai/graph/storage/SQLite.mjs is a 34,534-byte storage class with a Base.mjs abstraction beside it. Measured on dev:
- modules importing
graph/storage/SQLite — 5
- modules opening a direct handle (
new Database( or a better-sqlite3 import) — 36
The shared primitive exists and is bypassed roughly 7:1.
The Problem
Two of tonight's live incidents are in the bypass list, which is what moves this out of aesthetic debt:
ai/mcp/server/memory-core/helpers/TurnPresenceHookWriter.mjs — the defect in #16513. @neo-opus-grace measured every hook-written turn-presence beacon today as unreadable: 6,000+ rows, one store per checkout, and a peer reading idle seven minutes after opening a PR. Her diagnosis names the shape directly: a direct sqlite handle plus an import.meta.url-relative path, with the ChromaManager precedent as the fix "applied everywhere except here."
ai/scripts/diagnostics/mailboxReadStateProbe.mjs — the instrument currently being pointed at @neo-fable's mailbox read-state rollback (409 messages re-marked durable, then 13 unread with July-era items resurfacing inside 60s). A probe that opens its own handle can answer about a different store than the one that took the write.
When each caller resolves its own path, "which database" becomes a property of the caller's working directory rather than of the deployment. That is the split-brain class: presence beacons landing in the pre-Docker root while memories land in the container's.
Bounded claim
Not claimed: that all 36 are defects. One-shot migrations under ai/scripts/migrations/ and several ai/examples/ scripts may legitimately want a direct handle against an explicitly named file, and a few are deliberately host-side by design. The claim is narrower:
- The ratio is inverted — the bypass is the norm, not the exception, so a new caller has no reason to think importing the shared class is expected.
- No mechanism distinguishes a legitimate direct handle from an accidental one, so #16513's fix cannot generalize. Fixing one site leaves 35 with no signal about which were supposed to be next.
- Path resolution is per-caller, so the deployment cannot state which store is authoritative.
The Fix
Not prescribed here — the first deliverable is the census, mirroring #16514's ordering, because the 36 are not one population.
Sketch of the shape, to be settled by whoever owns it:
- Classify all 36 into should import the shared class · legitimate direct handle, explicitly justified · host-side by design. The middle bucket needs a stated reason at the call site, so the next reader is not left guessing.
- Give the legitimate bucket a named entry point rather than a bare
new Database(...), so "who may open the graph" is answerable by grep.
- Fail closed on ambiguity. A caller that cannot state which store it wants should not silently get the one nearest its
import.meta.url.
Acceptance Criteria
Out of Scope
- #16513 itself. @neo-opus-grace owns the
TurnPresenceHookWriter instance with measurement in hand; this ticket is the class and must not race her fix.
- The read-state rollback episode. Live, #16510-family, and only possibly downstream of this. No causal claim made.
- Chroma-side ownership.
ChromaManager is cited as the precedent to copy, not a surface to change.
Avoided Traps
- Flattening all 36 into one migration. Migrations and examples plausibly need direct handles; a sweep that converts them all would trade a diagnosability problem for a functional one.
- Counting the ratio as the finding. 36:5 is the symptom. The finding is that nothing distinguishes deliberate from accidental, which is why a fixed site does not teach the next one.
- Assuming the rollback is caused by this. It fits the shape, and fitting a shape is not evidence. Named as a hypothesis in the sweep that produced this ticket, and left there.
Related
#16513 — the first measured instance, owned and in flight.
#16514 — the sibling duplication family (lock/lease); same sweep, same ordering discipline (census before consolidation).
#16515 — the ai/ debt census; this is one entry in its duplication chapter, filed separately because the operator's read is that a family this size deserves its own owner.
Unassigned by intent — I hold #16514 and am not claiming a second consolidation lane.
Live latest-open sweep: latest 20 open issues checked plus a targeted search for sqlite/graph-handle scope; the only hits were unrelated epics (#14442, #11318). No [lane-claim] on graph-handle ownership.
Origin Session ID: 11695cce-9854-4be2-80c3-8ea4322298bf
Retrieval Hint: query_raw_memories("shared graph storage SQLite class bypassed direct better-sqlite3 handle per-caller path resolution split brain 36 sites")
Refs #16513 · #16514 · surfaced by a duplication-family sweep of
ai/after the operator observed that the lock/lease family (#16514) is unlikely to be the only one.Context
ai/graph/storage/SQLite.mjsis a 34,534-byte storage class with aBase.mjsabstraction beside it. Measured ondev:graph/storage/SQLite— 5new Database(or abetter-sqlite3import) — 36The shared primitive exists and is bypassed roughly 7:1.
The Problem
Two of tonight's live incidents are in the bypass list, which is what moves this out of aesthetic debt:
ai/mcp/server/memory-core/helpers/TurnPresenceHookWriter.mjs— the defect in #16513. @neo-opus-grace measured every hook-written turn-presence beacon today as unreadable: 6,000+ rows, one store per checkout, and a peer readingidleseven minutes after opening a PR. Her diagnosis names the shape directly: a direct sqlite handle plus animport.meta.url-relative path, with theChromaManagerprecedent as the fix "applied everywhere except here."ai/scripts/diagnostics/mailboxReadStateProbe.mjs— the instrument currently being pointed at @neo-fable's mailbox read-state rollback (409 messages re-marked durable, then 13 unread with July-era items resurfacing inside 60s). A probe that opens its own handle can answer about a different store than the one that took the write.When each caller resolves its own path, "which database" becomes a property of the caller's working directory rather than of the deployment. That is the split-brain class: presence beacons landing in the pre-Docker root while memories land in the container's.
Bounded claim
Not claimed: that all 36 are defects. One-shot migrations under
ai/scripts/migrations/and severalai/examples/scripts may legitimately want a direct handle against an explicitly named file, and a few are deliberately host-side by design. The claim is narrower:The Fix
Not prescribed here — the first deliverable is the census, mirroring #16514's ordering, because the 36 are not one population.
Sketch of the shape, to be settled by whoever owns it:
new Database(...), so "who may open the graph" is answerable by grep.import.meta.url.Acceptance Criteria
Out of Scope
TurnPresenceHookWriterinstance with measurement in hand; this ticket is the class and must not race her fix.ChromaManageris cited as the precedent to copy, not a surface to change.Avoided Traps
Related
#16513— the first measured instance, owned and in flight.#16514— the sibling duplication family (lock/lease); same sweep, same ordering discipline (census before consolidation).#16515— theai/debt census; this is one entry in its duplication chapter, filed separately because the operator's read is that a family this size deserves its own owner.Unassigned by intent — I hold #16514 and am not claiming a second consolidation lane.
Live latest-open sweep: latest 20 open issues checked plus a targeted search for sqlite/graph-handle scope; the only hits were unrelated epics (#14442, #11318). No
[lane-claim]on graph-handle ownership.Origin Session ID: 11695cce-9854-4be2-80c3-8ea4322298bf
Retrieval Hint:
query_raw_memories("shared graph storage SQLite class bypassed direct better-sqlite3 handle per-caller path resolution split brain 36 sites")