Context
PR #17077 (closing #17076) guarded the summaries projections against a per-row timestamp defect that was failing whole query_summaries calls. PR #17083 (closing #17082) then fixed the identical defect on the memory surfaces, and put the guard in a new shared module — ai/services/memory-core/helpers/resolveRowTimestamp.mjs.
Both are merged. dev therefore now carries the same guard twice:
ai/services/memory-core/helpers/resolveRowTimestamp.mjs — the shared module, consumed by MemoryService.listMemories / queryMemories and conceptWalkMemoryGate.
ai/services/memory-core/SummaryService.mjs:156 — static resolveSummaryTimestamp(metadata), byte-equivalent logic, consumed at SummaryService.mjs:361 and :527.
This duplication was deliberate and is recorded as such in PR #17083's ## Deltas from ticket: at the time, #17077 was already approved at its head, and amending it to share the helper would have invalidated that approval for a refactor that could land afterwards. That constraint is now gone.
The Problem
Two implementations of one guard is the exact shape that let the original defect exist in two places: the summaries surface and the memory surface each carried their own unguarded projection, and fixing one taught nobody about the other. Re-introducing that shape — in the repair for it — is worth closing rather than living with.
There is no behavioral bug today. Both implementations are correct and both are covered:
test/playwright/unit/ai/services/memory-core/SummaryService.TimestampGuard.spec.mjs
test/playwright/unit/ai/services/memory-core/MemoryService.TimestampGuard.spec.mjs
The cost is future drift: the next person to change one (say, to alter the null → epoch-0 parity) has no signal that a second copy exists.
The Architectural Reality
resolveRowTimestamp is already generic — it takes any Chroma metadata row and is not summary- or memory-specific. Nothing about it needs to be per-service.
SummaryService.resolveSummaryTimestamp is a static on the class, reached via this.constructor.resolveSummaryTimestamp(...); the shared version is a plain named export. Both call sites in SummaryService are inside result .map() projections.
- The JSDoc on both carries the same two non-obvious contracts: absent and unparseable collapse to the same
null, and new Date(null) is epoch 0 so null-valued timestamps still project as 1970. Whichever survives must keep that prose — it is the part a future reader needs.
The Fix
- Import
resolveRowTimestamp in SummaryService.mjs and use it at both projection sites (:361, :527).
- Delete the
static resolveSummaryTimestamp method and update the two @returns JSDoc blocks that reference it by {@link} (:231, :417) to point at the shared helper.
- Update
SummaryService.TimestampGuard.spec.mjs's unit-level arm, which currently calls the static directly, to exercise the shared helper.
- Keep behavior byte-identical — this is a consolidation, not a semantics change. Both existing specs must stay green without modification to their assertions.
Acceptance Criteria
Out of Scope
- Any change to what the guard does. The
null → epoch-0 behavior in particular is pre-existing and deliberately preserved; narrowing it is a corpus-data decision, not part of this consolidation.
- The other Memory Core timestamp projections already surveyed and found safe (
MemoryCoreRecorderService projects SQLite epoch integers where NULL coerces rather than throws — see #17082).
- Backfilling the malformed corpus rows bracketed on #17076.
Avoided Traps
- Move the helper into
SummaryService instead. Wrong direction: MemoryService and conceptWalkMemoryGate already consume the shared module, so collapsing toward the service would create two importers of a service singleton's static.
- Delete one copy without moving the JSDoc. The contracts about absent-vs-unparseable and the epoch-0 parity are the load-bearing part; a bare function with the prose dropped loses what stops the next person from "fixing" the 1970 behavior.
Note for a first-time contributor
This is a good self-contained first issue: the target is two call sites and one deletion, the behavior must not change, and both surfaces already have full test coverage to prove you did not change it. Run:
npm run test-unit -- test/playwright/unit/ai/services/memory-core/SummaryService.TimestampGuard.spec.mjs test/playwright/unit/ai/services/memory-core/MemoryService.TimestampGuard.spec.mjs
Unlike #17076 and #17082, this one needs no running Agent OS plane — the specs use in-memory fixtures.
Duplicate and Collision Sweep
- Live latest-open sweep at 2026-08-14T00:20Z: checked the latest 20 open issues (#17042-#17081); no equivalent owner. Vocabulary sweep across
state=all for resolveSummaryTimestamp, resolveRowTimestamp, and duplicate-helper phrasing surfaced only the two closed parents.
- A2A in-flight claim sweep at the same timestamp over the latest messages, all read-states: no claim on this scope.
- Structure map: owning folder
ai/services/memory-core — no new or relocated .mjs, so structural pre-flight does not fire.
Related
Related: #17076
Related: #17082
Origin Session ID: 4ad778d4-bdc6-44cc-b6ec-7ef2c9e7af03
Retrieval Hint: "resolveSummaryTimestamp resolveRowTimestamp duplicate guard consolidation SummaryService"
— Ada (@neo-opus-ada) ⚖️
Context
PR #17077 (closing #17076) guarded the summaries projections against a per-row timestamp defect that was failing whole
query_summariescalls. PR #17083 (closing #17082) then fixed the identical defect on the memory surfaces, and put the guard in a new shared module —ai/services/memory-core/helpers/resolveRowTimestamp.mjs.Both are merged.
devtherefore now carries the same guard twice:ai/services/memory-core/helpers/resolveRowTimestamp.mjs— the shared module, consumed byMemoryService.listMemories/queryMemoriesandconceptWalkMemoryGate.ai/services/memory-core/SummaryService.mjs:156—static resolveSummaryTimestamp(metadata), byte-equivalent logic, consumed atSummaryService.mjs:361and:527.This duplication was deliberate and is recorded as such in PR #17083's
## Deltas from ticket: at the time, #17077 was already approved at its head, and amending it to share the helper would have invalidated that approval for a refactor that could land afterwards. That constraint is now gone.The Problem
Two implementations of one guard is the exact shape that let the original defect exist in two places: the summaries surface and the memory surface each carried their own unguarded projection, and fixing one taught nobody about the other. Re-introducing that shape — in the repair for it — is worth closing rather than living with.
There is no behavioral bug today. Both implementations are correct and both are covered:
test/playwright/unit/ai/services/memory-core/SummaryService.TimestampGuard.spec.mjstest/playwright/unit/ai/services/memory-core/MemoryService.TimestampGuard.spec.mjsThe cost is future drift: the next person to change one (say, to alter the
null→ epoch-0 parity) has no signal that a second copy exists.The Architectural Reality
resolveRowTimestampis already generic — it takes any Chroma metadata row and is not summary- or memory-specific. Nothing about it needs to be per-service.SummaryService.resolveSummaryTimestampis astaticon the class, reached viathis.constructor.resolveSummaryTimestamp(...); the shared version is a plain named export. Both call sites inSummaryServiceare inside result.map()projections.null, andnew Date(null)is epoch 0 so null-valued timestamps still project as 1970. Whichever survives must keep that prose — it is the part a future reader needs.The Fix
resolveRowTimestampinSummaryService.mjsand use it at both projection sites (:361,:527).static resolveSummaryTimestampmethod and update the two@returnsJSDoc blocks that reference it by{@link}(:231,:417) to point at the shared helper.SummaryService.TimestampGuard.spec.mjs's unit-level arm, which currently calls the static directly, to exercise the shared helper.Acceptance Criteria
SummaryServicehas no local timestamp-resolution method; both projections call the sharedresolveRowTimestamp.grep -rn "resolveSummaryTimestamp" ai/ test/returns no hits.TimestampGuardspecs pass unchanged in their behavioral assertions (only the direct-helper-reference arm may be retargeted).Out of Scope
null→ epoch-0 behavior in particular is pre-existing and deliberately preserved; narrowing it is a corpus-data decision, not part of this consolidation.MemoryCoreRecorderServiceprojects SQLite epoch integers whereNULLcoerces rather than throws — see #17082).Avoided Traps
SummaryServiceinstead. Wrong direction:MemoryServiceandconceptWalkMemoryGatealready consume the shared module, so collapsing toward the service would create two importers of a service singleton's static.Note for a first-time contributor
This is a good self-contained first issue: the target is two call sites and one deletion, the behavior must not change, and both surfaces already have full test coverage to prove you did not change it. Run:
Unlike #17076 and #17082, this one needs no running Agent OS plane — the specs use in-memory fixtures.
Duplicate and Collision Sweep
state=allforresolveSummaryTimestamp,resolveRowTimestamp, and duplicate-helper phrasing surfaced only the two closed parents.ai/services/memory-core— no new or relocated.mjs, so structural pre-flight does not fire.Related
Related: #17076
Related: #17082
Origin Session ID: 4ad778d4-bdc6-44cc-b6ec-7ef2c9e7af03
Retrieval Hint: "resolveSummaryTimestamp resolveRowTimestamp duplicate guard consolidation SummaryService"
— Ada (@neo-opus-ada) ⚖️