memtrails 0.5.2 → 0.5.4
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 +19 -0
- package/README.md +24 -1
- package/dist/core/qdrant-binary.d.ts +5 -0
- package/dist/core/qdrant-binary.js +14 -0
- package/dist/core/store.js +6 -2
- package/dist/integrations/document.d.ts +14 -0
- package/dist/integrations/document.js +19 -0
- package/dist/langchain.d.ts +27 -0
- package/dist/langchain.js +30 -0
- package/dist/memory.js +19 -1
- package/package.json +14 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ versions follow [Semantic Versioning](https://semver.org/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.5.4] — 2026-11-05
|
|
9
|
+
|
|
10
|
+
- Windows (x64), in all three languages, tested on every change (the tests, and a real memory searched with the real
|
|
11
|
+
model and Qdrant). Fixed on the way: TypeScript rewrote a log over a file still open; Python checked a
|
|
12
|
+
process with `os.kill(pid, 0)`, which on Windows sends Ctrl+C to its console group, wrote `\r\n` where the files say
|
|
13
|
+
`\n` (records then read back as missing), removed lock files while they were open, took a time-zone offset 2³³
|
|
14
|
+
seconds off for some years, and printed in the console's code page; Rust took every process for alive, never stopped
|
|
15
|
+
its Qdrant at exit, and unpacked the Windows ONNX Runtime as gzip; and in all three, a lock being let go by another
|
|
16
|
+
process failed the one taking it. A memory whose path is too long for Qdrant's files on Windows is now said to be so.
|
|
17
|
+
|
|
18
|
+
## [0.5.3] — 2026-11-05
|
|
19
|
+
|
|
20
|
+
- LangChain and LlamaIndex retrievers: `memtrails/langchain` (with `@langchain/core` beside it, an optional peer), and
|
|
21
|
+
in Python `memtrails.integrations.langchain` and `memtrails.integrations.llama_index` (`pip install
|
|
22
|
+
memtrails[langchain]` / `[llama-index]`). Each found item becomes the framework's document with the same id, text and
|
|
23
|
+
flat metadata in both languages (`conformance/fixtures/retriever.json`). No TypeScript LlamaIndex adapter: its
|
|
24
|
+
packages are no longer maintained.
|
|
25
|
+
The Rust version is released at the same version, unchanged (LangChain and LlamaIndex are Python and JavaScript).
|
|
26
|
+
|
|
8
27
|
## [0.5.2] — 2026-11-05
|
|
9
28
|
|
|
10
29
|
- Examples in Python and Rust, the same four as in TypeScript (a quick start, importing sources, the agent tools,
|
package/README.md
CHANGED
|
@@ -58,10 +58,17 @@ More to run in [`examples/`](https://github.com/cmaftuleac/memtrails/blob/main/e
|
|
|
58
58
|
npm install memtrails
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
Node.js 22 or newer, on macOS or Linux (x64 or arm64)
|
|
61
|
+
Node.js 22 or newer, on macOS or Linux (x64 or arm64) or Windows (x64). The first `openMemory` downloads the embedding model
|
|
62
62
|
(218 MB, from Hugging Face) and the Qdrant binary (~30 MB, from Qdrant's GitHub release) into `~/.cache/memtrails`,
|
|
63
63
|
each checked against a pinned SHA-256. Set `MEMTRAILS_CACHE` to put them elsewhere.
|
|
64
64
|
|
|
65
|
+
On Windows, keep a memory's path under 90 characters: Qdrant's files sit up to about 170 characters deeper, and
|
|
66
|
+
Windows stops a path at 260 (memtrails says so when one is longer).
|
|
67
|
+
|
|
68
|
+
On Linux x64, `ONNXRUNTIME_NODE_INSTALL=skip npm install memtrails` skips the CUDA libraries ONNX Runtime's installer
|
|
69
|
+
downloads otherwise (about 350 MB; memtrails runs on the CPU and never uses them; see [SECURITY.md](SECURITY.md)). In
|
|
70
|
+
an MCP client's configuration the same goes in `"env": { "ONNXRUNTIME_NODE_INSTALL": "skip" }`.
|
|
71
|
+
|
|
65
72
|
### Python and Rust
|
|
66
73
|
|
|
67
74
|
The same library, written natively in each language, fully async, with the same results and the same files: a memory
|
|
@@ -100,6 +107,22 @@ The memory lives in `~/.memtrails` (`--home` or `MEMTRAILS_HOME` for another). T
|
|
|
100
107
|
call and closes it after five minutes without one (`--idle`), so `add` can run in between; `--read-only` leaves out
|
|
101
108
|
`memory_save`. From code: `serveMcp({ open: () => openMemory({ home }), version })`.
|
|
102
109
|
|
|
110
|
+
## LangChain and LlamaIndex
|
|
111
|
+
|
|
112
|
+
A memory is a LangChain retriever (install `@langchain/core` beside memtrails):
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { MemtrailsRetriever } from 'memtrails/langchain';
|
|
116
|
+
const docs = await new MemtrailsRetriever({ memory, search: { limit: 8 } }).invoke('why did renewal stop?');
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Each document is a found item: its summary (and the passage that matched) as the text, its id, and flat metadata
|
|
120
|
+
(`id`, `kind`, `title`, `date`, `ts`, `score`, and when it has them `eventAt`/`startAt`/`endAt`, `recordId`, `source`,
|
|
121
|
+
`author`, `url`); `memory.open(id)` gives all of it. `search` takes every search option but the query (`collections`,
|
|
122
|
+
`kinds`, `limit`, `from`, `to`; `limit` counts dated observations and other memories each). In Python, LangChain and
|
|
123
|
+
LlamaIndex both: `memtrails.integrations.langchain` and `memtrails.integrations.llama_index`. (LlamaIndex's TypeScript
|
|
124
|
+
packages are no longer maintained, so there is no TypeScript LlamaIndex adapter.)
|
|
125
|
+
|
|
103
126
|
## Adding data
|
|
104
127
|
|
|
105
128
|
A **record** is anything with text. Only `id` and `text` are required.
|
|
@@ -56,6 +56,11 @@ export declare const LOCK_STALE_MS = 120000;
|
|
|
56
56
|
* rests on the lock for correctness (every file is verified in a private temp and renamed into place); it keeps two
|
|
57
57
|
* runs from fetching the same hundreds of MB, and from swapping folders under each other.
|
|
58
58
|
*/
|
|
59
|
+
/** How long "access denied" on making a lock is taken for one being removed (Windows) before it is an error. */
|
|
60
|
+
export declare const DELETE_PENDING_MS = 2000;
|
|
61
|
+
/** Windows answers "access denied", not "exists" or "not found", for a file another process is removing at that moment:
|
|
62
|
+
* a lock being let go. A moment later it is gone. Elsewhere that answer is a real refusal. */
|
|
63
|
+
export declare function beingRemoved(err: unknown): boolean;
|
|
59
64
|
export declare function withLock<T>(path: string, f: () => Promise<T>, o?: {
|
|
60
65
|
signal?: AbortSignal;
|
|
61
66
|
staleMs?: number;
|
|
@@ -169,11 +169,20 @@ function lockStale(lock, staleMs) {
|
|
|
169
169
|
* rests on the lock for correctness (every file is verified in a private temp and renamed into place); it keeps two
|
|
170
170
|
* runs from fetching the same hundreds of MB, and from swapping folders under each other.
|
|
171
171
|
*/
|
|
172
|
+
/** How long "access denied" on making a lock is taken for one being removed (Windows) before it is an error. */
|
|
173
|
+
export const DELETE_PENDING_MS = 2000;
|
|
174
|
+
/** Windows answers "access denied", not "exists" or "not found", for a file another process is removing at that moment:
|
|
175
|
+
* a lock being let go. A moment later it is gone. Elsewhere that answer is a real refusal. */
|
|
176
|
+
export function beingRemoved(err) {
|
|
177
|
+
const code = err.code;
|
|
178
|
+
return process.platform === 'win32' && (code === 'EPERM' || code === 'EACCES');
|
|
179
|
+
}
|
|
172
180
|
export async function withLock(path, f, o = {}) {
|
|
173
181
|
const lock = `${path}.lock`;
|
|
174
182
|
const staleMs = o.staleMs ?? LOCK_STALE_MS;
|
|
175
183
|
const mine = `${process.pid} ${hostname()} ${randomBytes(8).toString('hex')}`;
|
|
176
184
|
mkdirSync(dirname(lock), { recursive: true });
|
|
185
|
+
let denied = null;
|
|
177
186
|
for (;;) {
|
|
178
187
|
try {
|
|
179
188
|
const fd = openSync(lock, 'wx', 0o600);
|
|
@@ -182,8 +191,13 @@ export async function withLock(path, f, o = {}) {
|
|
|
182
191
|
break;
|
|
183
192
|
}
|
|
184
193
|
catch (err) {
|
|
194
|
+
if (beingRemoved(err) && Date.now() - (denied ??= Date.now()) < DELETE_PENDING_MS) {
|
|
195
|
+
await new Promise((ok) => setTimeout(ok, o.pollMs ?? 250));
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
185
198
|
if (err.code !== 'EEXIST')
|
|
186
199
|
throw err;
|
|
200
|
+
denied = null;
|
|
187
201
|
}
|
|
188
202
|
if (lockStale(lock, staleMs)) {
|
|
189
203
|
rmSync(lock, { force: true });
|
package/dist/core/store.js
CHANGED
|
@@ -166,7 +166,7 @@ export class JsonlLog {
|
|
|
166
166
|
const order = [...this.live.entries()].sort((a, b) => a[1].at - b[1].at);
|
|
167
167
|
const tmp = `${this.path}.compact`;
|
|
168
168
|
const next = new Map();
|
|
169
|
-
|
|
169
|
+
let src = openSync(this.path, 'r');
|
|
170
170
|
let out = null;
|
|
171
171
|
try {
|
|
172
172
|
out = openSync(tmp, 'w', 0o600);
|
|
@@ -195,6 +195,9 @@ export class JsonlLog {
|
|
|
195
195
|
fsyncSync(out);
|
|
196
196
|
closeSync(out);
|
|
197
197
|
out = null;
|
|
198
|
+
// Closed before the rename: Windows does not replace a file that is open.
|
|
199
|
+
closeSync(src);
|
|
200
|
+
src = null;
|
|
198
201
|
renameSync(tmp, this.path);
|
|
199
202
|
this.live.clear();
|
|
200
203
|
for (const [uid, s] of next)
|
|
@@ -209,7 +212,8 @@ export class JsonlLog {
|
|
|
209
212
|
throw e;
|
|
210
213
|
}
|
|
211
214
|
finally {
|
|
212
|
-
|
|
215
|
+
if (src !== null)
|
|
216
|
+
closeSync(src);
|
|
213
217
|
}
|
|
214
218
|
}
|
|
215
219
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A found item as a framework's document (a LangChain `Document`; in Python also a LlamaIndex `TextNode`): its id,
|
|
3
|
+
* the text a model reads, and flat metadata a vector store or a prompt can carry. `memory.open(id)` gives the whole item.
|
|
4
|
+
*/
|
|
5
|
+
import type { RecalledItem } from '../core/recall.js';
|
|
6
|
+
export interface RetrievedDocument {
|
|
7
|
+
id: string;
|
|
8
|
+
/** The item's summary, and the passage that matched when that was not its start. */
|
|
9
|
+
text: string;
|
|
10
|
+
/** id, kind, title, date (YYYY-MM-DD of `ts`), ts, score; for an observation eventAt, startAt, endAt; for a record you
|
|
11
|
+
* added recordId, source, author, url. Only the ones it has. */
|
|
12
|
+
metadata: Record<string, string | number>;
|
|
13
|
+
}
|
|
14
|
+
export declare function retrievedDocument(h: RecalledItem): RetrievedDocument;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function retrievedDocument(h) {
|
|
2
|
+
const m = {
|
|
3
|
+
id: h.id,
|
|
4
|
+
kind: h.kind,
|
|
5
|
+
title: h.title,
|
|
6
|
+
date: h.ts > 0 ? new Date(h.ts).toISOString().slice(0, 10) : undefined,
|
|
7
|
+
ts: h.ts,
|
|
8
|
+
score: h.score,
|
|
9
|
+
eventAt: h.eventAt,
|
|
10
|
+
startAt: h.startAt,
|
|
11
|
+
endAt: h.endAt,
|
|
12
|
+
recordId: h.record?.id,
|
|
13
|
+
source: h.record?.source,
|
|
14
|
+
author: h.record?.author,
|
|
15
|
+
url: h.record?.url,
|
|
16
|
+
};
|
|
17
|
+
const metadata = Object.fromEntries(Object.entries(m).filter((e) => e[1] !== undefined && e[1] !== ''));
|
|
18
|
+
return { id: h.id, text: h.match ? `${h.summary} … ${h.match}` : h.summary, metadata };
|
|
19
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A memory as a LangChain retriever (`@langchain/core`, installed beside memtrails):
|
|
3
|
+
*
|
|
4
|
+
* import { MemtrailsRetriever } from 'memtrails/langchain';
|
|
5
|
+
* const retriever = new MemtrailsRetriever({ memory, search: { limit: 8 } });
|
|
6
|
+
* const docs = await retriever.invoke('why did the certificate stop renewing?');
|
|
7
|
+
*
|
|
8
|
+
* Each Document is a found item: its summary (and the passage that matched) as `pageContent`, its id, and flat metadata
|
|
9
|
+
* (integrations/document.ts). `search` is every search option but the query: collections, kinds, limit, from, to.
|
|
10
|
+
*/
|
|
11
|
+
import { Document } from '@langchain/core/documents';
|
|
12
|
+
import { BaseRetriever, type BaseRetrieverInput } from '@langchain/core/retrievers';
|
|
13
|
+
import type { RecalledItem, SearchQuery } from './core/recall.js';
|
|
14
|
+
export interface MemtrailsRetrieverInput extends BaseRetrieverInput {
|
|
15
|
+
/** An open memory (openMemory), or anything that searches like one. */
|
|
16
|
+
memory: {
|
|
17
|
+
search(q: SearchQuery): Promise<RecalledItem[]>;
|
|
18
|
+
};
|
|
19
|
+
search?: Omit<SearchQuery, 'query'>;
|
|
20
|
+
}
|
|
21
|
+
export declare class MemtrailsRetriever extends BaseRetriever {
|
|
22
|
+
lc_namespace: string[];
|
|
23
|
+
private readonly memory;
|
|
24
|
+
private readonly search;
|
|
25
|
+
constructor(fields: MemtrailsRetrieverInput);
|
|
26
|
+
_getRelevantDocuments(query: string): Promise<Document[]>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A memory as a LangChain retriever (`@langchain/core`, installed beside memtrails):
|
|
3
|
+
*
|
|
4
|
+
* import { MemtrailsRetriever } from 'memtrails/langchain';
|
|
5
|
+
* const retriever = new MemtrailsRetriever({ memory, search: { limit: 8 } });
|
|
6
|
+
* const docs = await retriever.invoke('why did the certificate stop renewing?');
|
|
7
|
+
*
|
|
8
|
+
* Each Document is a found item: its summary (and the passage that matched) as `pageContent`, its id, and flat metadata
|
|
9
|
+
* (integrations/document.ts). `search` is every search option but the query: collections, kinds, limit, from, to.
|
|
10
|
+
*/
|
|
11
|
+
import { Document } from '@langchain/core/documents';
|
|
12
|
+
import { BaseRetriever } from '@langchain/core/retrievers';
|
|
13
|
+
import { retrievedDocument } from './integrations/document.js';
|
|
14
|
+
export class MemtrailsRetriever extends BaseRetriever {
|
|
15
|
+
lc_namespace = ['memtrails', 'retrievers'];
|
|
16
|
+
memory;
|
|
17
|
+
search;
|
|
18
|
+
constructor(fields) {
|
|
19
|
+
super(fields);
|
|
20
|
+
this.memory = fields.memory;
|
|
21
|
+
this.search = fields.search ?? {};
|
|
22
|
+
}
|
|
23
|
+
async _getRelevantDocuments(query) {
|
|
24
|
+
const hits = await this.memory.search({ ...this.search, query });
|
|
25
|
+
return hits.map((h) => {
|
|
26
|
+
const d = retrievedDocument(h);
|
|
27
|
+
return new Document({ id: d.id, pageContent: d.text, metadata: d.metadata });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
package/dist/memory.js
CHANGED
|
@@ -21,7 +21,7 @@ import { remoteEmbedder, resolveEmbedRoute } from './core/embedder.js';
|
|
|
21
21
|
import { memoryGather } from './core/gather.js';
|
|
22
22
|
import { loadLocalEmbedder } from './core/local-embedder.js';
|
|
23
23
|
import { ensureModel } from './core/model.js';
|
|
24
|
-
import { ensureQdrant } from './core/qdrant-binary.js';
|
|
24
|
+
import { beingRemoved, DELETE_PENDING_MS, ensureQdrant } from './core/qdrant-binary.js';
|
|
25
25
|
import { COLLECTION, INDEX_LAYOUT, QdrantClient } from './core/qdrant-client.js';
|
|
26
26
|
import { alive, bootTime, QdrantProcess, sameBoot } from './core/qdrant-process.js';
|
|
27
27
|
import { memoryExplore, memoryOpen, memorySearch } from './core/recall.js';
|
|
@@ -76,14 +76,21 @@ function lockHolder(path) {
|
|
|
76
76
|
* ran before a reboot (its pid may be another's now), is taken over. Returns its release. */
|
|
77
77
|
function lockHome(dir) {
|
|
78
78
|
const path = join(dir, LOCK_FILE);
|
|
79
|
+
let denied = null;
|
|
79
80
|
for (let attempt = 0;; attempt++) {
|
|
80
81
|
let fd;
|
|
81
82
|
try {
|
|
82
83
|
fd = openSync(path, 'wx', 0o600);
|
|
83
84
|
}
|
|
84
85
|
catch (err) {
|
|
86
|
+
if (beingRemoved(err) && Date.now() - (denied ??= Date.now()) < DELETE_PENDING_MS) {
|
|
87
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10); // a moment, then again
|
|
88
|
+
attempt--;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
85
91
|
if (err.code !== 'EEXIST')
|
|
86
92
|
throw err;
|
|
93
|
+
denied = null;
|
|
87
94
|
const { text, pid } = lockHolder(path);
|
|
88
95
|
if (pid !== null || attempt > 0) {
|
|
89
96
|
throw new Error(`memory ${dir} is open${pid ? ` in process ${pid}` : ''}: close it there first (if no process has it, delete ${path})`);
|
|
@@ -197,7 +204,18 @@ export async function openMemory(o) {
|
|
|
197
204
|
throw err;
|
|
198
205
|
}
|
|
199
206
|
}
|
|
207
|
+
/** Windows stops at 260 characters a path, and Qdrant's files sit up to about 170 below a memory's own folder. */
|
|
208
|
+
const WINDOWS_HOME_MAX = 90;
|
|
209
|
+
/** Said before Qdrant fails on it with no more than "the system cannot find the path specified". */
|
|
210
|
+
function windowsHomeTooLong(dir, o, log) {
|
|
211
|
+
if (process.platform !== 'win32' || (o.qdrant && 'url' in o.qdrant) || dir.length <= WINDOWS_HOME_MAX)
|
|
212
|
+
return;
|
|
213
|
+
log(`[memtrails] ${dir} is ${dir.length} characters long: on Windows, Qdrant's files sit up to about 170 characters `
|
|
214
|
+
+ `deeper and a path stops at 260, so the index may fail ("the system cannot find the path specified"); keep a `
|
|
215
|
+
+ `memory's path under ${WINDOWS_HOME_MAX} characters`);
|
|
216
|
+
}
|
|
200
217
|
async function openIn(dir, o, log, started, release) {
|
|
218
|
+
windowsHomeTooLong(dir, o, log);
|
|
201
219
|
// Embedding: here, or on a server — the options' own, else the one the environment names.
|
|
202
220
|
const e = o.embedding ?? {};
|
|
203
221
|
const server = e.server ?? (e.modelDir ? null : resolveEmbedRoute());
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memtrails",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Retrieval and exploration for any text — e-mail, chat, notes, documents, news, commits, conversations. Searchable at once, on your own machine, in any language; optional consolidation into typed knowledge with a language model.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"retrieval",
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
"types": "./dist/importers/index.d.ts",
|
|
42
42
|
"import": "./dist/importers/index.js"
|
|
43
43
|
},
|
|
44
|
+
"./langchain": {
|
|
45
|
+
"types": "./dist/langchain.d.ts",
|
|
46
|
+
"import": "./dist/langchain.js"
|
|
47
|
+
},
|
|
44
48
|
"./package.json": "./package.json"
|
|
45
49
|
},
|
|
46
50
|
"bin": {
|
|
@@ -71,6 +75,7 @@
|
|
|
71
75
|
"onnxruntime-node": "1.23.2"
|
|
72
76
|
},
|
|
73
77
|
"devDependencies": {
|
|
78
|
+
"@langchain/core": "1.3.4",
|
|
74
79
|
"@modelcontextprotocol/sdk": "1.32.2",
|
|
75
80
|
"@types/node": "^22.10.0",
|
|
76
81
|
"esbuild": "^0.25.12",
|
|
@@ -80,5 +85,13 @@
|
|
|
80
85
|
},
|
|
81
86
|
"publishConfig": {
|
|
82
87
|
"access": "public"
|
|
88
|
+
},
|
|
89
|
+
"peerDependencies": {
|
|
90
|
+
"@langchain/core": ">=1.0.0 <2"
|
|
91
|
+
},
|
|
92
|
+
"peerDependenciesMeta": {
|
|
93
|
+
"@langchain/core": {
|
|
94
|
+
"optional": true
|
|
95
|
+
}
|
|
83
96
|
}
|
|
84
97
|
}
|