Skip to content

feat(agent-memory): Phase 6 — consolidate()#254

Merged
jamby77 merged 3 commits into
masterfrom
feature/agent-memory-phase6-consolidate
Jun 21, 2026
Merged

feat(agent-memory): Phase 6 — consolidate()#254
jamby77 merged 3 commits into
masterfrom
feature/agent-memory-phase6-consolidate

Conversation

@jamby77

@jamby77 jamby77 commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #253 (Phase 5 — eviction & TTL).

What

Phase 6 of @betterdb/agent-memory: consolidation — summarize many old/low-value memories into fewer durable ones. Callable, not scheduled (per spec).

consolidate(options):

  • Selects candidates in scope (threadId/agentId/namespace + tags) filtered by olderThanSeconds and maxImportance.
  • Calls the caller-supplied summarize(items) => Promise<string> (the library never hard-codes an LLM — mirrors the judge-injection pattern).
  • Writes the result as a normal memory with source: 'summary' at summaryImportance (default 0.7), scoped/tagged to the request.
  • With deleteSources (default true) removes the originals; returns { consolidated, created, deleted }.

Design / review notes

  • Write-before-delete: the summary is written before sources are deleted, so a failure can never destroy memories without leaving their consolidated replacement.
  • Mass-delete guard: requires at least one selection criterion (scope, tags, olderThanSeconds, or maxImportance) — a criteria-less consolidate({summarize}) would otherwise summarize+delete the entire store. Mirrors forgetByScope's guard.
  • Efficiency: the candidate scan uses an explicit RETURN of only the fields parseMemoryItem needs, so it never transfers vector blobs.
  • Bounded scan (CONSOLIDATE_SCAN_LIMIT); a single summarize() over a whole huge scope is impractical anyway, and the remainder consolidates on subsequent calls.

Tests (MemoryStore.consolidate.test.ts, 8)

candidate summarize+write+delete+counts · olderThanSeconds filter · maxImportance filter · summary scope+summaryImportance · deleteSources:false keeps sources · nothing-matches → zeros & no summarize/write · no-criteria guard throws · defaults (0.7 / delete).

55/55 package tests green · tsc --noEmit clean · prettier clean.


Note

Medium Risk
Bulk delete and summary writes touch persistent agent memory; safeguards (criteria guard, write-before-delete, summary exclusion) reduce data-loss risk but misconfigured callers could still remove large candidate sets.

Overview
Adds MemoryStore.consolidate() so callers can merge many scoped, low-value or old memories into one durable summary via an injected summarize(items) callback (no built-in LLM).

Candidates are selected with FT.SEARCH using a new buildConsolidateFilter (scope/tags plus server-side created_at / importance bounds, and -@source:{summary} so prior summaries are not re-folded). The flow writes the summary first (source: 'summary', default importance 0.7), optionally deletes sources (default on), and returns { consolidated, created, deleted }. remember is refactored through a private writeMemory path so consolidation skips enforceCapacity and cannot evict the new summary while sources still count toward the cap. A criteria guard blocks unscoped whole-store runs.

Public ConsolidateOptions / ConsolidateResult are exported; tests cover filters, defaults, deleteSources: false, empty matches, and the capacity bypass.

Reviewed by Cursor Bugbot for commit f834211. Bugbot is set up for automated code reviews on this repo. Configure here.

@jamby77 jamby77 force-pushed the feature/agent-memory-phase5-eviction branch from 6e7c2e9 to 2dc4339 Compare June 18, 2026 06:57
@jamby77 jamby77 force-pushed the feature/agent-memory-phase6-consolidate branch from 5cdeb4c to c317978 Compare June 18, 2026 06:57
Comment thread packages/agent-memory/src/MemoryStore.ts
@jamby77 jamby77 force-pushed the feature/agent-memory-phase5-eviction branch from 2dc4339 to e4646ec Compare June 18, 2026 07:19
@jamby77 jamby77 force-pushed the feature/agent-memory-phase6-consolidate branch 2 times, most recently from d5a95b9 to e27477f Compare June 18, 2026 07:28

@KIvanow KIvanow left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phase 6 reads well: write-before-delete is genuinely safe, the mass-delete guard mirrors forgetByScope, and keeping summarize injected is the right call. Two things I would like addressed before merging:

