memtrails 0.3.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/CHANGELOG.md +56 -0
- package/README.md +25 -9
- package/dist/core/consolidate.d.ts +24 -9
- package/dist/core/consolidate.js +101 -49
- package/dist/core/episodes.d.ts +1 -1
- package/dist/core/episodes.js +2 -8
- package/dist/core/extract.d.ts +3 -0
- package/dist/core/extract.js +12 -5
- package/dist/core/gather.d.ts +3 -0
- package/dist/core/gather.js +16 -10
- package/dist/core/model.d.ts +9 -4
- package/dist/core/model.js +39 -40
- package/dist/core/profiles.d.ts +1 -1
- package/dist/core/profiles.js +12 -9
- package/dist/core/qdrant-binary.d.ts +52 -5
- package/dist/core/qdrant-binary.js +197 -48
- package/dist/core/qdrant-client.d.ts +6 -21
- package/dist/core/qdrant-client.js +36 -100
- package/dist/core/qdrant-process.d.ts +30 -2
- package/dist/core/qdrant-process.js +217 -27
- package/dist/core/rebuild.d.ts +5 -1
- package/dist/core/rebuild.js +23 -5
- package/dist/core/recall-log.d.ts +3 -0
- package/dist/core/recall-log.js +6 -1
- package/dist/core/recall.js +19 -8
- package/dist/core/redact.d.ts +8 -0
- package/dist/core/redact.js +48 -9
- package/dist/core/sleep.d.ts +4 -3
- package/dist/core/sleep.js +27 -16
- package/dist/core/store.d.ts +7 -1
- package/dist/core/store.js +101 -33
- package/dist/core/text.d.ts +6 -2
- package/dist/core/text.js +19 -6
- package/dist/core/words.js +6 -2
- package/dist/core/writer.d.ts +2 -1
- package/dist/core/writer.js +9 -3
- package/dist/importers/files.js +96 -15
- package/dist/importers/git.js +3 -1
- package/dist/importers/jsonl.d.ts +6 -0
- package/dist/importers/jsonl.js +10 -5
- package/dist/importers/lines.d.ts +13 -0
- package/dist/importers/lines.js +70 -0
- package/dist/importers/mbox.js +32 -12
- package/dist/importers/slack.js +5 -2
- package/dist/importers/text.js +85 -14
- package/dist/llm.d.ts +10 -8
- package/dist/llm.js +53 -7
- package/dist/memory.d.ts +7 -2
- package/dist/memory.js +284 -57
- package/dist/records.js +6 -3
- package/dist/tools/agent-tools.d.ts +2 -5
- package/dist/tools/agent-tools.js +37 -21
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,62 @@
|
|
|
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
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [0.4.1] — 2026-11-05
|
|
9
|
+
|
|
10
|
+
- Native Python (`pip install memtrails`, asyncio) and Rust (`cargo add memtrails`, tokio) versions, with the same
|
|
11
|
+
results and the same files as this library: held to shared answers (`conformance/`), and a memory folder written by
|
|
12
|
+
any of the three is searched the same by the others (`interop/`).
|
|
13
|
+
- Fixes: search no longer shows a short record's own summary again as the passage that matched (`match`, and after
|
|
14
|
+
" … " in `searchText`); a match is now only ever a passage beyond the start of what is shown.
|
|
15
|
+
- Fixes: the downloaded Qdrant binary and embedding model are only run or loaded from folders that are this user's
|
|
16
|
+
alone. The cache folder (and the model's folder) is made private when it is created or found open to others, and
|
|
17
|
+
refused with a clear error when it belongs to someone else or is a link. `fromFiles` no longer reads through a note
|
|
18
|
+
that was replaced by a link after the folder was listed, and `fromJsonl` and `fromMbox` skip a line longer than
|
|
19
|
+
64 MiB (with the reason) instead of holding it in memory.
|
|
20
|
+
- Fixes: a rebuild that is stopped (a close, a newer rebuild) returns only once the batches it was writing are written,
|
|
21
|
+
so none of them lands in the index after it.
|
|
22
|
+
- Fixes: `close()` while memory's own Qdrant is being restarted (after it died) stops that restart and waits for the
|
|
23
|
+
Qdrant it began; one could come up after the close and stay running.
|
|
24
|
+
- Fixes: an add no longer runs beside a rebuild (a record could be logged but never indexed, then skipped as unchanged
|
|
25
|
+
when added again); `close()` waits for the adds and searches in flight, and every call after it is refused with
|
|
26
|
+
"memory is closed".
|
|
27
|
+
- `LlmRequest.signal` (optional): aborted when no one will read the answer any more; `openAiAsk` stops its call and its
|
|
28
|
+
retries. Asks made side by side (consolidation's windows, cues and reconcile batches, gathering's parts, sleep's
|
|
29
|
+
relate batches, profiles) stop the others when one fails. Consolidation that fails or is stopped starts no further
|
|
30
|
+
extraction and returns only once the ones being read are over; before, they ran on, paid for, after it returned.
|
|
31
|
+
|
|
32
|
+
## [0.4.0] — 2026-11-04
|
|
33
|
+
|
|
34
|
+
- Prompt examples are invented now. The extraction and gathering prompts had a few example sentences taken from LoCoMo
|
|
35
|
+
conversations, one of them (in `gather`'s notes rule) from a held-out conversation. Their effect on the published
|
|
36
|
+
results, measured on the judged answers: leaving out every held-out question that mentions any of those words or
|
|
37
|
+
people (48 questions) gives 81.1 against Mem0's 76.6 (+111/−55), against 80.9 and 76.5 on all 1,307. The question
|
|
38
|
+
the example came from was answered wrong by both systems.
|
|
39
|
+
- The search index layout is 7: `kinds` now selects records by their kind on every point and learned items by theirs,
|
|
40
|
+
so an existing index is rebuilt from the logs on the next open.
|
|
41
|
+
- Fixes: a crash in the middle of a write no longer loses the records added after it; consolidation no longer
|
|
42
|
+
overwrites records changed or removed while the model answers; a title cut through an emoji no longer fails a write;
|
|
43
|
+
a thread that never goes quiet is consolidated; imported history is related and kept like new knowledge (`learnedAt`);
|
|
44
|
+
a malformed `from`/`to` is an error; `gather` and `sleep` report `paused` when the model did not read.
|
|
45
|
+
- `bench/audited.ts`: judged LoCoMo answers without the questions whose golden answer an independent audit
|
|
46
|
+
([locomo-audit](https://github.com/dial481/locomo-audit)) found wrong, dropped for every system alike. As shipped,
|
|
47
|
+
84.8 against Mem0's 79.9 (+109/−49).
|
|
48
|
+
- `gather`: a memory a small model keeps by quoting its whole line (`[m3] (2023-06-03) …`) now counts; it read as nothing
|
|
49
|
+
kept before.
|
|
50
|
+
|
|
51
|
+
- Security: the secret filter catches common key formats it missed (GITHUB_TOKEN=, AWS_SECRET_ACCESS_KEY=, JSON-style
|
|
52
|
+
secrets, GitLab, Hugging Face, npm, SendGrid, Telegram, the whole password of a URL); crafted e-mail and HTML can no
|
|
53
|
+
longer make the importers or the filter take quadratic time; downloads are verified on disk in a temp folder of their
|
|
54
|
+
own under a lock, with a stall timeout, and the Qdrant binary runs only while it is this user's alone; Qdrant gets a
|
|
55
|
+
minimal environment and must prove it is ours before it is given the key; an OpenAI key is sent to OpenAI only; the
|
|
56
|
+
memory folder is private; Slack channel folders cannot lead out of the export.
|
|
57
|
+
- One process at a time in a memory folder (a lock); consolidate, sleep, rebuild and remove run one at a time; `close()`
|
|
58
|
+
stops the running job and waits for it; an open that fails stops what it started; `remove()` takes the record's kept
|
|
59
|
+
episode out of the long-term log too; `MEMTRAILS_EMBED_URL` and `MEMTRAILS_RERANK_URL` are used.
|
|
60
|
+
- The release is built and tested in a job without publishing rights; actions are pinned to commits.
|
|
61
|
+
|
|
6
62
|
## [0.3.0] — 2026-11-02
|
|
7
63
|
|
|
8
64
|
- `gather(q, { notes: true })`: the same model call also writes what the kept memories say that answers the question,
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ import { openMemory } from 'memtrails';
|
|
|
17
17
|
const memory = await openMemory({ home: './memory' });
|
|
18
18
|
|
|
19
19
|
await memory.add([
|
|
20
|
-
{ id: 'msg-1', kind: 'email', author: 'Ana', title: '
|
|
20
|
+
{ id: 'msg-1', kind: 'email', author: 'Ana', title: 'Proxy certificate expired', at: '2026-04-02',
|
|
21
21
|
text: 'The TLS certificate expired on the 1st. Renewal stopped because the cron job was removed in March.' },
|
|
22
22
|
{ id: 'c9f1e2', kind: 'commit', author: 'Ion', title: 'Restore certificate renewal cron', at: '2026-04-03',
|
|
23
23
|
text: 'Files: deploy/cron/renew.sh' },
|
|
@@ -41,7 +41,8 @@ await memory.close();
|
|
|
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
43
|
CPU scores 77.4 (strict judge) against Mem0's 76.5 — a tie, reported as one; with `gather` reading what search
|
|
44
|
-
found and noting what it says, 80.9 — ahead beyond chance.
|
|
44
|
+
found and noting what it says, 80.9 — ahead beyond chance; 84.8 against 79.9 without the questions whose official
|
|
45
|
+
answer an independent audit found wrong. [Benchmarks](docs/BENCHMARKS.md).
|
|
45
46
|
- **Explorable.** A concept graph links what records share — people, places, systems, names — so an agent can walk
|
|
46
47
|
from what it found to what a search for the words does not find.
|
|
47
48
|
- **Built for agents.** `memory.tools()` gives `memory_search`, `memory_open` and `memory_explore` with zod and JSON
|
|
@@ -57,6 +58,17 @@ Node.js 22 or newer, on macOS or Linux (x64 or arm64). Windows has the pieces it
|
|
|
57
58
|
(218 MB, from Hugging Face) and the Qdrant binary (~30 MB, from Qdrant's GitHub release) into `~/.cache/memtrails`,
|
|
58
59
|
each checked against a pinned SHA-256. Set `MEMTRAILS_CACHE` to put them elsewhere.
|
|
59
60
|
|
|
61
|
+
### Python and Rust
|
|
62
|
+
|
|
63
|
+
The same library, written natively in each language, fully async, with the same results and the same files: a memory
|
|
64
|
+
folder written by one opens in the others (checked on every change by [`interop/`](https://github.com/cmaftuleac/memtrails/tree/main/interop) and held to the shared
|
|
65
|
+
answers in [`conformance/`](https://github.com/cmaftuleac/memtrails/tree/main/conformance)).
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install memtrails # Python 3.10–3.13, asyncio — see python/README.md
|
|
69
|
+
cargo add memtrails # Rust 1.88+, tokio — see rust/README.md
|
|
70
|
+
```
|
|
71
|
+
|
|
60
72
|
## Adding data
|
|
61
73
|
|
|
62
74
|
A **record** is anything with text. Only `id` and `text` are required.
|
|
@@ -67,9 +79,9 @@ await memory.add({
|
|
|
67
79
|
source: 'work-mailbox', // part of the identity
|
|
68
80
|
kind: 'email', // any word: email, message, note, memo, article, commit, ticket, …
|
|
69
81
|
collection: 'ops', // search can be scoped to collections
|
|
70
|
-
thread: '
|
|
82
|
+
thread: 'proxy-cert', // records that belong together
|
|
71
83
|
author: 'Ana Popescu',
|
|
72
|
-
title: '
|
|
84
|
+
title: 'Proxy certificate expired',
|
|
73
85
|
text: '…',
|
|
74
86
|
at: '2026-04-02T09:14:00Z', // ms, ISO string or Date
|
|
75
87
|
url: 'https://mail.example/…',
|
|
@@ -111,7 +123,7 @@ await memory.search({ query: 'who renewed the certificate?', limit: 8 });
|
|
|
111
123
|
await memory.search({ query: 'standup notes', collections: ['ops'], kinds: ['message'], from: '2026-04', to: '2026-04' });
|
|
112
124
|
|
|
113
125
|
// The same results as a reader is shown them: one line a memory, best first or oldest first.
|
|
114
|
-
await memory.searchText({ query: 'what happened with the
|
|
126
|
+
await memory.searchText({ query: 'what happened with the proxy?', order: 'time' });
|
|
115
127
|
|
|
116
128
|
// The best sentences of what was found, for a small context window.
|
|
117
129
|
await memory.room({ query: 'what made the renewal stop?', chars: 6000 });
|
|
@@ -120,7 +132,7 @@ await memory.room({ query: 'what made the renewal stop?', chars: 6000 });
|
|
|
120
132
|
await memory.open(hits[0].id);
|
|
121
133
|
|
|
122
134
|
// What the concept graph reaches from a topic or from items, that search did not find.
|
|
123
|
-
await memory.explore({ topic: 'the
|
|
135
|
+
await memory.explore({ topic: 'the proxy' });
|
|
124
136
|
```
|
|
125
137
|
|
|
126
138
|
With dates, what happened within them comes first and the rest follows: a question that names a time is rarely only
|
|
@@ -141,7 +153,7 @@ const memory = await openMemory({
|
|
|
141
153
|
});
|
|
142
154
|
await memory.consolidate(); // after adding
|
|
143
155
|
await memory.sleep(); // once a day
|
|
144
|
-
await memory.gather({ query: 'every
|
|
156
|
+
await memory.gather({ query: 'every plumber we called this year' }); // a set, for counting and listing questions
|
|
145
157
|
```
|
|
146
158
|
|
|
147
159
|
Any provider works: pass `llm: { baseUrl, model, apiKey }` for an OpenAI-compatible server (a hosted model, vLLM,
|
|
@@ -174,7 +186,7 @@ for (const t of tools) server.tool(t.name, t.description, t.inputSchema, t.handl
|
|
|
174
186
|
| Environment | |
|
|
175
187
|
|---|---|
|
|
176
188
|
| `MEMTRAILS_CACHE` | Where downloads live (default `~/.cache/memtrails`). |
|
|
177
|
-
| `MEMTRAILS_MODEL`, `MEMTRAILS_LLM_API_KEY` or `OPENAI_API_KEY`, `MEMTRAILS_LLM_BASE_URL` | The language model when `llm` is not given. |
|
|
189
|
+
| `MEMTRAILS_MODEL`, `MEMTRAILS_LLM_API_KEY` or `OPENAI_API_KEY`, `MEMTRAILS_LLM_BASE_URL` | The language model when `llm` is not given. `OPENAI_API_KEY` is sent only to OpenAI's own address. |
|
|
178
190
|
| `MEMTRAILS_EMBED_URL`, `MEMTRAILS_RERANK_URL` (+ `_KEY`, `_MODEL`) | Servers, when not given in options. |
|
|
179
191
|
|
|
180
192
|
## How it works
|
|
@@ -191,17 +203,21 @@ the [API reference](docs/API.md), and [what was tried and why](docs/RESEARCH.md)
|
|
|
191
203
|
|---|---|---|---|
|
|
192
204
|
| LoCoMo, all 1,307 held-out questions, 6,000 characters, strict judge, `gather` at 40 with notes | **80.9** | 76.5 | — |
|
|
193
205
|
| the same, Mem0's own lenient judge | **91.9** | 87.8 | — |
|
|
206
|
+
| the same, strict, without the 80 questions whose golden answer is wrong³ | **84.8** | 79.9 | — |
|
|
194
207
|
| the same, `gather` without notes, strict / lenient | 79.5 / 91.4 | 76.5 / 87.8 | — |
|
|
195
208
|
| the same with `search` alone, no model when searching, strict / lenient | 77.4 / 89.3 | 76.5 / 87.8 | — |
|
|
196
209
|
| LoCoMo, 1,307 held-out questions, 28,000 characters, one search¹ | 79.9 | 80.6 | 42.1 |
|
|
197
210
|
| LoCoMo, the same with an agent using each system's own tools¹ | **79.0** | 77.1 | 47.0 |
|
|
211
|
+
| LongMemEval-S, 60-question sample, 6,000 characters, strict judge, `gather` at 40 with notes (a tie) | 90.0 | 88.3 | — |
|
|
198
212
|
| LongMemEval-S retrieval, strict all sessions @5 (467)² | **96.15** | — | — |
|
|
199
213
|
|
|
200
214
|
The same reader and judges for every system. With `gather` (a model reads what search found, keeps what the answer
|
|
201
215
|
needs and notes what it says) memtrails leads Mem0 beyond chance: +113/−55 questions strict, +78/−25 lenient (both
|
|
202
216
|
p < 0.001). With `search` alone,
|
|
203
217
|
the small-room and one-search comparisons are ties; the agent lead is
|
|
204
|
-
small and was judged once. ¹ An earlier build (2026-10-27). ² gbrain-evals publishes 95.96 on the same count: a tie.
|
|
218
|
+
small and was judged once. ¹ An earlier build (2026-10-27). ² gbrain-evals publishes 95.96 on the same count: a tie. ³ As found by an
|
|
219
|
+
independent audit of LoCoMo ([locomo-audit](https://github.com/dial481/locomo-audit)), dropped for both systems;
|
|
220
|
+
+109/−49 (p < 0.001), `bench/audited.ts` reproduces it. [Full results, protocol and limitations](docs/BENCHMARKS.md).
|
|
205
221
|
|
|
206
222
|
## Limitations
|
|
207
223
|
|
|
@@ -7,7 +7,7 @@ export declare const SAME_EPISODE = 0.97;
|
|
|
7
7
|
* days are two observations, and a list question needs both. */
|
|
8
8
|
export declare const SAME_OBSERVATION = 0.97;
|
|
9
9
|
export declare const RELATED = 0.8;
|
|
10
|
-
/** A conversation is consolidated once it has been quiet this long — or its
|
|
10
|
+
/** A conversation is consolidated once it has been quiet this long — or its oldest waiting entry is a day old. */
|
|
11
11
|
export declare const SETTLED_MS: number;
|
|
12
12
|
/** Extraction calls in flight: they are independent, and one at a time made a backfill take days (~86 s a window). */
|
|
13
13
|
export declare const EXTRACT_CONCURRENCY = 8;
|
|
@@ -34,7 +34,7 @@ export interface ConsolidateDeps extends WriterDeps {
|
|
|
34
34
|
/** What a window's reading may keep, checked before anything is reconciled or written (e.g. facts only
|
|
35
35
|
* from the person's own words and results the agent confirmed, each with where it came from). An answer it cannot
|
|
36
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 | {
|
|
37
|
+
guard?: (win: StmEntry[], x: Extraction, signal?: AbortSignal) => Promise<Extraction | {
|
|
38
38
|
unavailable: string;
|
|
39
39
|
} | {
|
|
40
40
|
busy: true;
|
|
@@ -57,6 +57,12 @@ export interface ConsolidationReport {
|
|
|
57
57
|
* them), and how many go in one ask. */
|
|
58
58
|
export declare const CUE_BACKLOG = 40;
|
|
59
59
|
export type Relation = 'same' | 'updates' | 'contradicts' | 'related' | 'different';
|
|
60
|
+
export type Decision = {
|
|
61
|
+
relation: Relation;
|
|
62
|
+
title: string;
|
|
63
|
+
summary: string;
|
|
64
|
+
body: string;
|
|
65
|
+
};
|
|
60
66
|
/** Links an item keeps to others about the same subject (exploration walks them), newest last. */
|
|
61
67
|
export declare const MAX_LINKS = 24;
|
|
62
68
|
/** Pairs asked about together: at most 8, and at most RELATE_CHARS of their texts — a long pair on its own. */
|
|
@@ -65,6 +71,16 @@ export declare function relateBatches<T extends {
|
|
|
65
71
|
it: MemoryItem;
|
|
66
72
|
known: MemoryItem;
|
|
67
73
|
}>(pairs: T[]): T[][];
|
|
74
|
+
/** When an item was written to long-term memory. `ts` is its source's own time — for history imported today, long ago —
|
|
75
|
+
* so "learned since the last sleep" and "unused for half a year" read this. An item written before the field existed
|
|
76
|
+
* has only `ts`. */
|
|
77
|
+
export declare const learnedAt: (m: {
|
|
78
|
+
ts: number;
|
|
79
|
+
learnedAt?: number;
|
|
80
|
+
}) => number;
|
|
81
|
+
export declare const stamped: (it: MemoryItem, at: number) => MemoryItem;
|
|
82
|
+
/** The number of a label the model wrote back: `p3`, or the line it labels quoted whole (`[p3] NEW …`) — 0 for none. */
|
|
83
|
+
export declare function labelNo(id: string, letter: 'p' | 'i'): number;
|
|
68
84
|
export declare class Consolidator {
|
|
69
85
|
private readonly d;
|
|
70
86
|
private readonly vecs;
|
|
@@ -128,17 +144,16 @@ export declare class Consolidator {
|
|
|
128
144
|
summary: string;
|
|
129
145
|
body: string;
|
|
130
146
|
} | null): Promise<void>;
|
|
131
|
-
/** Two items about the same subject that say different things: each lists the other (memory_explore walks these)
|
|
147
|
+
/** Two items about the same subject that say different things: each lists the other (memory_explore walks these) —
|
|
148
|
+
* the newest MAX_LINKS, so an item on a busy subject does not grow its list, and its payload, without end. */
|
|
132
149
|
link(a: MemoryItem, b: MemoryItem): Promise<void>;
|
|
133
150
|
/** Superseded: out of the index and of recall, kept in the log with what replaced it. */
|
|
134
151
|
retire(known: MemoryItem, by: string): Promise<void>;
|
|
152
|
+
/** The model's decision for each pair, by its label (`p1`, …) — or why it did not answer. */
|
|
135
153
|
relate(batch: Array<{
|
|
136
154
|
it: MemoryItem;
|
|
137
155
|
known: MemoryItem;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
summary: string;
|
|
142
|
-
body: string;
|
|
143
|
-
}> | null>;
|
|
156
|
+
}>, signal?: AbortSignal): Promise<Map<string, Decision> | {
|
|
157
|
+
paused: string;
|
|
158
|
+
}>;
|
|
144
159
|
}
|
package/dist/core/consolidate.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Then the window's entries are marked done and leave the short-term index: what mattered of them is long-term now.
|
|
16
16
|
*/
|
|
17
17
|
import { redactString } from './redact.js';
|
|
18
|
-
import { defaultAsk } from '../llm.js';
|
|
18
|
+
import { allOrAbort, defaultAsk } from '../llm.js';
|
|
19
19
|
import { episodeBody } from './episodes.js';
|
|
20
20
|
import { docText } from './embedder.js';
|
|
21
21
|
import { cleanConcepts, cleanCues, extractWindow, observeWindow, windowsOf } from './extract.js';
|
|
@@ -29,7 +29,7 @@ export const SAME_EPISODE = 0.97;
|
|
|
29
29
|
* days are two observations, and a list question needs both. */
|
|
30
30
|
export const SAME_OBSERVATION = 0.97;
|
|
31
31
|
export const RELATED = 0.80;
|
|
32
|
-
/** A conversation is consolidated once it has been quiet this long — or its
|
|
32
|
+
/** A conversation is consolidated once it has been quiet this long — or its oldest waiting entry is a day old. */
|
|
33
33
|
export const SETTLED_MS = 60 * 60_000;
|
|
34
34
|
/** Extraction calls in flight: they are independent, and one at a time made a backfill take days (~86 s a window). */
|
|
35
35
|
export const EXTRACT_CONCURRENCY = 8;
|
|
@@ -116,32 +116,41 @@ export function relateBatches(pairs) {
|
|
|
116
116
|
out.push(cur);
|
|
117
117
|
return out;
|
|
118
118
|
}
|
|
119
|
+
/** When an item was written to long-term memory. `ts` is its source's own time — for history imported today, long ago —
|
|
120
|
+
* so "learned since the last sleep" and "unused for half a year" read this. An item written before the field existed
|
|
121
|
+
* has only `ts`. */
|
|
122
|
+
export const learnedAt = (m) => m.learnedAt ?? m.ts;
|
|
123
|
+
export const stamped = (it, at) => ({ ...it, learnedAt: at });
|
|
124
|
+
/** The number of a label the model wrote back: `p3`, or the line it labels quoted whole (`[p3] NEW …`) — 0 for none. */
|
|
125
|
+
export function labelNo(id, letter) {
|
|
126
|
+
return Number(new RegExp(`\\b${letter}(\\d+)`).exec(id)?.[1] ?? 0);
|
|
127
|
+
}
|
|
119
128
|
/** One remembered episode or candidate as a long-term item (not yet reconciled). */
|
|
120
129
|
function itemOf(c, src, now, sources = [src]) {
|
|
121
130
|
const project = c.scope === 'project' ? src.project : c.scope;
|
|
122
131
|
// The files its requests changed, from their recorded steps — by code, not the model: paths are what summaries lose.
|
|
123
132
|
const files = [...new Set(sources.flatMap((e) => e.files))];
|
|
124
|
-
return {
|
|
133
|
+
return stamped({
|
|
125
134
|
uid: `mem:${fingerprint(c.kind, c.scope, project, c.title, c.summary)}`, kind: c.kind, scope: c.scope, project: src.project,
|
|
126
135
|
engine: src.engine, session: src.session, cwd: src.cwd, where: src.title, ts: src.ts || now, firstTs: src.ts || now,
|
|
127
136
|
title: c.title, summary: c.summary, body: c.body, files, appliesWhen: c.appliesWhen, confidence: c.confidence,
|
|
128
137
|
importance: c.importance, status: 'active', provenance: c.provenance, seen: 1,
|
|
129
138
|
fingerprint: fingerprint(c.kind, c.title, c.summary, c.body), cues: c.cues, concepts: c.concepts,
|
|
130
139
|
...(c.origin ? { origin: c.origin } : {}),
|
|
131
|
-
};
|
|
140
|
+
}, now);
|
|
132
141
|
}
|
|
133
|
-
function episodeItem(e, key) {
|
|
134
|
-
return {
|
|
142
|
+
function episodeItem(e, key, now) {
|
|
143
|
+
return stamped({
|
|
135
144
|
uid: `ep:${e.uid}`, kind: 'episode', scope: 'project', project: e.project, engine: e.engine, session: e.session, cwd: e.cwd,
|
|
136
145
|
where: e.title, ts: e.ts, firstTs: e.ts, title: key.title || e.title, summary: key.summary, body: episodeBody(e), files: e.files,
|
|
137
146
|
appliesWhen: '', confidence: 'observed', importance: e.files.length ? 3 : 2, status: 'active', provenance: [e.uid], seen: 1,
|
|
138
147
|
fingerprint: fingerprint(e.request, e.reply, e.files), cues: key.cues ?? [], concepts: key.concepts ?? [],
|
|
139
148
|
...(key.outcome ? { outcome: key.outcome } : {}), ...(key.origin ? { origin: key.origin } : {}),
|
|
140
|
-
};
|
|
149
|
+
}, now);
|
|
141
150
|
}
|
|
142
151
|
/** An observation as a long-term item: its text is its key, its date beside when it was said. */
|
|
143
|
-
function observationItem(o, src) {
|
|
144
|
-
return {
|
|
152
|
+
function observationItem(o, src, now) {
|
|
153
|
+
return stamped({
|
|
145
154
|
uid: `obs:${fingerprint(o.text, o.eventAt, o.provenance[0] ?? src.uid)}`, kind: 'observation', scope: 'person', project: src.project,
|
|
146
155
|
engine: src.engine, session: src.session, cwd: src.cwd, where: src.title, ts: src.ts, firstTs: src.ts,
|
|
147
156
|
title: cut(o.text, 100), summary: o.text, body: '', files: [], appliesWhen: '', confidence: 'stated', importance: 2,
|
|
@@ -149,7 +158,7 @@ function observationItem(o, src) {
|
|
|
149
158
|
cues: o.cues ?? [], concepts: o.concepts, ...(o.eventAt ? { eventAt: o.eventAt } : {}),
|
|
150
159
|
...(o.startAt ? { startAt: o.startAt } : {}), ...(o.endAt ? { endAt: o.endAt } : {}), phase: o.phase, ...(o.about ? { about: o.about } : {}),
|
|
151
160
|
...(o.origin ? { origin: o.origin } : {}),
|
|
152
|
-
};
|
|
161
|
+
}, now);
|
|
153
162
|
}
|
|
154
163
|
function cosine(a, b) {
|
|
155
164
|
let ab = 0;
|
|
@@ -225,13 +234,16 @@ export class Consolidator {
|
|
|
225
234
|
for (const [uid, m] of this.d.stm.entries()) {
|
|
226
235
|
if (m.done || m.synthetic)
|
|
227
236
|
continue;
|
|
228
|
-
const s = by.get(m.session) ?? { uids: [], last: 0 };
|
|
237
|
+
const s = by.get(m.session) ?? { uids: [], first: Infinity, last: 0 };
|
|
229
238
|
s.uids.push(uid);
|
|
239
|
+
s.first = Math.min(s.first, m.ts);
|
|
230
240
|
s.last = Math.max(s.last, m.ts);
|
|
231
241
|
by.set(m.session, s);
|
|
232
242
|
}
|
|
233
|
-
|
|
234
|
-
|
|
243
|
+
// Quiet for an hour, or waiting a day since its oldest unread entry: a thread that never goes quiet (a message every
|
|
244
|
+
// half hour) is still read.
|
|
245
|
+
return [...by.entries()].filter(([, s]) => now - s.last >= SETTLED_MS || now - s.first >= DAY_MS)
|
|
246
|
+
.map(([session, { uids, last }]) => ({ session, uids, last }))
|
|
235
247
|
.sort((a, b) => b.last - a.last);
|
|
236
248
|
}
|
|
237
249
|
/** Consolidate up to `maxWindows` windows. Stops early when the model pauses or the worker is stopped.
|
|
@@ -250,6 +262,8 @@ export class Consolidator {
|
|
|
250
262
|
}
|
|
251
263
|
const limit = Math.max(1, this.d.concurrency ?? EXTRACT_CONCURRENCY);
|
|
252
264
|
const extracting = [];
|
|
265
|
+
/** Aborted once the lanes end — failed, paused, stopped or done: what is still being read is read for no one. */
|
|
266
|
+
const stop = new AbortController();
|
|
253
267
|
// A pool: the next window starts when ANY one is read, not when the oldest is — one slow answer (measured: an
|
|
254
268
|
// answer that ran on for 19,000 lines) held every other place idle while the windows after it waited their turn.
|
|
255
269
|
// They are still reconciled in order.
|
|
@@ -260,17 +274,18 @@ export class Consolidator {
|
|
|
260
274
|
while (!stopped && reading < limit && next < wins.length && !this.d.aborted()) {
|
|
261
275
|
const win = wins[next];
|
|
262
276
|
const head = win[0];
|
|
263
|
-
const p = this.read(win, { cwd: head.cwd, engine: head.engine, title: head.title });
|
|
277
|
+
const p = this.read(win, { cwd: head.cwd, engine: head.engine, title: head.title }, stop.signal);
|
|
264
278
|
reading++;
|
|
265
|
-
// One left behind by a pause or a stop is not an unhandled rejection; the awaited one still throws.
|
|
266
|
-
|
|
279
|
+
// One left behind by a pause or a stop is not an unhandled rejection; the awaited one still throws. One that
|
|
280
|
+
// throws starts no other: the run throws when it is reached, and no one would read their answers.
|
|
281
|
+
p.then(() => { reading--; launch(); }, () => { reading--; stopped = true; });
|
|
267
282
|
extracting[next++] = p;
|
|
268
283
|
}
|
|
269
284
|
};
|
|
270
285
|
launch();
|
|
271
286
|
const together = Math.max(1, Math.min(limit, this.d.reconcileConcurrency ?? 1));
|
|
272
287
|
/** Window i, once read: reconciled, observed, marked done — or 'stop' when the run pauses. */
|
|
273
|
-
const settle = async (i) => {
|
|
288
|
+
const settle = async (i, signal) => {
|
|
274
289
|
const x = await extracting[i].catch((err) => { stopped = true; throw err; });
|
|
275
290
|
const win = wins[i];
|
|
276
291
|
const head = win[0];
|
|
@@ -304,17 +319,23 @@ export class Consolidator {
|
|
|
304
319
|
const learned = new Set(x.candidates.flatMap((c) => c.provenance));
|
|
305
320
|
const kept = x.episodes.filter((k) => byUid.has(k.uid) && !learned.has(k.uid));
|
|
306
321
|
const fresh = [
|
|
307
|
-
...kept.map((k) => episodeItem(byUid.get(k.uid), k)),
|
|
322
|
+
...kept.map((k) => episodeItem(byUid.get(k.uid), k, now)),
|
|
308
323
|
...x.candidates.map((c) => itemOf(c, byUid.get(c.provenance.at(-1) ?? '') ?? head, now, c.provenance.flatMap((u) => byUid.get(u) ?? []))),
|
|
309
324
|
];
|
|
310
|
-
await this.reconcile(fresh, r);
|
|
311
|
-
await this.observe(x.observations.map((o) => observationItem(o, byUid.get(o.provenance.at(-1) ?? '') ?? head)), r);
|
|
325
|
+
await this.reconcile(fresh, r, signal);
|
|
326
|
+
await this.observe(x.observations.map((o) => observationItem(o, byUid.get(o.provenance.at(-1) ?? '') ?? head, now)), r);
|
|
312
327
|
r.dropped += x.dropped;
|
|
313
328
|
r.episodes += kept.length;
|
|
314
329
|
// Read: marked done in the queue. The requests themselves stay searchable — the raw record is the truth and the
|
|
315
330
|
// memories learned from them are keys to it; dropping them lost every fact the extraction did not keep (measured on
|
|
316
|
-
// LongMemEval: 47 of 60 misses were facts said in passing inside a request).
|
|
317
|
-
|
|
331
|
+
// LongMemEval: 47 of 60 misses were facts said in passing inside a request). Each as it is now, not as it was read
|
|
332
|
+
// before the model's long answer: one removed meanwhile stays removed, one changed meanwhile is read again.
|
|
333
|
+
this.d.stm.put(win.flatMap((e) => {
|
|
334
|
+
if (this.d.stm.meta(e.uid)?.fp !== e.fingerprint)
|
|
335
|
+
return [];
|
|
336
|
+
const cur = this.d.stm.get(e.uid);
|
|
337
|
+
return cur ? [{ ...cur, done: true }] : [];
|
|
338
|
+
}));
|
|
318
339
|
r.windows++;
|
|
319
340
|
// One at a time, each window's keys are dropped after it; windows at once share the cache until the run ends.
|
|
320
341
|
if (together === 1)
|
|
@@ -323,19 +344,30 @@ export class Consolidator {
|
|
|
323
344
|
};
|
|
324
345
|
let at = 0;
|
|
325
346
|
let halt = false;
|
|
326
|
-
const lane = async () => {
|
|
327
|
-
while (!halt && at < wins.length && extracting[at] && !this.d.aborted()) {
|
|
347
|
+
const lane = async (signal) => {
|
|
348
|
+
while (!halt && at < wins.length && extracting[at] && !this.d.aborted() && !signal.aborted) {
|
|
349
|
+
// No window is started after a failure: the run throws, and no one would read its answer.
|
|
328
350
|
try {
|
|
329
|
-
if (await settle(at
|
|
351
|
+
if (await settle(at++, signal) === 'stop')
|
|
330
352
|
halt = true;
|
|
331
353
|
}
|
|
332
354
|
catch (err) {
|
|
333
355
|
halt = true;
|
|
356
|
+
stopped = true;
|
|
334
357
|
throw err;
|
|
335
358
|
}
|
|
336
359
|
}
|
|
337
360
|
};
|
|
338
|
-
|
|
361
|
+
try {
|
|
362
|
+
await allOrAbort(Array.from({ length: together }, () => lane), stop.signal);
|
|
363
|
+
}
|
|
364
|
+
finally {
|
|
365
|
+
// What is still being read once the lanes end is aborted, and over before the run goes on: no paid ask outlives
|
|
366
|
+
// it, and none starts after it (`stopped`).
|
|
367
|
+
stopped = true;
|
|
368
|
+
stop.abort();
|
|
369
|
+
await Promise.allSettled(extracting.filter((p) => p !== undefined));
|
|
370
|
+
}
|
|
339
371
|
if (halt && r.paused !== undefined)
|
|
340
372
|
return r;
|
|
341
373
|
this.vecs.clear();
|
|
@@ -350,22 +382,27 @@ export class Consolidator {
|
|
|
350
382
|
return r;
|
|
351
383
|
}
|
|
352
384
|
/** One window's reading, and what the guard (when there is one) lets it keep. */
|
|
353
|
-
async read(win, meta) {
|
|
354
|
-
const x = await this.extract(win, meta);
|
|
355
|
-
|
|
385
|
+
async read(win, meta, signal) {
|
|
386
|
+
const x = await this.extract(win, meta, signal);
|
|
387
|
+
if (!this.d.guard || !('candidates' in x))
|
|
388
|
+
return x;
|
|
389
|
+
// Read for no one once the run stopped: the guard (it may ask a model too) is not asked.
|
|
390
|
+
if (signal?.aborted)
|
|
391
|
+
return { unavailable: 'the run stopped' };
|
|
392
|
+
return this.d.guard(win, x, signal);
|
|
356
393
|
}
|
|
357
394
|
/** One window's extraction: the one ask — and, when observations are read apart, the window's small windows too, each
|
|
358
395
|
* with the one before it as context. Any part not answered leaves the whole window for the next run. */
|
|
359
|
-
async extract(win, meta) {
|
|
396
|
+
async extract(win, meta, signal) {
|
|
360
397
|
const chars = this.d.observeChars;
|
|
361
398
|
const known = this.d.resolve ? await this.knownFor(win) : undefined;
|
|
362
399
|
if (!chars)
|
|
363
|
-
return extractWindow(win, meta, { ask: this.d.ask, known });
|
|
400
|
+
return extractWindow(win, meta, { ask: this.d.ask, known, ...(signal ? { signal } : {}) });
|
|
364
401
|
const subs = windowsOf(win, chars);
|
|
365
|
-
const [x, ...seen] = await
|
|
366
|
-
extractWindow(win, meta, { ask: this.d.ask, observations: false, known }),
|
|
367
|
-
...subs.map((sub, k) => observeWindow(sub, k ? subs[k - 1] : [], meta, { ask: this.d.ask, chars, known })),
|
|
368
|
-
]);
|
|
402
|
+
const [x, ...seen] = await allOrAbort([
|
|
403
|
+
(s) => extractWindow(win, meta, { ask: this.d.ask, observations: false, known, signal: s }),
|
|
404
|
+
...subs.map((sub, k) => (s) => observeWindow(sub, k ? subs[k - 1] : [], meta, { ask: this.d.ask, chars, known, signal: s })),
|
|
405
|
+
], signal);
|
|
369
406
|
if (!('candidates' in x))
|
|
370
407
|
return x;
|
|
371
408
|
const failed = seen.find((o) => !('observations' in o));
|
|
@@ -427,7 +464,8 @@ export class Consolidator {
|
|
|
427
464
|
for (let i = 0; i < batches.length; i += limit) {
|
|
428
465
|
if (this.d.aborted())
|
|
429
466
|
break;
|
|
430
|
-
|
|
467
|
+
// One that throws stops the others (allOrAbort).
|
|
468
|
+
const answers = await allOrAbort(batches.slice(i, i + limit).map((b) => (signal) => this.cueBatch(b, signal)));
|
|
431
469
|
const paused = answers.find((a) => 'paused' in a);
|
|
432
470
|
const got = answers.flatMap((a) => ('items' in a ? a.items : []));
|
|
433
471
|
if (got.length) {
|
|
@@ -443,11 +481,12 @@ export class Consolidator {
|
|
|
443
481
|
}
|
|
444
482
|
return { cued };
|
|
445
483
|
}
|
|
446
|
-
async cueBatch(items) {
|
|
484
|
+
async cueBatch(items, signal) {
|
|
447
485
|
const prompt = items.map((it, n) => `[i${n + 1}] (${it.kind}) ${it.title}\n${it.summary}${it.appliesWhen ? `\nNeeded when: ${it.appliesWhen}` : ''}`).join('\n\n');
|
|
448
486
|
const a = await (this.d.ask ?? defaultAsk)({
|
|
449
487
|
purpose: 'memory', system: CUE_SYSTEM, prompt: redactString(prompt), schema: CUE_SCHEMA,
|
|
450
488
|
name: 'memory_cues', maxTokens: 550 * items.length + 200, timeoutMs: 120_000, consequence: 'so these memories wait for their cues',
|
|
489
|
+
...(signal ? { signal } : {}),
|
|
451
490
|
});
|
|
452
491
|
if ('unavailable' in a)
|
|
453
492
|
return { paused: a.unavailable };
|
|
@@ -455,7 +494,7 @@ export class Consolidator {
|
|
|
455
494
|
return { paused: 'the fast model is busy' };
|
|
456
495
|
const out = [];
|
|
457
496
|
for (const x of a.answer.items ?? []) {
|
|
458
|
-
const it = typeof x.id === 'string' ? items[
|
|
497
|
+
const it = typeof x.id === 'string' ? items[labelNo(x.id, 'i') - 1] : undefined;
|
|
459
498
|
const cues = it?.cues?.length ? it.cues : cleanCues(x.cues);
|
|
460
499
|
const concepts = it?.concepts?.length ? it.concepts : cleanConcepts(x.concepts);
|
|
461
500
|
if (it && (cues.length || concepts.length))
|
|
@@ -464,7 +503,7 @@ export class Consolidator {
|
|
|
464
503
|
return { items: out };
|
|
465
504
|
}
|
|
466
505
|
/** Each item against what long-term memory holds — written as it goes, so a later item sees an earlier one. */
|
|
467
|
-
async reconcile(items, r) {
|
|
506
|
+
async reconcile(items, r, signal) {
|
|
468
507
|
const ask = [];
|
|
469
508
|
// Every key of the window in one embedding call (the cache serves each below and again when it is written).
|
|
470
509
|
await this.embed(items.map((it) => docText(it.title, it.summary)), embedBudget(items.length));
|
|
@@ -495,10 +534,14 @@ export class Consolidator {
|
|
|
495
534
|
}
|
|
496
535
|
// The pairs are fixed before any is asked about: every batch asked at once, the answers applied in order.
|
|
497
536
|
const batches = relateBatches(ask);
|
|
498
|
-
|
|
537
|
+
// One that throws stops the others (allOrAbort); a run stopped meanwhile (a lane beside this one failed) applies none.
|
|
538
|
+
const answers = await allOrAbort(batches.map((b) => (s) => this.relate(b, s)), signal);
|
|
539
|
+
if (signal?.aborted)
|
|
540
|
+
throw new Error('consolidation stopped');
|
|
499
541
|
for (const [b, batch] of batches.entries()) {
|
|
542
|
+
const got = answers[b];
|
|
500
543
|
for (const [n, { it, known }] of batch.entries()) {
|
|
501
|
-
const d =
|
|
544
|
+
const d = got instanceof Map ? got.get(`p${n + 1}`) : undefined;
|
|
502
545
|
if (!d || d.relation === 'different') {
|
|
503
546
|
await this.add(it);
|
|
504
547
|
r.added++;
|
|
@@ -605,16 +648,18 @@ export class Consolidator {
|
|
|
605
648
|
files: [...new Set([...known.files, ...it.files])],
|
|
606
649
|
confidence: known.confidence === 'verified' || it.confidence === 'verified' ? 'verified' : known.confidence,
|
|
607
650
|
cues: [...new Set([...(known.cues ?? []), ...(it.cues ?? [])])],
|
|
608
|
-
related: [...new Set([...(known.related ?? []), ...(it.related ?? [])])].filter((u) => u !== known.uid),
|
|
651
|
+
related: [...new Set([...(known.related ?? []), ...(it.related ?? [])])].filter((u) => u !== known.uid).slice(-MAX_LINKS),
|
|
609
652
|
concepts: [...new Map([...(known.concepts ?? []), ...(it.concepts ?? [])].map((c) => [conceptKey(c), c])).values()],
|
|
610
653
|
absorbed: [...new Set([...(known.absorbed ?? []), ...kept.map((k) => k.uid)])],
|
|
611
654
|
};
|
|
612
655
|
merged.fingerprint = fingerprint(merged.kind, merged.title, merged.summary, merged.body);
|
|
656
|
+
// Changed now: what sleep relates next.
|
|
657
|
+
merged.learnedAt = Math.max(learnedAt(known), learnedAt(it));
|
|
613
658
|
if (text)
|
|
614
659
|
await writeLtm(this.writer, [merged]);
|
|
615
660
|
else {
|
|
616
661
|
await this.d.client.setPayload([known.uid], { ts: merged.ts, importance: merged.importance });
|
|
617
|
-
if (
|
|
662
|
+
if (merged.related?.join('\n') !== (known.related ?? []).join('\n'))
|
|
618
663
|
await this.d.client.setPayload([known.uid], { related: merged.related }, true);
|
|
619
664
|
if ((merged.concepts?.length ?? 0) > (known.concepts?.length ?? 0))
|
|
620
665
|
await writeConcepts(this.writer, [merged]);
|
|
@@ -624,9 +669,10 @@ export class Consolidator {
|
|
|
624
669
|
}
|
|
625
670
|
this.d.ltm.put([...kept, merged]);
|
|
626
671
|
}
|
|
627
|
-
/** Two items about the same subject that say different things: each lists the other (memory_explore walks these)
|
|
672
|
+
/** Two items about the same subject that say different things: each lists the other (memory_explore walks these) —
|
|
673
|
+
* the newest MAX_LINKS, so an item on a busy subject does not grow its list, and its payload, without end. */
|
|
628
674
|
async link(a, b) {
|
|
629
|
-
const add = (x, y) => ({ ...x, related: [...
|
|
675
|
+
const add = (x, y) => ({ ...x, related: [...(x.related ?? []).filter((u) => u !== y.uid), y.uid].slice(-MAX_LINKS) });
|
|
630
676
|
const [one, two] = [add(a, b), add(b, a)];
|
|
631
677
|
this.d.ltm.put([one, two]);
|
|
632
678
|
await this.d.client.setPayload([one.uid], { related: one.related }, true);
|
|
@@ -638,7 +684,8 @@ export class Consolidator {
|
|
|
638
684
|
await writeLtm(this.writer, [old]);
|
|
639
685
|
this.d.ltm.put([old]);
|
|
640
686
|
}
|
|
641
|
-
|
|
687
|
+
/** The model's decision for each pair, by its label (`p1`, …) — or why it did not answer. */
|
|
688
|
+
async relate(batch, signal) {
|
|
642
689
|
// Whole texts: a merge the model writes from a shortened body loses the rest of it.
|
|
643
690
|
const prompt = batch.map(({ it, known }, n) => `[p${n + 1}]\nNEW (${it.kind}): ${it.title}\n${it.summary}\n${it.body}\n\nKNOWN (${known.kind}): ${known.title}\n${known.summary}\n${known.body}`).join('\n\n');
|
|
644
691
|
// Room for every merged text in full (≈ 2.5 characters a token, the two sides together), with the decisions.
|
|
@@ -646,14 +693,19 @@ export class Consolidator {
|
|
|
646
693
|
const a = await (this.d.ask ?? defaultAsk)({
|
|
647
694
|
purpose: 'memory', system: RELATE_SYSTEM, prompt: redactString(prompt), schema: RELATE_SCHEMA,
|
|
648
695
|
name: 'memory_reconcile', maxTokens: 900 * batch.length + 200 + room, timeoutMs: 300_000, consequence: 'so these memories are kept apart for now',
|
|
696
|
+
...(signal ? { signal } : {}),
|
|
649
697
|
});
|
|
698
|
+
if ('unavailable' in a)
|
|
699
|
+
return { paused: a.unavailable };
|
|
650
700
|
if (!('answer' in a))
|
|
651
|
-
return
|
|
701
|
+
return { paused: 'the fast model is busy' };
|
|
652
702
|
const out = new Map();
|
|
653
703
|
for (const p of a.answer.pairs ?? []) {
|
|
654
|
-
|
|
704
|
+
// A small model may quote the label's line rather than the label: the number is looked for.
|
|
705
|
+
const n = typeof p.id === 'string' ? labelNo(p.id, 'p') : 0;
|
|
706
|
+
if (!n || !RELATIONS.includes(String(p.relation)))
|
|
655
707
|
continue;
|
|
656
|
-
out.set(p
|
|
708
|
+
out.set(`p${n}`, { relation: p.relation, title: (p.title ?? '').trim(), summary: (p.summary ?? '').trim(), body: (p.body ?? '').trim() });
|
|
657
709
|
}
|
|
658
710
|
return out;
|
|
659
711
|
}
|
package/dist/core/episodes.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export declare const isConversation: (e: {
|
|
|
22
22
|
engine?: string;
|
|
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
|
-
export declare function episodeBody(e: Pick<ReadableRequest, 'request' | 'reply' | 'files'> & Partial<Pick<ReadableRequest, 'said' | '
|
|
25
|
+
export declare function episodeBody(e: Pick<ReadableRequest, 'request' | 'reply' | 'files'> & Partial<Pick<ReadableRequest, 'said' | 'engine'>>): string;
|
|
26
26
|
/** For each day a request was made, the days a time said relatively that day means — yesterday, the week before, the
|
|
27
27
|
* last weekend, each of the six days before by name, this week, last month, last year (UTC as the requests' times) —
|
|
28
28
|
* so the model reads "last week" or "on Friday" off a line instead of counting. "Last week" is the seven days before,
|
package/dist/core/episodes.js
CHANGED
|
@@ -9,10 +9,7 @@ export const isConversation = (e) => e.engine === 'conversation';
|
|
|
9
9
|
export function episodeBody(e) {
|
|
10
10
|
if (e.engine !== undefined && !isConversation(e))
|
|
11
11
|
return e.reply.trim() ? `${e.request}\n${e.reply}` : e.request;
|
|
12
|
-
const parts = [`Asked: ${e.request}`];
|
|
13
|
-
if (e.steps?.length)
|
|
14
|
-
parts.push(`Steps:\n${e.steps.join('\n')}`);
|
|
15
|
-
parts.push(`Done: ${e.said?.trim() ? e.said : e.reply}`);
|
|
12
|
+
const parts = [`Asked: ${e.request}`, `Done: ${e.said?.trim() ? e.said : e.reply}`];
|
|
16
13
|
if (e.files.length)
|
|
17
14
|
parts.push(`Files: ${e.files.join(', ')}`);
|
|
18
15
|
return parts.join('\n');
|
|
@@ -41,10 +38,7 @@ export function renderForExtraction(e, n) {
|
|
|
41
38
|
// Whole: a record too long for one window is read in parts and its parts joined (extract.ts), never shortened.
|
|
42
39
|
if (e.engine !== undefined && !isConversation(e))
|
|
43
40
|
return [`## Record ${n} · ${when} · ${e.engine}`, e.request, e.reply].filter(Boolean).join('\n');
|
|
44
|
-
const
|
|
45
|
-
const parts = [`## Request ${n} · ${when} · ${who}`, e.request];
|
|
46
|
-
if (e.steps.length)
|
|
47
|
-
parts.push(`Steps:\n${e.steps.join('\n')}`);
|
|
41
|
+
const parts = [`## Request ${n} · ${when} · typed by the person`, e.request];
|
|
48
42
|
if (e.said)
|
|
49
43
|
parts.push(`Agent said:\n${e.said}`);
|
|
50
44
|
if (e.files.length)
|