Context
During the 2026-05-20 Sandman run, the operator reported this failure chain while orphan-node cleanup was running:
TypeError: Cannot read properties of null (reading 'id')
at Store.getKey (src/data/Store.mjs:554:25)
at Store.splice (src/collection/Base.mjs:1483:45)
at Store.splice (ai/graph/Store.mjs:161:30)
at Store.remove (src/collection/Base.mjs:1396:14)
at Database.removeNode (ai/graph/Database.mjs:413:18)
at GraphService.removeNodes (ai/services/memory-core/GraphService.mjs:985-986)The immediate follow-up question identified a small framework-core guard gap:
isItem(value) {
return typeof value === 'object'
}In JavaScript, typeof null === 'object', so Neo.collection.Base#isItem(null) currently returns true.
The Problem
null is not a valid Collection item, but the helper that routes key-vs-item branches classifies it as one. That is a low-level footgun: any caller that passes null into a collection path can be routed into item-key resolution rather than the key/missing-value path, and subclasses such as Neo.data.Store can then dereference the null item inside getKey().
The Sandman failure is one observed symptom, but the fix belongs in the shared Collection helper because isItem() is the structural classifier used by collection methods and subclasses.
The Architectural Reality
src/collection/Base.mjs:1294-1296 currently implements isItem(value) as typeof value === 'object'.
- The inline comment is correct that
Neo.isObject() is too narrow because collections can store Neo instances and records.
- The missing invariant is only the JavaScript null edge case: a broad object/instance classifier still needs
value !== null.
- This is framework-core behavior (
Neo.collection.Base), not Memory Core specific. Sandman merely surfaced the bug path.
The Fix
Update Neo.collection.Base#isItem(value) to exclude null while preserving current support for plain objects, records, and Neo instances:
return value !== null && typeof value === 'object'
Add focused unit coverage proving isItem(null) === false while object/record/instance-like values remain item-like.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
Neo.collection.Base#isItem(value) |
Operator-reported Sandman stack + src/collection/Base.mjs current helper |
Return true for object-valued collection items, but false for null. |
Primitive keys remain non-items; callers can still resolve them through key paths. |
Existing method comment should be preserved or minimally clarified to mention the null guard. |
Unit test for null plus existing object behavior. |
Acceptance Criteria
Out of Scope
- Changing
Store.getKey() semantics beyond any test fixture needed for the guard.
- Broad cleanup of orphan-node pruning or Sandman graph maintenance.
- Reclassifying arrays/functions/primitives beyond the existing
isItem() contract.
Related
- Sandman failure surfaced through
GraphService.removeNodes() / Database.removeNode() cleanup.
- Prior key-resolution history: archived #9067 / #9069 around
Store.getKey() and collection identity routing.
Origin Session ID: d13c94dd-e721-4e28-ac9e-4d0b3c0f66de
Handoff Retrieval Hints: Collection.isItem null Store.getKey Sandman removeNodes, src/collection/Base.mjs isItem typeof value object, Cannot read properties of null reading id Store.getKey.
Context
During the 2026-05-20 Sandman run, the operator reported this failure chain while orphan-node cleanup was running:
TypeError: Cannot read properties of null (reading 'id') at Store.getKey (src/data/Store.mjs:554:25) at Store.splice (src/collection/Base.mjs:1483:45) at Store.splice (ai/graph/Store.mjs:161:30) at Store.remove (src/collection/Base.mjs:1396:14) at Database.removeNode (ai/graph/Database.mjs:413:18) at GraphService.removeNodes (ai/services/memory-core/GraphService.mjs:985-986)The immediate follow-up question identified a small framework-core guard gap:
isItem(value) { // We can not use Neo.isObject() || Neo.isRecord(), since collections can store neo instances too. return typeof value === 'object' }In JavaScript,
typeof null === 'object', soNeo.collection.Base#isItem(null)currently returnstrue.The Problem
nullis not a valid Collection item, but the helper that routes key-vs-item branches classifies it as one. That is a low-level footgun: any caller that passesnullinto a collection path can be routed into item-key resolution rather than the key/missing-value path, and subclasses such asNeo.data.Storecan then dereference the null item insidegetKey().The Sandman failure is one observed symptom, but the fix belongs in the shared Collection helper because
isItem()is the structural classifier used by collection methods and subclasses.The Architectural Reality
src/collection/Base.mjs:1294-1296currently implementsisItem(value)astypeof value === 'object'.Neo.isObject()is too narrow because collections can store Neo instances and records.value !== null.Neo.collection.Base), not Memory Core specific. Sandman merely surfaced the bug path.The Fix
Update
Neo.collection.Base#isItem(value)to excludenullwhile preserving current support for plain objects, records, and Neo instances:return value !== null && typeof value === 'object'Add focused unit coverage proving
isItem(null) === falsewhile object/record/instance-like values remain item-like.Contract Ledger Matrix
Neo.collection.Base#isItem(value)src/collection/Base.mjscurrent helpertruefor object-valued collection items, butfalsefornull.nullplus existing object behavior.Acceptance Criteria
Collection.isItem(null)returnsfalse.Out of Scope
Store.getKey()semantics beyond any test fixture needed for the guard.isItem()contract.Related
GraphService.removeNodes()/Database.removeNode()cleanup.Store.getKey()and collection identity routing.Origin Session ID:
d13c94dd-e721-4e28-ac9e-4d0b3c0f66deHandoff Retrieval Hints:
Collection.isItem null Store.getKey Sandman removeNodes,src/collection/Base.mjs isItem typeof value object,Cannot read properties of null reading id Store.getKey.