memtrails 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/memory.js CHANGED
@@ -136,6 +136,44 @@ function saveIndexLayout(dir) {
136
136
  writeFileSync(`${path}.tmp`, JSON.stringify({ layout: INDEX_LAYOUT }), { mode: 0o600 });
137
137
  renameSync(`${path}.tmp`, path);
138
138
  }
139
+ /** Turns shared, and a turn alone: many share at once; one alone waits until no one shares, and keeps new sharers out
140
+ * while it waits, so a stream of them never holds it off. */
141
+ function turns() {
142
+ let sharing = 0;
143
+ let alone = false;
144
+ let waiting = 0;
145
+ let wakers = [];
146
+ const wake = () => { const w = wakers; wakers = []; for (const f of w)
147
+ f(); };
148
+ const until = async (ok) => { while (!ok())
149
+ await new Promise((r) => wakers.push(r)); };
150
+ return {
151
+ async shared(job) {
152
+ await until(() => !alone && !waiting);
153
+ sharing++;
154
+ try {
155
+ return await job();
156
+ }
157
+ finally {
158
+ sharing--;
159
+ wake();
160
+ }
161
+ },
162
+ async alone(job) {
163
+ waiting++;
164
+ await until(() => !alone && !sharing);
165
+ waiting--;
166
+ alone = true;
167
+ try {
168
+ return await job();
169
+ }
170
+ finally {
171
+ alone = false;
172
+ wake();
173
+ }
174
+ },
175
+ };
176
+ }
139
177
  export async function openMemory(o) {
140
178
  const dir = resolve(o.home);
141
179
  const log = o.log ?? (() => { });
@@ -243,6 +281,20 @@ async function openIn(dir, o, log, started, release) {
243
281
  return run;
244
282
  };
245
283
  let closed = null;
284
+ /** Every call (add, search, open, …) shares a turn; close() takes it alone, so it lets go of Qdrant, the model and the
285
+ * home only once the calls in flight are done. */
286
+ const calls = turns();
287
+ /** Adds share a turn; a rebuild takes it alone. An add beside a rebuild could write its points into the index about to
288
+ * be dropped and log its record after the rebuild read the log: logged, never indexed, and skipped as unchanged when
289
+ * added again. */
290
+ const adding = turns();
291
+ const check = () => { if (closing)
292
+ throw new Error('memory is closed'); };
293
+ /** A call's shared turn; refused once close() began, also when it waited for its turn meanwhile. */
294
+ const call = async (job) => {
295
+ check();
296
+ return calls.shared(async () => { check(); return job(); });
297
+ };
246
298
  /** The text word by word scores an item by: its key, as search returns it. */
247
299
  const keyText = (title, summary) => `${title}\n${summary}`;
248
300
  /** A found item's whole text for the room: a record's own, or what consolidation wrote. */
@@ -271,7 +323,8 @@ async function openIn(dir, o, log, started, release) {
271
323
  const room = `${x.request}\n${x.reply}`.slice(0, ROOM_TEXT);
272
324
  return [keyText(stmTitle(x), rawKey(x)), ...sentenceSpans(room).map(([a, b]) => room.slice(a, b))];
273
325
  });
