@remnic/core 10.0.94 → 10.0.96
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/abstraction-nodes.d.ts.map +1 -1
- package/dist/abstraction-nodes.js +7 -1
- package/dist/access-admin-ops-surface.js +91 -90
- package/dist/access-audit.js +2 -2
- package/dist/access-cli.js +91 -90
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +91 -90
- package/dist/access-http-briefing.js +91 -90
- package/dist/access-http-query.js +91 -90
- package/dist/access-http.js +91 -90
- package/dist/access-identity-continuity-surface.js +91 -90
- package/dist/access-lcm-surface.js +91 -90
- package/dist/access-mcp.js +91 -90
- package/dist/access-observe-write-surface.js +91 -90
- package/dist/access-operations-batch.js +91 -90
- package/dist/access-operations.js +91 -90
- package/dist/access-recall-concurrency.js +91 -90
- package/dist/access-recall-response.js +91 -90
- package/dist/access-recall-surface.js +91 -90
- package/dist/access-schema.d.ts +5 -5
- package/dist/access-service.js +91 -90
- package/dist/active-recall.js +16 -16
- package/dist/briefing.js +15 -15
- package/dist/buffer.js +7 -7
- package/dist/calibration.js +2 -2
- package/dist/{capsule-merge-6IDAYTKG.js → capsule-merge-ILR35DIB.js} +10 -10
- package/dist/causal-consolidation.js +16 -16
- package/dist/{chunk-QHARXWTC.js → chunk-2LCX6TAD.js} +2 -2
- package/dist/{chunk-UN4RVUG2.js → chunk-2R3ACFYN.js} +3 -3
- package/dist/{chunk-RQFXZYTF.js → chunk-33C4BK7I.js} +4 -4
- package/dist/{chunk-UBEKET2V.js → chunk-3R63SXC5.js} +4 -4
- package/dist/{chunk-LNGELD2M.js → chunk-3VWU3KF3.js} +2 -2
- package/dist/{chunk-FWRXW7CZ.js → chunk-4CXK3GT3.js} +29 -29
- package/dist/{chunk-DNY63QWJ.js → chunk-4HK7DYOL.js} +2 -2
- package/dist/{chunk-LLPMMMWG.js → chunk-4IJFJQPD.js} +2 -2
- package/dist/{chunk-GBJUTX44.js → chunk-4WKR4P7H.js} +2 -2
- package/dist/{chunk-ILY5SSOM.js → chunk-53ZVHXVZ.js} +6 -6
- package/dist/{chunk-FQAQ63QA.js → chunk-5AJIGWY4.js} +2 -2
- package/dist/{chunk-CIWMTNHW.js → chunk-5NFF2TSD.js} +2 -2
- package/dist/{chunk-D5XG2KPS.js → chunk-7E7J7QXB.js} +2 -2
- package/dist/{chunk-2WCLTWV2.js → chunk-7NVK6AGC.js} +2 -2
- package/dist/{chunk-PC5DRBGD.js → chunk-AP4BFWSW.js} +2 -2
- package/dist/{chunk-CL4JU3DF.js → chunk-B7L4ZYFY.js} +3 -3
- package/dist/{chunk-LQ5SS4OP.js → chunk-D6IGZYTN.js} +3 -3
- package/dist/{chunk-XJ2S5UBF.js → chunk-DWWX3FU3.js} +3 -3
- package/dist/{chunk-IL3RHBWM.js → chunk-EJ23D4JG.js} +3 -3
- package/dist/{chunk-F4QFU5IZ.js → chunk-EZ7XJJ5L.js} +2 -2
- package/dist/{chunk-FZZMLGWQ.js → chunk-F4BUSX3D.js} +2 -2
- package/dist/{chunk-LXATZLSC.js → chunk-FL3O34XF.js} +12 -5
- package/dist/chunk-FL3O34XF.js.map +1 -0
- package/dist/{chunk-DPPLNZ3A.js → chunk-FM4P7HSW.js} +4 -4
- package/dist/{chunk-QDESY76F.js → chunk-FQACH7MC.js} +2 -2
- package/dist/{chunk-XPXHQI6N.js → chunk-FVMEZ57K.js} +2 -2
- package/dist/{chunk-F2XNSIPP.js → chunk-G2CGIFRP.js} +10 -3
- package/dist/chunk-G2CGIFRP.js.map +1 -0
- package/dist/{chunk-ZDV5YWID.js → chunk-GLDIMGOS.js} +6 -6
- package/dist/{chunk-YIXJSW5M.js → chunk-HA5DASDG.js} +3 -3
- package/dist/{chunk-S3S72HRM.js → chunk-HCCRWSP7.js} +2 -2
- package/dist/{chunk-NMGHGTVR.js → chunk-HRMOKGQM.js} +6 -6
- package/dist/{chunk-OUVYYKYU.js → chunk-IBKGQ6EY.js} +2 -2
- package/dist/{chunk-RNG7S3FY.js → chunk-ICHLN5L6.js} +6 -6
- package/dist/chunk-ITAUMNQU.js +40 -0
- package/dist/chunk-ITAUMNQU.js.map +1 -0
- package/dist/{chunk-R23JWJD3.js → chunk-J426U7XK.js} +9 -9
- package/dist/{chunk-QMFAMOT2.js → chunk-JH5AV542.js} +2 -2
- package/dist/{chunk-WGGRZYSQ.js → chunk-KE3JWIZK.js} +8 -8
- package/dist/{chunk-SQ3GEIEL.js → chunk-KUQLUYIR.js} +5 -5
- package/dist/{chunk-RKABLU5M.js → chunk-KVHGCISP.js} +9 -9
- package/dist/{chunk-RUTN5MMC.js → chunk-LRLQ2PRP.js} +5 -5
- package/dist/{chunk-X5HGU2EM.js → chunk-NT772J7I.js} +2 -2
- package/dist/{chunk-BPK32FRM.js → chunk-NTDHSV7N.js} +61 -6
- package/dist/chunk-NTDHSV7N.js.map +1 -0
- package/dist/{chunk-4EZWSDFL.js → chunk-NX4XDDFQ.js} +3 -3
- package/dist/{chunk-IAYUTGF2.js → chunk-O3GZPLVY.js} +4 -4
- package/dist/{chunk-IGOW4TJE.js → chunk-O3JG4PD3.js} +2 -2
- package/dist/{chunk-UHMHJX5G.js → chunk-OIG4ZFMB.js} +4 -4
- package/dist/{chunk-IQ55QGY5.js → chunk-R4JYSBZE.js} +2 -2
- package/dist/{chunk-Z3FTSHT5.js → chunk-RUQE5C32.js} +2 -2
- package/dist/{chunk-ESLOLCD4.js → chunk-RUWKIPWJ.js} +2 -2
- package/dist/{chunk-W54Z2MOP.js → chunk-RXSGBO7Y.js} +2 -2
- package/dist/{chunk-D5S6RL32.js → chunk-TFMLRVMW.js} +166 -145
- package/dist/{chunk-D5S6RL32.js.map → chunk-TFMLRVMW.js.map} +1 -1
- package/dist/{chunk-BOK6BN5M.js → chunk-THV6PZ7V.js} +2 -2
- package/dist/{chunk-J7BIFOYP.js → chunk-U6TC6Z5B.js} +2 -2
- package/dist/{chunk-X2BVB74H.js → chunk-UZQXJDOE.js} +2 -2
- package/dist/{chunk-SUTEBYGI.js → chunk-V7T45MI6.js} +2 -2
- package/dist/{chunk-A4F7OIC5.js → chunk-WVIWDMFB.js} +2 -2
- package/dist/{chunk-BN2EBCLB.js → chunk-XPCUKWLE.js} +2 -2
- package/dist/{chunk-OJXK5C6Z.js → chunk-ZG4GNPRE.js} +7 -7
- package/dist/{chunk-TYRWSE4B.js → chunk-ZUMHQEW4.js} +2 -2
- package/dist/{chunk-DAOMWGO2.js → chunk-ZX5N5D6M.js} +4 -4
- package/dist/cli.js +91 -90
- package/dist/coding/export-okf-codegraph.js +14 -14
- package/dist/compounding/engine.js +16 -16
- package/dist/config.js +16 -16
- package/dist/connectors/codex-materialize-runner.js +15 -15
- package/dist/connectors/index.js +15 -15
- package/dist/consolidation-provenance-check.js +8 -8
- package/dist/consolidation-undo.js +6 -6
- package/dist/contradiction/index.js +6 -6
- package/dist/convergence-refresh.js +17 -17
- package/dist/corpus-watermark.js +17 -17
- package/dist/cue-anchors.d.ts.map +1 -1
- package/dist/cue-anchors.js +8 -2
- package/dist/{entity-canonical-id-migration-JJSTZF73.js → entity-canonical-id-migration-IG77BOGD.js} +5 -5
- package/dist/entity-mention-index-cache.js +2 -2
- package/dist/entity-retrieval.js +16 -16
- package/dist/explicit-capture.js +91 -90
- package/dist/extraction.js +21 -21
- package/dist/harmonic-construction.js +9 -3
- package/dist/harmonic-graph-version.d.ts +14 -0
- package/dist/harmonic-graph-version.d.ts.map +1 -0
- package/dist/harmonic-graph-version.js +17 -0
- package/dist/harmonic-retrieval.d.ts +19 -14
- package/dist/harmonic-retrieval.d.ts.map +1 -1
- package/dist/harmonic-retrieval.js +22 -17
- package/dist/index.js +105 -104
- package/dist/lcm/engine.js +3 -3
- package/dist/lcm/index.js +8 -8
- package/dist/local-llm.js +2 -2
- package/dist/location/index.js +16 -16
- package/dist/maintenance/memory-governance.js +15 -15
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +15 -15
- package/dist/maintenance/rebuild-memory-projection-runner.js +17 -17
- package/dist/maintenance/rebuild-memory-projection.js +16 -16
- package/dist/memory-browse.js +7 -7
- package/dist/namespaces/migrate.js +16 -16
- package/dist/namespaces/search.js +6 -6
- package/dist/namespaces/storage.js +15 -15
- package/dist/operator-doctor-corpus.js +18 -18
- package/dist/operator-toolkit.js +24 -24
- package/dist/orchestration/backlog-scheduler.js +92 -91
- package/dist/orchestration/backlog-scheduler.js.map +1 -1
- package/dist/orchestration/buffer-drain-runner.js +92 -91
- package/dist/orchestration/extraction-persist-failure.d.ts +25 -1
- package/dist/orchestration/extraction-persist-failure.d.ts.map +1 -1
- package/dist/orchestration/extraction-persist.d.ts +3 -6
- package/dist/orchestration/extraction-persist.d.ts.map +1 -1
- package/dist/orchestration/maintenance.js +25 -25
- package/dist/orchestrator.js +93 -92
- package/dist/page-versioning.js +6 -6
- package/dist/qmd.js +4 -4
- package/dist/recall-navigation.js +91 -90
- package/dist/recall-state-view-admission.js +7 -7
- package/dist/recall-why-service.js +7 -7
- package/dist/{resolution-OVVIF7ZY.js → resolution-G4SLHSEU.js} +7 -7
- package/dist/resolution-G4SLHSEU.js.map +1 -0
- package/dist/resume-bundles.js +17 -17
- package/dist/schemas.d.ts +9 -9
- package/dist/search/factory.js +5 -5
- package/dist/search/index.js +5 -5
- package/dist/semantic-consolidation.js +16 -16
- package/dist/semantic-rule-promotion.js +15 -15
- package/dist/semantic-rule-verifier.js +15 -15
- package/dist/source-agent-qualifier.js +18 -18
- package/dist/standup/build.js +15 -15
- package/dist/storage.js +14 -14
- package/dist/summarizer.js +3 -3
- package/dist/support-passport/index.js +3 -3
- package/dist/transfer/capsule-import.js +8 -8
- package/dist/transfer/export-okf.js +15 -15
- package/dist/verified-recall.js +15 -15
- package/dist/who-knows-cli.js +16 -16
- package/dist/who-knows.js +15 -15
- package/package.json +2 -2
- package/src/abstraction-nodes.ts +7 -2
- package/src/cue-anchors.ts +8 -3
- package/src/harmonic-graph-cache.test.ts +244 -0
- package/src/harmonic-graph-version.ts +46 -0
- package/src/harmonic-retrieval.ts +92 -2
- package/src/orchestration/extraction-persist-failure.test.ts +90 -0
- package/src/orchestration/extraction-persist-failure.ts +39 -1
- package/src/orchestration/extraction-persist.ts +29 -21
- package/src/orchestration/recall-internal.ts +1 -1
- package/src/orchestrator-persist-branch-failure.test.ts +271 -0
- package/dist/chunk-BPK32FRM.js.map +0 -1
- package/dist/chunk-F2XNSIPP.js.map +0 -1
- package/dist/chunk-LXATZLSC.js.map +0 -1
- /package/dist/{capsule-merge-6IDAYTKG.js.map → capsule-merge-ILR35DIB.js.map} +0 -0
- /package/dist/{chunk-QHARXWTC.js.map → chunk-2LCX6TAD.js.map} +0 -0
- /package/dist/{chunk-UN4RVUG2.js.map → chunk-2R3ACFYN.js.map} +0 -0
- /package/dist/{chunk-RQFXZYTF.js.map → chunk-33C4BK7I.js.map} +0 -0
- /package/dist/{chunk-UBEKET2V.js.map → chunk-3R63SXC5.js.map} +0 -0
- /package/dist/{chunk-LNGELD2M.js.map → chunk-3VWU3KF3.js.map} +0 -0
- /package/dist/{chunk-FWRXW7CZ.js.map → chunk-4CXK3GT3.js.map} +0 -0
- /package/dist/{chunk-DNY63QWJ.js.map → chunk-4HK7DYOL.js.map} +0 -0
- /package/dist/{chunk-LLPMMMWG.js.map → chunk-4IJFJQPD.js.map} +0 -0
- /package/dist/{chunk-GBJUTX44.js.map → chunk-4WKR4P7H.js.map} +0 -0
- /package/dist/{chunk-ILY5SSOM.js.map → chunk-53ZVHXVZ.js.map} +0 -0
- /package/dist/{chunk-FQAQ63QA.js.map → chunk-5AJIGWY4.js.map} +0 -0
- /package/dist/{chunk-CIWMTNHW.js.map → chunk-5NFF2TSD.js.map} +0 -0
- /package/dist/{chunk-D5XG2KPS.js.map → chunk-7E7J7QXB.js.map} +0 -0
- /package/dist/{chunk-2WCLTWV2.js.map → chunk-7NVK6AGC.js.map} +0 -0
- /package/dist/{chunk-PC5DRBGD.js.map → chunk-AP4BFWSW.js.map} +0 -0
- /package/dist/{chunk-CL4JU3DF.js.map → chunk-B7L4ZYFY.js.map} +0 -0
- /package/dist/{chunk-LQ5SS4OP.js.map → chunk-D6IGZYTN.js.map} +0 -0
- /package/dist/{chunk-XJ2S5UBF.js.map → chunk-DWWX3FU3.js.map} +0 -0
- /package/dist/{chunk-IL3RHBWM.js.map → chunk-EJ23D4JG.js.map} +0 -0
- /package/dist/{chunk-F4QFU5IZ.js.map → chunk-EZ7XJJ5L.js.map} +0 -0
- /package/dist/{chunk-FZZMLGWQ.js.map → chunk-F4BUSX3D.js.map} +0 -0
- /package/dist/{chunk-DPPLNZ3A.js.map → chunk-FM4P7HSW.js.map} +0 -0
- /package/dist/{chunk-QDESY76F.js.map → chunk-FQACH7MC.js.map} +0 -0
- /package/dist/{chunk-XPXHQI6N.js.map → chunk-FVMEZ57K.js.map} +0 -0
- /package/dist/{chunk-ZDV5YWID.js.map → chunk-GLDIMGOS.js.map} +0 -0
- /package/dist/{chunk-YIXJSW5M.js.map → chunk-HA5DASDG.js.map} +0 -0
- /package/dist/{chunk-S3S72HRM.js.map → chunk-HCCRWSP7.js.map} +0 -0
- /package/dist/{chunk-NMGHGTVR.js.map → chunk-HRMOKGQM.js.map} +0 -0
- /package/dist/{chunk-OUVYYKYU.js.map → chunk-IBKGQ6EY.js.map} +0 -0
- /package/dist/{chunk-RNG7S3FY.js.map → chunk-ICHLN5L6.js.map} +0 -0
- /package/dist/{chunk-R23JWJD3.js.map → chunk-J426U7XK.js.map} +0 -0
- /package/dist/{chunk-QMFAMOT2.js.map → chunk-JH5AV542.js.map} +0 -0
- /package/dist/{chunk-WGGRZYSQ.js.map → chunk-KE3JWIZK.js.map} +0 -0
- /package/dist/{chunk-SQ3GEIEL.js.map → chunk-KUQLUYIR.js.map} +0 -0
- /package/dist/{chunk-RKABLU5M.js.map → chunk-KVHGCISP.js.map} +0 -0
- /package/dist/{chunk-RUTN5MMC.js.map → chunk-LRLQ2PRP.js.map} +0 -0
- /package/dist/{chunk-X5HGU2EM.js.map → chunk-NT772J7I.js.map} +0 -0
- /package/dist/{chunk-4EZWSDFL.js.map → chunk-NX4XDDFQ.js.map} +0 -0
- /package/dist/{chunk-IAYUTGF2.js.map → chunk-O3GZPLVY.js.map} +0 -0
- /package/dist/{chunk-IGOW4TJE.js.map → chunk-O3JG4PD3.js.map} +0 -0
- /package/dist/{chunk-UHMHJX5G.js.map → chunk-OIG4ZFMB.js.map} +0 -0
- /package/dist/{chunk-IQ55QGY5.js.map → chunk-R4JYSBZE.js.map} +0 -0
- /package/dist/{chunk-Z3FTSHT5.js.map → chunk-RUQE5C32.js.map} +0 -0
- /package/dist/{chunk-ESLOLCD4.js.map → chunk-RUWKIPWJ.js.map} +0 -0
- /package/dist/{chunk-W54Z2MOP.js.map → chunk-RXSGBO7Y.js.map} +0 -0
- /package/dist/{chunk-BOK6BN5M.js.map → chunk-THV6PZ7V.js.map} +0 -0
- /package/dist/{chunk-J7BIFOYP.js.map → chunk-U6TC6Z5B.js.map} +0 -0
- /package/dist/{chunk-X2BVB74H.js.map → chunk-UZQXJDOE.js.map} +0 -0
- /package/dist/{chunk-SUTEBYGI.js.map → chunk-V7T45MI6.js.map} +0 -0
- /package/dist/{chunk-A4F7OIC5.js.map → chunk-WVIWDMFB.js.map} +0 -0
- /package/dist/{chunk-BN2EBCLB.js.map → chunk-XPCUKWLE.js.map} +0 -0
- /package/dist/{chunk-OJXK5C6Z.js.map → chunk-ZG4GNPRE.js.map} +0 -0
- /package/dist/{chunk-TYRWSE4B.js.map → chunk-ZUMHQEW4.js.map} +0 -0
- /package/dist/{chunk-DAOMWGO2.js.map → chunk-ZX5N5D6M.js.map} +0 -0
- /package/dist/{entity-canonical-id-migration-JJSTZF73.js.map → entity-canonical-id-migration-IG77BOGD.js.map} +0 -0
- /package/dist/{resolution-OVVIF7ZY.js.map → harmonic-graph-version.js.map} +0 -0
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { realpath } from "node:fs/promises";
|
|
3
3
|
import { listJsonFilesStrict, readJsonFile } from "./json-store.js";
|
|
4
4
|
import { throwIfAborted } from "./abort-error.js";
|
|
5
|
+
import { readHarmonicGraphStamp } from "./harmonic-graph-version.js";
|
|
5
6
|
import { inferMemoryStatus, toMemoryPathRel } from "./memory-lifecycle-ledger-utils.js";
|
|
6
7
|
import {
|
|
7
8
|
HARMONIC_SOURCE_MEMORY_INSERTED_AT_KEY,
|
|
@@ -403,7 +404,7 @@ export interface ProjectedHarmonicGraph {
|
|
|
403
404
|
* one of that node's surviving active sources, and is dropped once no
|
|
404
405
|
* reference survives.
|
|
405
406
|
*/
|
|
406
|
-
|
|
407
|
+
async function buildProjectedHarmonicGraph(options: {
|
|
407
408
|
memoryDir: string;
|
|
408
409
|
abstractionNodeStoreDir?: string;
|
|
409
410
|
anchorsEnabled: boolean;
|
|
@@ -460,6 +461,90 @@ export async function readProjectedHarmonicGraph(options: {
|
|
|
460
461
|
return { nodes: projectedNodes, anchors };
|
|
461
462
|
}
|
|
462
463
|
|
|
464
|
+
const GRAPH_CACHE_TTL_MS = 30_000;
|
|
465
|
+
const GRAPH_CACHE_MAX_ENTRIES = 8;
|
|
466
|
+
|
|
467
|
+
interface GraphCacheEntry {
|
|
468
|
+
stamp: string;
|
|
469
|
+
startedAtMs: number;
|
|
470
|
+
graph: ProjectedHarmonicGraph;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const graphCache = new Map<string, GraphCacheEntry>();
|
|
474
|
+
/** Counters for tests and diagnostics. */
|
|
475
|
+
export const harmonicGraphCacheStats = { hits: 0, misses: 0 };
|
|
476
|
+
|
|
477
|
+
/** Freeze records and their nested arrays/objects so a caller cannot corrupt the shared cache. */
|
|
478
|
+
function deepFreeze<T extends object>(value: T): T {
|
|
479
|
+
// isFrozen is true for primitives and null, and a frozen value is not revisited, so this ends on cycles too.
|
|
480
|
+
if (Object.isFrozen(value)) return value;
|
|
481
|
+
Object.freeze(value);
|
|
482
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
483
|
+
return value;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export function clearHarmonicGraphCache(): void {
|
|
487
|
+
graphCache.clear();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* `readProjectedHarmonicGraph` with an opt-in in-memory cache. Every recall used
|
|
492
|
+
* to re-read all nodes, anchors and source memories from disk (about 4.7 s on a
|
|
493
|
+
* 2107 node, 9199 anchor, 4105 source graph). With `useCache` a completed graph is
|
|
494
|
+
* reused while the stamp below is unchanged: the memory corpus, memory status and
|
|
495
|
+
* cold-write version sentinels (bumped by every memory write, in any process) and
|
|
496
|
+
* the graph version sentinel (bumped by node and anchor writes). A stamp read is
|
|
497
|
+
* four stats, not a scan of the memories. The TTL bounds the one input a sentinel
|
|
498
|
+
* cannot see, which is wall-clock validity expiry. An explicit `nowMs` bypasses the
|
|
499
|
+
* cache so tests stay deterministic. The cached arrays are frozen: callers must not
|
|
500
|
+
* mutate them.
|
|
501
|
+
*/
|
|
502
|
+
export async function readProjectedHarmonicGraph(options: {
|
|
503
|
+
memoryDir: string;
|
|
504
|
+
abstractionNodeStoreDir?: string;
|
|
505
|
+
anchorsEnabled: boolean;
|
|
506
|
+
abortSignal?: AbortSignal;
|
|
507
|
+
temporalExpiredInInjection?: boolean;
|
|
508
|
+
nowMs?: number;
|
|
509
|
+
useCache?: boolean;
|
|
510
|
+
}): Promise<ProjectedHarmonicGraph> {
|
|
511
|
+
if (options.useCache !== true || options.nowMs !== undefined) return buildProjectedHarmonicGraph(options);
|
|
512
|
+
// Same cancellation contract as the builder: a cache hit must not mask an aborted signal.
|
|
513
|
+
throwIfAborted(options.abortSignal, "harmonic retrieval aborted");
|
|
514
|
+
const nodeStoreDir = resolveAbstractionNodeStoreDir(options.memoryDir, options.abstractionNodeStoreDir);
|
|
515
|
+
const key = JSON.stringify([
|
|
516
|
+
options.memoryDir,
|
|
517
|
+
nodeStoreDir,
|
|
518
|
+
options.anchorsEnabled === true,
|
|
519
|
+
options.temporalExpiredInInjection === true,
|
|
520
|
+
]);
|
|
521
|
+
// The stamp is read BEFORE the build, so a write that lands mid-build leaves the
|
|
522
|
+
// entry stale and the next call rebuilds.
|
|
523
|
+
const stamp = readHarmonicGraphStamp(options.memoryDir, nodeStoreDir);
|
|
524
|
+
const startedAtMs = Date.now();
|
|
525
|
+
const hit = graphCache.get(key);
|
|
526
|
+
if (hit && hit.stamp === stamp && startedAtMs - hit.startedAtMs < GRAPH_CACHE_TTL_MS) {
|
|
527
|
+
harmonicGraphCacheStats.hits++;
|
|
528
|
+
graphCache.delete(key);
|
|
529
|
+
graphCache.set(key, hit);
|
|
530
|
+
return hit.graph;
|
|
531
|
+
}
|
|
532
|
+
harmonicGraphCacheStats.misses++;
|
|
533
|
+
const built = await buildProjectedHarmonicGraph(options);
|
|
534
|
+
const graph = {
|
|
535
|
+
nodes: deepFreeze(built.nodes),
|
|
536
|
+
anchors: deepFreeze(built.anchors),
|
|
537
|
+
};
|
|
538
|
+
graphCache.delete(key);
|
|
539
|
+
graphCache.set(key, { stamp, startedAtMs, graph });
|
|
540
|
+
while (graphCache.size > GRAPH_CACHE_MAX_ENTRIES) {
|
|
541
|
+
const oldest = graphCache.keys().next().value;
|
|
542
|
+
if (oldest === undefined) break;
|
|
543
|
+
graphCache.delete(oldest);
|
|
544
|
+
}
|
|
545
|
+
return graph;
|
|
546
|
+
}
|
|
547
|
+
|
|
463
548
|
export async function searchHarmonicRetrieval(options: {
|
|
464
549
|
memoryDir: string;
|
|
465
550
|
abstractionNodeStoreDir?: string;
|
|
@@ -469,6 +554,8 @@ export async function searchHarmonicRetrieval(options: {
|
|
|
469
554
|
anchorsEnabled: boolean;
|
|
470
555
|
abortSignal?: AbortSignal;
|
|
471
556
|
temporalExpiredInInjection?: boolean;
|
|
557
|
+
/** Share one projected graph between recalls until a write bumps a version sentinel (see readProjectedHarmonicGraph). */
|
|
558
|
+
useGraphCache?: boolean;
|
|
472
559
|
}): Promise<HarmonicRetrievalResult[]> {
|
|
473
560
|
throwIfAborted(options.abortSignal, "harmonic retrieval aborted");
|
|
474
561
|
const queryTokens = new Set(normalizeRecallTokens(options.query, ["what", "which"]));
|
|
@@ -476,7 +563,10 @@ export async function searchHarmonicRetrieval(options: {
|
|
|
476
563
|
|
|
477
564
|
// One projection for every graph reader: nodes and anchors both arrive
|
|
478
565
|
// already narrowed to the namespace's current active memories.
|
|
479
|
-
const { nodes: eligibleNodes, anchors } = await readProjectedHarmonicGraph(
|
|
566
|
+
const { nodes: eligibleNodes, anchors } = await readProjectedHarmonicGraph({
|
|
567
|
+
...options,
|
|
568
|
+
useCache: options.useGraphCache === true,
|
|
569
|
+
});
|
|
480
570
|
const candidates = new Map<string, HarmonicCandidate>();
|
|
481
571
|
|
|
482
572
|
for (const node of eligibleNodes) {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { CommittedWriteFinalization } from "./extraction-persist-failure.js";
|
|
5
|
+
|
|
6
|
+
function recordingFinalizer() {
|
|
7
|
+
const calls: boolean[] = [];
|
|
8
|
+
return {
|
|
9
|
+
calls,
|
|
10
|
+
finalize: async (scheduleQmd: boolean) => {
|
|
11
|
+
calls.push(scheduleQmd);
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test("#3288: the bound finalizer runs once across the success tail and catch sites", async () => {
|
|
17
|
+
const finalization = new CommittedWriteFinalization();
|
|
18
|
+
const { calls, finalize } = recordingFinalizer();
|
|
19
|
+
const once = finalization.bind(finalize);
|
|
20
|
+
|
|
21
|
+
const value = await finalization.run(async () => {
|
|
22
|
+
await once(true);
|
|
23
|
+
await once(false);
|
|
24
|
+
return "persisted";
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
assert.equal(value, "persisted");
|
|
28
|
+
assert.deepEqual(calls, [true], "a second caller reuses the first run instead of finalizing again");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("#3288: the pass boundary finalizes with QMD scheduling, then rethrows the original error", async () => {
|
|
32
|
+
const finalization = new CommittedWriteFinalization();
|
|
33
|
+
const { calls, finalize } = recordingFinalizer();
|
|
34
|
+
const cause = new Error("branch without its own catch site");
|
|
35
|
+
|
|
36
|
+
await assert.rejects(
|
|
37
|
+
finalization.run(async () => {
|
|
38
|
+
finalization.bind(finalize);
|
|
39
|
+
throw cause;
|
|
40
|
+
}),
|
|
41
|
+
(error: Error) => error === cause,
|
|
42
|
+
);
|
|
43
|
+
assert.deepEqual(calls, [true]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("#3288: the pass boundary does not finalize again after a catch site already did", async () => {
|
|
47
|
+
const finalization = new CommittedWriteFinalization();
|
|
48
|
+
const { calls, finalize } = recordingFinalizer();
|
|
49
|
+
const cause = new Error("catch site rethrow");
|
|
50
|
+
|
|
51
|
+
await assert.rejects(
|
|
52
|
+
finalization.run(async () => {
|
|
53
|
+
const once = finalization.bind(finalize);
|
|
54
|
+
await once(true);
|
|
55
|
+
throw cause;
|
|
56
|
+
}),
|
|
57
|
+
(error: Error) => error === cause,
|
|
58
|
+
);
|
|
59
|
+
assert.deepEqual(calls, [true]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("#3288: a throw before the finalizer is bound propagates without finalizing", async () => {
|
|
63
|
+
const finalization = new CommittedWriteFinalization();
|
|
64
|
+
const cause = new Error("failed before any write");
|
|
65
|
+
|
|
66
|
+
await assert.rejects(
|
|
67
|
+
finalization.run(async () => {
|
|
68
|
+
throw cause;
|
|
69
|
+
}),
|
|
70
|
+
(error: Error) => error === cause,
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("#3288: a failing finalizer does not mask the original error", async () => {
|
|
75
|
+
const finalization = new CommittedWriteFinalization();
|
|
76
|
+
const cause = new Error("branch failure");
|
|
77
|
+
let attempts = 0;
|
|
78
|
+
|
|
79
|
+
await assert.rejects(
|
|
80
|
+
finalization.run(async () => {
|
|
81
|
+
finalization.bind(async () => {
|
|
82
|
+
attempts += 1;
|
|
83
|
+
throw new Error("finalization failure");
|
|
84
|
+
});
|
|
85
|
+
throw cause;
|
|
86
|
+
}),
|
|
87
|
+
(error: Error) => error === cause,
|
|
88
|
+
);
|
|
89
|
+
assert.equal(attempts, 1);
|
|
90
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { log } from "../logger.js";
|
|
2
2
|
|
|
3
|
+
type FinalizeCommittedWrites = (scheduleQmd: boolean) => Promise<void>;
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Fail a persistence pass after a durable write rejected (#3227, same
|
|
5
7
|
* contract as the entity write path from #3228). Earlier writes in the batch
|
|
@@ -8,7 +10,7 @@ import { log } from "../logger.js";
|
|
|
8
10
|
* instead of recording the fingerprint and clearing the source turns.
|
|
9
11
|
*/
|
|
10
12
|
export async function rethrowAfterCommittedWriteFinalization(
|
|
11
|
-
finalizeCommittedWrites:
|
|
13
|
+
finalizeCommittedWrites: FinalizeCommittedWrites,
|
|
12
14
|
cause: unknown,
|
|
13
15
|
outputKind: string,
|
|
14
16
|
): Promise<never> {
|
|
@@ -17,3 +19,39 @@ export async function rethrowAfterCommittedWriteFinalization(
|
|
|
17
19
|
);
|
|
18
20
|
throw cause;
|
|
19
21
|
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Batch-end finalization for one persistExtraction pass (#3288).
|
|
25
|
+
*
|
|
26
|
+
* `bind` wraps the pass's finalizer so it runs at most once. The success
|
|
27
|
+
* tail, the per-branch catch sites and the pass boundary can all reach it,
|
|
28
|
+
* and a second run would append the same behavior signals again.
|
|
29
|
+
*
|
|
30
|
+
* `run` is the pass boundary. A throw from a branch with no catch site of its
|
|
31
|
+
* own (a chunked-parent post-write step, the semantic-merge post effects, the
|
|
32
|
+
* profile or question write) still finalizes the writes already committed
|
|
33
|
+
* before the original error propagates. When a catch site or the success tail
|
|
34
|
+
* already ran the finalizer, the boundary only rethrows. Nothing is durable
|
|
35
|
+
* before the finalizer is bound, so an earlier throw propagates as is.
|
|
36
|
+
*/
|
|
37
|
+
export class CommittedWriteFinalization {
|
|
38
|
+
private finalizer: FinalizeCommittedWrites | undefined;
|
|
39
|
+
private started: Promise<void> | undefined;
|
|
40
|
+
|
|
41
|
+
bind(finalize: FinalizeCommittedWrites): FinalizeCommittedWrites {
|
|
42
|
+
this.finalizer = (scheduleQmd) => (this.started ??= finalize(scheduleQmd));
|
|
43
|
+
return this.finalizer;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async run<T>(pass: () => Promise<T>): Promise<T> {
|
|
47
|
+
try {
|
|
48
|
+
return await pass();
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (!this.finalizer || this.started) throw err;
|
|
51
|
+
return rethrowAfterCommittedWriteFinalization(this.finalizer, err, "persist pass");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A pass method's parameter list without its leading CommittedWriteFinalization. */
|
|
57
|
+
export type ArgsAfterFinalization<Params> = Params extends [CommittedWriteFinalization, ...infer Rest] ? Rest : never;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { classifyExtractionOrigin, type ExtractionSourceContext } from "./extraction-origin-context.js";
|
|
2
2
|
import { confirmActiveFactDuplicate, persistUnderFactDedupWindow } from "./fact-dedup-window.js";
|
|
3
3
|
import { writePromotionUnderFactDedupWindow } from "./promotion-dedup-window.js";
|
|
4
|
-
import { rethrowAfterCommittedWriteFinalization } from "./extraction-persist-failure.js";
|
|
4
|
+
import { type ArgsAfterFinalization, CommittedWriteFinalization, rethrowAfterCommittedWriteFinalization } from "./extraction-persist-failure.js";
|
|
5
5
|
import { evaluateInjectionScreen, screenEntityForIndex, screenPersistStrings } from "./extraction-injection-gate.js";
|
|
6
6
|
/**
|
|
7
7
|
* Extraction persistence coordinator (issue #1526, seam 16).
|
|
@@ -152,7 +152,13 @@ export class ExtractionPersistCoordinator {
|
|
|
152
152
|
return this.deps.getDependencyPropagationDelivery();
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
|
|
155
|
+
persistExtraction(...args: ArgsAfterFinalization<Parameters<ExtractionPersistCoordinator["persistExtractionPass"]>>): Promise<ExtractionPersistResult> {
|
|
156
|
+
const finalization = new CommittedWriteFinalization(); // #3288: one batch-end finalization boundary per pass
|
|
157
|
+
return finalization.run(() => this.persistExtractionPass(finalization, ...args));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private async persistExtractionPass(
|
|
161
|
+
finalization: CommittedWriteFinalization,
|
|
156
162
|
result: ExtractionResult,
|
|
157
163
|
storage: StorageManager,
|
|
158
164
|
threadIdForExtraction?: string | null,
|
|
@@ -1125,7 +1131,7 @@ export class ExtractionPersistCoordinator {
|
|
|
1125
1131
|
0,
|
|
1126
1132
|
this.deps.config.extractionMaxProfileUpdatesPerRun,
|
|
1127
1133
|
);
|
|
1128
|
-
const finalizeCommittedWrites = async (scheduleQmd: boolean): Promise<void> => {
|
|
1134
|
+
const finalizeCommittedWrites = finalization.bind(async (scheduleQmd: boolean): Promise<void> => {
|
|
1129
1135
|
await finalizeExtractionCommittedWrites({
|
|
1130
1136
|
saveContentHashIndexes: () => this.deps.saveContentHashIndexes(),
|
|
1131
1137
|
behaviorSignalsByStorage: behaviorSignalsByStorage.values(),
|
|
@@ -1150,7 +1156,7 @@ export class ExtractionPersistCoordinator {
|
|
|
1150
1156
|
requestQmdMaintenance: () => this.deps.requestQmdMaintenance(),
|
|
1151
1157
|
scheduleQmd,
|
|
1152
1158
|
});
|
|
1153
|
-
};
|
|
1159
|
+
});
|
|
1154
1160
|
|
|
1155
1161
|
if (
|
|
1156
1162
|
facts.length < result.facts.length ||
|
|
@@ -2211,6 +2217,24 @@ export class ExtractionPersistCoordinator {
|
|
|
2211
2217
|
const tombstoneBlocked = parentWrite.tombstoneBlocked;
|
|
2212
2218
|
const postWriteGuard =
|
|
2213
2219
|
faithfulnessEnforceStatus === "pending_review" || tombstoneBlocked;
|
|
2220
|
+
// #3288: the parent is durable now. Track it before any post-write step
|
|
2221
|
+
// that can throw (non-chunked rule, #3231); a retry exact-dedupes it.
|
|
2222
|
+
trackBehaviorSignals(
|
|
2223
|
+
targetStorage,
|
|
2224
|
+
buildBehaviorSignalsForMemory({
|
|
2225
|
+
memoryId: parentId,
|
|
2226
|
+
category: writeCategory,
|
|
2227
|
+
content: fact.content,
|
|
2228
|
+
namespace: this.deps.storageDirNamespace(targetStorage.dir),
|
|
2229
|
+
confidence: fact.confidence,
|
|
2230
|
+
source: "extraction",
|
|
2231
|
+
}),
|
|
2232
|
+
);
|
|
2233
|
+
trackPersistedId(targetStorage, parentId, {
|
|
2234
|
+
pendingReview: postWriteGuard,
|
|
2235
|
+
category: writeCategory,
|
|
2236
|
+
harmonicFact,
|
|
2237
|
+
});
|
|
2214
2238
|
// #1645: defer contradiction auto-resolve until tombstone status is
|
|
2215
2239
|
// known (see applyDeferredContradictionResolve).
|
|
2216
2240
|
const contradictionOutcome = await this.deps.applyDeferredContradictionResolve(
|
|
@@ -2306,11 +2330,6 @@ export class ExtractionPersistCoordinator {
|
|
|
2306
2330
|
log.debug(
|
|
2307
2331
|
`chunked memory ${parentId} into ${chunkResult.chunks.length} chunks`,
|
|
2308
2332
|
);
|
|
2309
|
-
trackPersistedId(targetStorage, parentId, {
|
|
2310
|
-
pendingReview: postWriteGuard,
|
|
2311
|
-
category: writeCategory,
|
|
2312
|
-
harmonicFact,
|
|
2313
|
-
});
|
|
2314
2333
|
await anchorSnapshots.replace(targetStorage, parentId, writeCategory, memoryPathById);
|
|
2315
2334
|
// #1576 (cursor Medium): keep pending_review ids out of threadEpisodeIdsForGraph — else later active facts build thread-predecessor edges to an unfaithful memory.
|
|
2316
2335
|
if (
|
|
@@ -2490,17 +2509,6 @@ export class ExtractionPersistCoordinator {
|
|
|
2490
2509
|
} finally {
|
|
2491
2510
|
// #1522: the catalog write touch lives in the storage chokepoint.
|
|
2492
2511
|
}
|
|
2493
|
-
trackBehaviorSignals(
|
|
2494
|
-
targetStorage,
|
|
2495
|
-
buildBehaviorSignalsForMemory({
|
|
2496
|
-
memoryId: parentId,
|
|
2497
|
-
category: writeCategory,
|
|
2498
|
-
content: fact.content,
|
|
2499
|
-
namespace: this.deps.storageDirNamespace(targetStorage.dir),
|
|
2500
|
-
confidence: fact.confidence,
|
|
2501
|
-
source: "extraction",
|
|
2502
|
-
}),
|
|
2503
|
-
);
|
|
2504
2512
|
continue; // Skip the normal write below
|
|
2505
2513
|
}
|
|
2506
2514
|
}
|
|
@@ -2967,7 +2975,7 @@ export class ExtractionPersistCoordinator {
|
|
|
2967
2975
|
}
|
|
2968
2976
|
|
|
2969
2977
|
// Shared batch-end finalization (hash, behavior, harmonic, temporal).
|
|
2970
|
-
// Failure paths call
|
|
2978
|
+
// Failure paths call it with scheduleQmd=true (#3231); it runs once per pass (#3288).
|
|
2971
2979
|
await finalizeCommittedWrites(false);
|
|
2972
2980
|
|
|
2973
2981
|
const dedupSuffix = `${dedupedCount > 0 ? ` (${dedupedCount} deduped)` : ""}${semanticMergedCount > 0 ? ` (${semanticMergedCount} merged)` : ""}`;
|
|
@@ -1537,7 +1537,7 @@ export class RecallInternalCoordinator {
|
|
|
1537
1537
|
sessionKey,
|
|
1538
1538
|
anchorsEnabled: resolveConsolidationCapabilities(this.deps.config).abstractionAnchors,
|
|
1539
1539
|
temporalExpiredInInjection: this.deps.config.temporalExpiredInInjection,
|
|
1540
|
-
abortSignal: harmonicRetrievalAbort.signal,
|
|
1540
|
+
abortSignal: harmonicRetrievalAbort.signal, useGraphCache: true,
|
|
1541
1541
|
}),
|
|
1542
1542
|
),
|
|
1543
1543
|
);
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
|
|
6
|
+
import { resetExtractionLlmHealthForTests } from "./extraction-llm-health.js";
|
|
7
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
8
|
+
import type { StorageManager } from "./storage.js";
|
|
9
|
+
import {
|
|
10
|
+
cleanupDir,
|
|
11
|
+
harmonicNodeStoreStatus,
|
|
12
|
+
makeHarmonicLifecycleConfig,
|
|
13
|
+
mkTempMemoryDir,
|
|
14
|
+
} from "./testing/orchestrator-lite.js";
|
|
15
|
+
import type { ExtractionResult } from "./types.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* #3288: a branch that throws after an earlier fact in the same batch is
|
|
19
|
+
* durable must still land that fact's batch-end effects (behavior signal,
|
|
20
|
+
* harmonic node, temporal/tag index entry, QMD request) before the error
|
|
21
|
+
* reaches the caller. A retry exact-dedupes the earlier fact, so this failure
|
|
22
|
+
* path is the only chance those effects get.
|
|
23
|
+
*/
|
|
24
|
+
const EARLIER = "Prefers terse queue names for background jobs in Project Gamma.";
|
|
25
|
+
|
|
26
|
+
type BranchCase = {
|
|
27
|
+
label: string;
|
|
28
|
+
config?: Parameters<typeof makeHarmonicLifecycleConfig>[1];
|
|
29
|
+
/** Outputs persisted after the EARLIER fact; one of them hits the injected failure. */
|
|
30
|
+
later: Partial<ExtractionResult>;
|
|
31
|
+
/** Installs the failure and returns the error the pass must rethrow. */
|
|
32
|
+
inject: (orchestrator: Orchestrator, storage: StorageManager, memoryDir: string) => Promise<Error>;
|
|
33
|
+
/**
|
|
34
|
+
* Proves the failing branch had already committed its own write when it
|
|
35
|
+
* threw and returns that write's id. It is durable too, so batch-end
|
|
36
|
+
* finalization must cover it as well as the earlier fact.
|
|
37
|
+
*/
|
|
38
|
+
committedBeforeFailure?: (storage: StorageManager) => Promise<string>;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
async function assertEarlierFactFinalized(branch: BranchCase): Promise<void> {
|
|
42
|
+
const memoryDir = await mkTempMemoryDir(`persist-branch-${branch.label}`);
|
|
43
|
+
const config = makeHarmonicLifecycleConfig(memoryDir, {
|
|
44
|
+
chunkingEnabled: false,
|
|
45
|
+
multiGraphMemoryEnabled: false,
|
|
46
|
+
factDeduplicationEnabled: true,
|
|
47
|
+
extractionMinImportanceLevel: "trivial",
|
|
48
|
+
entityActivityLogEnabled: false,
|
|
49
|
+
queryAwareIndexingEnabled: true,
|
|
50
|
+
...branch.config,
|
|
51
|
+
});
|
|
52
|
+
const orchestrator = new Orchestrator(config);
|
|
53
|
+
const storage = await orchestrator.getStorageForNamespace();
|
|
54
|
+
const temporalIdBatches: string[][] = [];
|
|
55
|
+
// Finalization starts the temporal/tag update without awaiting it; cleanup
|
|
56
|
+
// waits for it so the temp dir is not removed under its lock.
|
|
57
|
+
const temporalUpdates: Promise<void>[] = [];
|
|
58
|
+
// Bracket access is TypeScript's typed escape hatch for the private method; the spy only wraps it.
|
|
59
|
+
const originalTemporal = orchestrator["updateTemporalTagIndexes"].bind(orchestrator);
|
|
60
|
+
orchestrator["updateTemporalTagIndexes"] = (target, persistedIds) => {
|
|
61
|
+
temporalIdBatches.push([...persistedIds]);
|
|
62
|
+
const update = originalTemporal(target, persistedIds);
|
|
63
|
+
temporalUpdates.push(update);
|
|
64
|
+
return update;
|
|
65
|
+
};
|
|
66
|
+
let qmdMaintenanceCalls = 0;
|
|
67
|
+
const originalQmd = orchestrator.requestQmdMaintenance.bind(orchestrator);
|
|
68
|
+
orchestrator.requestQmdMaintenance = () => {
|
|
69
|
+
qmdMaintenanceCalls += 1;
|
|
70
|
+
originalQmd();
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
await storage.ensureDirectories();
|
|
74
|
+
const failure = await branch.inject(orchestrator, storage, memoryDir);
|
|
75
|
+
const result: ExtractionResult = {
|
|
76
|
+
entities: [],
|
|
77
|
+
questions: [],
|
|
78
|
+
profileUpdates: [],
|
|
79
|
+
...branch.later,
|
|
80
|
+
facts: [
|
|
81
|
+
{ content: EARLIER, category: "preference", confidence: 0.95, tags: ["queue-naming"] },
|
|
82
|
+
...(branch.later.facts ?? []),
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
await assert.rejects(
|
|
87
|
+
orchestrator.persistExtraction(result, storage, null, { sessionKey: `session:${branch.label}` }),
|
|
88
|
+
(error: Error) => error === failure,
|
|
89
|
+
"the branch failure must reach the caller unchanged",
|
|
90
|
+
);
|
|
91
|
+
const branchWriteId = await branch.committedBeforeFailure?.(storage);
|
|
92
|
+
|
|
93
|
+
const earlier = (await storage.readAllMemories()).filter((m) => m.frontmatter.category === "preference");
|
|
94
|
+
assert.equal(earlier.length, 1, "the earlier fact must stay durable");
|
|
95
|
+
const factId = earlier[0]!.frontmatter.id;
|
|
96
|
+
assert.ok(
|
|
97
|
+
(await storage.readBehaviorSignals(50)).some(
|
|
98
|
+
(s) => s.memoryId === factId && s.signalType === "preference_affinity",
|
|
99
|
+
),
|
|
100
|
+
"the earlier fact's behavior signal must land before the rethrow",
|
|
101
|
+
);
|
|
102
|
+
const harmonicSources = (await harmonicNodeStoreStatus(memoryDir)).latestNode?.sourceMemoryIds ?? [];
|
|
103
|
+
for (const id of branchWriteId ? [factId, branchWriteId] : [factId]) {
|
|
104
|
+
assert.ok(harmonicSources.includes(id), `the harmonic node must cover durable write ${id} before the rethrow`);
|
|
105
|
+
assert.ok(
|
|
106
|
+
temporalIdBatches.some((ids) => ids.includes(id)),
|
|
107
|
+
`the temporal/tag index update must cover durable write ${id} before the rethrow`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
assert.ok(qmdMaintenanceCalls > 0, "the partial batch must still schedule QMD maintenance");
|
|
111
|
+
} finally {
|
|
112
|
+
await Promise.allSettled(temporalUpdates);
|
|
113
|
+
orchestrator.requestQmdMaintenance = originalQmd;
|
|
114
|
+
orchestrator["updateTemporalTagIndexes"] = originalTemporal;
|
|
115
|
+
try {
|
|
116
|
+
await orchestrator.destroy();
|
|
117
|
+
} finally {
|
|
118
|
+
resetExtractionLlmHealthForTests();
|
|
119
|
+
await cleanupDir(memoryDir);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
test("#3288: a chunk write failure after the chunked parent is durable finalizes the earlier fact", async () => {
|
|
125
|
+
await assertEarlierFactFinalized({
|
|
126
|
+
label: "chunk-write",
|
|
127
|
+
config: {
|
|
128
|
+
chunkingEnabled: true,
|
|
129
|
+
chunkingTargetTokens: 12,
|
|
130
|
+
chunkingMinTokens: 10,
|
|
131
|
+
chunkingOverlapSentences: 0,
|
|
132
|
+
},
|
|
133
|
+
later: {
|
|
134
|
+
facts: [
|
|
135
|
+
{
|
|
136
|
+
content:
|
|
137
|
+
"Project Gamma runs its nightly export on a dedicated worker pool. " +
|
|
138
|
+
"The export writes compressed archives to the cold storage bucket. " +
|
|
139
|
+
"Each archive is verified against a checksum manifest before upload. " +
|
|
140
|
+
"Failed uploads are retried three times with exponential backoff.",
|
|
141
|
+
category: "fact",
|
|
142
|
+
confidence: 0.9,
|
|
143
|
+
tags: ["export"],
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
inject: async (_orchestrator, storage) => {
|
|
148
|
+
const failure = new Error("synthetic chunk write failure");
|
|
149
|
+
storage.writeChunk = async () => {
|
|
150
|
+
throw failure;
|
|
151
|
+
};
|
|
152
|
+
return failure;
|
|
153
|
+
},
|
|
154
|
+
committedBeforeFailure: async (storage) => {
|
|
155
|
+
const parents = (await storage.readAllMemories()).filter((m) => m.frontmatter.tags?.includes("chunked"));
|
|
156
|
+
assert.equal(parents.length, 1, "the chunked parent must be durable before the chunk write throws");
|
|
157
|
+
return parents[0]!.frontmatter.id;
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const MERGE_TARGET_ID = "merge-target-gamma";
|
|
163
|
+
const MERGE_SEED = "The Project Gamma billing service deploys on a Tuesday cadence.";
|
|
164
|
+
const MERGE_INCOMING = "The Project Gamma billing service deploys at 09:00 UTC sharp.";
|
|
165
|
+
|
|
166
|
+
/** Seed an active merge target and a deterministic merge judge for MERGE_INCOMING only. */
|
|
167
|
+
async function armSemanticMerge(orchestrator: Orchestrator, memoryDir: string): Promise<void> {
|
|
168
|
+
const created = new Date(Date.now() - 3600_000).toISOString();
|
|
169
|
+
const seededDir = path.join(memoryDir, "facts", created.slice(0, 10));
|
|
170
|
+
await mkdir(seededDir, { recursive: true });
|
|
171
|
+
await writeFile(
|
|
172
|
+
path.join(seededDir, `${MERGE_TARGET_ID}.md`),
|
|
173
|
+
[
|
|
174
|
+
"---",
|
|
175
|
+
`id: ${MERGE_TARGET_ID}`,
|
|
176
|
+
"category: fact",
|
|
177
|
+
`created: ${created}`,
|
|
178
|
+
`updated: ${created}`,
|
|
179
|
+
"source: extraction",
|
|
180
|
+
"confidence: 0.9",
|
|
181
|
+
"confidenceTier: explicit",
|
|
182
|
+
"status: active",
|
|
183
|
+
"importanceScore: 0.9",
|
|
184
|
+
"importanceLevel: high",
|
|
185
|
+
"---",
|
|
186
|
+
"",
|
|
187
|
+
MERGE_SEED,
|
|
188
|
+
"",
|
|
189
|
+
].join("\n"),
|
|
190
|
+
"utf8",
|
|
191
|
+
);
|
|
192
|
+
// Band neighbor for the incoming fact only ([minSimilarity, threshold)).
|
|
193
|
+
const embeddings = orchestrator.embeddingFallback;
|
|
194
|
+
embeddings.isAvailable = async () => true;
|
|
195
|
+
embeddings.search = async (query) => (query === MERGE_INCOMING ? [{ id: MERGE_TARGET_ID, score: 0.85, path: "" }] : []);
|
|
196
|
+
embeddings.indexFile = async () => {};
|
|
197
|
+
embeddings.removeFromIndex = async () => {};
|
|
198
|
+
// Deterministic merge judge; every other local-LLM prompt gets no answer.
|
|
199
|
+
orchestrator.localLlm.chatCompletion = async (messages) =>
|
|
200
|
+
messages[0]?.role === "system" && messages[0].content.startsWith("You maintain a long-term memory store")
|
|
201
|
+
? {
|
|
202
|
+
content: JSON.stringify({
|
|
203
|
+
decision: "merge",
|
|
204
|
+
targetId: MERGE_TARGET_ID,
|
|
205
|
+
mergedContent: `${MERGE_SEED} ${MERGE_INCOMING}`,
|
|
206
|
+
reason: "deterministic judge for the merge-branch failure test",
|
|
207
|
+
}),
|
|
208
|
+
}
|
|
209
|
+
: null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
test("#3288: a semantic-merge post-effect failure after the merge commits finalizes the earlier fact", async () => {
|
|
213
|
+
await assertEarlierFactFinalized({
|
|
214
|
+
label: "semantic-merge",
|
|
215
|
+
config: {
|
|
216
|
+
embeddingFallbackEnabled: true,
|
|
217
|
+
versioningEnabled: true,
|
|
218
|
+
semanticMerge: { enabled: true },
|
|
219
|
+
multiGraphMemoryEnabled: true,
|
|
220
|
+
},
|
|
221
|
+
later: {
|
|
222
|
+
facts: [{ content: MERGE_INCOMING, category: "fact", confidence: 0.9, tags: [] }],
|
|
223
|
+
},
|
|
224
|
+
inject: async (orchestrator, _storage, memoryDir) => {
|
|
225
|
+
await armSemanticMerge(orchestrator, memoryDir);
|
|
226
|
+
// The merged-target graph rebuild fails open, but the edge-cache
|
|
227
|
+
// invalidation that follows it is not wrapped: an unavailable graph
|
|
228
|
+
// index escapes runMergedTargetPostEffects after the merge committed.
|
|
229
|
+
const failure = new Error("synthetic graph index failure");
|
|
230
|
+
orchestrator["graphIndexFor"] = () => {
|
|
231
|
+
throw failure;
|
|
232
|
+
};
|
|
233
|
+
return failure;
|
|
234
|
+
},
|
|
235
|
+
committedBeforeFailure: async (storage) => {
|
|
236
|
+
const target = await storage.getMemoryByIdIncludingArchived(MERGE_TARGET_ID);
|
|
237
|
+
assert.ok(target?.content.includes(MERGE_INCOMING), "the merge must be committed before the post effect throws");
|
|
238
|
+
return MERGE_TARGET_ID;
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("#3288: a profile append failure after an earlier fact is durable finalizes that fact", async () => {
|
|
244
|
+
await assertEarlierFactFinalized({
|
|
245
|
+
label: "profile-append",
|
|
246
|
+
later: { profileUpdates: ["Works mostly on the Project Gamma export pipeline."] },
|
|
247
|
+
inject: async (_orchestrator, storage) => {
|
|
248
|
+
const failure = new Error("synthetic profile append failure");
|
|
249
|
+
storage.appendToProfile = async () => {
|
|
250
|
+
throw failure;
|
|
251
|
+
};
|
|
252
|
+
return failure;
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("#3288: a question write failure after an earlier fact is durable finalizes that fact", async () => {
|
|
258
|
+
await assertEarlierFactFinalized({
|
|
259
|
+
label: "question-write",
|
|
260
|
+
later: {
|
|
261
|
+
questions: [{ question: "Which bucket holds the Project Gamma archives?", context: "export pipeline", priority: 0.5 }],
|
|
262
|
+
},
|
|
263
|
+
inject: async (_orchestrator, storage) => {
|
|
264
|
+
const failure = new Error("synthetic question write failure");
|
|
265
|
+
storage.writeQuestion = async () => {
|
|
266
|
+
throw failure;
|
|
267
|
+
};
|
|
268
|
+
return failure;
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
});
|