@agfpd/iapeer-memory 0.2.9 → 0.3.1
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 +2 -2
- package/src/cli.ts +2 -3
- package/src/commands/archive-stale.ts +6 -5
- package/src/commands/hook.ts +68 -35
- package/src/commands/init.ts +54 -37
- package/src/commands/memoryd.ts +16 -3
- package/src/commands/status.ts +4 -22
- package/src/commands/uninstall.ts +2 -2
- package/src/commands/update.ts +39 -31
- package/src/commands/verify.ts +27 -43
- package/src/paths.ts +0 -3
- package/src/provision.ts +14 -2
- package/src/surfaces/claude.ts +17 -12
- package/src/templates/guide-en.ts +63 -63
- package/src/templates/guide-ru.ts +60 -56
- package/src/templates/roles-en.ts +123 -147
- package/src/templates/roles-ru.ts +107 -134
- package/src/templates/skills.ts +13 -15
- package/src/watcher.ts +7 -74
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agfpd/iapeer-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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.
|
|
30
|
+
"@agfpd/iapeer-memory-core": "0.3.1"
|
|
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
|
-
|
|
76
|
-
staleness
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
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. (
|
|
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
|
}
|
package/src/commands/hook.ts
CHANGED
|
@@ -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.
|
|
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)
|
|
260
|
-
*
|
|
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
|
-
|
|
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
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
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
|
|
310
|
+
export async function collectDedupAndLinkHints(
|
|
296
311
|
eventJson: string,
|
|
297
312
|
egress: Egress,
|
|
298
313
|
env: Record<string, string | undefined> = process.env,
|
|
299
|
-
): Promise<
|
|
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 —
|
|
320
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
367
|
+
return { dup, link };
|
|
345
368
|
}
|
|
346
369
|
|
|
347
|
-
/** Combine the sync tag-teaching output with async dedup hints into
|
|
348
|
-
* additionalContext blob (or null when
|
|
349
|
-
export function mergeHookOutput(tagOutput: 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
|
-
|
|
359
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
|
470
|
-
const output = mergeHookOutput(result.output,
|
|
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
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -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
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
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
|
-
? `
|
|
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
|
-
|
|
399
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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):
|
package/src/commands/memoryd.ts
CHANGED
|
@@ -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
|
-
*
|
|
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,
|
package/src/commands/status.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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 [
|
|
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 : ${
|