@remnic/core 10.0.66 → 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 +60 -60
- 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-3H25IRJJ.js → chunk-4MPAKYZU.js} +2 -2
- package/dist/{chunk-IPCSZLLF.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-RAU25Q6J.js → chunk-6WKWL53V.js} +13 -13
- package/dist/{chunk-KFPJIKUW.js → chunk-75DCOS6I.js} +23 -23
- 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-T763ADEK.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-JYXFIILI.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-VC65GNUS.js → chunk-GGD2YDS6.js} +2 -2
- package/dist/{chunk-5JABRT5G.js → chunk-GHHQLSSY.js} +3 -3
- package/dist/{chunk-FMYAU5MP.js → chunk-GJ4HU6LK.js} +2 -2
- package/dist/{chunk-3NJJQAL3.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-VC7GKR2V.js → chunk-IG3EZ3TF.js} +2 -2
- package/dist/{chunk-QAN5ZDRS.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-XMBDI725.js → chunk-LFYEQK45.js} +674 -518
- package/dist/chunk-LFYEQK45.js.map +1 -0
- package/dist/{chunk-A5FOXI3B.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-ZGHPELZ6.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-F32JRMX2.js → chunk-QGFR73SN.js} +2 -2
- package/dist/{chunk-QNZPYH36.js → chunk-R2BIVU2Z.js} +352 -290
- package/dist/chunk-R2BIVU2Z.js.map +1 -0
- package/dist/{chunk-HG3CG557.js → chunk-RLVI2M5Q.js} +2 -2
- package/dist/{chunk-SH6ZYUTX.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-PL75KQGF.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-VSZNPAGL.js → chunk-V6SBA5O7.js} +3 -3
- package/dist/{chunk-B7VD36TI.js → chunk-WHROVQ5Q.js} +3 -3
- package/dist/{chunk-LDHR465K.js → chunk-Y3XPSGNF.js} +2 -2
- package/dist/{chunk-E6VQJ75T.js → chunk-ZORS5BG5.js} +4 -4
- package/dist/{chunk-KXY4HW2Y.js → chunk-ZXF3O7P4.js} +2 -2
- package/dist/cli.js +46 -46
- 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 +60 -60
- 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 +60 -60
- 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.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.d.ts +1 -8
- 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.js +60 -60
- 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/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/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/fact-dedup-window.test.ts +116 -0
- 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.ts +141 -138
- package/src/orchestration/extraction-queue-coordinator.ts +167 -56
- package/src/orchestration/extraction-run.ts +50 -93
- 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 +1 -1
- 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/types.ts +2 -0
- package/dist/chunk-NM2II57L.js.map +0 -1
- package/dist/chunk-QNZPYH36.js.map +0 -1
- package/dist/chunk-SEA56G6S.js.map +0 -1
- package/dist/chunk-XMBDI725.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-3H25IRJJ.js.map → chunk-4MPAKYZU.js.map} +0 -0
- /package/dist/{chunk-IPCSZLLF.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-RAU25Q6J.js.map → chunk-6WKWL53V.js.map} +0 -0
- /package/dist/{chunk-KFPJIKUW.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-T763ADEK.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-JYXFIILI.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-VC65GNUS.js.map → chunk-GGD2YDS6.js.map} +0 -0
- /package/dist/{chunk-5JABRT5G.js.map → chunk-GHHQLSSY.js.map} +0 -0
- /package/dist/{chunk-FMYAU5MP.js.map → chunk-GJ4HU6LK.js.map} +0 -0
- /package/dist/{chunk-3NJJQAL3.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-VC7GKR2V.js.map → chunk-IG3EZ3TF.js.map} +0 -0
- /package/dist/{chunk-QAN5ZDRS.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-A5FOXI3B.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-ZGHPELZ6.js.map → chunk-PMDOGXA2.js.map} +0 -0
- /package/dist/{chunk-F32JRMX2.js.map → chunk-QGFR73SN.js.map} +0 -0
- /package/dist/{chunk-HG3CG557.js.map → chunk-RLVI2M5Q.js.map} +0 -0
- /package/dist/{chunk-SH6ZYUTX.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-PL75KQGF.js.map → chunk-UW6LBFYN.js.map} +0 -0
- /package/dist/{chunk-VSZNPAGL.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-E6VQJ75T.js.map → chunk-ZORS5BG5.js.map} +0 -0
- /package/dist/{chunk-KXY4HW2Y.js.map → chunk-ZXF3O7P4.js.map} +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { classifyExtractionOrigin, type ExtractionSourceContext } from "./extraction-origin-context.js";
|
|
2
|
+
import { confirmActiveFactDuplicate, persistUnderFactDedupWindow } from "./fact-dedup-window.js";
|
|
2
3
|
import { evaluateInjectionScreen, screenEntityForIndex, screenPersistStrings } from "./extraction-injection-gate.js";
|
|
3
4
|
/**
|
|
4
5
|
* Extraction persistence coordinator (issue #1526, seam 16).
|
|
@@ -1663,148 +1664,123 @@ export class ExtractionPersistCoordinator {
|
|
|
1663
1664
|
fact.tags,
|
|
1664
1665
|
);
|
|
1665
1666
|
let exactDuplicate = false;
|
|
1666
|
-
let needsCorpusConfirm = false;
|
|
1667
1667
|
try {
|
|
1668
1668
|
exactDuplicate = await this.deps.hasContentHashDedup(targetStorage, contentHashDedupKey);
|
|
1669
|
-
if (factDedupEnabled && !exactDuplicate) {
|
|
1670
|
-
// Fact dedup disabled → skip the authority check AND (via the guarded
|
|
1671
|
-
// corpus block below) corpus confirmation, so a disabled deployment
|
|
1672
|
-
// never suppresses a write via dedup (PR #2016 thread SD-nH). Enabled:
|
|
1673
|
-
// a MISS is trustworthy only against an AUTHORITATIVE index — re-check.
|
|
1674
|
-
if (await targetStorage.isFactContentHashAuthoritative()) {
|
|
1675
|
-
exactDuplicate = await this.deps.hasContentHashDedup(targetStorage, contentHashDedupKey);
|
|
1676
|
-
} else {
|
|
1677
|
-
needsCorpusConfirm = true;
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
1669
|
} catch (err) {
|
|
1681
|
-
// Dedup lookup failed (e.g. the authoritative rebuild could not run).
|
|
1682
|
-
//
|
|
1683
|
-
|
|
1670
|
+
// Dedup lookup failed (e.g. the authoritative rebuild could not run).
|
|
1671
|
+
// When dedup is enabled the confirm below still scans the corpus; when
|
|
1672
|
+
// it is disabled, never suppress a write via dedup (PR #2016 SD-nH).
|
|
1673
|
+
exactDuplicate = false;
|
|
1684
1674
|
log.warn(
|
|
1685
1675
|
`content-hash dedup lookup unavailable for storage ${targetStorage.dir}; confirming against corpus: ${err}`,
|
|
1686
1676
|
);
|
|
1687
1677
|
}
|
|
1688
|
-
//
|
|
1689
|
-
//
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
const matchesActiveDuplicate = (m: MemoryFile): boolean => {
|
|
1707
|
-
if (m.frontmatter.category !== writeCategory) return false;
|
|
1708
|
-
if ((m.frontmatter.status ?? "active") !== "active") return false;
|
|
1709
|
-
// Thread 5 (QPDE5): for procedures the hash is keyed on the full
|
|
1710
|
-
// body (title + steps via buildProcedurePersistBody), so compare
|
|
1711
|
-
// against contentHashDedupKey — not canonicalContentForHash which
|
|
1712
|
-
// is the title only. Thread 3 (QO42V): facts with
|
|
1713
|
-
// structuredAttributes get an appended [Attributes: ...] suffix in
|
|
1714
|
-
// the stored body that the hash key lacks; strip it before
|
|
1715
|
-
// comparing so same-connector enriched facts dedup correctly.
|
|
1716
|
-
if (normalizeStoredHashSource(stripAttributesSuffix(m.content ?? "")) !==
|
|
1717
|
-
ContentHashIndex.normalizeContent(contentHashDedupKey)) return false;
|
|
1718
|
-
return (m.frontmatter.sourceConnector?.trim() || undefined) === nc;
|
|
1719
|
-
};
|
|
1720
|
-
exactDuplicate =
|
|
1721
|
-
hotMems.some(matchesActiveDuplicate) || coldMems.some(matchesActiveDuplicate);
|
|
1722
|
-
} catch (err) {
|
|
1723
|
-
log.warn(
|
|
1724
|
-
`connector-aware dedup scan failed for storage ${targetStorage.dir}; writing fail-open: ${err instanceof Error ? err.message : String(err)}`,
|
|
1725
|
-
);
|
|
1726
|
-
exactDuplicate = false;
|
|
1727
|
-
}
|
|
1678
|
+
// Shared confirm options for the persist-time check and the write-window
|
|
1679
|
+
// race recheck below.
|
|
1680
|
+
const raceDedupOpts = (key: string) => ({
|
|
1681
|
+
category: writeCategory,
|
|
1682
|
+
contentHashDedupKey: key,
|
|
1683
|
+
connector: sourceContext?.sourceConnector?.trim() || undefined,
|
|
1684
|
+
citationEnabled,
|
|
1685
|
+
citationTemplate,
|
|
1686
|
+
});
|
|
1687
|
+
// Fact dedup disabled → skip the authority check AND the corpus
|
|
1688
|
+
// confirmation, so a disabled deployment never suppresses a write via
|
|
1689
|
+
// dedup (PR #2016 thread SD-nH).
|
|
1690
|
+
if (factDedupEnabled) {
|
|
1691
|
+
exactDuplicate = await confirmActiveFactDuplicate(
|
|
1692
|
+
{ hasContentHashDedup: (storage, key) => this.deps.hasContentHashDedup(storage, key) },
|
|
1693
|
+
targetStorage,
|
|
1694
|
+
raceDedupOpts(contentHashDedupKey),
|
|
1695
|
+
);
|
|
1728
1696
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
//
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1697
|
+
// PR #2016/#3231: finalize committed writes before a fail-closed
|
|
1698
|
+
// window throw propagates (PRRT_kwDORJXyws6zxG6H).
|
|
1699
|
+
const finalizeOnWindowFailure = async (err: Error): Promise<never> => {
|
|
1700
|
+
await finalizeCommittedWrites(true).catch((finalizeErr) =>
|
|
1701
|
+
log.warn(`committed-write finalization after fact-dedup window failure failed: ${finalizeErr}`),
|
|
1702
|
+
);
|
|
1703
|
+
throw err;
|
|
1704
|
+
};
|
|
1705
|
+
const raceRecheck = (storage: StorageManager, key: string) => () => {
|
|
1706
|
+
// Gates mirror the persist-time suppression gates (PR #2016 SD-nH; #1645 quarantine).
|
|
1707
|
+
if (!factDedupEnabled || injectionScreenStatus === "pending_review") return Promise.resolve(false);
|
|
1708
|
+
return confirmActiveFactDuplicate(
|
|
1709
|
+
{ hasContentHashDedup: (s, k) => this.deps.hasContentHashDedup(s, k) },
|
|
1710
|
+
storage,
|
|
1711
|
+
raceDedupOpts(key),
|
|
1712
|
+
);
|
|
1713
|
+
};
|
|
1714
|
+
// #1671/#1707 — backfill bi-temporal bounds onto the surviving copy
|
|
1715
|
+
// when a duplicate short-circuits: persist-time hit OR raced duplicate
|
|
1716
|
+
// suppressed in the window (codex P2, PRRT_kwDORJXyws6zxlor). Facts
|
|
1717
|
+
// only (#1707 t3); a corrected start bound counts (t2). Skipped when a
|
|
1718
|
+
// downstream gate would reject the candidate — a non-durable candidate
|
|
1719
|
+
// must not expire an active fact (faithfulness, requireSpans, judge,
|
|
1720
|
+
// #372 importance).
|
|
1721
|
+
const backfillDuplicateTemporalBounds = async () => {
|
|
1722
|
+
if (!(biTemporal && writeCategory === "fact" && (biTemporal.validUntil || biTemporal.validFrom))) return;
|
|
1723
|
+
const fr = faithfulnessResultsByFactIndex?.get(factLoopIndex);
|
|
1724
|
+
const faithfulnessWouldPending =
|
|
1725
|
+
faithfulnessMode === "enforce" &&
|
|
1726
|
+
fr?.ok === true &&
|
|
1727
|
+
(fr.verdict === "unsupported" || fr.verdict === "contradicted");
|
|
1728
|
+
const requireSpansWouldPending =
|
|
1729
|
+
this.deps.config.provenance?.requireSpans === true &&
|
|
1730
|
+
fact.requireSpansPending === true;
|
|
1731
|
+
const judgeVerdict = judgeVerdictsByFactIndex?.get(factLoopIndex);
|
|
1732
|
+
const judgeWouldGate =
|
|
1733
|
+
!this.deps.config.extractionJudgeShadow &&
|
|
1734
|
+
judgeVerdict !== undefined &&
|
|
1735
|
+
!judgeVerdict.durable;
|
|
1736
|
+
// Importance gate (cursor PRRT_OvKnS): below-threshold duplicates
|
|
1737
|
+
// would never persist (#372) and carry no judge verdict (the
|
|
1738
|
+
// pre-filter skips them), so they must not expire an active fact.
|
|
1745
1739
|
if (
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1740
|
+
!isAboveImportanceThreshold(importance.level, this.deps.config.extractionMinImportanceLevel) ||
|
|
1741
|
+
faithfulnessWouldPending ||
|
|
1742
|
+
requireSpansWouldPending ||
|
|
1743
|
+
judgeWouldGate
|
|
1749
1744
|
) {
|
|
1750
|
-
|
|
1751
|
-
const faithfulnessWouldPending =
|
|
1752
|
-
faithfulnessMode === "enforce" &&
|
|
1753
|
-
fr?.ok === true &&
|
|
1754
|
-
(fr.verdict === "unsupported" || fr.verdict === "contradicted");
|
|
1755
|
-
const requireSpansWouldPending =
|
|
1756
|
-
this.deps.config.provenance?.requireSpans === true &&
|
|
1757
|
-
fact.requireSpansPending === true;
|
|
1758
|
-
const judgeVerdict = judgeVerdictsByFactIndex?.get(factLoopIndex);
|
|
1759
|
-
const judgeWouldGate =
|
|
1760
|
-
!this.deps.config.extractionJudgeShadow &&
|
|
1761
|
-
judgeVerdict !== undefined &&
|
|
1762
|
-
!judgeVerdict.durable;
|
|
1763
|
-
// Importance gate (cursor PRRT_OvKnS): below-threshold duplicates
|
|
1764
|
-
// would never persist (#372) and carry no judge verdict (the
|
|
1765
|
-
// pre-filter skips them), so they must not expire an active fact.
|
|
1766
|
-
if (
|
|
1767
|
-
isAboveImportanceThreshold(importance.level, this.deps.config.extractionMinImportanceLevel) &&
|
|
1768
|
-
!faithfulnessWouldPending &&
|
|
1769
|
-
!requireSpansWouldPending &&
|
|
1770
|
-
!judgeWouldGate
|
|
1771
|
-
) {
|
|
1772
|
-
await this.deps.backfillTemporalBoundsOnDedupHit(
|
|
1773
|
-
targetStorage,
|
|
1774
|
-
contentHashDedupKey,
|
|
1775
|
-
{
|
|
1776
|
-
invalidAt: biTemporal.validUntil,
|
|
1777
|
-
// #1707 thread 2 — carry the corrected start bound too.
|
|
1778
|
-
validFrom: biTemporal.validFrom,
|
|
1779
|
-
observedAt: biTemporal.observedAt,
|
|
1780
|
-
eventTimeSource: biTemporal.eventTimeSource,
|
|
1781
|
-
},
|
|
1782
|
-
fact.entityRef,
|
|
1783
|
-
sourceContext?.sourceConnector,
|
|
1784
|
-
);
|
|
1785
|
-
// #1707 thread 1 — the source branch short-circuits (`continue`
|
|
1786
|
-
// below) before the promotion dedup paths run, so promoted
|
|
1787
|
-
// shared/profile copies written before the source fact carried
|
|
1788
|
-
// resolved bounds stay stale and cross-namespace recall surfaces
|
|
1789
|
-
// an expired fact. Backfill the promotion targets too (fail-open,
|
|
1790
|
-
// backfill-only — never writes a new promoted copy).
|
|
1791
|
-
await backfillTemporalBoundsOnPromotionCopies({
|
|
1792
|
-
sourceStorage: targetStorage,
|
|
1793
|
-
content: fact.content,
|
|
1794
|
-
category: writeCategory,
|
|
1795
|
-
confidence: fact.confidence,
|
|
1796
|
-
entityRef: fact.entityRef,
|
|
1797
|
-
structuredAttributes: fact.structuredAttributes,
|
|
1798
|
-
sourceConnector: sourceContext?.sourceConnector,
|
|
1799
|
-
bounds: {
|
|
1800
|
-
invalidAt: biTemporal.validUntil,
|
|
1801
|
-
validFrom: biTemporal.validFrom,
|
|
1802
|
-
observedAt: biTemporal.observedAt,
|
|
1803
|
-
eventTimeSource: biTemporal.eventTimeSource,
|
|
1804
|
-
},
|
|
1805
|
-
});
|
|
1806
|
-
}
|
|
1745
|
+
return;
|
|
1807
1746
|
}
|
|
1747
|
+
await this.deps.backfillTemporalBoundsOnDedupHit(
|
|
1748
|
+
targetStorage,
|
|
1749
|
+
contentHashDedupKey,
|
|
1750
|
+
{
|
|
1751
|
+
invalidAt: biTemporal.validUntil,
|
|
1752
|
+
// #1707 thread 2 — carry the corrected start bound too.
|
|
1753
|
+
validFrom: biTemporal.validFrom,
|
|
1754
|
+
observedAt: biTemporal.observedAt,
|
|
1755
|
+
eventTimeSource: biTemporal.eventTimeSource,
|
|
1756
|
+
},
|
|
1757
|
+
fact.entityRef,
|
|
1758
|
+
sourceContext?.sourceConnector,
|
|
1759
|
+
);
|
|
1760
|
+
// #1707 thread 1 — a short-circuited source branch never reaches the
|
|
1761
|
+
// promotion dedup paths, so promoted shared/profile copies written
|
|
1762
|
+
// before the source fact carried resolved bounds stay stale and
|
|
1763
|
+
// cross-namespace recall surfaces an expired fact. Backfill the
|
|
1764
|
+
// promotion targets too (fail-open, backfill-only — never writes a
|
|
1765
|
+
// new promoted copy).
|
|
1766
|
+
await backfillTemporalBoundsOnPromotionCopies({
|
|
1767
|
+
sourceStorage: targetStorage,
|
|
1768
|
+
content: fact.content,
|
|
1769
|
+
category: writeCategory,
|
|
1770
|
+
confidence: fact.confidence,
|
|
1771
|
+
entityRef: fact.entityRef,
|
|
1772
|
+
structuredAttributes: fact.structuredAttributes,
|
|
1773
|
+
sourceConnector: sourceContext?.sourceConnector,
|
|
1774
|
+
bounds: {
|
|
1775
|
+
invalidAt: biTemporal.validUntil,
|
|
1776
|
+
validFrom: biTemporal.validFrom,
|
|
1777
|
+
observedAt: biTemporal.observedAt,
|
|
1778
|
+
eventTimeSource: biTemporal.eventTimeSource,
|
|
1779
|
+
},
|
|
1780
|
+
});
|
|
1781
|
+
};
|
|
1782
|
+
if (exactDuplicate && injectionScreenStatus !== "pending_review") {
|
|
1783
|
+
await backfillDuplicateTemporalBounds();
|
|
1808
1784
|
log.debug(
|
|
1809
1785
|
`dedup: skipping duplicate fact "${fact.content.slice(0, 60)}…" in storage ${targetStorage.dir}`,
|
|
1810
1786
|
);
|
|
@@ -2198,7 +2174,11 @@ export class ExtractionPersistCoordinator {
|
|
|
2198
2174
|
},
|
|
2199
2175
|
{ source: extractionWriteSource },
|
|
2200
2176
|
);
|
|
2201
|
-
const
|
|
2177
|
+
const parentWriteOutcome = await persistUnderFactDedupWindow(
|
|
2178
|
+
targetStorage.dir,
|
|
2179
|
+
raceRecheck(targetStorage, contentHashDedupKey),
|
|
2180
|
+
async () =>
|
|
2181
|
+
targetStorage.writeSealedMemory(parentWriteEnvelope, {
|
|
2202
2182
|
importance,
|
|
2203
2183
|
supersedes,
|
|
2204
2184
|
links: links.length > 0 ? links : undefined,
|
|
@@ -2218,7 +2198,16 @@ export class ExtractionPersistCoordinator {
|
|
|
2218
2198
|
// durable parent .md; flush the hash now so a throw can't strand it (dup).
|
|
2219
2199
|
deferHashIndexSave: false,
|
|
2220
2200
|
...(factToolScoped ? { toolScoped: true as const } : {}),
|
|
2221
|
-
|
|
2201
|
+
}),
|
|
2202
|
+
).catch(finalizeOnWindowFailure);
|
|
2203
|
+
if (parentWriteOutcome.suppressed) {
|
|
2204
|
+
// Codex P2: backfill bounds onto the winning copy first.
|
|
2205
|
+
await backfillDuplicateTemporalBounds();
|
|
2206
|
+
log.info(`dedup: raced duplicate suppressed in fact-dedup window (chunked parent) for storage ${targetStorage.dir}`);
|
|
2207
|
+
dedupedCount++;
|
|
2208
|
+
continue;
|
|
2209
|
+
}
|
|
2210
|
+
const parentWrite = parentWriteOutcome.result;
|
|
2222
2211
|
const parentId = parentWrite.id;
|
|
2223
2212
|
// #1645: surface the tombstone block and gate active post-write paths
|
|
2224
2213
|
const tombstoneBlocked = parentWrite.tombstoneBlocked;
|
|
@@ -2589,7 +2578,11 @@ export class ExtractionPersistCoordinator {
|
|
|
2589
2578
|
},
|
|
2590
2579
|
{ source: extractionWriteSource },
|
|
2591
2580
|
);
|
|
2592
|
-
const
|
|
2581
|
+
const factWriteOutcome = await persistUnderFactDedupWindow(
|
|
2582
|
+
targetStorage.dir,
|
|
2583
|
+
raceRecheck(targetStorage, contentHashDedupKey),
|
|
2584
|
+
async () =>
|
|
2585
|
+
targetStorage.writeSealedMemory(factWriteEnvelope, {
|
|
2593
2586
|
importance,
|
|
2594
2587
|
supersedes,
|
|
2595
2588
|
links: links.length > 0 ? links : undefined,
|
|
@@ -2607,11 +2600,21 @@ export class ExtractionPersistCoordinator {
|
|
|
2607
2600
|
// through to frontmatter so they survive end-to-end.
|
|
2608
2601
|
...(fact.sources && fact.sources.length > 0 ? { sources: fact.sources } : {}),
|
|
2609
2602
|
...(fact.provenance ? { provenance: fact.provenance } : {}),
|
|
2610
|
-
// #
|
|
2611
|
-
//
|
|
2612
|
-
|
|
2603
|
+
// PR #2016: never defer inside the commit window (chunked-path
|
|
2604
|
+
// rule): a peer loading an authoritative index after the release
|
|
2605
|
+
// must see this hash, or its recheck misses and duplicates it.
|
|
2606
|
+
deferHashIndexSave: false,
|
|
2613
2607
|
...(factToolScoped ? { toolScoped: true as const } : {}),
|
|
2614
|
-
|
|
2608
|
+
}),
|
|
2609
|
+
).catch(finalizeOnWindowFailure);
|
|
2610
|
+
if (factWriteOutcome.suppressed) {
|
|
2611
|
+
// Codex P2: backfill bounds onto the window's winning copy first.
|
|
2612
|
+
await backfillDuplicateTemporalBounds();
|
|
2613
|
+
log.info(`dedup: raced duplicate suppressed in fact-dedup window for storage ${targetStorage.dir}`);
|
|
2614
|
+
dedupedCount++;
|
|
2615
|
+
continue;
|
|
2616
|
+
}
|
|
2617
|
+
const factWrite = factWriteOutcome.result;
|
|
2615
2618
|
const memoryId = factWrite.id;
|
|
2616
2619
|
// #1645: surface the tombstone block; gate active post-write paths like #1576
|
|
2617
2620
|
// so a blocked fact creates no active shared copy / supersession / graph entry.
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extraction queue coordinator — extracted from the orchestrator (issue #1526).
|
|
3
3
|
*
|
|
4
|
-
* Owns the background
|
|
5
|
-
*
|
|
4
|
+
* Owns the background bounded-parallel queue that drains extraction tasks
|
|
5
|
+
* while avoiding races between concurrent flush/heartbeat/bulk-import
|
|
6
6
|
* triggers:
|
|
7
|
-
* - queue state (`
|
|
7
|
+
* - queue state (`queue` + `processing` + per-task preempt slots)
|
|
8
8
|
* - the scheduling trigger (start the drain when the first task lands and
|
|
9
|
-
*
|
|
10
|
-
* - the
|
|
9
|
+
* capacity is free)
|
|
10
|
+
* - the drain loop itself (`processQueue`): up to `concurrency` tasks run
|
|
11
|
+
* at once; tasks sharing an entry `key` (a buffer/session) never run
|
|
12
|
+
* concurrently, so the same buffer is never extracted twice in parallel
|
|
11
13
|
* - failure classification (`logExtractionQueueFailure`) — issue #549:
|
|
12
14
|
* AbortError from session transitions logs at debug, real failures at
|
|
13
15
|
* error
|
|
@@ -19,51 +21,102 @@
|
|
|
19
21
|
* orchestrator builds each task closure and hands it to `enqueue`; the
|
|
20
22
|
* coordinator owns only the queue mechanics.
|
|
21
23
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
24
|
+
* Preemption is per task: each running task receives an
|
|
25
|
+
* `ExtractionPreemptHandle` at run time and registers its own abort, so two
|
|
26
|
+
* concurrent automatic extractions cannot clobber each other's registration
|
|
27
|
+
* (the previous single-slot `runningPreempt` could only hold one).
|
|
28
|
+
* `preemptRunning` aborts at most ONE armed task — the requested key's own
|
|
29
|
+
* running work when it is armed, else the most recently armed task — which
|
|
30
|
+
* is exactly the single-slot semantics generalized from one worker to N.
|
|
25
31
|
*/
|
|
26
32
|
|
|
27
33
|
import { isAbortError, preemptedExtractionError } from "../abort-error.js";
|
|
28
34
|
import { log } from "../logger.js";
|
|
29
35
|
|
|
36
|
+
/** Preemption registration for ONE running task. Armed work can be aborted
|
|
37
|
+
* by a fail-closed flush; disarmed work (past persist) cannot. */
|
|
38
|
+
export interface ExtractionPreemptHandle {
|
|
39
|
+
/** Register the task's abort (replacing any previous registration). */
|
|
40
|
+
arm(abort: () => void): void;
|
|
41
|
+
/** Remove the task's registration (idempotent). */
|
|
42
|
+
disarm(): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A queued task; the coordinator passes the task's own preempt handle. */
|
|
46
|
+
export type ExtractionQueueTask = (preempt?: ExtractionPreemptHandle) => Promise<void>;
|
|
47
|
+
|
|
48
|
+
export interface ExtractionQueueEnqueueOptions {
|
|
49
|
+
/** Jump the queue and preempt the running automatic work (force flush). */
|
|
50
|
+
front?: boolean;
|
|
51
|
+
/** Buffer/session key. Tasks sharing a key never run concurrently. */
|
|
52
|
+
key?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface QueueEntry {
|
|
56
|
+
run: ExtractionQueueTask;
|
|
57
|
+
key?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface PreemptSlot {
|
|
61
|
+
key?: string;
|
|
62
|
+
armed?: { abort: () => void; seq: number };
|
|
63
|
+
}
|
|
64
|
+
|
|
30
65
|
/**
|
|
31
|
-
* Coordinates the background
|
|
32
|
-
*
|
|
66
|
+
* Coordinates the background bounded-parallel extraction queue. Owns the
|
|
67
|
+
* queue array, the per-key in-flight guard, and the per-task preempt slots.
|
|
33
68
|
*/
|
|
34
69
|
export class ExtractionQueueCoordinator {
|
|
70
|
+
/** Maximum tasks that run at the same time (config `extractionQueueConcurrency`). */
|
|
71
|
+
private readonly concurrency: number;
|
|
35
72
|
/**
|
|
36
|
-
* Background
|
|
37
|
-
*
|
|
73
|
+
* Background queue for extractions (agent_end optimization). Queue stores
|
|
74
|
+
* tasks that resolve when extraction should run.
|
|
38
75
|
*/
|
|
39
|
-
private readonly queue:
|
|
40
|
-
/** Whether the
|
|
76
|
+
private readonly queue: QueueEntry[] = [];
|
|
77
|
+
/** Whether the drain loop is currently running. */
|
|
41
78
|
private processing = false;
|
|
42
79
|
private accepting = true;
|
|
43
|
-
|
|
80
|
+
/** Buffer keys with a task currently running (per-key exclusion). */
|
|
81
|
+
private readonly activeKeys = new Set<string>();
|
|
82
|
+
/** Preempt slots of the currently running tasks, keyed by slot identity. */
|
|
83
|
+
private readonly preemptSlots = new Map<object, PreemptSlot>();
|
|
84
|
+
private preemptSeq = 0;
|
|
85
|
+
/** Resolves the processor's park while a new task arrives mid-drain. */
|
|
86
|
+
private wake?: () => void;
|
|
87
|
+
|
|
88
|
+
constructor(concurrency = 1) {
|
|
89
|
+
this.concurrency = Number.isInteger(concurrency) && concurrency >= 1 ? concurrency : 1;
|
|
90
|
+
}
|
|
44
91
|
|
|
45
92
|
/** Current queue depth (idle-wait + test seam). */
|
|
46
93
|
get length(): number {
|
|
47
94
|
return this.queue.length;
|
|
48
95
|
}
|
|
49
96
|
|
|
50
|
-
/** Whether the
|
|
97
|
+
/** Whether the drain loop is currently running (idle-wait + test seam). */
|
|
51
98
|
get isProcessing(): boolean {
|
|
52
99
|
return this.processing;
|
|
53
100
|
}
|
|
54
101
|
|
|
102
|
+
/** How many tasks may run at once (test seam + diagnostics). */
|
|
103
|
+
get maxConcurrency(): number {
|
|
104
|
+
return this.concurrency;
|
|
105
|
+
}
|
|
106
|
+
|
|
55
107
|
/**
|
|
56
|
-
* Enqueue a task and start the
|
|
108
|
+
* Enqueue a task and start the processor when capacity is free. This is
|
|
57
109
|
* the production entry point — the orchestrator's `queueBufferedExtraction`
|
|
58
|
-
* builds each task closure and hands it here.
|
|
110
|
+
* builds each task closure and hands it here with its buffer key.
|
|
59
111
|
*/
|
|
60
|
-
enqueue(task:
|
|
112
|
+
enqueue(task: ExtractionQueueTask, options?: ExtractionQueueEnqueueOptions): boolean {
|
|
61
113
|
if (!this.accepting) return false;
|
|
114
|
+
const entry: QueueEntry = { run: task, key: options?.key };
|
|
62
115
|
if (options?.front) {
|
|
63
|
-
this.preemptRunning();
|
|
64
|
-
this.queue.unshift(
|
|
116
|
+
this.preemptRunning(options.key);
|
|
117
|
+
this.queue.unshift(entry);
|
|
65
118
|
} else {
|
|
66
|
-
this.queue.push(
|
|
119
|
+
this.queue.push(entry);
|
|
67
120
|
}
|
|
68
121
|
if (!this.processing) {
|
|
69
122
|
this.processing = true;
|
|
@@ -71,38 +124,43 @@ export class ExtractionQueueCoordinator {
|
|
|
71
124
|
this.logExtractionQueueFailure(err, "processor");
|
|
72
125
|
this.processing = false;
|
|
73
126
|
});
|
|
127
|
+
} else {
|
|
128
|
+
// The processor may be parked waiting for an in-flight task while a
|
|
129
|
+
// slot is free; wake it so the new task starts instead of waiting
|
|
130
|
+
// behind work it could overlap with.
|
|
131
|
+
this.wake?.();
|
|
74
132
|
}
|
|
75
133
|
return true;
|
|
76
134
|
}
|
|
77
135
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
preemptRunning(): void {
|
|
88
|
-
const abort = this.runningPreempt;
|
|
89
|
-
this.runningPreempt = undefined;
|
|
136
|
+
preemptRunning(preferredKey?: string): void {
|
|
137
|
+
const armed = Array.from(this.preemptSlots.values()).filter((slot) => slot.armed !== undefined);
|
|
138
|
+
if (armed.length === 0) return;
|
|
139
|
+
const preferred = preferredKey !== undefined ? armed.find((slot) => slot.key === preferredKey) : undefined;
|
|
140
|
+
const chosen =
|
|
141
|
+
preferred ??
|
|
142
|
+
armed.reduce((a, b) => ((b.armed?.seq ?? -1) > (a.armed?.seq ?? -1) ? b : a));
|
|
143
|
+
const abort = chosen.armed?.abort;
|
|
144
|
+
chosen.armed = undefined;
|
|
90
145
|
abort?.();
|
|
91
146
|
}
|
|
147
|
+
|
|
92
148
|
runPreemptable<T>(
|
|
149
|
+
preempt: ExtractionPreemptHandle | undefined,
|
|
93
150
|
preemptable: boolean,
|
|
94
151
|
parent: AbortSignal | undefined,
|
|
95
152
|
run: (signal: AbortSignal) => Promise<T>,
|
|
96
153
|
): Promise<T> {
|
|
97
|
-
const
|
|
98
|
-
if (preemptable
|
|
99
|
-
|
|
154
|
+
const preemptController = new AbortController();
|
|
155
|
+
if (preemptable && preempt) {
|
|
156
|
+
preempt.arm(() => preemptController.abort(preemptedExtractionError()));
|
|
157
|
+
}
|
|
158
|
+
const signal = parent ? AbortSignal.any([parent, preemptController.signal]) : preemptController.signal;
|
|
100
159
|
return Promise.resolve(run(signal)).finally(() => {
|
|
101
|
-
if (preemptable)
|
|
160
|
+
if (preemptable && preempt) preempt.disarm();
|
|
102
161
|
});
|
|
103
162
|
}
|
|
104
163
|
|
|
105
|
-
|
|
106
164
|
/** Stop new producers from entering the queue. */
|
|
107
165
|
stopAccepting(): void {
|
|
108
166
|
this.accepting = false;
|
|
@@ -123,30 +181,82 @@ export class ExtractionQueueCoordinator {
|
|
|
123
181
|
}
|
|
124
182
|
|
|
125
183
|
/**
|
|
126
|
-
* Background
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* semantics directly.
|
|
184
|
+
* Background queue processor. Runs up to `concurrency` tasks at a time,
|
|
185
|
+
* never two tasks with the same entry key. Called automatically when items
|
|
186
|
+
* are queued via `enqueue`; also exposed for the characterization tests
|
|
187
|
+
* that push raw tasks and assert drain semantics directly.
|
|
131
188
|
*/
|
|
132
189
|
async processQueue(): Promise<void> {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
190
|
+
const inFlight = new Set<Promise<void>>();
|
|
191
|
+
try {
|
|
192
|
+
while (this.queue.length > 0 || inFlight.size > 0) {
|
|
193
|
+
while (inFlight.size < this.concurrency) {
|
|
194
|
+
const entry = this.takeRunnable();
|
|
195
|
+
if (!entry) break;
|
|
196
|
+
const running = this.runEntry(entry).finally(() => {
|
|
197
|
+
inFlight.delete(running);
|
|
198
|
+
});
|
|
199
|
+
inFlight.add(running);
|
|
200
|
+
}
|
|
201
|
+
// Park until a task settles OR a new task is enqueued. A queued task
|
|
202
|
+
// whose key is still held always implies an in-flight task holding
|
|
203
|
+
// it, so inFlight is non-empty here whenever the loop must wait.
|
|
204
|
+
if (inFlight.size === 0) break;
|
|
205
|
+
const { promise: wakePromise, resolve: wake } = Promise.withResolvers<void>();
|
|
206
|
+
this.wake = wake;
|
|
136
207
|
try {
|
|
137
|
-
await
|
|
138
|
-
}
|
|
139
|
-
this.
|
|
208
|
+
await Promise.race([wakePromise, ...inFlight]);
|
|
209
|
+
} finally {
|
|
210
|
+
this.wake = undefined;
|
|
140
211
|
}
|
|
141
212
|
}
|
|
213
|
+
} finally {
|
|
214
|
+
this.processing = false;
|
|
142
215
|
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Take the next queue entry whose key is not already running (FIFO within a key). */
|
|
219
|
+
private takeRunnable(): QueueEntry | undefined {
|
|
220
|
+
for (let index = 0; index < this.queue.length; index += 1) {
|
|
221
|
+
const entry = this.queue[index];
|
|
222
|
+
if (entry.key !== undefined && this.activeKeys.has(entry.key)) continue;
|
|
223
|
+
this.queue.splice(index, 1);
|
|
224
|
+
if (entry.key !== undefined) this.activeKeys.add(entry.key);
|
|
225
|
+
return entry;
|
|
226
|
+
}
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Run one entry with its own preempt slot; failures are classified + logged. */
|
|
231
|
+
private async runEntry(entry: QueueEntry): Promise<void> {
|
|
232
|
+
const slot: PreemptSlot = {};
|
|
233
|
+
this.preemptSlots.set(slot, { key: entry.key });
|
|
234
|
+
try {
|
|
235
|
+
await entry.run(this.createPreemptHandle(slot));
|
|
236
|
+
} catch (err) {
|
|
237
|
+
this.logExtractionQueueFailure(err, "task");
|
|
238
|
+
} finally {
|
|
239
|
+
this.preemptSlots.delete(slot);
|
|
240
|
+
if (entry.key !== undefined) this.activeKeys.delete(entry.key);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
143
243
|
|
|
144
|
-
|
|
244
|
+
private createPreemptHandle(slot: PreemptSlot): ExtractionPreemptHandle {
|
|
245
|
+
return {
|
|
246
|
+
arm: (abort: () => void) => {
|
|
247
|
+
const state = this.preemptSlots.get(slot);
|
|
248
|
+
if (state) state.armed = { abort, seq: this.preemptSeq++ };
|
|
249
|
+
},
|
|
250
|
+
disarm: () => {
|
|
251
|
+
const state = this.preemptSlots.get(slot);
|
|
252
|
+
if (state) state.armed = undefined;
|
|
253
|
+
},
|
|
254
|
+
};
|
|
145
255
|
}
|
|
146
256
|
|
|
147
257
|
/**
|
|
148
258
|
* Classify + log a failure from either the per-task catch inside
|
|
149
|
-
* `
|
|
259
|
+
* `runEntry` or the outer `processQueue().catch(...)` in
|
|
150
260
|
* `enqueue()`. Issue #549: `throwIfRecallAborted`
|
|
151
261
|
* (used throughout `runExtraction`) raises an Error whose `name` is
|
|
152
262
|
* `"AbortError"`. That path fires when `before_reset` aborts a
|
|
@@ -211,13 +321,14 @@ export class ExtractionQueueCoordinator {
|
|
|
211
321
|
* pre-extraction `this.extractionQueue.push(...)` used by the
|
|
212
322
|
* characterization tests that drive the drain manually via `processQueue`.
|
|
213
323
|
*/
|
|
214
|
-
pushRaw(task:
|
|
215
|
-
this.queue.push(task);
|
|
324
|
+
pushRaw(task: ExtractionQueueTask, options?: { key?: string }): void {
|
|
325
|
+
this.queue.push({ run: task, key: options?.key });
|
|
326
|
+
this.wake?.();
|
|
216
327
|
}
|
|
217
328
|
|
|
218
329
|
/** Remove + return the front task (test seam for the flush deadline tests). */
|
|
219
|
-
shift():
|
|
220
|
-
return this.queue.shift();
|
|
330
|
+
shift(): ExtractionQueueTask | undefined {
|
|
331
|
+
return this.queue.shift()?.run;
|
|
221
332
|
}
|
|
222
333
|
|
|
223
334
|
/**
|