@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 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 **explicit**. You invoke it, or `/handoff` does; nothing fires on its own.
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.12",
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",
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: remember
3
3
  version: 0.1.0
4
- description: Capture durable project knowledge from the current conversation into the shared memory graph, so a later conversation in any window or repository already knows it. Use when the user says remember this, save this, note this for later, or /remember.
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
- ## Two forms
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();