Skip to content

feat(agent-memory): Phase 1 — remember() write path#248

Open
jamby77 wants to merge 2 commits into
masterfrom
feature/agent-memory-phase1-remember
Open

feat(agent-memory): Phase 1 — remember() write path#248
jamby77 wants to merge 2 commits into
masterfrom
feature/agent-memory-phase1-remember

Conversation

@jamby77

@jamby77 jamby77 commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Agent Memory — Phase 1: remember() write path

Stacked on #247 (Phase 0) — base is the Phase 0 branch.

What's new

  • MemoryStore.remember(content, options?) → embeds content (once), HSETs the {name}:mem:{id} hash with content, encoded vector, scope fields (threadId/agentId/namespace), importance (default 0.5), tags (CSV), source, created_at/last_accessed_at, access_count=0; returns the id.
  • Dimension validation on first write — a later embedding with a mismatched length throws (mirrors semantic-cache).
  • Pure buildMemoryRecord() extracted and unit-tested standalone.
  • Memory types exported: EmbedFn, MemoryScope, RememberOptions, MemoryStoreOptions.

Tests

9 unit tests (mocked client + deterministic fakeEmbed); tsc --noEmit + prettier clean.

Review-driven changes

  • Reject commas in tags — tags are stored CSV and indexed as TAG fields (separator ,), so a comma inside a tag would silently break Phase 2 filtering.
  • Omit tags when empty (consistent with the other optional fields) rather than writing ''.
  • Trimmed RememberOptions to only implemented fields — removed ttl/metadata so the API doesn't advertise options Phase 1 doesn't honor (they return with their phases: ttl → Phase 5).

Deferred

  • Concurrent first-write dims race — low-likelihood (needs a non-deterministic embed dim) and superseded in Phase 2 once the FT index's dimension becomes authoritative.

Next

Phase 2 (recall() ranking — composite score pure-math first, then KNN) stacks on this.


Note

Low Risk
Additive write-only API with input validation and mocked tests; no auth or recall path yet, though bad embedFn dimensions or Valkey failures would surface at runtime.

Overview
Implements the Phase 1 long-term memory write path: MemoryStore is wired with a Valkey client, store name, and embedFn, and remember(content, options?) embeds text once, persists a {name}:mem:{uuid} hash via HSET, and returns the new id.

Record shape is built by a pure buildMemoryRecord() helper: content, float32-encoded vector, default importance 0.5 (validated to [0, 1]), timestamps, access_count 0, and optional scope/metadata (threadId, agentId, namespace, source, comma-joined tags). Empty optionals are omitted; tags with commas are rejected so CSV/TAG indexing stays safe. The store tracks embedding dimension on first write and throws on later mismatches (same idea as semantic-cache).

Public types EmbedFn, MemoryStoreClient, RememberOptions, and MemoryStoreOptions are exported from the package entrypoint. Unit tests cover remember (mock client) and buildMemoryRecord validation/defaults.

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

@jamby77 jamby77 force-pushed the feature/agent-memory-phase0-scaffold branch from ffc0fd3 to cecbb45 Compare June 18, 2026 06:53
@jamby77 jamby77 force-pushed the feature/agent-memory-phase1-remember branch from fe9da3d to b433d60 Compare June 18, 2026 06:57

@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.

One thing I think needs fixing (let me know if it's already addressed in the follow-up PRs).

importance is caller input but written unvalidated. buildMemoryRecord does String(options.importance ?? 0.5) with no [0, 1] clamp and no finiteness check, even though the type implies 0..1 and Phase 2 multiplies it straight into the composite score (weights.importance * importance). The failure chain is quiet: a caller passing importance: NaN/Infinity/out-of-range gets it stored verbatim into a NUMERIC-indexed field, then at recall parseFloat yields NaN, the composite score becomes NaN, and Phase 2's Number.isFinite guard silently drops the item. So a bad importance value makes the memory unrecallable with no error, or for a large finite value lets importance dominate ranking. Since this is a caller-supplied value at a system boundary, worth clamping (or validating and throwing) at write time rather than trusting it.

@jamby77

jamby77 commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

@KIvanow Fixed in 6bc85ee: buildMemoryRecord now rejects an importance that isn't a finite number in [0, 1] at the write boundary (matching the tag/embedFn guards), so a bad value can't silently poison recall. Added tests for the bounds + non-finite/out-of-range rejection.

@jamby77 jamby77 requested a review from KIvanow June 19, 2026 09:52
Base automatically changed from feature/agent-memory-phase0-scaffold to master June 19, 2026 09:58
jamby77 added 2 commits June 19, 2026 13:00
- MemoryStore.remember(): embed content, HSET {name}:mem:{id} hash with
  content, encoded vector, scope fields, importance (default 0.5), tags csv,
  source, created_at/last_accessed_at, access_count=0; returns the id
- Validate embedding dimension on first write; mismatch throws
- Extract pure buildMemoryRecord() and unit-test it standalone
- Export memory types (EmbedFn, MemoryScope, RememberOptions, MemoryStoreOptions)
… write

A caller-supplied importance was stored verbatim, so NaN/Infinity or an
out-of-range value silently poisoned recall: the composite score became NaN
and the memory was dropped, or a large value dominated ranking. Reject it at
the write boundary, matching the package's other input guards.
@jamby77 jamby77 force-pushed the feature/agent-memory-phase1-remember branch from 6bc85ee to a60f871 Compare June 19, 2026 10:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants