@justfortytwo/memory 0.1.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/LICENSE +21 -0
- package/README.md +183 -0
- package/dist/contract.d.ts +19 -0
- package/dist/contract.js +41 -0
- package/dist/contract.js.map +1 -0
- package/dist/db.d.ts +11 -0
- package/dist/db.js +33 -0
- package/dist/db.js.map +1 -0
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +43 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/embedder.d.ts +18 -0
- package/dist/embedder.js +44 -0
- package/dist/embedder.js.map +1 -0
- package/dist/enrichment.d.ts +58 -0
- package/dist/enrichment.js +109 -0
- package/dist/enrichment.js.map +1 -0
- package/dist/gate-approval-store.d.ts +17 -0
- package/dist/gate-approval-store.js +114 -0
- package/dist/gate-approval-store.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/memory.d.ts +74 -0
- package/dist/memory.js +179 -0
- package/dist/memory.js.map +1 -0
- package/dist/migrate.d.ts +2 -0
- package/dist/migrate.js +40 -0
- package/dist/migrate.js.map +1 -0
- package/dist/migrations/001_init.d.ts +3 -0
- package/dist/migrations/001_init.js +40 -0
- package/dist/migrations/001_init.js.map +1 -0
- package/dist/migrations/002_fts.d.ts +3 -0
- package/dist/migrations/002_fts.js +26 -0
- package/dist/migrations/002_fts.js.map +1 -0
- package/dist/migrations/003_approvals.d.ts +3 -0
- package/dist/migrations/003_approvals.js +37 -0
- package/dist/migrations/003_approvals.js.map +1 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +66 -0
- package/dist/tools.js.map +1 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Enrico Deleo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# @justfortytwo/memory
|
|
2
|
+
|
|
3
|
+
A standalone **semantic-memory MCP server**. It stores text "memories" with
|
|
4
|
+
free-form provenance and recalls them by meaning (vector search), by keyword
|
|
5
|
+
(FTS5), or by structured filter. Backed by SQLite + [`sqlite-vec`], embeddings
|
|
6
|
+
from a local [Ollama] model.
|
|
7
|
+
|
|
8
|
+
It is **persona-agnostic**: no journal/persona/approval coupling, just a generic
|
|
9
|
+
memory store and tool surface. It can be used on its own, or as a Claude Code
|
|
10
|
+
plugin.
|
|
11
|
+
|
|
12
|
+
[`sqlite-vec`]: https://github.com/asg017/sqlite-vec
|
|
13
|
+
[Ollama]: https://ollama.com
|
|
14
|
+
|
|
15
|
+
## What it stores
|
|
16
|
+
|
|
17
|
+
A memory is `content` plus provenance:
|
|
18
|
+
|
|
19
|
+
| field | meaning |
|
|
20
|
+
|-------|---------|
|
|
21
|
+
| `content` | the text (embedded for recall) |
|
|
22
|
+
| `source` | where it came from — free-form (`owner`, `web`, `tool:foo`) |
|
|
23
|
+
| `observed` | how it was observed — free-form (`stated`, `inferred`, `imported`) |
|
|
24
|
+
| `date` | ISO date the memory pertains to (defaults to today, UTC) |
|
|
25
|
+
| `tags` | free-form tags for filtering |
|
|
26
|
+
| `supersedes` | id of a prior memory this one replaces (history is **kept**) |
|
|
27
|
+
|
|
28
|
+
Recall is hybrid: `recall` (semantic), `lexical` (FTS5 keyword), and `query`
|
|
29
|
+
(structured). `reindex` + `recall_docs` index/search a directory of markdown
|
|
30
|
+
documents separately from the memory store.
|
|
31
|
+
|
|
32
|
+
## MCP tools
|
|
33
|
+
|
|
34
|
+
The server registers under the id **`fortytwo-memory`**, so a consumer calls the
|
|
35
|
+
tools as `mcp__fortytwo-memory__<tool>`:
|
|
36
|
+
|
|
37
|
+
| tool | description |
|
|
38
|
+
|------|-------------|
|
|
39
|
+
| `store` | store a memory (+ provenance); set `supersedes` to replace one |
|
|
40
|
+
| `query` | structured query (source/observed/tag/time; live rows by default) |
|
|
41
|
+
| `recall` | semantic top-k recall by meaning |
|
|
42
|
+
| `recall_docs` | semantic recall over reindexed markdown |
|
|
43
|
+
| `lexical` | full-text keyword search (FTS5) |
|
|
44
|
+
| `reindex` | self-heal the doc index from a markdown directory |
|
|
45
|
+
| `export_range` | render a date range of memories to markdown |
|
|
46
|
+
|
|
47
|
+
### Contract version
|
|
48
|
+
|
|
49
|
+
Consumers depend on the **tool surface**, not the internals. The contract is
|
|
50
|
+
versioned:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { MEMORY_TOOL_CONTRACT_VERSION, memoryToolContract } from '@justfortytwo/memory/contract';
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- A **major** change to a tool name, its required inputs, or its result shape is
|
|
57
|
+
a **contract break** → bump `MEMORY_TOOL_CONTRACT_VERSION`. Siblings pin a
|
|
58
|
+
caret range on `@justfortytwo/memory`, so a major bump forces an explicit
|
|
59
|
+
opt-in.
|
|
60
|
+
- Additive changes (new optional inputs, new tools) do **not** bump it.
|
|
61
|
+
|
|
62
|
+
`memoryToolContract` is the authoritative human-readable list of tools and their
|
|
63
|
+
guarantees, kept in sync with the wire schema in `src/tools.ts`.
|
|
64
|
+
|
|
65
|
+
## Embedder
|
|
66
|
+
|
|
67
|
+
The default embedder is **`OllamaEmbedder`**, which calls a local Ollama
|
|
68
|
+
`/api/embeddings` endpoint.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
OLLAMA_BASE_URL=http://localhost:11434 # default
|
|
72
|
+
EMBED_MODEL=qwen3-embedding:0.6b # default model (1024-dim)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Pull the model once:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
ollama pull qwen3-embedding:0.6b
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
If `EMBED_MODEL` is **unset**, the server falls back to a deterministic,
|
|
82
|
+
dependency-free **`FakeEmbedder`** — useful for tests, CI, and first-run smoke
|
|
83
|
+
checks with zero infra. (The vector tables are fixed at 1024-dim; a model with a
|
|
84
|
+
different dimensionality requires a schema change.)
|
|
85
|
+
|
|
86
|
+
## Standalone usage
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# build (once); the server runs the built JS, not TS
|
|
90
|
+
npm run build
|
|
91
|
+
|
|
92
|
+
# apply migrations to the DB (DB_PATH or ./memory.db)
|
|
93
|
+
DB_PATH=./memory.db npm run migrate
|
|
94
|
+
|
|
95
|
+
# run the MCP server over stdio
|
|
96
|
+
DB_PATH=./memory.db EMBED_MODEL=qwen3-embedding:0.6b fortytwo-memory
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The `bin` is `fortytwo-memory` → `dist/index.js`. You can also `npx -y
|
|
100
|
+
@justfortytwo/memory` once published.
|
|
101
|
+
|
|
102
|
+
### As a library
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { openDb, runMigrations, OllamaEmbedder, store, recall } from '@justfortytwo/memory';
|
|
106
|
+
|
|
107
|
+
const h = openDb('memory.db');
|
|
108
|
+
await runMigrations(h.k);
|
|
109
|
+
const embedder = new OllamaEmbedder();
|
|
110
|
+
|
|
111
|
+
await store(h, embedder, { content: 'the deploy script lives in scripts/deploy.sh', source: 'owner', observed: 'stated' });
|
|
112
|
+
const hits = await recall(h, embedder, 'how do I deploy?', 5);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## As a Claude Code plugin
|
|
116
|
+
|
|
117
|
+
`.claude-plugin/plugin.json` declares the plugin; `.mcp.json` registers the
|
|
118
|
+
`fortytwo-memory` server. By default it launches via `npx`:
|
|
119
|
+
|
|
120
|
+
```jsonc
|
|
121
|
+
{
|
|
122
|
+
"mcpServers": {
|
|
123
|
+
"fortytwo-memory": {
|
|
124
|
+
"command": "npx",
|
|
125
|
+
"args": ["-y", "@justfortytwo/memory"],
|
|
126
|
+
"env": {
|
|
127
|
+
"OLLAMA_BASE_URL": "http://localhost:11434",
|
|
128
|
+
"EMBED_MODEL": "qwen3-embedding:0.6b",
|
|
129
|
+
"DB_PATH": "${CLAUDE_PLUGIN_DATA}/memory.db"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`${CLAUDE_PLUGIN_DATA}` survives plugin updates, so the DB persists across
|
|
137
|
+
upgrades. When developing from source, build first (`npm run build`) and swap
|
|
138
|
+
the command to `node` with args `["${CLAUDE_PLUGIN_ROOT}/dist/index.js"]`.
|
|
139
|
+
Claude Code does **not** build MCP servers — they run via npm/npx.
|
|
140
|
+
|
|
141
|
+
## Continuous enrichment
|
|
142
|
+
|
|
143
|
+
`enrich(h, embedder, candidates)` folds a batch of candidate memories into the
|
|
144
|
+
store: it drops low-salience candidates, **dedupes** near-duplicates by meaning,
|
|
145
|
+
and writes the survivors with provenance — honoring an explicit `supersedes` to
|
|
146
|
+
replace a stale belief (history is kept, never a silent overwrite).
|
|
147
|
+
`enrichFromTurn(h, embedder, turn, extractor)` runs an injected `SalienceExtractor`
|
|
148
|
+
and feeds its candidates to `enrich`.
|
|
149
|
+
|
|
150
|
+
The salience extractor itself is model-driven and lives in the sibling
|
|
151
|
+
**`@justfortytwo/salience`** engine (a `SalienceExtractor` with an injected
|
|
152
|
+
`LlmClient`) — memory owns only the dedupe + write, never the model call.
|
|
153
|
+
|
|
154
|
+
## Peer seams
|
|
155
|
+
|
|
156
|
+
memory depends on two sibling packages **one-directionally** (declared as optional
|
|
157
|
+
peers, no cycle):
|
|
158
|
+
|
|
159
|
+
- **`@justfortytwo/gate`** — memory ships `GateApprovalStore`
|
|
160
|
+
(`src/gate-approval-store.ts`), a durable SQLite-backed implementation of
|
|
161
|
+
gate's `ApprovalStore` + `AuditLogger` interfaces. Pass it to gate's
|
|
162
|
+
`decide(..., { store, audit })` to back the safety gate's one-shot approvals
|
|
163
|
+
with memory's db instead of the gate's standalone JSONL store.
|
|
164
|
+
- **`@justfortytwo/salience`** — the model-driven salience extractor injected
|
|
165
|
+
into `enrichFromTurn` (see above).
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
npm run build # tsc
|
|
171
|
+
npm test # vitest run
|
|
172
|
+
npm run test:watch # vitest
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Set `RUN_OLLAMA_TESTS=1` to run the opt-in live-Ollama embedder test.
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT © 2026 Enrico Deleo
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
Created and maintained by [**Enrico Deleo**](https://enricodeleo.com).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Bump on any breaking change to the tool surface below. */
|
|
2
|
+
export declare const MEMORY_TOOL_CONTRACT_VERSION = 1;
|
|
3
|
+
/** The MCP server id under which these tools are registered. */
|
|
4
|
+
export declare const MEMORY_SERVER_ID = "fortytwo-memory";
|
|
5
|
+
/** Fully-qualified MCP tool names a consumer invokes. */
|
|
6
|
+
export declare const MEMORY_MCP_TOOLS: readonly ["mcp__fortytwo-memory__store", "mcp__fortytwo-memory__query", "mcp__fortytwo-memory__recall", "mcp__fortytwo-memory__recall_docs", "mcp__fortytwo-memory__lexical", "mcp__fortytwo-memory__reindex", "mcp__fortytwo-memory__export_range"];
|
|
7
|
+
export type MemoryMcpTool = (typeof MEMORY_MCP_TOOLS)[number];
|
|
8
|
+
/** Bare tool names (server-local, without the mcp__<server>__ prefix). */
|
|
9
|
+
export interface MemoryToolSpec {
|
|
10
|
+
/** Bare tool name as declared in the MCP ListTools response. */
|
|
11
|
+
name: string;
|
|
12
|
+
/** One-line human description of the tool's contract. */
|
|
13
|
+
summary: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Documented contract for each tool. Kept in sync with tools.ts (the wire
|
|
17
|
+
* schema). This is the authoritative human-readable list siblings code against.
|
|
18
|
+
*/
|
|
19
|
+
export declare const memoryToolContract: readonly MemoryToolSpec[];
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Cross-package contract for @justfortytwo/memory.
|
|
3
|
+
//
|
|
4
|
+
// Siblings depend on this server through a STABLE tool surface, not its
|
|
5
|
+
// internals. The contract version is the coordination point:
|
|
6
|
+
// - A MAJOR bump (breaking change to a tool name, its required inputs, or its
|
|
7
|
+
// result shape) is a CONTRACT BREAK. Siblings pin a caret range on
|
|
8
|
+
// @justfortytwo/memory; a major bump forces them to opt in.
|
|
9
|
+
// - Additive changes (new optional inputs, new tools) do NOT bump the version.
|
|
10
|
+
//
|
|
11
|
+
// The MCP tools are namespaced by the registered server id "fortytwo-memory"
|
|
12
|
+
// (see .mcp.json), so a consumer calls them as `mcp__fortytwo-memory__<tool>`.
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/** Bump on any breaking change to the tool surface below. */
|
|
15
|
+
export const MEMORY_TOOL_CONTRACT_VERSION = 1;
|
|
16
|
+
/** The MCP server id under which these tools are registered. */
|
|
17
|
+
export const MEMORY_SERVER_ID = 'fortytwo-memory';
|
|
18
|
+
/** Fully-qualified MCP tool names a consumer invokes. */
|
|
19
|
+
export const MEMORY_MCP_TOOLS = [
|
|
20
|
+
'mcp__fortytwo-memory__store',
|
|
21
|
+
'mcp__fortytwo-memory__query',
|
|
22
|
+
'mcp__fortytwo-memory__recall',
|
|
23
|
+
'mcp__fortytwo-memory__recall_docs',
|
|
24
|
+
'mcp__fortytwo-memory__lexical',
|
|
25
|
+
'mcp__fortytwo-memory__reindex',
|
|
26
|
+
'mcp__fortytwo-memory__export_range',
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* Documented contract for each tool. Kept in sync with tools.ts (the wire
|
|
30
|
+
* schema). This is the authoritative human-readable list siblings code against.
|
|
31
|
+
*/
|
|
32
|
+
export const memoryToolContract = [
|
|
33
|
+
{ name: 'store', summary: 'Store a memory (content + free-form provenance) and embed it for recall. Supports SUPERSEDE.' },
|
|
34
|
+
{ name: 'query', summary: 'Structured query over the memory store (source/observed/tag/time, live-only by default).' },
|
|
35
|
+
{ name: 'recall', summary: 'Semantic top-k recall over the memory store by meaning.' },
|
|
36
|
+
{ name: 'recall_docs', summary: 'Semantic top-k recall over reindexed markdown documents (the doc_vec index).' },
|
|
37
|
+
{ name: 'lexical', summary: 'Full-text keyword search over the memory store (FTS5).' },
|
|
38
|
+
{ name: 'reindex', summary: 'Self-heal the doc recall index from a directory of markdown files.' },
|
|
39
|
+
{ name: 'export_range', summary: 'Render a date range of memories to markdown (for debugging/export).' },
|
|
40
|
+
];
|
|
41
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,mDAAmD;AACnD,EAAE;AACF,wEAAwE;AACxE,6DAA6D;AAC7D,gFAAgF;AAChF,uEAAuE;AACvE,gEAAgE;AAChE,iFAAiF;AACjF,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,8EAA8E;AAE9E,6DAA6D;AAC7D,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC;AAE9C,gEAAgE;AAChE,MAAM,CAAC,MAAM,gBAAgB,GAAG,iBAAiB,CAAC;AAElD,yDAAyD;AACzD,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,6BAA6B;IAC7B,6BAA6B;IAC7B,8BAA8B;IAC9B,mCAAmC;IACnC,+BAA+B;IAC/B,+BAA+B;IAC/B,oCAAoC;CAC5B,CAAC;AAYX;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA8B;IAC3D,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,8FAA8F,EAAE;IAC1H,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,0FAA0F,EAAE;IACtH,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,yDAAyD,EAAE;IACtF,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,8EAA8E,EAAE;IAChH,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,wDAAwD,EAAE;IACtF,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,oEAAoE,EAAE;IAClG,EAAE,IAAI,EAAE,cAAc,EAAE,OAAO,EAAE,qEAAqE,EAAE;CAChG,CAAC"}
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { type Knex } from 'knex';
|
|
3
|
+
/** Embedding dimensionality. qwen3-embedding:0.6b emits 1024-dim vectors. */
|
|
4
|
+
export declare const EMBED_DIM = 1024;
|
|
5
|
+
export interface DbHandles {
|
|
6
|
+
/** Raw handle: sqlite-vec + FTS5 ops, and atomic relational+vector writes. */
|
|
7
|
+
raw: Database.Database;
|
|
8
|
+
/** Knex handle: migrations + portable relational reads/writes. */
|
|
9
|
+
k: Knex;
|
|
10
|
+
}
|
|
11
|
+
export declare function openDb(dbPath: string): DbHandles;
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import knexPkg from 'knex';
|
|
3
|
+
import * as sqliteVec from 'sqlite-vec';
|
|
4
|
+
// knex ships as CommonJS; under NodeNext ESM (`node dist/index.js`) a named
|
|
5
|
+
// import `{ knex }` throws "Named export 'knex' not found". Default-import the
|
|
6
|
+
// namespace and destructure — identical binding, ESM-safe. (vitest interops the
|
|
7
|
+
// named form fine, which is why db.test passes but the raw-node server did not.)
|
|
8
|
+
const { knex } = knexPkg;
|
|
9
|
+
/** Embedding dimensionality. qwen3-embedding:0.6b emits 1024-dim vectors. */
|
|
10
|
+
export const EMBED_DIM = 1024;
|
|
11
|
+
export function openDb(dbPath) {
|
|
12
|
+
const raw = new Database(dbPath);
|
|
13
|
+
raw.pragma('journal_mode = WAL');
|
|
14
|
+
raw.pragma('busy_timeout = 5000'); // wait up to 5s on writer contention
|
|
15
|
+
sqliteVec.load(raw); // registers the vec0 module + scalar helpers
|
|
16
|
+
// vec0 tables live on the raw handle: sqlite-vec is loaded here, NOT on Knex's
|
|
17
|
+
// own connection, so Knex migrations cannot create vec0 tables. (FTS5, which is
|
|
18
|
+
// compiled into SQLite, and the relational schema CAN be Knex migrations.)
|
|
19
|
+
//
|
|
20
|
+
// `memory_vec` indexes the generic memory store; `doc_vec` indexes reindexed
|
|
21
|
+
// markdown documents. (Generic rename of the original assistant's `journal_vec`.)
|
|
22
|
+
const ddl = (sql) => raw.exec(sql);
|
|
23
|
+
ddl(`CREATE VIRTUAL TABLE IF NOT EXISTS memory_vec USING vec0(embedding float[${EMBED_DIM}])`);
|
|
24
|
+
ddl(`CREATE VIRTUAL TABLE IF NOT EXISTS doc_vec USING vec0(embedding float[${EMBED_DIM}])`);
|
|
25
|
+
const k = knex({
|
|
26
|
+
client: 'better-sqlite3',
|
|
27
|
+
connection: { filename: dbPath },
|
|
28
|
+
useNullAsDefault: true,
|
|
29
|
+
pool: { min: 1, max: 1 },
|
|
30
|
+
});
|
|
31
|
+
return { raw, k };
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=db.js.map
|
package/dist/db.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,OAAsB,MAAM,MAAM,CAAC;AAC1C,OAAO,KAAK,SAAS,MAAM,YAAY,CAAC;AAExC,4EAA4E;AAC5E,+EAA+E;AAC/E,gFAAgF;AAChF,iFAAiF;AACjF,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;AAEzB,6EAA6E;AAC7E,MAAM,CAAC,MAAM,SAAS,GAAG,IAAI,CAAC;AAS9B,MAAM,UAAU,MAAM,CAAC,MAAc;IACnC,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;IACjC,GAAG,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IACjC,GAAG,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC,CAAC,qCAAqC;IACxE,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,6CAA6C;IAElE,+EAA+E;IAC/E,gFAAgF;IAChF,2EAA2E;IAC3E,EAAE;IACF,6EAA6E;IAC7E,kFAAkF;IAClF,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3C,GAAG,CAAC,4EAA4E,SAAS,IAAI,CAAC,CAAC;IAC/F,GAAG,CAAC,yEAAyE,SAAS,IAAI,CAAC,CAAC;IAE5F,MAAM,CAAC,GAAG,IAAI,CAAC;QACb,MAAM,EAAE,gBAAgB;QACxB,UAAU,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE;QAChC,gBAAgB,EAAE,IAAI;QACtB,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE;KACzB,CAAC,CAAC;IAEH,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC;AACpB,CAAC"}
|
package/dist/dispatch.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { store, query, recall, recallDocs, lexical, reindex, exportRange } from './memory.js';
|
|
2
|
+
// Route a bare MCP tool name + arguments to the memory store and return the RAW
|
|
3
|
+
// result. The index.ts CallTool handler wraps this in the MCP content envelope;
|
|
4
|
+
// keeping the routing here (free of any MCP transport) makes the tool surface
|
|
5
|
+
// unit-testable. Throws on an unknown tool.
|
|
6
|
+
export async function callTool(h, embedder, name, args) {
|
|
7
|
+
const a = args;
|
|
8
|
+
switch (name) {
|
|
9
|
+
case 'store':
|
|
10
|
+
return store(h, embedder, {
|
|
11
|
+
content: String(a.content),
|
|
12
|
+
source: a.source,
|
|
13
|
+
observed: a.observed,
|
|
14
|
+
date: a.date,
|
|
15
|
+
tags: Array.isArray(a.tags) ? a.tags : undefined,
|
|
16
|
+
meta: a.meta,
|
|
17
|
+
supersedes: a.supersedes,
|
|
18
|
+
});
|
|
19
|
+
case 'query':
|
|
20
|
+
return query(h, {
|
|
21
|
+
source: a.source,
|
|
22
|
+
observed: a.observed,
|
|
23
|
+
tag: a.tag,
|
|
24
|
+
since: a.since,
|
|
25
|
+
until: a.until,
|
|
26
|
+
liveOnly: a.live_only,
|
|
27
|
+
limit: a.limit,
|
|
28
|
+
});
|
|
29
|
+
case 'recall':
|
|
30
|
+
return recall(h, embedder, String(a.text), a.k ?? 5);
|
|
31
|
+
case 'recall_docs':
|
|
32
|
+
return recallDocs(h, embedder, String(a.text), a.k ?? 5);
|
|
33
|
+
case 'lexical':
|
|
34
|
+
return lexical(h, String(a.text), a.k ?? 50);
|
|
35
|
+
case 'reindex':
|
|
36
|
+
return reindex(h, embedder, String(a.root));
|
|
37
|
+
case 'export_range':
|
|
38
|
+
return exportRange(h, String(a.since), String(a.until));
|
|
39
|
+
default:
|
|
40
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=dispatch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE9F,gFAAgF;AAChF,gFAAgF;AAChF,8EAA8E;AAC9E,4CAA4C;AAC5C,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,CAAY,EACZ,QAAkB,EAClB,IAAY,EACZ,IAA6B;IAE7B,MAAM,CAAC,GAAG,IAAI,CAAC;IACf,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,OAAO;YACV,OAAO,KAAK,CAAC,CAAC,EAAE,QAAQ,EAAE;gBACxB,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;gBAC1B,MAAM,EAAE,CAAC,CAAC,MAA4B;gBACtC,QAAQ,EAAE,CAAC,CAAC,QAA8B;gBAC1C,IAAI,EAAE,CAAC,CAAC,IAA0B;gBAClC,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC,IAAiB,CAAC,CAAC,CAAC,SAAS;gBAC9D,IAAI,EAAE,CAAC,CAAC,IAA2C;gBACnD,UAAU,EAAE,CAAC,CAAC,UAAgC;aAC/C,CAAC,CAAC;QACL,KAAK,OAAO;YACV,OAAO,KAAK,CAAC,CAAC,EAAE;gBACd,MAAM,EAAE,CAAC,CAAC,MAA4B;gBACtC,QAAQ,EAAE,CAAC,CAAC,QAA8B;gBAC1C,GAAG,EAAE,CAAC,CAAC,GAAyB;gBAChC,KAAK,EAAE,CAAC,CAAC,KAA2B;gBACpC,KAAK,EAAE,CAAC,CAAC,KAA2B;gBACpC,QAAQ,EAAE,CAAC,CAAC,SAAgC;gBAC5C,KAAK,EAAE,CAAC,CAAC,KAA2B;aACrC,CAAC,CAAC;QACL,KAAK,QAAQ;YACX,OAAO,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAG,CAAC,CAAC,CAAY,IAAI,CAAC,CAAC,CAAC;QACnE,KAAK,aAAa;YAChB,OAAO,UAAU,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAG,CAAC,CAAC,CAAY,IAAI,CAAC,CAAC,CAAC;QACvE,KAAK,SAAS;YACZ,OAAO,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAG,CAAC,CAAC,CAAY,IAAI,EAAE,CAAC,CAAC;QAC3D,KAAK,SAAS;YACZ,OAAO,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QAC9C,KAAK,cAAc;YACjB,OAAO,WAAW,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1D;YACE,MAAM,IAAI,KAAK,CAAC,iBAAiB,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type Vec = Float32Array;
|
|
2
|
+
export interface Embedder {
|
|
3
|
+
embed(text: string): Promise<Vec>;
|
|
4
|
+
}
|
|
5
|
+
export declare function vecToBuffer(v: Vec): Buffer;
|
|
6
|
+
/** Deterministic, dependency-free embedder for hermetic unit tests. */
|
|
7
|
+
export declare class FakeEmbedder implements Embedder {
|
|
8
|
+
private dim;
|
|
9
|
+
constructor(dim?: number);
|
|
10
|
+
embed(text: string): Promise<Vec>;
|
|
11
|
+
}
|
|
12
|
+
/** Calls a local Ollama /api/embeddings endpoint. */
|
|
13
|
+
export declare class OllamaEmbedder implements Embedder {
|
|
14
|
+
private model;
|
|
15
|
+
private baseUrl;
|
|
16
|
+
constructor(model?: string, baseUrl?: string);
|
|
17
|
+
embed(text: string): Promise<Vec>;
|
|
18
|
+
}
|
package/dist/embedder.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function vecToBuffer(v) {
|
|
2
|
+
return Buffer.from(v.buffer, v.byteOffset, v.byteLength);
|
|
3
|
+
}
|
|
4
|
+
/** Deterministic, dependency-free embedder for hermetic unit tests. */
|
|
5
|
+
export class FakeEmbedder {
|
|
6
|
+
dim;
|
|
7
|
+
constructor(dim = 1024) {
|
|
8
|
+
this.dim = dim;
|
|
9
|
+
}
|
|
10
|
+
async embed(text) {
|
|
11
|
+
const v = new Float32Array(this.dim);
|
|
12
|
+
let h = 2166136261 >>> 0;
|
|
13
|
+
for (let i = 0; i < text.length; i++) {
|
|
14
|
+
h ^= text.charCodeAt(i);
|
|
15
|
+
h = Math.imul(h, 16777619);
|
|
16
|
+
}
|
|
17
|
+
for (let i = 0; i < this.dim; i++) {
|
|
18
|
+
h = Math.imul(h ^ (h >>> 13), 16777619);
|
|
19
|
+
v[i] = (h >>> 0) % 1000 / 1000;
|
|
20
|
+
}
|
|
21
|
+
return v;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Calls a local Ollama /api/embeddings endpoint. */
|
|
25
|
+
export class OllamaEmbedder {
|
|
26
|
+
model;
|
|
27
|
+
baseUrl;
|
|
28
|
+
constructor(model = 'qwen3-embedding:0.6b', baseUrl = process.env.OLLAMA_BASE_URL ?? 'http://localhost:11434') {
|
|
29
|
+
this.model = model;
|
|
30
|
+
this.baseUrl = baseUrl;
|
|
31
|
+
}
|
|
32
|
+
async embed(text) {
|
|
33
|
+
const res = await fetch(`${this.baseUrl}/api/embeddings`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'content-type': 'application/json' },
|
|
36
|
+
body: JSON.stringify({ model: this.model, prompt: text }),
|
|
37
|
+
});
|
|
38
|
+
if (!res.ok)
|
|
39
|
+
throw new Error(`Ollama embeddings failed: ${res.status} ${await res.text()}`);
|
|
40
|
+
const json = (await res.json());
|
|
41
|
+
return Float32Array.from(json.embedding);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=embedder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embedder.js","sourceRoot":"","sources":["../src/embedder.ts"],"names":[],"mappings":"AAMA,MAAM,UAAU,WAAW,CAAC,CAAM;IAChC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;AAC3D,CAAC;AAED,uEAAuE;AACvE,MAAM,OAAO,YAAY;IACH;IAApB,YAAoB,MAAM,IAAI;QAAV,QAAG,GAAH,GAAG,CAAO;IAAG,CAAC;IAClC,KAAK,CAAC,KAAK,CAAC,IAAY;QACtB,MAAM,CAAC,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,CAAC,GAAG,UAAU,KAAK,CAAC,CAAC;QACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACrC,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;YACxB,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;QAC7B,CAAC;QACD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC;YAClC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,QAAQ,CAAC,CAAC;YACxC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;QACjC,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC;CACF;AAED,qDAAqD;AACrD,MAAM,OAAO,cAAc;IAEf;IACA;IAFV,YACU,QAAQ,sBAAsB,EAC9B,UAAU,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,wBAAwB;QADjE,UAAK,GAAL,KAAK,CAAyB;QAC9B,YAAO,GAAP,OAAO,CAA0D;IACxE,CAAC;IACJ,KAAK,CAAC,KAAK,CAAC,IAAY;QACtB,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,iBAAiB,EAAE;YACxD,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;SAC1D,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,6BAA6B,GAAG,CAAC,MAAM,IAAI,MAAM,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QAC5F,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA4B,CAAC;QAC3D,OAAO,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC3C,CAAC;CACF"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { DbHandles } from './db.js';
|
|
2
|
+
import type { Embedder } from './embedder.js';
|
|
3
|
+
interface SalienceExtractor {
|
|
4
|
+
extractSalient(turn: {
|
|
5
|
+
text: string;
|
|
6
|
+
source?: string;
|
|
7
|
+
observed?: string;
|
|
8
|
+
date?: string;
|
|
9
|
+
meta?: Record<string, unknown>;
|
|
10
|
+
}): Promise<EnrichmentCandidate[]>;
|
|
11
|
+
}
|
|
12
|
+
/** Below this vector distance two memories are treated as near-duplicates. */
|
|
13
|
+
export declare const DEDUPE_DISTANCE = 0.15;
|
|
14
|
+
/** Drop candidates whose salience is below this score. */
|
|
15
|
+
export declare const SALIENCE_THRESHOLD = 0.5;
|
|
16
|
+
/** A distilled candidate memory produced by an (external) salience extractor. */
|
|
17
|
+
export interface EnrichmentCandidate {
|
|
18
|
+
content: string;
|
|
19
|
+
salience: number;
|
|
20
|
+
source?: string;
|
|
21
|
+
observed?: string;
|
|
22
|
+
date?: string;
|
|
23
|
+
tags?: string[];
|
|
24
|
+
meta?: Record<string, unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* If set, this candidate supersedes the given memory id — an upstream
|
|
27
|
+
* contradiction/update judgment (e.g. from the salience salience step).
|
|
28
|
+
* enrich() honors it: the new row is written and the old row is flagged
|
|
29
|
+
* superseded (history kept, never overwritten), even past the dedupe check.
|
|
30
|
+
*/
|
|
31
|
+
supersedes?: number | null;
|
|
32
|
+
}
|
|
33
|
+
export interface EnrichmentResult {
|
|
34
|
+
written: number[];
|
|
35
|
+
superseded: number[];
|
|
36
|
+
skipped: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Fold a batch of candidate memories into the store: dedupe, supersede stale
|
|
40
|
+
* beliefs (keeping history), and write the survivors with provenance.
|
|
41
|
+
*
|
|
42
|
+
* NOTE: candidates are produced UPSTREAM. This function owns dedupe + write
|
|
43
|
+
* only — it does not call any model.
|
|
44
|
+
*/
|
|
45
|
+
export declare function enrich(h: DbHandles, embedder: Embedder, candidates: EnrichmentCandidate[]): Promise<EnrichmentResult>;
|
|
46
|
+
/**
|
|
47
|
+
* Post-turn entry point: extract salient candidates from a turn, then enrich.
|
|
48
|
+
*
|
|
49
|
+
* The extraction step is delegated to the sibling @justfortytwo/salience
|
|
50
|
+
* engine via an INJECTED SalienceExtractor — memory owns dedupe + write, never the
|
|
51
|
+
* model client. The host builds the extractor (salience's createSalienceExtractor
|
|
52
|
+
* with its own LlmClient) and passes it in here.
|
|
53
|
+
*/
|
|
54
|
+
export declare function enrichFromTurn(h: DbHandles, embedder: Embedder, turn: {
|
|
55
|
+
text: string;
|
|
56
|
+
source?: string;
|
|
57
|
+
}, extractor: SalienceExtractor): Promise<EnrichmentResult>;
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { recall, store } from './memory.js';
|
|
2
|
+
// ===========================================================================
|
|
3
|
+
// Continuous enrichment — STUB.
|
|
4
|
+
//
|
|
5
|
+
// Goal: after each conversational turn, distil durable knowledge from the turn
|
|
6
|
+
// and fold it into the memory store WITHOUT ever silently destroying a prior
|
|
7
|
+
// belief. This is the write-side counterpart to recall: recall reads, enrichment
|
|
8
|
+
// curates what is worth remembering.
|
|
9
|
+
//
|
|
10
|
+
// PIPELINE (post-turn):
|
|
11
|
+
//
|
|
12
|
+
// 1. SALIENCE EXTRACTION
|
|
13
|
+
// Take the turn (user + assistant text, tool results) and extract a small
|
|
14
|
+
// set of candidate memories — atomic, self-contained statements worth
|
|
15
|
+
// keeping ("the owner's child is named X", "the deploy script lives at Y").
|
|
16
|
+
// Each candidate carries a salience score; below a threshold we drop it so
|
|
17
|
+
// the store does not fill with noise.
|
|
18
|
+
// The extractor is model-driven, and the LLM call is NOT owned by this
|
|
19
|
+
// package (a memory server must not embed a model client). The salience
|
|
20
|
+
// step lives in the sibling `@justfortytwo/salience` engine, which
|
|
21
|
+
// defines a `SalienceExtractor` (injected `LlmClient`) and returns scored
|
|
22
|
+
// candidates. We inject that extractor and pass its candidates IN to
|
|
23
|
+
// enrich(); this file owns only dedupe + write.
|
|
24
|
+
//
|
|
25
|
+
// 2. DEDUPE / SUPERSEDE (recency wins, history is kept, NEVER overwrite)
|
|
26
|
+
// For each candidate, semantically recall the nearest existing memories.
|
|
27
|
+
// - near-duplicate (distance below DEDUPE_DISTANCE) and same meaning:
|
|
28
|
+
// skip the write — we already know this.
|
|
29
|
+
// - contradiction / update of an existing belief:
|
|
30
|
+
// write the new memory and SUPERSEDE the old row (memory.store with
|
|
31
|
+
// `supersedes`). The old row is retained and flagged superseded_by,
|
|
32
|
+
// so the history of what we believed and when is fully auditable.
|
|
33
|
+
// Recency wins for live recall; nothing is deleted.
|
|
34
|
+
// TODO(impl): "same meaning" vs "contradiction" needs more than vector
|
|
35
|
+
// distance (two close vectors can be opposite facts). This likely needs
|
|
36
|
+
// the same sibling salience step as (1) to judge the relation.
|
|
37
|
+
//
|
|
38
|
+
// 3. WRITE (tagged provenance)
|
|
39
|
+
// Persist surviving candidates via memory.store, tagged with:
|
|
40
|
+
// - source: where the knowledge came from (e.g. the channel/actor)
|
|
41
|
+
// - observed: how we came to believe it (e.g. "stated" vs "inferred")
|
|
42
|
+
// - date: when it was observed
|
|
43
|
+
// Inferred memories MUST be marked observed:"inferred" so downstream
|
|
44
|
+
// consumers can weight stated facts over guesses.
|
|
45
|
+
//
|
|
46
|
+
// The contract: enrichment is ADDITIVE and AUDITABLE. A wrong inference can be
|
|
47
|
+
// superseded later; it is never silently erased.
|
|
48
|
+
// ===========================================================================
|
|
49
|
+
/** Below this vector distance two memories are treated as near-duplicates. */
|
|
50
|
+
export const DEDUPE_DISTANCE = 0.15;
|
|
51
|
+
/** Drop candidates whose salience is below this score. */
|
|
52
|
+
export const SALIENCE_THRESHOLD = 0.5;
|
|
53
|
+
/**
|
|
54
|
+
* Fold a batch of candidate memories into the store: dedupe, supersede stale
|
|
55
|
+
* beliefs (keeping history), and write the survivors with provenance.
|
|
56
|
+
*
|
|
57
|
+
* NOTE: candidates are produced UPSTREAM. This function owns dedupe + write
|
|
58
|
+
* only — it does not call any model.
|
|
59
|
+
*/
|
|
60
|
+
export async function enrich(h, embedder, candidates) {
|
|
61
|
+
const written = [];
|
|
62
|
+
const superseded = [];
|
|
63
|
+
let skipped = 0;
|
|
64
|
+
for (const c of candidates) {
|
|
65
|
+
if (c.salience < SALIENCE_THRESHOLD) {
|
|
66
|
+
skipped++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
// Dedupe by meaning — unless the candidate explicitly supersedes a prior row,
|
|
70
|
+
// in which case the update is intentional even if it reads similar. Note:
|
|
71
|
+
// recall is live-only, so we dedupe against LIVE memories, not superseded
|
|
72
|
+
// history (a candidate matching a dead row is intentionally re-written).
|
|
73
|
+
if (c.supersedes == null) {
|
|
74
|
+
const near = await recall(h, embedder, c.content, 1);
|
|
75
|
+
if (near.length > 0 && near[0].distance < DEDUPE_DISTANCE) {
|
|
76
|
+
skipped++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const id = await store(h, embedder, {
|
|
81
|
+
content: c.content,
|
|
82
|
+
source: c.source,
|
|
83
|
+
observed: c.observed,
|
|
84
|
+
date: c.date,
|
|
85
|
+
tags: c.tags,
|
|
86
|
+
meta: c.meta,
|
|
87
|
+
supersedes: c.supersedes ?? null,
|
|
88
|
+
});
|
|
89
|
+
written.push(id);
|
|
90
|
+
if (c.supersedes != null)
|
|
91
|
+
superseded.push(c.supersedes);
|
|
92
|
+
}
|
|
93
|
+
return { written, superseded, skipped };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Post-turn entry point: extract salient candidates from a turn, then enrich.
|
|
97
|
+
*
|
|
98
|
+
* The extraction step is delegated to the sibling @justfortytwo/salience
|
|
99
|
+
* engine via an INJECTED SalienceExtractor — memory owns dedupe + write, never the
|
|
100
|
+
* model client. The host builds the extractor (salience's createSalienceExtractor
|
|
101
|
+
* with its own LlmClient) and passes it in here.
|
|
102
|
+
*/
|
|
103
|
+
export async function enrichFromTurn(h, embedder, turn, extractor) {
|
|
104
|
+
// memory owns dedupe + write; the salience extraction (the model call) is the
|
|
105
|
+
// injected @justfortytwo/salience engine's. We only wire the two together.
|
|
106
|
+
const candidates = await extractor.extractSalient(turn);
|
|
107
|
+
return enrich(h, embedder, candidates);
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=enrichment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enrichment.js","sourceRoot":"","sources":["../src/enrichment.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ApprovalStore, AuditLogger, AuditEntry, PendingApproval, AddPendingInput } from '@justfortytwo/gate';
|
|
2
|
+
import type { DbHandles } from './db.js';
|
|
3
|
+
/**
|
|
4
|
+
* SQLite-backed ApprovalStore + AuditLogger for the gate. Pass an instance
|
|
5
|
+
* to gate's `decide(manifest, ctx, { store, audit })` so staged one-shots and the
|
|
6
|
+
* audit trail live in memory's durable db instead of the gate's JSONL default.
|
|
7
|
+
*/
|
|
8
|
+
export declare class GateApprovalStore implements ApprovalStore, AuditLogger {
|
|
9
|
+
private readonly h;
|
|
10
|
+
constructor(h: DbHandles);
|
|
11
|
+
addPending(input: AddPendingInput): Promise<string>;
|
|
12
|
+
getByToolUseId(toolUseId: string): Promise<PendingApproval | undefined>;
|
|
13
|
+
markExecutedByToolUseId(toolUseId: string): Promise<boolean>;
|
|
14
|
+
setDecisionByToolUseId(toolUseId: string, status: 'approved' | 'denied', by?: string): Promise<boolean>;
|
|
15
|
+
list(): Promise<PendingApproval[]>;
|
|
16
|
+
log(entry: AuditEntry): Promise<void>;
|
|
17
|
+
}
|