ta
TanStack
GitHub
ai
Workspace
GitHub
CI Pipeline Executions
Filtered
Runs
Demo
Insights
Compare tasks
Analytics
Sign in
Toggle sidebar
Overview
⌘K
ai
Loading workspace stats
Loading workspace insights...
Statistics interval
7 days
30 days
Latest CI Pipeline Executions
Status
Fix filter
Filter
Fuzzy
Filter range
Sort by
Sort by
Start time
Sort ascending
Sort descending
Succeeded
feat/openrouter-run-finished-metadata
e4df6249 feat(ai, ai-memory): server-side memory middleware with pluggable adapters (#541) * feat(ai): add memory types * feat(ai): add memory helper functions * feat(ai): expose @tanstack/ai/memory subpath * test(ai): add failing memory middleware tests * feat(ai): add memoryMiddleware * fix(ai): tighten memory middleware test types for noUncheckedIndexedAccess * feat(ai-event-client): add memory devtools events * feat(ai): emit memory devtools events from middleware * feat(ai-memory): scaffold new package * test(ai-memory): add shared adapter contract suite * feat(ai-memory): add inMemoryMemoryAdapter * fix(ai-memory): tighten in-memory adapter lint compliance * feat(ai-memory): add redisMemoryAdapter * docs(ai): add tanstack-ai-memory skill * docs(ai-memory): add in-memory adapter skill * docs(ai-memory): add redis adapter skill * docs: add memory middleware concept and quickstart pages * chore: changeset for memory middleware * chore: final formatting * fix(ai, ai-memory): clean up lint and knip findings * fix(ai, ai-memory): address whole-feature audit findings - 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 * ci: apply automated fixes * fix(ai): address CR Round 1 core middleware findings - 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) * fix(ai-memory): redis adapter scope semantics - 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 * feat(ai-memory): nodeRedisAsRedisLike helper for node-redis v4+ 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. * test(ai, ai-memory): tighten flaky and vacuous CR assertions - 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 * chore(ai-memory): set initial version to 0.0.0 for first publish 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. * fix(ai, ai-memory): address CR Round 2 bucket-a findings - 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 * fix(ai): close error-path observability gaps in memory middleware 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 * fix(ai, ai-memory): close remaining scope-value-validation gaps 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. * fix(ai-memory): escape _ in scope values to prevent placeholder collision 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). * chore: refresh pnpm-lock.yaml for ai-memory ioredis peer dep CI was failing with ERR_PNPM_OUTDATED_LOCKFILE because Group C added ioredis as an optional peer dependency without regenerating the lockfile. * ci: apply automated fixes * docs: consolidate memory pages into a top-level Memory section - 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 * fix(ai, ai-memory): address CodeRabbit code review feedback - 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 * docs, chore: address CodeRabbit polish feedback - 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 * fix(ai, ai-memory): address memory-middleware review findings Portable record ids - middleware.ts mints ids via crypto.randomUUID(), which is NOT a bare global on the declared Node 18 floor (Web Crypto became unflagged only in Node 19+) and threw ReferenceError there — silently dropping the whole turn's memory in non-strict mode. Add newRecordId() (real UUID when available, portable Date.now()+Math.random() fallback via try/catch). Close silent-failure gaps in the error plumbing - Wrap the scope resolver + shouldRetrieve (onConfig), the scope resolver (onAfterToolCall, onFinish), and shouldRemember (persistTurn) so a throwing user callback emits memory:error/onError and honours strict instead of escaping the hook and breaking chat in non-strict mode. - Make emitError defensive (like safeEmit) so a throwing onError handler can't break chat or mask the original failure. Honest strict-mode docs - Persistence runs via ctx.defer; the engine awaits deferred work with Promise.allSettled and discards results, so a strict-mode rethrow on the persist path does NOT abort the run. Correct the comments that claimed otherwise; memory:error is the observable signal in both modes. Redis: stop silently deleting malformed rows - loadAllForScope routed malformed JSON through the expired-sweep, permanently deleting possibly-recoverable rows behind a one-shot console.warn. Leave the row in place and skip it; warn per-distinct-id (bounded) so ongoing corruption keeps surfacing without spamming. Skill doc updated. Docs - Fix the memory overview's broken ../advanced/observability link (page moved on main) -> ../getting-started/devtools. Tests (+4) - Cross-turn persist->retrieve round trip (the feature's headline behaviour). - Throwing scope resolver / throwing shouldRemember don't break chat + emit memory:error. - Redis malformed row is skipped on read but NOT deleted. Gates: @tanstack/ai 1186 tests, @tanstack/ai-memory 72 tests, test:types, test:eslint (0 errors), knip, sherif, test:docs all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(memory): fix kiira doc-snippet type errors The memory docs failed `kiira check` (89 errors) — the doc code-snippet type-checker in CI's Test job. Fixes: - quickstart: inline `messages` instead of an undeclared var; self-contain the embedder example (adapter + scope) and guard the possibly-undefined embedding vector; `ignore` the ioredis swap (ioredis's `Redis` type is structurally broader than the minimal `RedisLike` contract) and the server-side scope-derivation pattern (app-defined context). - overview: drop the `MemoryScope` import that conflicted with the local type illustration; `ignore` the scope-derivation pattern block. - custom-adapter: drop the `MemoryAdapter` import that conflicted with the local interface illustration; `ignore` the pgvector scaffold, the method- body fragments, the contract-test file, and the wire-in snippet — all depend on the `pg` peer dep, elided bodies, or relative modules. No behavior change; docs-only. `test:kiira` and `test:docs` pass locally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(memory): recall/save adapter contract, consolidate into @tanstack/ai-memory Replace the CRUD MemoryAdapter contract with a single recall/save contract — the shape every memory backend naturally exposes — and move the middleware, contract, and helpers out of @tanstack/ai into @tanstack/ai-memory. Core no longer carries any memory code (the unreleased @tanstack/ai/memory subpath is removed). - Contract: MemoryAdapter = { id, recall(scope, query), save(scope, turn), inspect?, listFacts? } over a session-centric MemoryScope. recall returns a rendered systemPrompt plus optional fragments and LLM tools/toolGuidance; save persists a { user, assistant } turn. Extraction/ranking/rendering live in the adapter, not the middleware. - Thin memoryMiddleware: recall-on-init injects prompt + tools, deferred save-on-finish, memory:* devtools events, onRecall/onSave callbacks, and a save-only role. composeMemoryMiddleware stacks adapters. - Adapters on flat subpaths: inMemory(), redis() (BYO client, preserves the malformed-row + delimiter-escaping hardening), and vendor adapters hindsight(), mem0(), honcho() with lazily-loaded optional-peer SDKs. - Shared recall/save contract-test suite; redis hardening tests kept; new middleware unit test; a memory scenario added to the e2e middleware harness. - Docs rewritten (overview, quickstart, adapters, custom-adapter) with every option documented and an example of each; skills moved/rewritten; changeset, knip, and event payloads updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: apply automated fixes * refactor(memory): nest in-memory/redis under providers, add provider tests Move the built-in inMemory()/redis() adapters into src/providers/ so all adapters share one layout, and mirror that structure under tests/providers/. Public subpaths (@tanstack/ai-memory/in-memory, /redis) are unchanged — only internal file locations moved (package.json exports + vite entries updated). Add non-networked unit tests for the vendor providers: hindsight (fake runtime driving makeHindsightTools), mem0 (stubbed fetch), and honcho (mocked SDK + pure representation parser). Drop composeMemoryMiddleware — the recall/save contract + save-only role cover multi-backend use without a dedicated combinator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(panel): add in-memory Memory demo page Adds a `/memory` route to testing/panel that wires memoryMiddleware with the in-memory adapter into a chat and shows what's stored, letting you watch recall/save work end-to-end. - Shared inMemory() singleton (src/lib/memory-store.ts) so the chat route (writes via middleware) and the inspect route (reads via inspect/listFacts) hit the same process-local store. - api.memory-chat.ts: chat with memoryMiddleware, scope keyed on a client sessionId; onRecall records the injected recall for display. - api.memory-inspect.ts: GET returning { snapshot, facts, lastRecall }. - memory.tsx: two-pane UI — chat on the left, live memory inspector on the right (last recalled prompt, stored records, listFacts), sessionId persisted in localStorage with New session / Refresh controls. - Header nav link. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(devtools): surface memory state in the AI DevTools Add a Memory tab to the TanStack AI DevTools for chat hooks wired with `memoryMiddleware`. Per session scope it shows an operations timeline (each turn's recall — query, fragment count, injected prompt size, tools, duration) and the current stored records/facts when the adapter implements `inspect`/`listFacts`. The tab is chat-only (hidden for generation hooks). Server-side memory never reaches the browser event bus, so the middleware transports its state over the chat stream as a `memory:state` CUSTOM event (recall metrics + a start-of-turn snapshot). The chat client routes that event through its `onCustomEvent` handler — the designated path for CUSTOM stream events — to a first-class `ClientDevtoolsBridge.recordMemoryState`, which re-emits the browser `memory:*` events (mirroring how generation results reach the panel). The bridge caches the last state and replays it on `devtools:request-state`, so opening the panel mid-conversation isn't empty. - ai-event-client: add the `memory:snapshot` event. - ai-memory: inject the `memory:state` CUSTOM chunk from `onChunk`; export `MEMORY_STATE_EVENT` / `MemoryStateEventValue`. - ai-client: `recordMemoryState` bridge method (+ no-op parity), wired from `onCustomEvent`; replay on request-state. - ai-devtools-core: Memory tab + per-scope memory store slice. - testing/panel: mount `<TanStackDevtools>` so the panel exposes the DevTools (and the /memory demo hook is named). - e2e: devtools-memory route + spec covering the full browser flow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(memory): polish memory guides and rename node-redis wrapper Docs pass over docs/memory: lead each page with the reader's problem, drop em dashes, and un-ignore the Redis code samples so they typecheck. Add an Operating page and point its devtools section at the Memory Inspector. Rename the node-redis wrapper nodeRedisAsRedisLike -> fromNodeRedis across source, tests, skill, changeset, and docs, and add ioredis to the kiira dependency resolver so the un-ignored samples check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Jack Herrington <jherr@pobox.com>
by Alem Tuzlak
A
Failed
feat/openrouter-run-finished-metadata
e4df6249 feat(ai, ai-memory): server-side memory middleware with pluggable adapters (#541) * feat(ai): add memory types * feat(ai): add memory helper functions * feat(ai): expose @tanstack/ai/memory subpath * test(ai): add failing memory middleware tests * feat(ai): add memoryMiddleware * fix(ai): tighten memory middleware test types for noUncheckedIndexedAccess * feat(ai-event-client): add memory devtools events * feat(ai): emit memory devtools events from middleware * feat(ai-memory): scaffold new package * test(ai-memory): add shared adapter contract suite * feat(ai-memory): add inMemoryMemoryAdapter * fix(ai-memory): tighten in-memory adapter lint compliance * feat(ai-memory): add redisMemoryAdapter * docs(ai): add tanstack-ai-memory skill * docs(ai-memory): add in-memory adapter skill * docs(ai-memory): add redis adapter skill * docs: add memory middleware concept and quickstart pages * chore: changeset for memory middleware * chore: final formatting * fix(ai, ai-memory): clean up lint and knip findings * fix(ai, ai-memory): address whole-feature audit findings - 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 * ci: apply automated fixes * fix(ai): address CR Round 1 core middleware findings - 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) * fix(ai-memory): redis adapter scope semantics - 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 * feat(ai-memory): nodeRedisAsRedisLike helper for node-redis v4+ 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. * test(ai, ai-memory): tighten flaky and vacuous CR assertions - 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 * chore(ai-memory): set initial version to 0.0.0 for first publish 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. * fix(ai, ai-memory): address CR Round 2 bucket-a findings - 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 * fix(ai): close error-path observability gaps in memory middleware 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 * fix(ai, ai-memory): close remaining scope-value-validation gaps 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. * fix(ai-memory): escape _ in scope values to prevent placeholder collision 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). * chore: refresh pnpm-lock.yaml for ai-memory ioredis peer dep CI was failing with ERR_PNPM_OUTDATED_LOCKFILE because Group C added ioredis as an optional peer dependency without regenerating the lockfile. * ci: apply automated fixes * docs: consolidate memory pages into a top-level Memory section - 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 * fix(ai, ai-memory): address CodeRabbit code review feedback - 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 * docs, chore: address CodeRabbit polish feedback - 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 * fix(ai, ai-memory): address memory-middleware review findings Portable record ids - middleware.ts mints ids via crypto.randomUUID(), which is NOT a bare global on the declared Node 18 floor (Web Crypto became unflagged only in Node 19+) and threw ReferenceError there — silently dropping the whole turn's memory in non-strict mode. Add newRecordId() (real UUID when available, portable Date.now()+Math.random() fallback via try/catch). Close silent-failure gaps in the error plumbing - Wrap the scope resolver + shouldRetrieve (onConfig), the scope resolver (onAfterToolCall, onFinish), and shouldRemember (persistTurn) so a throwing user callback emits memory:error/onError and honours strict instead of escaping the hook and breaking chat in non-strict mode. - Make emitError defensive (like safeEmit) so a throwing onError handler can't break chat or mask the original failure. Honest strict-mode docs - Persistence runs via ctx.defer; the engine awaits deferred work with Promise.allSettled and discards results, so a strict-mode rethrow on the persist path does NOT abort the run. Correct the comments that claimed otherwise; memory:error is the observable signal in both modes. Redis: stop silently deleting malformed rows - loadAllForScope routed malformed JSON through the expired-sweep, permanently deleting possibly-recoverable rows behind a one-shot console.warn. Leave the row in place and skip it; warn per-distinct-id (bounded) so ongoing corruption keeps surfacing without spamming. Skill doc updated. Docs - Fix the memory overview's broken ../advanced/observability link (page moved on main) -> ../getting-started/devtools. Tests (+4) - Cross-turn persist->retrieve round trip (the feature's headline behaviour). - Throwing scope resolver / throwing shouldRemember don't break chat + emit memory:error. - Redis malformed row is skipped on read but NOT deleted. Gates: @tanstack/ai 1186 tests, @tanstack/ai-memory 72 tests, test:types, test:eslint (0 errors), knip, sherif, test:docs all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(memory): fix kiira doc-snippet type errors The memory docs failed `kiira check` (89 errors) — the doc code-snippet type-checker in CI's Test job. Fixes: - quickstart: inline `messages` instead of an undeclared var; self-contain the embedder example (adapter + scope) and guard the possibly-undefined embedding vector; `ignore` the ioredis swap (ioredis's `Redis` type is structurally broader than the minimal `RedisLike` contract) and the server-side scope-derivation pattern (app-defined context). - overview: drop the `MemoryScope` import that conflicted with the local type illustration; `ignore` the scope-derivation pattern block. - custom-adapter: drop the `MemoryAdapter` import that conflicted with the local interface illustration; `ignore` the pgvector scaffold, the method- body fragments, the contract-test file, and the wire-in snippet — all depend on the `pg` peer dep, elided bodies, or relative modules. No behavior change; docs-only. `test:kiira` and `test:docs` pass locally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(memory): recall/save adapter contract, consolidate into @tanstack/ai-memory Replace the CRUD MemoryAdapter contract with a single recall/save contract — the shape every memory backend naturally exposes — and move the middleware, contract, and helpers out of @tanstack/ai into @tanstack/ai-memory. Core no longer carries any memory code (the unreleased @tanstack/ai/memory subpath is removed). - Contract: MemoryAdapter = { id, recall(scope, query), save(scope, turn), inspect?, listFacts? } over a session-centric MemoryScope. recall returns a rendered systemPrompt plus optional fragments and LLM tools/toolGuidance; save persists a { user, assistant } turn. Extraction/ranking/rendering live in the adapter, not the middleware. - Thin memoryMiddleware: recall-on-init injects prompt + tools, deferred save-on-finish, memory:* devtools events, onRecall/onSave callbacks, and a save-only role. composeMemoryMiddleware stacks adapters. - Adapters on flat subpaths: inMemory(), redis() (BYO client, preserves the malformed-row + delimiter-escaping hardening), and vendor adapters hindsight(), mem0(), honcho() with lazily-loaded optional-peer SDKs. - Shared recall/save contract-test suite; redis hardening tests kept; new middleware unit test; a memory scenario added to the e2e middleware harness. - Docs rewritten (overview, quickstart, adapters, custom-adapter) with every option documented and an example of each; skills moved/rewritten; changeset, knip, and event payloads updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: apply automated fixes * refactor(memory): nest in-memory/redis under providers, add provider tests Move the built-in inMemory()/redis() adapters into src/providers/ so all adapters share one layout, and mirror that structure under tests/providers/. Public subpaths (@tanstack/ai-memory/in-memory, /redis) are unchanged — only internal file locations moved (package.json exports + vite entries updated). Add non-networked unit tests for the vendor providers: hindsight (fake runtime driving makeHindsightTools), mem0 (stubbed fetch), and honcho (mocked SDK + pure representation parser). Drop composeMemoryMiddleware — the recall/save contract + save-only role cover multi-backend use without a dedicated combinator. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(panel): add in-memory Memory demo page Adds a `/memory` route to testing/panel that wires memoryMiddleware with the in-memory adapter into a chat and shows what's stored, letting you watch recall/save work end-to-end. - Shared inMemory() singleton (src/lib/memory-store.ts) so the chat route (writes via middleware) and the inspect route (reads via inspect/listFacts) hit the same process-local store. - api.memory-chat.ts: chat with memoryMiddleware, scope keyed on a client sessionId; onRecall records the injected recall for display. - api.memory-inspect.ts: GET returning { snapshot, facts, lastRecall }. - memory.tsx: two-pane UI — chat on the left, live memory inspector on the right (last recalled prompt, stored records, listFacts), sessionId persisted in localStorage with New session / Refresh controls. - Header nav link. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(devtools): surface memory state in the AI DevTools Add a Memory tab to the TanStack AI DevTools for chat hooks wired with `memoryMiddleware`. Per session scope it shows an operations timeline (each turn's recall — query, fragment count, injected prompt size, tools, duration) and the current stored records/facts when the adapter implements `inspect`/`listFacts`. The tab is chat-only (hidden for generation hooks). Server-side memory never reaches the browser event bus, so the middleware transports its state over the chat stream as a `memory:state` CUSTOM event (recall metrics + a start-of-turn snapshot). The chat client routes that event through its `onCustomEvent` handler — the designated path for CUSTOM stream events — to a first-class `ClientDevtoolsBridge.recordMemoryState`, which re-emits the browser `memory:*` events (mirroring how generation results reach the panel). The bridge caches the last state and replays it on `devtools:request-state`, so opening the panel mid-conversation isn't empty. - ai-event-client: add the `memory:snapshot` event. - ai-memory: inject the `memory:state` CUSTOM chunk from `onChunk`; export `MEMORY_STATE_EVENT` / `MemoryStateEventValue`. - ai-client: `recordMemoryState` bridge method (+ no-op parity), wired from `onCustomEvent`; replay on request-state. - ai-devtools-core: Memory tab + per-scope memory store slice. - testing/panel: mount `<TanStackDevtools>` so the panel exposes the DevTools (and the /memory demo hook is named). - e2e: devtools-memory route + spec covering the full browser flow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(memory): polish memory guides and rename node-redis wrapper Docs pass over docs/memory: lead each page with the reader's problem, drop em dashes, and un-ignore the Redis code samples so they typecheck. Add an Operating page and point its devtools section at the Memory Inspector. Rename the node-redis wrapper nodeRedisAsRedisLike -> fromNodeRedis across source, tests, skill, changeset, and docs, and add ioredis to the kiira dependency resolver so the un-ignored samples check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Jack Herrington <jherr@pobox.com>
by Alem Tuzlak
A
Previous
Next