Skip to content

feat(ai, ai-memory): server-side memory middleware with pluggable adapters#541

Open
AlemTuzlak wants to merge 36 commits intomainfrom
feat/memory-middleware
Open

feat(ai, ai-memory): server-side memory middleware with pluggable adapters#541
AlemTuzlak wants to merge 36 commits intomainfrom
feat/memory-middleware

Conversation

@AlemTuzlak
Copy link
Copy Markdown
Contributor

@AlemTuzlak AlemTuzlak commented May 10, 2026

Summary

Adds first-class server-side memory support to chat() via a composable middleware plus a new @tanstack/ai-memory package shipping in-memory and Redis adapters.

  • @tanstack/ai/memory (new subpath) — memoryMiddleware, the MemoryAdapter contract (add/get/update/search/list/delete/clear with cursor pagination + TTL), MemoryOp ADD/UPDATE/DELETE union, and extension hooks: shouldRetrieve, rerank, shouldRemember, extractMemories, onToolResult, afterPersist. Per-request state isolated via WeakMap<ChatMiddlewareContext> so a single middleware instance is safe to reuse across concurrent tenants.
  • @tanstack/ai-memory (new package) — inMemoryMemoryAdapter() for dev/tests and redisMemoryAdapter({ redis }) for prod (with redis as an optional peer dep). Both pass a shared adapter contract suite covering scope isolation, expiry, pagination, kinds filtering, lexical-only and semantic ranking, and empty-scope safety.
  • Devtools events — five new entries on AIDevtoolsEventMap (memory:retrieve:started/completed, memory:persist:started/completed, memory:error) with 200-char preview caps; emits are fire-and-forget so telemetry never breaks chat.

Three agent skills ship with the packages (umbrella tanstack-ai-memory, plus per-adapter tanstack-ai-memory-in-memory and tanstack-ai-memory-redis). User-facing docs added at docs/middlewares/memory.md (concept) and docs/guides/memory-quickstart.md (task flow). Spec and design rationale at docs/superpowers/specs/2026-05-10-memory-middleware-design.md.

Designed as additive-friendly: deferred items (hierarchical scopes, multi-embedding spaces, RediSearch/RedisVL adapter, summarization/fact-extraction reference impls, useChat({ memory }) client sugar, per-iteration text persistence) can land later without breaking changes.

Test Plan

  • pnpm --filter @tanstack/ai test:lib (33 files, 785 tests, includes 17 memory middleware tests)
  • pnpm --filter @tanstack/ai-memory test:lib (in-memory + Redis contract suites, 48 tests total via ioredis-mock)
  • pnpm test:eslint
  • pnpm test:types
  • pnpm test:knip
  • pnpm test:sherif
  • pnpm test:docs (link checker)
  • pnpm build (all packages)
  • pnpm test:build (publint)
  • Manual smoke: wire inMemoryMemoryAdapter into an example app, send 3 turns with the same scope, confirm earlier turns surface in retrieved hits on turn 3
  • Manual smoke: swap to redisMemoryAdapter against a local Redis, confirm scoped records persist across process restarts and clear(scope) removes them

Summary by CodeRabbit

  • New Features

    • Server-side memory middleware with retrieval + deferred persistence, new public memory APIs, devtools events, and optional semantic embeddings
    • In-memory and Redis adapters for local dev and production; scope-based isolation for secure multi-tenant use
  • Documentation

    • Memory overview, quickstart, custom-adapter guide, and skill docs
  • Tests

    • Comprehensive adapter contract tests and middleware/unit tests added

Review Change Stack

AlemTuzlak added 21 commits May 10, 2026 18:27
- WeakMap-keyed per-request state to prevent cross-request leak
  when memoryMiddleware is reused (matches otel middleware pattern)
- scopeMatches treats empty scope as 'no match' to prevent
  clear({}) / search({scope:{}}) cross-tenant wipes
- Wrap deferred persist + tool-result writes so strict-mode failures
  surface via Promise.allSettled instead of being silently swallowed
- applyOps applies ops in array order; updates after adds in the
  same batch now find the inserted record
- shouldRemember gates the entire turn (including extractMemories)
  matching its documented JSDoc
- Add empty-scope safety tests to the shared adapter contract suite
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 10, 2026

📝 Walkthrough

Walkthrough

This PR adds server-side memory: memory types and helpers, a memoryMiddleware that retrieves and injects relevant memories at chat init and defers persistence at finish, in-memory and Redis adapters (with a node-redis wrapper), comprehensive tests (contract + middleware), package exports, build config, and documentation.

Changes

Memory System Implementation

Layer / File(s) Summary
Data Contracts & Types
packages/typescript/ai/src/memory/types.ts, packages/typescript/ai-event-client/src/index.ts
MemoryScope, MemoryRecord, MemoryAdapter, MemoryQuery/MemorySearchResult/MemoryListResult, MemoryOp, MemoryMiddlewareOptions, and five new devtools events (memory:retrieve:started/completed, memory:persist:started/completed, memory:error).
Scoring & Filtering Helpers
packages/typescript/ai/src/memory/helpers.ts
scopeMatches, cosine, lexicalOverlap, recencyScore, isExpired, defaultScoreHit (weights: 0.55 semantic, 0.20 lexical, 0.15 recency, 0.10 importance), and defaultRenderMemory.
Memory Middleware
packages/typescript/ai/src/memory/middleware.ts
memoryMiddleware(options) with per-request state, init-phase retrieval (paged search, optional embed/rerank, render+inject), buffering of tool-result ops, and deferred persistence on finish with shouldRemember, extractMemories, onToolResult, afterPersist, observability, and strict handling.
In-Memory Adapter
packages/typescript/ai-memory/src/adapters/in-memory.ts
inMemoryMemoryAdapter() using Map<string, MemoryRecord> with opportunistic expiry sweeping, scope filtering, scoring, sorting, and cursor-based pagination.
Redis Adapter
packages/typescript/ai-memory/src/adapters/redis.ts
redisMemoryAdapter({ redis, prefix? }) with RedisLike/NodeRedisLike types, nodeRedisAsRedisLike wrapper, escaped per-scope index keys, SCAN enumeration, JSON load skipping with one-time warn, expiry sweeps, scoring, and pagination.
Adapter Contract Tests
packages/typescript/ai-memory/tests/contract.ts, packages/typescript/ai-memory/tests/in-memory.test.ts, packages/typescript/ai-memory/tests/redis.test.ts
runMemoryAdapterContract(label, factory) validating add/get/update/search/list/delete/clear behavior, scope isolation, expiry filtering, cursor pagination, kinds filtering, lexical/semantic ranking, empty-scope safety, and scope-value escaping; adapter-specific tests for in-memory and Redis.
Helper & Middleware Tests
packages/typescript/ai/tests/memory/helpers.test.ts, packages/typescript/ai/tests/middlewares/memory.test.ts
Unit tests for helpers and extensive middleware integration tests covering retrieval injection, reranking, scope caching, persistence ordering, tool-result flows, observability, and strict vs non-strict errors.
Module Exports
packages/typescript/ai/src/memory/index.ts, packages/typescript/ai-memory/src/index.ts
Barrel exports exposing memory types, helpers, middleware, adapter implementations, wrapper utilities, and Redis-related types.
Package & Build Config
packages/typescript/ai-memory/package.json, packages/typescript/ai-memory/tsconfig.json, packages/typescript/ai-memory/vite.config.ts, packages/typescript/ai-memory/project.json, packages/typescript/ai/package.json, packages/typescript/ai/vite.config.ts, knip.json
New @tanstack/ai-memory package manifest and exports (adapter subpaths), TypeScript/Vite test config, project manifest, knip workspace ignore for optional Redis peers, and @tanstack/ai build entry addition for the memory barrel.

Documentation & User Guides

Layer / File(s) Summary
Overview & Navigation
docs/config.json, docs/memory/overview.md
New "Memory" docs section and overview describing middleware behavior, adapter contract/invariants, scope/security guidance, retrieval/persistence flows, extension hooks, devtools events, and failure modes.
Quickstart Guide
docs/memory/quickstart.md
Quickstart: install @tanstack/ai-memory, select adapter, wire memoryMiddleware into chat() with server-derived scope, optionally add an embedder.
Custom Adapter Guide
docs/memory/custom-adapter.md
Guide for implementing custom MemoryAdapters (pgvector example), reusing helpers, running the shared contract tests, and middleware wiring.
Adapter & Middleware Skills
packages/typescript/ai-memory/skills/tanstack-ai-memory-in-memory/SKILL.md, packages/typescript/ai-memory/skills/tanstack-ai-memory-redis/SKILL.md, packages/typescript/ai/skills/tanstack-ai-memory/SKILL.md
SKILL pages documenting in-memory (dev/test) and Redis (production wiring, ioredis/node-redis notes) adapters, plus middleware setup, scope security, hooks, and failure-mode guidance.

Release Notes

Layer / File(s) Summary
Changeset
.changeset/memory-middleware.md
Minor version bumps and public API surface: memory middleware, types/helpers, adapters, node-redis wrapper, devtools events, and extension hooks.

🎯 4 (Complex) | ⏱️ ~45 minutes

