@remnic/core 9.40.0 → 9.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/access-admin-ops-surface.d.ts +10 -10
- package/dist/access-admin-ops-surface.js +5 -4
- package/dist/access-authorization-probe.d.ts +10 -10
- package/dist/access-authorization-probe.js +6 -5
- package/dist/access-boundary.d.ts +10 -10
- package/dist/access-boundary.js +5 -4
- package/dist/access-cli.js +16 -15
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.d.ts +10 -10
- package/dist/access-extraction-force-flush.js +5 -4
- package/dist/access-health-types.d.ts +1 -1
- package/dist/access-http-lcm-compaction.d.ts +10 -10
- package/dist/access-http-lifecycle-flush.d.ts +10 -10
- package/dist/access-http.d.ts +10 -10
- package/dist/access-http.js +10 -9
- package/dist/access-identity-continuity-surface.d.ts +8 -8
- package/dist/access-identity-continuity-surface.js +5 -4
- package/dist/access-lcm-surface.d.ts +10 -10
- package/dist/access-lcm-surface.js +5 -4
- package/dist/access-mcp.d.ts +10 -10
- package/dist/access-mcp.js +8 -7
- package/dist/access-memory-search-fanout.d.ts +2 -2
- package/dist/{access-namespace-preflight-BbDJBq0e.d.ts → access-namespace-preflight-47dOyty9.d.ts} +1 -1
- package/dist/access-namespace-preflight.d.ts +3 -3
- package/dist/access-namespace-preflight.js +5 -4
- package/dist/access-observe-write-surface.d.ts +10 -10
- package/dist/access-observe-write-surface.js +5 -4
- package/dist/access-operations-batch.js +6 -5
- package/dist/access-operations.d.ts +10 -10
- package/dist/access-operations.js +7 -6
- package/dist/access-recall-concurrency.d.ts +10 -10
- package/dist/access-recall-concurrency.js +5 -4
- package/dist/access-recall-response.d.ts +10 -10
- package/dist/access-recall-response.js +5 -4
- package/dist/access-recall-surface.d.ts +10 -10
- package/dist/access-recall-surface.js +5 -4
- package/dist/{access-service-B2h6rITp.d.ts → access-service-D1BkxwSf.d.ts} +7 -7
- package/dist/access-service-helpers.d.ts +10 -10
- package/dist/access-service.d.ts +10 -10
- package/dist/access-service.js +5 -4
- package/dist/access-surface-catalog.d.ts +10 -10
- package/dist/access-wearables-meetings-surface.d.ts +3 -3
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/active-recall.js +2 -2
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +8 -8
- package/dist/briefing.d.ts +2 -2
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer-turn-helpers.d.ts +1 -1
- package/dist/buffer.d.ts +2 -2
- package/dist/bulk-import/index.d.ts +3 -3
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-Bz2LxGNb.d.ts → catalog-DPQynQZG.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-trajectory-graph.d.ts +1 -1
- package/dist/{chunk-443XCR34.js → chunk-3SG4FGDV.js} +53 -41
- package/dist/chunk-3SG4FGDV.js.map +1 -0
- package/dist/{chunk-XPQ5ZTL6.js → chunk-6I2C3EOH.js} +6 -6
- package/dist/chunk-7VIB7PAE.js +142 -0
- package/dist/chunk-7VIB7PAE.js.map +1 -0
- package/dist/{chunk-AYICY7IE.js → chunk-AXUU7SG3.js} +2 -2
- package/dist/{chunk-RK6WMGTX.js → chunk-CQ5MR2HQ.js} +13 -9
- package/dist/chunk-CQ5MR2HQ.js.map +1 -0
- package/dist/{chunk-KW4ZFTUS.js → chunk-CWPDM66T.js} +5 -5
- package/dist/{chunk-NEO4QCID.js → chunk-FHEJ22B7.js} +10 -4
- package/dist/chunk-FHEJ22B7.js.map +1 -0
- package/dist/{chunk-EBWOOJEP.js → chunk-GDC4INBF.js} +2 -2
- package/dist/{chunk-UIVRPXRR.js → chunk-KXGN6G2Y.js} +3 -3
- package/dist/chunk-KXGN6G2Y.js.map +1 -0
- package/dist/{chunk-AXHMCRS3.js → chunk-OYFDEJZB.js} +23 -6
- package/dist/chunk-OYFDEJZB.js.map +1 -0
- package/dist/{chunk-SD4A6IZY.js → chunk-PWKZV4A3.js} +2 -2
- package/dist/{chunk-XYXYEDPL.js → chunk-Q4XL7LJW.js} +26 -5
- package/dist/chunk-Q4XL7LJW.js.map +1 -0
- package/dist/{chunk-MOHZIKZM.js → chunk-TM72UOX2.js} +2 -2
- package/dist/{chunk-GLBK35BT.js → chunk-UGCUVLOL.js} +14 -10
- package/dist/chunk-UGCUVLOL.js.map +1 -0
- package/dist/{chunk-Q6QYN56R.js → chunk-UQVGDEAJ.js} +2 -2
- package/dist/{chunk-KYYRJVE6.js → chunk-XRWVXPEM.js} +2 -2
- package/dist/{cli-CO8qKPM5.d.ts → cli-IUhV-lmk.d.ts} +5 -5
- package/dist/cli.d.ts +12 -12
- package/dist/cli.js +15 -14
- package/dist/compounding/engine.d.ts +2 -2
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -2
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/consolidation-provenance-check.d.ts +2 -2
- package/dist/consolidation-undo.d.ts +2 -2
- package/dist/contradiction/index.d.ts +2 -2
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/corpus-watermark.d.ts +7 -1
- package/dist/corpus-watermark.js +2 -2
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +2 -2
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +8 -8
- package/dist/extraction-error-classification.d.ts +1 -1
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction-liveness.d.ts +1 -1
- package/dist/extraction-liveness.js +3 -1
- package/dist/extraction-source-grounding-rules.d.ts +1 -1
- package/dist/extraction-source-grounding.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/graph-dashboard-diff.d.ts +1 -1
- package/dist/graph-dashboard-key.d.ts +1 -1
- package/dist/graph-dashboard-parser.d.ts +1 -1
- package/dist/graph-edge-reinforcement.d.ts +1 -1
- package/dist/graph-snapshot.d.ts +1 -1
- package/dist/graph.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/importers/index.d.ts +1 -1
- package/dist/in-flight-reads.d.ts +1 -1
- package/dist/index.d.ts +19 -19
- package/dist/index.js +16 -15
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/local-model-endpoint.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -2
- package/dist/maintenance/rebuild-memory-projection.d.ts +2 -2
- package/dist/{maintenance-D2jkiT9r.d.ts → maintenance-DVBLXkFw.d.ts} +3 -3
- package/dist/mcp-memory-inspector-app.d.ts +10 -10
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +2 -2
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +4 -3
- package/dist/namespaces/migrate.js +1 -1
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +3 -3
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/offline-sync-impression-drain.d.ts +1 -1
- package/dist/operator-doctor-corpus.d.ts +1 -1
- package/dist/operator-doctor-corpus.js +3 -3
- package/dist/operator-doctor-replica.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +5 -4
- package/dist/operator-toolkit.js +7 -6
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -2
- package/dist/orchestration/maintenance.d.ts +4 -4
- package/dist/{orchestrator-SHaRXCUF.d.ts → orchestrator-fYTncnLy.d.ts} +7 -7
- package/dist/orchestrator.d.ts +8 -8
- package/dist/orchestrator.js +16 -15
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/{pipeline-B5_tS1Yc.d.ts → pipeline-Cs76XC-p.d.ts} +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/proactive-contention.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/{qmd-CkEV_SPG.d.ts → qmd-WJwvBgiF.d.ts} +1 -1
- package/dist/qmd-preflight.d.ts +2 -2
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +2 -2
- package/dist/recall-concurrency-config.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-memory-map.d.ts +1 -1
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-timings.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray.d.ts +1 -1
- package/dist/replay/normalizers/chatgpt.d.ts +1 -1
- package/dist/replay/normalizers/claude.d.ts +1 -1
- package/dist/replay/normalizers/openclaw.d.ts +1 -1
- package/dist/replay/normalizers/shared.d.ts +1 -1
- package/dist/replay/runner.d.ts +1 -1
- package/dist/replay/types.d.ts +1 -1
- package/dist/replica-divergence.d.ts +1 -1
- package/dist/replica-peers-config.d.ts +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +3 -3
- package/dist/retrieval-agents.d.ts +2 -2
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/salvage-envelope.d.ts +1 -1
- package/dist/{scope-profiles-DeAdnocS.d.ts → scope-profiles-CQ0NAleS.d.ts} +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-DR_h-JUD.d.ts → semantic-consolidation-BWxOBMtz.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/{service-BTb94UPJ.d.ts → service-CLuMO-pa.d.ts} +2 -2
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/source-agent-qualifier.d.ts +1 -1
- package/dist/{storage-ax2EIsVS.d.ts → storage-Blr71GpS.d.ts} +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +2 -2
- package/dist/temporal-timeline-recall.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +2 -2
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/trust-score-stage.d.ts +1 -1
- package/dist/trust-score.d.ts +1 -1
- package/dist/{types-CCGL-9NX.d.ts → types-CqWwMPr_.d.ts} +41 -59
- package/dist/types.d.ts +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/write-envelope.d.ts +1 -1
- package/package.json +2 -2
- package/src/access-http.test.ts +2 -2
- package/src/access-service-health.test.ts +39 -17
- package/src/access-service.ts +25 -3
- package/src/cli.ts +3 -3
- package/src/corpus-watermark.test.ts +16 -0
- package/src/corpus-watermark.ts +28 -5
- package/src/extraction-liveness.test.ts +81 -19
- package/src/extraction-liveness.ts +96 -98
- package/src/extraction-watermark.test.ts +290 -0
- package/src/namespaces/migrate.ts +10 -2
- package/src/operator-doctor-corpus.test.ts +7 -0
- package/src/operator-doctor-corpus.ts +1 -1
- package/src/operator-toolkit.ts +8 -8
- package/src/orchestration/extraction-watermark.ts +188 -0
- package/dist/chunk-443XCR34.js.map +0 -1
- package/dist/chunk-AXHMCRS3.js.map +0 -1
- package/dist/chunk-GLBK35BT.js.map +0 -1
- package/dist/chunk-NEO4QCID.js.map +0 -1
- package/dist/chunk-RK6WMGTX.js.map +0 -1
- package/dist/chunk-UIVRPXRR.js.map +0 -1
- package/dist/chunk-XYXYEDPL.js.map +0 -1
- /package/dist/{chunk-XPQ5ZTL6.js.map → chunk-6I2C3EOH.js.map} +0 -0
- /package/dist/{chunk-AYICY7IE.js.map → chunk-AXUU7SG3.js.map} +0 -0
- /package/dist/{chunk-KW4ZFTUS.js.map → chunk-CWPDM66T.js.map} +0 -0
- /package/dist/{chunk-EBWOOJEP.js.map → chunk-GDC4INBF.js.map} +0 -0
- /package/dist/{chunk-SD4A6IZY.js.map → chunk-PWKZV4A3.js.map} +0 -0
- /package/dist/{chunk-MOHZIKZM.js.map → chunk-TM72UOX2.js.map} +0 -0
- /package/dist/{chunk-Q6QYN56R.js.map → chunk-UQVGDEAJ.js.map} +0 -0
- /package/dist/{chunk-KYYRJVE6.js.map → chunk-XRWVXPEM.js.map} +0 -0
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { coerceBool, coerceNumber } from "./connectors/coerce.js";
|
|
22
22
|
import { log } from "./logger.js";
|
|
23
23
|
import type { OperatorDoctorCheck } from "./operator-doctor-types.js";
|
|
24
|
-
import type {
|
|
24
|
+
import type { PluginConfig } from "./types.js";
|
|
25
25
|
|
|
26
26
|
/** Default staleness window: 24h. A watermark older than this with a non-empty buffer is degraded. */
|
|
27
27
|
const DEFAULT_STALE_WINDOW_MS = 86_400_000;
|
|
@@ -60,23 +60,57 @@ export interface ExtractionBufferSource {
|
|
|
60
60
|
getBufferSnapshot(): Promise<ExtractionBufferSnapshot>;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
export interface ExtractionRootStats {
|
|
64
|
+
extractionCount?: number;
|
|
65
|
+
lastConsolidationAt?: string | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ExtractionWatermarkRead {
|
|
69
|
+
lastExtractionAt: string | null;
|
|
70
|
+
readFailed: boolean;
|
|
71
|
+
readError?: string;
|
|
72
|
+
pending?: boolean;
|
|
73
|
+
rootStats?: ExtractionRootStats;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type ExtractionWatermarkOrStorage =
|
|
77
|
+
| ExtractionWatermarkRead
|
|
78
|
+
| { readMetadata(): Promise<{ lastExtractionAt?: string | null } | null> }
|
|
79
|
+
| { loadMeta(): Promise<{ lastExtractionAt?: string | null }> };
|
|
80
|
+
|
|
81
|
+
export async function coerceExtractionWatermark(
|
|
82
|
+
watermark: ExtractionWatermarkOrStorage,
|
|
83
|
+
): Promise<ExtractionWatermarkRead> {
|
|
84
|
+
if (watermark && "readFailed" in watermark && typeof watermark.readFailed === "boolean") {
|
|
85
|
+
return watermark as ExtractionWatermarkRead;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
let lastExtractionAt: string | null = null;
|
|
89
|
+
if ("loadMeta" in watermark && typeof watermark.loadMeta === "function") {
|
|
90
|
+
const meta = await watermark.loadMeta();
|
|
91
|
+
lastExtractionAt = meta?.lastExtractionAt ?? null;
|
|
92
|
+
} else if ("readMetadata" in watermark && typeof watermark.readMetadata === "function") {
|
|
93
|
+
const meta = await watermark.readMetadata();
|
|
94
|
+
lastExtractionAt = meta?.lastExtractionAt ?? null;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
lastExtractionAt,
|
|
98
|
+
readFailed: true,
|
|
99
|
+
readError: "aggregate watermark unavailable from legacy root-store-only storage argument",
|
|
100
|
+
};
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return {
|
|
103
|
+
lastExtractionAt: null,
|
|
104
|
+
readFailed: true,
|
|
105
|
+
readError: error instanceof Error ? error.message : String(error),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
63
110
|
/**
|
|
64
|
-
* The liveness payload surfaced on `/health.extraction` and in
|
|
65
|
-
* details.
|
|
66
|
-
*
|
|
67
|
-
* DAEMON-SCOPED SIGNAL: a daemon exposes a single extraction verdict; the
|
|
68
|
-
* last-extraction watermark is read from the daemon's default/root store, so
|
|
69
|
-
* `/health` returns the same `extraction` block for every namespace argument.
|
|
70
|
-
*
|
|
71
|
-
* KNOWN LIMITATION (issue #2159): extraction stamps `lastExtractionAt` on the
|
|
72
|
-
* PER-NAMESPACE store (`storageFor(selfNamespace)`), so this watermark reflects
|
|
73
|
-
* only extraction that stamped the root store (default-namespace work). On a
|
|
74
|
-
* single-namespace deployment (the common case) the watermark and buffer share
|
|
75
|
-
* one scope and the verdict is exact. On a multi-namespace deployment the verdict
|
|
76
|
-
* is NOT guaranteed correct for work isolated to a non-default namespace: it can
|
|
77
|
-
* over-report `degraded` (the default namespace idle while another is extracting)
|
|
78
|
-
* or miss a stall confined to a non-default namespace. Aggregating the watermark
|
|
79
|
-
* across namespace stores is tracked in #2159.
|
|
111
|
+
* The daemon-scoped liveness payload surfaced on `/health.extraction` and in
|
|
112
|
+
* doctor/stats details. Its watermark is the maximum successful extraction
|
|
113
|
+
* across every distinct namespace store, matching the daemon-wide buffer scope.
|
|
80
114
|
*/
|
|
81
115
|
export interface ExtractionLivenessStatus {
|
|
82
116
|
lastExtractionAt: string | null;
|
|
@@ -85,18 +119,8 @@ export interface ExtractionLivenessStatus {
|
|
|
85
119
|
oldestBufferedTurnAgeMs: number | null;
|
|
86
120
|
degraded: boolean;
|
|
87
121
|
degradedReason: string | null;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
* daemon's default/root store only), `aggregate` once it is resolved across
|
|
91
|
-
* every namespace store (#2159). A monitor can alert on
|
|
92
|
-
* `watermarkScope !== "aggregate"` while namespaces are enabled instead of
|
|
93
|
-
* trusting a number that may be wrong in either direction.
|
|
94
|
-
*/
|
|
95
|
-
watermarkScope: "root-store" | "aggregate";
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
interface ExtractionLivenessStorageLike {
|
|
99
|
-
loadMeta(): Promise<Pick<MetaState, "lastExtractionAt">>;
|
|
122
|
+
watermarkPending?: boolean;
|
|
123
|
+
watermarkScope: "aggregate";
|
|
100
124
|
}
|
|
101
125
|
|
|
102
126
|
interface ExtractionLivenessOrchestratorLike {
|
|
@@ -190,7 +214,7 @@ export function evaluateExtractionLiveness(input: {
|
|
|
190
214
|
nowMs: number;
|
|
191
215
|
metaReadFailed?: boolean;
|
|
192
216
|
metaReadError?: string;
|
|
193
|
-
|
|
217
|
+
watermarkPending?: boolean;
|
|
194
218
|
}): ExtractionLivenessStatus {
|
|
195
219
|
const { config, lastExtractionAt, snapshot, nowMs } = input;
|
|
196
220
|
const lastMs = lastExtractionAt !== null ? Date.parse(lastExtractionAt) : Number.NaN;
|
|
@@ -204,8 +228,10 @@ export function evaluateExtractionLiveness(input: {
|
|
|
204
228
|
// (§22): a storage/pipeline outage must not read as healthy, and must stay
|
|
205
229
|
// distinct from a pipeline that has genuinely never extracted.
|
|
206
230
|
const metaReadFailed = input.metaReadFailed === true;
|
|
231
|
+
const watermarkPending = input.watermarkPending === true;
|
|
207
232
|
const readFailed = snapshot.readFailed === true;
|
|
208
|
-
const degraded =
|
|
233
|
+
const degraded =
|
|
234
|
+
config.enabled && (metaReadFailed || readFailed || (!watermarkPending && hasBacklog && stale));
|
|
209
235
|
|
|
210
236
|
let degradedReason: string | null = null;
|
|
211
237
|
if (degraded) {
|
|
@@ -230,7 +256,8 @@ export function evaluateExtractionLiveness(input: {
|
|
|
230
256
|
oldestBufferedTurnAgeMs: ageMsFrom(snapshot.oldestTurnTimestamp, nowMs),
|
|
231
257
|
degraded,
|
|
232
258
|
degradedReason,
|
|
233
|
-
|
|
259
|
+
...(watermarkPending ? { watermarkPending: true } : {}),
|
|
260
|
+
watermarkScope: "aggregate",
|
|
234
261
|
};
|
|
235
262
|
}
|
|
236
263
|
|
|
@@ -262,40 +289,27 @@ async function readBufferSnapshot(
|
|
|
262
289
|
}
|
|
263
290
|
|
|
264
291
|
/**
|
|
265
|
-
* Gather the liveness status from
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
* swallowed into a healthy-looking verdict; no failure path throws.
|
|
292
|
+
* Gather the liveness status from an aggregate watermark read and an optional
|
|
293
|
+
* buffer. A read failure on either side is surfaced as a degraded status with a
|
|
294
|
+
* distinct reason rather than swallowed into a healthy-looking verdict.
|
|
269
295
|
*/
|
|
270
296
|
export async function gatherExtractionLivenessStatus(input: {
|
|
271
297
|
config: ExtractionLivenessConfig;
|
|
272
|
-
|
|
298
|
+
watermark: ExtractionWatermarkOrStorage;
|
|
273
299
|
buffer: ExtractionBufferSource | undefined;
|
|
274
300
|
nowMs: number;
|
|
275
301
|
}): Promise<ExtractionLivenessStatus> {
|
|
276
|
-
const { config,
|
|
277
|
-
|
|
278
|
-
let metaReadFailed = false;
|
|
279
|
-
let metaReadError: string | undefined;
|
|
280
|
-
try {
|
|
281
|
-
lastExtractionAt = (await storage.loadMeta()).lastExtractionAt ?? null;
|
|
282
|
-
} catch (err) {
|
|
283
|
-
// A swallowed loadMeta() failure that reads as "never extracted" is the
|
|
284
|
-
// exact §22 conflation this feature exists to kill — surface it explicitly.
|
|
285
|
-
metaReadFailed = true;
|
|
286
|
-
metaReadError = err instanceof Error ? err.message : String(err);
|
|
287
|
-
}
|
|
302
|
+
const { config, buffer, nowMs } = input;
|
|
303
|
+
const watermarkRead = await coerceExtractionWatermark(input.watermark);
|
|
288
304
|
const snapshot = await readBufferSnapshot(buffer);
|
|
289
305
|
return evaluateExtractionLiveness({
|
|
290
306
|
config,
|
|
291
|
-
lastExtractionAt,
|
|
307
|
+
lastExtractionAt: watermarkRead.lastExtractionAt,
|
|
292
308
|
snapshot,
|
|
293
309
|
nowMs,
|
|
294
|
-
metaReadFailed,
|
|
295
|
-
metaReadError,
|
|
296
|
-
|
|
297
|
-
// "aggregate" when the resolver reads across every namespace store.
|
|
298
|
-
watermarkScope: "root-store",
|
|
310
|
+
metaReadFailed: watermarkRead.readFailed,
|
|
311
|
+
metaReadError: watermarkRead.readError,
|
|
312
|
+
watermarkPending: watermarkRead.pending,
|
|
299
313
|
});
|
|
300
314
|
}
|
|
301
315
|
|
|
@@ -349,25 +363,19 @@ export function resolveExtractionLivenessConfig(
|
|
|
349
363
|
}
|
|
350
364
|
|
|
351
365
|
/**
|
|
352
|
-
* Compute the liveness status for the authenticated `/health`
|
|
353
|
-
* the throttled
|
|
354
|
-
*
|
|
355
|
-
* The watermark is read from the daemon's default/root store (`orchestrator.storage`),
|
|
356
|
-
* which is why `/health` returns the same `extraction` block for any namespace
|
|
357
|
-
* argument. KNOWN LIMITATION (issue #2159): extraction writes `lastExtractionAt`
|
|
358
|
-
* per-namespace, so this watermark reflects only default-namespace extraction and
|
|
359
|
-
* the verdict is exact only for a single-namespace daemon (see
|
|
360
|
-
* `ExtractionLivenessStatus`).
|
|
366
|
+
* Compute the daemon-wide liveness status for the authenticated `/health`
|
|
367
|
+
* payload and emit the throttled aggregate warning.
|
|
361
368
|
*/
|
|
362
369
|
export async function computeExtractionLivenessStatus(
|
|
363
|
-
orchestrator: ExtractionLivenessOrchestratorLike
|
|
370
|
+
orchestrator: ExtractionLivenessOrchestratorLike,
|
|
371
|
+
watermark: ExtractionWatermarkOrStorage,
|
|
364
372
|
throttle?: ExtractionLivenessWarnThrottle,
|
|
365
373
|
nowMs: number = Date.now(),
|
|
366
374
|
): Promise<ExtractionLivenessStatus> {
|
|
367
375
|
const config = resolveExtractionLivenessConfig(orchestrator.config.extractionLiveness);
|
|
368
376
|
const status = await gatherExtractionLivenessStatus({
|
|
369
377
|
config,
|
|
370
|
-
|
|
378
|
+
watermark,
|
|
371
379
|
buffer: orchestrator.buffer,
|
|
372
380
|
nowMs,
|
|
373
381
|
});
|
|
@@ -381,22 +389,18 @@ export async function computeExtractionLivenessStatus(
|
|
|
381
389
|
*/
|
|
382
390
|
export async function summarizeExtractionLiveness(
|
|
383
391
|
config: Pick<PluginConfig, "extractionLiveness">,
|
|
384
|
-
|
|
392
|
+
watermark: ExtractionWatermarkOrStorage,
|
|
385
393
|
buffer?: ExtractionBufferSource,
|
|
386
394
|
nowMs: number = Date.now(),
|
|
387
395
|
): Promise<OperatorDoctorCheck> {
|
|
388
396
|
const livenessConfig = resolveExtractionLivenessConfig(config.extractionLiveness);
|
|
389
397
|
const status = await gatherExtractionLivenessStatus({
|
|
390
398
|
config: livenessConfig,
|
|
391
|
-
|
|
399
|
+
watermark,
|
|
392
400
|
buffer,
|
|
393
401
|
nowMs,
|
|
394
402
|
});
|
|
395
|
-
const scopeNote =
|
|
396
|
-
` Watermark scope: ${status.watermarkScope}` +
|
|
397
|
-
(status.watermarkScope === "root-store"
|
|
398
|
-
? " (default-namespace only; multi-namespace aggregation tracked in #2159)."
|
|
399
|
-
: ".");
|
|
403
|
+
const scopeNote = ` Watermark scope: ${status.watermarkScope}.`;
|
|
400
404
|
const summary =
|
|
401
405
|
(status.degraded
|
|
402
406
|
? `Extraction pipeline degraded: ${status.degradedReason}.`
|
|
@@ -416,44 +420,38 @@ export async function summarizeExtractionLiveness(
|
|
|
416
420
|
};
|
|
417
421
|
}
|
|
418
422
|
|
|
419
|
-
/** Meta fields the `remnic stats` view renders directly, beyond the liveness watermark. */
|
|
420
|
-
type ExtractionStatsMeta = Pick<MetaState, "extractionCount" | "lastExtractionAt" | "lastConsolidationAt">;
|
|
421
|
-
|
|
422
423
|
/**
|
|
423
|
-
* Lines for `remnic stats`: extraction watermark,
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
* crashing the whole `stats` command.
|
|
424
|
+
* Lines for `remnic stats`: the aggregate extraction watermark, root metadata
|
|
425
|
+
* counters, buffer backlog, and liveness verdict. Root counter failures render
|
|
426
|
+
* those counters unavailable; the injected aggregate read alone determines
|
|
427
|
+
* watermark liveness so stats cannot diverge from health and doctor.
|
|
428
428
|
*/
|
|
429
429
|
export async function renderExtractionLivenessStats(
|
|
430
|
-
orchestrator: ExtractionLivenessOrchestratorLike
|
|
430
|
+
orchestrator: ExtractionLivenessOrchestratorLike,
|
|
431
|
+
watermark: ExtractionWatermarkRead,
|
|
431
432
|
nowMs: number = Date.now(),
|
|
432
433
|
): Promise<string[]> {
|
|
433
|
-
let meta: ExtractionStatsMeta | null = null;
|
|
434
|
-
let metaReadFailed = false;
|
|
435
|
-
let metaReadError: string | undefined;
|
|
436
|
-
try {
|
|
437
|
-
meta = await orchestrator.storage.loadMeta();
|
|
438
|
-
} catch (err) {
|
|
439
|
-
metaReadFailed = true;
|
|
440
|
-
metaReadError = err instanceof Error ? err.message : String(err);
|
|
441
|
-
}
|
|
442
434
|
const status = evaluateExtractionLiveness({
|
|
443
435
|
config: resolveExtractionLivenessConfig(orchestrator.config.extractionLiveness),
|
|
444
|
-
lastExtractionAt:
|
|
436
|
+
lastExtractionAt: watermark.lastExtractionAt,
|
|
445
437
|
snapshot: await readBufferSnapshot(orchestrator.buffer),
|
|
446
438
|
nowMs,
|
|
447
|
-
metaReadFailed,
|
|
448
|
-
metaReadError,
|
|
439
|
+
metaReadFailed: watermark.readFailed,
|
|
440
|
+
metaReadError: watermark.readError,
|
|
441
|
+
watermarkPending: watermark.pending,
|
|
449
442
|
});
|
|
450
443
|
const oldestAge =
|
|
451
444
|
status.oldestBufferedTurnAgeMs !== null ? formatAgeMs(status.oldestBufferedTurnAgeMs) : "n/a";
|
|
452
|
-
const
|
|
445
|
+
const rootStats = watermark.rootStats;
|
|
446
|
+
const watermarkDisplay = watermark.readFailed
|
|
447
|
+
? "unavailable"
|
|
448
|
+
: watermark.pending
|
|
449
|
+
? "pending"
|
|
450
|
+
: status.lastExtractionAt ?? "never";
|
|
453
451
|
return [
|
|
454
|
-
`Extractions: ${
|
|
455
|
-
`Last extraction: ${
|
|
456
|
-
`Last consolidation: ${
|
|
452
|
+
`Extractions: ${rootStats?.extractionCount ?? "unavailable"}`,
|
|
453
|
+
`Last extraction: ${watermarkDisplay}`,
|
|
454
|
+
`Last consolidation: ${rootStats ? rootStats.lastConsolidationAt ?? "never" : "unavailable"}`,
|
|
457
455
|
`Buffered sessions: ${status.bufferedSessionCount} (${status.pendingTurnCount} turns pending)`,
|
|
458
456
|
`Oldest buffered turn age: ${oldestAge}`,
|
|
459
457
|
`Extraction watermark scope: ${status.watermarkScope}`,
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import { TOKEN_CAPABILITIES_VERSION } from "./access-token-capabilities.js";
|
|
8
|
+
import { parseConfig } from "./config.js";
|
|
9
|
+
import { CorpusWatermarkCache } from "./corpus-watermark.js";
|
|
10
|
+
import { readAggregateExtractionWatermark } from "./orchestration/extraction-watermark.js";
|
|
11
|
+
import type { PluginConfig } from "./types.js";
|
|
12
|
+
|
|
13
|
+
interface FakeWatermarkStorage {
|
|
14
|
+
readonly dir: string;
|
|
15
|
+
loadMeta(): Promise<{ lastExtractionAt: string | null }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function storage(dir: string, read: () => Promise<string | null>): FakeWatermarkStorage {
|
|
19
|
+
return {
|
|
20
|
+
dir,
|
|
21
|
+
loadMeta: async () => ({ lastExtractionAt: await read() }),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function namespacedFixture(): Promise<{
|
|
26
|
+
memoryDir: string;
|
|
27
|
+
config: PluginConfig;
|
|
28
|
+
namespaceDirs: Record<string, string>;
|
|
29
|
+
}> {
|
|
30
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-extraction-watermark-"));
|
|
31
|
+
const namespaceDirs = {
|
|
32
|
+
"team-a": path.join(memoryDir, "namespaces", "team-a"),
|
|
33
|
+
"team-b": path.join(memoryDir, "namespaces", "team-b"),
|
|
34
|
+
};
|
|
35
|
+
await Promise.all(Object.values(namespaceDirs).map((dir) => mkdir(dir, { recursive: true })));
|
|
36
|
+
return {
|
|
37
|
+
memoryDir,
|
|
38
|
+
config: parseConfig({ memoryDir, namespacesEnabled: true, defaultNamespace: "default" }),
|
|
39
|
+
namespaceDirs,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test("aggregate watermark selects the most recent extraction across distinct namespace stores", async () => {
|
|
44
|
+
const fixture = await namespacedFixture();
|
|
45
|
+
try {
|
|
46
|
+
const timestamps = {
|
|
47
|
+
default: "2026-07-20T12:00:00.000Z",
|
|
48
|
+
"team-a": "2026-07-21T12:00:00.000Z",
|
|
49
|
+
"team-b": "2026-07-22T12:00:00.000Z",
|
|
50
|
+
};
|
|
51
|
+
const result = await readAggregateExtractionWatermark({
|
|
52
|
+
config: fixture.config,
|
|
53
|
+
rootStorage: storage(fixture.memoryDir, async () => timestamps.default),
|
|
54
|
+
storageForNamespace: async (namespace) =>
|
|
55
|
+
storage(fixture.namespaceDirs[namespace], async () => timestamps[namespace as keyof typeof timestamps]),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
assert.deepEqual(result, {
|
|
59
|
+
lastExtractionAt: timestamps["team-b"],
|
|
60
|
+
readFailed: false,
|
|
61
|
+
});
|
|
62
|
+
} finally {
|
|
63
|
+
await rm(fixture.memoryDir, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("a namespace-isolated extraction advances the daemon aggregate watermark", async () => {
|
|
68
|
+
const fixture = await namespacedFixture();
|
|
69
|
+
try {
|
|
70
|
+
const rootTimestamp = "2026-07-20T12:00:00.000Z";
|
|
71
|
+
let teamTimestamp = "2026-07-19T12:00:00.000Z";
|
|
72
|
+
const options = {
|
|
73
|
+
config: fixture.config,
|
|
74
|
+
rootStorage: storage(fixture.memoryDir, async () => rootTimestamp),
|
|
75
|
+
storageForNamespace: async (namespace: string) =>
|
|
76
|
+
storage(fixture.namespaceDirs[namespace], async () => (namespace === "team-a" ? teamTimestamp : null)),
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
assert.equal((await readAggregateExtractionWatermark(options)).lastExtractionAt, rootTimestamp);
|
|
80
|
+
teamTimestamp = "2026-07-23T12:00:00.000Z";
|
|
81
|
+
assert.equal((await readAggregateExtractionWatermark(options)).lastExtractionAt, teamTimestamp);
|
|
82
|
+
} finally {
|
|
83
|
+
await rm(fixture.memoryDir, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("a partial namespace read returns readFailed instead of a survivor's fresh watermark", async () => {
|
|
88
|
+
const fixture = await namespacedFixture();
|
|
89
|
+
try {
|
|
90
|
+
const result = await readAggregateExtractionWatermark({
|
|
91
|
+
config: fixture.config,
|
|
92
|
+
rootStorage: storage(fixture.memoryDir, async () => "2026-07-23T12:00:00.000Z"),
|
|
93
|
+
storageForNamespace: async (namespace) =>
|
|
94
|
+
storage(fixture.namespaceDirs[namespace], async () => {
|
|
95
|
+
if (namespace === "team-b") throw new Error("meta store unavailable");
|
|
96
|
+
return "2026-07-22T12:00:00.000Z";
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
assert.equal(result.lastExtractionAt, null, "a surviving timestamp must not be reported as complete");
|
|
101
|
+
assert.equal(result.readFailed, true);
|
|
102
|
+
assert.match(result.readError ?? "", /namespace watermark unreadable/);
|
|
103
|
+
assert.match(result.readError ?? "", /meta store unavailable/);
|
|
104
|
+
} finally {
|
|
105
|
+
await rm(fixture.memoryDir, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
test("scoped capabilities filter candidate namespaces during aggregate read", async () => {
|
|
109
|
+
const fixture = await namespacedFixture();
|
|
110
|
+
try {
|
|
111
|
+
let scannedTeamB = false;
|
|
112
|
+
const result = await readAggregateExtractionWatermark({
|
|
113
|
+
config: fixture.config,
|
|
114
|
+
rootStorage: storage(fixture.memoryDir, async () => "2026-07-20T12:00:00.000Z"),
|
|
115
|
+
storageForNamespace: async (namespace) => {
|
|
116
|
+
if (namespace === "team-b") scannedTeamB = true;
|
|
117
|
+
return storage(fixture.namespaceDirs[namespace], async () => "2026-07-25T12:00:00.000Z");
|
|
118
|
+
},
|
|
119
|
+
caps: { version: TOKEN_CAPABILITIES_VERSION, namespaces: ["team-a"] },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
assert.equal(scannedTeamB, false, "team-b should be skipped when token is restricted to team-a");
|
|
123
|
+
assert.equal(result.readFailed, false);
|
|
124
|
+
} finally {
|
|
125
|
+
await rm(fixture.memoryDir, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("default root capability check uses normalized aliases for migrated storage", async () => {
|
|
130
|
+
const fixture = await namespacedFixture();
|
|
131
|
+
try {
|
|
132
|
+
const migratedDefaultDir = path.join(fixture.memoryDir, "namespaces", "default");
|
|
133
|
+
await mkdir(migratedDefaultDir, { recursive: true });
|
|
134
|
+
const result = await readAggregateExtractionWatermark({
|
|
135
|
+
config: { ...fixture.config, defaultNamespace: " default " },
|
|
136
|
+
rootStorage: storage(fixture.memoryDir, async () => {
|
|
137
|
+
throw new Error("inactive legacy root is unreadable");
|
|
138
|
+
}),
|
|
139
|
+
storageForNamespace: async (_namespace, rootDir) =>
|
|
140
|
+
storage(rootDir, async () =>
|
|
141
|
+
path.resolve(rootDir) === path.resolve(migratedDefaultDir)
|
|
142
|
+
? "2026-07-26T12:00:00.000Z"
|
|
143
|
+
: "2026-07-24T12:00:00.000Z",
|
|
144
|
+
),
|
|
145
|
+
caps: { version: TOKEN_CAPABILITIES_VERSION, namespaces: ["default"] },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
assert.equal(result.lastExtractionAt, "2026-07-26T12:00:00.000Z");
|
|
149
|
+
assert.equal(result.readFailed, false);
|
|
150
|
+
} finally {
|
|
151
|
+
await rm(fixture.memoryDir, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
test("unparsable non-null timestamp in namespace store fails the aggregate read", async () => {
|
|
155
|
+
const fixture = await namespacedFixture();
|
|
156
|
+
try {
|
|
157
|
+
const result = await readAggregateExtractionWatermark({
|
|
158
|
+
config: fixture.config,
|
|
159
|
+
rootStorage: storage(fixture.memoryDir, async () => "2026-07-20T12:00:00.000Z"),
|
|
160
|
+
storageForNamespace: async (namespace) =>
|
|
161
|
+
storage(fixture.namespaceDirs[namespace], async () => (namespace === "team-a" ? "invalid-date-string" : "2026-07-25T12:00:00.000Z")),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
assert.equal(result.lastExtractionAt, null);
|
|
165
|
+
assert.equal(result.readFailed, true);
|
|
166
|
+
assert.match(result.readError ?? "", /watermark timestamp invalid/);
|
|
167
|
+
} finally {
|
|
168
|
+
await rm(fixture.memoryDir, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("namespace discovery I/O failure is an explicit incomplete read", async () => {
|
|
173
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-extraction-watermark-enumeration-"));
|
|
174
|
+
try {
|
|
175
|
+
await writeFile(path.join(memoryDir, "namespaces"), "not a directory", "utf8");
|
|
176
|
+
const config = parseConfig({ memoryDir, namespacesEnabled: true });
|
|
177
|
+
const result = await readAggregateExtractionWatermark({
|
|
178
|
+
config,
|
|
179
|
+
rootStorage: storage(memoryDir, async () => "2026-07-23T12:00:00.000Z"),
|
|
180
|
+
storageForNamespace: async (_namespace, rootDir) => storage(rootDir, async () => null),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
assert.equal(result.lastExtractionAt, null);
|
|
184
|
+
assert.equal(result.readFailed, true);
|
|
185
|
+
assert.match(result.readError ?? "", /namespace watermark enumeration failed/);
|
|
186
|
+
assert.match(result.readError ?? "", /ENOTDIR/);
|
|
187
|
+
} finally {
|
|
188
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("a failed stale-cache refresh makes later reads incomplete", async () => {
|
|
193
|
+
const fixture = await namespacedFixture();
|
|
194
|
+
try {
|
|
195
|
+
let now = 1;
|
|
196
|
+
const cache = new CorpusWatermarkCache({ ttlMs: 1, clock: () => now });
|
|
197
|
+
const options = {
|
|
198
|
+
config: fixture.config,
|
|
199
|
+
rootStorage: storage(fixture.memoryDir, async () => "2026-07-20T12:00:00.000Z"),
|
|
200
|
+
storageForNamespace: async (namespace: string) =>
|
|
201
|
+
storage(fixture.namespaceDirs[namespace], async () => "2026-07-21T12:00:00.000Z"),
|
|
202
|
+
rootsCache: cache,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const coldRead = await readAggregateExtractionWatermark(options);
|
|
206
|
+
assert.equal(coldRead.readFailed, false);
|
|
207
|
+
assert.equal(coldRead.pending, true);
|
|
208
|
+
await cache.whenIdle();
|
|
209
|
+
assert.equal((await readAggregateExtractionWatermark(options)).readFailed, false);
|
|
210
|
+
|
|
211
|
+
await rm(path.join(fixture.memoryDir, "namespaces"), { recursive: true, force: true });
|
|
212
|
+
await writeFile(path.join(fixture.memoryDir, "namespaces"), "not a directory", "utf8");
|
|
213
|
+
now += 2;
|
|
214
|
+
assert.equal(
|
|
215
|
+
(await readAggregateExtractionWatermark(options)).readFailed,
|
|
216
|
+
false,
|
|
217
|
+
"the stale value may be served while refresh is still in flight",
|
|
218
|
+
);
|
|
219
|
+
await cache.whenIdle();
|
|
220
|
+
const failedRefresh = await readAggregateExtractionWatermark(options);
|
|
221
|
+
assert.equal(failedRefresh.lastExtractionAt, null);
|
|
222
|
+
assert.equal(failedRefresh.readFailed, true);
|
|
223
|
+
assert.match(failedRefresh.readError ?? "", /namespace watermark enumeration failed/);
|
|
224
|
+
} finally {
|
|
225
|
+
await rm(fixture.memoryDir, { recursive: true, force: true });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("a cold namespace-root cache is pending, not a failed read", async () => {
|
|
230
|
+
const fixture = await namespacedFixture();
|
|
231
|
+
try {
|
|
232
|
+
let rootReads = 0;
|
|
233
|
+
const result = await readAggregateExtractionWatermark({
|
|
234
|
+
config: fixture.config,
|
|
235
|
+
rootStorage: storage(fixture.memoryDir, async () => {
|
|
236
|
+
rootReads += 1;
|
|
237
|
+
return "2026-07-23T12:00:00.000Z";
|
|
238
|
+
}),
|
|
239
|
+
storageForNamespace: async (namespace) => storage(fixture.namespaceDirs[namespace], async () => null),
|
|
240
|
+
rootsCache: {
|
|
241
|
+
getResolvedRootsStatus: () => ({ roots: undefined, refreshError: undefined }),
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
assert.equal(result.lastExtractionAt, null);
|
|
246
|
+
assert.equal(result.readFailed, false);
|
|
247
|
+
assert.equal(result.pending, true);
|
|
248
|
+
assert.equal(rootReads, 0, "a root survivor is not read or published before enumeration completes");
|
|
249
|
+
} finally {
|
|
250
|
+
await rm(fixture.memoryDir, { recursive: true, force: true });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("namespaces disabled performs exactly one root metadata read", async () => {
|
|
255
|
+
const memoryDir = await mkdtemp(path.join(os.tmpdir(), "remnic-extraction-watermark-single-"));
|
|
256
|
+
try {
|
|
257
|
+
const config = parseConfig({ memoryDir, namespacesEnabled: false });
|
|
258
|
+
let rootReads = 0;
|
|
259
|
+
let namespaceResolutions = 0;
|
|
260
|
+
const timestamp = "2026-07-23T12:00:00.000Z";
|
|
261
|
+
const result = await readAggregateExtractionWatermark({
|
|
262
|
+
config,
|
|
263
|
+
rootStorage: {
|
|
264
|
+
dir: memoryDir,
|
|
265
|
+
loadMeta: async () => {
|
|
266
|
+
rootReads += 1;
|
|
267
|
+
return {
|
|
268
|
+
lastExtractionAt: timestamp,
|
|
269
|
+
extractionCount: 9,
|
|
270
|
+
lastConsolidationAt: null,
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
storageForNamespace: async () => {
|
|
275
|
+
namespaceResolutions += 1;
|
|
276
|
+
throw new Error("disabled path must not resolve namespace storage");
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
assert.deepEqual(result, {
|
|
281
|
+
lastExtractionAt: timestamp,
|
|
282
|
+
readFailed: false,
|
|
283
|
+
rootStats: { extractionCount: 9, lastConsolidationAt: null },
|
|
284
|
+
});
|
|
285
|
+
assert.equal(rootReads, 1);
|
|
286
|
+
assert.equal(namespaceResolutions, 0);
|
|
287
|
+
} finally {
|
|
288
|
+
await rm(memoryDir, { recursive: true, force: true });
|
|
289
|
+
}
|
|
290
|
+
});
|
|
@@ -65,6 +65,7 @@ async function hasAnyLegacyData(rootDir: string): Promise<boolean> {
|
|
|
65
65
|
|
|
66
66
|
async function discoverConfiguredNamespaces(
|
|
67
67
|
config: PluginConfig,
|
|
68
|
+
propagateDiscoveryErrors: boolean,
|
|
68
69
|
): Promise<string[]> {
|
|
69
70
|
const discovered = new Set<string>([
|
|
70
71
|
config.defaultNamespace,
|
|
@@ -85,7 +86,10 @@ async function discoverConfiguredNamespaces(
|
|
|
85
86
|
discovered.add(namespace);
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
|
-
} catch {
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (propagateDiscoveryErrors && (error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
89
93
|
// No namespace directory yet.
|
|
90
94
|
}
|
|
91
95
|
|
|
@@ -95,9 +99,13 @@ async function discoverConfiguredNamespaces(
|
|
|
95
99
|
export async function listNamespaces(options: {
|
|
96
100
|
config: PluginConfig;
|
|
97
101
|
storageRouter?: NamespaceStorageRouter;
|
|
102
|
+
propagateDiscoveryErrors?: boolean;
|
|
98
103
|
}): Promise<NamespaceInventoryEntry[]> {
|
|
99
104
|
const storageRouter = options.storageRouter ?? new NamespaceStorageRouter(options.config);
|
|
100
|
-
const namespaces = await discoverConfiguredNamespaces(
|
|
105
|
+
const namespaces = await discoverConfiguredNamespaces(
|
|
106
|
+
options.config,
|
|
107
|
+
options.propagateDiscoveryErrors ?? false,
|
|
108
|
+
);
|
|
101
109
|
const items = await Promise.all(
|
|
102
110
|
namespaces.map(async (namespace) => {
|
|
103
111
|
const storage = await storageRouter.storageFor(namespace);
|
|
@@ -143,11 +143,18 @@ test("runOperatorDoctor: includes the corpus_watermark check", async () => {
|
|
|
143
143
|
const { root, memoryDir, configPath, orchestrator } = await makeFixture();
|
|
144
144
|
try {
|
|
145
145
|
await writeMemory(memoryDir, "facts/2026-03-08/a.md");
|
|
146
|
+
let metadataReads = 0;
|
|
147
|
+
const loadMeta = orchestrator.storage.loadMeta.bind(orchestrator.storage);
|
|
148
|
+
orchestrator.storage.loadMeta = async () => {
|
|
149
|
+
metadataReads += 1;
|
|
150
|
+
return loadMeta();
|
|
151
|
+
};
|
|
146
152
|
const report = await runOperatorDoctor({ configPath, orchestrator });
|
|
147
153
|
const check = report.checks.find((c) => c.key === "corpus_watermark");
|
|
148
154
|
assert.ok(check, "expected a corpus_watermark check");
|
|
149
155
|
assert.equal(check?.status, "ok");
|
|
150
156
|
assert.equal(corpusOf(check)[0]?.memoryFileCount, 1);
|
|
157
|
+
assert.equal(metadataReads, 1, "doctor reuses the root metadata read for extraction liveness");
|
|
151
158
|
} finally {
|
|
152
159
|
await rm(root, { recursive: true, force: true });
|
|
153
160
|
}
|
|
@@ -42,7 +42,7 @@ export async function summarizeCorpusWatermark(
|
|
|
42
42
|
// The doctor orchestrator has no live namespace catalog, so config-driven
|
|
43
43
|
// enumeration (deduped by root inside the shared helper) is the whole set.
|
|
44
44
|
const rootByNamespace = new Map<string, string>();
|
|
45
|
-
for (const root of await resolveCorpusNamespaceRoots({ config })) {
|
|
45
|
+
for (const root of await resolveCorpusNamespaceRoots({ config, propagateDiscoveryErrors: true })) {
|
|
46
46
|
rootByNamespace.set(root.namespace, root.rootDir);
|
|
47
47
|
}
|
|
48
48
|
resolved = [...rootByNamespace.keys()];
|