@remnic/core 10.0.76 → 10.0.78
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 +27 -27
- package/dist/access-authorization-probe.js +28 -28
- package/dist/access-boundary.js +27 -27
- package/dist/access-cli.js +78 -77
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.js +27 -27
- package/dist/access-http-briefing.js +28 -28
- package/dist/access-http-query.js +29 -29
- package/dist/access-http.js +45 -45
- package/dist/access-identity-continuity-surface.js +27 -27
- package/dist/access-lcm-surface.js +27 -27
- package/dist/access-mcp.js +37 -37
- package/dist/access-namespace-preflight.js +27 -27
- package/dist/access-observe-helpers.d.ts.map +1 -1
- package/dist/access-observe-helpers.js +1 -1
- package/dist/access-observe-write-category.js +5 -5
- package/dist/access-observe-write-surface.js +27 -27
- package/dist/access-operations-batch.js +29 -29
- package/dist/access-operations.js +36 -36
- package/dist/access-recall-concurrency.js +27 -27
- package/dist/access-recall-response.js +27 -27
- package/dist/access-recall-surface.js +27 -27
- package/dist/access-schema.js +4 -4
- package/dist/access-service.js +27 -27
- package/dist/access-surface-catalog.js +29 -29
- package/dist/active-recall.js +7 -7
- package/dist/briefing.js +8 -8
- package/dist/calibration.js +1 -1
- package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-YO5QJ6L3.js} +2 -2
- package/dist/causal-consolidation.js +9 -9
- package/dist/{chunk-Z3CKK5RR.js → chunk-2XG7QECM.js} +3 -3
- package/dist/{chunk-BZTA7DR4.js → chunk-35RSL5B4.js} +3 -3
- package/dist/{chunk-3GGCMRO7.js → chunk-3RYP4OTZ.js} +2 -2
- package/dist/{chunk-VEQVXNWR.js → chunk-446H5UUO.js} +4 -4
- package/dist/{chunk-XC7OYOQC.js → chunk-4BCX6YLW.js} +2 -2
- package/dist/{chunk-RXGFITWS.js → chunk-4HLPFYF2.js} +7 -7
- package/dist/{chunk-WIAXVBQH.js → chunk-65SKM4BF.js} +2 -2
- package/dist/{chunk-VEWTHVX2.js → chunk-6HERQLF4.js} +2 -2
- package/dist/{chunk-4DLZ2ITE.js → chunk-6OGFQ5GH.js} +2 -2
- package/dist/{chunk-C44DRDZM.js → chunk-6WULHFNJ.js} +2 -2
- package/dist/{chunk-IVXOYE43.js → chunk-7IQ5HYBJ.js} +2 -2
- package/dist/{chunk-SZBZOUHR.js → chunk-A45ZOJV4.js} +2 -2
- package/dist/chunk-ADJXJKU6.js +74 -0
- package/dist/chunk-ADJXJKU6.js.map +1 -0
- package/dist/{chunk-DANFOVQG.js → chunk-B7PLF3HW.js} +2 -2
- package/dist/{chunk-PBE7NFB5.js → chunk-BNQJKWWZ.js} +4 -4
- package/dist/{chunk-AY2UK5XK.js → chunk-BSBGI4BD.js} +2 -2
- package/dist/{chunk-AY2UK5XK.js.map → chunk-BSBGI4BD.js.map} +1 -1
- package/dist/{chunk-2NLLXCJG.js → chunk-BXLOS5AJ.js} +2 -2
- package/dist/{chunk-IPHIIRDN.js → chunk-CYTTY4M2.js} +5 -7
- package/dist/chunk-CYTTY4M2.js.map +1 -0
- package/dist/{chunk-R24RYSQX.js → chunk-CZD654DQ.js} +22 -22
- package/dist/{chunk-MBENRPMY.js → chunk-D3WNJF7D.js} +12 -12
- package/dist/{chunk-5IQL4N37.js → chunk-D4WBMLPD.js} +2 -2
- package/dist/{chunk-FYQUUIPI.js → chunk-DLZGSZVC.js} +4 -4
- package/dist/{chunk-YK6SFDP2.js → chunk-DOJBV6WR.js} +4 -4
- package/dist/{chunk-X7Y7WX73.js → chunk-DQEMWVMT.js} +1 -1
- package/dist/{chunk-FETPTCQZ.js → chunk-E33UJHY3.js} +2 -2
- package/dist/{chunk-LJUQPT5G.js → chunk-EFRMEOZQ.js} +3 -3
- package/dist/{chunk-HTDXNIO6.js → chunk-HK3CUTAG.js} +2 -2
- package/dist/{chunk-FGUWCE5I.js → chunk-HUF4S4MY.js} +2 -2
- package/dist/{chunk-6Q6AQ36B.js → chunk-HWHNQVRV.js} +2 -2
- package/dist/{chunk-SYDPJUFR.js → chunk-IDGCL5OP.js} +2 -2
- package/dist/{chunk-F3FNXE7C.js → chunk-JHJP2IAU.js} +3 -3
- package/dist/{chunk-IBQ7LGUJ.js → chunk-JIGHVQF6.js} +6 -6
- package/dist/{chunk-P7HNMNN2.js → chunk-K4SVTZWP.js} +7 -7
- package/dist/{chunk-KAED7IBB.js → chunk-KDWO22ZK.js} +2 -2
- package/dist/{chunk-N6GEGRCT.js → chunk-KYVBGDGA.js} +2 -2
- package/dist/{chunk-HB5TNQMM.js → chunk-L7BOS7KV.js} +2 -2
- package/dist/{chunk-3MBSD53O.js → chunk-LE4MQ2WF.js} +2 -2
- package/dist/{chunk-I4LJJ44E.js → chunk-LNHBYT3F.js} +2 -2
- package/dist/{chunk-SHVLNI4L.js → chunk-LU66I56Q.js} +2 -2
- package/dist/{chunk-TD5VTVPT.js → chunk-N2H6VC65.js} +3 -3
- package/dist/{chunk-L4QYW2HO.js → chunk-NBFHOQIO.js} +2 -2
- package/dist/{chunk-KNZDGZSY.js → chunk-NVYY2TJG.js} +2 -2
- package/dist/{chunk-7FFGWIBN.js → chunk-PCSQRQPJ.js} +2 -2
- package/dist/{chunk-P3AQ2YSX.js → chunk-PFUXPTBT.js} +4 -4
- package/dist/{chunk-KQAFEZQX.js → chunk-R45L4TV6.js} +6 -6
- package/dist/{chunk-E22CQZ3H.js → chunk-R63X5BQA.js} +12 -12
- package/dist/{chunk-PLGBPFEO.js → chunk-RF7QJ736.js} +2 -2
- package/dist/{chunk-ZZSFRWKD.js → chunk-RIBCP4DJ.js} +2 -2
- package/dist/{chunk-HDN7IVIO.js → chunk-RIZ5NUNN.js} +16 -62
- package/dist/chunk-RIZ5NUNN.js.map +1 -0
- package/dist/{chunk-HR6NTVYZ.js → chunk-S34XZUTY.js} +172 -39
- package/dist/chunk-S34XZUTY.js.map +1 -0
- package/dist/{chunk-DEGE32C3.js → chunk-SLORPQZT.js} +2 -2
- package/dist/{chunk-6RPPZL7W.js → chunk-SMSJOWVE.js} +2 -2
- package/dist/{chunk-X53HJENJ.js → chunk-SPYFXSFZ.js} +2 -2
- package/dist/{chunk-IRFQKR4Y.js → chunk-SW6OAWK7.js} +2 -2
- package/dist/{chunk-VPEMGBTI.js → chunk-SZEPOUW2.js} +2 -2
- package/dist/{chunk-COIQ5N4J.js → chunk-TSITK6QK.js} +2 -2
- package/dist/{chunk-HCFNZWFB.js → chunk-VBNXDC7P.js} +2 -2
- package/dist/{chunk-FP2AT4WL.js → chunk-VJ3HFCG2.js} +2 -2
- package/dist/{chunk-OBQ2NZAR.js → chunk-VKDAJJQP.js} +4 -4
- package/dist/{chunk-HZNURRDS.js → chunk-VUROWXFL.js} +2 -2
- package/dist/{chunk-G5NH3SSX.js → chunk-X3KFSJLQ.js} +2 -2
- package/dist/{chunk-GCJA5KKT.js → chunk-XBNO56HC.js} +2 -2
- package/dist/{chunk-GWOFCHPN.js → chunk-YGECOOTF.js} +2 -2
- package/dist/{chunk-2R7OCBRR.js → chunk-YK7GOPIE.js} +2 -2
- package/dist/{chunk-ELUG6NKQ.js → chunk-YSA4CYTK.js} +2 -2
- package/dist/{chunk-233S3MXC.js → chunk-YXUNSOHD.js} +3 -3
- package/dist/{chunk-SY4AFWUZ.js → chunk-Z3O7ZOCC.js} +30 -30
- package/dist/{chunk-ZG57BEUS.js → chunk-ZAU2OPDW.js} +2 -2
- package/dist/cli.js +63 -62
- package/dist/coding/export-okf-codegraph.js +7 -7
- package/dist/compounding/engine.js +9 -9
- package/dist/config.js +7 -7
- package/dist/connectors/codex-materialize-runner.js +8 -8
- package/dist/connectors/index.js +8 -8
- package/dist/convergence-refresh.js +10 -10
- package/dist/corpus-watermark.js +10 -10
- package/dist/deep-recall-http-glue.js +5 -5
- package/dist/entity-retrieval.js +8 -8
- package/dist/external-wiki-access.js +28 -28
- package/dist/extraction.js +10 -10
- package/dist/harmonic-retrieval.js +8 -8
- package/dist/index.js +86 -85
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +5 -5
- package/dist/local-llm.js +1 -1
- package/dist/location/index.js +10 -10
- package/dist/maintenance/memory-governance.js +8 -8
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +8 -8
- package/dist/maintenance/rebuild-memory-projection-runner.js +10 -10
- package/dist/maintenance/rebuild-memory-projection.js +9 -9
- package/dist/memory-browse-operations.js +28 -28
- package/dist/namespaces/migrate.js +9 -9
- package/dist/namespaces/search.js +6 -6
- package/dist/namespaces/storage.js +8 -8
- package/dist/operator-doctor-corpus.js +11 -11
- package/dist/operator-toolkit.js +16 -16
- package/dist/orchestration/backlog-scheduler.js +79 -78
- package/dist/orchestration/backlog-scheduler.js.map +1 -1
- package/dist/orchestration/buffer-drain-runner.js +79 -78
- package/dist/orchestration/extraction-queue-snapshot-sharing.d.ts +113 -0
- package/dist/orchestration/extraction-queue-snapshot-sharing.d.ts.map +1 -0
- package/dist/orchestration/maintenance.js +19 -19
- package/dist/orchestration/turn-ingestion.d.ts +3 -0
- package/dist/orchestration/turn-ingestion.d.ts.map +1 -1
- package/dist/orchestrator.js +78 -77
- package/dist/recall-navigation-operations.js +28 -28
- package/dist/recall-navigation.js +27 -27
- package/dist/resume-bundles.js +10 -9
- package/dist/search/factory.js +5 -5
- package/dist/search/index.js +5 -5
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/orama-backend.js +2 -2
- package/dist/semantic-consolidation.js +9 -9
- package/dist/semantic-rule-promotion.js +8 -8
- package/dist/semantic-rule-verifier.js +8 -8
- package/dist/source-agent-qualifier.js +8 -8
- package/dist/standup/build.js +8 -8
- package/dist/storage.js +7 -7
- package/dist/summarizer.js +2 -2
- package/dist/support-passport/index.js +30 -30
- package/dist/transcript-ownership-digest.d.ts +31 -0
- package/dist/transcript-ownership-digest.d.ts.map +1 -0
- package/dist/transcript-ownership-digest.js +10 -0
- package/dist/transcript-ownership-digest.js.map +1 -0
- package/dist/transcript.d.ts +2 -5
- package/dist/transcript.d.ts.map +1 -1
- package/dist/transcript.js +2 -1
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +3 -3
- package/dist/transfer/capsule-import.js +2 -2
- package/dist/transfer/export-okf.js +8 -8
- package/dist/verified-recall.js +8 -8
- package/dist/who-knows-cli.js +9 -9
- package/dist/who-knows.js +8 -8
- package/package.json +2 -2
- package/src/access-observe-helpers.test.ts +156 -0
- package/src/access-observe-helpers.ts +6 -6
- package/src/local-llm-qos.test.ts +59 -0
- package/src/local-llm.ts +1 -1
- package/src/orchestration/extraction-queue-bounded-retention.test.ts +548 -0
- package/src/orchestration/extraction-queue-snapshot-sharing.ts +257 -0
- package/src/orchestration/turn-ingestion.ts +19 -6
- package/src/transcript-ownership-digest.test.ts +403 -0
- package/src/transcript-ownership-digest.ts +99 -0
- package/src/transcript.ts +14 -76
- package/dist/chunk-HDN7IVIO.js.map +0 -1
- package/dist/chunk-HR6NTVYZ.js.map +0 -1
- package/dist/chunk-IPHIIRDN.js.map +0 -1
- /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-YO5QJ6L3.js.map} +0 -0
- /package/dist/{chunk-Z3CKK5RR.js.map → chunk-2XG7QECM.js.map} +0 -0
- /package/dist/{chunk-BZTA7DR4.js.map → chunk-35RSL5B4.js.map} +0 -0
- /package/dist/{chunk-3GGCMRO7.js.map → chunk-3RYP4OTZ.js.map} +0 -0
- /package/dist/{chunk-VEQVXNWR.js.map → chunk-446H5UUO.js.map} +0 -0
- /package/dist/{chunk-XC7OYOQC.js.map → chunk-4BCX6YLW.js.map} +0 -0
- /package/dist/{chunk-RXGFITWS.js.map → chunk-4HLPFYF2.js.map} +0 -0
- /package/dist/{chunk-WIAXVBQH.js.map → chunk-65SKM4BF.js.map} +0 -0
- /package/dist/{chunk-VEWTHVX2.js.map → chunk-6HERQLF4.js.map} +0 -0
- /package/dist/{chunk-4DLZ2ITE.js.map → chunk-6OGFQ5GH.js.map} +0 -0
- /package/dist/{chunk-C44DRDZM.js.map → chunk-6WULHFNJ.js.map} +0 -0
- /package/dist/{chunk-IVXOYE43.js.map → chunk-7IQ5HYBJ.js.map} +0 -0
- /package/dist/{chunk-SZBZOUHR.js.map → chunk-A45ZOJV4.js.map} +0 -0
- /package/dist/{chunk-DANFOVQG.js.map → chunk-B7PLF3HW.js.map} +0 -0
- /package/dist/{chunk-PBE7NFB5.js.map → chunk-BNQJKWWZ.js.map} +0 -0
- /package/dist/{chunk-2NLLXCJG.js.map → chunk-BXLOS5AJ.js.map} +0 -0
- /package/dist/{chunk-R24RYSQX.js.map → chunk-CZD654DQ.js.map} +0 -0
- /package/dist/{chunk-MBENRPMY.js.map → chunk-D3WNJF7D.js.map} +0 -0
- /package/dist/{chunk-5IQL4N37.js.map → chunk-D4WBMLPD.js.map} +0 -0
- /package/dist/{chunk-FYQUUIPI.js.map → chunk-DLZGSZVC.js.map} +0 -0
- /package/dist/{chunk-YK6SFDP2.js.map → chunk-DOJBV6WR.js.map} +0 -0
- /package/dist/{chunk-X7Y7WX73.js.map → chunk-DQEMWVMT.js.map} +0 -0
- /package/dist/{chunk-FETPTCQZ.js.map → chunk-E33UJHY3.js.map} +0 -0
- /package/dist/{chunk-LJUQPT5G.js.map → chunk-EFRMEOZQ.js.map} +0 -0
- /package/dist/{chunk-HTDXNIO6.js.map → chunk-HK3CUTAG.js.map} +0 -0
- /package/dist/{chunk-FGUWCE5I.js.map → chunk-HUF4S4MY.js.map} +0 -0
- /package/dist/{chunk-6Q6AQ36B.js.map → chunk-HWHNQVRV.js.map} +0 -0
- /package/dist/{chunk-SYDPJUFR.js.map → chunk-IDGCL5OP.js.map} +0 -0
- /package/dist/{chunk-F3FNXE7C.js.map → chunk-JHJP2IAU.js.map} +0 -0
- /package/dist/{chunk-IBQ7LGUJ.js.map → chunk-JIGHVQF6.js.map} +0 -0
- /package/dist/{chunk-P7HNMNN2.js.map → chunk-K4SVTZWP.js.map} +0 -0
- /package/dist/{chunk-KAED7IBB.js.map → chunk-KDWO22ZK.js.map} +0 -0
- /package/dist/{chunk-N6GEGRCT.js.map → chunk-KYVBGDGA.js.map} +0 -0
- /package/dist/{chunk-HB5TNQMM.js.map → chunk-L7BOS7KV.js.map} +0 -0
- /package/dist/{chunk-3MBSD53O.js.map → chunk-LE4MQ2WF.js.map} +0 -0
- /package/dist/{chunk-I4LJJ44E.js.map → chunk-LNHBYT3F.js.map} +0 -0
- /package/dist/{chunk-SHVLNI4L.js.map → chunk-LU66I56Q.js.map} +0 -0
- /package/dist/{chunk-TD5VTVPT.js.map → chunk-N2H6VC65.js.map} +0 -0
- /package/dist/{chunk-L4QYW2HO.js.map → chunk-NBFHOQIO.js.map} +0 -0
- /package/dist/{chunk-KNZDGZSY.js.map → chunk-NVYY2TJG.js.map} +0 -0
- /package/dist/{chunk-7FFGWIBN.js.map → chunk-PCSQRQPJ.js.map} +0 -0
- /package/dist/{chunk-P3AQ2YSX.js.map → chunk-PFUXPTBT.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-R45L4TV6.js.map} +0 -0
- /package/dist/{chunk-E22CQZ3H.js.map → chunk-R63X5BQA.js.map} +0 -0
- /package/dist/{chunk-PLGBPFEO.js.map → chunk-RF7QJ736.js.map} +0 -0
- /package/dist/{chunk-ZZSFRWKD.js.map → chunk-RIBCP4DJ.js.map} +0 -0
- /package/dist/{chunk-DEGE32C3.js.map → chunk-SLORPQZT.js.map} +0 -0
- /package/dist/{chunk-6RPPZL7W.js.map → chunk-SMSJOWVE.js.map} +0 -0
- /package/dist/{chunk-X53HJENJ.js.map → chunk-SPYFXSFZ.js.map} +0 -0
- /package/dist/{chunk-IRFQKR4Y.js.map → chunk-SW6OAWK7.js.map} +0 -0
- /package/dist/{chunk-VPEMGBTI.js.map → chunk-SZEPOUW2.js.map} +0 -0
- /package/dist/{chunk-COIQ5N4J.js.map → chunk-TSITK6QK.js.map} +0 -0
- /package/dist/{chunk-HCFNZWFB.js.map → chunk-VBNXDC7P.js.map} +0 -0
- /package/dist/{chunk-FP2AT4WL.js.map → chunk-VJ3HFCG2.js.map} +0 -0
- /package/dist/{chunk-OBQ2NZAR.js.map → chunk-VKDAJJQP.js.map} +0 -0
- /package/dist/{chunk-HZNURRDS.js.map → chunk-VUROWXFL.js.map} +0 -0
- /package/dist/{chunk-G5NH3SSX.js.map → chunk-X3KFSJLQ.js.map} +0 -0
- /package/dist/{chunk-GCJA5KKT.js.map → chunk-XBNO56HC.js.map} +0 -0
- /package/dist/{chunk-GWOFCHPN.js.map → chunk-YGECOOTF.js.map} +0 -0
- /package/dist/{chunk-2R7OCBRR.js.map → chunk-YK7GOPIE.js.map} +0 -0
- /package/dist/{chunk-ELUG6NKQ.js.map → chunk-YSA4CYTK.js.map} +0 -0
- /package/dist/{chunk-233S3MXC.js.map → chunk-YXUNSOHD.js.map} +0 -0
- /package/dist/{chunk-SY4AFWUZ.js.map → chunk-Z3O7ZOCC.js.map} +0 -0
- /package/dist/{chunk-ZG57BEUS.js.map → chunk-ZAU2OPDW.js.map} +0 -0
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtempSync } from "node:fs";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
import { parseConfig } from "../config.js";
|
|
8
|
+
import { SmartBuffer } from "../buffer.js";
|
|
9
|
+
import type { BufferStorage } from "../buffer-persistence.js";
|
|
10
|
+
import type { CorrectionService } from "../correction/correction-service.js";
|
|
11
|
+
import type { StorageManager } from "../index.js";
|
|
12
|
+
import { SessionObserverState } from "../session-observer-state.js";
|
|
13
|
+
import { TranscriptManager } from "../transcript.js";
|
|
14
|
+
import type { BufferState, BufferTurn } from "../types.js";
|
|
15
|
+
import { ExtractionQueueCoordinator } from "./extraction-queue-coordinator.js";
|
|
16
|
+
import { queueBufferedCaptureScopeGroups } from "./extraction-task-queueing.js";
|
|
17
|
+
import { chainPrefixEquivalent, ExtractionSnapshotSharingRegistry, materializeChain, turnRepresentationEquivalent, type SnapshotChain } from "./extraction-queue-snapshot-sharing.js";
|
|
18
|
+
import { bufferTurnsEqual } from "../buffer-turn-helpers.js";
|
|
19
|
+
import {
|
|
20
|
+
TurnIngestionCoordinator,
|
|
21
|
+
type TurnIngestionDeps,
|
|
22
|
+
} from "./turn-ingestion.js";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Controlled real-queue workload for the unbounded-retention flaw (#3324
|
|
26
|
+
* follow-up). While the drain is blocked (the production OOM shape: LLM
|
|
27
|
+
* latency >> trigger cadence), every new buffered turn re-triggers
|
|
28
|
+
* extraction and enqueues a FULL prefix snapshot of the buffer. On current
|
|
29
|
+
* main each queued closure retains its own deep copy of every overlapping
|
|
30
|
+
* turn, so queued memory grows superlinearly with trigger count. The bounded
|
|
31
|
+
* design shares turn objects across overlapping queued snapshots of the same
|
|
32
|
+
* buffer key — exact value semantics (bufferTurnsEqual) with O(union)
|
|
33
|
+
* retention instead of O(sum of snapshots).
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
class FakeStorage implements BufferStorage {
|
|
37
|
+
constructor(private readonly initial: BufferState) {}
|
|
38
|
+
async loadBuffer(): Promise<BufferState> {
|
|
39
|
+
return structuredClone(this.initial);
|
|
40
|
+
}
|
|
41
|
+
async saveBuffer(): Promise<void> {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Harness {
|
|
45
|
+
coordinator: TurnIngestionCoordinator;
|
|
46
|
+
queue: ExtractionQueueCoordinator;
|
|
47
|
+
buffer: SmartBuffer;
|
|
48
|
+
/** Turn arrays as they reached the extraction boundary, in drain order. */
|
|
49
|
+
received: BufferTurn[][];
|
|
50
|
+
/** Release the blocked drain; resolves when every accepted task exited. */
|
|
51
|
+
release(): Promise<void>;
|
|
52
|
+
/** Accepted tasks: running or queued. */
|
|
53
|
+
outstanding(): number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function makeTurn(content: string): BufferTurn {
|
|
57
|
+
return {
|
|
58
|
+
role: "user",
|
|
59
|
+
content,
|
|
60
|
+
timestamp: "2026-08-17T00:00:00.000Z",
|
|
61
|
+
sessionKey: "k",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface BufferOverrides {
|
|
66
|
+
namespacesEnabled?: boolean;
|
|
67
|
+
namespacePolicies?: unknown[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function makeHarness(bufferMaxTurns = 1, configOverrides: BufferOverrides = {}): Harness {
|
|
71
|
+
const memoryDir = mkdtempSync(join(tmpdir(), "remnic-queue-retention-"));
|
|
72
|
+
const storage = new FakeStorage({ turns: [], lastExtractionAt: null, extractionCount: 0 });
|
|
73
|
+
const buffer = new SmartBuffer(
|
|
74
|
+
parseConfig({ triggerMode: "every_n", bufferMaxTurns, bufferSaveDebounceMs: 0, memoryDir }),
|
|
75
|
+
storage,
|
|
76
|
+
);
|
|
77
|
+
const queue = new ExtractionQueueCoordinator(1);
|
|
78
|
+
const received: BufferTurn[][] = [];
|
|
79
|
+
const { promise: gate, resolve: openGate } = Promise.withResolvers<void>();
|
|
80
|
+
let coordinatorRef: TurnIngestionCoordinator | undefined;
|
|
81
|
+
const config = parseConfig({
|
|
82
|
+
triggerMode: "every_n",
|
|
83
|
+
bufferMaxTurns,
|
|
84
|
+
bufferSaveDebounceMs: 0,
|
|
85
|
+
memoryDir,
|
|
86
|
+
...configOverrides,
|
|
87
|
+
});
|
|
88
|
+
const deps: TurnIngestionDeps = {
|
|
89
|
+
buffer,
|
|
90
|
+
config,
|
|
91
|
+
getStorage: async (): Promise<StorageManager> => {
|
|
92
|
+
throw new Error("unexpected getStorage");
|
|
93
|
+
},
|
|
94
|
+
bulkImportWriteNamespace: () => "default",
|
|
95
|
+
extractionQueueCoordinator: queue,
|
|
96
|
+
heartbeatObserverChains: new Map<string, Promise<void>>(),
|
|
97
|
+
lcmEngine: null,
|
|
98
|
+
passiveCorrectionDedup: new Set<string>(),
|
|
99
|
+
passiveCorrectionService: (): CorrectionService => {
|
|
100
|
+
throw new Error("unexpected passiveCorrectionService");
|
|
101
|
+
},
|
|
102
|
+
passiveCorrectionTelemetry: {
|
|
103
|
+
detected: 0,
|
|
104
|
+
queued: 0,
|
|
105
|
+
autoApplied: 0,
|
|
106
|
+
suppressedReasonCounts: {},
|
|
107
|
+
},
|
|
108
|
+
queueBufferedExtraction: (
|
|
109
|
+
...args: Parameters<TurnIngestionDeps["queueBufferedExtraction"]>
|
|
110
|
+
): Promise<void> => {
|
|
111
|
+
const coordinator = coordinatorRef;
|
|
112
|
+
assert.ok(coordinator, "coordinator must be constructed before queueing");
|
|
113
|
+
return coordinator.queueBufferedExtraction(...args);
|
|
114
|
+
},
|
|
115
|
+
resolveMemoryIdOrHandle: () => {
|
|
116
|
+
throw new Error("unexpected resolveMemoryIdOrHandle");
|
|
117
|
+
},
|
|
118
|
+
runExtraction: async (turns: BufferTurn[]) => {
|
|
119
|
+
received.push(turns);
|
|
120
|
+
await gate;
|
|
121
|
+
return {
|
|
122
|
+
status: "completed" as const,
|
|
123
|
+
persistedCount: 0,
|
|
124
|
+
durableOutputCount: 0,
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
sessionObserver: new SessionObserverState({ memoryDir, debounceMs: 0, bands: [] }),
|
|
128
|
+
shouldQueueExtraction: () => true,
|
|
129
|
+
transcript: new TranscriptManager(config),
|
|
130
|
+
};
|
|
131
|
+
const coordinator = new TurnIngestionCoordinator(deps);
|
|
132
|
+
coordinatorRef = coordinator;
|
|
133
|
+
return {
|
|
134
|
+
coordinator,
|
|
135
|
+
queue,
|
|
136
|
+
buffer,
|
|
137
|
+
received,
|
|
138
|
+
release: async () => {
|
|
139
|
+
openGate();
|
|
140
|
+
await queue.waitForIdle(30_000);
|
|
141
|
+
},
|
|
142
|
+
outstanding: () => queue.length + (queue.isProcessing ? 1 : 0),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
test("overlapping queued same-key snapshots share turn objects (bounded retention)", async () => {
|
|
147
|
+
const h = makeHarness();
|
|
148
|
+
const turns = 40;
|
|
149
|
+
for (let index = 1; index <= turns; index += 1) {
|
|
150
|
+
await h.coordinator.processTurn("user", `turn ${index} ${"x".repeat(64)}`, "k");
|
|
151
|
+
}
|
|
152
|
+
assert.equal(h.outstanding(), turns, "drain blocked: every trigger stays accepted");
|
|
153
|
+
await h.release();
|
|
154
|
+
|
|
155
|
+
assert.equal(h.received.length, turns, "every trigger extracts exactly its own window");
|
|
156
|
+
for (let index = 0; index < h.received.length; index += 1) {
|
|
157
|
+
assert.equal(h.received[index]?.length, index + 1, `window ${index} is the full prefix snapshot`);
|
|
158
|
+
}
|
|
159
|
+
// Bounded-memory proof: consecutive snapshots overlap by identity, so the
|
|
160
|
+
// queue retains the union of turn objects, not one deep copy per snapshot.
|
|
161
|
+
for (let index = 1; index < h.received.length; index += 1) {
|
|
162
|
+
const previous = h.received[index - 1];
|
|
163
|
+
const current = h.received[index];
|
|
164
|
+
assert.ok(previous && current, "every trigger produced a window");
|
|
165
|
+
for (let offset = 0; offset < previous.length; offset += 1) {
|
|
166
|
+
assert.ok(
|
|
167
|
+
previous[offset] === current[offset],
|
|
168
|
+
`snapshot ${index} must alias snapshot ${index - 1}'s turn at offset ${offset}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("behavior unchanged: FIFO windows and value equality", async () => {
|
|
175
|
+
const h = makeHarness();
|
|
176
|
+
const turns = 12;
|
|
177
|
+
for (let index = 1; index <= turns; index += 1) {
|
|
178
|
+
await h.coordinator.processTurn("user", `content ${index}`, "k");
|
|
179
|
+
}
|
|
180
|
+
await h.release();
|
|
181
|
+
|
|
182
|
+
assert.equal(h.received.length, turns);
|
|
183
|
+
for (let index = 0; index < turns; index += 1) {
|
|
184
|
+
const window = h.received[index];
|
|
185
|
+
assert.ok(window, "window present in drain order");
|
|
186
|
+
assert.equal(window.length, index + 1);
|
|
187
|
+
for (let offset = 0; offset <= index; offset += 1) {
|
|
188
|
+
assert.equal(window[offset]?.content, `content ${offset + 1}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
assert.equal(h.queue.isProcessing, false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("distinct scopes stay independent: different accepted pins never alias", async () => {
|
|
195
|
+
const h = makeHarness(
|
|
196
|
+
10_000,
|
|
197
|
+
{
|
|
198
|
+
namespacesEnabled: true,
|
|
199
|
+
namespacePolicies: [
|
|
200
|
+
{ name: "ns-a", readPrincipals: ["*"], writePrincipals: ["principal-a"] },
|
|
201
|
+
{ name: "ns-b", readPrincipals: ["*"], writePrincipals: ["principal-b"] },
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
const base = makeTurn("ordinary");
|
|
206
|
+
const acceptedA: BufferTurn = {
|
|
207
|
+
...makeTurn("accepted A"),
|
|
208
|
+
acceptedCapture: true,
|
|
209
|
+
writeNamespaceOverride: "ns-a",
|
|
210
|
+
principalOverride: "principal-a",
|
|
211
|
+
};
|
|
212
|
+
const acceptedB: BufferTurn = {
|
|
213
|
+
...makeTurn("accepted B"),
|
|
214
|
+
acceptedCapture: true,
|
|
215
|
+
writeNamespaceOverride: "ns-b",
|
|
216
|
+
principalOverride: "principal-b",
|
|
217
|
+
};
|
|
218
|
+
const seeded = [base, acceptedA, acceptedB, { ...acceptedA, content: "accepted A2" }];
|
|
219
|
+
for (const turn of seeded) await h.buffer.addTurn("k", turn);
|
|
220
|
+
const settlementPromises: Promise<void>[] = [];
|
|
221
|
+
queueBufferedCaptureScopeGroups(
|
|
222
|
+
h.coordinator.queueBufferedExtraction.bind(h.coordinator),
|
|
223
|
+
seeded,
|
|
224
|
+
"k",
|
|
225
|
+
{ onExtractionQueued: (promise) => settlementPromises.push(promise) },
|
|
226
|
+
);
|
|
227
|
+
await h.release();
|
|
228
|
+
await Promise.allSettled(settlementPromises);
|
|
229
|
+
// Each scope group is its own queued task with its own window; no cross-scope
|
|
230
|
+
// turn objects may be shared.
|
|
231
|
+
const acceptedAWindow = h.received.find(
|
|
232
|
+
(window) => window.some((turn) => turn.writeNamespaceOverride === "ns-a"),
|
|
233
|
+
);
|
|
234
|
+
const acceptedBWindow = h.received.find(
|
|
235
|
+
(window) => window.some((turn) => turn.writeNamespaceOverride === "ns-b"),
|
|
236
|
+
);
|
|
237
|
+
assert.ok(acceptedAWindow && acceptedBWindow, "both scope groups drain");
|
|
238
|
+
for (const a of acceptedAWindow) {
|
|
239
|
+
for (const b of acceptedBWindow) {
|
|
240
|
+
assert.ok(a !== b, "cross-scope turn objects must never alias");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
assert.ok(settlementPromises.length >= 1);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("front preemption still works: front task runs first, queued work intact", async () => {
|
|
247
|
+
const h = makeHarness();
|
|
248
|
+
await h.coordinator.processTurn("user", "queued 1", "k");
|
|
249
|
+
await h.coordinator.processTurn("user", "queued 2", "k");
|
|
250
|
+
assert.equal(h.outstanding(), 2);
|
|
251
|
+
const order: string[] = [];
|
|
252
|
+
h.queue.enqueue(
|
|
253
|
+
async () => {
|
|
254
|
+
order.push("front");
|
|
255
|
+
},
|
|
256
|
+
{ front: true, key: "k" },
|
|
257
|
+
);
|
|
258
|
+
await h.release();
|
|
259
|
+
assert.deepEqual(order, ["front"]);
|
|
260
|
+
assert.equal(h.received.length, 2, "queued extraction work survives a front task");
|
|
261
|
+
assert.equal(h.received[0]?.[0]?.content, "queued 1", "per-key FIFO order preserved");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("pre-aborted automatic capture settles without queueing", async () => {
|
|
265
|
+
const h = makeHarness();
|
|
266
|
+
const controller = new AbortController();
|
|
267
|
+
controller.abort();
|
|
268
|
+
const turns = [makeTurn("never extracted")];
|
|
269
|
+
await h.coordinator.queueBufferedExtraction(turns, "trigger_mode", {
|
|
270
|
+
bufferKey: "k",
|
|
271
|
+
preemptable: true,
|
|
272
|
+
abortSignal: controller.signal,
|
|
273
|
+
});
|
|
274
|
+
await h.release();
|
|
275
|
+
assert.equal(h.received.length, 0, "aborted before start: extraction never runs");
|
|
276
|
+
assert.equal(h.queue.length, 0);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("abort while queued: settled task starts, skips extraction, queue drains", async () => {
|
|
280
|
+
const h = makeHarness();
|
|
281
|
+
const controller = new AbortController();
|
|
282
|
+
// Occupies the single worker (starts synchronously on enqueue).
|
|
283
|
+
await h.coordinator.processTurn("user", "running", "k");
|
|
284
|
+
// Same key: queues behind the running task while the drain is blocked.
|
|
285
|
+
const settled: unknown[] = [];
|
|
286
|
+
await h.coordinator.queueBufferedExtraction([makeTurn("queued then aborted")], "trigger_mode", {
|
|
287
|
+
bufferKey: "k",
|
|
288
|
+
preemptable: true,
|
|
289
|
+
abortSignal: controller.signal,
|
|
290
|
+
onTaskSettled: (error) => settled.push(error),
|
|
291
|
+
});
|
|
292
|
+
assert.equal(h.outstanding(), 2);
|
|
293
|
+
controller.abort();
|
|
294
|
+
await h.release();
|
|
295
|
+
assert.equal(h.queue.length, 0);
|
|
296
|
+
assert.equal(h.queue.isProcessing, false);
|
|
297
|
+
assert.equal(h.received.length, 1, "running window finishes; queued window never extracts");
|
|
298
|
+
assert.equal(h.received[0]?.[0]?.content, "running");
|
|
299
|
+
assert.equal(settled.length, 1, "queued task settles exactly once from the abort");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("sharing bound: distinct reference slots equal the union, not the snapshot sum", async () => {
|
|
303
|
+
const h = makeHarness();
|
|
304
|
+
const turns = 30;
|
|
305
|
+
for (let index = 1; index <= turns; index += 1) {
|
|
306
|
+
await h.coordinator.processTurn("user", `bound ${index}`, "k");
|
|
307
|
+
}
|
|
308
|
+
const stats = h.coordinator.snapshotSharing.stats();
|
|
309
|
+
assert.equal(stats.queuedNodes, turns, "one chain node per accepted automatic task");
|
|
310
|
+
assert.equal(stats.queuedTailRefs, turns, "distinct reference slots = union of turns");
|
|
311
|
+
// Documents what un-shared retention would be for the same workload: the
|
|
312
|
+
// sum over full prefix snapshots (1+2+...+N), the quadratic term main pays.
|
|
313
|
+
assert.equal(stats.queuedTotalRefs, (turns * (turns + 1)) / 2);
|
|
314
|
+
await h.release();
|
|
315
|
+
const drained = h.coordinator.snapshotSharing.stats();
|
|
316
|
+
assert.equal(drained.queuedNodes, 0, "registry empties after the drain");
|
|
317
|
+
assert.equal(drained.queuedTailRefs, 0);
|
|
318
|
+
assert.equal(drained.queuedTotalRefs, 0);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("non-overlapping snapshots start fresh chains and stay exact", async () => {
|
|
322
|
+
const h = makeHarness();
|
|
323
|
+
await h.coordinator.queueBufferedExtraction([makeTurn("first era")], "trigger_mode", {
|
|
324
|
+
bufferKey: "k",
|
|
325
|
+
preemptable: true,
|
|
326
|
+
});
|
|
327
|
+
await h.coordinator.queueBufferedExtraction([makeTurn("second era")], "trigger_mode", {
|
|
328
|
+
bufferKey: "k",
|
|
329
|
+
preemptable: true,
|
|
330
|
+
});
|
|
331
|
+
const stats = h.coordinator.snapshotSharing.stats();
|
|
332
|
+
assert.equal(stats.queuedNodes, 2);
|
|
333
|
+
assert.equal(stats.queuedTailRefs, 2, "no overlap: nothing shared");
|
|
334
|
+
await h.release();
|
|
335
|
+
assert.equal(h.received[0]?.[0]?.content, "first era");
|
|
336
|
+
assert.equal(h.received[1]?.[0]?.content, "second era");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("representation guard: bufferTurnsEqual turns differing in ignored fields never share", async () => {
|
|
340
|
+
const registry = new ExtractionSnapshotSharingRegistry();
|
|
341
|
+
const plain: BufferTurn = makeTurn("identical");
|
|
342
|
+
const withParts: BufferTurn = { ...makeTurn("identical"), parts: [{ kind: "text", payload: { text: "identical" } }] };
|
|
343
|
+
const contextOnly: BufferTurn = { ...makeTurn("identical"), extractionContextOnly: true };
|
|
344
|
+
assert.ok(bufferTurnsEqual(plain, withParts), "precondition: bufferTurnsEqual ignores parts");
|
|
345
|
+
registry.admit("k", [plain]);
|
|
346
|
+
registry.admit("k", [withParts]);
|
|
347
|
+
registry.admit("k", [contextOnly]);
|
|
348
|
+
const stats = registry.stats();
|
|
349
|
+
assert.equal(stats.queuedNodes, 3, "each differing-representation turn starts its own chain");
|
|
350
|
+
assert.equal(stats.queuedTailRefs, 3, "nothing shared across representation-inequivalent turns");
|
|
351
|
+
// The representation check is strictly stronger than bufferTurnsEqual.
|
|
352
|
+
assert.ok(!turnRepresentationEquivalent(plain, withParts));
|
|
353
|
+
assert.ok(!turnRepresentationEquivalent(plain, contextOnly));
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("continuous enqueue/dequeue cycles: released ancestors erode, retention returns to zero", async () => {
|
|
357
|
+
// Codex P2 reproduction attempt: under continuous single-key
|
|
358
|
+
// enqueue/dequeue, completed ("released") ancestors stay reachable via
|
|
359
|
+
// `base` while any descendant is queued. Their content is the descendant's
|
|
360
|
+
// own window prefix (verified element-wise at admit), so retention is the
|
|
361
|
+
// union of the still-owed windows — and when every task has exited, the
|
|
362
|
+
// registry must return to exactly zero with no historical remainder.
|
|
363
|
+
const registry = new ExtractionSnapshotSharingRegistry();
|
|
364
|
+
const cycles = 500;
|
|
365
|
+
const live: { node: SnapshotChain }[] = [];
|
|
366
|
+
let peakQueuedTotalRefs = 0;
|
|
367
|
+
for (let cycle = 1; cycle <= cycles; cycle += 1) {
|
|
368
|
+
const turns: BufferTurn[] = [];
|
|
369
|
+
for (let offset = 1; offset <= cycle; offset += 1) turns.push(makeTurn(`cycle ${offset}`));
|
|
370
|
+
live.push({ node: registry.admit("k", turns) });
|
|
371
|
+
// Interleaved drain: the oldest live task exits before the next admit,
|
|
372
|
+
// exactly the continuous enqueue/dequeue shape.
|
|
373
|
+
if (live.length > 2) {
|
|
374
|
+
const oldest = live.shift();
|
|
375
|
+
assert.ok(oldest, "oldest live entry present");
|
|
376
|
+
registry.release(oldest.node);
|
|
377
|
+
}
|
|
378
|
+
const stats = registry.stats();
|
|
379
|
+
if (stats.queuedTotalRefs > peakQueuedTotalRefs) peakQueuedTotalRefs = stats.queuedTotalRefs;
|
|
380
|
+
}
|
|
381
|
+
// Drain everything.
|
|
382
|
+
for (const entry of live) registry.release(entry.node);
|
|
383
|
+
const final = registry.stats();
|
|
384
|
+
assert.equal(final.queuedNodes, 0, "no historical nodes survive full drain");
|
|
385
|
+
assert.equal(final.queuedTailRefs, 0, "no historical reference slots survive");
|
|
386
|
+
assert.equal(final.queuedTotalRefs, 0, "no historical total survives");
|
|
387
|
+
assert.ok(peakQueuedTotalRefs > 0, "peak was observed");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("single unchanged turn, one outstanding: reachable chain depth stays bounded", async () => {
|
|
391
|
+
// Exact Codex P2 shape: 10,000 cycles of one-unchanged-turn admissions
|
|
392
|
+
// with exactly one outstanding node (admit, then release the previous).
|
|
393
|
+
// Every admission against an equivalent base creates a tail=[] node
|
|
394
|
+
// chained onto the previous one: reachable depth grew to 10,000 while
|
|
395
|
+
// stats reported nodes=1 / tailRefs=0.
|
|
396
|
+
const registry = new ExtractionSnapshotSharingRegistry();
|
|
397
|
+
const cycles = 10_000;
|
|
398
|
+
const turn: BufferTurn = makeTurn("unchanged");
|
|
399
|
+
let previous: SnapshotChain | undefined;
|
|
400
|
+
let maxReachableDepth = 0;
|
|
401
|
+
for (let cycle = 1; cycle <= cycles; cycle += 1) {
|
|
402
|
+
const node = registry.admit("k", [turn]);
|
|
403
|
+
if (previous) registry.release(previous);
|
|
404
|
+
previous = node;
|
|
405
|
+
let depth = 0;
|
|
406
|
+
for (let walk: SnapshotChain | undefined = node; walk; walk = walk.base) depth += 1;
|
|
407
|
+
if (depth > maxReachableDepth) maxReachableDepth = depth;
|
|
408
|
+
}
|
|
409
|
+
assert.equal(maxReachableDepth, 1, "identical windows reuse the same chain node: depth stays 1");
|
|
410
|
+
const stats = registry.stats();
|
|
411
|
+
// The final release may or may not remove the shared node depending on
|
|
412
|
+
// which sharer exited last; either way at most one bounded node remains.
|
|
413
|
+
assert.ok(stats.queuedNodes <= 1, "at most one bounded node remains");
|
|
414
|
+
assert.ok(stats.queuedTailRefs <= 1, "truthful distinct reference count");
|
|
415
|
+
assert.ok(stats.queuedTotalRefs <= 1, "truthful total");
|
|
416
|
+
assert.ok(stats.queuedReachableDepth <= 1, "truthful reachable depth");
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("overlapping identical tasks keep the shared node until the last holder releases", async () => {
|
|
420
|
+
// Main follow-up to the Codex P2 fix: node reuse must keep registry
|
|
421
|
+
// accounting truthful while ANY sharer is still outstanding — the first
|
|
422
|
+
// completion may not drop the only bucket entry, and later identical
|
|
423
|
+
// admissions must keep aliasing onto the shared node.
|
|
424
|
+
const registry = new ExtractionSnapshotSharingRegistry();
|
|
425
|
+
const turn: BufferTurn = makeTurn("shared window");
|
|
426
|
+
const a = registry.admit("k", [turn]);
|
|
427
|
+
const b = registry.admit("k", [turn]);
|
|
428
|
+
const c = registry.admit("k", [turn]);
|
|
429
|
+
assert.ok(a === b && b === c, "three overlapping identical tasks share one node");
|
|
430
|
+
assert.equal(registry.stats().queuedNodes, 1);
|
|
431
|
+
registry.release(a);
|
|
432
|
+
// RED on current code: the first release must not empty the registry while
|
|
433
|
+
// b and c still hold the node.
|
|
434
|
+
let stats = registry.stats();
|
|
435
|
+
assert.equal(stats.queuedNodes, 1, "shared node survives until the last holder releases");
|
|
436
|
+
assert.equal(stats.queuedTailRefs, 1, "content still counted while sharers remain");
|
|
437
|
+
assert.equal(stats.queuedHolders, 2, "per-task holder count tracks outstanding sharers");
|
|
438
|
+
assert.equal(stats.unsharedEquivalentRefs, 2, "unshared equivalent: 2 tasks x 1-turn chain");
|
|
439
|
+
assert.equal(stats.queuedReachableDepth, 1);
|
|
440
|
+
const d = registry.admit("k", [turn]);
|
|
441
|
+
assert.equal(d, b, "later identical admission keeps aliasing onto the shared node");
|
|
442
|
+
registry.release(b);
|
|
443
|
+
stats = registry.stats();
|
|
444
|
+
assert.equal(stats.queuedNodes, 1, "c and d still hold the shared node");
|
|
445
|
+
registry.release(c);
|
|
446
|
+
registry.release(d);
|
|
447
|
+
stats = registry.stats();
|
|
448
|
+
assert.equal(stats.queuedNodes, 0, "registry empties when the last holder releases");
|
|
449
|
+
assert.equal(stats.queuedTailRefs, 0);
|
|
450
|
+
assert.equal(stats.queuedTotalRefs, 0);
|
|
451
|
+
assert.equal(stats.queuedHolders, 0);
|
|
452
|
+
assert.equal(stats.unsharedEquivalentRefs, 0);
|
|
453
|
+
assert.equal(stats.queuedReachableDepth, 0);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("refusal path releases exactly once: registry stays balanced for later work", async () => {
|
|
457
|
+
const h = makeHarness();
|
|
458
|
+
h.queue.stopAccepting();
|
|
459
|
+
await h.coordinator.queueBufferedExtraction([makeTurn("refused")], "trigger_mode", {
|
|
460
|
+
bufferKey: "k",
|
|
461
|
+
preemptable: true,
|
|
462
|
+
});
|
|
463
|
+
// The refusal branch releases the node before any closure existed; the
|
|
464
|
+
// accounting must be back to zero with no phantom holder, or the next
|
|
465
|
+
// admission cycle would drift.
|
|
466
|
+
assert.deepEqual(h.coordinator.snapshotSharing.stats(), {
|
|
467
|
+
queuedNodes: 0,
|
|
468
|
+
queuedTotalRefs: 0,
|
|
469
|
+
queuedTailRefs: 0,
|
|
470
|
+
queuedReachableDepth: 0,
|
|
471
|
+
queuedHolders: 0,
|
|
472
|
+
unsharedEquivalentRefs: 0,
|
|
473
|
+
});
|
|
474
|
+
h.queue.resumeAccepting();
|
|
475
|
+
await h.coordinator.queueBufferedExtraction([makeTurn("after refusal")], "trigger_mode", {
|
|
476
|
+
bufferKey: "k",
|
|
477
|
+
preemptable: true,
|
|
478
|
+
});
|
|
479
|
+
const stats = h.coordinator.snapshotSharing.stats();
|
|
480
|
+
assert.equal(stats.queuedNodes, 1, "post-refusal admission registers normally");
|
|
481
|
+
assert.equal(stats.queuedHolders, 1);
|
|
482
|
+
await h.release();
|
|
483
|
+
assert.equal(h.coordinator.snapshotSharing.stats().queuedHolders, 0);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("stats report per-task holders and unshared equivalent separately from nodes", async () => {
|
|
487
|
+
const registry = new ExtractionSnapshotSharingRegistry();
|
|
488
|
+
const turns: BufferTurn[] = [makeTurn("a"), makeTurn("b")];
|
|
489
|
+
registry.admit("k", turns);
|
|
490
|
+
registry.admit("k", turns);
|
|
491
|
+
registry.admit("k", turns);
|
|
492
|
+
const stats = registry.stats();
|
|
493
|
+
assert.equal(stats.queuedNodes, 1, "one distinct chain");
|
|
494
|
+
assert.equal(stats.queuedHolders, 3, "three outstanding tasks");
|
|
495
|
+
assert.equal(stats.queuedTotalRefs, 2, "chain content slots");
|
|
496
|
+
assert.equal(stats.unsharedEquivalentRefs, 6, "3 tasks x 2-turn chain without sharing");
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("released ancestors reachable via base are counted exactly once", async () => {
|
|
500
|
+
// Main stats-defect shape: admit [a], admit [a,b], release the first node.
|
|
501
|
+
// The first node is out of the bucket but still reachable through the
|
|
502
|
+
// second node's base, so both turn references are retained while only one
|
|
503
|
+
// direct tail remains — bucket-node sums under-reported this (tailRefs=1).
|
|
504
|
+
const registry = new ExtractionSnapshotSharingRegistry();
|
|
505
|
+
const a = registry.admit("k", [makeTurn("a")]);
|
|
506
|
+
const b = registry.admit("k", [makeTurn("a"), makeTurn("b")]);
|
|
507
|
+
registry.release(a);
|
|
508
|
+
const stats = registry.stats();
|
|
509
|
+
assert.equal(stats.queuedNodes, 1, "one registered node");
|
|
510
|
+
assert.equal(stats.queuedTailRefs, 2, "both reachable turn references counted");
|
|
511
|
+
assert.equal(stats.queuedReachableDepth, 2, "released ancestor still reachable via base");
|
|
512
|
+
assert.equal(stats.queuedHolders, 1, "one outstanding task");
|
|
513
|
+
assert.equal(stats.unsharedEquivalentRefs, 2, "1 task x 2-turn window");
|
|
514
|
+
assert.equal(stats.queuedTotalRefs, 2, "per-node window basis");
|
|
515
|
+
// Sibling nodes sharing a base chain: shared ancestors count once in the
|
|
516
|
+
// reachable tail set but once per node in the per-task window basis.
|
|
517
|
+
const c = registry.admit("k", [makeTurn("a"), makeTurn("b"), makeTurn("c")]);
|
|
518
|
+
assert.equal(c.base, b, "chains onto the queued sibling");
|
|
519
|
+
const siblingStats = registry.stats();
|
|
520
|
+
assert.equal(siblingStats.queuedTailRefs, 3, "shared ancestors counted once");
|
|
521
|
+
assert.equal(siblingStats.unsharedEquivalentRefs, 5, "2 + 3 per-task windows");
|
|
522
|
+
registry.release(b);
|
|
523
|
+
registry.release(c);
|
|
524
|
+
assert.equal(registry.stats().queuedTailRefs, 0, "all reachable slots released with the chain");
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("10,000-deep queued chain materializes without stack overflow", async () => {
|
|
528
|
+
const registry = new ExtractionSnapshotSharingRegistry();
|
|
529
|
+
const depth = 10_000;
|
|
530
|
+
let last: ReturnType<ExtractionSnapshotSharingRegistry["admit"]> | undefined;
|
|
531
|
+
// One shared array grown in place mirrors the real pump: every trigger
|
|
532
|
+
// hands out a full prefix snapshot of the same buffer.
|
|
533
|
+
const turns: BufferTurn[] = [];
|
|
534
|
+
for (let index = 1; index <= depth; index += 1) {
|
|
535
|
+
turns.push(makeTurn(`deep ${index}`));
|
|
536
|
+
last = registry.admit("k", turns);
|
|
537
|
+
}
|
|
538
|
+
const stats = registry.stats();
|
|
539
|
+
assert.equal(stats.queuedNodes, depth, "one node per trigger");
|
|
540
|
+
assert.equal(stats.queuedTailRefs, depth, "shared suffix refs equal the union");
|
|
541
|
+
assert.ok(last, "deepest node recorded");
|
|
542
|
+
const materialized = materializeChain(last);
|
|
543
|
+
assert.equal(materialized.length, depth);
|
|
544
|
+
assert.equal(materialized[0]?.content, "deep 1");
|
|
545
|
+
assert.equal(materialized[depth - 1]?.content, `deep ${depth}`);
|
|
546
|
+
turns.push(makeTurn(`deep ${depth + 1}`));
|
|
547
|
+
assert.ok(last && chainPrefixEquivalent(last, turns), "prefix equivalence at depth");
|
|
548
|
+
});
|