"🐰
I tuck memories in a neat little heap,
Prompts that remember, promises that keep,
Redis or Map, whichever you choose,
Scope-safe and swift—no data to lose!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(ai, ai-memory): server-side memory middleware with pluggable adapters' accurately summarizes the main feature addition—introducing server-side memory support via middleware and adapters.
Description check ✅ Passed The PR description covers the summary, key implementation details, test plan, and design goals, though the provided checklist items lack explicit checkmarks for validation completion.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/memory-middleware

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 10, 2026

🚀 Changeset Version Preview

3 package(s) bumped directly, 31 bumped as dependents.

🟥 Major bumps

Package Version Reason
@tanstack/ai-event-client 0.2.9 → 1.0.0 Changeset
@tanstack/ai-memory 0.0.0 → 1.0.0 Changeset
@tanstack/ai-anthropic 0.8.4 → 1.0.0 Dependent
@tanstack/ai-code-mode 0.1.9 → 1.0.0 Dependent
@tanstack/ai-code-mode-skills 0.1.9 → 1.0.0 Dependent
@tanstack/ai-elevenlabs 0.2.1 → 1.0.0 Dependent
@tanstack/ai-fal 0.7.1 → 1.0.0 Dependent
@tanstack/ai-gemini 0.10.1 → 1.0.0 Dependent
@tanstack/ai-grok 0.7.1 → 1.0.0 Dependent
@tanstack/ai-groq 0.1.9 → 1.0.0 Dependent
@tanstack/ai-isolate-node 0.1.9 → 1.0.0 Dependent
@tanstack/ai-isolate-quickjs 0.1.9 → 1.0.0 Dependent
@tanstack/ai-ollama 0.6.11 → 1.0.0 Dependent
@tanstack/ai-openai 0.8.3 → 1.0.0 Dependent
@tanstack/ai-openrouter 0.8.3 → 1.0.0 Dependent
@tanstack/ai-preact 0.6.21 → 1.0.0 Dependent
@tanstack/ai-react 0.8.1 → 1.0.0 Dependent
@tanstack/ai-react-ui 0.6.3 → 1.0.0 Dependent
@tanstack/ai-solid 0.7.1 → 1.0.0 Dependent
@tanstack/ai-solid-ui 0.6.3 → 1.0.0 Dependent
@tanstack/ai-svelte 0.7.1 → 1.0.0 Dependent
@tanstack/ai-vue 0.7.1 → 1.0.0 Dependent

🟨 Minor bumps

Package Version Reason
@tanstack/ai 0.15.0 → 0.16.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai-client 0.9.0 → 0.9.1 Dependent
@tanstack/ai-code-mode-models-eval 0.0.13 → 0.0.14 Dependent
@tanstack/ai-devtools-core 0.3.26 → 0.3.27 Dependent
@tanstack/ai-isolate-cloudflare 0.2.0 → 0.2.1 Dependent
@tanstack/ai-vue-ui 0.1.32 → 0.1.33 Dependent
@tanstack/preact-ai-devtools 0.1.30 → 0.1.31 Dependent
@tanstack/react-ai-devtools 0.2.30 → 0.2.31 Dependent
@tanstack/solid-ai-devtools 0.2.30 → 0.2.31 Dependent
ts-svelte-chat 0.1.39 → 0.1.40 Dependent
ts-vue-chat 0.1.39 → 0.1.40 Dependent
vanilla-chat 0.0.36 → 0.0.37 Dependent

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 10, 2026

View your CI Pipeline Execution ↗ for commit b478e8e

Command Status Duration Result
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 1m 49s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-10 20:49:40 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 10, 2026

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@541

@tanstack/ai-anthropic

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-anthropic@541

@tanstack/ai-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@541

@tanstack/ai-code-mode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode@541

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode-skills@541

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-devtools-core@541

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-elevenlabs@541

@tanstack/ai-event-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-event-client@541

@tanstack/ai-fal

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-fal@541

@tanstack/ai-gemini

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-gemini@541

@tanstack/ai-grok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok@541

@tanstack/ai-groq

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-groq@541

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-cloudflare@541

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-node@541

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs@541

@tanstack/ai-memory

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-memory@541

@tanstack/ai-ollama

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollama@541

@tanstack/ai-openai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@541

@tanstack/ai-openrouter

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openrouter@541

@tanstack/ai-preact

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-preact@541

@tanstack/ai-react

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@541

@tanstack/ai-react-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react-ui@541

@tanstack/ai-solid

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid@541

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid-ui@541

@tanstack/ai-svelte

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-svelte@541

@tanstack/ai-vue

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue@541

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue-ui@541

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/preact-ai-devtools@541

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/react-ai-devtools@541

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/solid-ai-devtools@541

commit: b478e8e

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/guides/memory-quickstart.md`:
- Around line 31-34: The blockquote contains an internal blank line triggering
MD028; remove the blank line so the two adapter descriptions are contiguous
within the same blockquote. Edit the block that mentions inMemoryMemoryAdapter()
and redisMemoryAdapter({ redis }) to combine the paragraphs (no empty line
between them) so the blockquote has no blank lines while preserving both
descriptions and formatting.
- Around line 135-136: The two markdown links titled "[In-memory adapter skill]"
and "[Redis adapter skill]" currently point to the repository root; update their
hrefs to the specific adapter skill documentation pages for the packages
`tanstack-ai-memory-in-memory` and `tanstack-ai-memory-redis` respectively so
the links go directly to each adapter's docs (replace the root URLs in those
link entries with the correct doc targets) and run the pnpm test:docs validation
to confirm they are valid.

In `@packages/typescript/ai-memory/package.json`:
- Line 45: The dependency for `@tanstack/ai` in packages/typescript/ai-memory's
package.json uses the workspace:^ protocol; update that dependency string to use
the workspace:* protocol instead (i.e., change the version specifier for
"@tanstack/ai" to "workspace:*") so it conforms to the repo guideline for
internal package dependencies.
- Around line 15-20: The package.json currently only exports the package root;
add a `/adapters` subpath export so provider adapter packages can be
tree-shaken. Update the "exports" object to include an entry for "./adapters/*"
that maps "types" to "./dist/esm/adapters/*.d.ts" and "import" to
"./dist/esm/adapters/*.js" (mirroring the root export shape), ensuring adapter
consumers can import specific adapters like "ai-memory/adapters/<adapter>".

In `@packages/typescript/ai-memory/skills/tanstack-ai-memory-redis/SKILL.md`:
- Around line 35-38: The fenced code block containing the Redis key patterns
({prefix}:record:{memoryId} and
{prefix}:index:{tenantId}:{userId}:{sessionId}:{threadId}:{namespace}) should
include a language tag to satisfy MD040; update the fence in SKILL.md so the
opening triple backticks are followed by "text" (i.e., ```text) so the block is
treated as plain text and the lint warning stops firing.

In `@packages/typescript/ai-memory/src/adapters/redis.ts`:
- Around line 207-215: The delete method currently removes index entries using
the query scope key (indexKey(scope)) which is incorrect; update the loop in
async delete(ids, scope) to call redis.srem(indexKey(r.scope), id) after loading
the record (r) and verifying scopeMatches(r.scope, scope), so the removal
targets the record's actual index key; ensure you still call await
redis.del(recordKey(id)) and only call srem when r exists and scopeMatches
passes.

In `@packages/typescript/ai-memory/tests/in-memory.test.ts`:
- Around line 1-2: Imports are out of order per the project's import/order rule;
swap the two import statements so inMemoryMemoryAdapter (from
'../src/adapters/in-memory') is imported before runMemoryAdapterContract (from
'./contract') to satisfy ESLint; locate the imports for runMemoryAdapterContract
and inMemoryMemoryAdapter at the top of
packages/typescript/ai-memory/tests/in-memory.test.ts and reorder them
accordingly.

In `@packages/typescript/ai-memory/tests/redis.test.ts`:
- Around line 5-6: The imports in the test file violate the import/order rule;
reorder them so external and shared-module imports come before local relative
imports — specifically import runMemoryAdapterContract before importing
redisMemoryAdapter — ensuring the import of runMemoryAdapterContract appears
above the import of ../src/adapters/redis (symbols: runMemoryAdapterContract,
redisMemoryAdapter) to satisfy the configured ordering.

In `@packages/typescript/ai-memory/tsconfig.json`:
- Around line 6-7: The tsconfig currently includes "vite.config.ts" but the
exclude pattern "**/*.config.ts" removes it; update tsconfig.json so the include
and exclude don't conflict—either remove "**/*.config.ts" from the "exclude"
array or narrow the exclude pattern so it doesn't match "vite.config.ts" (i.e.
keep "include": ["vite.config.ts", "./src", "./tests"] and adjust the "exclude":
["node_modules", "dist", ...] accordingly) to ensure vite.config.ts is
type-checked.

In `@packages/typescript/ai/src/memory/middleware.ts`:
- Around line 152-181: onAfterToolCall currently defers deferredApplyOps
immediately, so tool-result memories are written even when shouldRemember later
blocks turn persistence; change this to collect and store the ops instead of
applying them: parse and normalize the returned ops from options.onToolResult
using normalizeOps(out), assign them to a new/existing state field (e.g.,
state.pendingToolOps or merge into state.pendingToolOps array), and remove the
immediate ctx.defer(deferredApplyOps(...)) call; then update persistTurn to
accept pendingToolOps and only call/ctx.defer
deferredApplyOps(normalizeOps(pendingToolOps)) when shouldRemember passes
(ensuring the wrapper that emits memory:error and fires events.onError remains
intact). Apply the same change to the other handler mentioned (the block around
the other deferredApplyOps at lines 347-353).
- Around line 80-87: The devtools event payload leaks the full prompt because
safeEmit('memory:retrieve:started', ...) uses state.lastUserText directly;
truncate that value to a 200-character preview before emitting. Update the
memory middleware so the emitted query uses a capped string (e.g., preview =
state.lastUserText?.slice(0,200) or similar) and keep the original
state.lastUserText unchanged for internal logic; change only the emitted payload
passed to safeEmit.

