memhtml 0.1.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/LICENSE +201 -0
- package/README.md +531 -0
- package/agent/agent.ts +68 -0
- package/agent/channels/eve.ts +73 -0
- package/agent/instructions.md +142 -0
- package/agent/sandbox/sandbox.ts +102 -0
- package/dist/dist-Bubu4ZZa.mjs +3 -0
- package/dist/dist-CrYVXFO2.mjs +12846 -0
- package/dist/dist-CrYVXFO2.mjs.map +1 -0
- package/dist/dist-DUuomISL.mjs +2221 -0
- package/dist/dist-DUuomISL.mjs.map +1 -0
- package/dist/memhtml-mcp.mjs +4077 -0
- package/dist/memhtml-mcp.mjs.map +1 -0
- package/dist/memhtml.mjs +5009 -0
- package/dist/memhtml.mjs.map +1 -0
- package/guest/corpus.mjs +193 -0
- package/migrations/.gitkeep +0 -0
- package/migrations/0001_files.sql +111 -0
- package/migrations/0002_chunks.sql +31 -0
- package/migrations/0003_fts.sql +40 -0
- package/migrations/0004_edges.sql +40 -0
- package/migrations/0005_traces.sql +92 -0
- package/migrations/0006_sleep.sql +33 -0
- package/migrations/0007_watermark.sql +32 -0
- package/migrations/0008_tasks.sql +214 -0
- package/migrations/0009_frame_key.sql +54 -0
- package/migrations/0010_trace_consolidations.sql +45 -0
- package/package.json +59 -0
- package/src/agent-build.ts +280 -0
- package/src/client.ts +1155 -0
- package/src/contract.ts +443 -0
- package/src/index.ts +23 -0
- package/src/mount.ts +279 -0
- package/src/run-auth.ts +231 -0
- package/state-migrations/S0001_access.sql +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
# memhtml
|
|
2
|
+
|
|
3
|
+
[](https://github.com/memhtml/memhtml/actions/workflows/check.yml)
|
|
4
|
+
[](https://github.com/memhtml/memhtml/actions/workflows/security.yml)
|
|
5
|
+
[](https://scorecard.dev/viewer/?uri=github.com/memhtml/memhtml)
|
|
6
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
7
|
+
|
|
8
|
+
memhtml stores an agent's long-term memory as a git repository of semantic HTML5 files, one fact per
|
|
9
|
+
file. A rebuildable SQLite index sits over that tree, retrieval fuses four ranking arms, and a nightly
|
|
10
|
+
curation pipeline commits its work to a branch a human reviews before it lands.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
One package carries the whole system — the CLI, code mode, the sleep cycle, the trace indexer, and the
|
|
15
|
+
MCP server — and installs two binaries, `memhtml` and `memhtml-mcp`. Node 24 or newer.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm i -g memhtml # or: pnpm add -g memhtml, bun add -g memhtml
|
|
19
|
+
npx memhtml manifest # every command, flag, and error code, without installing anything
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Point it at a corpus with `MEMHTML_ROOT` (default `~/memhtml`), and at your transcripts with
|
|
23
|
+
`MEMHTML_TRACE_ROOT` (default `~/.claude`). Reading and writing memories needs no credentials;
|
|
24
|
+
embeddings and the sleep cycle's model calls use Bedrock through the default AWS credential chain, and
|
|
25
|
+
`MEMHTML_EMBED=off` / `MEMHTML_LLM=off` turn both off.
|
|
26
|
+
|
|
27
|
+
To register the MCP server with a client, the command is `memhtml-mcp` over stdio.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
memhtml init # scaffold $MEMHTML_ROOT: git init, PARA dirs, merge driver
|
|
31
|
+
memhtml write --title "WAL admits one writer and many readers" --type semantic \
|
|
32
|
+
--claim "A CLI command and a running memhtml serve mcp share one index.db."
|
|
33
|
+
memhtml search "one writer many readers" # FTS + vector + recency + salience, fused with RRF
|
|
34
|
+
memhtml serve mcp # the same store over stdio: 14 tools, 2 resources
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`memhtml manifest` (or a bare `memhtml`) answers with every command, flag, response type, and error code
|
|
38
|
+
the binary accepts, and it answers on a machine with no repo, no database, and no credentials. Every
|
|
39
|
+
command writes exactly one JSON envelope to stdout, logs go to stderr, and the exit code is 0 for
|
|
40
|
+
success, 2 for a usage error, 1 for a runtime failure. `AGENTS.md` is generated from the same table that
|
|
41
|
+
drives parsing, so the doc cannot drift from the binary.
|
|
42
|
+
|
|
43
|
+
## The design in three sentences
|
|
44
|
+
|
|
45
|
+
The git tree is the system of record, and `.memhtml/index.db` is a projection of it that can be deleted
|
|
46
|
+
and rebuilt without loss. Anything that must survive `rm index.db` lives in the files, where authored
|
|
47
|
+
links are `<link>` elements and metadata is `<meta>` elements, while re-derivable artifacts such as
|
|
48
|
+
embeddings and mined edges live only in the index. Every removal is a `git mv` into `archive/<YYYY>/`
|
|
49
|
+
that mirrors the original path, so the file stays in the tree and `git log --follow` reads straight
|
|
50
|
+
through a memory's whole life.
|
|
51
|
+
|
|
52
|
+
Figure 1 draws that. It is built from monospace box characters, which a screen reader reads as noise, so
|
|
53
|
+
skip to the paragraph below the figure: it says the same thing in words.
|
|
54
|
+
|
|
55
|
+
<!-- figure:system-topology -->
|
|
56
|
+
```text
|
|
57
|
+
+--------+ +---------------+ +----------------+
|
|
58
|
+
|the CLI | |the MCP server | |your file tools |
|
|
59
|
+
| | | | | |
|
|
60
|
+
+--------+ +---------------+ +----------------+
|
|
61
|
+
| | |
|
|
62
|
+
| commit |
|
|
63
|
+
| | |
|
|
64
|
+
+--commit---+ | +----yours----+
|
|
65
|
+
| | |
|
|
66
|
+
v v v
|
|
67
|
+
+-------------+
|
|
68
|
+
|the git tree |
|
|
69
|
+
| |
|
|
70
|
+
+-------------+
|
|
71
|
+
| |
|
|
72
|
+
+--------+ |
|
|
73
|
+
| |
|
|
74
|
+
git mv indexer
|
|
75
|
+
| |
|
|
76
|
+
v | .------.
|
|
77
|
+
+--------------+ v |\-____-/|
|
|
78
|
+
|archive/YYYY/ | +---------+ | |
|
|
79
|
+
| | |index.db | | |
|
|
80
|
+
+--------------+ | | |state.db|
|
|
81
|
+
+---------+ | |
|
|
82
|
+
| \-____-/
|
|
83
|
+
|
|
|
84
|
+
| |
|
|
85
|
+
3 arms |
|
|
86
|
+
| |
|
|
87
|
+
| salience
|
|
88
|
+
| |
|
|
89
|
+
+------+ |
|
|
90
|
+
| |
|
|
91
|
+
v v
|
|
92
|
+
+-------------+
|
|
93
|
+
|RRF then MMR |
|
|
94
|
+
| |
|
|
95
|
+
+-------------+
|
|
96
|
+
|
|
|
97
|
+
v
|
|
98
|
+
+------------+
|
|
99
|
+
|ranked hits |
|
|
100
|
+
| |
|
|
101
|
+
+------------+
|
|
102
|
+
```
|
|
103
|
+
<!-- /figure:system-topology -->
|
|
104
|
+
|
|
105
|
+
**Figure 1: every write door lands in the git tree, and every read is served from projections of it.**
|
|
106
|
+
Three doors reach in from outside: `memhtml write` and `memhtml apply`, the MCP server's 14 tools, and
|
|
107
|
+
your own file tools. All three commit into one git tree, and they differ only in who owns the commit.
|
|
108
|
+
Eviction moves a file to `archive/YYYY/` and leaves it in the tree. From the tree, a git-driven indexer
|
|
109
|
+
derives `index.db`, which supplies three of the four ranking arms. The fourth, salience, comes from
|
|
110
|
+
`state.db`, the one plane git cannot reproduce. A query enters the ranker at the bottom, RRF fuses the
|
|
111
|
+
four arms, MMR diversifies the result, and ranked hits come out. On the docs site the same drawing puts
|
|
112
|
+
into its borders what this caption has to spell out: a heavy border is a door, a double border is the
|
|
113
|
+
system of record, a dashed border is a projection that can be deleted and rebuilt, and a cylinder is a
|
|
114
|
+
database on disk.
|
|
115
|
+
|
|
116
|
+
## Why files
|
|
117
|
+
|
|
118
|
+
A memory an agent can be trusted with has to be reviewable, diffable, and recoverable. git supplies all
|
|
119
|
+
three directly, and that is why every write below lands as a commit:
|
|
120
|
+
|
|
121
|
+
- A correction is a commit. `memhtml correct` writes the new file and archives the old one in one
|
|
122
|
+
commit, so an interrupted run cannot leave two live memories contradicting each other.
|
|
123
|
+
- A batch is a commit. `memhtml apply` (JSONL ops) and `memory_write_batch` (MCP) stage N files, make
|
|
124
|
+
one commit, and reindex once. The batch is atomic by default, per-op results come back in input
|
|
125
|
+
order, and a duplicate succeeds with `deduped: true` and the existing path.
|
|
126
|
+
- A nightly curation run is a branch. `memhtml sleep run` walks fifteen phases and commits each one's
|
|
127
|
+
work on its own, so a human reads the curation one phase-shaped diff at a time, and
|
|
128
|
+
`memhtml sleep merge` fast-forwards `main` only after a quality gate that can refuse.
|
|
129
|
+
|
|
130
|
+
## Who does what
|
|
131
|
+
|
|
132
|
+
Three actors share one tree. The agent writes facts, and it resolves only the conflicts it found itself.
|
|
133
|
+
Sleep curates nightly on a branch, and it detects conflicts without resolving them. The human owns the
|
|
134
|
+
gate and every one-way door.
|
|
135
|
+
|
|
136
|
+
Figure 2 draws the cycle they form. A screen reader reads its box characters as noise, so the paragraph
|
|
137
|
+
beneath the figure carries the same content in words.
|
|
138
|
+
|
|
139
|
+
<!-- figure:three-actors -->
|
|
140
|
+
```text
|
|
141
|
+
+----------+
|
|
142
|
+
|the agent |
|
|
143
|
+
| |
|
|
144
|
+
+----------+
|
|
145
|
+
|
|
|
146
|
+
writes
|
|
147
|
+
|
|
|
148
|
+
v
|
|
149
|
+
+-------+
|
|
150
|
+
| main |
|
|
151
|
+
| |
|
|
152
|
+
+-------+
|
|
153
|
+
| ^
|
|
154
|
+
| +---+
|
|
155
|
+
| |
|
|
156
|
+
reads |
|
|
157
|
+
| |
|
|
158
|
+
v |
|
|
159
|
+
+---------------+ |
|
|
160
|
+
|sleep, nightly | |
|
|
161
|
+
| | |
|
|
162
|
+
+---------------+ |
|
|
163
|
+
| |
|
|
164
|
+
| merge
|
|
165
|
+
15 commits |
|
|
166
|
+
| |
|
|
167
|
+
v |
|
|
168
|
+
+-------------+ |
|
|
169
|
+
|sleep/<date> | |
|
|
170
|
+
| | |
|
|
171
|
+
+-------------+ |
|
|
172
|
+
| |
|
|
173
|
+
review |
|
|
174
|
+
| |
|
|
175
|
+
| +---+
|
|
176
|
+
| |
|
|
177
|
+
v |
|
|
178
|
+
+------------+
|
|
179
|
+
| the human |
|
|
180
|
+
| |
|
|
181
|
+
+------------+
|
|
182
|
+
```
|
|
183
|
+
<!-- /figure:three-actors -->
|
|
184
|
+
|
|
185
|
+
**Figure 2: the three actors form a cycle through `main`, and only one of them may settle a
|
|
186
|
+
contradiction.** Reading top to bottom: the agent writes to `main` at any hour, one fact per file.
|
|
187
|
+
Sleep reads `main` nightly and puts its fifteen commits on a `sleep/<date>` branch, leaving `main`
|
|
188
|
+
untouched. Those phases deduplicate, resolve entities, decay confidence, compress, and synthesize arcs,
|
|
189
|
+
and they flag a contradiction without choosing a winner. The human reviews that branch and merges, which
|
|
190
|
+
returns the cycle to `main` and to the agent. The two heavy-bordered boxes are the actors outside the
|
|
191
|
+
system, and `main` and the branch are double-bordered because they are the system of record.
|
|
192
|
+
|
|
193
|
+
## The file format
|
|
194
|
+
|
|
195
|
+
One fact per file, in standard HTML5 that a browser displays and a person can read in view-source. The
|
|
196
|
+
element vocabulary is closed, and `memhtml doctor` warns on anything outside it. Each element carries
|
|
197
|
+
meaning the indexer reads, which is the structure Markdown gives you no way to express:
|
|
198
|
+
|
|
199
|
+
```html
|
|
200
|
+
<article>
|
|
201
|
+
<p><mark>If a prod rollback is issued, drain the VIP before reverting the deploy.</mark>
|
|
202
|
+
The revert alone leaves in-flight connections pinned to the old target group,
|
|
203
|
+
observed on <time datetime="2026-07-28">July 28</time> during the <cite>checkout-api sev2</cite>.</p>
|
|
204
|
+
<dl><dt>Applies to</dt><dd>ALB/NLB target-group deploys</dd></dl>
|
|
205
|
+
<details><summary>How this was learned</summary><p>Three rollbacks replayed the same 500-spike…</p></details>
|
|
206
|
+
<aside><p>Fly.io and Cloud Run drain automatically; this is AWS-specific.</p></aside>
|
|
207
|
+
</article>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
The single `<mark>` is the claim. It becomes the gist every listing shows, and it is the span a
|
|
211
|
+
correction targets. `<time datetime>` records when the fact happened in the world, so an episodic memory
|
|
212
|
+
ranks by that date instead of by its write time. `<dl>` pairs index as facets and `<cite>` as citations.
|
|
213
|
+
`<details>` folds elaboration behind a summary, and recall always discloses that a fold is there.
|
|
214
|
+
`docs/format.md` is the full vocabulary. `docs/tasks.md` covers the task type (`memhtml-task-status`,
|
|
215
|
+
`memhtml-due`), which rides the same format.
|
|
216
|
+
|
|
217
|
+
## Writing
|
|
218
|
+
|
|
219
|
+
Three doors, all supported, all landing in the same tree:
|
|
220
|
+
|
|
221
|
+
1. The CLI. `memhtml write` takes one memory. Give it `--claim` plus `--body` and the template owns the
|
|
222
|
+
markup; give it `--article-html` and you own the markup, with the format check refusing violations
|
|
223
|
+
before anything is written. `memhtml apply` takes many: one JSONL op per line, every op validated for
|
|
224
|
+
shape before any of them executes, then one commit and one index pass.
|
|
225
|
+
2. The MCP server. `memhtml serve mcp` speaks stdio and exposes 14 tools and 2 resources over the same
|
|
226
|
+
repo: write, read, search, recall, correct, link, archive, batch writes, and trace search. A CLI
|
|
227
|
+
command and a running server share one store, because WAL admits one writer and any number of
|
|
228
|
+
readers, and a contended write retries on `SQLITE_BUSY` (see `RUNBOOK.md`, section 4).
|
|
229
|
+
3. Your file tools. The tree is the system of record, so a hand-written file is as real as one the CLI
|
|
230
|
+
wrote. You take on what the write path would have done: format validity (`memhtml doctor`), path
|
|
231
|
+
choice, dedup, and the commit. Sleep refuses to start on a dirty tree.
|
|
232
|
+
|
|
233
|
+
Dedup is enforced by the schema: a partial unique index over active files makes a duplicate write
|
|
234
|
+
impossible to index, so the write returns the existing path with `deduped: true` and creates nothing.
|
|
235
|
+
|
|
236
|
+
A memory's whole life is commits in one tree.
|
|
237
|
+
|
|
238
|
+
Figure 3 draws that life. A screen reader sounds out its box characters, so read the paragraph beneath
|
|
239
|
+
the figure, which states the same four transitions in words.
|
|
240
|
+
|
|
241
|
+
<!-- figure:memory-lifecycle -->
|
|
242
|
+
```text
|
|
243
|
+
+--------+
|
|
244
|
+
| write |
|
|
245
|
+
| |
|
|
246
|
+
+--------+
|
|
247
|
+
|
|
|
248
|
+
commit
|
|
249
|
+
|
|
|
250
|
+
v
|
|
251
|
+
+-----------+
|
|
252
|
+
| active |
|
|
253
|
+
| |
|
|
254
|
+
+-----------+
|
|
255
|
+
| | |
|
|
256
|
+
+----------+ | +----------+
|
|
257
|
+
| | |
|
|
258
|
+
| evict |
|
|
259
|
+
correct | compress
|
|
260
|
+
| | |
|
|
261
|
+
v v v
|
|
262
|
+
+-----------+ +---------+ +-----------+
|
|
263
|
+
|superseded | |archived | |compressed |
|
|
264
|
+
| | | | | |
|
|
265
|
+
+-----------+ +---------+ +-----------+
|
|
266
|
+
```
|
|
267
|
+
<!-- /figure:memory-lifecycle -->
|
|
268
|
+
|
|
269
|
+
**Figure 3: a memory has one entry and three exits, and every one of them is a commit.** A write enters
|
|
270
|
+
the corpus as a single dedup-checked commit, and the file is then active. It stays active while sleep
|
|
271
|
+
reinforces or decays its confidence in place. Three things can end that state: `memhtml correct` writes a
|
|
272
|
+
replacement and archives the original in one commit, which makes it superseded; retention triage scores
|
|
273
|
+
it into the EVICT band and archives it, which makes it archived; or compress folds it into a synthesized
|
|
274
|
+
canonical memory and archives it with a `supersedes` link, which makes it compressed. Each of the three
|
|
275
|
+
exits is a `git mv` into `archive/YYYY/` mirroring the original path, so the file survives all of them
|
|
276
|
+
and `git log --follow` reads straight through the whole life.
|
|
277
|
+
|
|
278
|
+
## Retrieval
|
|
279
|
+
|
|
280
|
+
Four ranking arms are fused by reciprocal rank fusion (RRF, k=60) inside one SQL statement, and maximal
|
|
281
|
+
marginal relevance (MMR) diversifies the result afterwards in TypeScript:
|
|
282
|
+
|
|
283
|
+
| arm | weight | what it ranks |
|
|
284
|
+
|---|---|---|
|
|
285
|
+
| fts | 1.0 | one denormalized title+gist+body column |
|
|
286
|
+
| vector | 1.0 | exact brute force over Cohere Embed v4 (1024-dim), grouped by path so a long memory does not outrank a relevant one |
|
|
287
|
+
| recency | 0.5 | `coalesce(event_at, updated_at)`, so an episodic memory sorts by when the fact happened |
|
|
288
|
+
| salience | 0.4 | the durable access plane, attached in the same statement, with tasks and `resources/people/` excluded |
|
|
289
|
+
|
|
290
|
+
A Bedrock outage narrows retrieval instead of stopping it. Arms that need a query vector are dropped
|
|
291
|
+
before the statement is assembled, the response carries `degraded: true`, and the remaining arms answer.
|
|
292
|
+
|
|
293
|
+
Salience counts the opens a caller chose. `memhtml read` and `memory_read` of a named path bump the access
|
|
294
|
+
plane; a path that `memhtml search` or `memhtml recall` merely returned does not, and neither does a sleep
|
|
295
|
+
phase. Bumping on a hit would make today's top five rank higher tomorrow for having been listed, while
|
|
296
|
+
the memory that should displace them never gets a first bump. `memhtml reinforce` is the explicit outcome
|
|
297
|
+
channel, and it moves the same exponentially weighted moving average. The arm also stays out of the way
|
|
298
|
+
of a `task` row and a `resources/people/` reference record: both are reached by predicate and by key, and
|
|
299
|
+
salience there would reward a stale task and decay a person's identity.
|
|
300
|
+
|
|
301
|
+
`memhtml recall` adds a disclosure fold on top. Arcs get their own character envelope, so a summary does
|
|
302
|
+
not compete with the memories it summarizes. Each fold quotes at most 2 memories per entity name, and
|
|
303
|
+
everything past the budget collapses to one index line plus a path to drill into.
|
|
304
|
+
|
|
305
|
+
## The discrimination gate
|
|
306
|
+
|
|
307
|
+
`memhtml eval discriminate` reports the number that says whether retrieval can tell two similar facts
|
|
308
|
+
apart.
|
|
309
|
+
Embeddings are weakest on the tokens that carry a fact's polarity: "drain the VIP before reverting" and
|
|
310
|
+
"do not drain the VIP before reverting" sit above 0.99 cosine similarity while asserting opposite things.
|
|
311
|
+
So the gate derives every control from the probe's own target by flipping a negation, a number, or a
|
|
312
|
+
qualifier, which makes each control a high-cosine wrong answer by construction. Every target has to
|
|
313
|
+
strictly outrank all of its own controls, mean reciprocal rank has to clear 0.85, and one inversion fails
|
|
314
|
+
the run.
|
|
315
|
+
|
|
316
|
+
Two places run it. `pnpm check` runs it, and CI runs `pnpm check`. `memhtml sleep merge` runs it a second
|
|
317
|
+
time, so a sleep run that degrades retrieval cannot land. Fake-embedder mode is deterministic and needs
|
|
318
|
+
no credentials. `live` mode is an operator diagnostic, and it reports `skipped: true` when it cannot
|
|
319
|
+
reach the model, so a skipped gate reads as skipped rather than as green.
|
|
320
|
+
|
|
321
|
+
## Sleep
|
|
322
|
+
|
|
323
|
+
`memhtml sleep run` executes fifteen curation phases on a `sleep/<date>` branch: dedup-merge, entity
|
|
324
|
+
resolution, conflict detection, confidence decay, arc synthesis, retention triage, compress, integrity,
|
|
325
|
+
and the rest. Each committing phase makes its own isolated commit with a machine-readable trailer, so
|
|
326
|
+
`memhtml sleep resume` re-runs only what is missing. Two phases commit nothing by design. `preflight`
|
|
327
|
+
refreshes the index, and `relationship-mining` writes derived edges to the index alone, because thousands
|
|
328
|
+
of re-derivable edges would bury every real diff. `trace-consolidation` hands unread session transcripts
|
|
329
|
+
to an agent and lands each distilled memory as its own commit, one per memory, so a reviewer reads one
|
|
330
|
+
claim at a time. A failed phase leaves the phases before it committed.
|
|
331
|
+
|
|
332
|
+
`memhtml sleep review` classifies every touched file. `memhtml sleep merge` re-runs the discrimination gate
|
|
333
|
+
and refuses to move `main` on a regression. Detecting a conflict is nightly and automatic; resolving one
|
|
334
|
+
stays with the writer or a human, because choosing a winner is a one-way door.
|
|
335
|
+
|
|
336
|
+
Figure 4 draws the branch and the gate. A screen reader sounds out its box characters, so read the
|
|
337
|
+
paragraph beneath the figure, which carries the same content in words.
|
|
338
|
+
|
|
339
|
+
<!-- figure:sleep-branch -->
|
|
340
|
+
```text
|
|
341
|
+
+-------+
|
|
342
|
+
| main |
|
|
343
|
+
| |
|
|
344
|
+
+-------+
|
|
345
|
+
|
|
|
346
|
+
branch
|
|
347
|
+
|
|
|
348
|
+
v
|
|
349
|
+
+-------------+
|
|
350
|
+
|sleep/<date> |
|
|
351
|
+
| |
|
|
352
|
+
+-------------+
|
|
353
|
+
|
|
|
354
|
+
v
|
|
355
|
+
+---------------+
|
|
356
|
+
|fifteen phases |
|
|
357
|
+
| |
|
|
358
|
+
+---------------+
|
|
359
|
+
|
|
|
360
|
+
review
|
|
361
|
+
|
|
|
362
|
+
v
|
|
363
|
+
+---------+
|
|
364
|
+
|the gate |
|
|
365
|
+
| |
|
|
366
|
+
+---------+
|
|
367
|
+
| |
|
|
368
|
+
+-----+ +-----+
|
|
369
|
+
| |
|
|
370
|
+
passes refuses
|
|
371
|
+
| |
|
|
372
|
+
v v
|
|
373
|
+
+-----------+ +-------------+
|
|
374
|
+
|main moves | |main unmoved |
|
|
375
|
+
| | | |
|
|
376
|
+
+-----------+ +-------------+
|
|
377
|
+
```
|
|
378
|
+
<!-- /figure:sleep-branch -->
|
|
379
|
+
|
|
380
|
+
**Figure 4: `main` moves only after a gate that can refuse says so.** A run branches `main` into
|
|
381
|
+
`sleep/<date>` before any phase executes and walks its fifteen phases there, thirteen of them committing,
|
|
382
|
+
each on its own, with `preflight` and `relationship-mining` committing nothing by design. Then it submits
|
|
383
|
+
the branch for review. That review re-runs the discrimination gate and has two outcomes, both drawn: it
|
|
384
|
+
passes and `main` moves, or it refuses and `main` stays exactly where it was. Those are the only two
|
|
385
|
+
outcomes, and neither needs a rollback, because nothing on `main` ever moved. The abort is
|
|
386
|
+
`git branch -D`.
|
|
387
|
+
|
|
388
|
+
## Code-mode
|
|
389
|
+
|
|
390
|
+
The closed vocabulary makes the corpus a queryable API with no new surface: `article mark` is always the
|
|
391
|
+
claim, `link[rel^="memhtml-"]` is always an authored edge, and `dl` pairs are always facets. Use the
|
|
392
|
+
descendant selector. The markup is `<article><p><mark>`, so `article > mark` matches nothing and a helper
|
|
393
|
+
written from that spelling reports zero claims while looking correct. An agent that writes parser code
|
|
394
|
+
against `$MEMHTML_ROOT` composes multi-hop traversals in one execution and answers corpus-shaped
|
|
395
|
+
questions no tool enumerates: live contradiction pairs, an orphan census, a walk up a supersedence chain.
|
|
396
|
+
The contract is read-only, and writes stay behind the three doors above.
|
|
397
|
+
|
|
398
|
+
`memhtml exec` ships this as a command. It runs your script in a QuickJS sandbox with the corpus mounted
|
|
399
|
+
read-only at `/mnt/memhtml` and a helper preloaded at `/workspace/lib/corpus.mjs`, and it answers one
|
|
400
|
+
`exec.report` envelope. Measured on the 305-file fixture corpus: 305/305 claims parsed and 410/410 edges
|
|
401
|
+
resolved in one execution. The script reads a pinned commit, so an answer is reproducible and an
|
|
402
|
+
uncommitted edit stays invisible to it. The sandbox reaches the structural and lexical planes and holds
|
|
403
|
+
no index handle, so a script that wants ranked retrieval shells out to `memhtml search` and consumes its
|
|
404
|
+
envelope. `docs/code-mode.md` is the cookbook, with a measured helper and five recipes.
|
|
405
|
+
|
|
406
|
+
## Measured
|
|
407
|
+
|
|
408
|
+
| benchmark | memhtml | published reference |
|
|
409
|
+
|---|---|---|
|
|
410
|
+
| MemoryAgentBench FactConsolidation single-hop (26KB to 1.1MB stores) | 92% to 97% | ~60% at 26KB only |
|
|
411
|
+
| MemoryAgentBench FactConsolidation multi-hop | 37% to 49% | ≤7% all methods |
|
|
412
|
+
| BEAM Contradiction Resolution (100K split, 40 probes) | 43.8% mean | 0% to 5% all systems |
|
|
413
|
+
| LongMemEval-S (25-instance smoke) | 68% | ~55% to 65% typical agent baselines |
|
|
414
|
+
|
|
415
|
+
Read the cross-judge numbers as reference points rather than as a ranking: the judges here are verbatim
|
|
416
|
+
prompt ports running haiku-4.5, where the papers used gpt-4o and gpt-4.1-mini. `ROADMAP.md` carries these
|
|
417
|
+
numbers and the horizons they rank.
|
|
418
|
+
|
|
419
|
+
## Layout
|
|
420
|
+
|
|
421
|
+
```
|
|
422
|
+
$MEMHTML_ROOT/ # its own git repo, one global memory store
|
|
423
|
+
projects/<workspace-slug>/ # a workspace IS a directory. There is no workspaces table.
|
|
424
|
+
areas/<area-slug>/ # ongoing responsibilities
|
|
425
|
+
areas/arcs/ # behavioural arcs (system-written by sleep only)
|
|
426
|
+
areas/inbox/ # where an unplaceable memory lands
|
|
427
|
+
resources/<topic>/
|
|
428
|
+
resources/people/<person>.html # the person plane
|
|
429
|
+
archive/<YYYY>/<original-path> # soft-evicted, path-preserving, injective
|
|
430
|
+
.memhtml/
|
|
431
|
+
index.db # gitignored, rebuildable from the tree
|
|
432
|
+
state.db # gitignored, NOT rebuildable from git
|
|
433
|
+
state/access.jsonl # committed sidecar: the state plane's only durable copy
|
|
434
|
+
sleep/<run-id>.html # committed sleep reports
|
|
435
|
+
sitemap.xml + per-dir index.html # generated by `memhtml publish`, committed
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
## Packages
|
|
439
|
+
|
|
440
|
+
The layering is strict and TypeScript project references enforce it. `@memhtml/contracts` and
|
|
441
|
+
`@memhtml/domain` import `effect` and nothing else, and a test reads `domain`'s own `dist` to confirm it
|
|
442
|
+
names no database driver, no SDK, and no `node:fs`.
|
|
443
|
+
|
|
444
|
+
None of them is published. All twelve are `private`, and `mise run package:assemble` bundles them into
|
|
445
|
+
the single `memhtml` package that carries the two binaries — so the table below is a map of the source,
|
|
446
|
+
not a list of things to install. `RELEASING.md` covers how the artifact is built and what must stay
|
|
447
|
+
outside the bundle.
|
|
448
|
+
|
|
449
|
+
| Package | What it owns |
|
|
450
|
+
|---|---|
|
|
451
|
+
| `@memhtml/contracts` | Schemas, the closed vocabularies, errors, path algebra. Zero I/O. |
|
|
452
|
+
| `@memhtml/domain` | Pure math: retention, decay, RRF, MMR, PageRank, the anti-merge guards. |
|
|
453
|
+
| `@memhtml/html` | The memory file format: parse, serialize, hash, surgical head editors. |
|
|
454
|
+
| `@memhtml/store` | The git-backed file store. One commit per operation, typed conflicts. |
|
|
455
|
+
| `@memhtml/index` | SQLite schema, the git-driven indexer, four-arm RRF retrieval, the state plane. |
|
|
456
|
+
| `@memhtml/traces` | Streaming JSONL parser over `~/.claude`, with a size+mtime+offset watermark. |
|
|
457
|
+
| `@memhtml/sleep` | The fifteen curation phases, each an isolated commit. |
|
|
458
|
+
| `@memhtml/llm` | Bedrock: Cohere embeddings and forced-tool structured output. |
|
|
459
|
+
| `@memhtml/eval` | The fixture corpus generator and the refusable discrimination gate. |
|
|
460
|
+
| `@memhtml/cli` | The `memhtml` binary, the envelope contract, and the one composition root. |
|
|
461
|
+
| `@memhtml/mcp` | The `memhtml-mcp` stdio server: 14 tools, 2 resources. |
|
|
462
|
+
|
|
463
|
+
## Development
|
|
464
|
+
|
|
465
|
+
[`mise`](https://mise.jdx.dev) is the command surface. It installs the toolchain from `mise.toml`, which
|
|
466
|
+
declares node, pnpm, lefthook, and the scanners, each pinned by checksum and provenance in the committed
|
|
467
|
+
`mise.lock`, so a clone resolves the same binaries CI does:
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
mise install # node 24, pnpm 11.21.0, lefthook, scanners, from mise.lock
|
|
471
|
+
mise run install # dependencies from the lockfile + the git hooks
|
|
472
|
+
mise run check # the definition of done: lint, typecheck, tests, integration, eval, a11y, budget
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
CI runs that same `mise run check`, so the gate cannot drift from the one you run locally. Every task
|
|
476
|
+
delegates to the pnpm script underneath it, and turbo owns the task graph and the cache. No mise task
|
|
477
|
+
declares `sources` or `outputs`, because mise decides freshness by mtime and turbo by content hash, so a
|
|
478
|
+
mise-level skip would preempt turbo's per-package hashing.
|
|
479
|
+
|
|
480
|
+
`check` includes the discrimination gate in fake mode, so a change that degrades retrieval fails the
|
|
481
|
+
build. Tests run against a real temp-dir git repo and a real SQLite database with the shipped migrations.
|
|
482
|
+
Fakes are limited to the two edges that reach the network, the embedder and the model, because a stateless
|
|
483
|
+
fake verifies the shape of a call and misses the state semantics behind it, which is where the defects in
|
|
484
|
+
this system have actually lived.
|
|
485
|
+
|
|
486
|
+
| Command | Delegates to | What it runs |
|
|
487
|
+
|---|---|---|
|
|
488
|
+
| `mise run build` | `pnpm build` | `tsc -b` across the project graph |
|
|
489
|
+
| `mise run lint` | `pnpm lint` | biome |
|
|
490
|
+
| `mise run typecheck` | `pnpm typecheck` | strict `tsc --noEmit`, tests included |
|
|
491
|
+
| `mise run test` | `pnpm test` | every package's unit and property suites |
|
|
492
|
+
| `mise run test:integration` | `pnpm test:integration` | the cross-package contracts over a real repo and a real database |
|
|
493
|
+
| `mise run test:eval` | `pnpm test:eval` | the discrimination gate (fake mode) |
|
|
494
|
+
| `mise run test:a11y` | `pnpm test:a11y` | WCAG 2.2 AA over the built docs site, in a real browser |
|
|
495
|
+
| `mise run test:budget` | `pnpm test:budget` | Lighthouse category floors and the byte budget for that site |
|
|
496
|
+
| `mise run gen:fixture` | `pnpm gen:fixture` | write a browsable fixture corpus (pure function of a seed) |
|
|
497
|
+
| `mise run agents-doc` | none | regenerate `AGENTS.md` from the built CLI's own table |
|
|
498
|
+
| `mise run security` | none | osv-scanner + semgrep + betterleaks, SARIF into `.sarif/` |
|
|
499
|
+
| `mise run tools:bump` | none | re-resolve every `latest` tool in `mise.lock` |
|
|
500
|
+
|
|
501
|
+
To narrow a run to one package, use `mise run test-pkg <package> [vitest args]`. The package name takes
|
|
502
|
+
either spelling, and everything after it goes to vitest:
|
|
503
|
+
|
|
504
|
+
```bash
|
|
505
|
+
mise run test-pkg domain rrf -t "strictly" # one test
|
|
506
|
+
mise run test-pkg index retrieval # one file
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
That path goes straight to the package's vitest, so it skips turbo and builds nothing first. Every
|
|
510
|
+
`@memhtml/*` package's exports resolve only to `./dist`, so run `mise run build` after editing another
|
|
511
|
+
package's `src/`.
|
|
512
|
+
|
|
513
|
+
`mise.toml`'s `[tools] pnpm` and `package.json`'s `packageManager` both declare the pnpm that runs, and
|
|
514
|
+
neither can be derived from the other. `mise run tools:verify` fails when they disagree, and
|
|
515
|
+
`mise run install` depends on it, so the check runs on every install. Declaration order matters too:
|
|
516
|
+
`pnpm` sits above `node` because node's own bin holds a `pnpm` symlink into corepack wherever
|
|
517
|
+
`corepack enable` has run, and with node first every pnpm call would resolve to corepack rather than to
|
|
518
|
+
the pinned binary.
|
|
519
|
+
|
|
520
|
+
## Docs
|
|
521
|
+
|
|
522
|
+
| File | What it holds |
|
|
523
|
+
|---|---|
|
|
524
|
+
| `AGENTS.md` | The full command surface, generated from the binary's own table |
|
|
525
|
+
| `RUNBOOK.md` | Operating the store day to day |
|
|
526
|
+
| `ROADMAP.md` | The system-level view: measured standing, ranked horizons |
|
|
527
|
+
| `docs/design.md` | Every architectural decision with its evidence |
|
|
528
|
+
| `docs/format.md` | The file format and the closed vocabulary |
|
|
529
|
+
| `docs/tasks.md` | The task memory type |
|
|
530
|
+
| `docs/code-mode.md` | Navigating the corpus with code |
|
|
531
|
+
| `docs/backlog.md` | The fine-grained ledger |
|
package/agent/agent.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock"
|
|
2
|
+
import { defineAgent } from "eve"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The consolidator agent's runtime config.
|
|
6
|
+
*
|
|
7
|
+
* eve is filesystem-first: this file IS the config, reached by `eve build` then `eve start`,
|
|
8
|
+
* never by a programmatic `defineAgent().run()`. `agent/instructions.md` is required and carries
|
|
9
|
+
* the TRACE-2 bar. The client wrapper in `src/client.ts` drives it over HTTP.
|
|
10
|
+
*
|
|
11
|
+
* Bedrock direct, not the Vercel AI Gateway: the gateway is an anti-goal, and a
|
|
12
|
+
* provider-authored `LanguageModel` is how eve is told to call a provider directly
|
|
13
|
+
* (node_modules/eve/docs/agent-config.md, "Set the model").
|
|
14
|
+
*
|
|
15
|
+
* Credentials are read from the environment by the provider itself and there is NO default AWS
|
|
16
|
+
* chain — `AWS_BEARER_TOKEN_BEDROCK`, else `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`. No
|
|
17
|
+
* shared config file, no SSO cache, no instance metadata. The provider is also lazy: this call
|
|
18
|
+
* and the `bedrock(...)` call below both succeed with zero credentials and nothing fails until
|
|
19
|
+
* the first request, which is exactly why `hasConsolidatorCredentials()` exists in
|
|
20
|
+
* `src/contract.ts` and runs before a server is ever spawned.
|
|
21
|
+
*/
|
|
22
|
+
const bedrock = createAmazonBedrock({ region: process.env.AWS_REGION ?? "us-east-1" })
|
|
23
|
+
|
|
24
|
+
export default defineAgent({
|
|
25
|
+
model: bedrock("global.anthropic.claude-opus-5"),
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* REQUIRED here, unlike for a gateway model id. eve's window catalog does not know this
|
|
29
|
+
* model id, and without the window it cannot judge when to compact. Verified against the
|
|
30
|
+
* installed 0.33.0 type: `modelContextWindowTokens?: number` on the agent definition
|
|
31
|
+
* (node_modules/eve/dist/src/shared/agent-definition.d.ts:59).
|
|
32
|
+
*
|
|
33
|
+
* **1,000,000 is the window Opus 5 serves on the Bedrock global inference profile**, confirmed by
|
|
34
|
+
* the operator. The reason a number has to be written here at all is that eve resolves windows from
|
|
35
|
+
* a Gateway catalog that does not know an inference profile id, so the resolution it would do is
|
|
36
|
+
* unavailable rather than merely wrong — and the number it previously carried, 200_000, was not a
|
|
37
|
+
* measurement of anything: it was the conservative value chosen when the catalog came up empty.
|
|
38
|
+
*
|
|
39
|
+
* Not cosmetic. This drives eve's COMPACTION THRESHOLD, so a window declared at a fifth of the real
|
|
40
|
+
* one makes the harness compact a session that had four fifths of its budget left — and compaction
|
|
41
|
+
* of a transcript-reading session discards the earlier reads a cross-session pattern is assembled
|
|
42
|
+
* from, which is the one thing this agent exists to find.
|
|
43
|
+
*/
|
|
44
|
+
modelContextWindowTokens: 1_000_000,
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Provider-agnostic reasoning ONLY. Do not add
|
|
48
|
+
* `modelOptions.providerOptions.bedrock.reasoningConfig`.
|
|
49
|
+
*
|
|
50
|
+
* Probed live: Opus 5 rejects `reasoningConfig: { type: "enabled", budgetTokens }` outright,
|
|
51
|
+
* and eve drops an unsupported `providerOptions` silently — so the pairing fails in the worst
|
|
52
|
+
* way, looking configured while being either an error or a no-op. `reasoning: "high"` is
|
|
53
|
+
* forwarded by eve to the turn's model calls and is what actually takes effect.
|
|
54
|
+
*/
|
|
55
|
+
reasoning: "high",
|
|
56
|
+
|
|
57
|
+
limits: {
|
|
58
|
+
/**
|
|
59
|
+
* A batch of transcripts is a read-heavy job with a small answer. This bounds the answer,
|
|
60
|
+
* not the reading, so a run cannot spin producing candidates.
|
|
61
|
+
*
|
|
62
|
+
* Crossing it does not raise a continuation prompt here: a run driven by the client wrapper
|
|
63
|
+
* has no human to ask, and eve fails the next model call with `SESSION_TOKEN_LIMIT_REACHED`
|
|
64
|
+
* for sessions that cannot reach one. That surfaces as a typed `ConsolidatorRunFailed`.
|
|
65
|
+
*/
|
|
66
|
+
maxOutputTokensPerSession: 50_000
|
|
67
|
+
}
|
|
68
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { jwtHmac } from "eve/channels/auth"
|
|
2
|
+
import { eveChannel } from "eve/channels/eve"
|
|
3
|
+
|
|
4
|
+
import { runVerifierConfig } from "../../src/run-auth.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The HTTP surface `src/client.ts` drives: `/eve/v1`, serving session create, follow-up, and
|
|
8
|
+
* the NDJSON event stream.
|
|
9
|
+
*
|
|
10
|
+
* ## Every request needs a bearer JWT signed with THIS RUN's secret
|
|
11
|
+
*
|
|
12
|
+
* `jwtHmac` verifies an HS256 bearer token against a secret read from the spawn environment
|
|
13
|
+
* (node_modules/eve/dist/src/public/channels/auth.d.ts:451; the config shape is `VerifyJwtHmacConfig`
|
|
14
|
+
* at :41-60). The secret is minted per spawn from `randomBytes` by the client that starts this server
|
|
15
|
+
* — `src/run-auth.ts` owns the whole mechanism, both halves of it, so what is signed and what is
|
|
16
|
+
* accepted cannot drift. There is no fixed default and no config key: a credential that reached this
|
|
17
|
+
* endpoint is good for one run, and a run's secret authenticates nothing after it.
|
|
18
|
+
*
|
|
19
|
+
* `[]` when the environment carries no usable secret, and that empty array is the FAIL-CLOSED path
|
|
20
|
+
* rather than an accident of expression. `routeAuth` returns a 401 when its walk exhausts, "including
|
|
21
|
+
* the empty-array case" (auth.d.ts:255-262, verified live: a spawn with the variable unset answers
|
|
22
|
+
* `{"ok":false,"code":"unauthorized"}` with `www-authenticate: Bearer` on both the create and info
|
|
23
|
+
* routes). So a server started without the variable serves nothing, which is the direction that
|
|
24
|
+
* matters — a fall back to anonymous would be strictly worse than the `none()` this replaces, because
|
|
25
|
+
* that at least announced itself.
|
|
26
|
+
*
|
|
27
|
+
* `runVerifierConfig` refuses an under-width secret for a measured reason: eve keys the verifier with
|
|
28
|
+
* `createSecretKey(Buffer.from(secret, "utf8"))`
|
|
29
|
+
* (node_modules/eve/dist/src/runtime/governance/auth/jwt-hmac.js) and jose does not check HS key width
|
|
30
|
+
* on verify — probed 2026-08-09, a three-character secret verifies its own token. The width floor is
|
|
31
|
+
* therefore this app's to enforce or nobody's.
|
|
32
|
+
*
|
|
33
|
+
* Beside the signature, `issuer`, `audiences`, and `subjects` are matched
|
|
34
|
+
* (`areTokenClaimMatchersSatisfied`, node_modules/eve/dist/src/runtime/governance/auth/token-claims.js),
|
|
35
|
+
* and eve rejects a token with no `sub` before consulting any matcher. That is what keeps a secret
|
|
36
|
+
* leaked into some other eve app's environment from cross-authenticating here.
|
|
37
|
+
*
|
|
38
|
+
* ## The bind address STAYS, as defence in depth
|
|
39
|
+
*
|
|
40
|
+
* `eve start` binds ALL INTERFACES by default (node_modules/eve/docs/reference/cli.md, `eve start
|
|
41
|
+
* --host`), so `src/client.ts` still spawns with `--host 127.0.0.1` from a constant that takes no
|
|
42
|
+
* caller value — see `LOOPBACK_HOST` there. It is no longer the only control, and it is not redundant:
|
|
43
|
+
* loopback bounds who can OPEN a connection, the token bounds who can be SERVED. Never start this
|
|
44
|
+
* agent by hand without that flag.
|
|
45
|
+
*
|
|
46
|
+
* The server is also short-lived: the wrapper spawns it per run on an ephemeral port and kills it when
|
|
47
|
+
* the run settles.
|
|
48
|
+
*
|
|
49
|
+
* ## What is still exposed
|
|
50
|
+
*
|
|
51
|
+
* **An attacker who can READ this process's spawn environment still wins.** Same-UID `/proc/<pid>/environ`
|
|
52
|
+
* is the concrete path, and `MEMHTML_CONSOLIDATOR_RUN_SECRET` is in there for the server's whole life; a
|
|
53
|
+
* reader mints valid tokens for as long as the run lasts. What is closed is the case the finding was
|
|
54
|
+
* about — a different local UID, which can reach loopback and cannot read another UID's environment.
|
|
55
|
+
* The remaining exposure is a same-UID one, and the mitigation for it is not in this app either: it is
|
|
56
|
+
* whatever isolates the UID the sleep cycle runs as.
|
|
57
|
+
*
|
|
58
|
+
* That matters because of what the sandbox behind this endpoint can do. Egress is on, and eve hardcodes
|
|
59
|
+
* it — an authenticated caller still gets a bash sandbox that reaches IMDS (`agent/sandbox/sandbox.ts`
|
|
60
|
+
* records the measurement and why nothing here can turn it off). Auth is what makes reaching that
|
|
61
|
+
* sandbox require a credential; it does not make the sandbox safe.
|
|
62
|
+
*
|
|
63
|
+
* If this app ever needs a non-loopback deployment, the secret channel is the thing to replace: an
|
|
64
|
+
* environment variable is fine between a parent and the child it spawned and is not a way to
|
|
65
|
+
* distribute a credential to a caller on another host. `httpBasic`, `oidc`, and `jwtEcdsa` are
|
|
66
|
+
* exported from the same module.
|
|
67
|
+
*/
|
|
68
|
+
export default eveChannel({
|
|
69
|
+
auth: (() => {
|
|
70
|
+
const config = runVerifierConfig(process.env)
|
|
71
|
+
return config === null ? [] : [jwtHmac(config)]
|
|
72
|
+
})()
|
|
73
|
+
})
|