agenticow 0.1.0 โ 0.1.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/README.md +150 -0
- package/examples/README.md +111 -0
- package/examples/_shared.mjs +34 -0
- package/examples/ab-branches.mjs +60 -0
- package/examples/checkpointing.mjs +64 -0
- package/examples/git-workflow.mjs +52 -0
- package/examples/parallel-agents.mjs +1 -1
- package/examples/personalization.mjs +66 -0
- package/examples/rollback-quarantine.mjs +54 -0
- package/package.json +8 -1
- package/src/index.js +24 -13
package/README.md
CHANGED
|
@@ -87,6 +87,133 @@ A worked script lives in [`examples/parallel-agents.mjs`](./examples/parallel-ag
|
|
|
87
87
|
|
|
88
88
|
---
|
|
89
89
|
|
|
90
|
+
## Applications
|
|
91
|
+
|
|
92
|
+
Concrete ways to use COW agent memory โ each with a runnable script in
|
|
93
|
+
[`examples/`](./examples). Framing is honest: **practical** ships and is proven,
|
|
94
|
+
**strategic** is shipped-but-early, **exotic** is vision/roadmap (clearly marked).
|
|
95
|
+
|
|
96
|
+
### ๐ข Personalization โ one base, a branch per user *(practical)*
|
|
97
|
+
Give every user/account/tenant their own memory branch off a shared base. Private
|
|
98
|
+
edits stay isolated; storage is delta-only (KB/user, not a full copy).
|
|
99
|
+
```js
|
|
100
|
+
const base = open('kb.rvf', { dimension: 1536 });
|
|
101
|
+
const userMem = base.fork(`user-${userId}`);
|
|
102
|
+
userMem.ingest([{ id, vector }]); // private to this user
|
|
103
|
+
userMem.query(q, 10); // reads through to the shared base
|
|
104
|
+
```
|
|
105
|
+
โ [`examples/personalization.mjs`](./examples/personalization.mjs) ยท [`parallel-agents.mjs`](./examples/parallel-agents.mjs)
|
|
106
|
+
|
|
107
|
+
### ๐ข Rollback / quarantine โ discard a poisoned branch *(practical)*
|
|
108
|
+
An agent ingests hallucinated or adversarial memories into a sandbox branch.
|
|
109
|
+
Detect it, drop the branch โ the base is instantly clean, no re-index.
|
|
110
|
+
```js
|
|
111
|
+
const sandbox = base.fork('untrusted');
|
|
112
|
+
sandbox.ingest(unvettedVectors);
|
|
113
|
+
// ...detect bad content...
|
|
114
|
+
sandbox.close(); // discard โ base never saw it
|
|
115
|
+
```
|
|
116
|
+
โ [`examples/rollback-quarantine.mjs`](./examples/rollback-quarantine.mjs)
|
|
117
|
+
|
|
118
|
+
### ๐ข Checkpointing โ crash recovery without replay *(practical)*
|
|
119
|
+
Checkpoint memory before each risky step (162 B each). On failure, roll back to
|
|
120
|
+
the last good checkpoint in ~0.5 ms โ earlier steps are not replayed.
|
|
121
|
+
```js
|
|
122
|
+
const ck = mem.checkpoint('step-30');
|
|
123
|
+
// ...step 31 crashes...
|
|
124
|
+
mem.rollback(ck.id); // resume from step 30
|
|
125
|
+
```
|
|
126
|
+
โ [`examples/checkpointing.mjs`](./examples/checkpointing.mjs)
|
|
127
|
+
|
|
128
|
+
### ๐ข Git-style memory workflow โ branch โ diff โ promote *(practical)*
|
|
129
|
+
Treat memory like code: branch a feature, review the change with `diff()`, and
|
|
130
|
+
`promote()` the vetted delta into production.
|
|
131
|
+
```js
|
|
132
|
+
const feature = prod.branch('feature');
|
|
133
|
+
feature.ingest(newFacts);
|
|
134
|
+
feature.diff(); // { added, overridden, deleted }
|
|
135
|
+
feature.promote(prod); // merge into production
|
|
136
|
+
```
|
|
137
|
+
โ [`examples/git-workflow.mjs`](./examples/git-workflow.mjs)
|
|
138
|
+
|
|
139
|
+
### ๐ก A/B testing & evolution โ score variants, promote the winner *(strategic)*
|
|
140
|
+
Fork N variant branches off one base, score each, and promote only the winner.
|
|
141
|
+
The substrate for Darwin-style / population-based agent-memory search.
|
|
142
|
+
```js
|
|
143
|
+
const variants = ids.map((i) => base.fork(`variant-${i}`));
|
|
144
|
+
// ...score each...
|
|
145
|
+
variants[best].promote(base); // keep the winner, drop the rest free
|
|
146
|
+
```
|
|
147
|
+
โ [`examples/ab-branches.mjs`](./examples/ab-branches.mjs)
|
|
148
|
+
|
|
149
|
+
### ๐ก Compliance & lineage โ provenance-tracked memory *(strategic)*
|
|
150
|
+
Every branch records its parent id + hash and a cryptographic witness chain.
|
|
151
|
+
`lineage()` gives an auditable history of how a memory state was reached โ
|
|
152
|
+
useful for reproducibility and audit trails.
|
|
153
|
+
|
|
154
|
+
### ๐ก Edge / local-first agents โ embedded, no server *(strategic)*
|
|
155
|
+
agenticow runs in-process over a single `.rvf` file โ no DB server, no network.
|
|
156
|
+
Thousands of cheap branches fit on-device for offline/edge multi-agent memory.
|
|
157
|
+
|
|
158
|
+
### ๐ญ Agent marketplaces & shared base memories *(exotic โ vision, not shipped)*
|
|
159
|
+
A published base memory that many agents branch from, contributing deltas back โ
|
|
160
|
+
a "memory package registry". The branch/promote primitives exist today; the
|
|
161
|
+
distribution, trust, and merge-policy layer is **roadmap, not shipped**.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## MetaHarness usage
|
|
166
|
+
|
|
167
|
+
agenticow is the **memory plane** of the [`@metaharness/*`](https://www.npmjs.com/org/metaharness)
|
|
168
|
+
agent-harness ecosystem. It pairs with [`@metaharness/jujutsu`](https://www.npmjs.com/package/@metaharness/jujutsu)
|
|
169
|
+
(`v0.1.0`), which wraps [`agentic-jujutsu`](https://www.npmjs.com/package/agentic-jujutsu)
|
|
170
|
+
(a Rust+NAPI Jujutsu `jj` op-log with QuantumDAG coordination, ReasoningBank
|
|
171
|
+
trajectories, and ML-DSA signing) โ the **code/op plane**.
|
|
172
|
+
|
|
173
|
+
**The dual-state bridge (ADR-202).** A coding agent that explores must branch and
|
|
174
|
+
roll back *two* planes: the **code/ops** it did and the **memory** it learned.
|
|
175
|
+
Used separately they drift (revert code but keep poisoned memory; promote a
|
|
176
|
+
memory delta whose ops were never merged). `@metaharness/jujutsu`'s
|
|
177
|
+
`DualStateBridge` ties them 1:1 โ one agent โ one op branch + one memory branch โ
|
|
178
|
+
mapping four lifecycle verbs onto agenticow:
|
|
179
|
+
|
|
180
|
+
| Verb | code/op plane (agentic-jujutsu) | memory plane (agenticow) |
|
|
181
|
+
|---|---|---|
|
|
182
|
+
| **spawn** | `jj bookmark create` + start trajectory | `fork()` off the base + `checkpoint('spawn')` |
|
|
183
|
+
| **learn** | finalize trajectory + read op-sequence | embed ops โ `ingest()` into the branch |
|
|
184
|
+
| **revert** | `jj undo` | `rollback()` to the spawn checkpoint |
|
|
185
|
+
| **merge** | `jj squash` ops into base | `promote()` the winning delta into the base |
|
|
186
|
+
|
|
187
|
+
Install alongside (both planes are **optional peer deps** โ the bridge runs
|
|
188
|
+
degraded/mock-backed if one is absent, per the ADR-150 *removable-augmentation*
|
|
189
|
+
principle):
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
npm install @metaharness/jujutsu agenticow agentic-jujutsu
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```js
|
|
196
|
+
import { open } from 'agenticow';
|
|
197
|
+
// @metaharness/jujutsu wires these two planes behind DualStateBridge:
|
|
198
|
+
const base = open('reasoning-bank.rvf', { dimension: 1536 });
|
|
199
|
+
const agentMem = base.fork('agent-007'); // memory branch (spawn)
|
|
200
|
+
agentMem.checkpoint('spawn');
|
|
201
|
+
agentMem.ingest(embeddedTrajectory); // learn
|
|
202
|
+
// if the trajectory scores poorly:
|
|
203
|
+
agentMem.rollback(/* spawn checkpoint id */); // revert โ code revert via `jj undo`
|
|
204
|
+
// if it wins:
|
|
205
|
+
agentMem.promote(base); // merge โ ops via `jj squash`
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Honest status (ADR-202):** spawn / learn / revert / merge are **wired
|
|
209
|
+
end-to-end** with both real native planes. **Cross-branch ANN query is stubbed**
|
|
210
|
+
behind a port โ agenticow's exact read-through answers it correctly but
|
|
211
|
+
unaccelerated across the COW boundary; the native single-index-across-the-branch
|
|
212
|
+
lands with [ruvnet/RuVector PR #617](https://github.com/ruvnet/RuVector/pull/617),
|
|
213
|
+
at which point only the adapter swaps.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
90
217
|
## How copy-on-write for vectors works
|
|
91
218
|
|
|
92
219
|

|
|
@@ -144,6 +271,29 @@ The acceptance test builds a brute-force ground truth (`base โช branch-inserts
|
|
|
144
271
|
|
|
145
272
|
\* **Honest concession.** On SIFT-1M, same machine, the underlying [ruvector](https://github.com/ruvnet/RuVector) HNSW does ~2,197 QPS @ recall 0.95 vs hnswlib-node ~9,344 QPS โ roughly **2.7ร slower** for raw ANN. If you need maximum raw similarity-search speed on a static index, use a dedicated ANN library. agenticow's edge is **cheap branching, checkpointing and rollback of agent memory** โ which none of the above have.
|
|
146
273
|
|
|
274
|
+
### Performance ยท storage ยท cost at scale
|
|
275
|
+
|
|
276
|
+
**Scenario: 1,000 branches over a 1M-vector base** (dim 128, ~496 MB base). agenticow figures are **measured** on an AMD Ryzen 9 9950X; competitor figures are **published / estimated** (sources below) and labeled as such โ not fabricated.
|
|
277
|
+
|
|
278
|
+
| Approach | Branch / snapshot create | Per-branch storage | Query latency (ANN) | Cost @ 1,000 branches | Native COW / rollback |
|
|
279
|
+
|---|---|---|---|---|---|
|
|
280
|
+
| **agenticow (COW)** | 0.47 ms / 162 B *(measured, flat to 1M)* | ~10.8 KB *(measured)* | ~2.7ร behind hnswlib *(measured\*)* | ~507 MB local ยท **~$0** infra (embedded) *(measuredโ )* | โ
instant (p50 0.57 ms) |
|
|
281
|
+
| Naive full-copy | 67 ms / 496 MB *(measured @1M)* | full base (~496 MB) | = source engine | ~484 GB local *(measured รN)* | โ |
|
|
282
|
+
| Pinecone (serverless) | no native branch โ full re-upsert | full copy (managed) | fast (core strength) | ~484 GB โ **$160/mo** storage + units *(est.ยน)* | โ |
|
|
283
|
+
| Milvus | snapshot = full copy / reindex | full copy | fast (core strength) | ~484 GB resident โ large cluster, **$$$/mo** *(est.ยฒ)* | โ |
|
|
284
|
+
| Qdrant | snapshot = full copy | full copy | fast (core strength) | ~484 GB โ managed/self-host, **$$$/mo** *(est.ยณ)* | โ |
|
|
285
|
+
| pgvector | SQL dump + reindex | full copy | moderate | ~484 GB in Postgres *(est.)* | โ |
|
|
286
|
+
| Chroma | full collection copy | full copy | moderate | ~484 GB local/managed *(est.)* | โ |
|
|
287
|
+
| lakeFS / DVC | fast metadata branch *(their strength)* | file-level delta (cheap) | n/a โ not a vector engine | cheap branching, but you still build/serve the ANN index yourself *(published)* | โ
data/files ยท โ vector index |
|
|
288
|
+
|
|
289
|
+
**Takeaway:** agenticow wins on branch-create speed, per-branch storage, and multi-branch cost, and is the only option with native COW branching + instant rollback of a live vector memory. It **concedes raw ANN search speed** to the dedicated vector DBs โ use those when single-index query throughput is the priority, and agenticow when you need cheap branching, checkpointing, and rollback of agent memory.
|
|
290
|
+
|
|
291
|
+
<sub>\* SIFT-1M same-machine (above). โ base ~496 MB + 1,000 ร ~10.8 KB โ 507 MB, in-process. ยน est. from [pinecone.io/pricing](https://www.pinecone.io/pricing/) (~$0.33/GB-mo storage, excl. read/write units). ยฒ est. from [zilliz.com/pricing](https://zilliz.com/pricing). ยณ est. from [qdrant.tech/pricing](https://qdrant.tech/pricing/). Competitor figures are published/estimated; only agenticow's are measured.</sub>
|
|
292
|
+
|
|
293
|
+
The [live site](https://ruvnet.github.io/agenticow/#bench) is mobile-friendly (responsive layout, horizontally-scrollable tables):
|
|
294
|
+
|
|
295
|
+
<img src="./assets/mobile-hero.png" width="280" alt="agenticow on mobile (375px width)" />
|
|
296
|
+
|
|
147
297
|
---
|
|
148
298
|
|
|
149
299
|
## Honest scope
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# agenticow examples
|
|
2
|
+
|
|
3
|
+
Runnable, optimized examples โ one per core use case. They import the library
|
|
4
|
+
from `../src/index.js` so they run against this repo with zero setup; in your own
|
|
5
|
+
project, import from `agenticow` instead.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
node examples/personalization.mjs
|
|
9
|
+
node examples/rollback-quarantine.mjs
|
|
10
|
+
node examples/checkpointing.mjs
|
|
11
|
+
node examples/git-workflow.mjs
|
|
12
|
+
node examples/ab-branches.mjs
|
|
13
|
+
node examples/parallel-agents.mjs
|
|
14
|
+
# or all of them:
|
|
15
|
+
npm run examples
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Vectors are generated by a seeded RNG (`_shared.mjs`) so the **id/order/recall
|
|
19
|
+
outputs are deterministic**; only the timings vary per machine. Outputs below
|
|
20
|
+
were captured on an AMD Ryzen 9 9950X, Node v22.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
### `personalization.mjs` โ one base, a cheap branch per user
|
|
25
|
+
|
|
26
|
+
A shared base + an isolated COW branch per user. Private edits stay private;
|
|
27
|
+
storage is delta-only.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
base: 10,000 vectors, 2.52 MB
|
|
31
|
+
branched 50 users in 58.55 ms (1.176 ms/user) โ total delta 42.2 KB (0.84 KB/user)
|
|
32
|
+
vs 50 full copies of the base: 125.91 MB โ 3,056x less storage
|
|
33
|
+
isolation: user-0 sees own pref (id 900000)=true, user-1's pref leaks to user-0=false
|
|
34
|
+
read-through: user-7 top hit for base vector #42 = id 42 (from base)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### `rollback-quarantine.mjs` โ quarantine a poisoned ingest
|
|
38
|
+
|
|
39
|
+
An untrusted agent ingests 100 unvetted vectors into a sandbox branch; discard
|
|
40
|
+
the branch โ the base is instantly clean. No re-index, no restore.
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
base: 2000 trusted vectors
|
|
44
|
+
agent ingested 100 unvetted vectors into its sandbox branch
|
|
45
|
+
before: query near poison[0] -> id 666000 (poison present in sandbox = true)
|
|
46
|
+
discarded branch in 0.256 ms โ poison present in base = false
|
|
47
|
+
base intact: 2000 vectors, base vector #1 still found = true
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### `checkpointing.mjs` โ zero-cost checkpoints + crash recovery without replay
|
|
51
|
+
|
|
52
|
+
Checkpoint every 10 steps; crash at step 31; roll back to step 30. The 30 steps
|
|
53
|
+
are NOT replayed โ they live in the 162-byte checkpoint.
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
checkpoint @ step 10: id 93b1eb356aโฆ (162 B, depth 1)
|
|
57
|
+
checkpoint @ step 20: id 8169723119โฆ (162 B, depth 2)
|
|
58
|
+
checkpoint @ step 30: id b39dc86be8โฆ (162 B, depth 3)
|
|
59
|
+
step 31 ran (partial work present = true) ... ๐ฅ simulated crash
|
|
60
|
+
recovered to step-30 in 0.731 ms (re-ingests during recovery: 0)
|
|
61
|
+
after recovery: step 31 gone = true, step 15 present = true, step 30 present = true
|
|
62
|
+
total ingest() calls across the whole run: 31 (31 steps, no replay)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `git-workflow.mjs` โ agent โ test โ prod memory pipeline
|
|
66
|
+
|
|
67
|
+
Branch a feature off production, ingest/override/delete in it, review with
|
|
68
|
+
`diff()`, then `promote()` the vetted delta into prod. Prod is untouched until
|
|
69
|
+
you promote.
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
prod: 2 vectors
|
|
73
|
+
diff(feature): +added=[100,101] ~overridden=[2] -deleted=[1]
|
|
74
|
+
prod before promote: id 1 present = true, feature id 100 present = false
|
|
75
|
+
promote โ 3 vectors merged, 1 tombstoned into prod
|
|
76
|
+
prod after promote: id 100 present = true, id 1 retracted = true
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### `ab-branches.mjs` โ N variant branches, score each, promote the winner
|
|
80
|
+
|
|
81
|
+
A/B / Darwin-style: fork variant branches, score each candidate against a
|
|
82
|
+
target, promote only the winner. Losing branches are discarded for free.
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
base: 1000 vectors; scoring 8 variant branches against a target
|
|
86
|
+
variant-7: candidate distance to target = 0.0000
|
|
87
|
+
variant-6: candidate distance to target = 0.0179
|
|
88
|
+
variant-5: candidate distance to target = 0.0568
|
|
89
|
+
variant-4: candidate distance to target = 0.3386
|
|
90
|
+
variant-3: candidate distance to target = 0.4558
|
|
91
|
+
variant-2: candidate distance to target = 0.5061
|
|
92
|
+
variant-0: candidate distance to target = 0.9004
|
|
93
|
+
variant-1: candidate distance to target = 1.1748
|
|
94
|
+
winner: variant-7 (dist 0.0000)
|
|
95
|
+
promoted variant-7 โ 1 vector merged; winner candidate now in base = true
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### `parallel-agents.mjs` โ fan out, query, roll one back
|
|
99
|
+
|
|
100
|
+
Fork N agent branches off a base, each with private memory + tombstones, query
|
|
101
|
+
each (read-through), and roll one back after a bad ingest.
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
base: 2000 vectors, 516 KB
|
|
105
|
+
forked 8 agent branches off the shared base
|
|
106
|
+
agent-0: top-5 = 5 base + 0 private (e.g. id 1503 @ 0.546)
|
|
107
|
+
...
|
|
108
|
+
agent-0 before rollback: hallucination present = true
|
|
109
|
+
agent-0 after rollback: hallucination present = false (base intact)
|
|
110
|
+
done.
|
|
111
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Shared helpers for the examples โ a deterministic RNG so outputs are
|
|
2
|
+
// reproducible, plus a tiny timer. Examples import the library from the local
|
|
3
|
+
// source so they run against this repo with zero setup:
|
|
4
|
+
//
|
|
5
|
+
// in your own project: import { open } from 'agenticow';
|
|
6
|
+
// in this repo: import { open } from '../src/index.js';
|
|
7
|
+
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
// mulberry32 โ small, fast, seedable PRNG (deterministic example output).
|
|
13
|
+
export function rng(seed = 0x9e3779b9) {
|
|
14
|
+
let a = seed >>> 0;
|
|
15
|
+
return () => {
|
|
16
|
+
a |= 0; a = (a + 0x6d2b79f5) | 0;
|
|
17
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
18
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
19
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function vecFactory(dim, seed) {
|
|
24
|
+
const r = rng(seed);
|
|
25
|
+
return () => Float32Array.from({ length: dim }, () => r() * 2 - 1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function tmpdir(tag) {
|
|
29
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), `agenticow-${tag}-`));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const ms = (t0) => `${(performance.now() - t0).toFixed(3)} ms`;
|
|
33
|
+
export const kb = (b) => `${(b / 1024).toFixed(1)} KB`;
|
|
34
|
+
export const mb = (b) => `${(b / 1024 / 1024).toFixed(2)} MB`;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// ab-branches.mjs โ N variant branches off one base, score each, promote winner.
|
|
2
|
+
//
|
|
3
|
+
// Demonstrates A/B testing / Darwin-style evolution of agent memory: fork many
|
|
4
|
+
// variant branches off a shared base, give each a different candidate memory,
|
|
5
|
+
// score each variant against a target query, and promote() only the winner back
|
|
6
|
+
// into the base. Losing branches are discarded for free.
|
|
7
|
+
//
|
|
8
|
+
// Run: node examples/ab-branches.mjs
|
|
9
|
+
//
|
|
10
|
+
// โโ verified output โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
11
|
+
// (see examples/README.md)
|
|
12
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { open } from '../src/index.js'; // in your project: from 'agenticow'
|
|
17
|
+
import { vecFactory, tmpdir } from './_shared.mjs';
|
|
18
|
+
|
|
19
|
+
const DIM = 32;
|
|
20
|
+
const VARIANTS = 8;
|
|
21
|
+
const dir = tmpdir('ab');
|
|
22
|
+
const vec = vecFactory(DIM, 31);
|
|
23
|
+
|
|
24
|
+
// 1. Base memory + a "target" we want a variant to match well.
|
|
25
|
+
const base = open(path.join(dir, 'base.rvf'), { dimension: DIM });
|
|
26
|
+
const flat = new Float32Array(1000 * DIM);
|
|
27
|
+
for (let i = 0; i < 1000; i++) flat.set(vec(), i * DIM);
|
|
28
|
+
base.ingest(flat, Array.from({ length: 1000 }, (_, i) => i));
|
|
29
|
+
const target = vec();
|
|
30
|
+
console.log(`base: ${base.status().totalVectors} vectors; scoring ${VARIANTS} variant branches against a target`);
|
|
31
|
+
|
|
32
|
+
// 2. Fork one branch per variant; each ingests a candidate vector (id 5000).
|
|
33
|
+
// Variant v's candidate is a blend of the target and noise โ higher v = closer.
|
|
34
|
+
const variants = [];
|
|
35
|
+
for (let v = 0; v < VARIANTS; v++) {
|
|
36
|
+
const br = base.fork(`variant-${v}`);
|
|
37
|
+
const blend = Float32Array.from(target, (x) => x * (v / (VARIANTS - 1)) + (vec()[0] * (1 - v / (VARIANTS - 1))));
|
|
38
|
+
br.ingest([{ id: 5000, vector: blend }]);
|
|
39
|
+
variants.push(br);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 3. Score each variant: distance of its candidate (id 5000) to the target.
|
|
43
|
+
// Use k > base size so the candidate is always included in the read-through.
|
|
44
|
+
const scores = variants.map((br, v) => {
|
|
45
|
+
const hit = br.query(target, 1001).find((h) => h.id === 5000);
|
|
46
|
+
return { v, dist: hit ? hit.distance : Infinity };
|
|
47
|
+
});
|
|
48
|
+
scores.sort((a, b) => a.dist - b.dist);
|
|
49
|
+
for (const s of scores) console.log(` variant-${s.v}: candidate distance to target = ${s.dist.toFixed(4)}`);
|
|
50
|
+
const winner = scores[0];
|
|
51
|
+
console.log(`winner: variant-${winner.v} (dist ${winner.dist.toFixed(4)})`);
|
|
52
|
+
|
|
53
|
+
// 4. Promote the winner into the base; discard the rest for free.
|
|
54
|
+
const r = variants[winner.v].promote(base);
|
|
55
|
+
const inBase = base.query(target, 5).some((h) => h.id === 5000);
|
|
56
|
+
console.log(`promoted variant-${winner.v} โ ${r.ingested} vector merged; winner candidate now in base = ${inBase}`);
|
|
57
|
+
|
|
58
|
+
base.close();
|
|
59
|
+
variants.forEach((br) => br.close());
|
|
60
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// checkpointing.mjs โ zero-cost checkpoints + crash recovery without replay.
|
|
2
|
+
//
|
|
3
|
+
// Demonstrates: an agent loop that checkpoints memory every 10 steps. A failure
|
|
4
|
+
// hits at step 31; we roll back to the step-30 checkpoint and resume. The first
|
|
5
|
+
// 30 steps are NOT replayed โ they live in the 162-byte checkpoint and are
|
|
6
|
+
// already visible via read-through.
|
|
7
|
+
//
|
|
8
|
+
// Run: node examples/checkpointing.mjs
|
|
9
|
+
//
|
|
10
|
+
// โโ verified output โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
11
|
+
// (see examples/README.md)
|
|
12
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { open } from '../src/index.js'; // in your project: from 'agenticow'
|
|
17
|
+
import { vecFactory, tmpdir, ms } from './_shared.mjs';
|
|
18
|
+
|
|
19
|
+
const DIM = 32;
|
|
20
|
+
const dir = tmpdir('checkpoint');
|
|
21
|
+
const vec = vecFactory(DIM, 11);
|
|
22
|
+
|
|
23
|
+
const mem = open(path.join(dir, 'agent.rvf'), { dimension: DIM });
|
|
24
|
+
const stored = new Map(); // step -> vector, to verify read-through later
|
|
25
|
+
|
|
26
|
+
let ingestCalls = 0;
|
|
27
|
+
function step(n) {
|
|
28
|
+
const v = vec();
|
|
29
|
+
mem.ingest([{ id: n, vector: v }]);
|
|
30
|
+
stored.set(n, v);
|
|
31
|
+
ingestCalls++;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Run steps 1..30, checkpoint at 10, 20, 30.
|
|
35
|
+
const checkpoints = {};
|
|
36
|
+
for (let n = 1; n <= 30; n++) {
|
|
37
|
+
step(n);
|
|
38
|
+
if (n % 10 === 0) {
|
|
39
|
+
checkpoints[n] = mem.checkpoint(`step-${n}`);
|
|
40
|
+
console.log(`checkpoint @ step ${n}: id ${checkpoints[n].id.slice(0, 10)}โฆ (162 B, depth ${checkpoints[n].depth})`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Step 31 does some work, then "crashes".
|
|
45
|
+
step(31);
|
|
46
|
+
const crashedHasGarbage = mem.query(stored.get(31), 1)[0].id === 31;
|
|
47
|
+
console.log(`step 31 ran (partial work present = ${crashedHasGarbage}) ... ๐ฅ simulated crash`);
|
|
48
|
+
|
|
49
|
+
// Recover: roll back to the step-30 checkpoint.
|
|
50
|
+
const callsBefore = ingestCalls;
|
|
51
|
+
const t0 = performance.now();
|
|
52
|
+
mem.rollback(checkpoints[30].id);
|
|
53
|
+
const recoverMs = ms(t0);
|
|
54
|
+
|
|
55
|
+
// The 30 steps are intact WITHOUT replay (ingestCalls unchanged), step 31 gone.
|
|
56
|
+
const step31Gone = mem.query(stored.get(31), 5).every((h) => h.id !== 31);
|
|
57
|
+
const step15Present = mem.query(stored.get(15), 1)[0].id === 15;
|
|
58
|
+
const step30Present = mem.query(stored.get(30), 1)[0].id === 30;
|
|
59
|
+
console.log(`recovered to step-30 in ${recoverMs} (re-ingests during recovery: ${ingestCalls - callsBefore})`);
|
|
60
|
+
console.log(`after recovery: step 31 gone = ${step31Gone}, step 15 present = ${step15Present}, step 30 present = ${step30Present}`);
|
|
61
|
+
console.log(`total ingest() calls across the whole run: ${ingestCalls} (31 steps, no replay)`);
|
|
62
|
+
|
|
63
|
+
mem.close();
|
|
64
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// git-workflow.mjs โ agent โ test โ prod memory pipeline (branch/diff/promote).
|
|
2
|
+
//
|
|
3
|
+
// Demonstrates the Git-style workflow for vector memory: branch a feature off
|
|
4
|
+
// production, ingest into it, review the change with diff(), then promote() the
|
|
5
|
+
// vetted delta back into production. Production is untouched until you promote.
|
|
6
|
+
//
|
|
7
|
+
// Run: node examples/git-workflow.mjs
|
|
8
|
+
//
|
|
9
|
+
// โโ verified output โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
10
|
+
// (see examples/README.md)
|
|
11
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { open } from '../src/index.js'; // in your project: from 'agenticow'
|
|
16
|
+
import { vecFactory, tmpdir } from './_shared.mjs';
|
|
17
|
+
|
|
18
|
+
const DIM = 32;
|
|
19
|
+
const dir = tmpdir('git-workflow');
|
|
20
|
+
const vec = vecFactory(DIM, 23);
|
|
21
|
+
|
|
22
|
+
// 1. Production memory.
|
|
23
|
+
const prod = open(path.join(dir, 'prod.rvf'), { dimension: DIM });
|
|
24
|
+
const v1 = vec(); const v2 = vec();
|
|
25
|
+
prod.ingest([{ id: 1, vector: v1 }, { id: 2, vector: v2 }]);
|
|
26
|
+
console.log(`prod: ${prod.status().totalVectors} vectors`);
|
|
27
|
+
|
|
28
|
+
// 2. Branch a feature, ingest new memories, override one, delete one.
|
|
29
|
+
const feature = prod.branch('feature/new-facts');
|
|
30
|
+
const f100 = vec(); const f101 = vec(); const v2new = vec();
|
|
31
|
+
feature.ingest([{ id: 100, vector: f100 }, { id: 101, vector: f101 }]);
|
|
32
|
+
feature.ingest([{ id: 2, vector: v2new }]); // override an existing prod id
|
|
33
|
+
feature.delete([1]); // retract a prod fact in the branch
|
|
34
|
+
|
|
35
|
+
// 3. Review the change set before merging.
|
|
36
|
+
const d = feature.diff();
|
|
37
|
+
console.log(`diff(feature): +added=${JSON.stringify(d.added)} ~overridden=${JSON.stringify(d.overridden)} -deleted=${JSON.stringify(d.deleted)}`);
|
|
38
|
+
|
|
39
|
+
// prod is still untouched at this point (feature edits are isolated):
|
|
40
|
+
const id100InProdBefore = prod.query(f100, 1)[0].id === 100;
|
|
41
|
+
console.log(`prod before promote: id 1 present = ${prod.query(v1, 1)[0].id === 1}, feature id 100 present = ${id100InProdBefore}`);
|
|
42
|
+
|
|
43
|
+
// 4. Promote the vetted delta into production.
|
|
44
|
+
const r = feature.promote(prod);
|
|
45
|
+
console.log(`promote โ ${r.ingested} vectors merged, ${r.deleted} tombstoned into prod`);
|
|
46
|
+
const id100InProd = prod.query(f100, 1)[0].id === 100;
|
|
47
|
+
const id1Retracted = prod.query(v1, 5).every((h) => h.id !== 1);
|
|
48
|
+
console.log(`prod after promote: id 100 present = ${id100InProd}, id 1 retracted = ${id1Retracted}`);
|
|
49
|
+
|
|
50
|
+
prod.close();
|
|
51
|
+
feature.close();
|
|
52
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import fs from 'node:fs';
|
|
11
11
|
import os from 'node:os';
|
|
12
12
|
import path from 'node:path';
|
|
13
|
-
import { open } from '
|
|
13
|
+
import { open } from '../src/index.js'; // in your project: from 'agenticow'
|
|
14
14
|
|
|
15
15
|
const DIM = 64;
|
|
16
16
|
const N_AGENTS = 8;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// personalization.mjs โ one shared base memory, a cheap COW branch per user.
|
|
2
|
+
//
|
|
3
|
+
// Demonstrates: per-user/per-account personalization without copying the base.
|
|
4
|
+
// Each user gets an isolated branch (their edits stay private) while still
|
|
5
|
+
// reading through to the shared base. Storage is delta-only (KB/user), not a
|
|
6
|
+
// full copy of the base (MB/user).
|
|
7
|
+
//
|
|
8
|
+
// Run: node examples/personalization.mjs
|
|
9
|
+
//
|
|
10
|
+
// โโ verified output โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
11
|
+
// (see examples/README.md โ outputs are deterministic via a seeded RNG)
|
|
12
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { open } from '../src/index.js'; // in your project: from 'agenticow'
|
|
17
|
+
import { vecFactory, tmpdir, ms, kb, mb } from './_shared.mjs';
|
|
18
|
+
|
|
19
|
+
const DIM = 64;
|
|
20
|
+
const USERS = 50;
|
|
21
|
+
const dir = tmpdir('personalization');
|
|
22
|
+
const vec = vecFactory(DIM, 1);
|
|
23
|
+
|
|
24
|
+
// 1. One shared base memory (e.g. the org-wide knowledge base).
|
|
25
|
+
const base = open(path.join(dir, 'base.rvf'), { dimension: DIM });
|
|
26
|
+
const N = 10_000;
|
|
27
|
+
const flat = new Float32Array(N * DIM);
|
|
28
|
+
const ids = [];
|
|
29
|
+
for (let i = 0; i < N; i++) { flat.set(vec(), i * DIM); ids.push(i); }
|
|
30
|
+
base.ingest(flat, ids);
|
|
31
|
+
const baseBytes = base.status().fileSize;
|
|
32
|
+
console.log(`base: ${N.toLocaleString()} vectors, ${mb(baseBytes)}`);
|
|
33
|
+
|
|
34
|
+
// 2. A cheap COW branch per user โ fork() off the read-only base.
|
|
35
|
+
// Each user adds one private preference vector (id 900000 + u).
|
|
36
|
+
const t0 = performance.now();
|
|
37
|
+
const users = [];
|
|
38
|
+
for (let u = 0; u < USERS; u++) {
|
|
39
|
+
const ub = base.fork(`user-${u}`);
|
|
40
|
+
ub.ingest([{ id: 900000 + u, vector: vec() }]);
|
|
41
|
+
users.push(ub);
|
|
42
|
+
}
|
|
43
|
+
const forkWall = ms(t0);
|
|
44
|
+
let deltaBytes = 0;
|
|
45
|
+
for (const u of users) deltaBytes += fs.statSync(u.lineage()[0].path).size;
|
|
46
|
+
const fullCopyBytes = baseBytes * USERS;
|
|
47
|
+
console.log(`branched ${USERS} users in ${forkWall} (${((performance.now() - t0) / USERS).toFixed(3)} ms/user) โ ` +
|
|
48
|
+
`total delta ${kb(deltaBytes)} (${(deltaBytes / USERS / 1024).toFixed(2)} KB/user)`);
|
|
49
|
+
console.log(`vs ${USERS} full copies of the base: ${mb(fullCopyBytes)} โ ` +
|
|
50
|
+
`${Math.round(fullCopyBytes / deltaBytes).toLocaleString()}x less storage`);
|
|
51
|
+
|
|
52
|
+
// 3. Isolation: user-0 sees its own preference; user-1's stays private.
|
|
53
|
+
const u0SeesOwn = users[0].diff().added.includes(900000);
|
|
54
|
+
const u1LeaksToU0 = users[0]
|
|
55
|
+
.query(flat.slice(0, DIM), USERS)
|
|
56
|
+
.some((h) => h.id === 900001);
|
|
57
|
+
console.log(`isolation: user-0 sees own pref (id 900000)=${u0SeesOwn}, user-1's pref leaks to user-0=${u1LeaksToU0}`);
|
|
58
|
+
|
|
59
|
+
// 4. Read-through: every user still queries the shared base.
|
|
60
|
+
const probe = flat.slice(42 * DIM, 43 * DIM);
|
|
61
|
+
const hit = users[7].query(probe, 1)[0];
|
|
62
|
+
console.log(`read-through: user-7 top hit for base vector #42 = id ${hit.id} (from ${hit.branch})`);
|
|
63
|
+
|
|
64
|
+
base.close();
|
|
65
|
+
users.forEach((u) => u.close());
|
|
66
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// rollback-quarantine.mjs โ quarantine a poisoned ingest, then discard it.
|
|
2
|
+
//
|
|
3
|
+
// Demonstrates: an agent ingests "hallucinated" / adversarial vectors into a
|
|
4
|
+
// branch. You detect them, then throw the branch away โ the shared base is
|
|
5
|
+
// instantly clean. No re-indexing, no restore-from-backup. Shows the query
|
|
6
|
+
// result before (poison visible in the branch) and after (gone, base intact).
|
|
7
|
+
//
|
|
8
|
+
// Run: node examples/rollback-quarantine.mjs
|
|
9
|
+
//
|
|
10
|
+
// โโ verified output โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
11
|
+
// (see examples/README.md)
|
|
12
|
+
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { open } from '../src/index.js'; // in your project: from 'agenticow'
|
|
17
|
+
import { vecFactory, tmpdir, ms } from './_shared.mjs';
|
|
18
|
+
|
|
19
|
+
const DIM = 48;
|
|
20
|
+
const dir = tmpdir('quarantine');
|
|
21
|
+
const vec = vecFactory(DIM, 7);
|
|
22
|
+
|
|
23
|
+
// 1. Trusted base memory.
|
|
24
|
+
const base = open(path.join(dir, 'base.rvf'), { dimension: DIM });
|
|
25
|
+
const known = Array.from({ length: 2000 }, () => vec());
|
|
26
|
+
const flat = new Float32Array(2000 * DIM);
|
|
27
|
+
known.forEach((v, i) => flat.set(v, i * DIM));
|
|
28
|
+
base.ingest(flat, known.map((_, i) => i));
|
|
29
|
+
console.log(`base: ${base.status().totalVectors} trusted vectors`);
|
|
30
|
+
|
|
31
|
+
// 2. An untrusted agent works in a sandbox branch and ingests poison.
|
|
32
|
+
const sandbox = base.fork('untrusted-agent');
|
|
33
|
+
const sandboxPath = sandbox.lineage()[0].path; // capture before we close it
|
|
34
|
+
const poison = Array.from({ length: 100 }, () => vec());
|
|
35
|
+
const POISON_IDS = poison.map((_, i) => 666000 + i);
|
|
36
|
+
poison.forEach((v, i) => sandbox.ingest([{ id: POISON_IDS[i], vector: v }]));
|
|
37
|
+
console.log(`agent ingested ${POISON_IDS.length} unvetted vectors into its sandbox branch`);
|
|
38
|
+
|
|
39
|
+
// 3. Detection: query near a poisoned vector โ it should surface in the sandbox.
|
|
40
|
+
const detect = sandbox.query(poison[0], 1)[0];
|
|
41
|
+
const poisonPresent = POISON_IDS.includes(detect.id);
|
|
42
|
+
console.log(`before: query near poison[0] -> id ${detect.id} (poison present in sandbox = ${poisonPresent})`);
|
|
43
|
+
|
|
44
|
+
// 4. Quarantine = discard the branch. The base never saw the poison.
|
|
45
|
+
const t0 = performance.now();
|
|
46
|
+
sandbox.close();
|
|
47
|
+
fs.rmSync(sandboxPath, { force: true });
|
|
48
|
+
const discardMs = ms(t0);
|
|
49
|
+
const poisonInBase = POISON_IDS.includes(base.query(poison[0], 1)[0].id);
|
|
50
|
+
console.log(`discarded branch in ${discardMs} โ poison present in base = ${poisonInBase}`);
|
|
51
|
+
console.log(`base intact: ${base.status().totalVectors} vectors, base vector #1 still found = ${base.query(known[1], 1)[0].id === 1}`);
|
|
52
|
+
|
|
53
|
+
base.close();
|
|
54
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agenticow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Git for Agent Memory: Copy-On-Write vector branching for embedded multi-agent memory. Branch a base memory in ~0.5ms / 162 bytes regardless of base size โ 83x faster, 3000x smaller than full-copy snapshots. Exact read-through queries (parent โช edits, child wins).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -27,6 +27,13 @@
|
|
|
27
27
|
"bench": "node bench/bench.js",
|
|
28
28
|
"acceptance": "node bench/acceptance.js",
|
|
29
29
|
"demo": "node bin/agenticow.js demo",
|
|
30
|
+
"examples": "for f in personalization rollback-quarantine checkpointing git-workflow ab-branches parallel-agents; do echo \"\\nโโ $f โโ\"; node examples/$f.mjs; done",
|
|
31
|
+
"example:personalization": "node examples/personalization.mjs",
|
|
32
|
+
"example:rollback": "node examples/rollback-quarantine.mjs",
|
|
33
|
+
"example:checkpointing": "node examples/checkpointing.mjs",
|
|
34
|
+
"example:git-workflow": "node examples/git-workflow.mjs",
|
|
35
|
+
"example:ab-branches": "node examples/ab-branches.mjs",
|
|
36
|
+
"example:parallel-agents": "node examples/parallel-agents.mjs",
|
|
30
37
|
"test": "node --test test/*.test.js"
|
|
31
38
|
},
|
|
32
39
|
"keywords": [
|
package/src/index.js
CHANGED
|
@@ -74,7 +74,7 @@ class Node {
|
|
|
74
74
|
|
|
75
75
|
export class AgenticMemory {
|
|
76
76
|
/** @private */
|
|
77
|
-
constructor(workingNode, ancestors, dim, metric, track = true) {
|
|
77
|
+
constructor(workingNode, ancestors, dim, metric, track = true, owned = null) {
|
|
78
78
|
/** @type {Node} */
|
|
79
79
|
this._working = workingNode;
|
|
80
80
|
/** @type {Node[]} ancestors newest -> oldest (base last) */
|
|
@@ -83,6 +83,10 @@ export class AgenticMemory {
|
|
|
83
83
|
this._metric = metric;
|
|
84
84
|
this._track = track;
|
|
85
85
|
this._normalize = String(metric).toLowerCase() === 'cosine';
|
|
86
|
+
// Nodes this instance is allowed to close. Ancestors shared from a parent
|
|
87
|
+
// (via fork/branch) are NOT owned, so closing a fork never closes the base.
|
|
88
|
+
/** @type {Set<Node>} */
|
|
89
|
+
this._owned = owned || new Set([workingNode]);
|
|
86
90
|
this._closed = false;
|
|
87
91
|
}
|
|
88
92
|
|
|
@@ -244,10 +248,12 @@ export class AgenticMemory {
|
|
|
244
248
|
const parentChildDb = frozen.db.derive(parentChildPath, this._deriveOpts());
|
|
245
249
|
const childPath = filePath || tmpChildPath(frozen.path, label);
|
|
246
250
|
const childDb = frozen.db.derive(childPath, this._deriveOpts());
|
|
247
|
-
// Parent continues, transparently, in its own fresh child.
|
|
251
|
+
// Parent continues, transparently, in its own fresh child (which it owns).
|
|
248
252
|
this._ancestors = [frozen, ...this._ancestors];
|
|
249
253
|
this._working = new Node(parentChildDb, parentChildPath, 'working');
|
|
250
|
-
|
|
254
|
+
this._owned.add(this._working);
|
|
255
|
+
// Branch shares the frozen snapshot + all older ancestors; it owns only its
|
|
256
|
+
// own working child.
|
|
251
257
|
const branchNode = new Node(childDb, childPath, label || 'branch');
|
|
252
258
|
return new AgenticMemory(branchNode, [...this._ancestors], this._dim, this._metric, this._track);
|
|
253
259
|
}
|
|
@@ -339,6 +345,7 @@ export class AgenticMemory {
|
|
|
339
345
|
const childNode = new Node(childDb, childPath, 'working');
|
|
340
346
|
this._ancestors = [frozen, ...this._ancestors];
|
|
341
347
|
this._working = childNode;
|
|
348
|
+
this._owned.add(childNode);
|
|
342
349
|
return {
|
|
343
350
|
id: frozen.id,
|
|
344
351
|
label: frozen.label,
|
|
@@ -364,18 +371,21 @@ export class AgenticMemory {
|
|
|
364
371
|
idx = this._ancestors.findIndex((n) => n.id === checkpointId);
|
|
365
372
|
if (idx === -1) throw new Error(`agenticow: checkpoint ${checkpointId} not found`);
|
|
366
373
|
}
|
|
367
|
-
// Discard the current poisoned working child and any checkpoints newer than
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
+
// Discard the current poisoned working child and any checkpoints newer than
|
|
375
|
+
// target โ but only ones THIS instance owns (never a shared ancestor).
|
|
376
|
+
const discarded = [this._working, ...this._ancestors.slice(0, idx)];
|
|
377
|
+
for (const n of discarded) {
|
|
378
|
+
if (!this._owned.has(n)) continue;
|
|
379
|
+
try { n.db.close(); } catch { /* ignore */ }
|
|
380
|
+
try { fs.rmSync(n.path, { force: true }); } catch { /* ignore */ }
|
|
381
|
+
this._owned.delete(n);
|
|
382
|
+
}
|
|
374
383
|
const target = this._ancestors[idx];
|
|
375
384
|
const newAncestors = this._ancestors.slice(idx); // target + older
|
|
376
385
|
const childPath = tmpChildPath(target.path, 'work');
|
|
377
386
|
const childDb = target.db.derive(childPath, this._deriveOpts());
|
|
378
387
|
this._working = new Node(childDb, childPath, 'working');
|
|
388
|
+
this._owned.add(this._working);
|
|
379
389
|
this._ancestors = newAncestors;
|
|
380
390
|
return { restoredTo: target.id, depth: this._ancestors.length };
|
|
381
391
|
}
|
|
@@ -444,13 +454,14 @@ export class AgenticMemory {
|
|
|
444
454
|
node.editVecs = new Map(Object.entries(nm.editVecs || {}).map(([id, v]) => [Number(id), Float32Array.from(v)]));
|
|
445
455
|
return node;
|
|
446
456
|
});
|
|
447
|
-
|
|
457
|
+
// A loaded instance opened every handle itself, so it owns the whole chain.
|
|
458
|
+
return new AgenticMemory(nodes[0], nodes.slice(1), m.dim, m.metric, m.track !== false, new Set(nodes));
|
|
448
459
|
}
|
|
449
460
|
|
|
450
|
-
/** Close
|
|
461
|
+
/** Close the handles this instance owns (never a shared parent/base handle). */
|
|
451
462
|
close() {
|
|
452
463
|
if (this._closed) return;
|
|
453
|
-
for (const n of this.
|
|
464
|
+
for (const n of this._owned) {
|
|
454
465
|
try { n.db.close(); } catch { /* ignore */ }
|
|
455
466
|
}
|
|
456
467
|
this._closed = true;
|