@vib795/agent-memory 0.1.13 → 0.1.15
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/HOWTO.md +15 -0
- package/README.md +37 -4
- package/package.json +1 -1
- package/src/cli.js +22 -4
- package/src/config.js +1 -0
- package/src/digest.js +3 -0
- package/src/staleness.js +62 -0
package/HOWTO.md
CHANGED
|
@@ -170,6 +170,21 @@ You can also point it at something specific:
|
|
|
170
170
|
**Don't bother remembering** anything the code already says. If someone can find it by
|
|
171
171
|
opening a file, it does not need a note. Notes are for what lives in people's heads.
|
|
172
172
|
|
|
173
|
+
**You will also see it happen without you asking.** When a decision settles or a
|
|
174
|
+
constraint turns up mid-conversation, the assistant writes it down and tells you in
|
|
175
|
+
one line:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
captured 2 notes [decision, constraint]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Then it carries on with whatever you were actually asking about. This is on purpose:
|
|
182
|
+
the fact worth keeping is usually the one nobody stops to write down. It costs you
|
|
183
|
+
nothing — your allowance is charged per message you send, not per thing the assistant
|
|
184
|
+
does while answering — and you can see everything it captured with `/recall` or
|
|
185
|
+
`agent-memory tree`. If it ever writes down something you did not want, the notes are
|
|
186
|
+
plain markdown files you can edit or delete.
|
|
187
|
+
|
|
173
188
|
### `/handoff` — for moving work between windows
|
|
174
189
|
|
|
175
190
|
This one is not a chat summary. Summaries read nicely and still leave the next
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Three user-level Agent Skills over one local store:
|
|
|
16
16
|
| Skill | What it does |
|
|
17
17
|
|---|---|
|
|
18
18
|
| `/handoff` | Writes a portable working-state file so another window can pick up this thread |
|
|
19
|
-
| `/remember` | Captures durable knowledge into a cross-repo graph |
|
|
19
|
+
| `/remember` | Captures durable knowledge into a cross-repo graph — and fires on its own when a juncture passes |
|
|
20
20
|
| `/recall` | Answers from that graph before deriving anything again |
|
|
21
21
|
|
|
22
22
|
**New here? Read the [HOW-TO](HOWTO.md)** — install, first five minutes, and the
|
|
@@ -78,6 +78,14 @@ recursive CTE *is* the graph engine, and `UNION` plus a depth bound is what keep
|
|
|
78
78
|
- **Staleness is visible at the moment of use.** Every note records the repo HEAD it
|
|
79
79
|
was captured at. On recall, `get` prints `captured 47 commits ago — verify before
|
|
80
80
|
trusting`. A note that quietly rots is worse than no note.
|
|
81
|
+
- **An empty graph says so.** Staleness cannot tell you a repo has nothing in it —
|
|
82
|
+
a repo nobody captured in has no stale notes either, so it reads as fully covered.
|
|
83
|
+
`tree` and `doctor` print `47 commits since anything was captured here` instead.
|
|
84
|
+
Silence has to mean covered, never empty.
|
|
85
|
+
- **Capture does not wait to be asked.** The agent invokes `/remember` itself when a
|
|
86
|
+
decision settles, a constraint surfaces or a root cause is found, and says so in one
|
|
87
|
+
line. Judged by the model in the turn where the context still exists — never on a
|
|
88
|
+
timer or a tool count, because volume is what makes a graph useless.
|
|
81
89
|
- **Redaction is fail-closed.** Keys, tokens, connection strings, private keys,
|
|
82
90
|
session cookies, internal hosts and foreign email addresses are replaced before any
|
|
83
91
|
byte reaches disk — not to a note, not to a temp file, not to the index.
|
|
@@ -336,7 +344,21 @@ Mid-session, when something worth keeping has been established:
|
|
|
336
344
|
```
|
|
337
345
|
|
|
338
346
|
Bare, it selects the durable knowledge itself. With an argument, it writes that and
|
|
339
|
-
nothing else.
|
|
347
|
+
nothing else.
|
|
348
|
+
|
|
349
|
+
You will also see it fire without being asked, when a decision settles, a constraint
|
|
350
|
+
surfaces, a root cause is found or a convention is agreed. It reports one line at the
|
|
351
|
+
end of whatever it was already answering and carries on:
|
|
352
|
+
|
|
353
|
+
```
|
|
354
|
+
captured 2 notes [decision, constraint]
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
That costs nothing — a request is charged per prompt, not per tool call, so capture
|
|
358
|
+
inside a turn you already paid for is free. Only typing `/remember` yourself spends
|
|
359
|
+
one.
|
|
360
|
+
|
|
361
|
+
Either way it makes one terminal call, and `write` reports each id:
|
|
340
362
|
|
|
341
363
|
```
|
|
342
364
|
created auth-service [system]
|
|
@@ -482,10 +504,21 @@ it. And because a request is charged per prompt rather than per tool call, captu
|
|
|
482
504
|
that rides inside a turn you already paid for is free — which is why it can afford to
|
|
483
505
|
happen at the moment the knowledge is fresh instead of whenever someone remembers.
|
|
484
506
|
|
|
507
|
+
The inverse is reported too. Staleness answers *is this note still true*; it cannot
|
|
508
|
+
answer *is there anything here yet*, and those fail in opposite directions — a repo
|
|
509
|
+
nobody has ever captured in has no stale notes either, so it reads exactly like one
|
|
510
|
+
that is fully covered. `tree` and `doctor` both say so:
|
|
511
|
+
|
|
512
|
+
```
|
|
513
|
+
47 commits since anything was captured for orders-api — /remember is behind
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Measured to the *nearest* capture, so one fresh note closes the gap however old the
|
|
517
|
+
rest of the graph is, and silent below `captureGapCommits` (50) so a young repo is
|
|
518
|
+
never nagged. Silence has to mean covered, never empty.
|
|
519
|
+
|
|
485
520
|
Not built yet, by choice:
|
|
486
521
|
|
|
487
|
-
- A capture-gap signal — the store knows when a note has gone stale, but not yet when
|
|
488
|
-
a repo has moved a hundred commits with nothing captured at all
|
|
489
522
|
- Team sharing, multi-machine sync
|
|
490
523
|
- An MCP server. It would read this same store, so it is an addition, not a rewrite.
|
|
491
524
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vib795/agent-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
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/src/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
import { neighborhood, applyBudget } from './graph.js';
|
|
10
10
|
import { buildTree, renderTree, buildDigest } from './digest.js';
|
|
11
11
|
import { compact, maybeCompact } from './compact.js';
|
|
12
|
-
import { staleness, currentRepo, reviewCandidates } from './staleness.js';
|
|
12
|
+
import { staleness, currentRepo, reviewCandidates, captureGap } from './staleness.js';
|
|
13
13
|
import { setup as runSetup, unlinkSkills, danglingSkillLinks, SKILLS } from './setup.js';
|
|
14
14
|
import { detectTargets, installableTargets } from './targets.js';
|
|
15
15
|
import { join } from 'node:path';
|
|
@@ -210,8 +210,11 @@ function cmdTree(opts) {
|
|
|
210
210
|
const db = openDb();
|
|
211
211
|
const repo = opts.repo === true ? null : (opts.repo ?? currentRepo());
|
|
212
212
|
const result = buildTree(db, { repo, all: !!opts.all, cfg });
|
|
213
|
+
// Only when scoped to a repo. Across all repos there is no single history to
|
|
214
|
+
// measure against, and a number that means nothing is worse than no number.
|
|
215
|
+
const gap = repo ? captureGap(db, { cfg, repo }) : null;
|
|
213
216
|
db.close();
|
|
214
|
-
return { ok: true, ...result, text: renderTree(result) };
|
|
217
|
+
return { ok: true, ...result, gap, text: renderTree({ ...result, gap }) };
|
|
215
218
|
}
|
|
216
219
|
|
|
217
220
|
function cmdGet(opts) {
|
|
@@ -515,6 +518,21 @@ function cmdDoctor() {
|
|
|
515
518
|
stale.length === 0,
|
|
516
519
|
stale.length ? `${stale.length} notes worth reviewing` : 'nothing far behind HEAD',
|
|
517
520
|
);
|
|
521
|
+
|
|
522
|
+
// The opposite failure to staleness, and the one that leaves no trace: a repo where
|
|
523
|
+
// nothing was ever captured has no stale notes either, so it passes every check
|
|
524
|
+
// above while knowing nothing at all.
|
|
525
|
+
const gap = captureGap(db, { cfg });
|
|
526
|
+
add(
|
|
527
|
+
'capture gap',
|
|
528
|
+
!gap?.note,
|
|
529
|
+
gap?.note
|
|
530
|
+
?? (!gap
|
|
531
|
+
? 'not in a repository'
|
|
532
|
+
: gap.notes === 0
|
|
533
|
+
? `nothing captured for ${gap.repo} yet, and not enough history for that to mean anything`
|
|
534
|
+
: `${gap.notes} notes, nearest capture is current`),
|
|
535
|
+
);
|
|
518
536
|
db.close();
|
|
519
537
|
|
|
520
538
|
const text = [
|
|
@@ -525,9 +543,9 @@ function cmdDoctor() {
|
|
|
525
543
|
].join('\n');
|
|
526
544
|
|
|
527
545
|
// Staleness is a report, not a failure. Being told about it is the whole feature.
|
|
528
|
-
const advisory = new Set(['staleness', 'skills linked']);
|
|
546
|
+
const advisory = new Set(['staleness', 'capture gap', 'skills linked']);
|
|
529
547
|
const fatal = checks.filter((c) => !c.ok && !advisory.has(c.name));
|
|
530
|
-
return { ok: fatal.length === 0, checks, stale, digest, text };
|
|
548
|
+
return { ok: fatal.length === 0, checks, stale, gap, digest, text };
|
|
531
549
|
}
|
|
532
550
|
|
|
533
551
|
// --- dispatch ---------------------------------------------------------------
|
package/src/config.js
CHANGED
|
@@ -31,6 +31,7 @@ export const DEFAULTS = {
|
|
|
31
31
|
decayDays: 90, // archive threshold for unreferenced, unread notes
|
|
32
32
|
staleAnnotateCommits: 10, // below this, staleness is not worth mentioning
|
|
33
33
|
staleReviewCommits: 100, // above this, doctor flags it for review
|
|
34
|
+
captureGapCommits: 50, // repo movement with no capture at all before it is worth saying
|
|
34
35
|
compactThreshold: 10, // node-count delta that triggers an automatic compact
|
|
35
36
|
};
|
|
36
37
|
|
package/src/digest.js
CHANGED
|
@@ -195,5 +195,8 @@ export function renderTree(result) {
|
|
|
195
195
|
// most dangerous thing this tool could do, because it looks like an answer.
|
|
196
196
|
out.push(`${result.omitted.length} nodes not shown, run agent-memory tree --all`);
|
|
197
197
|
}
|
|
198
|
+
// The map is what recall reads before answering, so it is where a thin graph has to
|
|
199
|
+
// admit that it is thin. A short list and a silent footer read as full coverage.
|
|
200
|
+
if (result.gap?.note) out.push(result.gap.note);
|
|
198
201
|
return out.join('\n');
|
|
199
202
|
}
|
package/src/staleness.js
CHANGED
|
@@ -145,6 +145,68 @@ export function reviewCandidates(db, { cwd = process.cwd(), cfg = loadConfig() }
|
|
|
145
145
|
return out;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* How far this repository has moved since anything was captured in it at all.
|
|
150
|
+
*
|
|
151
|
+
* Staleness answers "is this note still true". It cannot answer "is there anything
|
|
152
|
+
* here yet", and those fail in opposite directions: a repository nobody has ever run
|
|
153
|
+
* `/remember` in has no stale notes to warn about, so it reads exactly like one that
|
|
154
|
+
* is fully covered. Silence means both "all good" and "nothing here", which is the
|
|
155
|
+
* same conflation `doctor` already refuses to make about installed skills.
|
|
156
|
+
*
|
|
157
|
+
* Measured as the distance from HEAD to the nearest capture, so a single fresh note
|
|
158
|
+
* closes the gap no matter how old the rest of the graph is.
|
|
159
|
+
*/
|
|
160
|
+
export function captureGap(db, { cwd = process.cwd(), cfg = loadConfig(), repo } = {}) {
|
|
161
|
+
const here = repo ?? currentRepo(cwd);
|
|
162
|
+
if (!here) return null;
|
|
163
|
+
|
|
164
|
+
const rows = db
|
|
165
|
+
.prepare('SELECT id, captured_sha FROM nodes WHERE archived = 0 AND captured_sha IS NOT NULL')
|
|
166
|
+
.all();
|
|
167
|
+
const repoStmt = db.prepare('SELECT repo FROM node_repos WHERE node_id = ?');
|
|
168
|
+
|
|
169
|
+
let scoped = 0;
|
|
170
|
+
let nearest = null;
|
|
171
|
+
for (const row of rows) {
|
|
172
|
+
const repos = repoStmt.all(row.id).map((r) => r.repo);
|
|
173
|
+
if (repos.length && !repos.includes(here)) continue;
|
|
174
|
+
scoped += 1;
|
|
175
|
+
const res = commitsSince(row.captured_sha, cwd);
|
|
176
|
+
if (res.status !== 'ok') continue;
|
|
177
|
+
if (nearest === null || res.count < nearest) nearest = res.count;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (scoped === 0) {
|
|
181
|
+
// A repository with three commits in it does not need to be told it has no
|
|
182
|
+
// memory yet. Only say this once there is enough history for the absence to
|
|
183
|
+
// mean something.
|
|
184
|
+
let depth = 0;
|
|
185
|
+
try {
|
|
186
|
+
depth = Number.parseInt(git(['rev-list', '--count', 'HEAD'], cwd), 10) || 0;
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
if (depth < cfg.captureGapCommits) return { repo: here, notes: 0, commits: null, note: null };
|
|
191
|
+
return {
|
|
192
|
+
repo: here,
|
|
193
|
+
notes: 0,
|
|
194
|
+
commits: depth,
|
|
195
|
+
note: `nothing captured yet for ${here} — ${depth} commits of history and an empty graph`,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (nearest === null || nearest < cfg.captureGapCommits) {
|
|
200
|
+
return { repo: here, notes: scoped, commits: nearest, note: null };
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
repo: here,
|
|
204
|
+
notes: scoped,
|
|
205
|
+
commits: nearest,
|
|
206
|
+
note: `${nearest} commits since anything was captured for ${here} — /remember is behind`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
148
210
|
/** Testing seam: the per-process cache would otherwise outlive a fixture repo. */
|
|
149
211
|
export function resetCache() {
|
|
150
212
|
cache.clear();
|