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 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). Windows has the pieces it needs but is not tested yet. The first `openMemory` downloads the embedding model
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 });
@@ -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
- const src = openSync(this.path, 'r');
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
- closeSync(src);
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.2",
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
  }