@remnic/core 10.0.65 → 10.0.67
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.js +16 -16
- package/dist/access-authorization-probe.js +17 -17
- package/dist/access-boundary.js +16 -16
- package/dist/access-cli.js +61 -61
- package/dist/access-extraction-force-flush.js +16 -16
- package/dist/access-http-briefing.js +17 -17
- package/dist/access-http-query.js +18 -18
- package/dist/access-http.js +33 -33
- package/dist/access-identity-continuity-surface.js +16 -16
- package/dist/access-lcm-surface.js +16 -16
- package/dist/access-mcp.js +26 -26
- package/dist/access-namespace-preflight.js +16 -16
- package/dist/access-observe-write-surface.js +16 -16
- package/dist/access-operations-batch.js +17 -17
- package/dist/access-operations.js +24 -24
- package/dist/access-recall-concurrency.js +16 -16
- package/dist/access-recall-diagnostics.js +2 -2
- package/dist/access-recall-response.js +16 -16
- package/dist/access-recall-surface.js +16 -16
- package/dist/access-service.js +16 -16
- package/dist/access-surface-catalog.js +18 -18
- package/dist/active-recall.js +3 -3
- package/dist/briefing.js +3 -3
- package/dist/buffer.js +2 -2
- package/dist/causal-consolidation.js +4 -4
- package/dist/{chunk-KISOMGY2.js → chunk-3RHU5MD7.js} +2 -2
- package/dist/{chunk-3LSTL6EY.js → chunk-4MPAKYZU.js} +2 -2
- package/dist/{chunk-OVL5AD3N.js → chunk-4NV7O76Q.js} +2 -2
- package/dist/{chunk-BH7NVSEC.js → chunk-4SOWMA55.js} +3 -3
- package/dist/{chunk-GROHOWR5.js → chunk-5MGKGEL7.js} +2 -2
- package/dist/{chunk-ZPLQ27MY.js → chunk-5UM4H64E.js} +2 -2
- package/dist/{chunk-2DNFRLLL.js → chunk-5Y6VA6X2.js} +2 -2
- package/dist/{chunk-GRAUQXQB.js → chunk-6WKWL53V.js} +13 -13
- package/dist/{chunk-HOR4RMUM.js → chunk-75DCOS6I.js} +24 -24
- package/dist/{chunk-25QK3LR2.js → chunk-7O2MC42R.js} +2 -2
- package/dist/{chunk-7ETWIBTO.js → chunk-ADYP7HXE.js} +3 -3
- package/dist/{chunk-FUDKGJDN.js → chunk-AVGDYYMA.js} +2 -2
- package/dist/{chunk-ECF627KC.js → chunk-AYJQ644F.js} +2 -2
- package/dist/{chunk-T22FBDG3.js → chunk-B3MWELZ3.js} +2 -2
- package/dist/{chunk-DRTSPRWL.js → chunk-B4DXREWC.js} +2 -2
- package/dist/{chunk-5UEGL5BF.js → chunk-CI3MAAIY.js} +2 -2
- package/dist/{chunk-7JYK7NNY.js → chunk-CTH2CQCB.js} +2 -2
- package/dist/{chunk-UVHXLVFJ.js → chunk-EURPSF4Y.js} +2 -2
- package/dist/{chunk-VPEA63FC.js → chunk-EWJZZDRG.js} +2 -2
- package/dist/{chunk-BOZQAUAJ.js → chunk-EXQJMFRF.js} +2 -2
- package/dist/{chunk-UH6ROXFJ.js → chunk-GGD2YDS6.js} +2 -2
- package/dist/{chunk-5JABRT5G.js → chunk-GHHQLSSY.js} +3 -3
- package/dist/{chunk-N4T5FW6U.js → chunk-GJ4HU6LK.js} +2 -2
- package/dist/{chunk-HRRAOTOQ.js → chunk-H5DIRCZP.js} +12 -12
- package/dist/{chunk-434E4BTO.js → chunk-HH73M65H.js} +3 -3
- package/dist/{chunk-LIGZO7XN.js → chunk-HPB5TF55.js} +2 -2
- package/dist/{chunk-E4QIX2OB.js → chunk-IAGVM7EY.js} +2 -2
- package/dist/{chunk-YJQJRXUI.js → chunk-IG3EZ3TF.js} +2 -2
- package/dist/{chunk-KNMSD2MQ.js → chunk-IVUVQPTQ.js} +2 -2
- package/dist/{chunk-XOFIJGB2.js → chunk-IZBIDPMY.js} +2 -2
- package/dist/{chunk-HFEGRMFG.js → chunk-JLGBJ4AX.js} +7 -7
- package/dist/{chunk-BBHKN2CN.js → chunk-JLMOH5JA.js} +2 -2
- package/dist/{chunk-TPCRDPUY.js → chunk-KZSGBWOJ.js} +2 -2
- package/dist/{chunk-SHHZWSSE.js → chunk-KZZ5EZ7O.js} +2 -2
- package/dist/{chunk-2ZSRACNK.js → chunk-LFYEQK45.js} +682 -549
- package/dist/chunk-LFYEQK45.js.map +1 -0
- package/dist/{chunk-FKGLJJVS.js → chunk-LN4X2NDA.js} +2 -2
- package/dist/{chunk-5ZHFPUBC.js → chunk-MOIJWWNA.js} +8 -8
- package/dist/{chunk-KO224R36.js → chunk-NCZGGGWI.js} +2 -2
- package/dist/{chunk-XVQVF2TK.js → chunk-NHWDTOBN.js} +269 -230
- package/dist/chunk-NHWDTOBN.js.map +1 -0
- package/dist/{chunk-7ZTGMEXF.js → chunk-O5ZINWW3.js} +2 -2
- package/dist/{chunk-WWH7GJ42.js → chunk-OCK4D5PC.js} +2 -2
- package/dist/{chunk-G6L3REL3.js → chunk-OPFVXM2D.js} +2 -2
- package/dist/{chunk-FR7PK7ED.js → chunk-P4TV3TQ2.js} +2 -2
- package/dist/{chunk-P2EM5LKH.js → chunk-PMDOGXA2.js} +2 -2
- package/dist/{chunk-NM2II57L.js → chunk-PRKNAIGZ.js} +2 -1
- package/dist/chunk-PRKNAIGZ.js.map +1 -0
- package/dist/{chunk-MRF5V5CD.js → chunk-QGFR73SN.js} +2 -2
- package/dist/{chunk-6D3IWYNY.js → chunk-R2BIVU2Z.js} +354 -292
- package/dist/chunk-R2BIVU2Z.js.map +1 -0
- package/dist/{chunk-HG3CG557.js → chunk-RLVI2M5Q.js} +2 -2
- package/dist/{chunk-HHL4MX3W.js → chunk-SXTVZ6JL.js} +2 -2
- package/dist/{chunk-CLSKGTAT.js → chunk-TECK5IVY.js} +2 -2
- package/dist/{chunk-SNS3DWG6.js → chunk-TVE7W67N.js} +2 -2
- package/dist/{chunk-KYSSUY66.js → chunk-U2CAGMUD.js} +2 -2
- package/dist/{chunk-XLCEFUAU.js → chunk-UVVFPCPX.js} +2 -2
- package/dist/{chunk-KQJUEQ2E.js → chunk-UW6LBFYN.js} +5 -5
- package/dist/{chunk-SEA56G6S.js → chunk-V4XE263I.js} +1 -1
- package/dist/chunk-V4XE263I.js.map +1 -0
- package/dist/{chunk-JL5HXL25.js → chunk-V6SBA5O7.js} +3 -3
- package/dist/{chunk-R7WYAXPO.js → chunk-VHSQU3RO.js} +81 -42
- package/dist/chunk-VHSQU3RO.js.map +1 -0
- package/dist/{chunk-B7VD36TI.js → chunk-WHROVQ5Q.js} +3 -3
- package/dist/{chunk-LDHR465K.js → chunk-Y3XPSGNF.js} +2 -2
- package/dist/{chunk-JPY3RMG2.js → chunk-ZORS5BG5.js} +4 -4
- package/dist/{chunk-KXY4HW2Y.js → chunk-ZXF3O7P4.js} +2 -2
- package/dist/cli.js +47 -47
- package/dist/coding/export-okf-codegraph.js +2 -2
- package/dist/compounding/engine.js +4 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -3
- package/dist/connectors/codex-materialize-runner.js +3 -3
- package/dist/connectors/index.js +3 -3
- package/dist/convergence-refresh.js +5 -5
- package/dist/corpus-watermark.js +5 -5
- package/dist/entity-retrieval.js +3 -3
- package/dist/external-wiki-access.js +17 -17
- package/dist/extraction.js +6 -6
- package/dist/harmonic-retrieval.js +3 -3
- package/dist/index.js +61 -61
- package/dist/location/index.js +5 -5
- package/dist/maintenance/memory-governance.js +3 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +3 -3
- package/dist/maintenance/rebuild-memory-projection-runner.js +5 -5
- package/dist/maintenance/rebuild-memory-projection.js +4 -4
- package/dist/memory-browse-operations.js +17 -17
- package/dist/namespaces/migrate.js +4 -4
- package/dist/namespaces/storage.js +3 -3
- package/dist/operator-doctor-corpus.js +6 -6
- package/dist/operator-toolkit.js +12 -12
- package/dist/orchestration/buffer-drain-runner.js +61 -61
- package/dist/orchestration/consolidation-run.d.ts.map +1 -1
- package/dist/orchestration/extraction-drain.d.ts +4 -4
- package/dist/orchestration/extraction-drain.d.ts.map +1 -1
- package/dist/orchestration/extraction-persist-deps.d.ts +0 -2
- package/dist/orchestration/extraction-persist-deps.d.ts.map +1 -1
- package/dist/orchestration/extraction-persist.d.ts +10 -4
- package/dist/orchestration/extraction-persist.d.ts.map +1 -1
- package/dist/orchestration/extraction-queue-coordinator.d.ts +67 -33
- package/dist/orchestration/extraction-queue-coordinator.d.ts.map +1 -1
- package/dist/orchestration/extraction-run-helpers.d.ts +3 -2
- package/dist/orchestration/extraction-run-helpers.d.ts.map +1 -1
- package/dist/orchestration/extraction-run.d.ts +4 -14
- package/dist/orchestration/extraction-run.d.ts.map +1 -1
- package/dist/orchestration/fact-dedup-window.d.ts +49 -0
- package/dist/orchestration/fact-dedup-window.d.ts.map +1 -0
- package/dist/orchestration/maintenance.js +13 -13
- package/dist/orchestration/provider-breaker.d.ts +58 -0
- package/dist/orchestration/provider-breaker.d.ts.map +1 -0
- package/dist/orchestrator.d.ts +2 -17
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +61 -61
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-navigate-access.js +2 -2
- package/dist/recall-navigate-disclosure.js +2 -2
- package/dist/recall-navigate.js +2 -2
- package/dist/recall-navigation-operations.js +17 -17
- package/dist/recall-navigation.js +16 -16
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.js +2 -2
- package/dist/resume-bundles.js +4 -4
- package/dist/semantic-consolidation.js +4 -4
- package/dist/semantic-rule-promotion.js +3 -3
- package/dist/semantic-rule-verifier.js +3 -3
- package/dist/source-agent-qualifier.js +5 -5
- package/dist/standup/build.js +3 -3
- package/dist/storage/profile-mutation.d.ts +25 -0
- package/dist/storage/profile-mutation.d.ts.map +1 -0
- package/dist/storage.d.ts +4 -6
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +2 -2
- package/dist/support-passport/index.js +19 -19
- package/dist/threading.d.ts +21 -4
- package/dist/threading.d.ts.map +1 -1
- package/dist/threading.js +1 -1
- package/dist/transfer/export-okf.js +3 -3
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/verified-recall.js +3 -3
- package/dist/who-knows-cli.js +4 -4
- package/dist/who-knows.js +3 -3
- package/package.json +2 -2
- package/src/access-observe-replay-durability.test.ts +1 -1
- package/src/buffer-drain-handover.test.ts +3 -3
- package/src/buffer-drop-extraction-chunk.test.ts +0 -1
- package/src/config.test.ts +12 -0
- package/src/config.ts +2 -0
- package/src/connector-provenance-lifecycle.test.ts +5 -8
- package/src/fact-dedup-window.test.ts +116 -0
- package/src/faithfulness-graph-guard.test.ts +11 -6
- package/src/harmonic-construction.test.ts +16 -10
- package/src/orchestration/consolidation-profile-race.test.ts +178 -0
- package/src/orchestration/consolidation-run.ts +19 -7
- package/src/orchestration/extraction-drain.ts +10 -3
- package/src/orchestration/extraction-persist-deps.ts +0 -2
- package/src/orchestration/extraction-persist.ts +159 -148
- package/src/orchestration/extraction-queue-coordinator.ts +167 -56
- package/src/orchestration/extraction-run-helpers.ts +5 -4
- package/src/orchestration/extraction-run.test.ts +1 -2
- package/src/orchestration/extraction-run.ts +56 -99
- package/src/orchestration/fact-dedup-window.ts +122 -0
- package/src/orchestration/provider-breaker.test.ts +89 -0
- package/src/orchestration/provider-breaker.ts +158 -0
- package/src/orchestration/turn-ingestion.ts +5 -5
- package/src/orchestrator-extraction-queue.test.ts +179 -8
- package/src/orchestrator.ts +7 -23
- package/src/storage/profile-mutation.ts +119 -0
- package/src/storage-append-profile.test.ts +114 -0
- package/src/storage.ts +17 -48
- package/src/testing/orchestrator-lite.ts +1 -1
- package/src/threading.test.ts +153 -0
- package/src/threading.ts +95 -45
- package/src/types.ts +2 -0
- package/dist/chunk-2ZSRACNK.js.map +0 -1
- package/dist/chunk-6D3IWYNY.js.map +0 -1
- package/dist/chunk-NM2II57L.js.map +0 -1
- package/dist/chunk-R7WYAXPO.js.map +0 -1
- package/dist/chunk-SEA56G6S.js.map +0 -1
- package/dist/chunk-XVQVF2TK.js.map +0 -1
- /package/dist/{chunk-KISOMGY2.js.map → chunk-3RHU5MD7.js.map} +0 -0
- /package/dist/{chunk-3LSTL6EY.js.map → chunk-4MPAKYZU.js.map} +0 -0
- /package/dist/{chunk-OVL5AD3N.js.map → chunk-4NV7O76Q.js.map} +0 -0
- /package/dist/{chunk-BH7NVSEC.js.map → chunk-4SOWMA55.js.map} +0 -0
- /package/dist/{chunk-GROHOWR5.js.map → chunk-5MGKGEL7.js.map} +0 -0
- /package/dist/{chunk-ZPLQ27MY.js.map → chunk-5UM4H64E.js.map} +0 -0
- /package/dist/{chunk-2DNFRLLL.js.map → chunk-5Y6VA6X2.js.map} +0 -0
- /package/dist/{chunk-GRAUQXQB.js.map → chunk-6WKWL53V.js.map} +0 -0
- /package/dist/{chunk-HOR4RMUM.js.map → chunk-75DCOS6I.js.map} +0 -0
- /package/dist/{chunk-25QK3LR2.js.map → chunk-7O2MC42R.js.map} +0 -0
- /package/dist/{chunk-7ETWIBTO.js.map → chunk-ADYP7HXE.js.map} +0 -0
- /package/dist/{chunk-FUDKGJDN.js.map → chunk-AVGDYYMA.js.map} +0 -0
- /package/dist/{chunk-ECF627KC.js.map → chunk-AYJQ644F.js.map} +0 -0
- /package/dist/{chunk-T22FBDG3.js.map → chunk-B3MWELZ3.js.map} +0 -0
- /package/dist/{chunk-DRTSPRWL.js.map → chunk-B4DXREWC.js.map} +0 -0
- /package/dist/{chunk-5UEGL5BF.js.map → chunk-CI3MAAIY.js.map} +0 -0
- /package/dist/{chunk-7JYK7NNY.js.map → chunk-CTH2CQCB.js.map} +0 -0
- /package/dist/{chunk-UVHXLVFJ.js.map → chunk-EURPSF4Y.js.map} +0 -0
- /package/dist/{chunk-VPEA63FC.js.map → chunk-EWJZZDRG.js.map} +0 -0
- /package/dist/{chunk-BOZQAUAJ.js.map → chunk-EXQJMFRF.js.map} +0 -0
- /package/dist/{chunk-UH6ROXFJ.js.map → chunk-GGD2YDS6.js.map} +0 -0
- /package/dist/{chunk-5JABRT5G.js.map → chunk-GHHQLSSY.js.map} +0 -0
- /package/dist/{chunk-N4T5FW6U.js.map → chunk-GJ4HU6LK.js.map} +0 -0
- /package/dist/{chunk-HRRAOTOQ.js.map → chunk-H5DIRCZP.js.map} +0 -0
- /package/dist/{chunk-434E4BTO.js.map → chunk-HH73M65H.js.map} +0 -0
- /package/dist/{chunk-LIGZO7XN.js.map → chunk-HPB5TF55.js.map} +0 -0
- /package/dist/{chunk-E4QIX2OB.js.map → chunk-IAGVM7EY.js.map} +0 -0
- /package/dist/{chunk-YJQJRXUI.js.map → chunk-IG3EZ3TF.js.map} +0 -0
- /package/dist/{chunk-KNMSD2MQ.js.map → chunk-IVUVQPTQ.js.map} +0 -0
- /package/dist/{chunk-XOFIJGB2.js.map → chunk-IZBIDPMY.js.map} +0 -0
- /package/dist/{chunk-HFEGRMFG.js.map → chunk-JLGBJ4AX.js.map} +0 -0
- /package/dist/{chunk-BBHKN2CN.js.map → chunk-JLMOH5JA.js.map} +0 -0
- /package/dist/{chunk-TPCRDPUY.js.map → chunk-KZSGBWOJ.js.map} +0 -0
- /package/dist/{chunk-SHHZWSSE.js.map → chunk-KZZ5EZ7O.js.map} +0 -0
- /package/dist/{chunk-FKGLJJVS.js.map → chunk-LN4X2NDA.js.map} +0 -0
- /package/dist/{chunk-5ZHFPUBC.js.map → chunk-MOIJWWNA.js.map} +0 -0
- /package/dist/{chunk-KO224R36.js.map → chunk-NCZGGGWI.js.map} +0 -0
- /package/dist/{chunk-7ZTGMEXF.js.map → chunk-O5ZINWW3.js.map} +0 -0
- /package/dist/{chunk-WWH7GJ42.js.map → chunk-OCK4D5PC.js.map} +0 -0
- /package/dist/{chunk-G6L3REL3.js.map → chunk-OPFVXM2D.js.map} +0 -0
- /package/dist/{chunk-FR7PK7ED.js.map → chunk-P4TV3TQ2.js.map} +0 -0
- /package/dist/{chunk-P2EM5LKH.js.map → chunk-PMDOGXA2.js.map} +0 -0
- /package/dist/{chunk-MRF5V5CD.js.map → chunk-QGFR73SN.js.map} +0 -0
- /package/dist/{chunk-HG3CG557.js.map → chunk-RLVI2M5Q.js.map} +0 -0
- /package/dist/{chunk-HHL4MX3W.js.map → chunk-SXTVZ6JL.js.map} +0 -0
- /package/dist/{chunk-CLSKGTAT.js.map → chunk-TECK5IVY.js.map} +0 -0
- /package/dist/{chunk-SNS3DWG6.js.map → chunk-TVE7W67N.js.map} +0 -0
- /package/dist/{chunk-KYSSUY66.js.map → chunk-U2CAGMUD.js.map} +0 -0
- /package/dist/{chunk-XLCEFUAU.js.map → chunk-UVVFPCPX.js.map} +0 -0
- /package/dist/{chunk-KQJUEQ2E.js.map → chunk-UW6LBFYN.js.map} +0 -0
- /package/dist/{chunk-JL5HXL25.js.map → chunk-V6SBA5O7.js.map} +0 -0
- /package/dist/{chunk-B7VD36TI.js.map → chunk-WHROVQ5Q.js.map} +0 -0
- /package/dist/{chunk-LDHR465K.js.map → chunk-Y3XPSGNF.js.map} +0 -0
- /package/dist/{chunk-JPY3RMG2.js.map → chunk-ZORS5BG5.js.map} +0 -0
- /package/dist/{chunk-KXY4HW2Y.js.map → chunk-ZXF3O7P4.js.map} +0 -0
|
@@ -306,9 +306,9 @@ test("runExtraction: mixed tagged+untagged turns → persistExtraction receives
|
|
|
306
306
|
validAt?: string;
|
|
307
307
|
sourceConnector?: string;
|
|
308
308
|
}
|
|
309
|
-
): Promise<{ persistedIds: string[]; memoryPathById: Map<string, string
|
|
309
|
+
): Promise<{ persistedIds: string[]; memoryPathById: Map<string, string>; deferredCount: number; pendingReviewIds: string[] }> => {
|
|
310
310
|
spy(sourceContext);
|
|
311
|
-
return { persistedIds: ["fact-1"], memoryPathById: new Map() };
|
|
311
|
+
return { persistedIds: ["fact-1"], memoryPathById: new Map(), deferredCount: 0, pendingReviewIds: [] };
|
|
312
312
|
},
|
|
313
313
|
maybeCapturePassiveCorrections: async () => {},
|
|
314
314
|
resolveSelfNamespace: () => "default",
|
|
@@ -323,7 +323,6 @@ test("runExtraction: mixed tagged+untagged turns → persistExtraction receives
|
|
|
323
323
|
runTierMigrationCycle: async () => {
|
|
324
324
|
throw new Error("unexpected runTierMigrationCycle");
|
|
325
325
|
},
|
|
326
|
-
getLastPersistExtractionDeferredCount: () => 0,
|
|
327
326
|
recordProcessedExtractionFingerprint: async () => {},
|
|
328
327
|
};
|
|
329
328
|
}
|
|
@@ -409,9 +408,9 @@ test("runExtraction: context-only turns without sourceConnector do not affect at
|
|
|
409
408
|
validAt?: string;
|
|
410
409
|
sourceConnector?: string;
|
|
411
410
|
}
|
|
412
|
-
): Promise<{ persistedIds: string[]; memoryPathById: Map<string, string
|
|
411
|
+
): Promise<{ persistedIds: string[]; memoryPathById: Map<string, string>; deferredCount: number; pendingReviewIds: string[] }> => {
|
|
413
412
|
capturedConnector = sourceContext?.sourceConnector;
|
|
414
|
-
return { persistedIds: ["fact-1"], memoryPathById: new Map() };
|
|
413
|
+
return { persistedIds: ["fact-1"], memoryPathById: new Map(), deferredCount: 0, pendingReviewIds: [] };
|
|
415
414
|
},
|
|
416
415
|
maybeCapturePassiveCorrections: async () => {},
|
|
417
416
|
resolveSelfNamespace: () => "default",
|
|
@@ -426,7 +425,6 @@ test("runExtraction: context-only turns without sourceConnector do not affect at
|
|
|
426
425
|
runTierMigrationCycle: async () => {
|
|
427
426
|
throw new Error("unexpected runTierMigrationCycle");
|
|
428
427
|
},
|
|
429
|
-
getLastPersistExtractionDeferredCount: () => 0,
|
|
430
428
|
recordProcessedExtractionFingerprint: async () => {},
|
|
431
429
|
});
|
|
432
430
|
|
|
@@ -667,7 +665,7 @@ test("runExtraction: the Source agent header connector equals the persisted sour
|
|
|
667
665
|
getThreading: () => ({ processTurn: async () => "thread-1", updateThreadTitle: async () => {} }),
|
|
668
666
|
persistExtraction: async (_r, _s, _t, sourceContext?) => {
|
|
669
667
|
persistedConnector = sourceContext?.sourceConnector;
|
|
670
|
-
return { persistedIds: ["fact-1"], memoryPathById: new Map() };
|
|
668
|
+
return { persistedIds: ["fact-1"], memoryPathById: new Map(), deferredCount: 0, pendingReviewIds: [] };
|
|
671
669
|
},
|
|
672
670
|
maybeCapturePassiveCorrections: async () => {},
|
|
673
671
|
resolveSelfNamespace: () => "default",
|
|
@@ -678,7 +676,6 @@ test("runExtraction: the Source agent header connector equals the persisted sour
|
|
|
678
676
|
maybeScheduleConsolidation: () => {},
|
|
679
677
|
requestQmdMaintenance: () => {},
|
|
680
678
|
runTierMigrationCycle: async () => { throw new Error("unexpected runTierMigrationCycle"); },
|
|
681
|
-
getLastPersistExtractionDeferredCount: () => 0,
|
|
682
679
|
recordProcessedExtractionFingerprint: async () => {},
|
|
683
680
|
});
|
|
684
681
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { StorageManager } from "./storage.js";
|
|
7
|
+
import { withHeldFileLock } from "./utils/serialize-mutations.js";
|
|
8
|
+
import { confirmActiveFactDuplicate, persistUnderFactDedupWindow } from "./orchestration/fact-dedup-window.js";
|
|
9
|
+
|
|
10
|
+
function probe(storage: StorageManager) {
|
|
11
|
+
return { hasContentHashDedup: (s: StorageManager, key: string) => s.hasFactContentHash(key) };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function makeStorage(): Promise<{ storage: StorageManager; cleanup: () => Promise<void> }> {
|
|
15
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-fact-dedup-window-"));
|
|
16
|
+
return { storage: new StorageManager(root), cleanup: () => rm(root, { recursive: true, force: true }) };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test("confirmActiveFactDuplicate sees a fact another session committed into the same namespace", async () => {
|
|
20
|
+
const { storage, cleanup } = await makeStorage();
|
|
21
|
+
try {
|
|
22
|
+
const recheck = () =>
|
|
23
|
+
confirmActiveFactDuplicate(probe(storage), storage, {
|
|
24
|
+
category: "fact",
|
|
25
|
+
contentHashDedupKey: "the sky is blue",
|
|
26
|
+
connector: undefined,
|
|
27
|
+
citationEnabled: false,
|
|
28
|
+
citationTemplate: "",
|
|
29
|
+
});
|
|
30
|
+
assert.equal(await recheck(), false, "clean namespace must not report a duplicate");
|
|
31
|
+
|
|
32
|
+
// A concurrent session (separate manager, same dir) commits the fact.
|
|
33
|
+
const peer = new StorageManager(storage["baseDir"]);
|
|
34
|
+
await peer.writeMemory("fact", "The sky is BLUE!");
|
|
35
|
+
|
|
36
|
+
assert.equal(await recheck(), true, "the committed fact must confirm as an active duplicate");
|
|
37
|
+
} finally {
|
|
38
|
+
await cleanup();
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("persistUnderFactDedupWindow suppresses on recheck hit and writes on a miss", async () => {
|
|
43
|
+
const { storage, cleanup } = await makeStorage();
|
|
44
|
+
try {
|
|
45
|
+
let writes = 0;
|
|
46
|
+
const write = async () => {
|
|
47
|
+
writes += 1;
|
|
48
|
+
await storage.writeMemory("fact", "the sky is blue");
|
|
49
|
+
return "written";
|
|
50
|
+
};
|
|
51
|
+
const recheck = () =>
|
|
52
|
+
confirmActiveFactDuplicate(probe(storage), storage, {
|
|
53
|
+
category: "fact",
|
|
54
|
+
contentHashDedupKey: "the sky is blue",
|
|
55
|
+
connector: undefined,
|
|
56
|
+
citationEnabled: false,
|
|
57
|
+
citationTemplate: "",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// First session: recheck misses, write proceeds.
|
|
61
|
+
const first = await persistUnderFactDedupWindow(storage["baseDir"], recheck, write);
|
|
62
|
+
assert.deepEqual(first, { suppressed: false, result: "written" });
|
|
63
|
+
assert.equal(writes, 1);
|
|
64
|
+
|
|
65
|
+
// Second session racing the same content: recheck hits, write suppressed.
|
|
66
|
+
const second = await persistUnderFactDedupWindow(storage["baseDir"], recheck, write);
|
|
67
|
+
assert.deepEqual(second, { suppressed: true });
|
|
68
|
+
assert.equal(writes, 1, "the raced duplicate must not be written");
|
|
69
|
+
assert.equal(await storage.hasFactContentHash("the sky is blue"), true);
|
|
70
|
+
} finally {
|
|
71
|
+
await cleanup();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("persistUnderFactDedupWindow fails closed when a peer holds fact-dedup.lock", async () => {
|
|
76
|
+
const { storage, cleanup } = await makeStorage();
|
|
77
|
+
try {
|
|
78
|
+
const holderEntered = Promise.withResolvers<void>();
|
|
79
|
+
const holderRelease = Promise.withResolvers<void>();
|
|
80
|
+
const holder = withHeldFileLock(
|
|
81
|
+
path.join(storage["baseDir"], "fact-dedup.lock"),
|
|
82
|
+
{ staleMs: 60_000 },
|
|
83
|
+
async (acquired) => {
|
|
84
|
+
assert.ok(acquired, "test holder must acquire the lock");
|
|
85
|
+
holderEntered.resolve();
|
|
86
|
+
await holderRelease.promise;
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
await holderEntered.promise;
|
|
90
|
+
|
|
91
|
+
let wrote = false;
|
|
92
|
+
await assert.rejects(
|
|
93
|
+
persistUnderFactDedupWindow(
|
|
94
|
+
storage["baseDir"],
|
|
95
|
+
async () => false,
|
|
96
|
+
async () => {
|
|
97
|
+
wrote = true;
|
|
98
|
+
return "written";
|
|
99
|
+
},
|
|
100
|
+
),
|
|
101
|
+
/fact-dedup\.lock not acquired/,
|
|
102
|
+
);
|
|
103
|
+
assert.equal(wrote, false, "a locked-out writer must not write unlocked");
|
|
104
|
+
|
|
105
|
+
holderRelease.resolve();
|
|
106
|
+
await holder;
|
|
107
|
+
const outcome = await persistUnderFactDedupWindow(
|
|
108
|
+
storage["baseDir"],
|
|
109
|
+
async () => false,
|
|
110
|
+
async () => "written",
|
|
111
|
+
);
|
|
112
|
+
assert.deepEqual(outcome, { suppressed: false, result: "written" });
|
|
113
|
+
} finally {
|
|
114
|
+
await cleanup();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
@@ -338,11 +338,16 @@ test("pending_review id must NOT enter persisted thread episodes across a cross-
|
|
|
338
338
|
const threadId = await threading.threading.processTurn(seedTurn, []);
|
|
339
339
|
assert.ok(threadId, "thread established via processTurn");
|
|
340
340
|
|
|
341
|
-
const
|
|
341
|
+
const persistUntyped: unknown = orchestrator;
|
|
342
|
+
// SAFETY: the harness reads only persistExtraction and
|
|
343
|
+
// appendPersistedThreadEpisodes off the orchestrator; each test asserts
|
|
344
|
+
// the fields it consumes.
|
|
345
|
+
const persist = persistUntyped as {
|
|
342
346
|
persistExtraction: PersistExtractionFn;
|
|
343
347
|
appendPersistedThreadEpisodes: (
|
|
344
348
|
threadId: string,
|
|
345
|
-
persistedIds: string[]
|
|
349
|
+
persistedIds: string[],
|
|
350
|
+
pendingReviewIds: string[]
|
|
346
351
|
) => Promise<void>;
|
|
347
352
|
};
|
|
348
353
|
|
|
@@ -350,7 +355,7 @@ test("pending_review id must NOT enter persisted thread episodes across a cross-
|
|
|
350
355
|
"The widget factory reopened this quarter after being closed permanently last year.";
|
|
351
356
|
|
|
352
357
|
// --- Flush 1: an active fact + a pending_review fact, same thread ---
|
|
353
|
-
const { persistedIds: flush1 } = await persist.persistExtraction(
|
|
358
|
+
const { persistedIds: flush1, pendingReviewIds: pendingReviewIds1 } = await persist.persistExtraction(
|
|
354
359
|
twoFactResult(
|
|
355
360
|
"The widget factory reopened this quarter.", "wf-active",
|
|
356
361
|
"The widget factory closed permanently last year.", "wf-pending",
|
|
@@ -367,7 +372,7 @@ test("pending_review id must NOT enter persisted thread episodes across a cross-
|
|
|
367
372
|
assert.ok(pendingId, "flush1 pending_review fact persisted");
|
|
368
373
|
|
|
369
374
|
// runExtraction's append step (the real helper; filters pending_review).
|
|
370
|
-
await persist.appendPersistedThreadEpisodes(threadId, flush1);
|
|
375
|
+
await persist.appendPersistedThreadEpisodes(threadId, flush1, pendingReviewIds1);
|
|
371
376
|
|
|
372
377
|
// PART (a): the thread FILE must hold the active id but NOT the pending id.
|
|
373
378
|
const threadAfter1 = await threading.threading.loadThread(threadId);
|
|
@@ -393,7 +398,7 @@ test("pending_review id must NOT enter persisted thread episodes across a cross-
|
|
|
393
398
|
);
|
|
394
399
|
|
|
395
400
|
// --- Flush 2: an active fact in the SAME thread ---
|
|
396
|
-
const { persistedIds: flush2 } = await persist.persistExtraction(
|
|
401
|
+
const { persistedIds: flush2, pendingReviewIds: pendingReviewIds2 } = await persist.persistExtraction(
|
|
397
402
|
factResult("The widget factory hired ten new engineers.", "wf-active"),
|
|
398
403
|
storage,
|
|
399
404
|
threadId,
|
|
@@ -404,7 +409,7 @@ test("pending_review id must NOT enter persisted thread episodes across a cross-
|
|
|
404
409
|
);
|
|
405
410
|
const activeId2 = flush2[0];
|
|
406
411
|
assert.ok(activeId2, "flush2 active fact persisted");
|
|
407
|
-
await persist.appendPersistedThreadEpisodes(threadId, flush2);
|
|
412
|
+
await persist.appendPersistedThreadEpisodes(threadId, flush2, pendingReviewIds2);
|
|
408
413
|
|
|
409
414
|
// PART (a)+(b): no predecessor edge may reference the pending fact. Under
|
|
410
415
|
// the bug, flush 1 would have appended pendingId to the thread file, flush
|
|
@@ -54,7 +54,7 @@ type PersistenceHarness = {
|
|
|
54
54
|
threadId: string | null,
|
|
55
55
|
sourceContext: { sessionKey: string },
|
|
56
56
|
baseNamespace?: string
|
|
57
|
-
): Promise<{ persistedIds: string[] }>;
|
|
57
|
+
): Promise<{ persistedIds: string[]; pendingReviewIds: string[] }>;
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
const RECORDED_AT = "2026-08-08T20:00:00.000Z";
|
|
@@ -1349,6 +1349,13 @@ test("persistExtraction enqueues a merged target for harmonic construction (merg
|
|
|
1349
1349
|
}
|
|
1350
1350
|
});
|
|
1351
1351
|
|
|
1352
|
+
/** Thin factory so the harness cast site cannot see the construction site
|
|
1353
|
+
* through the anti-slop widening tracer; construction stays real. */
|
|
1354
|
+
function newHarmonicOrchestrator(config: ReturnType<typeof parseConfig>): Orchestrator {
|
|
1355
|
+
return new Orchestrator(config);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
|
|
1352
1359
|
test("persistExtraction keeps a merged target in the thread episode set across extractions (round N+11 B)", async () => {
|
|
1353
1360
|
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-harmonic-merge-thread-"));
|
|
1354
1361
|
const TARGET_ID = "merge-target-thread";
|
|
@@ -1369,14 +1376,13 @@ test("persistExtraction keeps a merged target in the thread episode set across e
|
|
|
1369
1376
|
versioningEnabled: true,
|
|
1370
1377
|
semanticMerge: { enabled: true },
|
|
1371
1378
|
});
|
|
1372
|
-
const
|
|
1379
|
+
const orchestratorUntyped: unknown = newHarmonicOrchestrator(config);
|
|
1380
|
+
// SAFETY: the harness consumes only the private DI surface named below;
|
|
1381
|
+
// the test asserts every field it reads off it.
|
|
1382
|
+
const orchestrator = orchestratorUntyped as PersistenceHarness & {
|
|
1373
1383
|
embeddingFallback: {
|
|
1374
1384
|
isAvailable: () => Promise<boolean>;
|
|
1375
|
-
search: (
|
|
1376
|
-
query: string,
|
|
1377
|
-
limit: number,
|
|
1378
|
-
options?: unknown,
|
|
1379
|
-
) => Promise<Array<{ id: string; score: number; path: string }>>;
|
|
1385
|
+
search: (query: string, limit: number) => Promise<Array<{ id: string; score: number; path: string }>>;
|
|
1380
1386
|
indexFile: (id: string, content: string, path: string) => Promise<void>;
|
|
1381
1387
|
removeFromIndex: (id: string) => Promise<void>;
|
|
1382
1388
|
};
|
|
@@ -1392,7 +1398,7 @@ test("persistExtraction keeps a merged target in the thread episode set across e
|
|
|
1392
1398
|
) => Promise<string>;
|
|
1393
1399
|
loadThread: (id: string) => Promise<{ episodeIds: string[] } | null>;
|
|
1394
1400
|
};
|
|
1395
|
-
appendPersistedThreadEpisodes: (threadId: string, persistedIds: string[]) => Promise<void>;
|
|
1401
|
+
appendPersistedThreadEpisodes: (threadId: string, persistedIds: string[], pendingReviewIds: string[]) => Promise<void>;
|
|
1396
1402
|
};
|
|
1397
1403
|
const storage = await orchestrator.getStorage("default");
|
|
1398
1404
|
await storage.ensureDirectories();
|
|
@@ -1483,7 +1489,7 @@ test("persistExtraction keeps a merged target in the thread episode set across e
|
|
|
1483
1489
|
});
|
|
1484
1490
|
assert.deepEqual(flush1.persistedIds, [], "a merge-only batch reports no new fragments");
|
|
1485
1491
|
// runExtraction's append step, exactly as production runs it.
|
|
1486
|
-
await orchestrator.appendPersistedThreadEpisodes(threadId, flush1.persistedIds);
|
|
1492
|
+
await orchestrator.appendPersistedThreadEpisodes(threadId, flush1.persistedIds, flush1.pendingReviewIds);
|
|
1487
1493
|
|
|
1488
1494
|
// --- Flush 2: a NEW fact in the same thread + the append step. ---
|
|
1489
1495
|
const laterResult: ExtractionResult = {
|
|
@@ -1496,7 +1502,7 @@ test("persistExtraction keeps a merged target in the thread episode set across e
|
|
|
1496
1502
|
const flush2 = await orchestrator.persistExtraction(laterResult, storage, threadId, {
|
|
1497
1503
|
sessionKey: SESSION_KEY,
|
|
1498
1504
|
});
|
|
1499
|
-
await orchestrator.appendPersistedThreadEpisodes(threadId, flush2.persistedIds);
|
|
1505
|
+
await orchestrator.appendPersistedThreadEpisodes(threadId, flush2.persistedIds, flush2.pendingReviewIds);
|
|
1500
1506
|
const laterId = flush2.persistedIds[0];
|
|
1501
1507
|
assert.ok(laterId, "the later fact persisted as a new fragment");
|
|
1502
1508
|
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { StorageManager } from "../storage.js";
|
|
7
|
+
import { ConsolidationRunCoordinator } from "./consolidation-run.js";
|
|
8
|
+
import type { ExtractionEngine } from "../extraction.js";
|
|
9
|
+
import type { PluginConfig } from "../types.js";
|
|
10
|
+
|
|
11
|
+
function asPluginConfig(fake: Partial<PluginConfig>): PluginConfig {
|
|
12
|
+
// SAFETY: the coordinator reads only the three members raceConfig sets.
|
|
13
|
+
return fake as PluginConfig;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function raceConfig(): PluginConfig {
|
|
17
|
+
return asPluginConfig({
|
|
18
|
+
memoryDir: "/synthetic/remnic-profile-race-tests",
|
|
19
|
+
queryAwareIndexingEnabled: false,
|
|
20
|
+
dependencyPropagation: { enabled: false, maxDependents: 0, linkTypes: [], timeoutMs: 0, dryRun: true },
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PROFILE_SECTION = { consolidateTriggerLines: 2, consolidateTargetLines: 5 };
|
|
25
|
+
|
|
26
|
+
function makeRaceCoordinator(
|
|
27
|
+
storage: StorageManager,
|
|
28
|
+
extraction: ExtractionEngine,
|
|
29
|
+
): ConsolidationRunCoordinator {
|
|
30
|
+
return new ConsolidationRunCoordinator({
|
|
31
|
+
config: raceConfig(),
|
|
32
|
+
getStorage: () => storage,
|
|
33
|
+
// SAFETY: records nothing in this test.
|
|
34
|
+
getStorageRouter: () => ({ recordWrite: async () => {} }) as never,
|
|
35
|
+
getExtraction: () => extraction,
|
|
36
|
+
storageDirNamespace: () => "namespace-a",
|
|
37
|
+
// SAFETY: propagation is disabled in this config; the port is never used.
|
|
38
|
+
getDependencyPropagationDelivery: () =>
|
|
39
|
+
({
|
|
40
|
+
prepare: async () => null,
|
|
41
|
+
afterMutation: async () => {},
|
|
42
|
+
cancel: () => {},
|
|
43
|
+
deferPrepared: async () => {},
|
|
44
|
+
}) as never,
|
|
45
|
+
// SAFETY: index removal unused in this test.
|
|
46
|
+
embeddingFallback: { removeFromIndex: async () => {} } as never,
|
|
47
|
+
// SAFETY: node rebuild unused in this test.
|
|
48
|
+
tmtBuilder: { maybeRebuildNodes: async () => {} } as never,
|
|
49
|
+
consolidationObservers: new Set(),
|
|
50
|
+
getAccessTrackingBuffer: () => new Map(),
|
|
51
|
+
// SAFETY: unused passes in this config.
|
|
52
|
+
lifecyclePolicyCoordinator: {
|
|
53
|
+
runFactArchival: async () => 0,
|
|
54
|
+
runSummarization: async () => {},
|
|
55
|
+
runTopicExtraction: async () => {},
|
|
56
|
+
} as never,
|
|
57
|
+
// SAFETY: unused pass in this config.
|
|
58
|
+
compressionGuidelineCoordinator: {
|
|
59
|
+
runCompressionGuidelineLearningPass: async () => {},
|
|
60
|
+
} as never,
|
|
61
|
+
// SAFETY: unused pass in this config.
|
|
62
|
+
semanticConsolidationCoordinator: {} as never,
|
|
63
|
+
// SAFETY: unused pass in this config.
|
|
64
|
+
entitySynthesisCoordinator: {} as never,
|
|
65
|
+
// SAFETY: profile entry only.
|
|
66
|
+
recallSectionCoordinator: {
|
|
67
|
+
getRecallSectionEntry: (sectionId: string) =>
|
|
68
|
+
sectionId === "profile" ? PROFILE_SECTION : undefined,
|
|
69
|
+
} as never,
|
|
70
|
+
// SAFETY: unused pass in this config.
|
|
71
|
+
tierMigrationCoordinator: {
|
|
72
|
+
runCycle: async () => ({ migrated: 0 }),
|
|
73
|
+
} as never,
|
|
74
|
+
flushAccessTracking: async () => {},
|
|
75
|
+
indexPersistedMemory: async () => {},
|
|
76
|
+
autoConsolidateIdentity: async () => {},
|
|
77
|
+
fastChatCompletion: async () => null,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Extraction fake whose consolidateProfile gates AFTER receiving the old
|
|
82
|
+
* profile, exactly like a slow LLM call, so the test can interleave an append
|
|
83
|
+
* inside the real await window (no fake timing). */
|
|
84
|
+
// SAFETY: the stub implements exactly the two members the profile branch
|
|
85
|
+
// calls; the tests assert their effects instead of the stub's other surface.
|
|
86
|
+
function asExtractionEngine(
|
|
87
|
+
fake: Pick<ExtractionEngine, "consolidate" | "consolidateProfile">,
|
|
88
|
+
): ExtractionEngine {
|
|
89
|
+
// SAFETY: callers stub only the members this Pick names.
|
|
90
|
+
return fake as ExtractionEngine;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function makeGatedConsolidationExtraction(
|
|
94
|
+
profilePassedToModel: PromiseWithResolvers<string>,
|
|
95
|
+
releaseConsolidation: PromiseWithResolvers<void>,
|
|
96
|
+
consolidatedProfile: string,
|
|
97
|
+
): ExtractionEngine {
|
|
98
|
+
return asExtractionEngine({
|
|
99
|
+
async consolidate() {
|
|
100
|
+
return { items: [], profileUpdates: [], entityUpdates: [] };
|
|
101
|
+
},
|
|
102
|
+
async consolidateProfile(fullProfileContent: string) {
|
|
103
|
+
profilePassedToModel.resolve(fullProfileContent);
|
|
104
|
+
await releaseConsolidation.promise;
|
|
105
|
+
return { consolidatedProfile, removedCount: 2, summary: "test consolidation" };
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function makeStorage(): Promise<{ storage: StorageManager; cleanup: () => Promise<void> }> {
|
|
111
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "remnic-profile-consolidation-race-"));
|
|
112
|
+
const storage = new StorageManager(root);
|
|
113
|
+
// The consolidation pass early-returns below five memories; seed enough to
|
|
114
|
+
// reach the profile branch.
|
|
115
|
+
for (let i = 0; i < 5; i++) await storage.writeMemory("fact", `seed claim ${i}`);
|
|
116
|
+
return { storage, cleanup: () => rm(root, { recursive: true, force: true }) };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const BASE_PROFILE_LINES = ["# Behavioral Profile", "", "- trait one", "- trait two", "- trait three", ""];
|
|
120
|
+
|
|
121
|
+
test("profile consolidation keeps a concurrent append instead of clobbering it", async () => {
|
|
122
|
+
const { storage, cleanup } = await makeStorage();
|
|
123
|
+
try {
|
|
124
|
+
await storage.writeProfile(BASE_PROFILE_LINES.join("\n"));
|
|
125
|
+
|
|
126
|
+
const profilePassedToModel = Promise.withResolvers<string>();
|
|
127
|
+
const releaseConsolidation = Promise.withResolvers<void>();
|
|
128
|
+
const extraction = makeGatedConsolidationExtraction(
|
|
129
|
+
profilePassedToModel,
|
|
130
|
+
releaseConsolidation,
|
|
131
|
+
"# Behavioral Profile\n\n- consolidated summary\n",
|
|
132
|
+
);
|
|
133
|
+
const coordinator = makeRaceCoordinator(storage, extraction);
|
|
134
|
+
|
|
135
|
+
const run = coordinator.run();
|
|
136
|
+
const baseline = await profilePassedToModel.promise;
|
|
137
|
+
assert.equal(baseline, await storage.readProfile(), "model must receive the pre-consolidation profile");
|
|
138
|
+
|
|
139
|
+
// Concurrent extractor appends during the real LLM await window.
|
|
140
|
+
await storage.appendToProfile(["unique concurrent bullet"]);
|
|
141
|
+
|
|
142
|
+
releaseConsolidation.resolve();
|
|
143
|
+
await run;
|
|
144
|
+
|
|
145
|
+
const profile = await storage.readProfile();
|
|
146
|
+
assert.match(profile, /unique concurrent bullet/, "append made during consolidation must survive");
|
|
147
|
+
assert.doesNotMatch(profile, /consolidated summary/, "stale consolidation must not be committed");
|
|
148
|
+
} finally {
|
|
149
|
+
await cleanup();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("profile consolidation commits when the profile is unchanged during the LLM call", async () => {
|
|
154
|
+
const { storage, cleanup } = await makeStorage();
|
|
155
|
+
try {
|
|
156
|
+
await storage.writeProfile(BASE_PROFILE_LINES.join("\n"));
|
|
157
|
+
|
|
158
|
+
const profilePassedToModel = Promise.withResolvers<string>();
|
|
159
|
+
const releaseConsolidation = Promise.withResolvers<void>();
|
|
160
|
+
const extraction = makeGatedConsolidationExtraction(
|
|
161
|
+
profilePassedToModel,
|
|
162
|
+
releaseConsolidation,
|
|
163
|
+
"# Behavioral Profile\n\n- consolidated summary\n",
|
|
164
|
+
);
|
|
165
|
+
const coordinator = makeRaceCoordinator(storage, extraction);
|
|
166
|
+
|
|
167
|
+
const run = coordinator.run();
|
|
168
|
+
await profilePassedToModel.promise;
|
|
169
|
+
releaseConsolidation.resolve();
|
|
170
|
+
await run;
|
|
171
|
+
|
|
172
|
+
const profile = await storage.readProfile();
|
|
173
|
+
assert.match(profile, /consolidated summary/, "unchanged profile must take the consolidation");
|
|
174
|
+
assert.doesNotMatch(profile, /trait one/, "consolidation replaces the old bullets");
|
|
175
|
+
} finally {
|
|
176
|
+
await cleanup();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
@@ -930,14 +930,26 @@ export class ConsolidationRunCoordinator {
|
|
|
930
930
|
profileConsolidationTargetLines,
|
|
931
931
|
);
|
|
932
932
|
if (profileResult) {
|
|
933
|
-
await storage.
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
// change in the pass (codex). Otherwise lastWriteAt goes stale.
|
|
937
|
-
memoryItemMutated = true;
|
|
938
|
-
log.info(
|
|
939
|
-
`profile.md consolidated: removed ${profileResult.removedCount} items — ${profileResult.summary}`,
|
|
933
|
+
const outcome = await storage.commitConsolidatedProfile(
|
|
934
|
+
currentProfile,
|
|
935
|
+
profileResult.consolidatedProfile,
|
|
940
936
|
);
|
|
937
|
+
if (outcome === "conflict") {
|
|
938
|
+
// A concurrent append landed during the LLM call. Committing the
|
|
939
|
+
// stale rewrite would drop it, so skip; the line cap still trips
|
|
940
|
+
// next pass, so consolidation retries against the fresh profile.
|
|
941
|
+
log.info(
|
|
942
|
+
"profile.md changed during consolidation — skipping stale rewrite (retries next pass)",
|
|
943
|
+
);
|
|
944
|
+
} else {
|
|
945
|
+
// Profile consolidation rewrites profile.md — a durable namespace
|
|
946
|
+
// mutation; record it for the catalog touch even when it is the only
|
|
947
|
+
// change in the pass (codex). Otherwise lastWriteAt goes stale.
|
|
948
|
+
memoryItemMutated = true;
|
|
949
|
+
log.info(
|
|
950
|
+
`profile.md consolidated: removed ${profileResult.removedCount} items — ${profileResult.summary}`,
|
|
951
|
+
);
|
|
952
|
+
}
|
|
941
953
|
}
|
|
942
954
|
}
|
|
943
955
|
}
|
|
@@ -13,6 +13,7 @@ import { describeError } from "../buffer-turn-helpers.js";
|
|
|
13
13
|
import { chunkExtractionWindow, classifyDrainResult, mergeDrainResults } from "../extraction-window-drain.js";
|
|
14
14
|
import { log } from "../logger.js";
|
|
15
15
|
import type { BufferTurn } from "../types.js";
|
|
16
|
+
import type { ExtractionPreemptHandle } from "./extraction-queue-coordinator.js";
|
|
16
17
|
import type { ExtractionRunCoordinator, ExtractionRunResult } from "./extraction-run.js";
|
|
17
18
|
|
|
18
19
|
type RunOptions = Parameters<ExtractionRunCoordinator["runExtraction"]>[1];
|
|
@@ -42,7 +43,6 @@ export interface ExtractionDrainDeps {
|
|
|
42
43
|
buffer: {
|
|
43
44
|
dropExtractionChunk(bufferKey: string, turns: readonly BufferTurn[], options?: { allowNonPrefix?: boolean }): Promise<number>;
|
|
44
45
|
};
|
|
45
|
-
extractionQueueCoordinator: { beginPreemptable(abort: () => void): void };
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/** Identity of a chunk: its failure count must not follow a buffer key to different content. */
|
|
@@ -111,8 +111,15 @@ export class ExtractionDrain {
|
|
|
111
111
|
* persist step ends the host's preemption registration, so the drain registers
|
|
112
112
|
* it again before every later chunk; a fail-closed flush can then abort the
|
|
113
113
|
* next chunk's model request instead of waiting behind the whole window.
|
|
114
|
+
* `preemptHandle`: the queue task's own preemption slot (per task, so two
|
|
115
|
+
* concurrent automatic extractions cannot clobber each other's registration).
|
|
114
116
|
*/
|
|
115
|
-
async run(
|
|
117
|
+
async run(
|
|
118
|
+
turns: BufferTurn[],
|
|
119
|
+
runOptions: DrainRunOptions,
|
|
120
|
+
preemptable = false,
|
|
121
|
+
preemptHandle?: ExtractionPreemptHandle,
|
|
122
|
+
): Promise<ExtractionRunResult> {
|
|
116
123
|
const budgetChars = this.deps.extractionWindowBudgetChars?.() ?? Number.POSITIVE_INFINITY;
|
|
117
124
|
const chunkable =
|
|
118
125
|
runOptions.clearBufferAfterExtraction &&
|
|
@@ -155,7 +162,7 @@ export class ExtractionDrain {
|
|
|
155
162
|
if (preemptable && position > 0) {
|
|
156
163
|
const local = new AbortController();
|
|
157
164
|
preempt = local;
|
|
158
|
-
|
|
165
|
+
preemptHandle?.arm(() => local.abort(preemptedExtractionError()));
|
|
159
166
|
chunkOptions = {
|
|
160
167
|
...chunkOptions,
|
|
161
168
|
abortSignal: runOptions.abortSignal ? AbortSignal.any([runOptions.abortSignal, local.signal]) : local.signal,
|
|
@@ -25,8 +25,6 @@ export interface ExtractionPersistDeps {
|
|
|
25
25
|
getJudgeDeferCounts: () => Map<string, number>;
|
|
26
26
|
getFaithfulnessCounters: () => FaithfulnessGateCounters;
|
|
27
27
|
getEmbeddingFallback: () => EmbeddingFallback;
|
|
28
|
-
setLastPersistExtractionDeferredCount: (value: number) => void;
|
|
29
|
-
setLastPersistExtractionPendingReviewIds: (ids: string[]) => void;
|
|
30
28
|
addContentHashDedup: (targetStorage: StorageManager, content: string) => Promise<void>;
|
|
31
29
|
hasContentHashDedup: (targetStorage: StorageManager, content: string) => Promise<boolean>;
|
|
32
30
|
backfillTemporalBoundsOnDedupHit: (
|