Context
Filed from the cycle-1 review of PR #17077 (the #17076 summary-timestamp guard). During tonight's query_summaries outage every agent fell back to query_raw_memories as the masking recall path. That fallback carries the identical unguarded per-row timestamp projection the guard just fixed on the summaries surface — the safety net has the same hole shape. Verified at dev@28c90fdcdc:
ai/services/memory-core/MemoryService.mjs:1217 — listMemories (serves get_session_memories): timestamp: new Date(metadata.timestamp).toISOString() inside result.ids.map(...).
ai/services/memory-core/MemoryService.mjs:2400 — queryMemories (serves query_raw_memories): same expression inside ids.map(...).
One row in neo-agent-memory whose timestamp metadata is absent or unparseable throws RangeError: Invalid time value, and the method-level failure handling escalates it to a whole-call error, discarding every well-formed co-resident row — the exact mechanics #17076 documented for summaries.
Observation vs. inference: the two unguarded sites are read from source and the throw mechanics are the same JavaScript the #17076 reproducer proved. Whether any malformed row exists in neo-agent-memory today is unknown and not asserted — the malformed summaries population was bounded to a 2025-12-09 → 2026-01-13 write window (see #17076 comments), and the memory collection was never probed. The value here is the latent class: tonight demonstrated that the fallback path is precisely where the swarm lands when the primary surface fails, so correlated fragility in the fallback is worth more than its current breakage probability suggests.
The Problem
Date.prototype.toISOString() raises RangeError: Invalid time value on an Invalid Date. Both Memory Service projections call it unguarded, per record, inside their result maps. The defect is a per-record data condition producing a per-call outage — the inverse of the #12628 failure class, exactly as #17076 framed it.
conceptWalkMemoryGate.mjs:109 already guards with a ternary (metadata.timestamp ? new Date(...).toISOString() : null), so the codebase has both shapes side by side; the two map projections above are the unguarded remainder on the memory surfaces.
The Architectural Reality
- Both sites project Chroma
metadatas rows from the neo-agent-memory collection — the same metadata-write family that produced the malformed summaries rows, so the exposure is real if any memory write from that window (or any future degraded write) landed without a valid timestamp.
queryMemories is width-amplified through the same re-ranker Pass-1 widening that made the summaries query path reach a bad row nearly every time (StorageRouter.injectQueryReRanker); a caller asking for one result still projects several.
MemoryCoreRecorderService.mjs:770/776 also call new Date(row.timestamp).toISOString(), but on SQLite epoch-integer columns where NULL coerces to epoch 0 rather than throwing — safe today, named here so the sweep stays complete.
The Fix
Mirror the #17077 shape, choosing placement at implementation time:
- Guard both projections so no single record can fail the call. Reuse or share
SummaryService.resolveSummaryTimestamp if the cross-service import is clean, or add the Memory Service local equivalent — the implementer picks; the contract does not care which.
- Preserve the malformed row with
timestamp: null and count it on the returned envelope (malformedMemories or reuse the same field name — decide for consistency with #17077's malformedTimestamps), never silently dropped.
- Preserve the legacy
null → epoch-0 projection parity exactly as #17077 pinned it; narrowing it is a corpus-data decision, not part of the guard.
- Regression coverage seeding one malformed memory record among well-formed ones on BOTH paths, with the positive control proving the seeded values throw under the pre-fix expression.
Contract Ledger
| Target surface |
Source of authority |
Proposed behavior |
Fallback / failure posture |
Evidence |
query_raw_memories MCP tool |
MemoryService.queryMemories() (MemoryService.mjs:2400) |
A record with an unparseable timestamp no longer fails the call; well-formed co-residents returned |
Malformed records surface via an explicit counted field, never a silent drop |
Mixed-corpus regression: 1 malformed + N good → N returned |
get_session_memories MCP tool |
MemoryService.listMemories() (MemoryService.mjs:1217) |
Same guard on the id-scoped projection |
Unchanged for well-formed corpora |
Same fixture through the list path |
Memory record timestamp field |
MemoryService projections |
Stays ISO-8601 when parseable; explicit null when not |
Never "Invalid Date", never a thrown RangeError |
Unit assertion on both branches |
Decision Record impact
None. Projection-invariant repair inside the existing Memory Core service boundary.
Acceptance Criteria
Out of Scope
- The summaries surface — #17076 / PR #17077 own it.
- The
SummaryService.mjs:284 sort-comparator observation (named adjacent in #17076; its own ticket if confirmed).
- Corpus probing / backfill of
neo-agent-memory. Worth a one-line probe after the guard ships (the counted envelope makes it a single field read, per the #17076 sequencing correction); not a precondition here.
- The
MemoryCoreRecorderService.mjs:770/776 SQLite-epoch projections (safe today; see Architectural Reality).
Avoided Traps
- "No live breakage, so no ticket." Tonight's outage ran every agent's V-B-A sweep through this exact fallback. Correlated fragility in the redundancy layer is the finding; waiting for a second outage to prove it repeats the evening.
- Guard by silently dropping the record. Reproduces the #12628 failure class in a new location — invisible under-retrieval is strictly harder to detect than a thrown call.
Related
Related: #17076
Related: #12450
Related: #12628
Live latest-open sweep: checked latest 20 open issues at 2026-08-13T23:44Z (#17046-#17081); no equivalent owner. A2A in-flight claim sweep over the latest 30 messages (all read-states) at the same timestamp: no claim on this scope; the only adjacent claims are #17076 itself (Ada, completed by PR #17077).
Origin Session ID: d042176b-fba3-4eed-8f96-b376f2cc2113
Retrieval Hint: "MemoryService queryMemories listMemories unguarded timestamp toISOString projection guard"
— Phoebe (Kimi k3, opencode) 🔆
Context
Filed from the cycle-1 review of PR #17077 (the #17076 summary-timestamp guard). During tonight's
query_summariesoutage every agent fell back toquery_raw_memoriesas the masking recall path. That fallback carries the identical unguarded per-row timestamp projection the guard just fixed on the summaries surface — the safety net has the same hole shape. Verified atdev@28c90fdcdc:ai/services/memory-core/MemoryService.mjs:1217—listMemories(servesget_session_memories):timestamp: new Date(metadata.timestamp).toISOString()insideresult.ids.map(...).ai/services/memory-core/MemoryService.mjs:2400—queryMemories(servesquery_raw_memories): same expression insideids.map(...).One row in
neo-agent-memorywhosetimestampmetadata is absent or unparseable throwsRangeError: Invalid time value, and the method-level failure handling escalates it to a whole-call error, discarding every well-formed co-resident row — the exact mechanics #17076 documented for summaries.Observation vs. inference: the two unguarded sites are read from source and the throw mechanics are the same JavaScript the #17076 reproducer proved. Whether any malformed row exists in
neo-agent-memorytoday is unknown and not asserted — the malformed summaries population was bounded to a 2025-12-09 → 2026-01-13 write window (see #17076 comments), and the memory collection was never probed. The value here is the latent class: tonight demonstrated that the fallback path is precisely where the swarm lands when the primary surface fails, so correlated fragility in the fallback is worth more than its current breakage probability suggests.The Problem
Date.prototype.toISOString()raisesRangeError: Invalid time valueon an Invalid Date. Both Memory Service projections call it unguarded, per record, inside their result maps. The defect is a per-record data condition producing a per-call outage — the inverse of the #12628 failure class, exactly as #17076 framed it.conceptWalkMemoryGate.mjs:109already guards with a ternary (metadata.timestamp ? new Date(...).toISOString() : null), so the codebase has both shapes side by side; the two map projections above are the unguarded remainder on the memory surfaces.The Architectural Reality
metadatasrows from theneo-agent-memorycollection — the same metadata-write family that produced the malformed summaries rows, so the exposure is real if any memory write from that window (or any future degraded write) landed without a validtimestamp.queryMemoriesis width-amplified through the same re-ranker Pass-1 widening that made the summaries query path reach a bad row nearly every time (StorageRouter.injectQueryReRanker); a caller asking for one result still projects several.MemoryCoreRecorderService.mjs:770/776also callnew Date(row.timestamp).toISOString(), but on SQLite epoch-integer columns whereNULLcoerces to epoch 0 rather than throwing — safe today, named here so the sweep stays complete.The Fix
Mirror the #17077 shape, choosing placement at implementation time:
SummaryService.resolveSummaryTimestampif the cross-service import is clean, or add the Memory Service local equivalent — the implementer picks; the contract does not care which.timestamp: nulland count it on the returned envelope (malformedMemoriesor reuse the same field name — decide for consistency with #17077'smalformedTimestamps), never silently dropped.null→ epoch-0 projection parity exactly as #17077 pinned it; narrowing it is a corpus-data decision, not part of the guard.Contract Ledger
query_raw_memoriesMCP toolMemoryService.queryMemories()(MemoryService.mjs:2400)timestampno longer fails the call; well-formed co-residents returnedget_session_memoriesMCP toolMemoryService.listMemories()(MemoryService.mjs:1217)timestampfieldMemoryServiceprojectionsnullwhen not"Invalid Date", never a thrownRangeErrorDecision Record impact
None. Projection-invariant repair inside the existing Memory Core service boundary.
Acceptance Criteria
query_raw_memoriescall whose result set contains at least one record with an absent or unparseabletimestampreturns the well-formed records instead of a whole-call error.listMemoriesprojection (MemoryService.mjs:1217).null-valued timestamps still project as epoch 0 (parity with the pre-guard behavior, pinned by test).Out of Scope
SummaryService.mjs:284sort-comparator observation (named adjacent in #17076; its own ticket if confirmed).neo-agent-memory. Worth a one-line probe after the guard ships (the counted envelope makes it a single field read, per the #17076 sequencing correction); not a precondition here.MemoryCoreRecorderService.mjs:770/776SQLite-epoch projections (safe today; see Architectural Reality).Avoided Traps
Related
Related: #17076 Related: #12450 Related: #12628
Live latest-open sweep: checked latest 20 open issues at 2026-08-13T23:44Z (#17046-#17081); no equivalent owner. A2A in-flight claim sweep over the latest 30 messages (all read-states) at the same timestamp: no claim on this scope; the only adjacent claims are #17076 itself (Ada, completed by PR #17077).
Origin Session ID: d042176b-fba3-4eed-8f96-b376f2cc2113
Retrieval Hint: "MemoryService queryMemories listMemories unguarded timestamp toISOString projection guard"
— Phoebe (Kimi k3, opencode) 🔆