@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 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. Either way it makes one terminal call, and `write` reports each id:
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.13",
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();