274
- const write = async (entries, total) => {
326
+ const write = (entries, total) => call(() => adding.shared(async () => {
327
+ check(); // it may have waited for a rebuild that a close stopped
275
328
  const fresh = entries.filter((x) => stm.meta(x.uid)?.fp !== x.fingerprint);
276
329
  for (let i = 0; i < fresh.length; i += BATCH) {
277
330
  const part = fresh.slice(i, i + BATCH);
@@ -281,7 +334,7 @@ async function openIn(dir, o, log, started, release) {
281
334
  }
282
335
  stm.compact();
283
336
  return { added: fresh.length, unchanged: total - fresh.length };
284
- };
337
+ }));
285
338
  /** Unlocked: consolidate and sleep run it inside their own turn. */
286
339
  const indexWords = async () => {
287
340
  if (!encode)
@@ -351,16 +404,19 @@ async function openIn(dir, o, log, started, release) {
351
404
  await Promise.all([stm.flush(), ltm.flush()]);
352
405
  return uids.length;
353
406
  }),
354
- search: (s) => memorySearch(deps, s),
355
- searchText: async (s) => searchText(await memorySearch(deps, s), s.order ?? 'relevance'),
407
+ search: (s) => call(() => memorySearch(deps, s)),
408
+ searchText: (s) => call(async () => searchText(await memorySearch(deps, s), s.order ?? 'relevance')),
356
409
  async room(s) {
410
+ check();
357
411
  if (!encode)
358
412
  throw new Error('the room needs the local embedding model (embedding.modelDir, or no embedding.server)');
359
- const found = await memorySearch(deps, { ...s, limit: s.limit ?? 20 });
360
- return sentenceRoom(s.query, found.map(roomItem), encode, words, s.chars ?? ROOM_CHARS);
413
+ return call(async () => {
414
+ const found = await memorySearch(deps, { ...s, limit: s.limit ?? 20 });
415
+ return sentenceRoom(s.query, found.map(roomItem), encode, words, s.chars ?? ROOM_CHARS);
416
+ });
361
417
  },
362
418
  async open(id) {
363
- const p = await memoryOpen(deps, id);
419
+ const p = await call(() => memoryOpen(deps, id));
364
420
  if (!p)
365
421
  return null;
366
422
  // A record: its kind as given. Something learned: the records it was learned from, whole.
@@ -371,8 +427,8 @@ async function openIn(dir, o, log, started, release) {
371
427
  .map((x) => ({ id: stmItem(x.uid), when: x.ts, title: x.title, body: episodeBody(x), ...(x.record ? { record: x.record } : {}) }));
372
428
  return sources.length ? { ...p, sources } : p;
373
429
  },
374
- explore: (s) => memoryExplore(deps, s),
375
- gather: (s, g) => memoryGather(deps, s, { ask, ...(s.limit ? { limit: s.limit } : {}), ...(g?.notes ? { notes: true } : {}) }),
430
+ explore: (s) => call(() => memoryExplore(deps, s)),
431
+ gather: (s, g) => call(() => memoryGather(deps, s, { ask, ...(s.limit ? { limit: s.limit } : {}), ...(g?.notes ? { notes: true } : {}) })),
376
432
  consolidate: () => exclusive(async () => {
377
433
  const worker = new Consolidator({ ...base, concurrency: o.concurrency ?? 4 });
378
434
  const total = { windows: 0, added: 0, merged: 0, updated: 0, superseded: 0, linked: 0, dropped: 0, episodes: 0, observed: 0, cued: 0 };
@@ -397,31 +453,34 @@ async function openIn(dir, o, log, started, release) {
397
453
  return r;
398
454
  }),
399
455
  indexWords: () => exclusive(indexWords),
400
- rebuild: () => exclusive(async () => {
456
+ rebuild: () => exclusive(() => adding.alone(async () => {
401
457
  rebuild.begin(); // before the drop: a crash, or a close, after it resumes at the next open
402
458
  await client.dropCollection();
403
459
  await client.ensureCollection();
404
460
  await rebuild.run();
405
- }),
461
+ })),
406
462
  tools: () => buildAgentTools({
407
- search: (s) => memorySearch(deps, s),
463
+ search: (s) => memory.search(s),
408
464
  open: (id) => memory.open(id),
409
- explore: (s) => memoryExplore(deps, s),
465
+ explore: (s) => memory.explore(s),
410
466
  }),
411
467
  stats: () => ({ records: stm.size(), items: [...ltm.entries()].filter(([, m]) => m.status === 'active').length, words: words.size }),
412
468
  close() {
413
469
  closed ??= (async () => {
414
470
  closing = true;
415
471
  await queue; // the job running stops at its next step; those waiting are refused
416
- try {
417
- await book.settled();
418
- usage.flush();
419
- await proc?.stopAndWait();
420
- await local?.close();
421
- }
422
- finally {
423
- release();
424
- }
472
+ // The adds and searches in flight end first; those waiting for their turn, and every call after, are refused.
473
+ await calls.alone(async () => {
474
+ try {
475
+ await book.settled();
476
+ usage.flush();
477
+ await proc?.stopAndWait();
478
+ await local?.close();
479
+ }
480
+ finally {
481
+ release();
482
+ }
483
+ });
425
484
  })();
426
485
  return closed;
427
486
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memtrails",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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",