LearnNewsExamplesServices
Frontmatter
id17082
titleOne malformed memory row can fail whole query_raw_memories / get_session_memories calls
stateClosed
labels
bugairegressionagent-os
assigneesneo-opus-ada
createdAtAug 14, 2026, 1:42 AM
updatedAtAug 14, 2026, 2:18 AM
githubUrlhttps://github.com/neomjs/neo/issues/17082
authorneo-kimi-phoebe
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 14, 2026, 2:18 AM

One malformed memory row can fail whole query_raw_memories / get_session_memories calls

Closed Backlog/active-chunk-16 bugairegressionagent-os
neo-kimi-phoebe
neo-kimi-phoebe commented on Aug 14, 2026, 1:42 AM

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:1217listMemories (serves get_session_memories): timestamp: new Date(metadata.timestamp).toISOString() inside result.ids.map(...).
  • ai/services/memory-core/MemoryService.mjs:2400queryMemories (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:

  1. 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.
  2. 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.
  3. 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.
  4. 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

  • A query_raw_memories call whose result set contains at least one record with an absent or unparseable timestamp returns the well-formed records instead of a whole-call error.
  • The malformed-record count is observable on the returned envelope; rows are never silently dropped.
  • The same guard is applied at the listMemories projection (MemoryService.mjs:1217).
  • A regression test seeds a malformed record among well-formed ones and asserts surviving results plus the malformed count, through both paths, with a positive control that the seeded values throw pre-fix.
  • Legacy null-valued timestamps still project as epoch 0 (parity with the pre-guard behavior, pinned by test).

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) 🔆

tobiu referenced in commit e272e06 - "fix(ai): guard the memory-row timestamp projections (#17082) (#17083) on Aug 14, 2026, 2:18 AM
tobiu closed this issue on Aug 14, 2026, 2:18 AM