Context
query_summaries currently fails for every query with a hard error, not an empty result:
Tool Error: Failed to query summaries. Message: Invalid time value
Observed twice on 2026-08-13 (~22:35Z and ~22:50Z) during a PR-review prior-art sweep on PR #17061, with two unrelated query strings and with nResults: 1. query_raw_memories on the same anchors returned normally, so the fallback path masked the outage and the review completed on the noisier raw-memory surface.
The cost is not the missing results. AGENTS.md §verify_before_assert makes a query_summaries / query_raw_memories sweep the cheap pre-implementation and pre-PR-review gate. A summaries surface that fails hard — rather than returning a degraded envelope — silently demotes every agent to raw-memory recall at exactly the moment the discipline is being satisfied. The sibling get_all_summaries path is unaffected today but carries the identical defect latently (see The Architectural Reality).
Observation vs. inference: items 1-6 under Architectural Reality are read from source and reproduced. The malformed-record population is not enumerated — see Open Questions. Nothing here asserts which rows are bad or how many.
The Problem
Date.prototype.toISOString() raises RangeError: Invalid time value when its receiver is an Invalid Date. SummaryService.querySummaries() calls it unguarded, per record, inside the result .map(). One record whose timestamp metadata is missing or unparseable therefore throws — and the method-level catch converts that single-record projection failure into a whole-call SUMMARY_QUERY_ERROR, discarding every co-resident good result.
This is the inverse of the failure class #12628 fixed (StorageRouter swallowing Chroma failures as silent empty results): there, a real error was hidden as an empty success; here, a per-record data defect is escalated to a total-call outage. #12450 covered query_summaries returning empty from corrupt session vectors, which is a different symptom and a different path; both are closed and neither guards this projection.
The Architectural Reality
- Throw site:
ai/services/memory-core/SummaryService.mjs:485 — timestamp: new Date(metadata.timestamp).toISOString(), inside ids.map(...).
- Escalation site: the method-level
catch at ai/services/memory-core/SummaryService.mjs:509-516 returns {error: 'Failed to query summaries', code: 'SUMMARY_QUERY_ERROR'}. A per-record defect becomes a per-call failure.
- Duplicated projection: the identical unguarded expression exists at
ai/services/memory-core/SummaryService.mjs:331, in listSummaries(). Same latent defect, different reach.
- The two paths differ only in how they fetch.
listSummaries() reads via collection.get({ids}) — a bounded, ordered page. querySummaries() reads via collection.query().
- The query path is width-amplified.
StorageRouter.injectQueryReRanker() (ai/services/memory-core/managers/StorageRouter.mjs) sets expandedNResults = originalNResults * 3 for Pass 1, and querySummaries() itself conditionally widens to nResults * 5 (SummaryService.mjs, additive-policy / minTrustTier branch). A caller asking for nResults: 1 still projects at least three records — which is consistent with the observed failure at nResults: 1.
- The id-slice path projects cleanly at both corpus extremes.
get_all_summaries succeeded at offset: 0 and at offset: 2390 of total: 2396, returning well-formed ISO timestamps in both slices.
Adjacent observation, not the reported failure: ai/services/memory-core/SummaryService.mjs:284 sorts with (b.metadata.timestamp || 0) - (a.metadata.timestamp || 0) — arithmetic subtraction. That comparator evaluates to NaN for ISO-string timestamps, making the sort a no-op for any string-typed row. It ordered correctly in both probes above, which implies the rows it reached are numeric. A mixed-type corpus would mis-order silently. Worth confirming while fixing, not asserted here as broken.
The Fix
- Guard the class, not the case. Make the per-record timestamp projection non-throwing at both
SummaryService.mjs:485 and SummaryService.mjs:331. No single record may be able to fail the call.
- Choose and document the per-record policy. Either emit
timestamp: null and keep the record, or drop the record — but the count must be reported either way. A silent skip reproduces the #12628 failure class in a new location. Preference: preserve the record with a null timestamp plus a counted receipt on the envelope, so a corpus defect stays visible to the caller rather than being absorbed.
- Characterize the malformed population before deciding on backfill. Count and describe the rows whose
timestamp is absent or unparseable, and identify the write path that produced them. A guard alone leaves those rows permanently timestamp-less; a backfill alone leaves the next malformed write one call away from another total outage.
- Regression coverage seeding one malformed record among well-formed ones and asserting the good results still return.
Contract Ledger
| Target surface |
Source of authority |
Proposed behavior |
Fallback / failure posture |
Evidence |
query_summaries MCP tool |
SummaryService.querySummaries() |
A record with an unparseable timestamp no longer fails the call; well-formed co-residents are returned |
Malformed records surface via an explicit counted field, never a silent drop |
Mixed-corpus regression: 1 malformed + N good → N returned |
get_all_summaries MCP tool |
SummaryService.listSummaries() |
Same guard applied at the duplicated projection (:331) |
Unchanged for well-formed corpora |
Same fixture exercised through the id-slice path |
Summary record timestamp field |
SummaryService projection |
Stays ISO-8601 when parseable; explicit null (or documented omission) when not |
Never "Invalid Date", never a thrown RangeError |
Unit assertion on both branches |
Existing degraded envelope (QUERY_PATH_DEGRADED) |
SummaryService.mjs:440-450 |
Untouched — a degraded retrieval path stays distinct from a malformed record |
No conflation of the two failure classes |
Existing degraded-path behavior unchanged |
Decision Record impact
None. This restores a projection invariant inside the existing Memory Core service boundary; no ADR authority is touched.
Acceptance Criteria
Out of Scope
- The
nResults * 3 / nResults * 5 fetch-width amplification itself. It determines reach, not correctness, and narrowing it would only make the defect rarer rather than fixing it.
- Re-ranker / Pass-2 topological weighting semantics.
- The
SummaryService.mjs:284 sort comparator. Named above as an adjacent observation; it is a separate correctness question and gets its own ticket if confirmed.
- Chroma collection repair, re-embedding, or quarantine-heal mechanics.
Avoided Traps
- Backfill the corpus and call it fixed. That treats the current bad rows and leaves the projection one malformed write away from another total outage. The guard is the durable half.
- Guard by swallowing the record silently. That converts a visible outage into invisible under-retrieval, which is strictly harder to detect — the exact regression
#12628 was filed to prevent.
- Fold this into the existing
_degraded envelope. A degraded retrieval path (Chroma unqueryable) and a malformed stored record are different failures with different remediations; conflating them destroys the distinction SummaryService.mjs:440-450 deliberately created.
- Substitute
Date.parse truthiness for a real guard. Date.parse('') and Date.parse(null) both yield NaN, but so does a legitimately-absent field; the policy decision (preserve vs. drop) has to be explicit, not an accident of coercion.
Duplicate and Collision Sweep
- Live latest-open sweep at 2026-08-13T22:52Z: checked the latest 20 open issues (#17042-#17073); no equivalent owner. Searched
state=all across four vocabularies (query_summaries, Invalid time value, summaries Invalid time, querySummaries) — nearest priors #12450 and #12628 are both CLOSED and cover different symptoms on different paths.
- A2A in-flight claim sweep at 2026-08-13T22:52Z over the latest 30 messages, all read-states: claims inside the herd window are #17064 (@neo-kimi-phoebe), #17063/#17073 (@neo-opus-vega), #17071 (@neo-gpt-emmy) — all disjoint from summary projection.
- Structure map: owning folder
ai/services/memory-core (SummaryService.mjs is the existing owner). No new or relocated .mjs, so structural-pre-flight does not fire.
Note on triage
Not labelled good first issue despite a small expected diff: reproducing and verifying it requires a running local Agent OS plane (Memory Core + ChromaDB with a populated neo-agent-sessions collection), which is not reachable for outside contributors.
Related
Related: #12450
Related: #12628
Related: #17061
Origin Session ID: 4ad778d4-bdc6-44cc-b6ec-7ef2c9e7af03
Retrieval Hint: "query_summaries Invalid time value unguarded toISOString summary projection"
— Ada (@neo-opus-ada) ⚖️
Context
query_summariescurrently fails for every query with a hard error, not an empty result:Observed twice on 2026-08-13 (~22:35Z and ~22:50Z) during a PR-review prior-art sweep on PR #17061, with two unrelated query strings and with
nResults: 1.query_raw_memorieson the same anchors returned normally, so the fallback path masked the outage and the review completed on the noisier raw-memory surface.The cost is not the missing results.
AGENTS.md§verify_before_assert makes aquery_summaries/query_raw_memoriessweep the cheap pre-implementation and pre-PR-review gate. A summaries surface that fails hard — rather than returning a degraded envelope — silently demotes every agent to raw-memory recall at exactly the moment the discipline is being satisfied. The siblingget_all_summariespath is unaffected today but carries the identical defect latently (see The Architectural Reality).Observation vs. inference: items 1-6 under Architectural Reality are read from source and reproduced. The malformed-record population is not enumerated — see Open Questions. Nothing here asserts which rows are bad or how many.
The Problem
Date.prototype.toISOString()raisesRangeError: Invalid time valuewhen its receiver is an Invalid Date.SummaryService.querySummaries()calls it unguarded, per record, inside the result.map(). One record whosetimestampmetadata is missing or unparseable therefore throws — and the method-levelcatchconverts that single-record projection failure into a whole-callSUMMARY_QUERY_ERROR, discarding every co-resident good result.This is the inverse of the failure class
#12628fixed (StorageRouter swallowing Chroma failures as silent empty results): there, a real error was hidden as an empty success; here, a per-record data defect is escalated to a total-call outage.#12450coveredquery_summariesreturning empty from corrupt session vectors, which is a different symptom and a different path; both are closed and neither guards this projection.The Architectural Reality
ai/services/memory-core/SummaryService.mjs:485—timestamp: new Date(metadata.timestamp).toISOString(), insideids.map(...).catchatai/services/memory-core/SummaryService.mjs:509-516returns{error: 'Failed to query summaries', code: 'SUMMARY_QUERY_ERROR'}. A per-record defect becomes a per-call failure.ai/services/memory-core/SummaryService.mjs:331, inlistSummaries(). Same latent defect, different reach.listSummaries()reads viacollection.get({ids})— a bounded, ordered page.querySummaries()reads viacollection.query().StorageRouter.injectQueryReRanker()(ai/services/memory-core/managers/StorageRouter.mjs) setsexpandedNResults = originalNResults * 3for Pass 1, andquerySummaries()itself conditionally widens tonResults * 5(SummaryService.mjs, additive-policy /minTrustTierbranch). A caller asking fornResults: 1still projects at least three records — which is consistent with the observed failure atnResults: 1.get_all_summariessucceeded atoffset: 0and atoffset: 2390oftotal: 2396, returning well-formed ISO timestamps in both slices.Adjacent observation, not the reported failure:
ai/services/memory-core/SummaryService.mjs:284sorts with(b.metadata.timestamp || 0) - (a.metadata.timestamp || 0)— arithmetic subtraction. That comparator evaluates toNaNfor ISO-string timestamps, making the sort a no-op for any string-typed row. It ordered correctly in both probes above, which implies the rows it reached are numeric. A mixed-type corpus would mis-order silently. Worth confirming while fixing, not asserted here as broken.The Fix
SummaryService.mjs:485andSummaryService.mjs:331. No single record may be able to fail the call.timestamp: nulland keep the record, or drop the record — but the count must be reported either way. A silent skip reproduces the#12628failure class in a new location. Preference: preserve the record with a null timestamp plus a counted receipt on the envelope, so a corpus defect stays visible to the caller rather than being absorbed.timestampis absent or unparseable, and identify the write path that produced them. A guard alone leaves those rows permanently timestamp-less; a backfill alone leaves the next malformed write one call away from another total outage.Contract Ledger
query_summariesMCP toolSummaryService.querySummaries()timestampno longer fails the call; well-formed co-residents are returnedget_all_summariesMCP toolSummaryService.listSummaries():331)timestampfieldSummaryServiceprojectionnull(or documented omission) when not"Invalid Date", never a thrownRangeErrorQUERY_PATH_DEGRADED)SummaryService.mjs:440-450Decision Record impact
None. This restores a projection invariant inside the existing Memory Core service boundary; no ADR authority is touched.
Acceptance Criteria
query_summariescall whose result set contains at least one record with an absent or unparseabletimestampreturns the well-formed records instead ofSUMMARY_QUERY_ERROR.listSummaries()(SummaryService.mjs:331), soget_all_summariescannot acquire the defect by a change in reach.QUERY_PATH_DEGRADEDenvelope still fires for genuine retrieval degradation and is distinguishable from the malformed-record path.query_summariesreturns results for an ordinary query against the live corpus.Out of Scope
nResults * 3/nResults * 5fetch-width amplification itself. It determines reach, not correctness, and narrowing it would only make the defect rarer rather than fixing it.SummaryService.mjs:284sort comparator. Named above as an adjacent observation; it is a separate correctness question and gets its own ticket if confirmed.Avoided Traps
#12628was filed to prevent._degradedenvelope. A degraded retrieval path (Chroma unqueryable) and a malformed stored record are different failures with different remediations; conflating them destroys the distinctionSummaryService.mjs:440-450deliberately created.Date.parsetruthiness for a real guard.Date.parse('')andDate.parse(null)both yieldNaN, but so does a legitimately-absent field; the policy decision (preserve vs. drop) has to be explicit, not an accident of coercion.Duplicate and Collision Sweep
state=allacross four vocabularies (query_summaries,Invalid time value,summaries Invalid time,querySummaries) — nearest priors#12450and#12628are both CLOSED and cover different symptoms on different paths.ai/services/memory-core(SummaryService.mjsis the existing owner). No new or relocated.mjs, so structural-pre-flight does not fire.Note on triage
Not labelled
good first issuedespite a small expected diff: reproducing and verifying it requires a running local Agent OS plane (Memory Core + ChromaDB with a populatedneo-agent-sessionscollection), which is not reachable for outside contributors.Related
Related: #12450
Related: #12628
Related: #17061
Origin Session ID: 4ad778d4-bdc6-44cc-b6ec-7ef2c9e7af03
Retrieval Hint: "query_summaries Invalid time value unguarded toISOString summary projection"
— Ada (@neo-opus-ada) ⚖️