In `@packages/typescript/ai/tests/middlewares/memory.test.ts`:
- Around line 1-567: Reorder and alphabetize the imports so the test helper
import (the named import containing ev, collectChunks and createMockAdapter)
comes before the type imports and ensure createMockAdapter is alphabetized
inside that import, and convert all bracket-style array types to generic form
(e.g. MemoryRecord[] → Array<MemoryRecord>, MemoryHit[] → Array<MemoryHit>,
MemoryQuery[] → Array<MemoryQuery>, MemoryListResult[] →
Array<MemoryListResult>, MemorySearchResult[] → Array<MemorySearchResult>,
StreamChunk[] → Array<StreamChunk>, etc.) across the file (look for usages in
fakeAdapter, rec, tests, and type annotations such as the return types and
variables referencing store/hits/iterations) to satisfy the project's
Array<Type> style rule.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d4b7f25f-3933-42c9-b1e8-0d0003ed96af

📥 Commits

Reviewing files that changed from the base of the PR and between 4a943d6 and 6576f7c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (27)
  • .changeset/memory-middleware.md
  • docs/config.json
  • docs/guides/memory-quickstart.md
  • docs/middlewares/memory.md
  • knip.json
  • packages/typescript/ai-event-client/src/index.ts
  • packages/typescript/ai-memory/package.json
  • packages/typescript/ai-memory/project.json
  • packages/typescript/ai-memory/skills/tanstack-ai-memory-in-memory/SKILL.md
  • packages/typescript/ai-memory/skills/tanstack-ai-memory-redis/SKILL.md
  • packages/typescript/ai-memory/src/adapters/in-memory.ts
  • packages/typescript/ai-memory/src/adapters/redis.ts
  • packages/typescript/ai-memory/src/index.ts
  • packages/typescript/ai-memory/tests/contract.ts
  • packages/typescript/ai-memory/tests/in-memory.test.ts
  • packages/typescript/ai-memory/tests/redis.test.ts
  • packages/typescript/ai-memory/tsconfig.json
  • packages/typescript/ai-memory/vite.config.ts
  • packages/typescript/ai/package.json
  • packages/typescript/ai/skills/tanstack-ai-memory/SKILL.md
  • packages/typescript/ai/src/memory/helpers.ts
  • packages/typescript/ai/src/memory/index.ts
  • packages/typescript/ai/src/memory/middleware.ts
  • packages/typescript/ai/src/memory/types.ts
  • packages/typescript/ai/tests/memory/helpers.test.ts
  • packages/typescript/ai/tests/middlewares/memory.test.ts
  • packages/typescript/ai/vite.config.ts

