@agfpd/iapeer-memory-core 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 +1 -1
- package/src/archive.ts +1 -1
- package/src/config.ts +7 -10
- package/src/db.ts +2 -2
- package/src/embedding.ts +1 -1
- package/src/frontmatter-fill.ts +20 -55
- package/src/graph.ts +1 -1
- package/src/human-edit-detect.ts +42 -47
- package/src/index-render.ts +1 -1
- package/src/index.ts +17 -0
- package/src/indexer.ts +1 -1
- package/src/mcp-tools.ts +17 -13
- package/src/memoryd.ts +156 -198
- package/src/mode.ts +76 -0
- package/src/permanent-detect.ts +4 -82
- package/src/search.ts +34 -10
- package/src/silent-edit-detect.ts +11 -20
- package/src/taxonomy.ts +5 -15
- package/src/utils.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agfpd/iapeer-memory-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "iapeer-memory core — host-neutral TypeScript memory primitive: vault schema/taxonomy config, search engine, memoryd, context renderer, role contracts. Consumed by the @agfpd/iapeer-memory facade; version kept in lockstep by its release flow (docs/10-distribution.md).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/archive.ts
CHANGED
|
@@ -24,7 +24,7 @@ function firstSegment(relPath: string): string {
|
|
|
24
24
|
* Folders whose notes are subject to archiving — UNIFIED rule, no exceptions
|
|
25
25
|
* (decision Артур 15.06, §2.2a): the six monitored content folders (five
|
|
26
26
|
* canonical permanent + agent memory + `03_Projects`). NOT the archive
|
|
27
|
-
* itself
|
|
27
|
+
* itself or the system folder. A completed phase/project
|
|
28
28
|
* (status `completed`/`cancelled`) is stale like any other note and moves to
|
|
29
29
|
* the archive; active `03_Projects` then shows only live work. Wikilinks
|
|
30
30
|
* survive by title; the archive stays searchable.
|
package/src/config.ts
CHANGED
|
@@ -50,15 +50,13 @@ export type CoreConfig = {
|
|
|
50
50
|
dbPath: string;
|
|
51
51
|
fullScanOnStartup: boolean;
|
|
52
52
|
};
|
|
53
|
-
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
/** Каденция курации (директива Артура 10.06 ~15:31): канон-правки уходят
|
|
54
|
+
* ПАЧКОЙ, не событиями — правки должны устаканиться, 10 пишущих агентов
|
|
55
|
+
* не дёргают конвейер постоянно. */
|
|
56
56
|
batch: {
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
* как в старом контуре: человек пишет долго, ночная партия). */
|
|
61
|
-
humanInboxHour: number;
|
|
57
|
+
/** CURATOR_TICK period, ms (default 6h) — the curation pass over changed
|
|
58
|
+
* canon (Scriber/Index links + health). */
|
|
59
|
+
curatorMs: number;
|
|
62
60
|
};
|
|
63
61
|
/**
|
|
64
62
|
* MCP-http endpoint of memoryd (ADR-012). The default port 8766 is the
|
|
@@ -261,8 +259,7 @@ export function configFromEnv(): CoreConfig {
|
|
|
261
259
|
fullScanOnStartup: envBoolean("IAPEER_MEMORY_FULL_SCAN_ON_STARTUP", true),
|
|
262
260
|
},
|
|
263
261
|
batch: {
|
|
264
|
-
|
|
265
|
-
humanInboxHour: envNumber("IAPEER_MEMORY_HUMAN_INBOX_HOUR", 4),
|
|
262
|
+
curatorMs: envNumber("IAPEER_MEMORY_CURATOR_TICK_SECS", 6 * 3600) * 1000,
|
|
266
263
|
},
|
|
267
264
|
mcp: {
|
|
268
265
|
port: envNumber("IAPEER_MEMORY_MCP_PORT", 8766),
|
package/src/db.ts
CHANGED
|
@@ -144,7 +144,7 @@ export function openDatabase(config: CoreConfig, options: OpenDatabaseOptions =
|
|
|
144
144
|
|
|
145
145
|
-- Wikilinks that could not be resolved to a real note. Kept first-class
|
|
146
146
|
-- instead of being silently dropped from edges: a missing/ambiguous link
|
|
147
|
-
-- is a vault health signal (surfaced via
|
|
147
|
+
-- is a vault health signal (surfaced via memory_map orphan_wikilinks +
|
|
148
148
|
-- the Index nightly health-check). reason ∈ 'missing' | 'ambiguous'.
|
|
149
149
|
CREATE TABLE IF NOT EXISTS unresolved_links (
|
|
150
150
|
source_path TEXT NOT NULL,
|
|
@@ -536,7 +536,7 @@ export function documentExists(db: CoreDb, docPath: string): boolean {
|
|
|
536
536
|
|
|
537
537
|
/**
|
|
538
538
|
* Unresolved wikilinks (missing target / ambiguous basename across folders).
|
|
539
|
-
* First-class health signal — surfaced through
|
|
539
|
+
* First-class health signal — surfaced through memory_map's opt-in
|
|
540
540
|
* `orphan_wikilinks` part and the Index nightly health-check.
|
|
541
541
|
*/
|
|
542
542
|
export function getUnresolvedLinks(
|
package/src/embedding.ts
CHANGED
|
@@ -45,7 +45,7 @@ export type EmbeddingConfig = {
|
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* Status of a single embedding call, surfaced up to the
|
|
48
|
+
* Status of a single embedding call, surfaced up to the memory_search
|
|
49
49
|
* response (`pipeline.embedding`).
|
|
50
50
|
*
|
|
51
51
|
* - `ok` — valid response;
|
package/src/frontmatter-fill.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fill a vault note's frontmatter according to its zone
|
|
3
|
-
* (
|
|
3
|
+
* (permanent / memory). The страж is the SHARED fill logic — the same
|
|
4
|
+
* functions run the agent hook path (processFile) and the human path
|
|
5
|
+
* (decideUpdate, human-edit-detect.ts). Inbox zones were removed with the
|
|
6
|
+
* direct-to-canon model: authors write straight into the typed canon folders.
|
|
4
7
|
*
|
|
5
8
|
* TS port of the reference `scripts/mergemind-frontmatter-fill.py`
|
|
6
9
|
* (behavioural parity against `tests/python/test_frontmatter_fill.py`,
|
|
@@ -16,20 +19,16 @@
|
|
|
16
19
|
* - **identity = peer personality** (нюанс 10): `resolveAgentName` prefers
|
|
17
20
|
* `PEER_PERSONALITY`, falling back to `IAPEER_MEMORY_AGENT_NAME`.
|
|
18
21
|
*
|
|
19
|
-
* Zone behaviour
|
|
20
|
-
* -
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* -
|
|
28
|
-
*
|
|
29
|
-
* - memory: PERMANENT service-field semantics + idempotent fill of the
|
|
30
|
-
* constants. `author` is parsed from the subfolder name, NOT
|
|
31
|
-
* from the caller identity — load-bearing for DreamWeaver
|
|
32
|
-
* writing into a foreign subfolder on an Index task.
|
|
22
|
+
* Zone behaviour:
|
|
23
|
+
* - permanent: FULL canon fill (`fillPermanentFull`) — title ← filename,
|
|
24
|
+
* type/status ← the folder's genre (§2.1), created, author for
|
|
25
|
+
* non-curators + needs_review, and an UPSERT of the stamp pair
|
|
26
|
+
* (last_edited_by, updated). The author now hand-writes only
|
|
27
|
+
* body + tags + inline links + title; the rest is deterministic.
|
|
28
|
+
* - memory: service-field semantics + idempotent fill of the constants.
|
|
29
|
+
* `author` is parsed from the subfolder name, NOT from the caller
|
|
30
|
+
* identity — load-bearing for DreamWeaver writing into a foreign
|
|
31
|
+
* subfolder on an Index task.
|
|
33
32
|
*
|
|
34
33
|
* The YAML-safe normalisation of `description` is load-bearing: typographic
|
|
35
34
|
* guillemets `«…»` are a display convention, not YAML quotes; any `: ` inside
|
|
@@ -47,7 +46,7 @@ import { guardedWriteFileSync, guardedUnlinkSync } from "./fs-guard.js";
|
|
|
47
46
|
|
|
48
47
|
const FRONTMATTER_RE = /^---[^\S\n]*\n([\s\S]*?\n)---[^\S\n]*(?:\n|$)/;
|
|
49
48
|
|
|
50
|
-
export const VALID_ZONES = ["
|
|
49
|
+
export const VALID_ZONES = ["permanent", "memory"] as const;
|
|
51
50
|
export type Zone = (typeof VALID_ZONES)[number];
|
|
52
51
|
|
|
53
52
|
/**
|
|
@@ -202,8 +201,9 @@ export function parseMemoryAuthor(
|
|
|
202
201
|
* Zone of a file by the first path segment relative to the vault — the
|
|
203
202
|
* single source of truth for zone routing (the reference de-duplicated a
|
|
204
203
|
* bash `case` into exactly this function). Folder whitelist comes from the
|
|
205
|
-
* taxonomy preset (ADR-002):
|
|
206
|
-
*
|
|
204
|
+
* taxonomy preset (ADR-002): archive and system are NOT in the whitelist →
|
|
205
|
+
* null → caller no-ops. (Inbox zones removed with the direct-to-canon model:
|
|
206
|
+
* authors write straight into the typed canon folders, the страж fills.)
|
|
207
207
|
*/
|
|
208
208
|
export function resolveZone(
|
|
209
209
|
filePath: string,
|
|
@@ -215,7 +215,6 @@ export function resolveZone(
|
|
|
215
215
|
if (!parts || parts.length === 0) return null;
|
|
216
216
|
const head = parts[0];
|
|
217
217
|
const f = taxonomy.folders;
|
|
218
|
-
if (head === f.inbox) return "inbox";
|
|
219
218
|
if (
|
|
220
219
|
head === f.knowledge ||
|
|
221
220
|
head === f.decisions ||
|
|
@@ -229,38 +228,6 @@ export function resolveZone(
|
|
|
229
228
|
return null;
|
|
230
229
|
}
|
|
231
230
|
|
|
232
|
-
export function fillInbox(
|
|
233
|
-
fmBlock: string,
|
|
234
|
-
opts: { path: string; agent: string; today: string; nowStamp: string; ctx: FillContext },
|
|
235
|
-
): string {
|
|
236
|
-
const { taxonomy } = opts.ctx;
|
|
237
|
-
// STAMP PAIR (дефект-репро boris 11.06, ложный unstamped): upsert как в
|
|
238
|
-
// fillPermanent/fillMemory. Без неё у inbox-черновиков пара тождественно
|
|
239
|
-
// (null, null) — «штамп не двинулся» детектора немых записей выполняется
|
|
240
|
-
// на ЛЮБОЙ легитимной хук-правке, и inbox-ветка вырождается в «hash
|
|
241
|
-
// двинулся → unstamped» (7 ложных ре-стампов за день 11.06). Оба поля
|
|
242
|
-
// сервисные для smart-hash → семантический hash и INBOX_NEW-диффы не
|
|
243
|
-
// двигаются, эхо невозможно. Побочный выигрыш: source-фильтр кураторов
|
|
244
|
-
// INBOX_NEW впервые получает живой last_edited_by.
|
|
245
|
-
fmBlock = upsert(fmBlock, "last_edited_by", opts.agent);
|
|
246
|
-
fmBlock = upsert(fmBlock, "updated", opts.nowStamp);
|
|
247
|
-
fmBlock = setIfMissing(fmBlock, "title", basenameNoExt(opts.path));
|
|
248
|
-
fmBlock = setIfMissing(fmBlock, "status", taxonomy.statusTokens.draft);
|
|
249
|
-
fmBlock = setIfMissing(fmBlock, "created", opts.today);
|
|
250
|
-
// AUTHOR GUARD (инвариант Артура, инверсия 10.06): агент curator-set
|
|
251
|
-
// НИКОГДА не становится author в inbox-зоне. Курайторы трогают черновики
|
|
252
|
-
// только как курирование (rename/правки стиля — горячая дорожка
|
|
253
|
-
// инвертированного конвейера: копирайтер пишет НОВЫЙ файл при ренейме);
|
|
254
|
-
// бесхозный файл от курайтора остаётся без author до явной атрибуции —
|
|
255
|
-
// видимая аномалия лучше тихо присвоенного авторства. Существующий
|
|
256
|
-
// author и без гарда неприкосновенен (setIfMissing).
|
|
257
|
-
if (!isCurator(opts.agent, opts.ctx)) {
|
|
258
|
-
fmBlock = setIfMissing(fmBlock, "author", opts.agent);
|
|
259
|
-
fmBlock = setIfMissing(fmBlock, "needs_review", "true");
|
|
260
|
-
}
|
|
261
|
-
return fmBlock;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
231
|
/**
|
|
265
232
|
* Full permanent-zone (canon) fill — the lean §2 «страж» core, SHARED between
|
|
266
233
|
* the post-write hook (`processFile`) and the human-edit detector
|
|
@@ -288,7 +255,7 @@ export function fillPermanentFull(
|
|
|
288
255
|
): string {
|
|
289
256
|
const { taxonomy } = opts.ctx;
|
|
290
257
|
// Service stamp (always) — load-bearing for smart-hash echo-safety and the
|
|
291
|
-
// unstamped detector, symmetric with
|
|
258
|
+
// unstamped detector, symmetric with fillMemory.
|
|
292
259
|
fmBlock = upsert(fmBlock, "last_edited_by", opts.agent);
|
|
293
260
|
fmBlock = upsert(fmBlock, "updated", opts.nowStamp);
|
|
294
261
|
// Canon frontmatter the author no longer hand-writes (§2.1). setIfMissing —
|
|
@@ -689,9 +656,7 @@ export function processFile(filePath: string, opts: ProcessOptions): boolean {
|
|
|
689
656
|
|
|
690
657
|
let newFm: string | null;
|
|
691
658
|
let newBody = rest;
|
|
692
|
-
if (zone === "
|
|
693
|
-
newFm = fillInbox(fmBlock, { path: filePath, agent: opts.agent, today, nowStamp, ctx });
|
|
694
|
-
} else if (zone === "permanent") {
|
|
659
|
+
if (zone === "permanent") {
|
|
695
660
|
newFm = fillPermanentFull(fmBlock, {
|
|
696
661
|
path: filePath,
|
|
697
662
|
agent: opts.agent,
|
package/src/graph.ts
CHANGED
|
@@ -54,7 +54,7 @@ function loadGraph(db: CoreDb, agentMemoryMarker: string): { adj: AdjMap; direct
|
|
|
54
54
|
target_path: string;
|
|
55
55
|
}>;
|
|
56
56
|
|
|
57
|
-
//
|
|
57
|
+
// memory_map is the CANONICAL / shared topology — used by the Index nightly
|
|
58
58
|
// health-check (orphans, hubs, bridges). Agent operative notes are personal
|
|
59
59
|
// memory: excluding them here keeps that health picture honest (a
|
|
60
60
|
// canonically-isolated note must read as orphan even if some agent journaled
|
package/src/human-edit-detect.ts
CHANGED
|
@@ -10,15 +10,15 @@
|
|
|
10
10
|
* hash persistence) arrives with the memoryd stage; everything
|
|
11
11
|
* load-bearing (the anti-loop decision core) lives here, pure.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
13
|
+
* Canon folders (the five typed + project notes) — the human writes there
|
|
14
|
+
* directly from an external editor:
|
|
14
15
|
* - `last_edited_by == human` AND `updated` fresh → echo of our own
|
|
15
16
|
* write — skip;
|
|
16
17
|
* - `last_edited_by` is an agent AND fresh → the post-write hook just
|
|
17
18
|
* ran — skip (the hook's zone);
|
|
18
|
-
* - otherwise → an external-editor edit:
|
|
19
|
-
* `last_edited_by: <human>` +
|
|
20
|
-
*
|
|
21
|
-
* HUMAN-INBOX mode: anti-loop as above, then the idempotent 4-field fill.
|
|
19
|
+
* - otherwise → an external-editor edit: the shared `fillPermanentFull`
|
|
20
|
+
* completes a bare-body note and stamps `last_edited_by: <human>` +
|
|
21
|
+
* `updated` + `needs_review: true`.
|
|
22
22
|
*
|
|
23
23
|
* `freshEditWindowS` is CONFIG (the stage-7 review note): default 90s —
|
|
24
24
|
* the reference VALUE IS 90 (bumped from the historical 30: second-
|
|
@@ -32,7 +32,12 @@
|
|
|
32
32
|
import crypto from "node:crypto";
|
|
33
33
|
import path from "node:path";
|
|
34
34
|
import type { TaxonomyPreset } from "./taxonomy.js";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
fillPermanentFull,
|
|
37
|
+
stripEmptyArrays,
|
|
38
|
+
normalizeAllScalars,
|
|
39
|
+
normalizeLinksBlock,
|
|
40
|
+
} from "./frontmatter-fill.js";
|
|
36
41
|
|
|
37
42
|
export const DEFAULT_FRESH_EDIT_WINDOW_S = 90;
|
|
38
43
|
|
|
@@ -85,9 +90,11 @@ export function setIfMissing(fm: string, key: string, value: string): string {
|
|
|
85
90
|
return `${fm}${tail}${key}: ${value}\n`;
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
export type HumanEditZone = "permanent"
|
|
93
|
+
export type HumanEditZone = "permanent";
|
|
89
94
|
|
|
90
|
-
/** Zone of a file for the detector: permanent
|
|
95
|
+
/** Zone of a file for the detector: permanent (the typed canon folders) /
|
|
96
|
+
* null. The human writes straight into canon from Obsidian — the страж
|
|
97
|
+
* completes a bare-body note via fillPermanentFull. */
|
|
91
98
|
export function getZone(
|
|
92
99
|
filepath: string,
|
|
93
100
|
vault: string,
|
|
@@ -96,7 +103,6 @@ export function getZone(
|
|
|
96
103
|
const rel = path.relative(vault, filepath);
|
|
97
104
|
const first = rel.split(path.sep)[0];
|
|
98
105
|
const f = taxonomy.folders;
|
|
99
|
-
if (first === f.inboxHuman) return "human-inbox";
|
|
100
106
|
if (
|
|
101
107
|
first === f.knowledge ||
|
|
102
108
|
first === f.decisions ||
|
|
@@ -180,54 +186,43 @@ export function decideUpdate(input: DecideUpdateInput): DecideUpdateResult {
|
|
|
180
186
|
return { action: "skip", recordHash: currentHash, reason: "echo-agent" };
|
|
181
187
|
}
|
|
182
188
|
|
|
183
|
-
// Otherwise — an external-editor edit
|
|
189
|
+
// Otherwise — an external-editor edit of a canon note. The SHARED guard
|
|
190
|
+
// fill (mandate §2: identical to the hook path). Existing notes: stamp-only
|
|
191
|
+
// no-op on the constants; a human's bare-body canon note: full frontmatter
|
|
192
|
+
// (title/type-from-folder/status/created/author). `created` ← birthtime
|
|
193
|
+
// (file creation, not edit time).
|
|
184
194
|
const nowStamp = formatStamp(new Date(input.nowMs));
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
newFm = setIfMissing(newFm, "status", input.taxonomy.statusTokens.draft);
|
|
199
|
-
newFm = setIfMissing(newFm, "created", createdDate);
|
|
200
|
-
newFm = setIfMissing(newFm, "author", input.human);
|
|
201
|
-
newFm = setIfMissing(newFm, "needs_review", "true");
|
|
202
|
-
} else {
|
|
203
|
-
// PERMANENT (canon) — the SHARED guard fill (mandate §2: identical to the
|
|
204
|
-
// hook path). Existing notes: stamp-only no-op on the constants; a human's
|
|
205
|
-
// bare-body canon note: full frontmatter (title/type-from-folder/status/
|
|
206
|
-
// created/author). `created` ← birthtime (file creation, not edit time).
|
|
207
|
-
const createdSource =
|
|
208
|
-
input.birthtimeMs > 0 ? new Date(input.birthtimeMs) : new Date(input.mtimeMs);
|
|
209
|
-
const createdDate = createdSource.toISOString().slice(0, 10);
|
|
210
|
-
newFm = fillPermanentFull(newFm, {
|
|
211
|
-
path: input.path,
|
|
212
|
-
agent: input.human,
|
|
213
|
-
vault: input.vault,
|
|
214
|
-
today: createdDate,
|
|
215
|
-
nowStamp,
|
|
216
|
-
ctx: { taxonomy: input.taxonomy },
|
|
217
|
-
});
|
|
218
|
-
}
|
|
195
|
+
const createdSource =
|
|
196
|
+
input.birthtimeMs > 0 ? new Date(input.birthtimeMs) : new Date(input.mtimeMs);
|
|
197
|
+
// LOCAL date (symmetric with the hook path's localDateIso) — `.toISOString()`
|
|
198
|
+
// is UTC and would shift `created` by a day for early-local-time creations.
|
|
199
|
+
const createdDate = `${createdSource.getFullYear()}-${pad(createdSource.getMonth() + 1)}-${pad(createdSource.getDate())}`;
|
|
200
|
+
let newFm = fillPermanentFull(fmBlock, {
|
|
201
|
+
path: input.path,
|
|
202
|
+
agent: input.human,
|
|
203
|
+
vault: input.vault,
|
|
204
|
+
today: createdDate,
|
|
205
|
+
nowStamp,
|
|
206
|
+
ctx: { taxonomy: input.taxonomy },
|
|
207
|
+
});
|
|
219
208
|
|
|
220
209
|
if (newFm === fmBlock) {
|
|
221
210
|
return { action: "skip", recordHash: currentHash, reason: "noop" };
|
|
222
211
|
}
|
|
223
212
|
|
|
213
|
+
// Mirror the hook path's normalization (mandate §2: the страж is IDENTICAL on
|
|
214
|
+
// both paths) — YAML-safety on every scalar + ## Связи structural validity, so
|
|
215
|
+
// a human's colon-bearing scalar never produces unparseable YAML that silently
|
|
216
|
+
// drops the note from the index.
|
|
217
|
+
newFm = normalizeAllScalars(stripEmptyArrays(newFm));
|
|
218
|
+
const newBody = normalizeLinksBlock(body, input.taxonomy);
|
|
224
219
|
const fmTail = newFm.endsWith("\n") ? "" : "\n";
|
|
225
|
-
const bodyPrefix =
|
|
226
|
-
const newContent = `---\n${newFm}${fmTail}---${bodyPrefix}${
|
|
220
|
+
const bodyPrefix = newBody.startsWith("\n") ? "" : "\n";
|
|
221
|
+
const newContent = `---\n${newFm}${fmTail}---${bodyPrefix}${newBody}`;
|
|
227
222
|
return {
|
|
228
223
|
action: "write",
|
|
229
224
|
newContent,
|
|
230
225
|
recordHash: sha256(newContent),
|
|
231
|
-
reason:
|
|
226
|
+
reason: "stamp",
|
|
232
227
|
};
|
|
233
228
|
}
|
package/src/index-render.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - **taxonomy/ranking config instead of constants** (ADR-002/011): folder
|
|
9
9
|
* names, type/subtype tokens, status boost GROUPS and coefficients come
|
|
10
10
|
* from the same config the search pipeline uses — bucket synchronisation
|
|
11
|
-
* with `
|
|
11
|
+
* with `memory_search` holds BY CONSTRUCTION (one source), where the
|
|
12
12
|
* reference kept two hand-synchronised constant sets.
|
|
13
13
|
* - **personality is a parameter**: identity resolution (PEER_PERSONALITY
|
|
14
14
|
* first) happens at the call level, never inside the renderer.
|
package/src/index.ts
CHANGED
|
@@ -42,6 +42,23 @@ export {
|
|
|
42
42
|
} from "./frontmatter-fill.js";
|
|
43
43
|
export { fmUpdate, collectOps, yamlSafeScalar, type FmUpdateOptions, type Op } from "./fm-update.js";
|
|
44
44
|
|
|
45
|
+
// mode + per-role proactivity (lean §7/§7.1)
|
|
46
|
+
export {
|
|
47
|
+
resolveMode,
|
|
48
|
+
curationPlan,
|
|
49
|
+
type MemoryMode,
|
|
50
|
+
type RoleSet,
|
|
51
|
+
type CurationPlan,
|
|
52
|
+
} from "./mode.js";
|
|
53
|
+
|
|
54
|
+
// dedup + link-hint bands (lean §3a/§3b)
|
|
55
|
+
export {
|
|
56
|
+
runDedup,
|
|
57
|
+
DEFAULT_DEDUP_THRESHOLD,
|
|
58
|
+
DEFAULT_LINK_HINT_THRESHOLD,
|
|
59
|
+
type DedupMatch,
|
|
60
|
+
} from "./search.js";
|
|
61
|
+
|
|
45
62
|
// deterministic archiving (lean §2.2a)
|
|
46
63
|
export {
|
|
47
64
|
isArchivableZone,
|
package/src/indexer.ts
CHANGED
|
@@ -70,7 +70,7 @@ export function addTitlePath(
|
|
|
70
70
|
* bare basename resolves only when exactly one note has it — never the last
|
|
71
71
|
* indexed one. Unresolvable links are NOT silently dropped (Audit #5): they
|
|
72
72
|
* move to `unresolved_links` with a reason (`missing` | `ambiguous`) so the
|
|
73
|
-
*
|
|
73
|
+
* memory_map / nightly health-check can see vault rot. The map carries ALL
|
|
74
74
|
* indexed files including unchanged ones (Audit #1), so a link to a note that
|
|
75
75
|
* simply wasn't re-parsed this run still resolves instead of being dropped.
|
|
76
76
|
*
|
package/src/mcp-tools.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { agentMemoryFolderMarker } from "./taxonomy.js";
|
|
|
19
19
|
// синхронный fetch с iCloud, без таймаута Node event loop замораживается
|
|
20
20
|
// на минуты. 30 секунд — большой запас, на нормальной сети iCloud
|
|
21
21
|
// укладывается за 5-15 секунд. При срабатывании пишем warn в stderr и
|
|
22
|
-
// возвращаем not-found, чтобы caller мог упасть на
|
|
22
|
+
// возвращаем not-found, чтобы caller мог упасть на memory_search fallback.
|
|
23
23
|
const READ_TIMEOUT_MS = 30000;
|
|
24
24
|
|
|
25
25
|
function isAbortError(err: unknown): boolean {
|
|
@@ -28,7 +28,7 @@ function isAbortError(err: unknown): boolean {
|
|
|
28
28
|
return e.name === "AbortError" || e.code === "ABORT_ERR";
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// ----
|
|
31
|
+
// ---- memory_search ----
|
|
32
32
|
|
|
33
33
|
export async function runSearch(
|
|
34
34
|
db: CoreDb,
|
|
@@ -47,12 +47,12 @@ export async function runSearch(
|
|
|
47
47
|
/**
|
|
48
48
|
* Public MCP tool surface (ADR-008): exactly three read-only tools.
|
|
49
49
|
* `vault_read` is deliberately NOT part of the surface — in-session reading
|
|
50
|
-
* is the harness's native Read (after
|
|
51
|
-
* backlinks are covered by
|
|
50
|
+
* is the harness's native Read (after memory_search the path is known), and
|
|
51
|
+
* backlinks are covered by memory_related(depth=1, incoming). `runRead` below
|
|
52
52
|
* stays a LIBRARY function of core — for memoryd, the Index runtime, CLI and
|
|
53
53
|
* programmatic consumers outside harness sessions.
|
|
54
54
|
*/
|
|
55
|
-
export const MCP_TOOL_SURFACE = ["
|
|
55
|
+
export const MCP_TOOL_SURFACE = ["memory_search", "memory_related", "memory_map"] as const;
|
|
56
56
|
|
|
57
57
|
// ---- vault_read — library read function (NOT on the MCP surface, ADR-008) ----
|
|
58
58
|
|
|
@@ -60,7 +60,7 @@ export const MCP_TOOL_SURFACE = ["vault_search", "vault_graph", "vault_map"] as
|
|
|
60
60
|
* Validate and resolve a user-supplied vault path before any disk access.
|
|
61
61
|
*
|
|
62
62
|
* vault_read is reachable from any agent that loads the reference plugin,
|
|
63
|
-
* including ones whose context can be poisoned by
|
|
63
|
+
* including ones whose context can be poisoned by untrusted note content (prompt
|
|
64
64
|
* injection). Without containment, an attacker-controlled `path` like
|
|
65
65
|
* `../../.ssh/id_rsa`, `../../.mergemind/env`, or `/etc/passwd` would
|
|
66
66
|
* exfiltrate arbitrary files the MCP process can read.
|
|
@@ -110,8 +110,8 @@ function validateVaultPath(
|
|
|
110
110
|
throw new Error("Path must not contain empty, '.' or '..' segments");
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
// Respect excludeFolders even for direct disk reads.
|
|
114
|
-
//
|
|
113
|
+
// Respect excludeFolders even for direct disk reads. The system folder is
|
|
114
|
+
// intentionally hidden from search — leaking it
|
|
115
115
|
// through vault_read would defeat the privacy contract excludeFolders
|
|
116
116
|
// is supposed to provide. Same "Document not found" wording as the
|
|
117
117
|
// not-in-index branch so we don't reveal whether the path exists.
|
|
@@ -145,8 +145,8 @@ export async function runRead(
|
|
|
145
145
|
const meta = getDocumentMeta(db, docPath);
|
|
146
146
|
|
|
147
147
|
// Fallback path: the requested document is not in the index (typically
|
|
148
|
-
// because it lives in an excluded folder like `99_System
|
|
149
|
-
//
|
|
148
|
+
// because it lives in an excluded folder like `99_System/`, or the watcher
|
|
149
|
+
// hasn't picked it up yet). Read it directly from disk and
|
|
150
150
|
// parse it on the fly — without backlinks, since those depend on the index.
|
|
151
151
|
if (!meta) {
|
|
152
152
|
let text: string;
|
|
@@ -234,7 +234,7 @@ export async function runRead(
|
|
|
234
234
|
};
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
// ----
|
|
237
|
+
// ---- memory_related ----
|
|
238
238
|
|
|
239
239
|
// Oneway-фильтр графа: backlinks из `06_Оперативка_агентов/` **не**
|
|
240
240
|
// показываются при запросе графа канонической заметки — граф референса не
|
|
@@ -257,7 +257,11 @@ export function runGraph(
|
|
|
257
257
|
const centerMeta = getDocumentMeta(db, docPath);
|
|
258
258
|
|
|
259
259
|
if (!centerMeta) {
|
|
260
|
-
return {
|
|
260
|
+
return {
|
|
261
|
+
found: false,
|
|
262
|
+
error: `Document not found: ${docPath}`,
|
|
263
|
+
hint: "If you don't have the exact path, find the note first with memory_search, then pass its `path` here.",
|
|
264
|
+
};
|
|
261
265
|
}
|
|
262
266
|
|
|
263
267
|
type Node = {
|
|
@@ -354,7 +358,7 @@ export function runGraph(
|
|
|
354
358
|
};
|
|
355
359
|
}
|
|
356
360
|
|
|
357
|
-
// ----
|
|
361
|
+
// ---- memory_map ----
|
|
358
362
|
|
|
359
363
|
// Summary-mode caps. Full topology of a 300+ note vault crosses 25KB JSON
|
|
360
364
|
// before it reaches the agent — most of that is per-cluster node lists and
|