@tanstack/ai-memory 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/internal/store.d.ts +95 -0
- package/dist/esm/internal/store.js +201 -0
- package/dist/esm/internal/store.js.map +1 -0
- package/dist/esm/internal/store.test.d.ts +1 -0
- package/dist/esm/middleware.d.ts +73 -0
- package/dist/esm/middleware.js +250 -0
- package/dist/esm/middleware.js.map +1 -0
- package/dist/esm/providers/hindsight/index.d.ts +50 -0
- package/dist/esm/providers/hindsight/index.js +152 -0
- package/dist/esm/providers/hindsight/index.js.map +1 -0
- package/dist/esm/providers/hindsight/tools.d.ts +16 -0
- package/dist/esm/providers/hindsight/tools.js +119 -0
- package/dist/esm/providers/hindsight/tools.js.map +1 -0
- package/dist/esm/providers/honcho/index.d.ts +16 -0
- package/dist/esm/providers/honcho/index.js +150 -0
- package/dist/esm/providers/honcho/index.js.map +1 -0
- package/dist/esm/providers/in-memory/index.d.ts +19 -0
- package/dist/esm/providers/in-memory/index.js +46 -0
- package/dist/esm/providers/in-memory/index.js.map +1 -0
- package/dist/esm/providers/mem0/index.d.ts +14 -0
- package/dist/esm/providers/mem0/index.js +150 -0
- package/dist/esm/providers/mem0/index.js.map +1 -0
- package/dist/esm/providers/redis/index.d.ts +54 -0
- package/dist/esm/providers/redis/index.js +118 -0
- package/dist/esm/providers/redis/index.js.map +1 -0
- package/dist/esm/types.d.ts +112 -0
- package/package.json +100 -0
- package/skills/tanstack-ai-memory/SKILL.md +99 -0
- package/skills/tanstack-ai-memory-hindsight/SKILL.md +40 -0
- package/skills/tanstack-ai-memory-honcho/SKILL.md +40 -0
- package/skills/tanstack-ai-memory-in-memory/SKILL.md +51 -0
- package/skills/tanstack-ai-memory-mem0/SKILL.md +36 -0
- package/skills/tanstack-ai-memory-redis/SKILL.md +83 -0
- package/src/index.ts +20 -0
- package/src/internal/store.test.ts +63 -0
- package/src/internal/store.ts +378 -0
- package/src/middleware.ts +386 -0
- package/src/providers/hindsight/index.ts +237 -0
- package/src/providers/hindsight/tools.ts +139 -0
- package/src/providers/honcho/index.ts +238 -0
- package/src/providers/in-memory/index.ts +63 -0
- package/src/providers/mem0/index.ts +199 -0
- package/src/providers/redis/index.ts +179 -0
- package/src/types.ts +159 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanstack-ai-memory
|
|
3
|
+
description: Use when wiring memoryMiddleware from @tanstack/ai-memory into a chat() call — covers the recall/save adapter contract, scope shape and server-side scope security, the recall-inject / deferred-save lifecycle, choosing an adapter (inMemory, redis, hindsight, mem0, honcho), and devtools events.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TanStack AI Memory Middleware
|
|
7
|
+
|
|
8
|
+
Use this when adding **server-side memory** to a `chat()` call. Everything lives in
|
|
9
|
+
`@tanstack/ai-memory`. A memory adapter is a single contract with two verbs — `recall`
|
|
10
|
+
and `save` — and the middleware is thin: it recalls into the system prompt before the
|
|
11
|
+
model runs and defers `save` after the turn finishes.
|
|
12
|
+
|
|
13
|
+
## When to reach for it
|
|
14
|
+
|
|
15
|
+
- A user expects "remember what I told you last time."
|
|
16
|
+
- Per-user or per-thread context that must survive across sessions.
|
|
17
|
+
- A hosted memory service (mem0, Honcho, Hindsight).
|
|
18
|
+
|
|
19
|
+
Do NOT use this just to keep recent messages — that's the `messages` array on `chat()`.
|
|
20
|
+
Memory is for cross-turn / cross-session recall, not within-turn history.
|
|
21
|
+
|
|
22
|
+
## Wire it up
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { chat } from '@tanstack/ai'
|
|
26
|
+
import { openaiText } from '@tanstack/ai-openai'
|
|
27
|
+
import { memoryMiddleware } from '@tanstack/ai-memory'
|
|
28
|
+
import { inMemory } from '@tanstack/ai-memory/in-memory'
|
|
29
|
+
|
|
30
|
+
const memory = inMemory() // dev/tests only — see the in-memory skill
|
|
31
|
+
|
|
32
|
+
const stream = chat({
|
|
33
|
+
adapter: openaiText('gpt-5.5'),
|
|
34
|
+
messages,
|
|
35
|
+
context: { session }, // attached by your auth middleware
|
|
36
|
+
middleware: [
|
|
37
|
+
memoryMiddleware({
|
|
38
|
+
adapter: memory,
|
|
39
|
+
// Derive scope server-side from trusted session state.
|
|
40
|
+
scope: (ctx) => {
|
|
41
|
+
const session = getSession(ctx)
|
|
42
|
+
return { threadId: session.threadId, userId: session.userId }
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
],
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`memoryMiddleware` options: `adapter`, `scope` (static or a function of `ctx`),
|
|
50
|
+
`role` (`'recall+save'` default, or `'save-only'`), and `onRecall` / `onSave` telemetry
|
|
51
|
+
callbacks.
|
|
52
|
+
|
|
53
|
+
## The contract
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
interface MemoryAdapter {
|
|
57
|
+
id: string
|
|
58
|
+
recall(scope, query): Promise<RecallResult> // { systemPrompt, fragments?, tools?, toolGuidance? }
|
|
59
|
+
save(scope, turn): Promise<Array<SaveReceipt>> // turn = { user, assistant }; extraction lives HERE
|
|
60
|
+
inspect?(scope): Promise<MemorySnapshot> // optional (devtools)
|
|
61
|
+
listFacts?(scope): Promise<Array<MemoryFact>> // optional (devtools)
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- `recall` decides relevance and renders a `systemPrompt`; it may also return `tools` +
|
|
66
|
+
`toolGuidance` to hand the model direct control of memory (hindsight does this).
|
|
67
|
+
- `save` owns extraction — turning the raw turn into whatever gets persisted.
|
|
68
|
+
|
|
69
|
+
## Scope security
|
|
70
|
+
|
|
71
|
+
`MemoryScope` is an alias of the shared `Scope` type from `@tanstack/ai`:
|
|
72
|
+
`{ threadId, userId?, tenantId?, namespace? }`. It is the isolation boundary. **Never
|
|
73
|
+
trust a client-supplied `userId`/`threadId`.** Resolve scope server-side from
|
|
74
|
+
session/auth and pass the validated session through `chat({ context: { session } })`. If
|
|
75
|
+
you accept a thread id from the request body, validate it belongs to the session user
|
|
76
|
+
BEFORE using it.
|
|
77
|
+
|
|
78
|
+
## Adapters
|
|
79
|
+
|
|
80
|
+
- `inMemory()` / `redis()` — exact match on `threadId` + optional `userId`/`tenantId`
|
|
81
|
+
(`namespace` ignored). Redis index keys include all three segments.
|
|
82
|
+
- `hindsight()` — bank `{tenant|_}__{user}__{threadId}`.
|
|
83
|
+
- `mem0()` — `user_id` + `run_id` (`threadId`); no `tenantId`.
|
|
84
|
+
- `honcho()` — session `{tenant|_}__{threadId}`; peer tenant-prefixed when set.
|
|
85
|
+
- Custom — implement `recall`/`save` and run `@tanstack/ai-memory/tests/contract`.
|
|
86
|
+
|
|
87
|
+
## Failure modes
|
|
88
|
+
|
|
89
|
+
Memory failures are non-fatal: a throwing `recall` or `save` emits `memory:error` and
|
|
90
|
+
the run continues with degraded memory. Streaming is never blocked; a failed save never
|
|
91
|
+
fails the turn.
|
|
92
|
+
|
|
93
|
+
## Devtools
|
|
94
|
+
|
|
95
|
+
Five events on `aiEventClient` (from `@tanstack/ai-event-client`):
|
|
96
|
+
`memory:retrieve:started` / `:completed`, `memory:persist:started` / `:completed`,
|
|
97
|
+
`memory:error` (`phase: 'recall' | 'save'`). Payloads carry the adapter id and
|
|
98
|
+
fragment/receipt counts, not full memory text. Error events include `scope` only
|
|
99
|
+
when it was already resolved; if the resolver threw, `scope` is omitted.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanstack-ai-memory-hindsight
|
|
3
|
+
description: Use when wiring hindsight() from @tanstack/ai-memory/hindsight — a hosted memory adapter that buckets memory per conversation and exposes retain/recall/reflect tools to the model. Requires the optional @vectorize-io/hindsight-client peer.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Hindsight Memory Adapter
|
|
7
|
+
|
|
8
|
+
Hosted `recall`/`save` adapter backed by Hindsight. Hindsight owns extraction and
|
|
9
|
+
ranking server-side, buckets memory into per-conversation "banks"
|
|
10
|
+
(`{tenantId|_}__{user}__{threadId}`), and — uniquely — exposes LLM **tools** through `recall` so the
|
|
11
|
+
model can retain/recall/reflect directly.
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { memoryMiddleware } from '@tanstack/ai-memory'
|
|
17
|
+
import { hindsight } from '@tanstack/ai-memory/hindsight'
|
|
18
|
+
|
|
19
|
+
const memory = hindsight({ user: currentUserId }) // baseUrl defaults to HINDSIGHT_URL
|
|
20
|
+
|
|
21
|
+
memoryMiddleware({ adapter: memory, scope })
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`@vectorize-io/hindsight-client` is an **optional peer dependency**, loaded lazily on
|
|
25
|
+
first use — install it where you use `hindsight()`.
|
|
26
|
+
|
|
27
|
+
## Options
|
|
28
|
+
|
|
29
|
+
- `user` — durable user id for the bank key (falls back to `scope.userId`).
|
|
30
|
+
- `baseUrl` — Hindsight server URL (default `HINDSIGHT_URL` or `http://localhost:8888`).
|
|
31
|
+
- `budget` — recall budget: `'low' | 'mid' | 'high'` (default `'mid'`).
|
|
32
|
+
- `onToolRetain` / `onToolRecall` — callbacks fired when the model uses the memory tools.
|
|
33
|
+
|
|
34
|
+
**Scope fields:** bank id is `{tenantId|_}__{user}__{threadId}`. `namespace` is ignored.
|
|
35
|
+
|
|
36
|
+
## Tools
|
|
37
|
+
|
|
38
|
+
`recall` returns `hindsight_retain`, `hindsight_recall`, and `hindsight_reflect` in its
|
|
39
|
+
`tools` plus a `toolGuidance` block. `memoryMiddleware` merges them into the run so the
|
|
40
|
+
model can manage long-term memory itself.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanstack-ai-memory-honcho
|
|
3
|
+
description: Use when wiring honcho() from @tanstack/ai-memory/honcho — a hosted memory adapter where recall is a dialectic answer over the user's representation (no discrete fragments). Requires the optional @honcho-ai/sdk peer.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Honcho Memory Adapter
|
|
7
|
+
|
|
8
|
+
Hosted `recall`/`save` adapter backed by Honcho. Honcho models memory as peers
|
|
9
|
+
exchanging messages in a session; `recall` returns a **synthesized dialectic answer**
|
|
10
|
+
over the user peer's representation (so there are no discrete fragments), and `save`
|
|
11
|
+
appends the turn's messages to the session.
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { memoryMiddleware } from '@tanstack/ai-memory'
|
|
17
|
+
import { honcho } from '@tanstack/ai-memory/honcho'
|
|
18
|
+
|
|
19
|
+
const memory = honcho({ user: currentUserId }) // baseURL defaults to HONCHO_URL
|
|
20
|
+
|
|
21
|
+
memoryMiddleware({ adapter: memory, scope })
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`@honcho-ai/sdk` is an **optional peer dependency**, loaded lazily on first use — install
|
|
25
|
+
it where you use `honcho()`.
|
|
26
|
+
|
|
27
|
+
## Options
|
|
28
|
+
|
|
29
|
+
- `user` — user peer id (falls back to `scope.userId`, then `'demo-user'`).
|
|
30
|
+
- `baseURL` — Honcho server URL (default `HONCHO_URL` or `http://localhost:8001`).
|
|
31
|
+
- `workspaceId` — default `HONCHO_APP_NAME` or `'ai-memory'`.
|
|
32
|
+
- `apiKey` — default `HONCHO_API_KEY`.
|
|
33
|
+
- `assistantId` — assistant peer id (default `'assistant'`).
|
|
34
|
+
|
|
35
|
+
**Scope fields:** session key = `{tenantId|_}__{threadId}`; peer id is
|
|
36
|
+
`{tenantId}__{user}` when `tenantId` is set, otherwise `user` / `scope.userId`.
|
|
37
|
+
`namespace` is ignored.
|
|
38
|
+
|
|
39
|
+
`recall` calls the user peer's dialectic `chat()` and injects the answer as the system
|
|
40
|
+
prompt; Honcho exposes no LLM tools.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanstack-ai-memory-in-memory
|
|
3
|
+
description: Use when wiring inMemory() from @tanstack/ai-memory/in-memory — explains setup, options (embedder, extract, topK/minScore), when to pick it (dev/tests/single-process demos), and what NOT to use it for (multi-process or persistent).
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# In-Memory Memory Adapter
|
|
7
|
+
|
|
8
|
+
Zero-dependency `recall`/`save` adapter backed by a `Map`. Records vanish on process
|
|
9
|
+
restart.
|
|
10
|
+
|
|
11
|
+
## When to use it
|
|
12
|
+
|
|
13
|
+
- Local development.
|
|
14
|
+
- Vitest / Playwright tests.
|
|
15
|
+
- Single-process demos where users don't need persistence.
|
|
16
|
+
|
|
17
|
+
## When NOT to use it
|
|
18
|
+
|
|
19
|
+
- Production multi-process deployments — every worker has its own `Map`; users get
|
|
20
|
+
inconsistent memory.
|
|
21
|
+
- Anything that needs survival across restarts.
|
|
22
|
+
|
|
23
|
+
For production, use `redis()` (see the `tanstack-ai-memory-redis` skill).
|
|
24
|
+
|
|
25
|
+
## Setup
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { memoryMiddleware } from '@tanstack/ai-memory'
|
|
29
|
+
import { inMemory } from '@tanstack/ai-memory/in-memory'
|
|
30
|
+
|
|
31
|
+
const memory = inMemory()
|
|
32
|
+
|
|
33
|
+
memoryMiddleware({ adapter: memory, scope })
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Options
|
|
37
|
+
|
|
38
|
+
`inMemory(options?)` accepts:
|
|
39
|
+
|
|
40
|
+
- `topK` (default 6), `minScore` (default 0.15), `kinds` — recall tuning.
|
|
41
|
+
- `embedder: { embed(text): Promise<number[]> }` — enable semantic scoring (both
|
|
42
|
+
`recall` and `save` embed through it).
|
|
43
|
+
- `extract(turn, scope)` — return derived facts to persist alongside the raw turn
|
|
44
|
+
(e.g. call an LLM to pull out preferences). Without it, `save` stores the raw
|
|
45
|
+
user/assistant messages and `recall` scores them lexically + by recency.
|
|
46
|
+
- `render(hits)` — replace the built-in prompt renderer.
|
|
47
|
+
|
|
48
|
+
## Capacity
|
|
49
|
+
|
|
50
|
+
The adapter scans every record in a scope per `recall`. Fine up to ~100k records; beyond
|
|
51
|
+
that, switch to Redis.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanstack-ai-memory-mem0
|
|
3
|
+
description: Use when wiring mem0() from @tanstack/ai-memory/mem0 — a hosted memory adapter that talks to a mem0 server over plain HTTP (no SDK peer). Requires a running mem0 server.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# mem0 Memory Adapter
|
|
7
|
+
|
|
8
|
+
Hosted `recall`/`save` adapter backed by a mem0 server. mem0 owns extraction and ranking
|
|
9
|
+
server-side. Talks to the server over plain HTTP — **no SDK peer dependency**.
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { memoryMiddleware } from '@tanstack/ai-memory'
|
|
15
|
+
import { mem0 } from '@tanstack/ai-memory/mem0'
|
|
16
|
+
|
|
17
|
+
const memory = mem0({ user: currentUserId }) // baseUrl defaults to MEM0_URL
|
|
18
|
+
|
|
19
|
+
memoryMiddleware({ adapter: memory, scope })
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Requires a running mem0 server (self-hosted or hosted). Point it via `baseUrl` (or
|
|
23
|
+
`MEM0_URL`); pass `apiKey` (or `MEM0_ADMIN_API_KEY`) when secured.
|
|
24
|
+
|
|
25
|
+
## Options
|
|
26
|
+
|
|
27
|
+
- `user` — mem0 `user_id` (falls back to `scope.userId`, then `'demo-user'`).
|
|
28
|
+
- `baseUrl` — mem0 server URL (default `MEM0_URL` or `http://localhost:8000`).
|
|
29
|
+
- `apiKey` — bearer token (default `MEM0_ADMIN_API_KEY`).
|
|
30
|
+
- `rerank` (default `true`), `threshold` (default `0.1`) — search tuning.
|
|
31
|
+
|
|
32
|
+
**Scope fields:** requests send `user_id` and `run_id` (`scope.threadId`). `tenantId`
|
|
33
|
+
and `namespace` are not sent — encode multi-tenant isolation into `user` if needed.
|
|
34
|
+
|
|
35
|
+
`save` posts the `{ user, assistant }` turn to `/memories`; `recall` queries `/search`
|
|
36
|
+
and renders the results into the system prompt. mem0 exposes no LLM tools.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanstack-ai-memory-redis
|
|
3
|
+
description: Use when wiring redis() from @tanstack/ai-memory/redis in production — covers client setup (ioredis or node-redis via fromNodeRedis), the storage model, client-side ranking limits, and troubleshooting.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Redis Memory Adapter
|
|
7
|
+
|
|
8
|
+
Production-grade `recall`/`save` adapter backed by plain Redis (no vector index
|
|
9
|
+
required). Ranks client-side (lexical + optional cosine + recency + importance).
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
Bring your own Redis client. `ioredis` wires in directly; `redis` (node-redis v4+) needs
|
|
14
|
+
a small wrapper.
|
|
15
|
+
|
|
16
|
+
### Option A: `ioredis`
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import Redis from 'ioredis'
|
|
20
|
+
import { memoryMiddleware } from '@tanstack/ai-memory'
|
|
21
|
+
import { redis } from '@tanstack/ai-memory/redis'
|
|
22
|
+
|
|
23
|
+
const client = new Redis(process.env.REDIS_URL)
|
|
24
|
+
const memory = redis({ redis: client, prefix: 'myapp:memory' })
|
|
25
|
+
|
|
26
|
+
memoryMiddleware({ adapter: memory, scope })
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Option B: `redis` (node-redis v4+)
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { createClient } from 'redis'
|
|
33
|
+
import { memoryMiddleware } from '@tanstack/ai-memory'
|
|
34
|
+
import { redis, fromNodeRedis } from '@tanstack/ai-memory/redis'
|
|
35
|
+
|
|
36
|
+
const client = createClient({ url: process.env.REDIS_URL })
|
|
37
|
+
await client.connect()
|
|
38
|
+
|
|
39
|
+
const memory = redis({
|
|
40
|
+
redis: fromNodeRedis(client),
|
|
41
|
+
prefix: 'myapp:memory',
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
memoryMiddleware({ adapter: memory, scope })
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
node-redis exposes a camelCase API (`sAdd`, `mGet`); `fromNodeRedis` translates it
|
|
48
|
+
to the lowercase `RedisLike` shape. Passing a raw node-redis client without the wrapper
|
|
49
|
+
throws `client.sadd is not a function`.
|
|
50
|
+
|
|
51
|
+
`redis()` accepts the same `topK` / `minScore` / `kinds` / `embedder` / `extract` options
|
|
52
|
+
as `inMemory()`.
|
|
53
|
+
|
|
54
|
+
## Storage model
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
{prefix}:record:{id} -> JSON record
|
|
58
|
+
{prefix}:index:{tenantId or _}:{userId or _}:{threadId} -> Set<id>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`save` writes the record and adds it to the scope's index set; `recall` loads the set,
|
|
62
|
+
scores, and renders. Scope values are escaped (`:`, `\`, and `_`) so a delimiter or the
|
|
63
|
+
unset placeholder inside a dim can't collide two scopes.
|
|
64
|
+
|
|
65
|
+
**Hard cut:** there is no dual-read of older index layouts. If you previously wrote under
|
|
66
|
+
a different shape (e.g. without `tenantId`), reindex or wipe — old keys are orphaned.
|
|
67
|
+
|
|
68
|
+
Always pass the same `tenantId`/`userId`/`threadId` on write and read: missing optional
|
|
69
|
+
dims become `_`, so omit ≠ "match any".
|
|
70
|
+
|
|
71
|
+
## Ranking limits
|
|
72
|
+
|
|
73
|
+
Ranking is client-side: `recall` loads every record for the scope into Node and scores
|
|
74
|
+
it. Fine up to ~10k records per scope. Beyond that, write a vector-index-aware adapter
|
|
75
|
+
against the same `recall`/`save` contract.
|
|
76
|
+
|
|
77
|
+
## Troubleshooting
|
|
78
|
+
|
|
79
|
+
- **Records not visible across processes:** ensure every process uses the same
|
|
80
|
+
`REDIS_URL` and `prefix`.
|
|
81
|
+
- **Malformed JSON rows:** a row whose JSON won't parse is skipped on read and **left in
|
|
82
|
+
place** (never deleted) — the signal is a one-time `console.warn` per bad id. Fix or
|
|
83
|
+
delete the offending `{prefix}:record:{id}` key to remediate.
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export {
|
|
2
|
+
memoryMiddleware,
|
|
3
|
+
MEMORY_STATE_EVENT,
|
|
4
|
+
type MemoryMiddlewareOptions,
|
|
5
|
+
type MemoryMiddlewareRole,
|
|
6
|
+
type MemoryRecallInfo,
|
|
7
|
+
type MemorySaveInfo,
|
|
8
|
+
type MemoryStateEventValue,
|
|
9
|
+
} from './middleware'
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
MemoryAdapter,
|
|
13
|
+
MemoryScope,
|
|
14
|
+
MemoryTurn,
|
|
15
|
+
MemoryFragment,
|
|
16
|
+
RecallResult,
|
|
17
|
+
SaveReceipt,
|
|
18
|
+
MemorySnapshot,
|
|
19
|
+
MemoryFact,
|
|
20
|
+
} from './types'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { sameScope } from './store'
|
|
3
|
+
|
|
4
|
+
describe('sameScope', () => {
|
|
5
|
+
const full = {
|
|
6
|
+
threadId: 's',
|
|
7
|
+
userId: 'u',
|
|
8
|
+
tenantId: 'a',
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
it('matches when threadId and optional dims agree', () => {
|
|
12
|
+
expect(sameScope(full, full)).toBe(true)
|
|
13
|
+
expect(sameScope(full, { threadId: 's', userId: 'u', tenantId: 'a' })).toBe(
|
|
14
|
+
true,
|
|
15
|
+
)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('rejects different threadId', () => {
|
|
19
|
+
expect(sameScope(full, { ...full, threadId: 'other' })).toBe(false)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('rejects different userId', () => {
|
|
23
|
+
expect(
|
|
24
|
+
sameScope(full, { threadId: 's', userId: 'other', tenantId: 'a' }),
|
|
25
|
+
).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('rejects different tenantId', () => {
|
|
29
|
+
expect(sameScope(full, { ...full, tenantId: 'b' })).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('treats omitted optional dims as exact (not wildcards)', () => {
|
|
33
|
+
// A query without tenant/user must not match a record that has them —
|
|
34
|
+
// same isolation model as Redis composite index keys.
|
|
35
|
+
expect(sameScope(full, { threadId: 's', userId: 'u' })).toBe(false)
|
|
36
|
+
expect(sameScope(full, { threadId: 's' })).toBe(false)
|
|
37
|
+
expect(sameScope(full, { threadId: 's', userId: '', tenantId: '' })).toBe(
|
|
38
|
+
false,
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('matches when both sides omit the same optional dims', () => {
|
|
43
|
+
expect(sameScope({ threadId: 's' }, { threadId: 's' })).toBe(true)
|
|
44
|
+
expect(
|
|
45
|
+
sameScope({ threadId: 's', userId: 'u' }, { threadId: 's', userId: 'u' }),
|
|
46
|
+
).toBe(true)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('treats empty string as unset for optional dims', () => {
|
|
50
|
+
expect(sameScope({ threadId: 's', userId: '' }, { threadId: 's' })).toBe(
|
|
51
|
+
true,
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('ignores namespace (reserved — no subsystem keys on it yet)', () => {
|
|
56
|
+
expect(
|
|
57
|
+
sameScope(
|
|
58
|
+
{ ...full, namespace: 'bank-a' },
|
|
59
|
+
{ ...full, namespace: 'bank-b' },
|
|
60
|
+
),
|
|
61
|
+
).toBe(true)
|
|
62
|
+
})
|
|
63
|
+
})
|