LearnNewsExamplesServices
Frontmatter
id17076
titleOne unparseable timestamp fails the whole query_summaries call
stateClosed
labels
bugairegressionmodel-experienceagent-os
assigneesneo-opus-ada
createdAtAug 14, 2026, 12:53 AM
updatedAtAug 14, 2026, 2:12 AM
githubUrlhttps://github.com/neomjs/neo/issues/17076
authorneo-opus-ada
commentsCount3
parentIssue17072
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 14, 2026, 1:54 AM

One unparseable timestamp fails the whole query_summaries call

Closed Backlog/active-chunk-15 bugairegressionmodel-experienceagent-os
neo-opus-ada
neo-opus-ada commented on Aug 14, 2026, 12:53 AM

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

  1. Throw site: ai/services/memory-core/SummaryService.mjs:485timestamp: new Date(metadata.timestamp).toISOString(), inside ids.map(...).
  2. 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.
  3. Duplicated projection: the identical unguarded expression exists at ai/services/memory-core/SummaryService.mjs:331, in listSummaries(). Same latent defect, different reach.
  4. The two paths differ only in how they fetch. listSummaries() reads via collection.get({ids}) — a bounded, ordered page. querySummaries() reads via collection.query().
  5. 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.
  6. 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

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

  • A query_summaries call whose result set contains at least one record with an absent or unparseable timestamp returns the well-formed records instead of SUMMARY_QUERY_ERROR.
  • The malformed-record count is observable by the caller on the returned envelope; it is not silently dropped.
  • The same guard is applied at the duplicated projection in listSummaries() (SummaryService.mjs:331), so get_all_summaries cannot acquire the defect by a change in reach.
  • A regression test seeds a malformed record alongside well-formed ones and asserts both the surviving results and the malformed count, through the query path and the id-slice path.
  • The malformed population is characterized in a ticket comment (count + the write path that produced it) before any backfill decision is recorded.
  • The existing QUERY_PATH_DEGRADED envelope still fires for genuine retrieval degradation and is distinguishable from the malformed-record path.
  • query_summaries returns results for an ordinary query against the live corpus.

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) ⚖️

tobiu referenced in commit 961269d - "fix(ai): guard the summary timestamp projection (#17076) (#17077) on Aug 14, 2026, 1:54 AM
tobiu closed this issue on Aug 14, 2026, 1:54 AM
tobiu added parent issue #17072 on Aug 14, 2026, 2:31 AM