memtrails 0.1.0 → 0.2.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 +9 -0
- package/README.md +8 -4
- package/dist/core/consolidate.js +1 -1
- package/dist/core/episodes.d.ts +5 -3
- package/dist/core/episodes.js +9 -5
- package/dist/core/extract.d.ts +3 -0
- package/dist/core/extract.js +12 -7
- package/dist/core/qdrant-client.js +11 -3
- package/dist/memory.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
All notable changes are listed here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
4
4
|
versions follow [Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## [0.2.0] — 2026-11-01
|
|
7
|
+
|
|
8
|
+
- Consolidation reads "last week" as the seven days before it was said, and the calendar line names the last weekend
|
|
9
|
+
and each of the six days before, so "on Friday" is dated without counting. Dates had landed a week early.
|
|
10
|
+
- Observations carry 2 to 4 cues (questions they answer, in the asker's words); search reads cues as their own channel
|
|
11
|
+
at a tenth of the dense weight. LoCoMo, all 1,307 held-out questions: multi-hop 50.6 → 55.2, all 77.2 → 77.4.
|
|
12
|
+
- `gather` reads as many as `limit` asks for (it always read 200). At 40, for any question, the rooms it builds score
|
|
13
|
+
79.5 on LoCoMo against Mem0's 76.5 (+110/−71 per question); `bench/locomo.ts --gather 40` reproduces it.
|
|
14
|
+
|
|
6
15
|
## [0.1.0] — 2026-10-31
|
|
7
16
|
|
|
8
17
|
First public release.
|
package/README.md
CHANGED
|
@@ -40,7 +40,8 @@ await memory.close();
|
|
|
40
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
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
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.
|
|
43
|
+
CPU scores 77.4 (strict judge) against Mem0's 76.5 — a tie, reported as one; with `gather` reading what search
|
|
44
|
+
found, 79.5 — ahead beyond chance. [Benchmarks](docs/BENCHMARKS.md).
|
|
44
45
|
- **Explorable.** A concept graph links what records share — people, places, systems, names — so an agent can walk
|
|
45
46
|
from what it found to what a search for the words does not find.
|
|
46
47
|
- **Built for agents.** `memory.tools()` gives `memory_search`, `memory_open` and `memory_explore` with zod and JSON
|
|
@@ -188,13 +189,16 @@ the [API reference](docs/API.md), and [what was tried and why](docs/RESEARCH.md)
|
|
|
188
189
|
|
|
189
190
|
| | memtrails | Mem0 | Graphiti |
|
|
190
191
|
|---|---|---|---|
|
|
191
|
-
| LoCoMo, all 1,307 held-out questions, 6,000 characters, strict judge | **
|
|
192
|
-
| the same, Mem0's own lenient judge | **
|
|
192
|
+
| LoCoMo, all 1,307 held-out questions, 6,000 characters, strict judge, `gather` at 40 | **79.5** | 76.5 | — |
|
|
193
|
+
| the same, Mem0's own lenient judge | **91.4** | 87.8 | — |
|
|
194
|
+
| the same with `search` alone, no model when searching, strict / lenient | 77.4 / 89.3 | 76.5 / 87.8 | — |
|
|
193
195
|
| LoCoMo, 1,307 held-out questions, 28,000 characters, one search¹ | 79.9 | 80.6 | 42.1 |
|
|
194
196
|
| LoCoMo, the same with an agent using each system's own tools¹ | **79.0** | 77.1 | 47.0 |
|
|
195
197
|
| LongMemEval-S retrieval, strict all sessions @5 (467)² | **96.15** | — | — |
|
|
196
198
|
|
|
197
|
-
The same reader and judges for every system.
|
|
199
|
+
The same reader and judges for every system. With `gather` (a model reads what search found and keeps what the answer
|
|
200
|
+
needs) memtrails leads Mem0 beyond chance: +110/−71 questions strict (p 0.005), +78/−32 lenient. With `search` alone,
|
|
201
|
+
the small-room and one-search comparisons are ties; the agent lead is
|
|
198
202
|
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
203
|
|
|
200
204
|
## Limitations
|
package/dist/core/consolidate.js
CHANGED
|
@@ -146,7 +146,7 @@ function observationItem(o, src) {
|
|
|
146
146
|
engine: src.engine, session: src.session, cwd: src.cwd, where: src.title, ts: src.ts, firstTs: src.ts,
|
|
147
147
|
title: cut(o.text, 100), summary: o.text, body: '', files: [], appliesWhen: '', confidence: 'stated', importance: 2,
|
|
148
148
|
status: 'active', provenance: o.provenance, seen: 1, fingerprint: fingerprint('observation', o.text, o.eventAt),
|
|
149
|
-
cues: [], concepts: o.concepts, ...(o.eventAt ? { eventAt: o.eventAt } : {}),
|
|
149
|
+
cues: o.cues ?? [], concepts: o.concepts, ...(o.eventAt ? { eventAt: o.eventAt } : {}),
|
|
150
150
|
...(o.startAt ? { startAt: o.startAt } : {}), ...(o.endAt ? { endAt: o.endAt } : {}), phase: o.phase, ...(o.about ? { about: o.about } : {}),
|
|
151
151
|
...(o.origin ? { origin: o.origin } : {}),
|
|
152
152
|
};
|
package/dist/core/episodes.d.ts
CHANGED
|
@@ -23,8 +23,10 @@ export declare const isConversation: (e: {
|
|
|
23
23
|
}) => boolean;
|
|
24
24
|
/** A record's body: what its key summarises, its chunks are cut from, and opening it shows — whole, never shortened. */
|
|
25
25
|
export declare function episodeBody(e: Pick<ReadableRequest, 'request' | 'reply' | 'files'> & Partial<Pick<ReadableRequest, 'said' | 'steps' | 'engine'>>): string;
|
|
26
|
-
/** For each day a request was made, the days a time said relatively that day means — yesterday,
|
|
27
|
-
* last
|
|
28
|
-
*
|
|
26
|
+
/** For each day a request was made, the days a time said relatively that day means — yesterday, the week before, the
|
|
27
|
+
* last weekend, each of the six days before by name, this week, last month, last year (UTC as the requests' times) —
|
|
28
|
+
* so the model reads "last week" or "on Friday" off a line instead of counting. "Last week" is the seven days before,
|
|
29
|
+
* as people mean it, not the Monday-to-Sunday week before this one: measured on LoCoMo, the calendar week put events
|
|
30
|
+
* said on a Sunday thirteen days back, a week early. */
|
|
29
31
|
export declare function calendarOf(tss: number[]): string;
|
|
30
32
|
export declare function renderForExtraction(e: ReadableRequest, n: number): string;
|
package/dist/core/episodes.js
CHANGED
|
@@ -17,19 +17,23 @@ export function episodeBody(e) {
|
|
|
17
17
|
parts.push(`Files: ${e.files.join(', ')}`);
|
|
18
18
|
return parts.join('\n');
|
|
19
19
|
}
|
|
20
|
-
/** For each day a request was made, the days a time said relatively that day means — yesterday,
|
|
21
|
-
* last
|
|
22
|
-
*
|
|
20
|
+
/** For each day a request was made, the days a time said relatively that day means — yesterday, the week before, the
|
|
21
|
+
* last weekend, each of the six days before by name, this week, last month, last year (UTC as the requests' times) —
|
|
22
|
+
* so the model reads "last week" or "on Friday" off a line instead of counting. "Last week" is the seven days before,
|
|
23
|
+
* as people mean it, not the Monday-to-Sunday week before this one: measured on LoCoMo, the calendar week put events
|
|
24
|
+
* said on a Sunday thirteen days back, a week early. */
|
|
23
25
|
export function calendarOf(tss) {
|
|
24
26
|
const DAY = 86_400_000;
|
|
25
27
|
const iso = (t) => new Date(t).toISOString().slice(0, 10);
|
|
26
28
|
const wd = (t) => new Date(t).toLocaleDateString('en-GB', { weekday: 'short', timeZone: 'UTC' });
|
|
27
29
|
const days = [...new Set(tss.filter((t) => t > 0).map((t) => Math.floor(t / DAY) * DAY))].sort((a, b) => a - b);
|
|
28
30
|
return days.map((d) => {
|
|
29
|
-
const monday = d - ((new Date(d).getUTCDay() + 6) % 7) * DAY;
|
|
30
31
|
const dt = new Date(d);
|
|
32
|
+
const monday = d - ((dt.getUTCDay() + 6) % 7) * DAY;
|
|
33
|
+
const sunday = d - (dt.getUTCDay() === 0 ? 7 : dt.getUTCDay()) * DAY;
|
|
31
34
|
const lastMonth = new Date(Date.UTC(dt.getUTCFullYear(), dt.getUTCMonth() - 1, 1)).toISOString().slice(0, 7);
|
|
32
|
-
|
|
35
|
+
const before = [2, 3, 4, 5, 6, 7].map((n) => `${wd(d - n * DAY)} ${iso(d - n * DAY)}`).join(', ');
|
|
36
|
+
return `- ${wd(d)} ${iso(d)}: yesterday ${wd(d - DAY)} ${iso(d - DAY)} · the days before ${before} · last week (the week before) ${iso(d - 7 * DAY)} to ${iso(d - DAY)} · last weekend ${iso(sunday - DAY)} to ${iso(sunday)} · this week ${iso(monday)} to ${iso(monday + 6 * DAY)} · last month ${lastMonth} · last year ${dt.getUTCFullYear() - 1}`;
|
|
33
37
|
}).join('\n');
|
|
34
38
|
}
|
|
35
39
|
export function renderForExtraction(e, n) {
|
package/dist/core/extract.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface Observation {
|
|
|
41
41
|
/** Episode uids it was told in. */
|
|
42
42
|
provenance: string[];
|
|
43
43
|
concepts: string[];
|
|
44
|
+
/** Questions it answers, in the asker's words (searched beside its text, quieter). */
|
|
45
|
+
cues: string[];
|
|
44
46
|
origin?: MemoryOrigin;
|
|
45
47
|
}
|
|
46
48
|
export interface KeptEpisode {
|
|
@@ -72,6 +74,7 @@ type RawObservation = {
|
|
|
72
74
|
phase?: string;
|
|
73
75
|
requests?: number[];
|
|
74
76
|
concepts?: unknown;
|
|
77
|
+
cues?: unknown;
|
|
75
78
|
};
|
|
76
79
|
/** The observations as memory keeps them: with text, from a request of this part, scrubbed; a date only when it is one. */
|
|
77
80
|
export declare function cleanObservations(raw: RawObservation[], uidOf: (n: number) => string | undefined): Observation[];
|
package/dist/core/extract.js
CHANGED
|
@@ -42,9 +42,9 @@ what they tell about themselves counts as much. For each:
|
|
|
42
42
|
- text: 10 to 60 words that stand alone — the event or fact itself, as told ("Melanie took her family camping in the
|
|
43
43
|
mountains"), never "On <date>, X said that …" (the date has its own field). Name who and what (names, never "he",
|
|
44
44
|
"she", "it", "they"); keep every proper noun, title, number and quoted phrase exactly. A time said relatively stays
|
|
45
|
-
as said, with the date it means, read off the Calendar line of the day it was said: "last week (the week
|
|
46
|
-
2023)", "yesterday (7 May 2023)". When the part names something only by description
|
|
47
|
-
home country", "the kids") and KNOWN FROM OTHER CONVERSATIONS names it, write the name too ("Becoming Nicole",
|
|
45
|
+
as said, with the date it means, read off the Calendar line of the day it was said: "last week (the week before 26 June
|
|
46
|
+
2023)", "yesterday (7 May 2023)", "on Friday (12 April 2024)". When the part names something only by description
|
|
47
|
+
("the book you recommended", "my home country", "the kids") and KNOWN FROM OTHER CONVERSATIONS names it, write the name too ("Becoming Nicole",
|
|
48
48
|
"Sweden", "her three children") — never a fact from KNOWN that this part does not tell. What the person or a speaker
|
|
49
49
|
says about their own life is true as told: no commentary on what the conversation did or did not confirm. A plan is
|
|
50
50
|
written as a plan ("Melanie planned to go swimming with the kids"). When it replaces something said before, say both
|
|
@@ -52,7 +52,8 @@ what they tell about themselves counts as much. For each:
|
|
|
52
52
|
- about: who or what it is about, named as the conversation names them ("the person" for the one whose requests these
|
|
53
53
|
are, unless the conversation names them).
|
|
54
54
|
- event_date: when it happened or applies, resolved with the Calendar line of the day its request was made ("yesterday"
|
|
55
|
-
is that line's yesterday; "last week" its
|
|
55
|
+
is that line's yesterday; "last week" the first day of its week before; "last weekend" its last weekend's Saturday;
|
|
56
|
+
"on Friday" the Friday among its days before; "last month" its last month; "in June" is 2023-06)
|
|
56
57
|
— YYYY-MM-DD, YYYY-MM or
|
|
57
58
|
YYYY, as precise as the conversation makes it; empty when no time is given or implied. A plan's event_date is when it
|
|
58
59
|
is planned for.
|
|
@@ -63,7 +64,10 @@ what they tell about themselves counts as much. For each:
|
|
|
63
64
|
implies no such time, and for a one-off event (its event_date says it).
|
|
64
65
|
- phase: done (it happened), plan (intended, not done yet), ongoing, or state (how things are).
|
|
65
66
|
- requests: the request numbers it comes from.
|
|
66
|
-
- concepts: up to 6 specific things it names — people, places, organisations, products, titles, files, services
|
|
67
|
+
- concepts: up to 6 specific things it names — people, places, organisations, products, titles, files, services.
|
|
68
|
+
- cues: 2 to 4 questions someone may ask later that this answers, in the words they would ask in rather than its own —
|
|
69
|
+
from the broad ("What instruments does Priya play?", "Who are Priya's siblings?") to the specific ("When did Priya
|
|
70
|
+
start the cello?"), in the language the conversation is in.`;
|
|
67
71
|
const SYSTEM = `You consolidate the long-term memory of a person who works with AI agents (coding, research, writing,
|
|
68
72
|
operations). You get part of one of their conversations: the person's requests (numbered, each with its date), the
|
|
69
73
|
agent's steps (tool calls, with errors) and what the agent said. Ask of everything you write: will a future agent act
|
|
@@ -184,12 +188,13 @@ const SCHEMA = {
|
|
|
184
188
|
observations: {
|
|
185
189
|
type: 'array',
|
|
186
190
|
items: {
|
|
187
|
-
type: 'object', additionalProperties: false, required: ['text', 'about', 'event_date', 'start_date', 'end_date', 'phase', 'requests', 'concepts'],
|
|
191
|
+
type: 'object', additionalProperties: false, required: ['text', 'about', 'event_date', 'start_date', 'end_date', 'phase', 'requests', 'concepts', 'cues'],
|
|
188
192
|
properties: {
|
|
189
193
|
text: { type: 'string' }, about: { type: 'string' }, event_date: { type: 'string' },
|
|
190
194
|
start_date: { type: 'string' }, end_date: { type: 'string' },
|
|
191
195
|
phase: { type: 'string', enum: [...PHASES] },
|
|
192
196
|
requests: { type: 'array', items: { type: 'integer' } }, concepts: { type: 'array', items: { type: 'string' } },
|
|
197
|
+
cues: { type: 'array', items: { type: 'string' } },
|
|
193
198
|
},
|
|
194
199
|
},
|
|
195
200
|
},
|
|
@@ -244,7 +249,7 @@ export function cleanObservations(raw, uidOf) {
|
|
|
244
249
|
text, about: redactString((x.about ?? '').trim()), eventAt: dateOf(x.event_date),
|
|
245
250
|
...(dateOf(x.start_date) ? { startAt: dateOf(x.start_date) } : {}), ...(dateOf(x.end_date) ? { endAt: dateOf(x.end_date) } : {}),
|
|
246
251
|
phase: PHASES.includes(String(x.phase)) ? x.phase : 'done',
|
|
247
|
-
provenance, concepts: cleanConcepts(x.concepts),
|
|
252
|
+
provenance, concepts: cleanConcepts(x.concepts), cues: cleanCues(x.cues).slice(0, 4),
|
|
248
253
|
});
|
|
249
254
|
}
|
|
250
255
|
return out;
|
|
@@ -46,6 +46,10 @@ const SMALL = { optimizers_config: { default_segment_number: 2 } };
|
|
|
46
46
|
* was the best share on both benchmarks, and CombSUM beat rank fusion at the top of both — LoCoMo requests any@3
|
|
47
47
|
* 71.6 → 74.5, nDCG@10 64.6 → 65.7; LongMemEval-S any@1 63.4 → 64.5, sessions any@1 92.6 → 93.2. */
|
|
48
48
|
const DENSE_SHARE = 0.7;
|
|
49
|
+
/** Cues — the questions an item answers, in the asker's words — search as their own channel at this share of the dense
|
|
50
|
+
* weight rather than among the item's points. Measured on LoCoMo's 1,307 held-out questions, together with cues on
|
|
51
|
+
* every observation and the seven-days-before calendar: multi-hop 50.6 → 55.2, overall 77.2 → 77.4 (docs/RESEARCH.md). */
|
|
52
|
+
const CUE_SHARE = 0.1;
|
|
49
53
|
/** A point's id from its item and its place (0 = the key, n = the n-th child): the same item writes the same ids. */
|
|
50
54
|
export function pointId(item, n) {
|
|
51
55
|
const h = createHash('sha1').update(`${item}#${n}`).digest('hex');
|
|
@@ -278,15 +282,19 @@ export class QdrantClient {
|
|
|
278
282
|
const channels = [];
|
|
279
283
|
const sparse = q.sparse?.indices.length ? q.sparse : null;
|
|
280
284
|
const denseWeight = (sparse ? DENSE_SHARE : 1) / Math.max(1, q.dense.length);
|
|
281
|
-
|
|
282
|
-
|
|
285
|
+
const cueOff = { ...(filter ?? {}), must_not: [...(filter?.must_not ?? []), { key: 'part', match: { value: 'cue' } }] };
|
|
286
|
+
const cueOnly = { ...(filter ?? {}), must: [...(filter?.must ?? []), { key: 'part', match: { value: 'cue' } }] };
|
|
287
|
+
for (const v of q.dense) {
|
|
288
|
+
channels.push({ query: v, using: 'dense', params: RESCORE, weight: denseWeight * (1 - CUE_SHARE), filter: cueOff });
|
|
289
|
+
channels.push({ query: v, using: 'dense', params: RESCORE, weight: denseWeight * CUE_SHARE, filter: cueOnly });
|
|
290
|
+
}
|
|
283
291
|
if (sparse)
|
|
284
292
|
channels.push({ query: sparse, using: 'bm25', weight: 1 - DENSE_SHARE });
|
|
285
293
|
if (!channels.length)
|
|
286
294
|
return [];
|
|
287
295
|
const lists = await Promise.all(channels.map((c) => this.call('POST', `/collections/${this.collection}/points/query/groups`, {
|
|
288
296
|
query: c.query, using: c.using, ...(c.params ? { params: c.params } : {}),
|
|
289
|
-
...(filter ? { filter } : {}),
|
|
297
|
+
...(c.filter ?? filter ? { filter: c.filter ?? filter } : {}),
|
|
290
298
|
group_by: 'item',
|
|
291
299
|
group_size: 1,
|
|
292
300
|
limit: per,
|
package/dist/memory.js
CHANGED
|
@@ -201,7 +201,7 @@ export async function openMemory(o) {
|
|
|
201
201
|
return sources.length ? { ...p, sources } : p;
|
|
202
202
|
},
|
|
203
203
|
explore: (s) => memoryExplore(deps, s),
|
|
204
|
-
gather: (s) => memoryGather(deps, s, { ask }),
|
|
204
|
+
gather: (s) => memoryGather(deps, s, { ask, ...(s.limit ? { limit: s.limit } : {}) }),
|
|
205
205
|
async consolidate() {
|
|
206
206
|
const worker = new Consolidator({ ...base, concurrency: o.concurrency ?? 4 });
|
|
207
207
|
const total = { windows: 0, added: 0, merged: 0, updated: 0, superseded: 0, linked: 0, dropped: 0, episodes: 0, observed: 0, cued: 0 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memtrails",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|