Comment thread docs/memory/quickstart.md
Comment thread docs/guides/memory-quickstart.md Outdated
Comment on lines +135 to +136
- [In-memory adapter skill](https://github.com/TanStack/ai) — `tanstack-ai-memory-in-memory` (when to use, capacity limits)
- [Redis adapter skill](https://github.com/TanStack/ai) — `tanstack-ai-memory-redis` (vector search, key layout, ops)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace placeholder repo-root links with direct targets.

Line 135-136 currently send users to the repository root, not the adapter skill docs.

Proposed fix
-- [In-memory adapter skill](https://github.com/TanStack/ai) — `tanstack-ai-memory-in-memory` (when to use, capacity limits)
-- [Redis adapter skill](https://github.com/TanStack/ai) — `tanstack-ai-memory-redis` (vector search, key layout, ops)
+- [In-memory adapter skill](https://github.com/TanStack/ai/blob/main/packages/typescript/ai-memory/skills/tanstack-ai-memory-in-memory/SKILL.md) — `tanstack-ai-memory-in-memory` (when to use, capacity limits)
+- [Redis adapter skill](https://github.com/TanStack/ai/blob/main/packages/typescript/ai-memory/skills/tanstack-ai-memory-redis/SKILL.md) — `tanstack-ai-memory-redis` (vector search, key layout, ops)

As per coding guidelines, "Verify documentation links are valid via pnpm test:docs command."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- [In-memory adapter skill](https://github.com/TanStack/ai)`tanstack-ai-memory-in-memory` (when to use, capacity limits)
- [Redis adapter skill](https://github.com/TanStack/ai)`tanstack-ai-memory-redis` (vector search, key layout, ops)
- [In-memory adapter skill](https://github.com/TanStack/ai/blob/main/packages/typescript/ai-memory/skills/tanstack-ai-memory-in-memory/SKILL.md)`tanstack-ai-memory-in-memory` (when to use, capacity limits)
- [Redis adapter skill](https://github.com/TanStack/ai/blob/main/packages/typescript/ai-memory/skills/tanstack-ai-memory-redis/SKILL.md)`tanstack-ai-memory-redis` (vector search, key layout, ops)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/guides/memory-quickstart.md` around lines 135 - 136, The two markdown
links titled "[In-memory adapter skill]" and "[Redis adapter skill]" currently
point to the repository root; update their hrefs to the specific adapter skill
documentation pages for the packages `tanstack-ai-memory-in-memory` and
`tanstack-ai-memory-redis` respectively so the links go directly to each
adapter's docs (replace the root URLs in those link entries with the correct doc
targets) and run the pnpm test:docs validation to confirm they are valid.

Comment thread packages/typescript/ai-memory/package.json
"rag"
],
"peerDependencies": {
"@tanstack/ai": "workspace:^",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use workspace:* for internal package dependency.

Line 45 uses workspace:^ for @tanstack/ai; repo guidelines require workspace:*.

Proposed fix
-    "@tanstack/ai": "workspace:^",
+    "@tanstack/ai": "workspace:*",

As per coding guidelines, "packages/**/package.json: Use workspace:* protocol for internal package dependencies in package.json."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@tanstack/ai": "workspace:^",
"@tanstack/ai": "workspace:*",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/typescript/ai-memory/package.json` at line 45, The dependency for
`@tanstack/ai` in packages/typescript/ai-memory's package.json uses the
workspace:^ protocol; update that dependency string to use the workspace:*
protocol instead (i.e., change the version specifier for "@tanstack/ai" to
"workspace:*") so it conforms to the repo guideline for internal package
dependencies.

Comment thread packages/typescript/ai-memory/skills/tanstack-ai-memory-redis/SKILL.md Outdated
Comment thread packages/typescript/ai-memory/tests/redis.test.ts Outdated
Comment thread packages/typescript/ai-memory/tsconfig.json Outdated
Comment thread packages/typescript/ai/src/memory/middleware.ts
Comment thread packages/typescript/ai/src/memory/middleware.ts Outdated
Comment on lines +1 to +567
// packages/typescript/ai/tests/middlewares/memory.test.ts
import { describe, expect, it, vi } from 'vitest'
import { aiEventClient } from '@tanstack/ai-event-client'
import { chat } from '../../src/activities/chat/index'
import { memoryMiddleware } from '../../src/memory'
import type {
MemoryAdapter,
MemoryHit,
MemoryListResult,
MemoryQuery,
MemoryRecord,
MemoryScope,
MemorySearchResult,
} from '../../src/memory'
import type { StreamChunk } from '../../src/types'
import { ev, createMockAdapter, collectChunks } from '../test-utils'

// Local test double — keeps tests isolated from @tanstack/ai-memory.
function fakeAdapter(seed: MemoryRecord[] = []): MemoryAdapter & {
store: Map<string, MemoryRecord>
searchCalls: MemoryQuery[]
} {
const store = new Map<string, MemoryRecord>()
for (const r of seed) store.set(r.id, r)
const searchCalls: MemoryQuery[] = []
return {
name: 'fake',
store,
searchCalls,
async add(input) {
const list = Array.isArray(input) ? input : [input]
for (const r of list) store.set(r.id, { ...r, updatedAt: Date.now() })
},
async get(id, scope) {
const r = store.get(id)
if (!r) return undefined
// simple scope check
for (const k of Object.keys(scope) as Array<keyof MemoryScope>) {
if (scope[k] && r.scope[k] !== scope[k]) return undefined
}
return r
},
async update(id, scope, patch) {
const existing = await this.get(id, scope)
if (!existing) return undefined
const next = { ...existing, ...patch, updatedAt: Date.now() }
store.set(id, next)
return next
},
async search(query): Promise<MemorySearchResult> {
searchCalls.push(query)
const hits: MemoryHit[] = []
for (const r of store.values()) {
let match = true
for (const k of Object.keys(query.scope) as Array<keyof MemoryScope>) {
if (query.scope[k] && r.scope[k] !== query.scope[k]) {
match = false
break
}
}
if (!match) continue
if (query.kinds && !query.kinds.includes(r.kind)) continue
hits.push({ record: r, score: 0.9 })
}
return { hits: hits.slice(0, query.topK ?? 6) }
},
async list(scope, options): Promise<MemoryListResult> {
const items: MemoryRecord[] = []
for (const r of store.values()) {
let match = true
for (const k of Object.keys(scope) as Array<keyof MemoryScope>) {
if (scope[k] && r.scope[k] !== scope[k]) {
match = false
break
}
}
if (match) items.push(r)
}
return { items: items.slice(0, options?.limit ?? items.length) }
},
async delete(ids) {
for (const id of ids) store.delete(id)
},
async clear() {
store.clear()
},
}
}

const baseScope: MemoryScope = { tenantId: 't1', userId: 'u1' }

function rec(over: Partial<MemoryRecord> = {}): MemoryRecord {
return {
id: over.id ?? crypto.randomUUID(),
scope: over.scope ?? baseScope,
text: over.text ?? 'sample',
kind: over.kind ?? 'fact',
createdAt: over.createdAt ?? Date.now(),
...over,
}
}

describe('memoryMiddleware — retrieval', () => {
it('is a no-op when there is no user message', async () => {
const { adapter } = createMockAdapter({
iterations: [
[ev.runStarted(), ev.textContent('hi'), ev.runFinished('stop')],
],
})
const memory = fakeAdapter([rec({ text: 'X' })])
const stream = chat({
adapter,
messages: [],
middleware: [memoryMiddleware({ adapter: memory, scope: baseScope })],
})
await collectChunks(stream as AsyncIterable<StreamChunk>)
expect(memory.searchCalls).toHaveLength(0)
})

it('retrieves at init and injects a memory system prompt', async () => {
const memory = fakeAdapter([
rec({ text: 'User likes TS.', kind: 'preference' }),
])
const { adapter, calls } = createMockAdapter({
iterations: [
[ev.runStarted(), ev.textContent('ok'), ev.runFinished('stop')],
],
})
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'hi' }],
middleware: [memoryMiddleware({ adapter: memory, scope: baseScope })],
})
await collectChunks(stream as AsyncIterable<StreamChunk>)
const first = calls[0] as { systemPrompts?: string[] }
expect(first.systemPrompts?.some((p) => p.includes('User likes TS.'))).toBe(
true,
)
})

it('does not re-inject across agent-loop iterations', async () => {
const memory = fakeAdapter([rec({ text: 'X' })])
const { adapter, calls } = createMockAdapter({
iterations: [
[
ev.runStarted(),
ev.toolStart('c1', 't'),
ev.toolArgs('c1', '{}'),
ev.toolEnd('c1', 't'),
ev.runFinished('tool_calls'),
],
[ev.runStarted(), ev.textContent('done'), ev.runFinished('stop')],
],
})
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'hi' }],
tools: [{ name: 't', description: 'noop', execute: async () => ({}) }],
middleware: [memoryMiddleware({ adapter: memory, scope: baseScope })],
})
await collectChunks(stream as AsyncIterable<StreamChunk>)
const iter1 =
(calls[0] as { systemPrompts?: string[] }).systemPrompts?.length ?? 0
const iter2 =
(calls[1] as { systemPrompts?: string[] }).systemPrompts?.length ?? 0
expect(iter1).toBe(iter2)
})

it('skips retrieval and injection when shouldRetrieve returns false', async () => {
const memory = fakeAdapter([rec({ text: 'X' })])
const { adapter } = createMockAdapter({
iterations: [
[ev.runStarted(), ev.textContent('ok'), ev.runFinished('stop')],
],
})
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'hi' }],
middleware: [
memoryMiddleware({
adapter: memory,
scope: baseScope,
shouldRetrieve: () => false,
}),
],
})
await collectChunks(stream as AsyncIterable<StreamChunk>)
expect(memory.searchCalls).toHaveLength(0)
})

it('calls rerank between search and render', async () => {
const memory = fakeAdapter([
rec({ id: 'a', text: 'A' }),
rec({ id: 'b', text: 'B' }),
])
const { adapter, calls } = createMockAdapter({
iterations: [
[ev.runStarted(), ev.textContent('ok'), ev.runFinished('stop')],
],
})
const rerank = vi.fn(async (hits: MemoryHit[]) => [...hits].reverse())
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'hi' }],
middleware: [
memoryMiddleware({ adapter: memory, scope: baseScope, rerank }),
],
})
await collectChunks(stream as AsyncIterable<StreamChunk>)
expect(rerank).toHaveBeenCalledTimes(1)
const promptText = (
calls[0] as { systemPrompts: string[] }
).systemPrompts.join('\n')
expect(promptText.indexOf('B')).toBeLessThan(promptText.indexOf('A'))
})

it('resolves function-form scope once and caches it', async () => {
const memory = fakeAdapter([rec({ text: 'X' })])
const { adapter } = createMockAdapter({
iterations: [
[ev.runStarted(), ev.textContent('ok'), ev.runFinished('stop')],
],
})
const scopeFn = vi.fn(() => baseScope)
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'hi' }],
middleware: [memoryMiddleware({ adapter: memory, scope: scopeFn })],
})
await collectChunks(stream as AsyncIterable<StreamChunk>)
expect(scopeFn).toHaveBeenCalledTimes(1)
})
})

describe('memoryMiddleware — persistence', () => {
it('persists user and assistant messages on finish', async () => {
const memory = fakeAdapter()
const { adapter } = createMockAdapter({
iterations: [
[ev.runStarted(), ev.textContent('Pong.'), ev.runFinished('stop')],
],
})
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'Ping' }],
middleware: [memoryMiddleware({ adapter: memory, scope: baseScope })],
})
await collectChunks(stream as AsyncIterable<StreamChunk>)
const texts = [...memory.store.values()].map((r) => r.text).sort()
expect(texts).toEqual(['Ping', 'Pong.'])
})

it('shouldRemember=false skips the entire turn (base records and extractMemories)', async () => {
// Per-turn semantics: shouldRemember is evaluated ONCE per turn and
// gates the whole persist path. The user message is short ("hi", 2
// chars) so the gate returns false and NOTHING is persisted — the
// assistant message is dropped too, and `extractMemories` is never
// called.
const memory = fakeAdapter()
const { adapter } = createMockAdapter({
iterations: [
[
ev.runStarted(),
ev.textContent('long enough response text'),
ev.runFinished('stop'),
],
],
})
const extractMemories = vi.fn(async () => [
rec({ text: 'should not run', kind: 'fact' }),
])
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'hi' }],
middleware: [
memoryMiddleware({
adapter: memory,
scope: baseScope,
shouldRemember: ({ message }) => message.content.length > 10,
extractMemories,
}),
],
})
await collectChunks(stream as AsyncIterable<StreamChunk>)
expect([...memory.store.values()]).toEqual([])
expect(extractMemories).not.toHaveBeenCalled()
})

it('shouldRemember=true persists user, assistant, and extracted records', async () => {
const memory = fakeAdapter()
const { adapter } = createMockAdapter({
iterations: [
[
ev.runStarted(),
ev.textContent('long enough response text'),
ev.runFinished('stop'),
],
],
})
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'a meaningful user message' }],
middleware: [
memoryMiddleware({
adapter: memory,
scope: baseScope,
// 25-char user message + non-empty response — gate keeps the turn.
shouldRemember: ({ message }) => message.content.length > 10,
}),
],
})
await collectChunks(stream as AsyncIterable<StreamChunk>)
const texts = [...memory.store.values()].map((r) => r.text).sort()
expect(texts).toEqual([
'a meaningful user message',
'long enough response text',
])
})

it('extractMemories returning records adds them as kind: fact', async () => {
const memory = fakeAdapter()
const { adapter } = createMockAdapter({
iterations: [
[ev.runStarted(), ev.textContent('R'), ev.runFinished('stop')],
],
})
const extractMemories = vi.fn(async () => [
rec({ text: 'extracted', kind: 'fact' }),
])
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'U' }],
middleware: [
memoryMiddleware({
adapter: memory,
scope: baseScope,
extractMemories,
}),
],
})
await collectChunks(stream as AsyncIterable<StreamChunk>)
expect(extractMemories).toHaveBeenCalledTimes(1)
const kinds = [...memory.store.values()].map((r) => r.kind).sort()
expect(kinds).toEqual(['fact', 'message', 'message'])
})

it('extractMemories MemoryOp[] dispatches to add/update/delete', async () => {
const existing = rec({ id: 'old', text: 'old text', kind: 'fact' })
const memory = fakeAdapter([existing])
const { adapter } = createMockAdapter({
iterations: [
[ev.runStarted(), ev.textContent('R'), ev.runFinished('stop')],
],
})
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'U' }],
middleware: [
memoryMiddleware({
adapter: memory,
scope: baseScope,
extractMemories: () => [
{ op: 'add', record: rec({ text: 'new fact', kind: 'fact' }) },
{ op: 'update', id: 'old', patch: { text: 'updated text' } },
],
}),
],
})
await collectChunks(stream as AsyncIterable<StreamChunk>)
expect(memory.store.get('old')?.text).toBe('updated text')
expect([...memory.store.values()].some((r) => r.text === 'new fact')).toBe(
true,
)
})

it('applies ops in array order: update after add in same batch sees the add', async () => {
// Order-sensitivity regression test. Previously, all `add` ops were
// batched and flushed at the END after updates/deletes, meaning an
// `update` of an id added in the SAME batch silently no-op'd. With
// strict in-order dispatch the update now sees the just-added record.
const memory = fakeAdapter()
const { adapter } = createMockAdapter({
iterations: [
[ev.runStarted(), ev.textContent('R'), ev.runFinished('stop')],
],
})
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'U' }],
middleware: [
memoryMiddleware({
adapter: memory,
scope: baseScope,
extractMemories: () => [
{
op: 'add',
record: rec({ id: 'X', text: 'initial', kind: 'fact' }),
},
{ op: 'update', id: 'X', patch: { text: 'patched' } },
],
}),
],
})
await collectChunks(stream as AsyncIterable<StreamChunk>)
expect(memory.store.get('X')?.text).toBe('patched')
})

it('afterPersist receives newly-added records (not updates/deletes)', async () => {
const memory = fakeAdapter()
const { adapter } = createMockAdapter({
iterations: [
[ev.runStarted(), ev.textContent('R'), ev.runFinished('stop')],
],
})
const afterPersist = vi.fn()
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'U' }],
middleware: [
memoryMiddleware({ adapter: memory, scope: baseScope, afterPersist }),
],
})
await collectChunks(stream as AsyncIterable<StreamChunk>)
expect(afterPersist).toHaveBeenCalledTimes(1)
const arg = afterPersist.mock.calls[0]?.[0] as
| { newRecords: MemoryRecord[] }
| undefined
expect(arg?.newRecords.length).toBe(2) // user + assistant
})

it('onToolResult persists kind: tool-result records', async () => {
const memory = fakeAdapter()
const { adapter } = createMockAdapter({
iterations: [
[
ev.runStarted(),
ev.toolStart('c1', 'echo'),
ev.toolArgs('c1', '{}'),
ev.toolEnd('c1', 'echo'),
ev.runFinished('tool_calls'),
],
[ev.runStarted(), ev.textContent('done'), ev.runFinished('stop')],
],
})
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'U' }],
tools: [
{ name: 'echo', description: 'noop', execute: async () => ({ ok: 1 }) },
],
middleware: [
memoryMiddleware({
adapter: memory,
scope: baseScope,
onToolResult: ({ toolName, result }) => [
rec({
text: `${toolName}:${JSON.stringify(result)}`,
kind: 'tool-result',
role: 'tool',
}),
],
}),
],
})
await collectChunks(stream as AsyncIterable<StreamChunk>)
const toolResults = [...memory.store.values()].filter(
(r) => r.kind === 'tool-result',
)
expect(toolResults).toHaveLength(1)
expect(toolResults[0]?.text).toContain('echo')
})
})

describe('memoryMiddleware — failure handling', () => {
it('non-strict: retrieval failure does not abort chat', async () => {
const memory = fakeAdapter()
memory.search = async () => {
throw new Error('boom')
}
const { adapter } = createMockAdapter({
iterations: [
[ev.runStarted(), ev.textContent('ok'), ev.runFinished('stop')],
],
})
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'hi' }],
middleware: [memoryMiddleware({ adapter: memory, scope: baseScope })],
})
const chunks = await collectChunks(stream as AsyncIterable<StreamChunk>)
expect(chunks.some((c) => c.type === 'TEXT_MESSAGE_CONTENT')).toBe(true)
})

it('strict: retrieval failure rejects the stream', async () => {
const memory = fakeAdapter()
memory.search = async () => {
throw new Error('boom')
}
const { adapter } = createMockAdapter({
iterations: [
[ev.runStarted(), ev.textContent('ok'), ev.runFinished('stop')],
],
})
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'hi' }],
middleware: [
memoryMiddleware({ adapter: memory, scope: baseScope, strict: true }),
],
})
await expect(
collectChunks(stream as AsyncIterable<StreamChunk>),
).rejects.toThrow('boom')
})
})

describe('memoryMiddleware — devtools events', () => {
it('emits retrieve and persist events in order', async () => {
const memory = fakeAdapter([rec({ text: 'X' })])
const { adapter } = createMockAdapter({
iterations: [
[ev.runStarted(), ev.textContent('R'), ev.runFinished('stop')],
],
})
const seen: string[] = []
const opts = { withEventTarget: true } as const
const off1 = aiEventClient.on(
'memory:retrieve:started',
() => seen.push('retrieve:started'),
opts,
)
const off2 = aiEventClient.on(
'memory:retrieve:completed',
() => seen.push('retrieve:completed'),
opts,
)
const off3 = aiEventClient.on(
'memory:persist:started',
() => seen.push('persist:started'),
opts,
)
const off4 = aiEventClient.on(
'memory:persist:completed',
() => seen.push('persist:completed'),
opts,
)
try {
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'U' }],
middleware: [memoryMiddleware({ adapter: memory, scope: baseScope })],
})
await collectChunks(stream as AsyncIterable<StreamChunk>)
expect(seen).toEqual([
'retrieve:started',
'retrieve:completed',
'persist:started',
'persist:completed',
])
} finally {
off1()
off2()
off3()
off4()
}
})
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix ESLint violations for array types and import order.

Static analysis flagged multiple style violations:

  1. Import order (line 16): The test-utils import should come before type imports.
  2. Import sorting (line 16): createMockAdapter should be sorted alphabetically.
  3. Array type style (19 instances): Use Array<Type> instead of Type[] to match project conventions.
📝 Proposed fixes

Fix import order and sorting (lines 2-16):

 import { describe, expect, it, vi } from 'vitest'
 import { aiEventClient } from '@tanstack/ai-event-client'
+import { collectChunks, createMockAdapter, ev } from '../test-utils'
 import { chat } from '../../src/activities/chat/index'
 import { memoryMiddleware } from '../../src/memory'
 import type {
   MemoryAdapter,
   MemoryHit,
   MemoryListResult,
   MemoryQuery,
   MemoryRecord,
   MemoryScope,
   MemorySearchResult,
 } from '../../src/memory'
 import type { StreamChunk } from '../../src/types'
-import { ev, createMockAdapter, collectChunks } from '../test-utils'

Fix array types (apply throughout file):

-function fakeAdapter(seed: MemoryRecord[] = []): MemoryAdapter & {
+function fakeAdapter(seed: Array<MemoryRecord> = []): MemoryAdapter & {
   store: Map<string, MemoryRecord>
-  searchCalls: MemoryQuery[]
+  searchCalls: Array<MemoryQuery>
 } {

Repeat this pattern for all 19 flagged array type violations.

🧰 Tools
🪛 ESLint

[error] 16-16: ../test-utils import should occur before type import of ../../src/memory

(import/order)


[error] 16-16: Member 'createMockAdapter' of the import declaration should be sorted alphabetically.

(sort-imports)


[error] 19-19: Array type using 'MemoryRecord[]' is forbidden. Use 'Array' instead.

(@typescript-eslint/array-type)


[error] 21-21: Array type using 'MemoryQuery[]' is forbidden. Use 'Array' instead.

(@typescript-eslint/array-type)


[error] 25-25: Array type using 'MemoryQuery[]' is forbidden. Use 'Array' instead.

(@typescript-eslint/array-type)


[error] 52-52: Array type using 'MemoryHit[]' is forbidden. Use 'Array' instead.

(@typescript-eslint/array-type)


[error] 68-68: Array type using 'MemoryRecord[]' is forbidden. Use 'Array' instead.

(@typescript-eslint/array-type)


[error] 135-135: Array type using 'string[]' is forbidden. Use 'Array' instead.

(@typescript-eslint/array-type)


[error] 163-163: Array type using 'string[]' is forbidden. Use 'Array' instead.

(@typescript-eslint/array-type)


[error] 165-165: Array type using 'string[]' is forbidden. Use 'Array' instead.

(@typescript-eslint/array-type)


[error] 201-201: Array type using 'MemoryHit[]' is forbidden. Use 'Array' instead.

(@typescript-eslint/array-type)


[error] 212-212: Array type using 'string[]' is forbidden. Use 'Array' instead.

(@typescript-eslint/array-type)


[error] 426-426: Array type using 'MemoryRecord[]' is forbidden. Use 'Array' instead.

(@typescript-eslint/array-type)


[error] 525-525: Array type using 'string[]' is forbidden. Use 'Array' instead.

(@typescript-eslint/array-type)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/typescript/ai/tests/middlewares/memory.test.ts` around lines 1 -
567, Reorder and alphabetize the imports so the test helper import (the named
import containing ev, collectChunks and createMockAdapter) comes before the type
imports and ensure createMockAdapter is alphabetized inside that import, and
convert all bracket-style array types to generic form (e.g. MemoryRecord[] →
Array<MemoryRecord>, MemoryHit[] → Array<MemoryHit>, MemoryQuery[] →
Array<MemoryQuery>, MemoryListResult[] → Array<MemoryListResult>,
MemorySearchResult[] → Array<MemorySearchResult>, StreamChunk[] →
Array<StreamChunk>, etc.) across the file (look for usages in fakeAdapter, rec,
tests, and type annotations such as the return types and variables referencing
store/hits/iterations) to satisfy the project's Array<Type> style rule.

- getMessageText reads ContentPart.content (not .text); structured user
  messages now feed retrieval and persistence correctly
- extractMemories strict-mode failure no longer double-emits
  memory:error or drops base user/assistant records
- defaultScoreHit threads its 'now' parameter through to recencyScore
  so callers can score deterministically
- Default importance contribution drops from 0.5 to 0 so a recent record
  with zero lexical/semantic match no longer clears the default
  minScore=0.15 floor
- MemoryAdapter.clear/search/list JSDoc fixed: empty scope matches
  NOTHING (matches scopeMatches and the contract suite)
- search/list/clear with a partial scope now traverse all matching
  index buckets via SCAN instead of just the exact-match bucket
- delete srem now keys off record scope (not caller scope) so
  ids in narrower index buckets are properly cleaned up
- add upsert removes the id from the old scope's index when the
  scope of an existing record changes
- skill troubleshooting drops the false SerializationError claim;
  malformed rows now log once per process via console.warn
- Contract suite gains 5 partial-scope tests covering search, list,
  clear, delete, and upsert; in-memory and redis must both pass them
The RedisLike interface is lowercase to match ioredis directly. node-redis
v4+ uses camelCase by default, which previously required users to enable
legacyMode to wire it up. The new nodeRedisAsRedisLike(client) helper
translates camelCase to the RedisLike shape so users can wire node-redis
v4+ default-mode clients with a one-line wrapper.

Skill and quickstart docs updated with separate ioredis vs node-redis
wiring examples. ioredis added as a parallel optional peer dep alongside
redis. New unit test for the helper.
- recencyScore half-life test passes 'now' explicitly so it does
  not race the internal Date.now() call (Group A added the param)
- Pagination contract test now asserts every record is visible
  across pages (catches adapters that drop or duplicate)
- Upsert contract test now verifies updatedAt strictly advances
  on the second add, not merely that updatedAt >= createdAt
- Tightened 'every(...)' assertions in the contract suite with
  preceding non-empty length checks so they cannot silently pass
  on an empty result set
Changesets bumps from the package.json version, so a minor entry on
0.1.0 would publish 0.2.0 (skipping 0.1.0). Setting the baseline to
0.0.0 makes the same minor changeset land at 0.1.0 on first release.
- Redis SCAN MATCH patterns now escape glob metacharacters in scope
  values (*, ?, [, ], \) so a scope like tenantId='t*' cannot cross-
  match other tenants' buckets
- onToolResult deferred persistence now flows through the same
  observability pipeline as finish-turn persist: emits
  memory:persist:started/completed, fires events.onPersistStart/End,
  and calls afterPersist with the newly-added tool-result records
- Skill and docs examples replaced with self-contained snippets so
  copy-pasters don't hit ReferenceError on undeclared 'body' or an
  infinite-recursion 'embed' shadow
- nodeRedisAsRedisLike scan now passes cursor through unchanged
  (no Number() coercion) so node-redis v5 string cursors past
  MAX_SAFE_INTEGER round-trip correctly; COUNT now validated >0
Convergence-audit response to Round 2 (onToolResult) + Round 3
(embedder failure) findings sharing the same root-cause class:
error paths that did not emit memory:error.

- persistTurn assistant-side embedder failure now emits memory:error
  (phase: persist) and continues with embedding: undefined in
  non-strict mode (matches retrieval-side embedder handling)
- onAfterToolCall tool-args JSON parse failure now emits memory:error
  (phase: extract) before falling back to {}
- All catch blocks in middleware.ts now uniformly do safeEmit +
  events.onError + strict-rethrow before exiting
- types.ts onError JSDoc updated to document which sub-cases each
  phase covers
Round 4 convergence-audit fixes for scope as a tenant-isolation boundary:

- redis.ts scopeKey now escapes : and \ in scope values so a tenant
  whose value contains a colon cannot collide with a multi-key scope
  that produces the same delimiter pattern (analogous to the Group F
  glob-metacharacter escape, applied to the EXACT-MATCH path)
- scopeMatches treats empty-string scope values as undefined; a query
  with all-empty-string keys matches nothing (same safety guarantee as
  the {} empty-scope guard)
- applyOps now overrides the scope on records returned by
  extractMemories/onToolResult to the resolved scope before
  persisting; a buggy or hostile callback cannot write into another
  tenant's bucket

Contract suite gains scope-value safety tests for both adapters; the
middleware test suite gains a regression for the extract-scope override.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/typescript/ai-memory/src/adapters/redis.ts`:
- Around line 199-216: scopeKey currently maps both missing segments and a
literal '_' value to the same sentinel, causing bucket collisions; change the
absent-segment sentinel to an unambiguous token that cannot collide with user
data (for example a non-printable marker or a unique prefix like '__MISSING__')
and update escapeScopeValue to escape that sentinel as well so any user-supplied
value cannot produce the sentinel; ensure scopeKey, indexKey, and any callers
(search, list, clear, hasAnyScopeKey, scopeMatches) consistently use the new
sentinel and escaping rules so destructive operations like clear rely on exact
encoded keys; add a regression test that inserts a memory with a literal '_'
scope value and verify that clear/search/list do not affect that distinct
bucket.
- Around line 361-372: The malformed-rows branch currently only calls
warnMalformedRowOnce(id, err) and leaves the bad payload and its id in Redis;
change the catch so malformed entries are treated like expired/missing ones by
removing the id from the index and deleting the associated record key: after
warnMalformedRowOnce(id, err) also call the same cleanup used for
missing/expired rows (e.g., remove the id from the set/index key and delete
recordKey(id)) so future search()/list() calls won't repeatedly reparse the same
bad payload; reference the existing helpers recordKey(id) and the index removal
logic used by markExpired or the surrounding code to perform the cleanup.
- Around line 442-461: In search (in
packages/typescript/ai-memory/src/adapters/redis.ts) capture a single timestamp
up-front (e.g., const now = Date.now()) before computing scores and use that
same now when calling defaultScoreHit inside the scored mapping so every record
in this search uses the same reference time; if defaultScoreHit doesn't accept a
now param, add an optional now parameter to its signature and update its callers
accordingly (including the scored map) so scoring is stable and
pagination/tie-breaking won't vary by iteration order.

In `@packages/typescript/ai/src/memory/helpers.ts`:
- Around line 130-141: The memory renderer injects raw user-controlled text
(hit.record.text) into a system prompt; serialize or fence it so it cannot
inject prompt syntax or newlines. In defaultRenderMemory replace the direct
interpolation of hit.record.text with a safe serialized representation (e.g.,
JSON.stringify(hit.record.text) or wrap the content in a fenced block) when
building the list in defaultRenderMemory so each item becomes literal data
instead of executable prompt text. Ensure the change is applied inside the
hits.map callback and preserves the existing numbering and record.kind
formatting.

In `@packages/typescript/ai/tests/middlewares/memory.test.ts`:
- Around line 141-167: The test currently only asserts iter1 === iter2, which
allows a false positive when both are 0; update the assertions to first verify
that the initial injection actually occurred (e.g.,
expect(iter1).toBeGreaterThan(0)) and then verify the second iteration did not
add another injection (e.g., expect(iter2).toBe(iter1)); update the assertions
that reference iter1 and iter2 in the test that uses memoryMiddleware and the
calls array/systemPrompts so the test fails if the initial memory injection
disappears or a duplicate is added.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 939ace3f-8511-4535-82a7-29dda9ae962a

📥 Commits

Reviewing files that changed from the base of the PR and between 6576f7c and 59ec97e.

📒 Files selected for processing (15)
  • .changeset/memory-middleware.md
  • docs/guides/memory-quickstart.md
  • knip.json
  • packages/typescript/ai-memory/package.json
  • packages/typescript/ai-memory/skills/tanstack-ai-memory-redis/SKILL.md
  • packages/typescript/ai-memory/src/adapters/redis.ts
  • packages/typescript/ai-memory/src/index.ts
  • packages/typescript/ai-memory/tests/contract.ts
  • packages/typescript/ai-memory/tests/redis.test.ts
  • packages/typescript/ai/skills/tanstack-ai-memory/SKILL.md
  • packages/typescript/ai/src/memory/helpers.ts
  • packages/typescript/ai/src/memory/middleware.ts
  • packages/typescript/ai/src/memory/types.ts
  • packages/typescript/ai/tests/memory/helpers.test.ts
  • packages/typescript/ai/tests/middlewares/memory.test.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/typescript/ai/skills/tanstack-ai-memory/SKILL.md
  • .changeset/memory-middleware.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • knip.json
  • packages/typescript/ai-memory/src/index.ts
  • packages/typescript/ai-memory/package.json
  • packages/typescript/ai/src/memory/types.ts
  • packages/typescript/ai/src/memory/middleware.ts

Comment thread packages/typescript/ai-memory/src/adapters/redis.ts
Comment thread packages/typescript/ai-memory/src/adapters/redis.ts
Comment thread packages/typescript/ai-memory/src/adapters/redis.ts
Comment thread packages/typescript/ai/src/memory/helpers.ts
Comment thread packages/typescript/ai/tests/middlewares/memory.test.ts
AlemTuzlak and others added 4 commits May 10, 2026 21:45
…sion

Round 5 convergence fix completing the scope-value-validation class
closed in Group H. The Redis adapter uses literal '_' as the
placeholder for an UNSET scope key, but Group H's escapeScopeValue
only escaped ':' and '\'. A user-supplied scope value of literal
'_' (e.g., userId: '_') would have produced the same index key as
'userId unset', creating a cross-leak surface on clear().

Now '_' is also escaped, so {tenantId: 't1', userId: '_'} indexes
distinctly from {tenantId: 't1'}.

Contract suite gains 2 tests verifying isolation under literal
underscore scope values (run against both adapters).
CI was failing with ERR_PNPM_OUTDATED_LOCKFILE because Group C added
ioredis as an optional peer dependency without regenerating the lockfile.
- Move docs/middlewares/memory.md -> docs/memory/overview.md (rename frontmatter title to 'Overview')
- Move docs/guides/memory-quickstart.md -> docs/memory/quickstart.md
- Add docs/memory/custom-adapter.md authoring guide covering the 8-member contract, the three isolation invariants, the shared contract suite, common pitfalls (delimiter escaping, atomicity, partial-scope cascade), and packaging conventions
- Replace single-child 'Middlewares' and 'Guides' sidebar sections with a unified 'Memory' section
- Rewire internal cross-links between the three pages
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
docs/memory/quickstart.md (1)

31-33: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the blank line inside this blockquote.

This still triggers MD028 / no-blanks-blockquote, so docs lint will keep flagging the section until the two quoted paragraphs are contiguous.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/memory/quickstart.md` around lines 31 - 33, Remove the blank line
between the two blockquote paragraphs so they become a single contiguous
blockquote; specifically edit the section containing the references to
inMemoryMemoryAdapter() and redisMemoryAdapter({ redis }) and delete the empty
line separating those two quoted paragraphs so MD028/no-blanks-blockquote is no
longer triggered.
packages/typescript/ai-memory/src/adapters/redis.ts (2)

446-458: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Snapshot now once before scoring.

defaultScoreHit() falls back to Date.now(), so later candidates in the same search get a slightly smaller recency contribution than earlier ones. That makes near-tie ordering depend on iteration speed and can destabilize cursor pagination.

Suggested fix
     async search(query: MemoryQuery): Promise<MemorySearchResult> {
       const records = await loadAllForScope(query.scope)
+      const now = Date.now()
       const candidates = records.filter((r) => {
         if (query.kinds?.length && !query.kinds.includes(r.kind)) return false
         return true
       })
@@
       const scored = candidates
         .map((record) => ({
           record,
-          score: defaultScoreHit({ record, query }),
+          score: defaultScoreHit({ record, query, now }),
         }))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/typescript/ai-memory/src/adapters/redis.ts` around lines 446 - 458,
In search() capture a single timestamp before scoring (e.g., const now =
Date.now()) and use that same now when computing scores so all candidates use
the same recency baseline; update the scored mapping to call defaultScoreHit
with the captured now (defaultScoreHit({ record, query, now })) or, if its
signature doesn't accept now yet, add an optional now parameter to
defaultScoreHit and pass it through from search() so scoring is stable across
the iteration.

365-375: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sweep malformed rows from Redis instead of re-skipping them forever.

The catch path only warns and continues, so the bad payload and its stale set member survive every later search() / list() call. Treat malformed rows like missing/expired rows here and reuse the existing cleanup path.

Suggested fix
       } catch (err) {
         warnMalformedRowOnce(id, err)
-        /* skip malformed */
+        markExpired(id)
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/typescript/ai-memory/src/adapters/redis.ts` around lines 365 - 375,
The catch block currently calls warnMalformedRowOnce(id, err) and continues,
leaving the bad Redis payload in place; change the catch path to treat malformed
rows like missing/expired ones by calling markExpired(id) after
warnMalformedRowOnce(id, err) (then continue) so the stale set member is cleaned
up; locate this in the search()/list() loop where JSON.parse(raw) is wrapped and
update the catch to call markExpired with the same id used elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/memory/custom-adapter.md`:
- Around line 242-244: The docs currently claim that runMemoryAdapterContract()
covers middleware-level extractMemories/resolved-scope behavior but the shared
test suite doesn't exercise that; either remove or soften the sentence
referencing “the resolved-scope override for records returned by
extractMemories” from the paragraph, or add explicit tests to the contract suite
that invoke extractMemories through the middleware path and assert the
resolved-scope override behavior (i.e., extend runMemoryAdapterContract with a
new test that calls extractMemories and verifies resolved-scope on returned
records). Ensure you reference the runMemoryAdapterContract and extractMemories
symbols when implementing the change.
- Around line 190-198: The WHERE clause currently uses "IS NOT DISTINCT FROM $N"
for partial-scope parameters which enforces exact NULL-matching; replace those
conditions for tenant_id, user_id, session_id, thread_id, and namespace so each
reads like "(column = $N OR $N IS NULL)" (i.e., for $2..$6 use tenant_id,
user_id, session_id, thread_id, namespace respectively) to implement
partial-scope semantics; keep the existing expires_at and kind checks and the
rest of the query (ORDER BY score DESC OFFSET $9 LIMIT $10) unchanged.

In `@docs/memory/quickstart.md`:
- Line 75: The link text points to the repository root instead of the specific
node-redis example; update the markdown link target so the "Redis adapter skill"
reference goes directly to the node-redis integration example that demonstrates
nodeRedisAsRedisLike (the example showing how to wrap a node-redis client to
satisfy the RedisLike contract), ensuring the anchor/URL points to that example
file/section in the repo so readers find the node-redis usage and integration
steps.

---

Duplicate comments:
In `@docs/memory/quickstart.md`:
- Around line 31-33: Remove the blank line between the two blockquote paragraphs
so they become a single contiguous blockquote; specifically edit the section
containing the references to inMemoryMemoryAdapter() and redisMemoryAdapter({
redis }) and delete the empty line separating those two quoted paragraphs so
MD028/no-blanks-blockquote is no longer triggered.

In `@packages/typescript/ai-memory/src/adapters/redis.ts`:
- Around line 446-458: In search() capture a single timestamp before scoring
(e.g., const now = Date.now()) and use that same now when computing scores so
all candidates use the same recency baseline; update the scored mapping to call
defaultScoreHit with the captured now (defaultScoreHit({ record, query, now }))
or, if its signature doesn't accept now yet, add an optional now parameter to
defaultScoreHit and pass it through from search() so scoring is stable across
the iteration.
- Around line 365-375: The catch block currently calls warnMalformedRowOnce(id,
err) and continues, leaving the bad Redis payload in place; change the catch
path to treat malformed rows like missing/expired ones by calling
markExpired(id) after warnMalformedRowOnce(id, err) (then continue) so the stale
set member is cleaned up; locate this in the search()/list() loop where
JSON.parse(raw) is wrapped and update the catch to call markExpired with the
same id used elsewhere.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 83bafe3d-ace3-4cc9-b73c-672c03e60d06

📥 Commits

Reviewing files that changed from the base of the PR and between 59ec97e and ed23b50.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • docs/config.json
  • docs/memory/custom-adapter.md
  • docs/memory/overview.md
  • docs/memory/quickstart.md
  • packages/typescript/ai-memory/src/adapters/redis.ts
  • packages/typescript/ai-memory/tests/contract.ts
  • packages/typescript/ai/src/memory/middleware.ts
  • packages/typescript/ai/tests/memory/helpers.test.ts
✅ Files skipped from review due to trivial changes (2)
  • docs/config.json
  • docs/memory/overview.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/typescript/ai/src/memory/middleware.ts

Comment thread docs/memory/custom-adapter.md Outdated
Comment thread docs/memory/custom-adapter.md Outdated
Comment thread docs/memory/quickstart.md Outdated
- middleware.ts: preview-cap memory:retrieve:started query payload
  (was emitting full lastUserText, breaking the documented 200-char
  preview contract for devtools events)
- helpers.ts: JSON-stringify memory text in defaultRenderMemory so
  newline-or-instruction-shaped persisted memory cannot break out of
  the list structure and steer subsequent turns at system priority
- middleware.ts: shouldRemember now gates tool-result memories from
  onToolResult; buffered ops flush inside persistTurn after the gate
  passes, matching the documented 'short-circuits the entire persist
  path for the current turn' contract. Persist events now fire once
  per turn (covers base + extracted + tool-result records together)
- redis.ts: malformed JSON rows in loadAllForScope are now swept from
  the index and record key (was warned-and-skipped, leaving the bad
  payload to be reparsed on every subsequent read)
- redis.ts and in-memory.ts: snapshot now once per search and thread
  through defaultScoreHit so recency ranking is stable across same-
  pass candidates
- custom-adapter.md: fix pgvector SQL example to use partial-scope
  semantics (($N IS NULL OR col = $N)) instead of IS NOT DISTINCT
  FROM, which had the wrong matching semantics for partial scopes
- custom-adapter.md: soften contract suite coverage claim - the
  shared suite does not exercise middleware-level extractMemories
  resolved-scope override
- quickstart.md: drop blank line in adapter blockquote (MD028);
  replace placeholder skill link with direct doc + repo SKILL.md link
- redis SKILL.md: add 'text' language tag to storage model fence (MD040)
- ai-memory package.json: add /adapters/in-memory and /adapters/redis
  subpath exports per repo convention
- ai-memory tsconfig.json: drop **/*.config.ts exclude so vite.config.ts
  (which is in include) actually gets type-checked
- in-memory.test.ts: reorder imports to satisfy import/order
- memory.test.ts: tighten the re-inject regression test with
  expect(iter1).toBeGreaterThan(0) so the assertion catches the
  case where injection is fully disabled in both iterations
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/typescript/ai-memory/package.json (1)

53-53: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use workspace:* for the internal peer dependency version.

Line 53 still uses workspace:^ for @tanstack/ai; this should be workspace:* to match repo policy.

Suggested fix
-    "@tanstack/ai": "workspace:^",
+    "@tanstack/ai": "workspace:*",

As per coding guidelines, "packages/**/package.json: Use workspace:* protocol for internal package dependencies in package.json".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/typescript/ai-memory/package.json` at line 53, Update the internal
dependency entry for "@tanstack/ai" in
packages/typescript/ai-memory/package.json from "workspace:^" to "workspace:*";
locate the dependency key "@tanstack/ai" in the package.json dependencies block
and replace the version string so it follows the repo policy of using
workspace:* for internal peer dependencies.
🧹 Nitpick comments (3)
packages/typescript/ai/tests/memory/helpers.test.ts (1)

1-10: ⚡ Quick win

Fix import member sorting to comply with ESLint.

The import members are not sorted alphabetically, violating the project's sort-imports rule.

📝 Proposed fix for import ordering
-import { describe, it, expect } from 'vitest'
+import { describe, expect, it } from 'vitest'
 import {
-  scopeMatches,
   cosine,
-  lexicalOverlap,
-  recencyScore,
   defaultRenderMemory,
   defaultScoreHit,
   isExpired,
+  lexicalOverlap,
+  recencyScore,
+  scopeMatches,
 } from '../../src/memory/helpers'

As per coding guidelines, this addresses the ESLint sort-imports violations flagged by static analysis.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/typescript/ai/tests/memory/helpers.test.ts` around lines 1 - 10, The
import specifiers in tests/memory/helpers.test.ts are not alphabetically sorted
causing an ESLint sort-imports violation; reorder the named imports from
'../../src/memory/helpers' so they are alphabetized (for example ensure entries
like cosine, defaultRenderMemory, defaultScoreHit, isExpired, lexicalOverlap,
recencyScore, scopeMatches appear in lexical order) so the import line imports
scopeMatches, cosine, lexicalOverlap, recencyScore, defaultRenderMemory,
defaultScoreHit, isExpired in sorted order per project linting rules.
packages/typescript/ai/tests/middlewares/memory.test.ts (2)

397-409: ⚡ Quick win

The delete branch is still uncovered here.

The test name says add/update/delete, but this batch only exercises add and update. A regression that stops forwarding delete ops to the adapter would still pass. Please seed a removable record, include a delete op, and assert it is gone afterward.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/typescript/ai/tests/middlewares/memory.test.ts` around lines 397 -
409, The test currently exercises only add/update paths but should also cover
delete; modify the test (the case that calls collectChunks and uses
extractMemories) to seed a removable record in memory.store (e.g., insert a
record with id 'toRemove' before calling collectChunks), include a delete op in
the extractMemories array (op: 'delete', id: 'toRemove'), then after await
collectChunks(...) assert that memory.store.get('toRemove') is undefined or that
no record with that id exists to ensure the delete branch in the adapter is
exercised.

536-549: ⚡ Quick win

This doesn't prove updates/deletes are excluded from afterPersist.

In this setup afterPersist only ever sees the two base inserts, so the test would still pass if updated or deleted records were incorrectly included in newRecords. Add at least one update/delete in the same turn and assert the callback receives only the newly added records.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/typescript/ai/tests/middlewares/memory.test.ts` around lines 536 -
549, The test currently only inserts the base two records so it doesn't verify
updates/deletes are excluded from afterPersist; modify the test that creates the
stream (using chat, memoryMiddleware, afterPersist) to perform at least one
update or delete on the memory store within the same turn (e.g., call the memory
adapter's upsert/update or delete method targeting one of the baseScope records
before awaiting collectChunks), then assert afterPersist was called once and
that arg.newRecords includes only the newly inserted user+assistant records
(length === 2) and does NOT include the id(s) of the updated/deleted record(s);
reference the existing symbols afterPersist, memoryMiddleware, memory (adapter),
collectChunks, and MemoryRecord to locate and implement the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/typescript/ai/tests/middlewares/memory.test.ts`:
- Around line 362-379: The test currently uses rec() which pre-fills
kind:'fact', so change the mocked extractor to return a record object that omits
the kind property (e.g., replace rec({ text: 'extracted', kind: 'fact' }) with a
plain record without a kind field or use a helper that doesn't default kind) so
memoryMiddleware is forced to apply the default; keep the same assertions
(extractMemories, collectChunks, and the kinds check) and reference the existing
symbols extractMemories, rec, memoryMiddleware, and the kinds array to locate
and update the mock return value.

---

Duplicate comments:
In `@packages/typescript/ai-memory/package.json`:
- Line 53: Update the internal dependency entry for "@tanstack/ai" in
packages/typescript/ai-memory/package.json from "workspace:^" to "workspace:*";
locate the dependency key "@tanstack/ai" in the package.json dependencies block
and replace the version string so it follows the repo policy of using
workspace:* for internal peer dependencies.

---

Nitpick comments:
In `@packages/typescript/ai/tests/memory/helpers.test.ts`:
- Around line 1-10: The import specifiers in tests/memory/helpers.test.ts are
not alphabetically sorted causing an ESLint sort-imports violation; reorder the
named imports from '../../src/memory/helpers' so they are alphabetized (for
example ensure entries like cosine, defaultRenderMemory, defaultScoreHit,
isExpired, lexicalOverlap, recencyScore, scopeMatches appear in lexical order)
so the import line imports scopeMatches, cosine, lexicalOverlap, recencyScore,
defaultRenderMemory, defaultScoreHit, isExpired in sorted order per project
linting rules.

In `@packages/typescript/ai/tests/middlewares/memory.test.ts`:
- Around line 397-409: The test currently exercises only add/update paths but
should also cover delete; modify the test (the case that calls collectChunks and
uses extractMemories) to seed a removable record in memory.store (e.g., insert a
record with id 'toRemove' before calling collectChunks), include a delete op in
the extractMemories array (op: 'delete', id: 'toRemove'), then after await
collectChunks(...) assert that memory.store.get('toRemove') is undefined or that
no record with that id exists to ensure the delete branch in the adapter is
exercised.
- Around line 536-549: The test currently only inserts the base two records so
it doesn't verify updates/deletes are excluded from afterPersist; modify the
test that creates the stream (using chat, memoryMiddleware, afterPersist) to
perform at least one update or delete on the memory store within the same turn
(e.g., call the memory adapter's upsert/update or delete method targeting one of
the baseScope records before awaiting collectChunks), then assert afterPersist
was called once and that arg.newRecords includes only the newly inserted
user+assistant records (length === 2) and does NOT include the id(s) of the
updated/deleted record(s); reference the existing symbols afterPersist,
memoryMiddleware, memory (adapter), collectChunks, and MemoryRecord to locate
and implement the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e0f9355e-00e5-4596-96a9-d7c759a409de

📥 Commits

Reviewing files that changed from the base of the PR and between ed23b50 and b478e8e.

📒 Files selected for processing (13)
  • docs/memory/custom-adapter.md
  • docs/memory/quickstart.md
  • packages/typescript/ai-memory/package.json
  • packages/typescript/ai-memory/skills/tanstack-ai-memory-redis/SKILL.md
  • packages/typescript/ai-memory/src/adapters/in-memory.ts
  • packages/typescript/ai-memory/src/adapters/redis.ts
  • packages/typescript/ai-memory/tests/in-memory.test.ts
  • packages/typescript/ai-memory/tsconfig.json
  • packages/typescript/ai/src/memory/helpers.ts
  • packages/typescript/ai/src/memory/middleware.ts
  • packages/typescript/ai/src/memory/types.ts
  • packages/typescript/ai/tests/memory/helpers.test.ts
  • packages/typescript/ai/tests/middlewares/memory.test.ts
✅ Files skipped from review due to trivial changes (4)
  • packages/typescript/ai-memory/tsconfig.json
  • packages/typescript/ai-memory/tests/in-memory.test.ts
  • docs/memory/quickstart.md
  • packages/typescript/ai-memory/skills/tanstack-ai-memory-redis/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/typescript/ai/src/memory/helpers.ts
  • packages/typescript/ai-memory/src/adapters/in-memory.ts
  • packages/typescript/ai/src/memory/types.ts
  • packages/typescript/ai/src/memory/middleware.ts
  • packages/typescript/ai-memory/src/adapters/redis.ts

Comment on lines +362 to +379
const extractMemories = vi.fn(async () => [
rec({ text: 'extracted', kind: 'fact' }),
])
const stream = chat({
adapter,
messages: [{ role: 'user', content: 'U' }],
middleware: [
memoryMiddleware({
adapter: memory,
scope: baseScope,
extractMemories,
}),
],
})
await collectChunks(stream as AsyncIterable<StreamChunk>)
expect(extractMemories).toHaveBeenCalledTimes(1)
const kinds = [...memory.store.values()].map((r) => r.kind).sort()
expect(kinds).toEqual(['fact', 'message', 'message'])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

This test doesn't actually verify defaulting kind to fact.

rec() already injects kind: 'fact', so this still passes if middleware stops applying the default. Return a record without kind (or use a helper that doesn't prefill defaults) so the assertion protects the behavior named in the test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/typescript/ai/tests/middlewares/memory.test.ts` around lines 362 -
379, The test currently uses rec() which pre-fills kind:'fact', so change the
mocked extractor to return a record object that omits the kind property (e.g.,
replace rec({ text: 'extracted', kind: 'fact' }) with a plain record without a
kind field or use a helper that doesn't default kind) so memoryMiddleware is
forced to apply the default; keep the same assertions (extractMemories,
collectChunks, and the kinds check) and reference the existing symbols
extractMemories, rec, memoryMiddleware, and the kinds array to locate and update
the mock return value.

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.

1 participant