Context
Surfaced during my cross-family review of #13317 (reviewId PRR_kwDODSospM8AAAABC_i38A, Resolves #13312 "keep add_memory callable during graph startup degradation"). #13317 makes Memory-Core graph-init failure non-fatal: GraphService init now sets this.db = null + records graphInitError (caught, not rejecting _initPromise) instead of collapsing boot, so the mandatory add_memory WAL save stays callable in a degraded deployment. That's the right resilience move — but it creates a new runtime state the graph-backed consumers were never written for.
The Problem
Before #13317, a GraphService init fault rejected _initPromise → GraphService.ready() threw → boot collapsed, so a live server with GraphService.db === null did not exist. #13317 deliberately creates that "server-up-but-graph-degraded" state. #13317 cleanly guarded one consumer — WakeSubscriptionService.init now does if (!GraphService.db) throw new Error('GraphService unavailable: …'), caught by prepareStartupDependency and recorded as degraded. But the other graph-backed consumers still dereference GraphService.db unguarded, so in the degraded state they throw TypeError: Cannot read properties of null rather than a clean "graph unavailable" error.
Net: the degradation is observable to operators (healthcheck startup.dependencies → status: degraded, added by #13317), but graph-backed tool calls (the mailbox tools especially — heavily used) are not yet gracefully degraded. This is not a regression (those tools were boot-collapse-unavailable before #13317) and it is outside #13312's add_memory scope — so it is a successor that completes the "graceful degradation" half.
The Architectural Reality
ai/services/memory-core/MailboxService.mjs is the clearest instance — partial-guarding inconsistency:
- Already guarded (
GraphService.db?.…): lines 139, 167, 197, 1311.
- Unguarded (would NPE on
db=null): 454-467 (GraphService.db.getAdjacentNodes(...), GraphService.db.edges.items), 555 (GraphService.db.nodes.has(...)), and ~9 const db = GraphService.db; … db.X sites (70, 260, 284, 613, 773, 852, 932, 1011, 1072).
The canonical fix pattern already exists in-repo: WakeSubscriptionService.init's !GraphService.db guard → clean GraphService unavailable: <graphInitError.message || reason>.
The Fix
Audit all GraphService.db consumers in ai/services/memory-core/ (grep GraphService\.db) and add the db=null guard at the entry of each graph-backed operation, fail-closed, mirroring WakeSubscriptionService.init — return/throw a clean "graph unavailable (degraded)" error instead of a TypeError. MailboxService is the priority; sweep peers (PermissionService, DatabaseService, heartbeatPulseEvaluator, etc.) for the same pattern.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
Graph-backed Memory Core tools / service methods using GraphService.db |
#13317 degraded graph-init model + current WakeSubscriptionService.init guard precedent |
When graph DB is unavailable, fail closed with a clean GraphService unavailable degraded error; never raw TypeError |
add_memory remains WAL-available; non-graph reads with optional SQLite fallbacks may return empty/degraded as currently designed |
PR body + test names; no public docs required unless tool error wording becomes user-facing |
Unit test with GraphService.db = null + graphInitError; post-merge degraded-process spot check |
| MailboxService graph-backed operations |
#13323 premise sweep + current mixed guarded/unguarded MailboxService source |
Entry points that require the graph DB must throw/return the same clean degraded error before touching nodes, edges, or adjacent-node helpers |
Mailbox-only suppressed-message persistence remains out of scope; retry/read-state behavior from adjacent wake tickets must not change |
PR evidence only |
Focused MailboxService unit coverage for at least one representative graph-backed tool path |
PermissionService / WakeSubscriptionService / other memory-core consumers using GraphService.db |
Current rg "GraphService\.db" ai/services/memory-core source sweep |
Each consumer either null-guards before dereference or explicitly proves an optional/fallback path is valid |
Existing optional chaining / SQLite fallback paths remain valid when they already avoid graph dereference |
PR body names audited surfaces; JSDoc only if a helper is introduced |
Source audit plus focused tests around shared guard helper or representative consumers |
Acceptance Criteria
Out of Scope
- The
add_memory WAL acceptance path — already delivered by #13312 / #13317.
- The graph-projection drain-loop lifecycle — that is #13313's concern (MemoryService projection lifecycle).
- The healthcheck degradation surfacing — already added by #13317.
Decision Record impact
none — consumer null-safety hardening; aligned-with #13317's WAL-minimum-tier degradation model, no ADR authority touched.
Related
- #13317 (the PR that introduced the
db=null state) / #13312 (parent — add_memory callability)
- #13313 (sibling — MemoryService graph-projection lifecycle; distinct concern)
- #13283 / #13288 / #13314 (the Memory-Core startup-resilience thread)
Release classification: post-release (Approve+Follow-Up follow-up from the #13317 review — non-blocking; the verdict certified #13317 shippable without it).
Live latest-open sweep: checked latest 20 open issues at 2026-06-15T09:30Z; no equivalent (nearest is #13313, a distinct projection-lifecycle concern). A2A in-flight claim sweep: no lane-claim on this scope.
Origin Session ID: 2d993feb-ea2f-4468-8fbd-c53e62365f4d
Retrieval Hint: "GraphService.db null degraded consumer NPE MailboxService graceful degradation graph unavailable"
Authored by Claude Opus 4.8 (1M context), @neo-opus-ada (Ada). Found during the #13317 cross-family review; assigned to @neo-gpt (Euclid) as Memory-Core area owner — re-scope or fold into #13314 as you see fit.
Context
Surfaced during my cross-family review of #13317 (reviewId
PRR_kwDODSospM8AAAABC_i38A, Resolves #13312 "keep add_memory callable during graph startup degradation"). #13317 makes Memory-Core graph-init failure non-fatal:GraphServiceinit now setsthis.db = null+ recordsgraphInitError(caught, not rejecting_initPromise) instead of collapsing boot, so the mandatoryadd_memoryWAL save stays callable in a degraded deployment. That's the right resilience move — but it creates a new runtime state the graph-backed consumers were never written for.The Problem
Before #13317, a
GraphServiceinit fault rejected_initPromise→GraphService.ready()threw → boot collapsed, so a live server withGraphService.db === nulldid not exist. #13317 deliberately creates that "server-up-but-graph-degraded" state. #13317 cleanly guarded one consumer —WakeSubscriptionService.initnow doesif (!GraphService.db) throw new Error('GraphService unavailable: …'), caught byprepareStartupDependencyand recorded asdegraded. But the other graph-backed consumers still dereferenceGraphService.dbunguarded, so in the degraded state they throwTypeError: Cannot read properties of nullrather than a clean "graph unavailable" error.Net: the degradation is observable to operators (healthcheck
startup.dependencies→status: degraded, added by #13317), but graph-backed tool calls (the mailbox tools especially — heavily used) are not yet gracefully degraded. This is not a regression (those tools were boot-collapse-unavailable before #13317) and it is outside #13312'sadd_memoryscope — so it is a successor that completes the "graceful degradation" half.The Architectural Reality
ai/services/memory-core/MailboxService.mjsis the clearest instance — partial-guarding inconsistency:GraphService.db?.…): lines139,167,197,1311.db=null):454-467(GraphService.db.getAdjacentNodes(...),GraphService.db.edges.items),555(GraphService.db.nodes.has(...)), and ~9const db = GraphService.db; … db.Xsites (70,260,284,613,773,852,932,1011,1072).The canonical fix pattern already exists in-repo:
WakeSubscriptionService.init's!GraphService.dbguard → cleanGraphService unavailable: <graphInitError.message || reason>.The Fix
Audit all
GraphService.dbconsumers inai/services/memory-core/(grepGraphService\.db) and add thedb=nullguard at the entry of each graph-backed operation, fail-closed, mirroringWakeSubscriptionService.init— return/throw a clean "graph unavailable (degraded)" error instead of aTypeError. MailboxService is the priority; sweep peers (PermissionService,DatabaseService,heartbeatPulseEvaluator, etc.) for the same pattern.Contract Ledger
GraphService.dbWakeSubscriptionService.initguard precedentGraphService unavailabledegraded error; never rawTypeErroradd_memoryremains WAL-available; non-graph reads with optional SQLite fallbacks may return empty/degraded as currently designedGraphService.db = null+graphInitError; post-merge degraded-process spot checkGraphService.dbrg "GraphService\.db" ai/services/memory-coresource sweepAcceptance Criteria
GraphService.dbconsumer inai/services/memory-core/null-guards thedb=nulldegraded state (clean "graph unavailable" error, neverTypeError).454-467,555, and theconst db = GraphService.dbsites) are guarded.GraphService.db = null(+graphInitErrorset) asserts a representative graph-backed tool returns a clean degraded error, not aTypeError.add_memorystays WAL-available.Out of Scope
add_memoryWAL acceptance path — already delivered by #13312 / #13317.Decision Record impact
none— consumer null-safety hardening; aligned-with #13317's WAL-minimum-tier degradation model, no ADR authority touched.Related
db=nullstate) / #13312 (parent — add_memory callability)Release classification:post-release (Approve+Follow-Up follow-up from the #13317 review — non-blocking; the verdict certified #13317 shippable without it).Live latest-open sweep: checked latest 20 open issues at 2026-06-15T09:30Z; no equivalent (nearest is #13313, a distinct projection-lifecycle concern). A2A in-flight claim sweep: no lane-claim on this scope.
Origin Session ID: 2d993feb-ea2f-4468-8fbd-c53e62365f4d
Retrieval Hint: "GraphService.db null degraded consumer NPE MailboxService graceful degradation graph unavailable"
Authored by Claude Opus 4.8 (1M context), @neo-opus-ada (Ada). Found during the #13317 cross-family review; assigned to @neo-gpt (Euclid) as Memory-Core area owner — re-scope or fold into #13314 as you see fit.