@remnic/core 9.69.52 → 9.69.53
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/dist/access-admin-ops-surface.d.ts +1 -1
- package/dist/access-admin-ops-surface.js +4 -4
- package/dist/access-authorization-probe.d.ts +1 -1
- package/dist/access-authorization-probe.js +5 -5
- package/dist/access-boundary.d.ts +1 -1
- package/dist/access-boundary.js +4 -4
- package/dist/access-cli.js +20 -20
- package/dist/access-extraction-force-flush.d.ts +1 -1
- package/dist/access-extraction-force-flush.js +4 -4
- package/dist/access-http-briefing.d.ts +1 -1
- package/dist/access-http-briefing.js +5 -5
- package/dist/access-http-lcm-compaction.d.ts +1 -1
- package/dist/access-http-lifecycle-flush.d.ts +1 -1
- package/dist/access-http-offline-stream.d.ts +1 -1
- package/dist/access-http-query.js +4 -4
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +16 -16
- package/dist/access-identity-continuity-surface.js +4 -4
- package/dist/access-lcm-surface.d.ts +1 -1
- package/dist/access-lcm-surface.js +4 -4
- package/dist/access-mcp-shared-context-tools.d.ts +1 -1
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +11 -11
- package/dist/access-namespace-preflight.js +4 -4
- package/dist/access-observe-write-surface.d.ts +1 -1
- package/dist/access-observe-write-surface.js +4 -4
- package/dist/access-operations-batch.js +5 -5
- package/dist/access-operations.d.ts +1 -1
- package/dist/access-operations.js +10 -10
- package/dist/access-recall-concurrency.d.ts +1 -1
- package/dist/access-recall-concurrency.js +4 -4
- package/dist/access-recall-response.d.ts +1 -1
- package/dist/access-recall-response.js +4 -4
- package/dist/access-recall-surface.d.ts +1 -1
- package/dist/access-recall-surface.js +4 -4
- package/dist/access-schema.d.ts +113 -113
- package/dist/{access-service-CUNM5RfA.d.ts → access-service-H-qEdnSs.d.ts} +64 -64
- package/dist/access-service-helpers.d.ts +1 -1
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +4 -4
- package/dist/access-surface-catalog.d.ts +1 -1
- package/dist/access-surface-catalog.js +6 -6
- package/dist/{chunk-TEQQQTQS.js → chunk-2HJKAVAS.js} +2 -2
- package/dist/{chunk-HWT6KRGS.js → chunk-6UEM5EOB.js} +2 -2
- package/dist/{chunk-UCY4EHAG.js → chunk-A4TQFNRX.js} +3 -3
- package/dist/{chunk-72N2P44O.js → chunk-ARRKSBQU.js} +2 -2
- package/dist/{chunk-WBW34YCJ.js → chunk-BCXZ4BDS.js} +2 -2
- package/dist/{chunk-V5BU5PVE.js → chunk-CO7KBRPF.js} +2 -2
- package/dist/{chunk-HG42J2EX.js → chunk-EQNZC3BH.js} +2 -2
- package/dist/{chunk-35EH6KW4.js → chunk-EYK3UHAP.js} +2 -2
- package/dist/{chunk-DCRK7I5E.js → chunk-HUQMVI3A.js} +93 -4
- package/dist/chunk-HUQMVI3A.js.map +1 -0
- package/dist/{chunk-MSTKJI5T.js → chunk-K7FXR75S.js} +442 -318
- package/dist/chunk-K7FXR75S.js.map +1 -0
- package/dist/{chunk-U6CMLAP4.js → chunk-M7HFDWOS.js} +2 -2
- package/dist/{chunk-2UDAABV6.js → chunk-MTJUIIM4.js} +2 -2
- package/dist/{chunk-GRX6YH4E.js → chunk-MXTCV3B5.js} +2 -2
- package/dist/{chunk-6KCQNFDQ.js → chunk-SOOWBRMH.js} +8 -8
- package/dist/{chunk-SOFATSHJ.js → chunk-TS66MUUJ.js} +5 -5
- package/dist/{chunk-72AV2H3R.js → chunk-U3NBO4IX.js} +39 -3
- package/dist/chunk-U3NBO4IX.js.map +1 -0
- package/dist/{chunk-EW3Y73MN.js → chunk-UJBIWY5M.js} +2 -2
- package/dist/{chunk-SVUUOLKM.js → chunk-VMKAA5SU.js} +2 -2
- package/dist/{chunk-UQTXER7M.js → chunk-ZADYEZY4.js} +2 -2
- package/dist/{chunk-2INZSV27.js → chunk-ZANEMAEV.js} +2 -2
- package/dist/{cli-CCc34xV7.d.ts → cli-VGDSn2zY.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +17 -17
- package/dist/deep-recall-http-glue.d.ts +1 -1
- package/dist/deep-recall-mcp-tools.d.ts +1 -1
- package/dist/directory-sidecars.d.ts +30 -7
- package/dist/directory-sidecars.js +5 -1
- package/dist/external-wiki-access.d.ts +1 -1
- package/dist/external-wiki-access.js +5 -5
- package/dist/external-wiki-mcp-tools.d.ts +1 -1
- package/dist/index.d.ts +680 -680
- package/dist/index.js +20 -20
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/memory-browse-http-glue.d.ts +1 -1
- package/dist/memory-browse-mcp-tools.d.ts +1 -1
- package/dist/memory-browse-operations.js +5 -5
- package/dist/memory-browse.js +3 -3
- package/dist/memory-subject-cli.d.ts +1 -1
- package/dist/memory-subject.d.ts +1 -1
- package/dist/orchestrator.js +20 -20
- package/dist/{public-http-UWVcqMcb.d.ts → public-http-P0cnHzYB.d.ts} +1 -1
- package/dist/recall-navigation-http-glue.d.ts +1 -1
- package/dist/recall-navigation-mcp-tools.d.ts +1 -1
- package/dist/recall-navigation-operations.js +5 -5
- package/dist/recall-navigation.js +4 -4
- package/dist/recall-state-view-admission.js +3 -3
- package/dist/schemas.d.ts +160 -160
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/support-passport/index.d.ts +4 -4
- package/dist/support-passport/index.js +7 -7
- package/dist/transfer/types.d.ts +78 -78
- package/dist/who-knows-cli.d.ts +1 -1
- package/dist/who-knows.d.ts +1 -1
- package/package.json +2 -2
- package/src/directory-sidecars.test.ts +113 -0
- package/src/directory-sidecars.ts +70 -8
- package/src/experience/session-experience-mining.test.ts +186 -0
- package/src/experience/session-experience-mining.ts +90 -0
- package/src/orchestration/persistence-index.ts +50 -0
- package/src/orchestration/recall-recognition-index.test.ts +249 -0
- package/src/orchestration/recall-recognition-index.ts +169 -0
- package/src/procedural/procedure-miner.ts +8 -6
- package/src/testing/subjects/recall-recognition-index.test.ts +97 -0
- package/dist/chunk-72AV2H3R.js.map +0 -1
- package/dist/chunk-DCRK7I5E.js.map +0 -1
- package/dist/chunk-MSTKJI5T.js.map +0 -1
- /package/dist/{chunk-TEQQQTQS.js.map → chunk-2HJKAVAS.js.map} +0 -0
- /package/dist/{chunk-HWT6KRGS.js.map → chunk-6UEM5EOB.js.map} +0 -0
- /package/dist/{chunk-UCY4EHAG.js.map → chunk-A4TQFNRX.js.map} +0 -0
- /package/dist/{chunk-72N2P44O.js.map → chunk-ARRKSBQU.js.map} +0 -0
- /package/dist/{chunk-WBW34YCJ.js.map → chunk-BCXZ4BDS.js.map} +0 -0
- /package/dist/{chunk-V5BU5PVE.js.map → chunk-CO7KBRPF.js.map} +0 -0
- /package/dist/{chunk-HG42J2EX.js.map → chunk-EQNZC3BH.js.map} +0 -0
- /package/dist/{chunk-35EH6KW4.js.map → chunk-EYK3UHAP.js.map} +0 -0
- /package/dist/{chunk-U6CMLAP4.js.map → chunk-M7HFDWOS.js.map} +0 -0
- /package/dist/{chunk-2UDAABV6.js.map → chunk-MTJUIIM4.js.map} +0 -0
- /package/dist/{chunk-GRX6YH4E.js.map → chunk-MXTCV3B5.js.map} +0 -0
- /package/dist/{chunk-6KCQNFDQ.js.map → chunk-SOOWBRMH.js.map} +0 -0
- /package/dist/{chunk-SOFATSHJ.js.map → chunk-TS66MUUJ.js.map} +0 -0
- /package/dist/{chunk-EW3Y73MN.js.map → chunk-UJBIWY5M.js.map} +0 -0
- /package/dist/{chunk-SVUUOLKM.js.map → chunk-VMKAA5SU.js.map} +0 -0
- /package/dist/{chunk-UQTXER7M.js.map → chunk-ZADYEZY4.js.map} +0 -0
- /package/dist/{chunk-2INZSV27.js.map → chunk-ZANEMAEV.js.map} +0 -0
|
@@ -8,6 +8,8 @@ import { test } from "node:test";
|
|
|
8
8
|
import {
|
|
9
9
|
DIRECTORY_SIDECAR_BASENAME,
|
|
10
10
|
DIRECTORY_SIDECAR_MARKER,
|
|
11
|
+
NEIGHBORHOOD_ABSTRACT_LABEL,
|
|
12
|
+
applyDirectorySidecarDrillDown,
|
|
11
13
|
findDirectorySidecarsForQuery,
|
|
12
14
|
loadDirectorySidecar,
|
|
13
15
|
parseDirectorySidecarsEnabled,
|
|
@@ -385,3 +387,114 @@ test("write-path refresh stays inside the changed namespace and skips unrelated
|
|
|
385
387
|
rmSync(root, { recursive: true, force: true });
|
|
386
388
|
}
|
|
387
389
|
});
|
|
390
|
+
|
|
391
|
+
function hit(rel: string, snippet: string, score = 1) {
|
|
392
|
+
return { path: rel, snippet, score };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
test("drill-down is a no-op when disabled even if sidecars exist", async () => {
|
|
396
|
+
const root = store();
|
|
397
|
+
try {
|
|
398
|
+
await writeMemory(root, "facts/2026-01-02/fact-1.md", "Telescope calibration log for the north dome.");
|
|
399
|
+
await runDirectorySidecarMaintenance(root, true);
|
|
400
|
+
const incoming = [hit("facts/2026-01-02/fact-1.md", "Telescope calibration log")];
|
|
401
|
+
const out = await applyDirectorySidecarDrillDown("/does-not-exist", "telescope", incoming, {
|
|
402
|
+
enabled: false,
|
|
403
|
+
});
|
|
404
|
+
assert.deepEqual(out, incoming);
|
|
405
|
+
assert.equal(out[0].snippet, "Telescope calibration log");
|
|
406
|
+
} finally {
|
|
407
|
+
rmSync(root, { recursive: true, force: true });
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("drill-down attaches the neighborhood abstract to hits under a fresh sidecar", async () => {
|
|
412
|
+
const root = store();
|
|
413
|
+
try {
|
|
414
|
+
await writeMemory(root, "facts/2026-01-02/fact-1.md", "Telescope calibration log for the north dome.");
|
|
415
|
+
await writeMemory(root, "facts/2026-01-02/fact-2.md", "Mirror alignment drift measured at 3 arcmin.");
|
|
416
|
+
await writeMemory(root, "entities/crew-1.md", "Catering roster for the base kitchen.");
|
|
417
|
+
await runDirectorySidecarMaintenance(root, true);
|
|
418
|
+
|
|
419
|
+
const incoming = [
|
|
420
|
+
hit("facts/2026-01-02/fact-1.md", "Telescope calibration log"),
|
|
421
|
+
hit("entities/crew-1.md", "Catering roster"),
|
|
422
|
+
];
|
|
423
|
+
const out = await applyDirectorySidecarDrillDown(root, "telescope calibration", incoming, {
|
|
424
|
+
enabled: true,
|
|
425
|
+
});
|
|
426
|
+
assert.match(out[0].snippet, new RegExp(`^${NEIGHBORHOOD_ABSTRACT_LABEL} `));
|
|
427
|
+
assert.match(out[0].snippet, /Telescope/i);
|
|
428
|
+
assert.match(out[0].snippet, /Telescope calibration log$/m);
|
|
429
|
+
assert.equal(out[1].snippet, "Catering roster", "unrelated hit keeps its snippet");
|
|
430
|
+
assert.equal(out[0].path, incoming[0].path);
|
|
431
|
+
assert.equal(out[1].path, incoming[1].path);
|
|
432
|
+
} finally {
|
|
433
|
+
rmSync(root, { recursive: true, force: true });
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("drill-down skips stale sidecars and does not write", async () => {
|
|
438
|
+
const root = store();
|
|
439
|
+
try {
|
|
440
|
+
const dayDir = path.join(root, "facts", "2026-01-02");
|
|
441
|
+
await writeMemory(root, "facts/2026-01-02/fact-1.md", "Boiler pressure nominal.");
|
|
442
|
+
await runDirectorySidecarMaintenance(root, true);
|
|
443
|
+
await writeMemory(root, "facts/2026-01-02/fact-2.md", "Coolant pump replaced with spare unit.");
|
|
444
|
+
const stale = await loadDirectorySidecar(dayDir, { refresh: false });
|
|
445
|
+
assert.equal(stale?.fresh, false);
|
|
446
|
+
|
|
447
|
+
const mtimeBefore = (await stat(path.join(dayDir, DIRECTORY_SIDECAR_BASENAME))).mtimeMs;
|
|
448
|
+
const out = await applyDirectorySidecarDrillDown(
|
|
449
|
+
root,
|
|
450
|
+
"boiler",
|
|
451
|
+
[hit("facts/2026-01-02/fact-1.md", "Boiler pressure nominal.")],
|
|
452
|
+
{ enabled: true },
|
|
453
|
+
);
|
|
454
|
+
assert.equal(out[0].snippet, "Boiler pressure nominal.");
|
|
455
|
+
const mtimeAfter = (await stat(path.join(dayDir, DIRECTORY_SIDECAR_BASENAME))).mtimeMs;
|
|
456
|
+
assert.equal(mtimeAfter, mtimeBefore, "retrieval must not refresh a stale sidecar");
|
|
457
|
+
} finally {
|
|
458
|
+
rmSync(root, { recursive: true, force: true });
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("drill-down namespace scoping never attaches a foreign abstract", async () => {
|
|
463
|
+
const root = store();
|
|
464
|
+
try {
|
|
465
|
+
await writeMemory(root, "namespaces/alpha/facts/2026-05-01/a-1.md", "Alpha wing telescope mirror audit.");
|
|
466
|
+
await writeMemory(root, "namespaces/beta/facts/2026-05-01/b-1.md", "Beta wing telescope mirror audit.");
|
|
467
|
+
await runDirectorySidecarMaintenance(root, true);
|
|
468
|
+
|
|
469
|
+
const betaHits = [hit("namespaces/beta/facts/2026-05-01/b-1.md", "Beta wing telescope mirror audit.")];
|
|
470
|
+
const out = await applyDirectorySidecarDrillDown(root, "telescope mirror", betaHits, {
|
|
471
|
+
enabled: true,
|
|
472
|
+
namespace: "beta",
|
|
473
|
+
});
|
|
474
|
+
assert.match(out[0].snippet, new RegExp(`^${NEIGHBORHOOD_ABSTRACT_LABEL} `));
|
|
475
|
+
assert.match(out[0].snippet, /Beta wing/i);
|
|
476
|
+
assert.doesNotMatch(out[0].snippet, /Alpha wing/i);
|
|
477
|
+
} finally {
|
|
478
|
+
rmSync(root, { recursive: true, force: true });
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("drill-down prefers the most specific matching directory", async () => {
|
|
483
|
+
const root = store();
|
|
484
|
+
try {
|
|
485
|
+
await writeMemory(root, "facts/2026-01-02/fact-1.md", "Telescope calibration log for the north dome.");
|
|
486
|
+
await writeMemory(root, "facts/2026-01-03/fact-2.md", "Guide star acquisition failed twice.");
|
|
487
|
+
await runDirectorySidecarMaintenance(root, true);
|
|
488
|
+
|
|
489
|
+
const out = await applyDirectorySidecarDrillDown(
|
|
490
|
+
root,
|
|
491
|
+
"telescope calibration",
|
|
492
|
+
[hit("facts/2026-01-02/fact-1.md", "Telescope calibration log")],
|
|
493
|
+
{ enabled: true },
|
|
494
|
+
);
|
|
495
|
+
assert.match(out[0].snippet, /2026-01-02/);
|
|
496
|
+
assert.doesNotMatch(out[0].snippet, /Guide star/);
|
|
497
|
+
} finally {
|
|
498
|
+
rmSync(root, { recursive: true, force: true });
|
|
499
|
+
}
|
|
500
|
+
});
|
|
@@ -27,12 +27,16 @@
|
|
|
27
27
|
* sidecar path WITHOUT the marker are user content: never overwritten, never
|
|
28
28
|
* removed.
|
|
29
29
|
*
|
|
30
|
-
* Layer 2
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
30
|
+
* Layer 2: {@link refreshDirectorySidecarsAfterWrite} refreshes only the
|
|
31
|
+
* changed directory and its category ancestors. `enabled: false` is a
|
|
32
|
+
* proven no-op (no reads, no writes).
|
|
33
|
+
*
|
|
34
|
+
* Layer 3 (this slice): {@link applyDirectorySidecarDrillDown} scores
|
|
35
|
+
* fresh sidecars and attaches the winning directory's abstract to hits.
|
|
36
|
+
* `enabled` stays a function argument — not a parseConfig key (`config.ts`
|
|
37
|
+
* and `types.ts` are at their file-size ratchets). Retrieval never writes.
|
|
38
|
+
* xray trajectory, storage write-hook, parseConfig wiring, and LLM
|
|
39
|
+
* summarization remain later layers on #2977.
|
|
36
40
|
*/
|
|
37
41
|
|
|
38
42
|
import { createHash } from "node:crypto";
|
|
@@ -47,6 +51,8 @@ import { RECALL_FALLBACK_DIRS } from "./utils/category-dir.js";
|
|
|
47
51
|
|
|
48
52
|
export const DIRECTORY_SIDECAR_BASENAME = "overview.md";
|
|
49
53
|
export const DIRECTORY_SIDECAR_MARKER = "<!-- remnic-directory-sidecar -->";
|
|
54
|
+
/** Prefix prepended to a hit snippet when a fresh directory abstract attaches. */
|
|
55
|
+
export const NEIGHBORHOOD_ABSTRACT_LABEL = "Neighborhood:";
|
|
50
56
|
|
|
51
57
|
const ABSTRACT_MAX_CHARS = 256;
|
|
52
58
|
const OVERVIEW_MAX_CHARS = 4000;
|
|
@@ -87,6 +93,13 @@ export interface DirectorySidecarMatch extends DirectorySidecar {
|
|
|
87
93
|
readonly score: number;
|
|
88
94
|
}
|
|
89
95
|
|
|
96
|
+
/** Minimal hit shape drill-down mutates. Extra fields pass through. */
|
|
97
|
+
export interface DirectorySidecarHit {
|
|
98
|
+
path: string;
|
|
99
|
+
snippet: string;
|
|
100
|
+
score: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
90
103
|
export interface DirectorySidecarReport {
|
|
91
104
|
written: string[];
|
|
92
105
|
removed: string[];
|
|
@@ -435,7 +448,7 @@ export async function loadDirectorySidecar(
|
|
|
435
448
|
export async function findDirectorySidecarsForQuery(
|
|
436
449
|
memoryDir: string,
|
|
437
450
|
query: string,
|
|
438
|
-
options: { namespace?: string; limit?: number } = {},
|
|
451
|
+
options: { namespace?: string; limit?: number; refresh?: boolean } = {},
|
|
439
452
|
): Promise<DirectorySidecarMatch[]> {
|
|
440
453
|
const scope = options.namespace ? path.join(memoryDir, "namespaces", options.namespace) : memoryDir;
|
|
441
454
|
if (!isRealDirectory(scope)) return [];
|
|
@@ -447,7 +460,7 @@ export async function findDirectorySidecarsForQuery(
|
|
|
447
460
|
const matches: DirectorySidecarMatch[] = [];
|
|
448
461
|
for (const root of scopeRoots(scope)) {
|
|
449
462
|
for (const dir of collectSidecarDirs(root)) {
|
|
450
|
-
const sidecar = await loadDirectorySidecar(dir);
|
|
463
|
+
const sidecar = await loadDirectorySidecar(dir, { refresh: options.refresh });
|
|
451
464
|
if (!sidecar) continue;
|
|
452
465
|
const abstractLower = sidecar.abstract.toLowerCase();
|
|
453
466
|
const overviewLower = sidecar.overview.toLowerCase();
|
|
@@ -466,3 +479,52 @@ export async function findDirectorySidecarsForQuery(
|
|
|
466
479
|
.filter((candidate) => !matches.some((other) => other.dir.startsWith(`${candidate.dir}${path.sep}`)))
|
|
467
480
|
.slice(0, limit);
|
|
468
481
|
}
|
|
482
|
+
|
|
483
|
+
function attachNeighborhoodAbstract(snippet: string, abstract: string): string {
|
|
484
|
+
const line = `${NEIGHBORHOOD_ABSTRACT_LABEL} ${abstract}`;
|
|
485
|
+
if (snippet === line || snippet.startsWith(`${line}\n`)) return snippet;
|
|
486
|
+
return snippet.length === 0 ? line : `${line}\n${snippet}`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function mostSpecificSidecarForHit(
|
|
490
|
+
memoryDir: string,
|
|
491
|
+
hitPath: string,
|
|
492
|
+
matches: readonly DirectorySidecarMatch[],
|
|
493
|
+
): DirectorySidecarMatch | null {
|
|
494
|
+
if (path.basename(hitPath) === DIRECTORY_SIDECAR_BASENAME) return null;
|
|
495
|
+
const abs = path.isAbsolute(hitPath) ? path.resolve(hitPath) : path.resolve(memoryDir, hitPath);
|
|
496
|
+
let best: DirectorySidecarMatch | null = null;
|
|
497
|
+
for (const match of matches) {
|
|
498
|
+
const rel = path.relative(match.dir, abs);
|
|
499
|
+
if (rel === "" || rel.startsWith("..") || path.isAbsolute(rel)) continue;
|
|
500
|
+
if (best === null || match.dir.length > best.dir.length) best = match;
|
|
501
|
+
}
|
|
502
|
+
return best;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Retrieval drill-down (layer 3 of #2977): score fresh directory sidecars
|
|
507
|
+
* and attach the winning neighborhood abstract to hits under that directory.
|
|
508
|
+
* `enabled: false` (the default) returns hits unchanged with no I/O.
|
|
509
|
+
* Never writes. Does not reorder. parseConfig is not consulted.
|
|
510
|
+
*/
|
|
511
|
+
export async function applyDirectorySidecarDrillDown<T extends DirectorySidecarHit>(
|
|
512
|
+
memoryDir: string,
|
|
513
|
+
query: string,
|
|
514
|
+
hits: readonly T[],
|
|
515
|
+
options: { enabled?: boolean; namespace?: string } = {},
|
|
516
|
+
): Promise<T[]> {
|
|
517
|
+
if (options.enabled !== true) return [...hits];
|
|
518
|
+
const matches = await findDirectorySidecarsForQuery(memoryDir, query, {
|
|
519
|
+
namespace: options.namespace,
|
|
520
|
+
refresh: false,
|
|
521
|
+
});
|
|
522
|
+
const fresh = matches.filter((match) => match.fresh && match.abstract.length > 0);
|
|
523
|
+
if (fresh.length === 0) return [...hits];
|
|
524
|
+
return hits.map((hit) => {
|
|
525
|
+
const match = mostSpecificSidecarForHit(memoryDir, hit.path, fresh);
|
|
526
|
+
if (!match) return hit;
|
|
527
|
+
const snippet = attachNeighborhoodAbstract(hit.snippet, match.abstract);
|
|
528
|
+
return snippet === hit.snippet ? hit : { ...hit, snippet };
|
|
529
|
+
});
|
|
530
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-end experience memories as procedure-mining input (issue #2979 layer 3).
|
|
3
|
+
*
|
|
4
|
+
* Promoted episodes (experience_* attributes, status active) join the miner
|
|
5
|
+
* record set when sessionExperience.enabled is on. Gate off: miner input is
|
|
6
|
+
* byte-identical to the trajectory-only set (no experience records).
|
|
7
|
+
* pending_review stays out.
|
|
8
|
+
*/
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
|
|
15
|
+
import { parseConfig } from "../config.js";
|
|
16
|
+
import type { BufferTurn, PluginConfig } from "../types.js";
|
|
17
|
+
import { StorageManager } from "../storage.js";
|
|
18
|
+
import {
|
|
19
|
+
filterTrajectoriesByLookbackDays,
|
|
20
|
+
readCausalTrajectoryRecords,
|
|
21
|
+
recordCausalTrajectory,
|
|
22
|
+
} from "../causal-trajectory.js";
|
|
23
|
+
import { runProcedureMining } from "../procedural/procedure-miner.js";
|
|
24
|
+
import { runSessionExperienceExtraction } from "./session-experience.js";
|
|
25
|
+
import { collectProcedureMiningRecords } from "./session-experience-mining.js";
|
|
26
|
+
|
|
27
|
+
function turn(role: "user" | "assistant", content: string): BufferTurn {
|
|
28
|
+
return { role, content, timestamp: "2026-01-15T10:00:00.000Z" };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function successSession(): BufferTurn[] {
|
|
32
|
+
return [
|
|
33
|
+
turn("user", "Fix the failing integration test in the payments module. The test times out after 30 seconds."),
|
|
34
|
+
turn("assistant", "I will inspect the payments test file and reproduce the timeout locally first."),
|
|
35
|
+
turn("assistant", "The timeout came from an unawaited database call in the test setup. I added the missing await."),
|
|
36
|
+
turn("assistant", "Verified: the payments integration test passes now and the full suite is green."),
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function miningConfig(memoryDir: string, experienceEnabled: boolean): PluginConfig {
|
|
41
|
+
return parseConfig({
|
|
42
|
+
memoryDir,
|
|
43
|
+
sessionExperience: { enabled: experienceEnabled },
|
|
44
|
+
procedural: {
|
|
45
|
+
enabled: true,
|
|
46
|
+
minOccurrences: 2,
|
|
47
|
+
lookbackDays: 30,
|
|
48
|
+
successFloor: 1,
|
|
49
|
+
autoPromoteEnabled: false,
|
|
50
|
+
autoPromoteOccurrences: 10,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function writePromotedExperience(
|
|
56
|
+
storage: StorageManager,
|
|
57
|
+
config: PluginConfig,
|
|
58
|
+
sessionKey: string,
|
|
59
|
+
): Promise<string> {
|
|
60
|
+
const written = await runSessionExperienceExtraction({
|
|
61
|
+
turns: successSession(),
|
|
62
|
+
sessionKey,
|
|
63
|
+
config,
|
|
64
|
+
storage,
|
|
65
|
+
});
|
|
66
|
+
assert.equal(written.written, true);
|
|
67
|
+
assert.ok(written.memoryId);
|
|
68
|
+
const promoted = await storage.updateMemoryFrontmatter(written.memoryId, { status: "active" });
|
|
69
|
+
assert.equal(promoted, true);
|
|
70
|
+
return written.memoryId;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
test("gate off: miner input is byte-identical to the trajectory-only set", async () => {
|
|
74
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-sx-mine-off-"));
|
|
75
|
+
try {
|
|
76
|
+
const storage = new StorageManager(memoryDir);
|
|
77
|
+
await storage.ensureDirectories();
|
|
78
|
+
const on = miningConfig(memoryDir, true);
|
|
79
|
+
await writePromotedExperience(storage, on, "session-mine-off-a");
|
|
80
|
+
await writePromotedExperience(storage, on, "session-mine-off-b");
|
|
81
|
+
await recordCausalTrajectory({
|
|
82
|
+
memoryDir,
|
|
83
|
+
record: {
|
|
84
|
+
schemaVersion: 1,
|
|
85
|
+
trajectoryId: "traj-off-1",
|
|
86
|
+
recordedAt: new Date().toISOString(),
|
|
87
|
+
sessionKey: "traj-session",
|
|
88
|
+
goal: "Ship the demo gateway to staging",
|
|
89
|
+
actionSummary: "Confirm the requirements and prepare the working plan.",
|
|
90
|
+
observationSummary: "The run produced a reusable sequence of actions.",
|
|
91
|
+
outcomeKind: "success",
|
|
92
|
+
outcomeSummary: "The workflow completed successfully with a reusable result.",
|
|
93
|
+
entityRefs: ["project-demo"],
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const off = miningConfig(memoryDir, false);
|
|
98
|
+
const { trajectories } = await readCausalTrajectoryRecords({ memoryDir });
|
|
99
|
+
const nowMs = Date.now();
|
|
100
|
+
const expected = filterTrajectoriesByLookbackDays(trajectories, off.procedural.lookbackDays, nowMs);
|
|
101
|
+
const actual = await collectProcedureMiningRecords({
|
|
102
|
+
trajectories,
|
|
103
|
+
lookbackDays: off.procedural.lookbackDays,
|
|
104
|
+
storage: {
|
|
105
|
+
async readAllMemories() {
|
|
106
|
+
throw new Error("gate-off miner input must not read memories");
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
experienceEnabled: false,
|
|
110
|
+
nowMs,
|
|
111
|
+
});
|
|
112
|
+
assert.equal(JSON.stringify(actual), JSON.stringify(expected));
|
|
113
|
+
assert.equal(
|
|
114
|
+
actual.some((r) => r.trajectoryId.startsWith("experience:")),
|
|
115
|
+
false,
|
|
116
|
+
);
|
|
117
|
+
} finally {
|
|
118
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("gate on: promoted experience memories feed the miner; pending_review does not", async () => {
|
|
123
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-sx-mine-on-"));
|
|
124
|
+
try {
|
|
125
|
+
const storage = new StorageManager(memoryDir);
|
|
126
|
+
await storage.ensureDirectories();
|
|
127
|
+
const config = miningConfig(memoryDir, true);
|
|
128
|
+
await writePromotedExperience(storage, config, "session-mine-on-a");
|
|
129
|
+
await writePromotedExperience(storage, config, "session-mine-on-b");
|
|
130
|
+
const pending = await runSessionExperienceExtraction({
|
|
131
|
+
turns: successSession(),
|
|
132
|
+
sessionKey: "session-mine-on-pending",
|
|
133
|
+
config,
|
|
134
|
+
storage,
|
|
135
|
+
});
|
|
136
|
+
assert.equal(pending.written, true);
|
|
137
|
+
|
|
138
|
+
const { trajectories } = await readCausalTrajectoryRecords({ memoryDir });
|
|
139
|
+
const records = await collectProcedureMiningRecords({
|
|
140
|
+
trajectories,
|
|
141
|
+
lookbackDays: config.procedural.lookbackDays,
|
|
142
|
+
storage,
|
|
143
|
+
experienceEnabled: true,
|
|
144
|
+
});
|
|
145
|
+
const experienceIds = records.filter((r) => r.trajectoryId.startsWith("experience:")).map((r) => r.trajectoryId);
|
|
146
|
+
assert.equal(experienceIds.length, 2, "only the two promoted episodes enter mining");
|
|
147
|
+
assert.ok(records.every((r) => r.goal.toLowerCase().includes("payments")));
|
|
148
|
+
|
|
149
|
+
const result = await runProcedureMining({ memoryDir, storage, config });
|
|
150
|
+
assert.equal(result.proceduresWritten, 1);
|
|
151
|
+
assert.ok((result.clustersProcessed ?? 0) >= 1);
|
|
152
|
+
const memories = await storage.readAllMemories();
|
|
153
|
+
const mined = memories.find(
|
|
154
|
+
(m) =>
|
|
155
|
+
m.frontmatter.category === "procedure" &&
|
|
156
|
+
typeof m.frontmatter.structuredAttributes?.procedure_cluster_hash === "string" &&
|
|
157
|
+
(m.frontmatter.structuredAttributes.trajectory_ids ?? "").includes("experience:"),
|
|
158
|
+
);
|
|
159
|
+
assert.ok(mined, "mined procedure must cite experience trajectory ids");
|
|
160
|
+
assert.equal(mined.frontmatter.status, "pending_review");
|
|
161
|
+
assert.equal(mined.frontmatter.structuredAttributes?.trajectory_count, "2");
|
|
162
|
+
} finally {
|
|
163
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("gate off: two promoted experiences do not produce a mined procedure", async () => {
|
|
168
|
+
const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-sx-mine-off-write-"));
|
|
169
|
+
try {
|
|
170
|
+
const storage = new StorageManager(memoryDir);
|
|
171
|
+
await storage.ensureDirectories();
|
|
172
|
+
const enabled = miningConfig(memoryDir, true);
|
|
173
|
+
await writePromotedExperience(storage, enabled, "session-mine-off-write-a");
|
|
174
|
+
await writePromotedExperience(storage, enabled, "session-mine-off-write-b");
|
|
175
|
+
const off = miningConfig(memoryDir, false);
|
|
176
|
+
const result = await runProcedureMining({ memoryDir, storage, config: off });
|
|
177
|
+
assert.equal(result.proceduresWritten, 0);
|
|
178
|
+
const memories = await storage.readAllMemories();
|
|
179
|
+
assert.equal(
|
|
180
|
+
memories.filter((m) => m.frontmatter.structuredAttributes?.procedure_cluster_hash !== undefined).length,
|
|
181
|
+
0,
|
|
182
|
+
);
|
|
183
|
+
} finally {
|
|
184
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-experience mining helpers (issue #2979 layer 3).
|
|
3
|
+
*
|
|
4
|
+
* Promoted experience memories are `category: procedure` with
|
|
5
|
+
* `experience_*` attributes. When `sessionExperience.enabled` is true they
|
|
6
|
+
* join the procedure-miner record set as pre-structured trajectories.
|
|
7
|
+
* Call `collectProcedureMiningRecords` with `experienceEnabled: false` for
|
|
8
|
+
* the gate-off path — that branch must not read storage and must return
|
|
9
|
+
* the trajectory lookback set unchanged.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
filterTrajectoriesByLookbackDays,
|
|
13
|
+
type CausalTrajectoryRecord,
|
|
14
|
+
} from "../causal-trajectory.js";
|
|
15
|
+
import { isActiveMemoryStatus } from "../memory-lifecycle-ledger-utils.js";
|
|
16
|
+
import type { ObjectiveStateOutcome } from "../objective-state.js";
|
|
17
|
+
import type { MemoryFile } from "../types.js";
|
|
18
|
+
import { isSessionExperienceMemory } from "./session-experience-recall.js";
|
|
19
|
+
|
|
20
|
+
export interface ExperienceMiningStorage {
|
|
21
|
+
readAllMemories(): Promise<MemoryFile[]>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CollectProcedureMiningRecordsOptions {
|
|
25
|
+
trajectories: CausalTrajectoryRecord[];
|
|
26
|
+
lookbackDays: number;
|
|
27
|
+
storage: ExperienceMiningStorage;
|
|
28
|
+
experienceEnabled: boolean;
|
|
29
|
+
nowMs?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function mapExperienceOutcome(raw: string | undefined): ObjectiveStateOutcome {
|
|
33
|
+
if (raw === "success" || raw === "failure" || raw === "partial") return raw;
|
|
34
|
+
return "unknown";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Convert one promoted episode into a mining record, or `null`. */
|
|
38
|
+
export function experienceMemoryToMiningRecord(memory: MemoryFile): CausalTrajectoryRecord | null {
|
|
39
|
+
if (!isSessionExperienceMemory(memory)) return null;
|
|
40
|
+
if (!isActiveMemoryStatus(memory.frontmatter.status)) return null;
|
|
41
|
+
const attrs = memory.frontmatter.structuredAttributes ?? {};
|
|
42
|
+
const situation = (attrs.experience_situation ?? "").trim();
|
|
43
|
+
const hash = attrs.experience_session_hash;
|
|
44
|
+
if (situation.length === 0 || typeof hash !== "string" || hash.length === 0) return null;
|
|
45
|
+
const recordedAt = memory.frontmatter.created;
|
|
46
|
+
if (!recordedAt || !Number.isFinite(Date.parse(recordedAt))) return null;
|
|
47
|
+
const approach = (attrs.experience_approach ?? "").trim();
|
|
48
|
+
const reflection = (attrs.experience_reflection ?? "").trim();
|
|
49
|
+
const record: CausalTrajectoryRecord = {
|
|
50
|
+
schemaVersion: 1,
|
|
51
|
+
trajectoryId: `experience:${hash}`,
|
|
52
|
+
recordedAt,
|
|
53
|
+
sessionKey: hash,
|
|
54
|
+
goal: situation,
|
|
55
|
+
actionSummary: approach,
|
|
56
|
+
observationSummary: reflection,
|
|
57
|
+
outcomeKind: mapExperienceOutcome(attrs.experience_outcome),
|
|
58
|
+
outcomeSummary: reflection,
|
|
59
|
+
};
|
|
60
|
+
if (typeof memory.frontmatter.entityRef === "string" && memory.frontmatter.entityRef.trim().length > 0) {
|
|
61
|
+
record.entityRefs = [memory.frontmatter.entityRef.trim()];
|
|
62
|
+
}
|
|
63
|
+
return record;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function collectExperienceMiningRecords(
|
|
67
|
+
storage: ExperienceMiningStorage,
|
|
68
|
+
): Promise<CausalTrajectoryRecord[]> {
|
|
69
|
+
const memories = await storage.readAllMemories();
|
|
70
|
+
const out: CausalTrajectoryRecord[] = [];
|
|
71
|
+
for (const memory of memories) {
|
|
72
|
+
const record = experienceMemoryToMiningRecord(memory);
|
|
73
|
+
if (record) out.push(record);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Records the miner will cluster. Gate off returns the lookback-filtered
|
|
80
|
+
* trajectories only and does not call storage.
|
|
81
|
+
*/
|
|
82
|
+
export async function collectProcedureMiningRecords(
|
|
83
|
+
options: CollectProcedureMiningRecordsOptions,
|
|
84
|
+
): Promise<CausalTrajectoryRecord[]> {
|
|
85
|
+
const source =
|
|
86
|
+
options.experienceEnabled === true
|
|
87
|
+
? options.trajectories.concat(await collectExperienceMiningRecords(options.storage))
|
|
88
|
+
: options.trajectories;
|
|
89
|
+
return filterTrajectoriesByLookbackDays(source, options.lookbackDays, options.nowMs);
|
|
90
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* - content-hash dedup index add/has/remove/save
|
|
8
8
|
* - temporal-bounds backfill on dedup hits (bitemporal, #1578)
|
|
9
9
|
* - temporal tag index updates and persisted-memory indexing
|
|
10
|
+
* - recognition-index write-path maintenance when recallRecognitionTier is on
|
|
10
11
|
* - graph edge construction for newly persisted memories
|
|
11
12
|
* - semantic dedup candidate lookup
|
|
12
13
|
*
|
|
@@ -22,7 +23,9 @@ import { GraphIndex, type GraphEdge } from "../graph.js";
|
|
|
22
23
|
import { ContentHashIndex, StorageManager } from "../index.js";
|
|
23
24
|
import { log } from "../logger.js";
|
|
24
25
|
import { isActiveMemoryStatus } from "../memory-lifecycle-ledger-utils.js";
|
|
26
|
+
import { loadRecognitionIndex } from "../recall-recognition-tier.js";
|
|
25
27
|
import { stripCitationForTemplate } from "../source-attribution.js";
|
|
28
|
+
import { maintainRecognitionIndexAfterWrite, type RecognitionIndexChange } from "./recall-recognition-index.js";
|
|
26
29
|
import { clearIndexesAsync, indexesExistAsync } from "../temporal-index.js";
|
|
27
30
|
import { indexMemoriesBatchAsync } from "../temporal-index-batch.js";
|
|
28
31
|
import { normalizeSupersessionKey } from "../temporal-supersession.js";
|
|
@@ -447,6 +450,9 @@ export class PersistenceIndexCoordinator {
|
|
|
447
450
|
storage: StorageManager,
|
|
448
451
|
persistedIds: string[],
|
|
449
452
|
): Promise<void> {
|
|
453
|
+
// Recognition-index write-path (issue #2975): independent of temporal
|
|
454
|
+
// capability gates. Off-path is zero index I/O and cannot block persist.
|
|
455
|
+
await this.maintainRecognitionIndex(storage, persistedIds);
|
|
450
456
|
const caps = resolveCapabilities(this.deps.config); // #1566 Cluster C
|
|
451
457
|
// Build temporal/tag indexes whenever either consumer is enabled:
|
|
452
458
|
// - queryAwareIndexingEnabled: uses indexes for query-aware prefiltering in recall
|
|
@@ -625,4 +631,48 @@ export class PersistenceIndexCoordinator {
|
|
|
625
631
|
);
|
|
626
632
|
return enriched;
|
|
627
633
|
}
|
|
634
|
+
/**
|
|
635
|
+
* Upsert persisted memories into the namespace recognition index.
|
|
636
|
+
* `recallRecognitionTier !== true` returns before any index I/O.
|
|
637
|
+
* A missing index is bootstrapped from the active corpus so the first
|
|
638
|
+
* post-enable write cannot publish a partial index that recall would
|
|
639
|
+
* treat as the full working set.
|
|
640
|
+
*/
|
|
641
|
+
private async maintainRecognitionIndex(
|
|
642
|
+
storage: StorageManager,
|
|
643
|
+
persistedIds: string[],
|
|
644
|
+
): Promise<void> {
|
|
645
|
+
try {
|
|
646
|
+
if (this.deps.config.recallRecognitionTier !== true) return;
|
|
647
|
+
const indexMissing = (await loadRecognitionIndex(storage.dir)) === null;
|
|
648
|
+
if (!indexMissing && persistedIds.length === 0) return;
|
|
649
|
+
const changes: RecognitionIndexChange[] = [];
|
|
650
|
+
const pushUpsert = (memory: MemoryFile): void => {
|
|
651
|
+
if (!isActiveMemoryStatus(memory.frontmatter.status)) return;
|
|
652
|
+
const memoryId = memory.frontmatter.id;
|
|
653
|
+
if (typeof memoryId !== "string" || memoryId.trim().length === 0) return;
|
|
654
|
+
changes.push({ action: "upsert", id: memoryId, content: memory.content ?? "" });
|
|
655
|
+
};
|
|
656
|
+
if (indexMissing) {
|
|
657
|
+
const corpus = await storage.readAllMemories().catch(() => []);
|
|
658
|
+
for (const memory of corpus) pushUpsert(memory);
|
|
659
|
+
}
|
|
660
|
+
for (const id of persistedIds) {
|
|
661
|
+
let memory = await storage.getMemoryById(id);
|
|
662
|
+
if (!memory) {
|
|
663
|
+
const cold = await storage.getMemoryByIdIncludingArchived(id).catch(() => null);
|
|
664
|
+
if (cold && isActiveMemoryStatus(cold.frontmatter.status)) memory = cold;
|
|
665
|
+
}
|
|
666
|
+
if (memory) pushUpsert(memory);
|
|
667
|
+
}
|
|
668
|
+
await maintainRecognitionIndexAfterWrite({
|
|
669
|
+
memoryDir: storage.dir,
|
|
670
|
+
enabled: true,
|
|
671
|
+
changes,
|
|
672
|
+
});
|
|
673
|
+
} catch (err) {
|
|
674
|
+
log.debug(`recognition-index update error (non-fatal): ${err}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
628
678
|
}
|