@vib795/agent-memory 0.7.2 → 0.7.4
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 +2 -2
- package/package.json +1 -1
- package/skills/remember/SKILL.md +9 -1
- package/src/cli.js +47 -3
- package/src/config.js +1 -0
- package/src/digest.js +15 -12
package/README.md
CHANGED
|
@@ -440,7 +440,7 @@ equivalent and is not: npm links the global install to that folder rather than c
|
|
|
440
440
|
it, which shows up as an arrow in `npm list -g`:
|
|
441
441
|
|
|
442
442
|
```
|
|
443
|
-
`-- @vib795/agent-memory@0.
|
|
443
|
+
`-- @vib795/agent-memory@0.7.4 -> .\..\..\..\agent-memory
|
|
444
444
|
```
|
|
445
445
|
|
|
446
446
|
Move or delete the clone afterwards and the global install points at nothing — the same
|
|
@@ -561,7 +561,7 @@ them as skipped, which is the intended outcome, not a failure.
|
|
|
561
561
|
|
|
562
562
|
Needs Node 22.5 or newer; `doctor` says so plainly if the version is too old.
|
|
563
563
|
|
|
564
|
-
Run `npm test` for the suite (
|
|
564
|
+
Run `npm test` for the suite (111 tests, no dependencies). CI runs it on Linux,
|
|
565
565
|
macOS and Windows across Node 22 and 24, and separately installs the packed tarball
|
|
566
566
|
and exercises it end to end on all three.
|
|
567
567
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vib795/agent-memory",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Durable cross-repo knowledge graph for GitHub Copilot and Claude Code. Markdown source of truth, disposable SQLite index, zero runtime dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github-copilot",
|
package/skills/remember/SKILL.md
CHANGED
|
@@ -223,7 +223,15 @@ Then stop. Do not summarize the conversation.
|
|
|
223
223
|
Warnings from `write` are worth surfacing verbatim:
|
|
224
224
|
|
|
225
225
|
- `title collision` means an existing note reads as the same thing under a different
|
|
226
|
-
id.
|
|
226
|
+
id. The warning now carries that note's type, title and body, so decide in **this
|
|
227
|
+
turn** — do not run `get` to fetch what you were already handed:
|
|
228
|
+
- **Same claim, better wording** — update the existing id with the fuller body. One
|
|
229
|
+
note, improved.
|
|
230
|
+
- **The claim changed** — set `supersedes` to the old id on your new note.
|
|
231
|
+
- **They genuinely disagree** — link them with `contradicts` and say so to the user.
|
|
232
|
+
|
|
233
|
+
Two notes making one claim is the thing compaction cannot repair for you: it merges
|
|
234
|
+
on identical content, and these are not identical, only synonymous.
|
|
227
235
|
- `redacted Nx <kind>` means the guard caught something. Say what kind was caught so
|
|
228
236
|
the user knows a secret was in play, never what the value was.
|
|
229
237
|
|
package/src/cli.js
CHANGED
|
@@ -400,6 +400,20 @@ function readNodesFrom(file) {
|
|
|
400
400
|
return [raw];
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
+
/**
|
|
404
|
+
* A readable slice of a note body, cut on a boundary rather than mid-word.
|
|
405
|
+
*
|
|
406
|
+
* Enough to decide whether two notes make the same claim, and no more: this rides
|
|
407
|
+
* inside a `write` response, which is not the place to reproduce a 4 KB note.
|
|
408
|
+
*/
|
|
409
|
+
function clip(body, limit) {
|
|
410
|
+
const text = String(body ?? '').trim();
|
|
411
|
+
if (text.length <= limit) return { text, truncated: false };
|
|
412
|
+
const head = text.slice(0, limit);
|
|
413
|
+
const cut = Math.max(head.lastIndexOf('\n'), head.lastIndexOf(' '));
|
|
414
|
+
return { text: (cut > limit / 2 ? head.slice(0, cut) : head).trimEnd(), truncated: true };
|
|
415
|
+
}
|
|
416
|
+
|
|
403
417
|
function cmdWrite(opts) {
|
|
404
418
|
const file = opts['from-json'];
|
|
405
419
|
if (!file || file === true || (file !== '-' && !existsSync(file))) {
|
|
@@ -432,6 +446,8 @@ function cmdWrite(opts) {
|
|
|
432
446
|
|
|
433
447
|
// Normalized titles of what already exists, so two agents naming one thing two
|
|
434
448
|
// different ways surface as a collision instead of quietly becoming two nodes.
|
|
449
|
+
// Bodies are deliberately not loaded here: a collision is rare, and paying for every
|
|
450
|
+
// body in the store to describe the one that collided is the wrong trade.
|
|
435
451
|
const titles = new Map();
|
|
436
452
|
for (const row of db.prepare('SELECT id, title, content_hash FROM nodes').all()) {
|
|
437
453
|
titles.set(normalizeTitle(row.title), { id: row.id, hash: row.content_hash });
|
|
@@ -440,6 +456,7 @@ function cmdWrite(opts) {
|
|
|
440
456
|
const written = [];
|
|
441
457
|
const failed = [];
|
|
442
458
|
const warnings = [];
|
|
459
|
+
const collisions = [];
|
|
443
460
|
for (const raw of incoming) {
|
|
444
461
|
const node = { ...raw };
|
|
445
462
|
node.source = node.source || (opts.source === true ? undefined : opts.source) || 'manual';
|
|
@@ -453,9 +470,25 @@ function cmdWrite(opts) {
|
|
|
453
470
|
try {
|
|
454
471
|
const res = writeNote(node, { selfEmail });
|
|
455
472
|
if (collision && collision.id !== res.node.id) {
|
|
473
|
+
// The id alone forced a second turn: deciding whether this is a duplicate to
|
|
474
|
+
// merge or a genuine contradiction needs the other note's words, and fetching
|
|
475
|
+
// them meant another `get`, which on a per-prompt biller is another request.
|
|
476
|
+
// One row, and only when a collision actually happened.
|
|
477
|
+
const other = getNodeRow(db, collision.id);
|
|
478
|
+
const excerpt = other ? clip(other.body, cfg.collisionBodyChars) : null;
|
|
479
|
+
collisions.push({
|
|
480
|
+
id: res.node.id,
|
|
481
|
+
existing: collision.id,
|
|
482
|
+
existingType: other?.type ?? null,
|
|
483
|
+
existingTitle: other?.title ?? null,
|
|
484
|
+
existingArchived: Boolean(other?.archived),
|
|
485
|
+
excerpt: excerpt?.text ?? null,
|
|
486
|
+
truncated: Boolean(excerpt?.truncated),
|
|
487
|
+
});
|
|
456
488
|
warnings.push(
|
|
457
|
-
`title collision: ${res.node.id} reads the same as existing ${collision.id}
|
|
458
|
-
|
|
489
|
+
`title collision: ${res.node.id} reads the same as existing ${collision.id}` +
|
|
490
|
+
(other ? ` [${other.type}] ${other.title}` : '') +
|
|
491
|
+
(other?.archived ? ' (archived)' : ''),
|
|
459
492
|
);
|
|
460
493
|
}
|
|
461
494
|
written.push({
|
|
@@ -479,9 +512,18 @@ function cmdWrite(opts) {
|
|
|
479
512
|
const compacted = maybeCompact(db, before, after, cfg);
|
|
480
513
|
db.close();
|
|
481
514
|
|
|
515
|
+
// Printed under the warning and indented, so it reads as evidence rather than as a
|
|
516
|
+
// second instruction competing with the first.
|
|
517
|
+
const collisionLines = collisions.flatMap((c) => [
|
|
518
|
+
...(c.excerpt ? c.excerpt.split('\n').map((l) => ` ${l}`) : []),
|
|
519
|
+
...(c.truncated ? [` ... agent-memory get ${c.existing} for the rest`] : []),
|
|
520
|
+
` -> update ${c.existing} by id, or set supersedes or contradicts on ${c.id}.`,
|
|
521
|
+
]);
|
|
522
|
+
|
|
482
523
|
const text = [
|
|
483
524
|
...written.map((w) => `${w.created ? 'created' : 'updated'} ${w.id} [${w.type}]`),
|
|
484
525
|
...warnings.map((w) => `warning: ${w}`),
|
|
526
|
+
...collisionLines,
|
|
485
527
|
...failed.map((f) => `failed ${f.id ?? '<no id>'}: ${f.errors.join('; ')}`),
|
|
486
528
|
written.length ? '' : 'No nodes written.',
|
|
487
529
|
compacted ? `compacted: ${compacted.indexed} notes indexed` : '',
|
|
@@ -489,7 +531,9 @@ function cmdWrite(opts) {
|
|
|
489
531
|
.filter(Boolean)
|
|
490
532
|
.join('\n');
|
|
491
533
|
|
|
492
|
-
return {
|
|
534
|
+
return {
|
|
535
|
+
ok: failed.length === 0, written, failed, warnings, collisions, compacted: !!compacted, text,
|
|
536
|
+
};
|
|
493
537
|
}
|
|
494
538
|
|
|
495
539
|
function cmdCompact() {
|
package/src/config.js
CHANGED
|
@@ -104,6 +104,7 @@ export const DEFAULTS = {
|
|
|
104
104
|
compactThreshold: 10, // node-count delta that triggers an automatic compact
|
|
105
105
|
briefRecentMinutes: 120, // window the capture brief calls already covered
|
|
106
106
|
briefRecentIds: 10, // ids printed from that window before the rest are counted
|
|
107
|
+
collisionBodyChars: 500, // excerpt returned with a title collision, so one turn can fix it
|
|
107
108
|
};
|
|
108
109
|
|
|
109
110
|
// Written by the installer: every SKILL.md whose description compact regenerates.
|
package/src/digest.js
CHANGED
|
@@ -3,22 +3,25 @@ import { createHash } from 'node:crypto';
|
|
|
3
3
|
import { captureGap, currentRepo } from './staleness.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Tiers 1 and 2 of a three-tier ladder.
|
|
7
7
|
*
|
|
8
|
-
* Tier 1 is
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Tier 1 is a skill description, loaded into every chat whether or not memory is ever
|
|
9
|
+
* used. It is standing cost, so it has to read like a description rather than a
|
|
10
|
+
* document. It has two occupants, not one: `recall`'s description advertises what the
|
|
11
|
+
* store knows; `remember`'s advertises what it is missing. Both are the same mechanism
|
|
12
|
+
* — a line of frontmatter that code regenerates and every conversation loads —
|
|
13
|
+
* pointed at opposite halves of the same problem.
|
|
11
14
|
*
|
|
12
|
-
* Tier 2 is
|
|
13
|
-
* cost, paid once, and only when someone
|
|
15
|
+
* Tier 2 is printed only when a skill actually fires: the tree for `recall`, the
|
|
16
|
+
* capture brief for `remember`. Per-invocation cost, paid once, and only when someone
|
|
17
|
+
* is already looking something up or about to write one down.
|
|
14
18
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
19
|
+
* Tier 3 — note bodies, and the colliding note that `write` hands back — lives
|
|
20
|
+
* outside this module, because by then the question is which note rather than which
|
|
21
|
+
* of them.
|
|
17
22
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* line of frontmatter that code regenerates and every conversation loads — pointed at
|
|
21
|
-
* opposite halves of the same problem.
|
|
23
|
+
* Neither tier here costs a premium request. A request is charged per prompt, not per
|
|
24
|
+
* tool call, so both of these ride inside a turn that was already paid for.
|
|
22
25
|
*/
|
|
23
26
|
|
|
24
27
|
// Never dropped from either tier. A constraint is what stops an agent from burning
|