memtrails 0.0.0-stage → 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/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +217 -2
- package/dist/core/consolidate.d.ts +144 -0
- package/dist/core/consolidate.js +660 -0
- package/dist/core/embedder.d.ts +30 -0
- package/dist/core/embedder.js +137 -0
- package/dist/core/episodes.d.ts +30 -0
- package/dist/core/episodes.js +49 -0
- package/dist/core/extract.d.ts +139 -0
- package/dist/core/extract.js +447 -0
- package/dist/core/gather.d.ts +17 -0
- package/dist/core/gather.js +98 -0
- package/dist/core/graph.d.ts +48 -0
- package/dist/core/graph.js +208 -0
- package/dist/core/local-embedder.d.ts +40 -0
- package/dist/core/local-embedder.js +98 -0
- package/dist/core/model.d.ts +16 -0
- package/dist/core/model.js +74 -0
- package/dist/core/paths.d.ts +1 -0
- package/dist/core/paths.js +7 -0
- package/dist/core/profiles.d.ts +30 -0
- package/dist/core/profiles.js +147 -0
- package/dist/core/qdrant-binary.d.ts +25 -0
- package/dist/core/qdrant-binary.js +108 -0
- package/dist/core/qdrant-client.d.ts +115 -0
- package/dist/core/qdrant-client.js +319 -0
- package/dist/core/qdrant-process.d.ts +30 -0
- package/dist/core/qdrant-process.js +160 -0
- package/dist/core/rebuild.d.ts +31 -0
- package/dist/core/rebuild.js +109 -0
- package/dist/core/recall-log.d.ts +29 -0
- package/dist/core/recall-log.js +82 -0
- package/dist/core/recall.d.ts +91 -0
- package/dist/core/recall.js +172 -0
- package/dist/core/redact.d.ts +2 -0
- package/dist/core/redact.js +48 -0
- package/dist/core/reranker.d.ts +26 -0
- package/dist/core/reranker.js +67 -0
- package/dist/core/room.d.ts +20 -0
- package/dist/core/room.js +63 -0
- package/dist/core/sleep.d.ts +44 -0
- package/dist/core/sleep.js +199 -0
- package/dist/core/store.d.ts +198 -0
- package/dist/core/store.js +190 -0
- package/dist/core/text.d.ts +31 -0
- package/dist/core/text.js +101 -0
- package/dist/core/words.d.ts +32 -0
- package/dist/core/words.js +127 -0
- package/dist/core/writer.d.ts +36 -0
- package/dist/core/writer.js +146 -0
- package/dist/importers/files.d.ts +31 -0
- package/dist/importers/files.js +157 -0
- package/dist/importers/git.d.ts +30 -0
- package/dist/importers/git.js +118 -0
- package/dist/importers/index.d.ts +14 -0
- package/dist/importers/index.js +26 -0
- package/dist/importers/jsonl.d.ts +20 -0
- package/dist/importers/jsonl.js +65 -0
- package/dist/importers/mbox.d.ts +20 -0
- package/dist/importers/mbox.js +347 -0
- package/dist/importers/slack.d.ts +21 -0
- package/dist/importers/slack.js +134 -0
- package/dist/importers/text.d.ts +31 -0
- package/dist/importers/text.js +191 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +22 -0
- package/dist/llm.d.ts +60 -0
- package/dist/llm.js +60 -0
- package/dist/memory.d.ts +106 -0
- package/dist/memory.js +262 -0
- package/dist/records.d.ts +62 -0
- package/dist/records.js +107 -0
- package/dist/tools/agent-tools.d.ts +52 -0
- package/dist/tools/agent-tools.js +142 -0
- package/package.json +78 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes are listed here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
4
|
+
versions follow [Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [0.1.0] — 2026-10-31
|
|
7
|
+
|
|
8
|
+
First public release.
|
|
9
|
+
|
|
10
|
+
- `openMemory`: records of any kind and conversations, searchable at once with no server and no language model.
|
|
11
|
+
- Local embedding (EmbeddingGemma-300m, 4-bit ONNX, fetched and verified on first use) or any OpenAI-compatible
|
|
12
|
+
embeddings server; a Qdrant process of its own (pinned, verified) or your Qdrant server.
|
|
13
|
+
- Search: dense keys, chunks and cues plus BM25 keywords, fused by score at the item level; dates first when asked;
|
|
14
|
+
the best 30 reordered word by word (MaxSim on one-bit token vectors), or by any rerank server.
|
|
15
|
+
- `room`: the best sentences of what was found, for a small context.
|
|
16
|
+
- `open`, `explore` (concept graph, Personalized PageRank), `remove`, `rebuild`, `stats`.
|
|
17
|
+
- Optional, with any language model: `consolidate` (typed items, dated observations, episodes, cues, concepts),
|
|
18
|
+
`sleep` (merge, supersede, link, archive, profiles), `gather` (sets for counting and listing questions).
|
|
19
|
+
- Agent tools: `memory_search`, `memory_open`, `memory_explore` with zod and JSON schemas.
|
|
20
|
+
- Importers: mbox and .eml, Slack exports, Git history, folders of Markdown, text and HTML, JSONL.
|
|
21
|
+
- Benchmarks and research notes: docs/BENCHMARKS.md, docs/RESEARCH.md.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Corneliu Maftuleac
|
|
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
CHANGED
|
@@ -1,3 +1,218 @@
|
|
|
1
|
-
#
|
|
1
|
+
# memtrails
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Retrieval and exploration for any text — e-mail, chat, notes, memos, documents, news, commit messages, conversations.**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/cmaftuleac/memtrails/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/memtrails)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
memtrails keeps what you give it and finds it again. Everything you add is searchable the moment it is added, on your
|
|
10
|
+
own machine, with no server and no language model: a local embedding model, a keyword channel, and a word-by-word
|
|
11
|
+
reordering that needs no reranker. In any language. Give it a language model too, and it consolidates what it holds
|
|
12
|
+
into typed knowledge, dated events and profiles of people and things — and an agent can explore it.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { openMemory } from 'memtrails';
|
|
16
|
+
|
|
17
|
+
const memory = await openMemory({ home: './memory' });
|
|
18
|
+
|
|
19
|
+
await memory.add([
|
|
20
|
+
{ id: 'msg-1', kind: 'email', author: 'Ana', title: 'Gateway certificate expired', at: '2026-04-02',
|
|
21
|
+
text: 'The TLS certificate expired on the 1st. Renewal stopped because the cron job was removed in March.' },
|
|
22
|
+
{ id: 'c9f1e2', kind: 'commit', author: 'Ion', title: 'Restore certificate renewal cron', at: '2026-04-03',
|
|
23
|
+
text: 'Files: deploy/cron/renew.sh' },
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const hits = await memory.search({ query: 'why did the certificate stop renewing?' });
|
|
27
|
+
// [{ id, kind: 'email', title, summary, match, ts, record: { id: 'msg-1', author: 'Ana' }, score }, …]
|
|
28
|
+
|
|
29
|
+
await memory.close();
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Why memtrails
|
|
33
|
+
|
|
34
|
+
- **Searchable at once.** No ingestion pipeline to wait for and no model call per record. Add a mailbox and search it.
|
|
35
|
+
- **On your machine.** The embedding model (EmbeddingGemma-300m, 4-bit, ~220 MB) and the index (Qdrant) run locally
|
|
36
|
+
and are fetched on first use. Nothing leaves the machine unless you turn on a language model.
|
|
37
|
+
- **Any language.** Multilingual embeddings, keyword folding for any script, no word lists. A question in Romanian
|
|
38
|
+
finds an English memory and the other way round.
|
|
39
|
+
- **Little to read, much found.** Search reorders its best 30 word by word (MaxSim over the model's own token vectors,
|
|
40
|
+
stored at one bit a number): on 2,011 benchmark questions the answer reached the reader in 53.1% of top-5 lists
|
|
41
|
+
against 50.3% for plain search, and most of a cross-encoder reranker's gain, in about 3 ms on one CPU thread.
|
|
42
|
+
- **Measured, honestly.** On all 1,307 held-out LoCoMo questions with 6,000 characters to read, memtrails alone on a
|
|
43
|
+
CPU scores 77.2 (strict judge) against Mem0's 76.5 — a tie, reported as one. [Benchmarks](docs/BENCHMARKS.md).
|
|
44
|
+
- **Explorable.** A concept graph links what records share — people, places, systems, names — so an agent can walk
|
|
45
|
+
from what it found to what a search for the words does not find.
|
|
46
|
+
- **Built for agents.** `memory.tools()` gives `memory_search`, `memory_open` and `memory_explore` with zod and JSON
|
|
47
|
+
schemas, ready for an MCP server or any function-calling API.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install memtrails
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Node.js 22 or newer, on macOS or Linux (x64 or arm64). Windows has the pieces it needs but is not tested yet. The first `openMemory` downloads the embedding model
|
|
56
|
+
(218 MB, from Hugging Face) and the Qdrant binary (~30 MB, from Qdrant's GitHub release) into `~/.cache/memtrails`,
|
|
57
|
+
each checked against a pinned SHA-256. Set `MEMTRAILS_CACHE` to put them elsewhere.
|
|
58
|
+
|
|
59
|
+
## Adding data
|
|
60
|
+
|
|
61
|
+
A **record** is anything with text. Only `id` and `text` are required.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
await memory.add({
|
|
65
|
+
id: '<CAF8w@mail.example>', // unique within its source; adding the same id again updates it
|
|
66
|
+
source: 'work-mailbox', // part of the identity
|
|
67
|
+
kind: 'email', // any word: email, message, note, memo, article, commit, ticket, …
|
|
68
|
+
collection: 'ops', // search can be scoped to collections
|
|
69
|
+
thread: 'gateway-cert', // records that belong together
|
|
70
|
+
author: 'Ana Popescu',
|
|
71
|
+
title: 'Gateway certificate expired',
|
|
72
|
+
text: '…',
|
|
73
|
+
at: '2026-04-02T09:14:00Z', // ms, ISO string or Date
|
|
74
|
+
url: 'https://mail.example/…',
|
|
75
|
+
metadata: { to: 'ops@example.com' },
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Conversations (a person and an assistant, or any two parties) are added as turns; each time the person speaks becomes
|
|
80
|
+
one record, with what was answered:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
await memory.addConversation({ id: 'chat-42', turns: [
|
|
84
|
+
{ role: 'user', text: 'Where do we have lunch on Friday?' },
|
|
85
|
+
{ role: 'assistant', text: 'At the Italian place near the office, at one.' },
|
|
86
|
+
] });
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Importers
|
|
90
|
+
|
|
91
|
+
Streaming importers for common sources, in `memtrails/importers`:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import { fromMbox, fromSlackExport, fromGit, fromFiles, fromJsonl } from 'memtrails/importers';
|
|
95
|
+
|
|
96
|
+
await memory.import(fromMbox('./archive.mbox')); // e-mail (mbox / .eml)
|
|
97
|
+
await memory.import(fromSlackExport('./slack-export')); // a Slack workspace export
|
|
98
|
+
await memory.import(fromGit('./my-repo', { since: '2025-01-01' })); // commit messages and changed files
|
|
99
|
+
await memory.import(fromFiles('./notes')); // Markdown, text and HTML files
|
|
100
|
+
await memory.import(fromJsonl('./records.jsonl')); // one record per line
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`memory.import` takes any async iterable of records, so a database cursor or an API client works the same way.
|
|
104
|
+
Unchanged records cost nothing; `memory.remove([...ids])` forgets.
|
|
105
|
+
|
|
106
|
+
## Searching
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
await memory.search({ query: 'who renewed the certificate?', limit: 8 });
|
|
110
|
+
await memory.search({ query: 'standup notes', collections: ['ops'], kinds: ['message'], from: '2026-04', to: '2026-04' });
|
|
111
|
+
|
|
112
|
+
// The same results as a reader is shown them: one line a memory, best first or oldest first.
|
|
113
|
+
await memory.searchText({ query: 'what happened with the gateway?', order: 'time' });
|
|
114
|
+
|
|
115
|
+
// The best sentences of what was found, for a small context window.
|
|
116
|
+
await memory.room({ query: 'what made the renewal stop?', chars: 6000 });
|
|
117
|
+
|
|
118
|
+
// One item whole — for something learned, with the records it was learned from.
|
|
119
|
+
await memory.open(hits[0].id);
|
|
120
|
+
|
|
121
|
+
// What the concept graph reaches from a topic or from items, that search did not find.
|
|
122
|
+
await memory.explore({ topic: 'the gateway' });
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
With dates, what happened within them comes first and the rest follows: a question that names a time is rarely only
|
|
126
|
+
about that time.
|
|
127
|
+
|
|
128
|
+
## Consolidation and sleep (optional, with a language model)
|
|
129
|
+
|
|
130
|
+
Records are kept as they are, for good. With a language model, memtrails also reads threads in windows and keeps what
|
|
131
|
+
matters as typed items — facts, decisions, procedures, pitfalls, preferences, dated observations ("Ana renewed the
|
|
132
|
+
certificate on 2026-04-03"), kept episodes — reconciled against what it already knows, so nothing is stored twice. A
|
|
133
|
+
nightly `sleep` merges near-duplicates, retires what was superseded, links related items and writes a profile of each
|
|
134
|
+
person or thing that many items mention.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const memory = await openMemory({
|
|
138
|
+
home: './memory',
|
|
139
|
+
llm: { model: 'gpt-6-luna', reasoningEffort: 'low', apiKey: process.env.OPENAI_API_KEY }, // or any OpenAI-compatible endpoint: baseUrl
|
|
140
|
+
});
|
|
141
|
+
await memory.consolidate(); // after adding
|
|
142
|
+
await memory.sleep(); // once a day
|
|
143
|
+
await memory.gather({ query: 'every doctor I saw this year' }); // a set, for counting and listing questions
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Any provider works: pass `llm: { baseUrl, model, apiKey }` for an OpenAI-compatible server (a hosted model, vLLM,
|
|
147
|
+
Ollama, …), or your own function `(request) => Promise<answer>`. Every call asks for strict structured output, and
|
|
148
|
+
`onUsage` reports each call's tokens. On the LoCoMo benchmark, consolidating and sleeping a conversation of ~700
|
|
149
|
+
messages cost about $0.07 with GPT-6 Luna (the model every benchmark here used).
|
|
150
|
+
|
|
151
|
+
## For agents
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const tools = memory.tools();
|
|
155
|
+
// [{ name: 'memory_search', description, inputSchema (zod), jsonSchema, handler }, memory_open, memory_explore]
|
|
156
|
+
|
|
157
|
+
// With the MCP TypeScript SDK:
|
|
158
|
+
for (const t of tools) server.tool(t.name, t.description, t.inputSchema, t.handler);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Configuration
|
|
162
|
+
|
|
163
|
+
| Option | Default | |
|
|
164
|
+
|---|---|---|
|
|
165
|
+
| `home` | — | Where this memory lives: its logs, index and word vectors. |
|
|
166
|
+
| `embedding.modelDir` | fetched into the cache | A directory with EmbeddingGemma-300m ONNX (`onnx/model_q4.onnx`, tokenizer). |
|
|
167
|
+
| `embedding.server` | — | `{ url, apiKey?, model? }`: an OpenAI-compatible embeddings server serving EmbeddingGemma (text-embeddings-inference, Ollama, …). |
|
|
168
|
+
| `embedding.threads` | 2 | CPU threads for the local model. |
|
|
169
|
+
| `rerank` | `'words'` | `'words'`, a rerank server `{ url, apiKey?, model? }` (any `/rerank` API), your own function, or `false`. |
|
|
170
|
+
| `llm` | from the environment | `{ model, apiKey?, baseUrl?, reasoningEffort?, onUsage? }` or your own `Ask` function. |
|
|
171
|
+
| `qdrant` | a process of its own | `{ url, apiKey?, collection? }` for your Qdrant server, or `{ binary }`. |
|
|
172
|
+
|
|
173
|
+
| Environment | |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `MEMTRAILS_CACHE` | Where downloads live (default `~/.cache/memtrails`). |
|
|
176
|
+
| `MEMTRAILS_MODEL`, `MEMTRAILS_LLM_API_KEY` or `OPENAI_API_KEY`, `MEMTRAILS_LLM_BASE_URL` | The language model when `llm` is not given. |
|
|
177
|
+
| `MEMTRAILS_EMBED_URL`, `MEMTRAILS_RERANK_URL` (+ `_KEY`, `_MODEL`) | Servers, when not given in options. |
|
|
178
|
+
|
|
179
|
+
## How it works
|
|
180
|
+
|
|
181
|
+
Every record goes to an append-only log (the truth) and to a Qdrant index (derived, rebuilt from the logs whenever it
|
|
182
|
+
is lost or out of date): a key vector of its start, its whole text in ~96-word chunks, and a BM25 keyword vector.
|
|
183
|
+
A search runs every channel at once, fuses them by score at the level of items (dense 0.7, keywords 0.3), and reorders
|
|
184
|
+
the best 30 word by word. Consolidation, sleep and the concept graph sit on top. See [Architecture](docs/ARCHITECTURE.md),
|
|
185
|
+
the [API reference](docs/API.md), and [what was tried and why](docs/RESEARCH.md).
|
|
186
|
+
|
|
187
|
+
## Benchmarks
|
|
188
|
+
|
|
189
|
+
| | memtrails | Mem0 | Graphiti |
|
|
190
|
+
|---|---|---|---|
|
|
191
|
+
| LoCoMo, all 1,307 held-out questions, 6,000 characters, strict judge | **77.2** (no server) | 76.5 | — |
|
|
192
|
+
| the same, Mem0's own lenient judge | **89.3** | 87.8 | — |
|
|
193
|
+
| LoCoMo, 1,307 held-out questions, 28,000 characters, one search¹ | 79.9 | 80.6 | 42.1 |
|
|
194
|
+
| LoCoMo, the same with an agent using each system's own tools¹ | **79.0** | 77.1 | 47.0 |
|
|
195
|
+
| LongMemEval-S retrieval, strict all sessions @5 (467)² | **96.15** | — | — |
|
|
196
|
+
|
|
197
|
+
The same reader and judges for every system. The small-room and one-search comparisons are ties; the agent lead is
|
|
198
|
+
small and was judged once. ¹ An earlier build (2026-10-27). ² gbrain-evals publishes 95.96 on the same count: a tie. [Full results, protocol and limitations](docs/BENCHMARKS.md).
|
|
199
|
+
|
|
200
|
+
## Limitations
|
|
201
|
+
|
|
202
|
+
- English benchmarks dominate the evidence. Other languages were measured with translated questions over English memories, not memories written in other languages.
|
|
203
|
+
- The word-by-word reorder was trained on nothing; a small per-person table taught on a person's own use did better in tests but is not in this release.
|
|
204
|
+
- Questions whose answer is only implied — relevant through outside knowledge, with no word in common — are rarely found by any retrieval system, this one included.
|
|
205
|
+
- Consolidation's prompts were tuned on conversations; on other records they are reasonable but unmeasured.
|
|
206
|
+
- The index stores 768-dimensional EmbeddingGemma vectors; another embedding model needs its own index and is not measured.
|
|
207
|
+
|
|
208
|
+
## Privacy
|
|
209
|
+
|
|
210
|
+
Records stay in `home`. Nothing is sent anywhere unless you configure a language model, an embedding server or a
|
|
211
|
+
rerank server; then what they need is sent to them. Secrets (keys, tokens, credentials in URLs, private keys) are
|
|
212
|
+
scrubbed from what is sent to a language model and from what consolidation keeps.
|
|
213
|
+
|
|
214
|
+
## Licence
|
|
215
|
+
|
|
216
|
+
MIT © Corneliu Maftuleac. Third parties: EmbeddingGemma is Google's, under the
|
|
217
|
+
[Gemma Terms of Use](https://ai.google.dev/gemma/terms), downloaded from Hugging Face, not shipped; Qdrant is Apache-2.0,
|
|
218
|
+
downloaded from its release; LoCoMo (CC BY-NC 4.0) and LongMemEval are linked, never redistributed.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { type Ask, type Extraction } from './extract.js';
|
|
2
|
+
import { type LtmStore, type MemoryItem, type StmEntry, type StmStore } from './store.js';
|
|
3
|
+
import { type WriterDeps } from './writer.js';
|
|
4
|
+
export declare const SAME = 0.93;
|
|
5
|
+
export declare const SAME_EPISODE = 0.97;
|
|
6
|
+
/** An observation is the same only when it says the same thing (this close) about the same date: two dinners on two
|
|
7
|
+
* days are two observations, and a list question needs both. */
|
|
8
|
+
export declare const SAME_OBSERVATION = 0.97;
|
|
9
|
+
export declare const RELATED = 0.8;
|
|
10
|
+
/** A conversation is consolidated once it has been quiet this long — or its entries are a day old. */
|
|
11
|
+
export declare const SETTLED_MS: number;
|
|
12
|
+
/** Extraction calls in flight: they are independent, and one at a time made a backfill take days (~86 s a window). */
|
|
13
|
+
export declare const EXTRACT_CONCURRENCY = 8;
|
|
14
|
+
export interface ConsolidateDeps extends WriterDeps {
|
|
15
|
+
stm: StmStore;
|
|
16
|
+
ltm: LtmStore;
|
|
17
|
+
log: (line: string) => void;
|
|
18
|
+
aborted: () => boolean;
|
|
19
|
+
ask?: Ask;
|
|
20
|
+
now?: () => number;
|
|
21
|
+
/** Windows the fast model extracts at once (EXTRACT_CONCURRENCY when not given). */
|
|
22
|
+
concurrency?: number;
|
|
23
|
+
/** Windows reconciled at once (1, in order, when not given). A backfill sets more: a window may then not see what one
|
|
24
|
+
* reconciled beside it wrote — the duplicates that leaves are the sleep pass's to merge — and an item two windows
|
|
25
|
+
* touch at once is changed under its lock, from what it is by then. */
|
|
26
|
+
reconcileConcurrency?: number;
|
|
27
|
+
/** A window's size in characters (WINDOW_CHARS when not given). */
|
|
28
|
+
windowChars?: number;
|
|
29
|
+
/** Observations read apart, in windows of this many characters (observeWindow); absent: in the window's one ask. */
|
|
30
|
+
observeChars?: number;
|
|
31
|
+
/** Each window read with what other conversations say that it may refer to (knownFor): "the book you recommended"
|
|
32
|
+
* written as the book's name. */
|
|
33
|
+
resolve?: boolean;
|
|
34
|
+
/** What a window's reading may keep, checked before anything is reconciled or written (e.g. facts only
|
|
35
|
+
* from the person's own words and results the agent confirmed, each with where it came from). An answer it cannot
|
|
36
|
+
* give leaves the window for the next run, as an extraction that was not answered does. */
|
|
37
|
+
guard?: (win: StmEntry[], x: Extraction) => Promise<Extraction | {
|
|
38
|
+
unavailable: string;
|
|
39
|
+
} | {
|
|
40
|
+
busy: true;
|
|
41
|
+
}>;
|
|
42
|
+
}
|
|
43
|
+
export interface ConsolidationReport {
|
|
44
|
+
windows: number;
|
|
45
|
+
added: number;
|
|
46
|
+
merged: number;
|
|
47
|
+
updated: number;
|
|
48
|
+
superseded: number;
|
|
49
|
+
linked: number;
|
|
50
|
+
dropped: number;
|
|
51
|
+
episodes: number;
|
|
52
|
+
observed: number;
|
|
53
|
+
cued: number;
|
|
54
|
+
paused?: string;
|
|
55
|
+
}
|
|
56
|
+
/** Items given their cues and concepts after a run's windows (the backlog: saved notes, what was learned before
|
|
57
|
+
* them), and how many go in one ask. */
|
|
58
|
+
export declare const CUE_BACKLOG = 40;
|
|
59
|
+
export type Relation = 'same' | 'updates' | 'contradicts' | 'related' | 'different';
|
|
60
|
+
/** Links an item keeps to others about the same subject (exploration walks them), newest last. */
|
|
61
|
+
export declare const MAX_LINKS = 24;
|
|
62
|
+
/** Pairs asked about together: at most 8, and at most RELATE_CHARS of their texts — a long pair on its own. */
|
|
63
|
+
export declare const RELATE_CHARS = 60000;
|
|
64
|
+
export declare function relateBatches<T extends {
|
|
65
|
+
it: MemoryItem;
|
|
66
|
+
known: MemoryItem;
|
|
67
|
+
}>(pairs: T[]): T[][];
|
|
68
|
+
export declare class Consolidator {
|
|
69
|
+
private readonly d;
|
|
70
|
+
private readonly vecs;
|
|
71
|
+
/** Windows whose extraction failed, by their first request: how many times in a row. */
|
|
72
|
+
private readonly failed;
|
|
73
|
+
/** Windows read in smaller parts from now on, by their first request: at most this many requests a part. */
|
|
74
|
+
private readonly narrowed;
|
|
75
|
+
/** An item changed by one window at a time (windows reconciled at once): the last holder's turn, by uid. */
|
|
76
|
+
private readonly locks;
|
|
77
|
+
constructor(d: ConsolidateDeps);
|
|
78
|
+
/** Runs with the items locked — taken in uid order, so two windows never wait on each other (sleep's answers too). */
|
|
79
|
+
locked<T>(uids: string[], run: () => Promise<T>): Promise<T>;
|
|
80
|
+
/** The item as it is now, or null when it stopped being active (merged or superseded by a window beside this one). */
|
|
81
|
+
private latest;
|
|
82
|
+
/** Embed with a per-run cache: the key is embedded once for reconcile and reused when it is written. */
|
|
83
|
+
private readonly embed;
|
|
84
|
+
private get writer();
|
|
85
|
+
/** Conversations with requests waiting, settled, newest first. */
|
|
86
|
+
pending(now?: number): Array<{
|
|
87
|
+
session: string;
|
|
88
|
+
uids: string[];
|
|
89
|
+
last: number;
|
|
90
|
+
}>;
|
|
91
|
+
/** Consolidate up to `maxWindows` windows. Stops early when the model pauses or the worker is stopped.
|
|
92
|
+
* The fast model extracts several windows at once (they are independent); each is reconciled in order, newest
|
|
93
|
+
* conversation first, so a later item still sees what an earlier one wrote. */
|
|
94
|
+
run(maxWindows?: number): Promise<ConsolidationReport>;
|
|
95
|
+
/** One window's reading, and what the guard (when there is one) lets it keep. */
|
|
96
|
+
private read;
|
|
97
|
+
/** One window's extraction: the one ask — and, when observations are read apart, the window's small windows too, each
|
|
98
|
+
* with the one before it as context. Any part not answered leaves the whole window for the next run. */
|
|
99
|
+
private extract;
|
|
100
|
+
/** What other conversations say that this window may refer to: a few of its requests searched for (memory_search —
|
|
101
|
+
* the raw requests of every conversation, captured before any is consolidated, and what was learned), the best
|
|
102
|
+
* passages outside this conversation, dated. The order conversations are consolidated in does not matter: the raw
|
|
103
|
+
* record holds them all. Nothing found, or a search that fails, reads the window without it. */
|
|
104
|
+
private knownFor;
|
|
105
|
+
/** A window as it is read now: in parts of at most the size its earlier failures narrowed it to, each part narrowed
|
|
106
|
+
* again by its own failures. */
|
|
107
|
+
private narrow;
|
|
108
|
+
/** Encoding's backlog: active items without cues or concepts — saved notes (they come in curated, without the
|
|
109
|
+
* consolidation call) and what was learned before either existed — get them, newest first, a few an ask, several
|
|
110
|
+
* asks at once. Cues an item already has stay. */
|
|
111
|
+
fillCues(max?: number): Promise<{
|
|
112
|
+
cued: number;
|
|
113
|
+
paused?: string;
|
|
114
|
+
}>;
|
|
115
|
+
private cueBatch;
|
|
116
|
+
/** Each item against what long-term memory holds — written as it goes, so a later item sees an earlier one. */
|
|
117
|
+
private reconcile;
|
|
118
|
+
/** Observations are only ever added: one is the same as a known one only when it says the same (SAME_OBSERVATION)
|
|
119
|
+
* about the same date — then the known one is told again (provenance, `seen`). Never asked about, never merged into
|
|
120
|
+
* another text, never superseded: an older one stays, dated, beside the newer. */
|
|
121
|
+
private observe;
|
|
122
|
+
private add;
|
|
123
|
+
/** The known item absorbs the new one: provenance, `seen`, the newest time, the higher importance — and, when the
|
|
124
|
+
* model merged the texts, the merged key (embedded again). Nothing of either is lost: the new item's own text, and the
|
|
125
|
+
* known one's before a rewrite, stay in the log as `merged` records the item lists (`absorbed`). */
|
|
126
|
+
merge(known: MemoryItem, it: MemoryItem, text: {
|
|
127
|
+
title: string;
|
|
128
|
+
summary: string;
|
|
129
|
+
body: string;
|
|
130
|
+
} | null): Promise<void>;
|
|
131
|
+
/** Two items about the same subject that say different things: each lists the other (memory_explore walks these). */
|
|
132
|
+
link(a: MemoryItem, b: MemoryItem): Promise<void>;
|
|
133
|
+
/** Superseded: out of the index and of recall, kept in the log with what replaced it. */
|
|
134
|
+
retire(known: MemoryItem, by: string): Promise<void>;
|
|
135
|
+
relate(batch: Array<{
|
|
136
|
+
it: MemoryItem;
|
|
137
|
+
known: MemoryItem;
|
|
138
|
+
}>): Promise<Map<string, {
|
|
139
|
+
relation: Relation;
|
|
140
|
+
title: string;
|
|
141
|
+
summary: string;
|
|
142
|
+
body: string;
|
|
143
|
+
}> | null>;
|
|
144
|
+
}
|