@staix/agent-mem 0.20.3 → 0.21.0

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.
Files changed (52) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +4 -3
  3. package/package.json +2 -2
  4. package/src/cli.ts +3 -0
  5. package/src/hook-spool.ts +45 -3
  6. package/src/index.ts +25 -2
  7. package/src/memory/bridge.ts +66 -18
  8. package/src/memory/candidates.ts +71 -9
  9. package/src/memory/generation-index.ts +95 -18
  10. package/src/memory/generation-sync.ts +3 -0
  11. package/src/memory/generation.ts +33 -15
  12. package/src/memory/platform.ts +38 -3
  13. package/src/memory/policy.ts +28 -15
  14. package/src/memory/prune-publisher-lineage.ts +50 -12
  15. package/src/memory/public-prune-advance.ts +11 -5
  16. package/src/memory/public-prune-inputs.ts +17 -16
  17. package/src/memory/shard-sync-applications.ts +153 -0
  18. package/src/memory/shard-sync-format.ts +15 -0
  19. package/src/memory/shard-sync-graph.ts +6 -4
  20. package/src/memory/shard-sync-history.ts +238 -0
  21. package/src/memory/shard-sync.ts +67 -151
  22. package/src/memory/shard.ts +32 -6
  23. package/src/memory/store.ts +14 -1
  24. package/src/memory/sync-binding.ts +63 -0
  25. package/src/memory/sync-placement-active.ts +75 -0
  26. package/src/memory/sync-placement-cache.ts +32 -0
  27. package/src/memory/sync-placement-capture.ts +292 -0
  28. package/src/memory/sync-placement-dependencies.ts +255 -0
  29. package/src/memory/sync-placement-format.ts +182 -0
  30. package/src/memory/sync-placement-git.ts +95 -0
  31. package/src/memory/sync-placement-guard.ts +98 -0
  32. package/src/memory/sync-placement-inspection-evidence.ts +257 -0
  33. package/src/memory/sync-placement-inspection-pending.ts +66 -0
  34. package/src/memory/sync-placement-inspection.ts +236 -0
  35. package/src/memory/sync-placement-journal-format.ts +114 -0
  36. package/src/memory/sync-placement-journal-storage.ts +513 -0
  37. package/src/memory/sync-placement-journal.ts +153 -0
  38. package/src/memory/sync-placement-materialize.ts +149 -0
  39. package/src/memory/sync-placement-plan.ts +171 -0
  40. package/src/memory/sync-placement-projection.ts +79 -0
  41. package/src/memory/sync-placement-publication-plan.ts +189 -0
  42. package/src/memory/sync-placement-publication-storage.ts +87 -0
  43. package/src/memory/sync-placement-transport.ts +489 -0
  44. package/src/memory/sync-policy-inspection.ts +126 -0
  45. package/src/memory/sync-screen.ts +128 -33
  46. package/src/memory/sync.ts +20 -64
  47. package/src/ui/inbox-cache.ts +219 -0
  48. package/src/ui/inbox-worker-version.ts +14 -0
  49. package/src/ui/inbox-worker.ts +32 -0
  50. package/src/ui/inbox.ts +281 -0
  51. package/src/ui/panel.js +7 -3
  52. package/src/ui/plugin.ts +26 -53
package/src/ui/plugin.ts CHANGED
@@ -4,6 +4,9 @@
4
4
  * Review writes run the same library calls as `amem review accept|reject` and `review walk`'s edit, stamped as the person.
5
5
  */
6
6
  import { createHash } from "node:crypto";
7
+ import { InboxCache } from "./inbox-cache.ts";
8
+ import { candidateText, candidateTargets as targetsOf, visibleCandidate as visible } from "./inbox.ts";
9
+ export { candidateText } from "./inbox.ts";
7
10
  import { existsSync, readFileSync } from "node:fs";
8
11
  import { join } from "node:path";
9
12
  import { createMemoryMcp } from "../mcp-memory.ts";
@@ -35,14 +38,9 @@ type ActionResult =
35
38
  | ({ ok: true; message?: string; undo?: { action: string } & Args } & Args)
36
39
  | { ok: false; error: string }
37
40
  | { ok: false; needsConfirm: true; preview: string[]; fingerprint: string; verb?: string; changes?: Change[]; warnings?: string[] };
38
- interface InboxItem {
39
- id: string; group: "candidate" | "alert"; kind: string; title: string; detail?: string; project?: string; at: string;
40
- chip?: { text: string; tone: "ok" | "warn" | "err" | "neutral" };
41
- verbs: { id: string; label: string; primary?: boolean; reason?: "optional" | "required" }[];
42
- link: { panel: string; params?: Record<string, string> };
43
- }
44
41
  interface Provider {
45
42
  assets: { js: string; css?: string };
43
+ close(): Promise<void>;
46
44
  snapshot(input: { after: number; panel: string } & Args): Promise<Args>;
47
45
  action(input: { action: string; confirmed?: true; fingerprint?: string } & Args): Promise<ActionResult>;
48
46
  }
@@ -60,25 +58,12 @@ const sha = (v: unknown) => createHash("sha256").update(JSON.stringify(v)).diges
60
58
  const errorText = (e: unknown) => (e instanceof Error ? e.message : String(e)).split("\n")[0]!;
61
59
  const oneLine = (text: string, max = 160) => { const flat = text.replace(/\s+/g, " ").trim(); return flat.length > max ? `${flat.slice(0, max - 3)}...` : flat; };
62
60
 
63
- /** What a candidate proposes, as text. */
64
- export function candidateText(input: CandidateInput): string {
65
- switch (input.kind) {
66
- case "memory": return input.title ? `${input.title}: ${input.text}` : input.text;
67
- case "fail": return `Approach: ${input.approach}\nSymptom: ${input.symptom}\nCause: ${input.cause}\nRetry when: ${input.retryWhen}`;
68
- case "correction": return `${input.proposedText}\n\nReason: ${input.reason}`;
69
- case "decay": return input.reason;
70
- case "promotion": return `Promote mem:${input.target.id} to long-term for goal:${input.goalId}`;
71
- case "consolidation": return `${input.action}: ${input.text}`;
72
- case "model-answer": return input.text;
73
- }
74
- }
75
61
  /** The text `review walk`'s edit opens for a memory or fail candidate. */
76
62
  function editText(input: CandidateInput): string | null {
77
63
  if (input.kind === "fail") return `Approach: ${input.approach}\nSymptom: ${input.symptom}\nCause: ${input.cause}\nEvidence: ${input.evidenceRefs.join(", ")}\nRetry when: ${input.retryWhen}\n`;
78
64
  if (input.kind !== "memory") return null;
79
65
  return input.noteKind === "fail" && !hasFailLabel(input.text) ? `Approach: ${input.text}\nSymptom: \nCause: \nEvidence: \nRetry when: \n` : input.text;
80
66
  }
81
- const targetsOf = (input: CandidateInput) => "targets" in input ? input.targets : "target" in input && input.target ? [input.target] : [];
82
67
 
83
68
  /** The CLI's curatedStore on an explicit home (src/cli.ts keeps its own copy; cli.ts is not a library module). */
84
69
  function openCurated(base: string, project: string, opts: StoreOptions, reconciled = true): MemoryStore {
@@ -154,7 +139,8 @@ export const plugin = {
154
139
  const pick = (args: Args, list: string[]) => {
155
140
  const asked = str(args.project), key = asked ? resolveAlias(base, asked) : undefined;
156
141
  if (key && !list.includes(key) && existsSync(base) && exists(key)) list.push(key);
157
- if (key && list.includes(key)) chosen = key;
142
+ if (key && !list.includes(key)) return undefined;
143
+ if (key) chosen = key;
158
144
  return chosen && list.includes(chosen) ? chosen : list[0];
159
145
  };
160
146
 
@@ -176,16 +162,6 @@ export const plugin = {
176
162
  try { return await mcp.tools[tool](forward(tool, args)); } finally { mcp.close(); }
177
163
  }
178
164
 
179
- /** The CLI's rule for agent-marked review, applied to the person here: the console shows only what passes the screen, targets included. */
180
- const visible = (candidate: Candidate, project: string, store: MemoryStore | undefined, p: StandalonePolicy): boolean => {
181
- if (!p.screenStandalone(candidate)) return false;
182
- const input = candidate.input, targets = targetsOf(input);
183
- if (!targets.length && input.kind !== "promotion") return true;
184
- if (!store) return false;
185
- if (targets.some(t => { const view = store.get(t.id, project); return !view || view.project !== project || !p.screenStandalone(view); })) return false;
186
- if (input.kind === "promotion") { const goal = store.goalOf(input.goalId, project); if (!goal || !p.screenStandalone(goal)) return false; }
187
- return true;
188
- };
189
165
  const projectOf = new Map<string, string>();
190
166
  async function pending(project: string, detail: boolean) {
191
167
  if (!hasCandidates()) return { rows: [], hidden: 0 };
@@ -218,26 +194,7 @@ export const plugin = {
218
194
  if (!await policy().screen(report)) return { withheld: true as const };
219
195
  return { report, applyNeeded: report.current.capMb !== report.proposal.capMb || report.current.retentionDays !== report.proposal.retentionDays };
220
196
  }
221
- // The shell keys snooze and oldest-first order on `at`: an alert keeps the time it was first seen.
222
- const firstSeen = new Map<string, string>();
223
- const since = (id: string) => { const at = firstSeen.get(id) ?? new Date().toISOString(); firstSeen.set(id, at); return at; };
224
- async function inbox(): Promise<InboxItem[]> {
225
- const items: InboxItem[] = [];
226
- for (const project of projects()) {
227
- for (const row of (await pending(project, false)).rows) items.push({
228
- id: row.id, group: "candidate", kind: row.kind, title: oneLine(row.text, 120), detail: `${row.kind} candidate in ${project}`, project, at: row.createdAt,
229
- verbs: [...row.acceptable ? [{ id: "accept", label: "Accept", primary: true }] : [], { id: "reject", label: "Reject", reason: "optional" as const }],
230
- link: { panel: "review", params: { project, id: row.id } },
231
- });
232
- }
233
- const cap = await capacity();
234
- if (cap && "report" in cap && cap.applyNeeded) {
235
- const { current, proposal } = cap.report;
236
- items.push({ id: "capacity:proposal", group: "alert", kind: "capacity", title: `Capture capacity: ${current.capMb} MB / ${current.retentionDays} d, proposed ${proposal.capMb} MB / ${proposal.retentionDays} d`,
237
- detail: proposal.reason, at: since("capacity:proposal"), chip: { text: "proposal", tone: "warn" }, verbs: [{ id: "capacity_apply", label: "Apply..." }], link: { panel: "capacity" } });
238
- } else firstSeen.delete("capacity:proposal");
239
- return items;
240
- }
197
+ const inboxCache = new InboxCache(base, { log: ctx.log });
241
198
 
242
199
  function searchHit(hit: Args, project: string, q: string) {
243
200
  const view = hit.view as View | undefined;
@@ -440,10 +397,16 @@ export const plugin = {
440
397
 
441
398
  return {
442
399
  assets: { js: asset("./panel.js"), css: asset("./panel.css") },
400
+ close: () => inboxCache.close(),
443
401
  async snapshot(input) {
444
402
  const args = input as Args;
445
- if (input.panel === "inbox") return { items: await inbox() };
403
+ if (input.panel === "inbox") {
404
+ const snapshot = await inboxCache.snapshot();
405
+ for (const item of snapshot.items) if (item.group === "candidate" && item.link.params?.project) projectOf.set(item.id, item.link.params.project);
406
+ return { ...snapshot };
407
+ }
446
408
  const list = projects(), project = pick(args, list);
409
+ if (str(args.project) && !project) return { projects: list, project: null, error: "The requested memory project is unavailable; choose an active project from the project selector." };
447
410
  if (input.panel === "search") {
448
411
  const q = str(args.q);
449
412
  if (!q || !project) return { hits: [], total: 0 };
@@ -461,6 +424,17 @@ export const plugin = {
461
424
  }
462
425
  },
463
426
  async action(input) {
427
+ const result = await performAction(input);
428
+ if (result.ok) {
429
+ const id = str(input.id);
430
+ const hard = ["capacity_apply", "project_forget", "prune", "decay_apply", "accept_undo"].includes(input.action);
431
+ const removes = ["accept", "reject", "edit_accept"].includes(input.action);
432
+ inboxCache.invalidate({ hard, capacity: hard || input.action === "gc", ...(removes && id ? { removeIds: [id] } : {}), ...(input.action === "reject_undo" ? { added: 1 } : {}) });
433
+ }
434
+ return result;
435
+ },
436
+ };
437
+ async function performAction(input: { action: string; confirmed?: true; fingerprint?: string } & Args): Promise<ActionResult> {
464
438
  const args = input as Args;
465
439
  try {
466
440
  switch (input.action) {
@@ -499,8 +473,7 @@ export const plugin = {
499
473
  ctx.log(`amem: ${input.action} failed: ${errorText(e)}`);
500
474
  return { ok: false, error: errorText(e) };
501
475
  }
502
- },
503
- };
476
+ }
504
477
  },
505
478
  };
506
479