@vib795/agent-memory 0.1.12 → 0.1.14
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 +29 -2
- package/package.json +1 -1
- package/skills/remember/SKILL.md +42 -2
- 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/README.md
CHANGED
|
@@ -465,11 +465,38 @@ that drifts as the work progresses does not create a duplicate.
|
|
|
465
465
|
|
|
466
466
|
## Status
|
|
467
467
|
|
|
468
|
-
Capture is **
|
|
468
|
+
Capture is **judged, not scheduled.** You invoke it, `/handoff` does, or the agent
|
|
469
|
+
does on its own when a juncture has just passed — a decision settled, a constraint
|
|
470
|
+
found, a root cause identified, a convention agreed. It says so in one line and
|
|
471
|
+
carries on with what you actually asked:
|
|
472
|
+
|
|
473
|
+
```
|
|
474
|
+
captured 2 notes [decision, constraint]
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
Nothing fires on a timer, on a tool count, or on every reply. That distinction is the
|
|
478
|
+
whole design: a store full of task chatter is worse than an empty one, because it
|
|
479
|
+
buries the four notes that mattered. The judgment of what is durable belongs to the
|
|
480
|
+
model, in the turn where the context still exists; there is no keyword list deciding
|
|
481
|
+
it. And because a request is charged per prompt rather than per tool call, capture
|
|
482
|
+
that rides inside a turn you already paid for is free — which is why it can afford to
|
|
483
|
+
happen at the moment the knowledge is fresh instead of whenever someone remembers.
|
|
484
|
+
|
|
485
|
+
The inverse is reported too. Staleness answers *is this note still true*; it cannot
|
|
486
|
+
answer *is there anything here yet*, and those fail in opposite directions — a repo
|
|
487
|
+
nobody has ever captured in has no stale notes either, so it reads exactly like one
|
|
488
|
+
that is fully covered. `tree` and `doctor` both say so:
|
|
489
|
+
|
|
490
|
+
```
|
|
491
|
+
47 commits since anything was captured for orders-api — /remember is behind
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Measured to the *nearest* capture, so one fresh note closes the gap however old the
|
|
495
|
+
rest of the graph is, and silent below `captureGapCommits` (50) so a young repo is
|
|
496
|
+
never nagged. Silence has to mean covered, never empty.
|
|
469
497
|
|
|
470
498
|
Not built yet, by choice:
|
|
471
499
|
|
|
472
|
-
- Automatic or ambient capture
|
|
473
500
|
- Team sharing, multi-machine sync
|
|
474
501
|
- An MCP server. It would read this same store, so it is an addition, not a rewrite.
|
|
475
502
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vib795/agent-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: remember
|
|
3
3
|
version: 0.1.0
|
|
4
|
-
description: Capture durable project knowledge from
|
|
4
|
+
description: Capture durable project knowledge from this conversation into a cross-repo memory graph, so a later conversation anywhere already knows it. Use when the user says remember this, save this, note this for later, or /remember. Also invoke it unasked when a juncture passes - a decision settled, a constraint found, a root cause identified, a convention agreed - then say so in one line and carry on.
|
|
5
5
|
license: MIT
|
|
6
6
|
allowed-tools: Bash Read Write
|
|
7
7
|
triggers:
|
|
@@ -25,11 +25,51 @@ call. Do not ask the user to confirm each node.
|
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
-
##
|
|
28
|
+
## Three forms
|
|
29
29
|
|
|
30
30
|
- **`/remember`** — you select the durable knowledge from the conversation so far.
|
|
31
31
|
- **`/remember <what>`** — the user named the thing. Write that, with full context
|
|
32
32
|
from the conversation, and write nothing unrelated to it.
|
|
33
|
+
- **Unasked** — you noticed a juncture pass and captured it without being told.
|
|
34
|
+
|
|
35
|
+
### Capturing unasked
|
|
36
|
+
|
|
37
|
+
A memory that only grows when someone remembers to grow it stays thin, and thin is
|
|
38
|
+
how it dies: the one fact worth having is the one nobody stopped to write down.
|
|
39
|
+
|
|
40
|
+
Invoke this yourself the moment one of these has just happened, in the same turn:
|
|
41
|
+
|
|
42
|
+
- a **decision** was settled, and the reasons and rejected options are still in view
|
|
43
|
+
- a **constraint** surfaced — a blocked tool, a policy, an environment restriction
|
|
44
|
+
- a **root cause** was found, as opposed to a symptom worked around
|
|
45
|
+
- a **convention** was agreed, or discovered by reading the code
|
|
46
|
+
|
|
47
|
+
Do not capture on a timer, on a tool count, or at every reply. Those produce volume,
|
|
48
|
+
and volume is what makes a graph useless — a store full of task chatter is worse than
|
|
49
|
+
an empty one, because it buries the four notes that mattered.
|
|
50
|
+
|
|
51
|
+
Never capture: task status, what you are about to do next, anything already in the
|
|
52
|
+
graph, or anything that will be false next month. If you captured a juncture earlier
|
|
53
|
+
in this conversation, do not capture it again because it came up a second time.
|
|
54
|
+
|
|
55
|
+
**It is free, and that is the point.** A request is charged per prompt, not per tool
|
|
56
|
+
call, so capturing inside a turn you were already answering costs nothing. Only a
|
|
57
|
+
user typing `/remember` spends a request. That is the whole reason to do this
|
|
58
|
+
yourself rather than wait to be asked.
|
|
59
|
+
|
|
60
|
+
**Report it in one line, then carry on:**
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
captured 2 notes [decision, constraint]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
At the end of the answer you were already giving. Do not print the JSON, do not
|
|
67
|
+
summarize what you wrote, and do not make it the subject of the reply — the user
|
|
68
|
+
asked you about something else and is still waiting for it. One line is enough for
|
|
69
|
+
them to know it happened and to run `/recall` if they want the detail.
|
|
70
|
+
|
|
71
|
+
If nothing durable happened, say nothing at all. Silence is the correct output for
|
|
72
|
+
most turns.
|
|
33
73
|
|
|
34
74
|
---
|
|
35
75
|
|
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();
|