1. Push olderThanSeconds and maxImportance into the query instead of filtering after the scan. Right now the FT.SEARCH uses only buildScopeFilter, fetches up to CONSOLIDATE_SCAN_LIMIT in index order, and then isConsolidationCandidate filters in JS. Both fields are indexed NUMERIC, so they can go straight into the filter as @created_at:[-inf (cutoff] and @importance:[-inf max]. As written, a scope larger than the scan limit returns an arbitrary first window and filters that, so matching old or low-importance candidates beyond the window are never selected even though a range query would find them. It also pulls content for rows that are then discarded. Pushing the predicates down makes the cap apply to actual matches and trims the transfer.

2. The scan does not exclude prior summaries, so default runs fold summaries into summaries. Because there is no @source:{summary} exclusion, a default consolidate({ summarize, threadId }) with no maxImportance re-selects the previous summary (importance 0.7) and deletes it into a new one. A maxImportance below 0.7 happens to protect them, but only implicitly. If rolling re-consolidation is intended, it is worth documenting; if not, the candidate scan should exclude @source:{summary}.

@jamby77 jamby77 force-pushed the feature/agent-memory-phase5-eviction branch from a820313 to ad379d5 Compare June 19, 2026 09:51
@jamby77 jamby77 force-pushed the feature/agent-memory-phase6-consolidate branch from e27477f to 6864f6b Compare June 19, 2026 09:51
@jamby77

jamby77 commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

@KIvanow Addressed in 6864f6b: (1) olderThanSeconds/maxImportance are now NUMERIC range clauses in the FT.SEARCH filter, so the scan limit applies to actual matches (not an arbitrary window) and content isn't transferred for discarded rows; (2) the scan excludes -@source:{summary}, so a default run no longer re-folds prior summaries (target them explicitly to do so). Extracted buildConsolidateFilter; tests assert the pushed-down predicates + exclusion.

@jamby77 jamby77 requested a review from KIvanow June 19, 2026 09:52
@jamby77 jamby77 force-pushed the feature/agent-memory-phase5-eviction branch from ad379d5 to 7ee56df Compare June 19, 2026 10:01
@jamby77 jamby77 force-pushed the feature/agent-memory-phase6-consolidate branch from 6864f6b to 3ee71c8 Compare June 19, 2026 10:01

@KIvanow KIvanow left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, both points are addressed: the NUMERIC range clauses (@created_at:[-inf ...], @importance:[-inf ...]) are faithful to the old isConsolidationCandidate semantics, candidates now come straight from the response, and -@source:{summary} stops the default re-folding. I also confirmed it never degenerates into a pure-negation query (the criteria guard always contributes a positive clause) and joinClauses never returns * here, so the mass-delete guard stays intact.

One small wording fix before I approve: the commit message and the code comment both say prior summaries can be re-consolidated by "targeting them explicitly," but MemoryStore always passes excludeSource: SUMMARY_SOURCE, so -@source:{summary} is unconditional and there is no exposed way to include summaries. The behavior is fine (unconditional exclusion fully resolves the accidental-folding concern), but the comment promises an option that does not exist. Please either drop the "target them explicitly" phrasing or actually expose the toggle.

@jamby77 jamby77 force-pushed the feature/agent-memory-phase6-consolidate branch from 3ee71c8 to 1b71d3e Compare June 19, 2026 13:10

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1b71d3e. Configure here.

Comment thread packages/agent-memory/src/MemoryStore.ts
@jamby77

jamby77 commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

@KIvanow Fixed in 1b71d3e: dropped the misleading 'target them explicitly' phrasing from the code comment, the commit message, and the PR description — the exclusion is unconditional (-@source:{summary}) and there's no toggle, as you noted. Behavior unchanged.

@jamby77 jamby77 requested a review from KIvanow June 19, 2026 13:17

@KIvanow KIvanow left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified at 384e10b. The outstanding wording fix is resolved: the "target them explicitly" phrasing is gone from the code comment, commit message, and PR description, and the consolidate comment now correctly states prior summaries are always excluded via -@source:{summary} with no toggle implied. The earlier predicate push-down (NUMERIC range clauses for olderThanSeconds/maxImportance) and the summary exclusion both hold.

384e10b is a good catch on top: writing the summary through remember() let enforceCapacity evict the just-written summary while the sources still inflated the scope, then the sources were deleted, losing the consolidated content. Routing consolidate through the new capacity-free writeMemory() (shared with remember(), which keeps its best-effort enforceCapacity) fixes it, and the reasoning holds since consolidation is a net reduction so the cap stays honored without a pass. Write-before-delete ordering is preserved.

Non-blocking minor: there does not appear to be a test covering the capacity-free consolidate path (consolidate with maxItemsPerScope set should keep the summary rather than drop it). Worth adding to lock in the 384e10b behavior, but not a blocker.

Approving.

@jamby77 jamby77 force-pushed the feature/agent-memory-phase5-eviction branch from 7ee56df to 2c6482d Compare June 21, 2026 14:31
Base automatically changed from feature/agent-memory-phase5-eviction to master June 21, 2026 14:35
jamby77 added 3 commits June 21, 2026 17:35
- consolidate() summarizes old/low-importance memories into one durable
  source:'summary' memory via a caller-supplied summarize(items) callback
- Select candidates by scope/tags + olderThanSeconds + maxImportance;
  write the summary before deleting sources so a failure never destroys
  memories without leaving the replacement
- deleteSources defaults true; summaryImportance defaults 0.7
- Require at least one selection criterion to prevent whole-store
  consolidation (mirrors forgetByScope's mass-delete guard)
- Fetch only the fields parseMemoryItem needs (no vector blobs) on the
  candidate scan
- Add ConsolidateOptions/ConsolidateResult types
…p summaries

- olderThanSeconds/maxImportance are now NUMERIC range clauses in the
  FT.SEARCH filter, so the scan limit applies to actual matches instead of an
  arbitrary first window, and content isn't transferred for discarded rows
- the candidate scan always excludes @source:{summary}, so a run never
  re-folds prior summaries into a new summary
- extract buildConsolidateFilter (shares scope-clause logic with buildScopeFilter)
With maxItemsPerScope set, consolidate wrote the summary via remember(), whose
enforceCapacity could evict that very summary while the sources still inflated
the scope — then the sources were deleted, losing the consolidated content
entirely. Write the summary via a capacity-free path; consolidation is a net
reduction, so the cap stays honored without a pass.
@jamby77 jamby77 force-pushed the feature/agent-memory-phase6-consolidate branch from 384e10b to f834211 Compare June 21, 2026 14:35
@jamby77 jamby77 merged commit af6542f into master Jun 21, 2026
3 checks passed
@jamby77 jamby77 deleted the feature/agent-memory-phase6-consolidate branch June 21, 2026 14:38
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants