@agfpd/iapeer-memory 0.2.9 → 0.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer-memory",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
4
4
  "description": "iapeer-memory — peer memory for the iapeer ecosystem: vault, memoryd (index/search/MCP-http), layer-5 context fragments, role doctrines. The package IS the system; the claude/codex plugins are thin session sockets (docs/10-distribution.md, ADR-009).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  "access": "public"
28
28
  },
29
29
  "dependencies": {
30
- "@agfpd/iapeer-memory-core": "0.2.9"
30
+ "@agfpd/iapeer-memory-core": "0.3.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/bun": "^1.2.0",
package/src/cli.ts CHANGED
@@ -43,7 +43,6 @@ Commands:
43
43
  uninstall [--keep-binary] remove the system: slot declaration + binary
44
44
  (vault and config are kept — user-owned)
45
45
  status read-only diagnostics: verify + slot + MCP probe
46
- + inbox load
47
46
  verify [--repair] check (and repair) the live surfaces: config,
48
47
  memory-provider slot, memoryd heartbeat, role
49
48
  doctrine versions
@@ -72,8 +71,8 @@ Commands:
72
71
  archive-stale [--commit] deliberate backlog archiver (lean §2.2a): move
73
72
  pre-existing stale notes to the archive. Dry-run by
74
73
  default (lists what would move); --commit executes.
75
- 03_Projects is excluded; memoryd archives ongoing
76
- staleness incrementally on its own.
74
+ ALL content folders incl. 03_Projects (unified rule);
75
+ memoryd archives ongoing staleness on its own.
77
76
  render index|fragment|doctrine|guide
78
77
  render one artifact explicitly (memoryd does this
79
78
  continuously; render is the manual/scripted path)
@@ -10,9 +10,10 @@
10
10
  * archive-stale DRY-RUN — list what would move + a count
11
11
  * archive-stale --commit actually move them
12
12
  *
13
- * `03_Projects` is excluded (a completed phase is a project record, not
14
- * displaced knowledge `isArchivableZone`). memoryd reindexes the moves on
15
- * its next pass (or on restart); the verb itself only moves files.
13
+ * Scope = ALL content folders, INCLUDING `03_Projects` (unified rule, decision
14
+ * Артур 15.06: a completed phase/project is stale like any note and archives
15
+ * too `isArchivableZone`). memoryd reindexes the moves on its next pass (or
16
+ * on restart); the verb itself only moves files.
16
17
  */
17
18
 
18
19
  import fs from "node:fs";
@@ -31,7 +32,7 @@ export function cmdArchiveStale(argv: string[]): number {
31
32
  const taxonomy = config.taxonomy;
32
33
 
33
34
  // Candidates: notes in the monitored content folders carrying a final
34
- // status, EXCLUDING 03_Projects (filtered by shouldArchive → isArchivableZone).
35
+ // status ALL content folders incl. 03_Projects (shouldArchive → isArchivableZone).
35
36
  const snap = snapshotVault(vault, taxonomy);
36
37
  const reserved = new Set<string>(); // archive targets claimed within this run
37
38
  const moves: Array<{ from: string; to: string }> = [];
@@ -63,7 +64,7 @@ export function cmdArchiveStale(argv: string[]): number {
63
64
  );
64
65
  for (const m of moves) console.log(` ${m.from} → ${m.to}`);
65
66
  console.log(
66
- `\nPass --commit to move them. (03_Projects is excluded — completed phases stay project-local.)`,
67
+ `\nPass --commit to move them. (All content folders incl. 03_Projects a completed phase/project archives like any stale note; memoryd archives ongoing staleness on its own.)`,
67
68
  );
68
69
  return 0;
69
70
  }
@@ -45,6 +45,8 @@ import {
45
45
  parseDictionaryTags,
46
46
  tagGateProblems,
47
47
  tagsDictionarySourceRel,
48
+ DEFAULT_DEDUP_THRESHOLD,
49
+ DEFAULT_LINK_HINT_THRESHOLD,
48
50
  type TaxonomyPreset,
49
51
  } from "@agfpd/iapeer-memory-core";
50
52
  import { memoryPaths, type MemoryPaths } from "../paths.js";
@@ -203,7 +205,7 @@ export function runPostWrite(
203
205
  * Tag-gate problems across the just-written CANON files (lean §3). Reads the
204
206
  * dictionary from the vault; FAIL-OPEN — an unreadable/empty dictionary (e.g.
205
207
  * an evicted iCloud placeholder) yields no problems rather than rejecting
206
- * every tag. Operative/inbox zones are not gated (canon only).
208
+ * every tag. The agent-memory (operative) zone is not gated (canon only).
207
209
  */
208
210
  export function collectTagProblems(
209
211
  files: string[],
@@ -255,25 +257,36 @@ type DedupResponse = {
255
257
  matches: Array<{ path: string; title: string; similarity: number }>;
256
258
  };
257
259
 
260
+ /** Dedup band (≥ DEDUP_THRESHOLD → «possible duplicate», §3a) and link-hint
261
+ * band ([LINK_HINT_THRESHOLD, DEDUP_THRESHOLD) → «maybe link», §3b) for a
262
+ * canon write. One `/dedup` query, classified here. */
263
+ export type WriteHints = { dup: string[]; link: string[] };
264
+
265
+ function numEnv(v: string | undefined, fallback: number): number {
266
+ const n = Number(v);
267
+ return v !== undefined && !Number.isNaN(n) ? n : fallback;
268
+ }
269
+
258
270
  /** POST the note body to memoryd's loopback /dedup RPC through the egress hub
259
- * (loopback allowance — П6 topology). Fail-open: any error (timeout,
260
- * connection refused, bad JSON) → null → the caller stays silent. */
271
+ * (loopback allowance — П6 topology), asking for matches ≥ queryThreshold.
272
+ * Fail-open: any error (timeout, refused, bad JSON) → null → silent. */
261
273
  async function dedupFetch(
262
274
  egress: Egress,
263
275
  env: Record<string, string | undefined>,
264
276
  content: string,
277
+ dupThreshold: number,
278
+ linkThreshold: number,
265
279
  ): Promise<DedupResponse | null> {
266
280
  const port = env.IAPEER_MEMORY_MCP_PORT || "8766";
267
- const thr = env.IAPEER_MEMORY_DEDUP_THRESHOLD;
268
- const body: { content: string; threshold?: number } = { content };
269
- if (thr && !Number.isNaN(Number(thr))) body.threshold = Number(thr);
270
281
  const controller = new AbortController();
271
282
  const timer = setTimeout(() => controller.abort(), DEDUP_TIMEOUT_MS);
272
283
  try {
273
284
  const res = await egress.fetch(`http://127.0.0.1:${port}/dedup`, {
274
285
  method: "POST",
275
286
  headers: { "Content-Type": "application/json" },
276
- body: JSON.stringify(body),
287
+ // Send BOTH band bounds so the daemon caps each band independently and
288
+ // the §3b link band is never starved by a burst of §3a dup matches.
289
+ body: JSON.stringify({ content, threshold: dupThreshold, linkThreshold }),
277
290
  signal: controller.signal,
278
291
  });
279
292
  if (!res.ok) return null;
@@ -286,17 +299,20 @@ async function dedupFetch(
286
299
  }
287
300
 
288
301
  /**
289
- * Dedup hint lines for a just-written CANON note (lean §3a). Re-parses the
290
- * event independently of `runPostWrite` (keeps that sync contract intact).
291
- * Claude-only (Write/EditadditionalContext); canon-zone only; queries
292
- * memoryd by the note BODY. Embeddings-off → memoryd returns enabled:false →
293
- * no hint (silent).
302
+ * Dedup + link hints for a just-written CANON note (lean §3a + §3b). ONE
303
+ * `/dedup` query (threshold = the lower band bound), classified into two
304
+ * contiguous bands: cosine ≥ DEDUP_THRESHOLD «possible duplicate» (§3a);
305
+ * [LINK_HINT_THRESHOLD, DEDUP_THRESHOLD) «maybe link» (§3b). Re-parses the
306
+ * event independently of `runPostWrite` (keeps that sync contract). Runtime-
307
+ * agnostic (Write/Edit + codex apply_patch). Canon-zone only. Embeddings-off →
308
+ * memoryd returns enabled:false → both bands empty (silent).
294
309
  */
295
- export async function collectDedupHints(
310
+ export async function collectDedupAndLinkHints(
296
311
  eventJson: string,
297
312
  egress: Egress,
298
313
  env: Record<string, string | undefined> = process.env,
299
- ): Promise<string[]> {
314
+ ): Promise<WriteHints> {
315
+ const empty: WriteHints = { dup: [], link: [] };
300
316
  let event: {
301
317
  tool_name?: string;
302
318
  cwd?: string;
@@ -306,27 +322,32 @@ export async function collectDedupHints(
306
322
  try {
307
323
  event = JSON.parse(eventJson) as typeof event;
308
324
  } catch {
309
- return [];
325
+ return empty;
310
326
  }
311
327
  const tool = event.tool_name ?? "";
312
- if (!POST_WRITE_TOOLS.has(tool)) return [];
328
+ if (!POST_WRITE_TOOLS.has(tool)) return empty;
313
329
  const vault = env.IAPEER_MEMORY_VAULT_PATH ?? "";
314
- if (!vault) return [];
330
+ if (!vault) return empty;
315
331
  const localeRaw = env.IAPEER_MEMORY_LOCALE || "en";
316
- if (!isLocaleId(localeRaw)) return [];
332
+ if (!isLocaleId(localeRaw)) return empty;
317
333
  const taxonomy = getTaxonomy(localeRaw);
334
+
335
+ const dupThreshold = numEnv(env.IAPEER_MEMORY_DEDUP_THRESHOLD, DEFAULT_DEDUP_THRESHOLD);
336
+ const linkLow = numEnv(env.IAPEER_MEMORY_LINK_HINT_THRESHOLD, DEFAULT_LINK_HINT_THRESHOLD);
337
+
318
338
  // Candidate files: claude Write/Edit carry one file_path; codex apply_patch
319
- // carries a patch over possibly many (RUNTIME-AGNOSTIC — codex receives the
320
- // additionalContext hint via the same channel).
339
+ // carries a patch over possibly many (RUNTIME-AGNOSTIC — the additionalContext
340
+ // hint reaches both runtimes via the same channel).
321
341
  const candidates =
322
342
  tool === "apply_patch" ? applyPatchPaths(event) : [event.tool_input?.file_path ?? ""];
323
343
  const vaultPrefix = vault.endsWith(path.sep) ? vault : vault + path.sep;
324
344
  const files = candidates.filter(
325
345
  (p) => p.endsWith(".md") && p.startsWith(vaultPrefix) && fs.existsSync(p),
326
346
  );
327
- const hints: string[] = [];
347
+ const dup: string[] = [];
348
+ const link: string[] = [];
328
349
  for (const file of files) {
329
- if (resolveZone(file, vault, taxonomy) !== "permanent") continue; // canon only (§3a)
350
+ if (resolveZone(file, vault, taxonomy) !== "permanent") continue; // canon only
330
351
  let body: string;
331
352
  try {
332
353
  body = splitFrontmatter(fs.readFileSync(file, "utf-8"))[1];
@@ -334,19 +355,21 @@ export async function collectDedupHints(
334
355
  continue;
335
356
  }
336
357
  if (!body.trim()) continue;
337
- const result = await dedupFetch(egress, env, body);
358
+ const result = await dedupFetch(egress, env, body, dupThreshold, linkLow);
338
359
  if (!result?.enabled || !result.matches?.length) continue;
339
360
  for (const m of result.matches) {
340
361
  if (path.basename(m.path) === path.basename(file)) continue; // self-guard (belt + braces)
341
- hints.push(`[[${m.title}]] (${Math.round(m.similarity * 100)}%)`);
362
+ const entry = `[[${m.title}]] (${Math.round(m.similarity * 100)}%)`;
363
+ if (m.similarity >= dupThreshold) dup.push(entry);
364
+ else if (m.similarity >= linkLow) link.push(entry);
342
365
  }
343
366
  }
344
- return hints;
367
+ return { dup, link };
345
368
  }
346
369
 
347
- /** Combine the sync tag-teaching output with async dedup hints into one
348
- * additionalContext blob (or null when both are empty). */
349
- export function mergeHookOutput(tagOutput: string | null, dedupHints: string[]): string | null {
370
+ /** Combine the sync tag-teaching output with the async dedup + link hints into
371
+ * one additionalContext blob (or null when all empty). */
372
+ export function mergeHookOutput(tagOutput: string | null, hints: WriteHints): string | null {
350
373
  let ctx = "";
351
374
  if (tagOutput) {
352
375
  try {
@@ -355,12 +378,22 @@ export function mergeHookOutput(tagOutput: string | null, dedupHints: string[]):
355
378
  ctx = "";
356
379
  }
357
380
  }
358
- if (dedupHints.length) {
359
- const dedupText =
381
+ const add = (section: string) => {
382
+ ctx = ctx ? `${ctx}\n\n${section}` : section;
383
+ };
384
+ if (hints.dup.length) {
385
+ add(
360
386
  "[iapeer-memory] possible duplicate(s) of this canon note — verify, then extend " +
361
- "the existing note or keep only new material:\n" +
362
- dedupHints.map((h) => `- ${h}`).join("\n");
363
- ctx = ctx ? `${ctx}\n\n${dedupText}` : dedupText;
387
+ "the existing note or keep only new material:\n" +
388
+ hints.dup.map((h) => `- ${h}`).join("\n"),
389
+ );
390
+ }
391
+ if (hints.link.length) {
392
+ add(
393
+ "[iapeer-memory] semantically close note(s) — consider linking [[…]] in the text " +
394
+ "if related (you decide; not every close note belongs):\n" +
395
+ hints.link.map((h) => `- ${h}`).join("\n"),
396
+ );
364
397
  }
365
398
  if (!ctx) return null;
366
399
  return JSON.stringify({
@@ -466,8 +499,8 @@ export async function cmdHook(argv: string[], egress: Egress): Promise<number> {
466
499
  case "post-write": {
467
500
  const text = await Bun.stdin.text();
468
501
  const result = runPostWrite(text); // sync: stamp + tag gate
469
- const dedupHints = await collectDedupHints(text, egress); // async, fail-open §3a
470
- const output = mergeHookOutput(result.output, dedupHints);
502
+ const hints = await collectDedupAndLinkHints(text, egress); // async, fail-open §3a/§3b
503
+ const output = mergeHookOutput(result.output, hints);
471
504
  if (output) console.log(output);
472
505
  return 0;
473
506
  }
@@ -28,8 +28,11 @@ import {
28
28
  getTaxonomy,
29
29
  isLocaleId,
30
30
  renderDoctrine,
31
+ resolveMode,
32
+ curationPlan,
31
33
  writeHostWideGuideFragment,
32
34
  type LocaleId,
35
+ type MemoryMode,
33
36
  } from "@agfpd/iapeer-memory-core";
34
37
  import { installBinary } from "../binary.js";
35
38
  import { IAPEER_BIN, type Egress } from "../egress.js";
@@ -56,10 +59,8 @@ import {
56
59
  patchWakePolicyEphemeral,
57
60
  registerTimer,
58
61
  registerWatcher,
59
- sweepTimerMessage,
60
62
  writeDreamGateScript,
61
63
  writeLauncherScript,
62
- writeStaleCheckScript,
63
64
  } from "../watcher.js";
64
65
 
65
66
  type InitFlags = {
@@ -68,6 +69,8 @@ type InitFlags = {
68
69
  human?: string;
69
70
  embeddingEndpoint?: string;
70
71
  rerankerEndpoint?: string;
72
+ /** Curation mode (lean §7); default lean for new installs. */
73
+ mode?: string;
71
74
  nonInteractive: boolean;
72
75
  skipDeps: boolean;
73
76
  skipEcosystem: boolean;
@@ -98,6 +101,7 @@ function parseFlags(argv: string[]): InitFlags | null {
98
101
  case "--human": f.human = take(); break;
99
102
  case "--embedding-endpoint": f.embeddingEndpoint = take(); break;
100
103
  case "--reranker-endpoint": f.rerankerEndpoint = take(); break;
104
+ case "--mode": f.mode = take(); break;
101
105
  case "--non-interactive": f.nonInteractive = true; break;
102
106
  case "--skip-deps": f.skipDeps = true; break;
103
107
  case "--skip-ecosystem": f.skipEcosystem = true; break;
@@ -195,6 +199,21 @@ export async function cmdInit(argv: string[], egress: Egress): Promise<number> {
195
199
  return 2;
196
200
  }
197
201
  const locale: LocaleId = localeRaw;
202
+
203
+ // Curation mode (lean §7). Resolution: an explicit --mode wins; else PRESERVE
204
+ // the host's existing mode (cli.ts loads config.env into process.env before
205
+ // dispatch — a re-init of a curated host must NOT silently flip to lean and
206
+ // mis-wire its triggers, §10.3 / mode.ts); else default lean for a truly NEW
207
+ // install (cheap by default — the curation overlay is a deliberate opt-in).
208
+ const envMode = (process.env.IAPEER_MEMORY_MODE ?? "").trim().toLowerCase();
209
+ const preserved = envMode === "lean" || envMode === "curated" ? envMode : "lean";
210
+ const modeRaw = (flags.mode ?? preserved).trim().toLowerCase();
211
+ if (modeRaw !== "lean" && modeRaw !== "curated") {
212
+ console.error(`iapeer-memory init: --mode must be "lean" or "curated" (got "${flags.mode}")`);
213
+ return 2;
214
+ }
215
+ const mode: MemoryMode = modeRaw;
216
+ const plan = curationPlan(resolveMode({ ...process.env, IAPEER_MEMORY_MODE: mode }).roles);
198
217
  if (!human) {
199
218
  human = interactive
200
219
  ? ask("Human owner personality (empty = no human role)", humanDefault)
@@ -254,11 +273,12 @@ export async function cmdInit(argv: string[], egress: Egress): Promise<number> {
254
273
  configFile: paths.configFile,
255
274
  vaultPath: vault,
256
275
  locale,
276
+ mode,
257
277
  human: human || null,
258
278
  embeddingEndpoint: embeddingEndpoint || null,
259
279
  rerankerEndpoint: rerankerEndpoint || null,
260
280
  });
261
- step("config", `${paths.configFile} (${cfg})`);
281
+ step("config", `${paths.configFile} (${cfg}) mode=${mode}`);
262
282
 
263
283
  // 4. stable binary
264
284
  if (flags.skipBinary) {
@@ -372,10 +392,12 @@ export async function cmdInit(argv: string[], egress: Egress): Promise<number> {
372
392
  );
373
393
  }
374
394
 
375
- // 7. notifier wiring (ADR-015, инверсия): the EVENT trigger targets the
376
- // SCRIBER (first receiver); two TIMERS target the indexweekly
377
- // DREAM_TICK and the check-gated fail-open sweep. Registrant = index for
378
- // all three (one durable profile to verify); same-id re-send = replace.
395
+ // 7. notifier wiring GATED by the curation plan (lean §7). The WATCHER
396
+ // ALWAYS registers: its script LAUNCHES memoryd (the baseдетектор/архив/
397
+ // проекция/dedup runs in BOTH modes; never gated). Its forward target is
398
+ // the §7.1 conditional (scriber→index→placeholder), and memoryd SUPPRESSES
399
+ // curation emits in full-lean so the forward is empty. The SWEEP (→index)
400
+ // and DREAM (→dreamweaver) timers register ONLY when their role is proactive.
379
401
  if (flags.skipEcosystem) {
380
402
  step("watcher", "skipped (--skip-ecosystem)");
381
403
  step("timers", "skipped (--skip-ecosystem)");
@@ -383,6 +405,7 @@ export async function cmdInit(argv: string[], egress: Egress): Promise<number> {
383
405
  writeLauncherScript({ launcherPath: paths.launcherPath, binaryPath: paths.binaryPath });
384
406
  const sent = registerWatcher(egress, {
385
407
  launcherPath: paths.launcherPath,
408
+ target: plan.eventTarget ?? undefined, // null (full-lean) → default placeholder; memoryd emits nothing
386
409
  iapeerBin: flags.iapeerBin,
387
410
  });
388
411
  step(
@@ -390,41 +413,35 @@ export async function cmdInit(argv: string[], egress: Egress): Promise<number> {
390
413
  sent.suppressed
391
414
  ? "skipped (test sandbox — sends suppressed)"
392
415
  : sent.ok
393
- ? `registration sent (${paths.launcherPath} target scriber); confirm: iapeer-memory verify`
416
+ ? `registered (launches memoryd; curation target: ${plan.eventTarget ?? "none — lean: base runs, curation silent"}); confirm: iapeer-memory verify`
394
417
  : `registration failed — ${sent.detail}`,
395
418
  sent.ok || Boolean(sent.suppressed),
396
419
  );
397
420
 
398
- writeStaleCheckScript({
399
- checkScriptPath: paths.checkScriptPath,
400
- vaultPath: vault,
401
- inboxFolders: [getTaxonomy(locale).folders.inbox, getTaxonomy(locale).folders.inboxHuman],
402
- });
403
- const sweep = registerTimer(egress, {
404
- message: sweepTimerMessage({ checkScriptPath: paths.checkScriptPath }),
405
- iapeerBin: flags.iapeerBin,
406
- });
407
- writeDreamGateScript({
408
- dreamGateScriptPath: paths.dreamGateScriptPath,
409
- binaryPath: paths.binaryPath,
410
- });
411
- const dream = registerTimer(egress, {
412
- message: dreamTimerMessage({
413
- cron: process.env.IAPEER_MEMORY_DREAM_CRON,
421
+ if (plan.dream) {
422
+ writeDreamGateScript({
414
423
  dreamGateScriptPath: paths.dreamGateScriptPath,
415
- }),
416
- iapeerBin: flags.iapeerBin,
417
- });
418
- const timersSandboxed = sweep.suppressed && dream.suppressed;
419
- step(
420
- "timers",
421
- timersSandboxed
422
- ? "skipped (test sandbox — sends suppressed)"
423
- : sweep.ok && dream.ok
424
- ? `sweep (@every 1h, check ${paths.checkScriptPath}) + dream-tick (weekly, gated, → ${DREAM_TARGET})`
425
- : `sweep: ${sweep.ok ? "sent" : sweep.detail}; dream: ${dream.ok ? "sent" : dream.detail}`,
426
- Boolean(timersSandboxed) || (sweep.ok && dream.ok),
427
- );
424
+ binaryPath: paths.binaryPath,
425
+ });
426
+ const dream = registerTimer(egress, {
427
+ message: dreamTimerMessage({
428
+ cron: process.env.IAPEER_MEMORY_DREAM_CRON,
429
+ dreamGateScriptPath: paths.dreamGateScriptPath,
430
+ }),
431
+ iapeerBin: flags.iapeerBin,
432
+ });
433
+ step(
434
+ "dream",
435
+ dream.suppressed
436
+ ? "skipped (test sandbox)"
437
+ : dream.ok
438
+ ? `dream-tick (weekly, gated → ${DREAM_TARGET})`
439
+ : `dream: ${dream.detail}`,
440
+ dream.ok || Boolean(dream.suppressed),
441
+ );
442
+ } else {
443
+ step("dream", `not registered (mode ${mode}: dreamweaver not proactive)`);
444
+ }
428
445
  }
429
446
 
430
447
  // 8. slot + surfaces + v1.1 migration — ORDER MATTERS (ADR-009 v1.2):
@@ -4,8 +4,8 @@
4
4
  * iapeer-memory memoryd [--mcp-port N | --no-mcp] [--human NAME]
5
5
  *
6
6
  * This IS the watcher script the notifier supervises: stdout carries the
7
- * event signal lines (INBOX_NEW / PERMANENT_* — core emits them), stderr
8
- * carries logs, SIGTERM/SIGINT shut down cleanly (flush + close). All state
7
+ * curation signal line (CURATOR_TICK — core emits it per cadence pass),
8
+ * stderr carries logs, SIGTERM/SIGINT shut down cleanly (flush + close). All state
9
9
  * paths come from the shared `paths.ts` namespace — the heartbeat lands
10
10
  * exactly where `verify` reads it, by construction.
11
11
  *
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import fs from "node:fs";
20
- import { configFromEnv, startMemoryd } from "@agfpd/iapeer-memory-core";
20
+ import { configFromEnv, startMemoryd, resolveMode, curationPlan } from "@agfpd/iapeer-memory-core";
21
21
  import { authorIndexPath, memoryPaths } from "../paths.js";
22
22
  import { guardedWriteFileSync, guardedUnlinkSync } from "@agfpd/iapeer-memory-core";
23
23
 
@@ -51,6 +51,17 @@ export async function cmdMemoryd(argv: string[]): Promise<number> {
51
51
  }
52
52
 
53
53
  const config = configFromEnv();
54
+ // Lean §7 emit-suppression: memoryd ALWAYS runs (the base — детектор/архив/
55
+ // проекция/dedup), but it EMITS the curation event (CURATOR_TICK) only when
56
+ // a proactive curation receiver exists (scriber ∥ index). Full-lean → a
57
+ // no-op emit: the curator tick still runs (the baseline stays current — a
58
+ // later lean→curated switch is clean), the watcher forwards nothing. The
59
+ // watcher trigger ITSELF always registers (it launches memoryd; never gated).
60
+ const { mode, roles } = resolveMode(process.env);
61
+ const emitCuration = curationPlan(roles).emit;
62
+ process.stderr.write(
63
+ `iapeer-memory memoryd: mode=${mode} curation-emit=${emitCuration ? "on" : "off (lean)"}\n`,
64
+ );
54
65
  const paths = memoryPaths();
55
66
  for (const dir of [paths.stateDir, paths.cacheDir, paths.logsDir]) {
56
67
  fs.mkdirSync(dir, { recursive: true });
@@ -67,6 +78,8 @@ export async function cmdMemoryd(argv: string[]): Promise<number> {
67
78
 
68
79
  const handle = await startMemoryd({
69
80
  config,
81
+ // full-lean → suppress curation emits (no-op); else core default (stdout).
82
+ emit: emitCuration ? undefined : () => {},
70
83
  heartbeatPath: paths.heartbeatPath,
71
84
  hashStatePath: paths.hashStatePath,
72
85
  tagsMirrorPath: paths.tagsMirrorPath,
@@ -2,7 +2,7 @@
2
2
  * `iapeer-memory status` — read-only diagnostics of the whole chain
3
3
  * (ADR-009: the status surface must diagnose a socket without a system).
4
4
  * Aggregates: verify checks (NO repair) + slot declaration + a live TCP
5
- * probe of the MCP endpoint + the inbox load. Never mutates anything;
5
+ * probe of the MCP endpoint. Never mutates anything;
6
6
  * exit 1 when something needs attention.
7
7
  */
8
8
 
@@ -26,7 +26,7 @@ import { runVerify } from "./verify.js";
26
26
  * back must say so here and why.
27
27
  *
28
28
  * Source of truth ladder: (1) the LIVE pipeline as reported by the running
29
- * memoryd (a real vault_search call over MCP — per-component statuses, the
29
+ * memoryd (a real memory_search call over MCP — per-component statuses, the
30
30
  * same object every search returns); (2) when memoryd is down — the static
31
31
  * configuration view + the sqlite runtime probe. P3c live-smoke fact: in
32
32
  * the compiled binary the sqlite-vec dylib does not resolve from /$bunfs —
@@ -50,7 +50,7 @@ export function searchPipelineLine(env: Record<string, string | undefined>): str
50
50
  }
51
51
 
52
52
  /** Live pipeline from the running memoryd — the same per-component statuses
53
- * every vault_search returns. Null when memoryd is unreachable. */
53
+ * every memory_search returns. Null when memoryd is unreachable. */
54
54
  export async function probeSearchPipeline(
55
55
  egress: Egress,
56
56
  port: number,
@@ -68,7 +68,7 @@ export async function probeSearchPipeline(
68
68
  jsonrpc: "2.0",
69
69
  id: 1,
70
70
  method: "tools/call",
71
- params: { name: "vault_search", arguments: { query: "status probe" } },
71
+ params: { name: "memory_search", arguments: { query: "status probe" } },
72
72
  }),
73
73
  signal: AbortSignal.timeout(8000),
74
74
  });
@@ -145,23 +145,5 @@ export async function cmdStatus(argv: string[], egress: Egress): Promise<number>
145
145
  (livePipeline ?? `${searchPipelineLine(process.env)} (memoryd down — static view)`),
146
146
  );
147
147
 
148
- const vault = process.env.IAPEER_MEMORY_VAULT_PATH ?? "";
149
- const localeRaw = process.env.IAPEER_MEMORY_LOCALE || "en";
150
- if (vault && isLocaleId(localeRaw)) {
151
- const inboxDir = path.join(vault, getTaxonomy(localeRaw).folders.inbox);
152
- let count = 0;
153
- try {
154
- count = fs.readdirSync(inboxDir).filter((f) => f.endsWith(".md")).length;
155
- } catch {
156
- count = -1;
157
- }
158
- console.log(
159
- ` ${"inbox".padEnd(width)} ` +
160
- (count < 0
161
- ? `folder missing (${inboxDir})`
162
- : `${count} draft(s) awaiting the pipeline (a growing pile = the scriber thread is stuck; the sweep places stale drafts unvetted)`),
163
- );
164
- }
165
-
166
148
  return results.some((r) => r.status === "fail") ? 1 : 0;
167
149
  }
@@ -27,7 +27,7 @@ import { sweepUnprovision } from "../surfaces/sweep.js";
27
27
  import { guardedUnlinkSync } from "@agfpd/iapeer-memory-core";
28
28
  import {
29
29
  DREAM_TRIGGER_ID,
30
- SWEEP_TRIGGER_ID,
30
+ LEGACY_SWEEP_TRIGGER_ID,
31
31
  unregisterTimer,
32
32
  unregisterWatcher,
33
33
  WATCHER_TRIGGER_ID,
@@ -160,7 +160,7 @@ export function cmdUninstall(argv: string[], egress: Egress): number {
160
160
  : `unregister not sent (${unreg.detail}) — remove the trigger manually via the watcher peer`
161
161
  }`,
162
162
  );
163
- for (const id of [SWEEP_TRIGGER_ID, DREAM_TRIGGER_ID]) {
163
+ for (const id of [LEGACY_SWEEP_TRIGGER_ID, DREAM_TRIGGER_ID]) {
164
164
  const t = unregisterTimer(egress, { id, iapeerBin });
165
165
  console.log(
166
166
  `timer : ${