@remnic/core 9.47.0 → 9.48.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 +6 -5
- package/dist/access-admin-ops-surface.js +27 -25
- package/dist/access-audit.js +2 -2
- package/dist/access-authorization-probe.d.ts +6 -5
- package/dist/access-authorization-probe.js +28 -26
- package/dist/access-boundary.d.ts +6 -5
- package/dist/access-boundary.js +27 -25
- package/dist/access-cli.js +87 -85
- package/dist/access-cli.js.map +1 -1
- package/dist/access-extraction-force-flush.d.ts +6 -5
- package/dist/access-extraction-force-flush.js +27 -25
- package/dist/access-http-lcm-compaction.d.ts +6 -5
- package/dist/access-http-lifecycle-flush.d.ts +6 -5
- package/dist/access-http-offline-stream.d.ts +6 -5
- package/dist/access-http-query.js +27 -25
- package/dist/access-http.d.ts +6 -5
- package/dist/access-http.js +36 -34
- package/dist/access-identity-continuity-surface.d.ts +5 -4
- package/dist/access-identity-continuity-surface.js +27 -25
- package/dist/access-lcm-surface.d.ts +6 -5
- package/dist/access-lcm-surface.js +27 -25
- package/dist/access-mcp.d.ts +6 -5
- package/dist/access-mcp.js +32 -30
- package/dist/access-namespace-preflight.js +27 -25
- package/dist/access-observe-write-surface.d.ts +6 -5
- package/dist/access-observe-write-surface.js +27 -25
- package/dist/access-offline-manifest.d.ts +5 -4
- package/dist/access-offline-manifest.js +4 -4
- package/dist/access-operations-batch.js +29 -27
- package/dist/access-operations.d.ts +9 -8
- package/dist/access-operations.js +31 -29
- package/dist/access-recall-concurrency.d.ts +6 -5
- package/dist/access-recall-concurrency.js +27 -25
- package/dist/access-recall-response.d.ts +6 -5
- package/dist/access-recall-response.js +27 -25
- package/dist/access-recall-surface.d.ts +11 -10
- package/dist/access-recall-surface.js +27 -25
- package/dist/access-schema.d.ts +86 -86
- package/dist/{access-service-DNBYHK33.d.ts → access-service-andNjwfD.d.ts} +3 -3
- package/dist/access-service-helpers.d.ts +6 -5
- package/dist/access-service.d.ts +10 -9
- package/dist/access-service.js +27 -25
- package/dist/access-surface-catalog.d.ts +6 -5
- package/dist/access-wearables-meetings-surface.d.ts +3 -2
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/artifact-search.d.ts +37 -0
- package/dist/artifact-search.js +13 -0
- package/dist/artifact-search.js.map +1 -0
- package/dist/bootstrap.d.ts +5 -4
- package/dist/briefing.d.ts +2 -1
- package/dist/briefing.js +10 -7
- package/dist/buffer.d.ts +2 -1
- package/dist/buffer.js +4 -4
- package/dist/causal-consolidation.js +11 -8
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-T6JRMVOV.js → chunk-2EJNXE53.js} +2 -2
- package/dist/{chunk-SIP5DBDN.js → chunk-2P3GQVM2.js} +152 -130
- package/dist/chunk-2P3GQVM2.js.map +1 -0
- package/dist/{chunk-64BHFFKZ.js → chunk-2RPJ4NEE.js} +2 -2
- package/dist/{chunk-JFO5QZ7N.js → chunk-3JUYWPFJ.js} +2 -2
- package/dist/{chunk-L7G72WY4.js → chunk-3NDN5P55.js} +2 -2
- package/dist/{chunk-IXH2U2QT.js → chunk-45OTV7FZ.js} +19 -19
- package/dist/chunk-4O4TKIXQ.js +171 -0
- package/dist/chunk-4O4TKIXQ.js.map +1 -0
- package/dist/{chunk-BDNL34CT.js → chunk-4WBGZYZZ.js} +33 -8
- package/dist/chunk-4WBGZYZZ.js.map +1 -0
- package/dist/{chunk-U6JGQ37L.js → chunk-52I3YZZ6.js} +3 -3
- package/dist/{chunk-IZQ5TDFT.js → chunk-53AJILXF.js} +3 -3
- package/dist/chunk-5AUY5Z5E.js +34 -0
- package/dist/chunk-5AUY5Z5E.js.map +1 -0
- package/dist/{chunk-YWCOJOUI.js → chunk-5L7PTX2Q.js} +2 -2
- package/dist/{chunk-ELIL2A37.js → chunk-7FGL6CGA.js} +5 -5
- package/dist/{chunk-TVVEYCNW.js → chunk-7K5Q6COX.js} +4 -4
- package/dist/{chunk-HXXTXLKA.js → chunk-A2TFK5PG.js} +2 -2
- package/dist/{chunk-LI7OE7G6.js → chunk-ANCC3JJH.js} +2 -2
- package/dist/{chunk-ILBLZ5Y5.js → chunk-D5NGH2EW.js} +2 -2
- package/dist/{chunk-ST5IEK3H.js → chunk-DUEEQCTT.js} +2 -2
- package/dist/{chunk-QISYTKG4.js → chunk-EZNXW27Q.js} +6 -6
- package/dist/{chunk-47J4EDGR.js → chunk-FJ2ND6CZ.js} +2 -2
- package/dist/{chunk-FQU3HTQM.js → chunk-FMCHFSZF.js} +6 -6
- package/dist/{chunk-FULKGK7P.js → chunk-G2RBWVFQ.js} +2 -2
- package/dist/{chunk-W2ZSNIBP.js → chunk-GD7UPIBG.js} +2 -2
- package/dist/{chunk-Z54EM5YZ.js → chunk-GF22HWS2.js} +4 -4
- package/dist/{chunk-ZDAYWJV6.js → chunk-GVX3NAUU.js} +2 -2
- package/dist/{chunk-M2INOSTA.js → chunk-GWHPQOUC.js} +2 -2
- package/dist/{chunk-N5XULCU3.js → chunk-HIDEBEAN.js} +9 -9
- package/dist/{chunk-74HN5QUH.js → chunk-HRMEYQRO.js} +2 -2
- package/dist/{chunk-VFZS2HAA.js → chunk-IP3DHDU2.js} +2 -2
- package/dist/{chunk-RKNJBZ55.js → chunk-JBPKEARU.js} +4 -4
- package/dist/{chunk-PFJXPOD4.js → chunk-JYPO7L3G.js} +2 -2
- package/dist/{chunk-G2L5IZV4.js → chunk-KSGEGHYA.js} +44 -44
- package/dist/{chunk-PKCLQ7KM.js → chunk-LZGCJSQR.js} +5 -5
- package/dist/{chunk-WOC6DXP5.js → chunk-M4DMO67I.js} +4 -4
- package/dist/{chunk-JCNJIXZN.js → chunk-NXUJRXSF.js} +2 -2
- package/dist/{chunk-5VQPZ4GV.js → chunk-PELT6ADC.js} +3 -3
- package/dist/{chunk-IDE5AR5W.js → chunk-PLCZD3LG.js} +28 -66
- package/dist/chunk-PLCZD3LG.js.map +1 -0
- package/dist/{chunk-KMWYZTIE.js → chunk-PRVJH7CY.js} +2 -2
- package/dist/{chunk-RVO6TCTU.js → chunk-QCECHYZL.js} +6 -6
- package/dist/{chunk-KA47C7BF.js → chunk-RSPSHRZA.js} +2 -2
- package/dist/{chunk-E4FOAENZ.js → chunk-S55OAS5P.js} +5 -5
- package/dist/{chunk-BIPBQQFI.js → chunk-UMOO7IY7.js} +2 -2
- package/dist/{chunk-5U7ID3EZ.js → chunk-UTVZKHJK.js} +2 -2
- package/dist/{chunk-V5FE3UAI.js → chunk-WOEVJKKQ.js} +2 -2
- package/dist/{chunk-N3LL5NEU.js → chunk-WQIHRA5F.js} +2 -2
- package/dist/chunk-Z7L72ZHL.js +75 -0
- package/dist/chunk-Z7L72ZHL.js.map +1 -0
- package/dist/{cli-BWXSPxB9.d.ts → cli-p_WmVo2h.d.ts} +3 -3
- package/dist/cli.d.ts +7 -6
- package/dist/cli.js +55 -53
- package/dist/compounding/engine.d.ts +2 -1
- package/dist/compounding/engine.js +10 -7
- package/dist/connectors/codex-materialize-runner.js +10 -7
- package/dist/connectors/index.js +10 -7
- package/dist/consolidation-provenance-check.d.ts +2 -1
- package/dist/consolidation-undo.d.ts +2 -1
- package/dist/contradiction/index.d.ts +7 -6
- package/dist/convergence-refresh.d.ts +2 -1
- package/dist/convergence-refresh.js +12 -9
- package/dist/corpus-watermark.js +15 -13
- package/dist/entity-recall-cancellation.d.ts +36 -0
- package/dist/entity-recall-cancellation.js +19 -0
- package/dist/entity-recall-cancellation.js.map +1 -0
- package/dist/entity-retrieval.d.ts +14 -6
- package/dist/entity-retrieval.js +11 -7
- package/dist/explicit-capture.d.ts +5 -4
- package/dist/external-wiki-access.d.ts +6 -5
- package/dist/external-wiki-access.js +28 -26
- package/dist/external-wiki-mcp-tools.d.ts +6 -5
- package/dist/extraction.js +15 -13
- package/dist/index.d.ts +687 -686
- package/dist/index.js +114 -112
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +7 -7
- package/dist/maintenance/memory-governance.js +10 -7
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +7 -6
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +10 -7
- package/dist/maintenance/rebuild-memory-projection.d.ts +2 -1
- package/dist/maintenance/rebuild-memory-projection.js +11 -8
- package/dist/{maintenance-DQrL93Uu.d.ts → maintenance-BfcuwfJS.d.ts} +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +6 -5
- package/dist/memory-worth-outcomes.d.ts +2 -1
- package/dist/namespaces/migrate.d.ts +2 -1
- package/dist/namespaces/migrate.js +14 -12
- package/dist/namespaces/search.js +4 -4
- package/dist/namespaces/storage.d.ts +7 -6
- package/dist/namespaces/storage.js +10 -7
- package/dist/offline-sync-impression-drain.js +3 -3
- package/dist/operator-doctor-corpus.js +16 -14
- package/dist/operator-toolkit.d.ts +2 -1
- package/dist/operator-toolkit.js +20 -18
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -1
- package/dist/orchestration/maintenance.d.ts +3 -2
- package/dist/orchestration/maintenance.js +14 -12
- package/dist/{orchestrator-CRR-6sVB.d.ts → orchestrator-B7nLo4z1.d.ts} +3 -3
- package/dist/orchestrator.d.ts +5 -4
- package/dist/orchestrator.js +89 -87
- package/dist/qmd.js +4 -4
- package/dist/recall-qos.d.ts +109 -2
- package/dist/recall-qos.js +12 -3
- package/dist/schemas.d.ts +76 -76
- package/dist/search/factory.js +3 -3
- package/dist/search/index.js +3 -3
- package/dist/semantic-consolidation.js +11 -8
- package/dist/semantic-rule-promotion.js +10 -7
- package/dist/semantic-rule-verifier.js +10 -7
- package/dist/{service-DJ0LDyuN.d.ts → service-B5W5IYCx.d.ts} +1 -1
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/source-agent-qualifier.js +13 -11
- package/dist/{storage-y340tYKb.d.ts → storage-COL907QF.d.ts} +2 -1
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +9 -6
- package/dist/temporal-supersession.d.ts +2 -1
- package/dist/tier-migration.d.ts +2 -1
- package/dist/transfer/types.d.ts +66 -66
- package/dist/verified-recall.js +10 -7
- package/package.json +2 -2
- package/src/artifact-search.test.ts +110 -0
- package/src/artifact-search.ts +115 -0
- package/src/entity-recall-cancellation.ts +58 -0
- package/src/entity-retrieval.ts +58 -4
- package/src/orchestration/namespace-read-fanout.ts +4 -1
- package/src/orchestration/recall-internal-deps.ts +2 -0
- package/src/orchestration/recall-internal.ts +76 -55
- package/src/orchestration/recall-search-pipeline.ts +9 -1
- package/src/orchestration/recall-search-prefilter.ts +18 -1
- package/src/orchestrator.ts +5 -6
- package/src/recall-qos.test.ts +289 -0
- package/src/recall-qos.ts +238 -1
- package/src/storage.ts +12 -56
- package/dist/chunk-BDNL34CT.js.map +0 -1
- package/dist/chunk-C3AKM55W.js +0 -59
- package/dist/chunk-C3AKM55W.js.map +0 -1
- package/dist/chunk-IDE5AR5W.js.map +0 -1
- package/dist/chunk-SIP5DBDN.js.map +0 -1
- /package/dist/{chunk-T6JRMVOV.js.map → chunk-2EJNXE53.js.map} +0 -0
- /package/dist/{chunk-64BHFFKZ.js.map → chunk-2RPJ4NEE.js.map} +0 -0
- /package/dist/{chunk-JFO5QZ7N.js.map → chunk-3JUYWPFJ.js.map} +0 -0
- /package/dist/{chunk-L7G72WY4.js.map → chunk-3NDN5P55.js.map} +0 -0
- /package/dist/{chunk-IXH2U2QT.js.map → chunk-45OTV7FZ.js.map} +0 -0
- /package/dist/{chunk-U6JGQ37L.js.map → chunk-52I3YZZ6.js.map} +0 -0
- /package/dist/{chunk-IZQ5TDFT.js.map → chunk-53AJILXF.js.map} +0 -0
- /package/dist/{chunk-YWCOJOUI.js.map → chunk-5L7PTX2Q.js.map} +0 -0
- /package/dist/{chunk-ELIL2A37.js.map → chunk-7FGL6CGA.js.map} +0 -0
- /package/dist/{chunk-TVVEYCNW.js.map → chunk-7K5Q6COX.js.map} +0 -0
- /package/dist/{chunk-HXXTXLKA.js.map → chunk-A2TFK5PG.js.map} +0 -0
- /package/dist/{chunk-LI7OE7G6.js.map → chunk-ANCC3JJH.js.map} +0 -0
- /package/dist/{chunk-ILBLZ5Y5.js.map → chunk-D5NGH2EW.js.map} +0 -0
- /package/dist/{chunk-ST5IEK3H.js.map → chunk-DUEEQCTT.js.map} +0 -0
- /package/dist/{chunk-QISYTKG4.js.map → chunk-EZNXW27Q.js.map} +0 -0
- /package/dist/{chunk-47J4EDGR.js.map → chunk-FJ2ND6CZ.js.map} +0 -0
- /package/dist/{chunk-FQU3HTQM.js.map → chunk-FMCHFSZF.js.map} +0 -0
- /package/dist/{chunk-FULKGK7P.js.map → chunk-G2RBWVFQ.js.map} +0 -0
- /package/dist/{chunk-W2ZSNIBP.js.map → chunk-GD7UPIBG.js.map} +0 -0
- /package/dist/{chunk-Z54EM5YZ.js.map → chunk-GF22HWS2.js.map} +0 -0
- /package/dist/{chunk-ZDAYWJV6.js.map → chunk-GVX3NAUU.js.map} +0 -0
- /package/dist/{chunk-M2INOSTA.js.map → chunk-GWHPQOUC.js.map} +0 -0
- /package/dist/{chunk-N5XULCU3.js.map → chunk-HIDEBEAN.js.map} +0 -0
- /package/dist/{chunk-74HN5QUH.js.map → chunk-HRMEYQRO.js.map} +0 -0
- /package/dist/{chunk-VFZS2HAA.js.map → chunk-IP3DHDU2.js.map} +0 -0
- /package/dist/{chunk-RKNJBZ55.js.map → chunk-JBPKEARU.js.map} +0 -0
- /package/dist/{chunk-PFJXPOD4.js.map → chunk-JYPO7L3G.js.map} +0 -0
- /package/dist/{chunk-G2L5IZV4.js.map → chunk-KSGEGHYA.js.map} +0 -0
- /package/dist/{chunk-PKCLQ7KM.js.map → chunk-LZGCJSQR.js.map} +0 -0
- /package/dist/{chunk-WOC6DXP5.js.map → chunk-M4DMO67I.js.map} +0 -0
- /package/dist/{chunk-JCNJIXZN.js.map → chunk-NXUJRXSF.js.map} +0 -0
- /package/dist/{chunk-5VQPZ4GV.js.map → chunk-PELT6ADC.js.map} +0 -0
- /package/dist/{chunk-KMWYZTIE.js.map → chunk-PRVJH7CY.js.map} +0 -0
- /package/dist/{chunk-RVO6TCTU.js.map → chunk-QCECHYZL.js.map} +0 -0
- /package/dist/{chunk-KA47C7BF.js.map → chunk-RSPSHRZA.js.map} +0 -0
- /package/dist/{chunk-E4FOAENZ.js.map → chunk-S55OAS5P.js.map} +0 -0
- /package/dist/{chunk-BIPBQQFI.js.map → chunk-UMOO7IY7.js.map} +0 -0
- /package/dist/{chunk-5U7ID3EZ.js.map → chunk-UTVZKHJK.js.map} +0 -0
- /package/dist/{chunk-V5FE3UAI.js.map → chunk-WOEVJKKQ.js.map} +0 -0
- /package/dist/{chunk-N3LL5NEU.js.map → chunk-WQIHRA5F.js.map} +0 -0
package/src/orchestrator.ts
CHANGED
|
@@ -120,6 +120,7 @@ import { ConsolidationRunCoordinator } from "./orchestration/consolidation-run.j
|
|
|
120
120
|
import { ExtractionPersistCoordinator } from "./orchestration/extraction-persist.js";
|
|
121
121
|
import { RecallInternalCoordinator } from "./orchestration/recall-internal.js";
|
|
122
122
|
import { RecallSearchPipelineCoordinator } from "./orchestration/recall-search-pipeline.js";
|
|
123
|
+
import type { ArtifactRecallOptions } from "./orchestration/recall-search-prefilter.js";
|
|
123
124
|
import { TurnIngestionCoordinator, type TurnIngestionOptions } from "./orchestration/turn-ingestion.js";
|
|
124
125
|
import { RecallIntrospectionCoordinator } from "./orchestration/recall-introspection.js";
|
|
125
126
|
import { OrchestratorInitCoordinator } from "./orchestration/orchestrator-init.js";
|
|
@@ -2460,11 +2461,10 @@ export class Orchestrator {
|
|
|
2460
2461
|
namespace: string,
|
|
2461
2462
|
prompt: string,
|
|
2462
2463
|
targetCount: number,
|
|
2464
|
+
options: ArtifactRecallOptions = {},
|
|
2463
2465
|
): Promise<MemoryFile[]> {
|
|
2464
2466
|
return this.recallSearchPipelineCoordinator.fetchActiveArtifactsForNamespace(
|
|
2465
|
-
namespace,
|
|
2466
|
-
prompt,
|
|
2467
|
-
targetCount,
|
|
2467
|
+
namespace, prompt, targetCount, options,
|
|
2468
2468
|
);
|
|
2469
2469
|
}
|
|
2470
2470
|
|
|
@@ -2472,11 +2472,10 @@ export class Orchestrator {
|
|
|
2472
2472
|
prompt: string,
|
|
2473
2473
|
recallNamespaces: string[],
|
|
2474
2474
|
targetCount: number,
|
|
2475
|
+
options: ArtifactRecallOptions = {},
|
|
2475
2476
|
): Promise<MemoryFile[]> {
|
|
2476
2477
|
return this.namespaceReadFanoutCoordinator.recallArtifactsAcrossNamespaces(
|
|
2477
|
-
prompt,
|
|
2478
|
-
recallNamespaces,
|
|
2479
|
-
targetCount,
|
|
2478
|
+
prompt, recallNamespaces, targetCount, options,
|
|
2480
2479
|
);
|
|
2481
2480
|
}
|
|
2482
2481
|
|
package/src/recall-qos.test.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
+
import { throwIfAborted } from "./abort-error.js";
|
|
3
4
|
import {
|
|
5
|
+
createBoundedCoreSectionRunner,
|
|
4
6
|
createRecallSectionMetricRecorder,
|
|
7
|
+
resolveRecallCoreSectionDeadlineMs,
|
|
8
|
+
runRecallSectionWithinDeadline,
|
|
9
|
+
yieldToEventLoop,
|
|
5
10
|
type RecallSectionMetric,
|
|
6
11
|
} from "./recall-qos.js";
|
|
7
12
|
|
|
@@ -83,3 +88,287 @@ test("recall section metric recorder respects timing overrides and logs enrichme
|
|
|
83
88
|
success: true,
|
|
84
89
|
});
|
|
85
90
|
});
|
|
91
|
+
|
|
92
|
+
test("timed-out section metric reports the breach instead of a plain duration", () => {
|
|
93
|
+
const timings: Record<string, string> = {};
|
|
94
|
+
const calls: Array<{ level: "info" | "debug"; payload: unknown[] }> = [];
|
|
95
|
+
const recorder = createRecallSectionMetricRecorder({
|
|
96
|
+
timings,
|
|
97
|
+
logger: {
|
|
98
|
+
info: (_message: string, ...payload: unknown[]) => calls.push({ level: "info", payload }),
|
|
99
|
+
debug: (_message: string, ...payload: unknown[]) => calls.push({ level: "debug", payload }),
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
recorder({
|
|
104
|
+
section: "entityRetrieval",
|
|
105
|
+
priority: "core",
|
|
106
|
+
durationMs: 75_004,
|
|
107
|
+
deadlineMs: 75_000,
|
|
108
|
+
source: "timeout",
|
|
109
|
+
success: false,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
assert.equal(timings.entityRetrieval, "timeout(75004ms)");
|
|
113
|
+
// A breach is not a success, so it must not be logged as one.
|
|
114
|
+
assert.equal(calls[0]?.level, "debug");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("recall section deadline degrades to the fallback and cancels the section", async () => {
|
|
118
|
+
let observedSignal: AbortSignal | undefined;
|
|
119
|
+
let cancelledDuringWork = false;
|
|
120
|
+
|
|
121
|
+
// A real timer is the subject here, not a guessed wait: the section below
|
|
122
|
+
// never settles on its own, so the deadline always wins regardless of load.
|
|
123
|
+
const outcome = await runRecallSectionWithinDeadline({
|
|
124
|
+
deadlineMs: 15,
|
|
125
|
+
fallback: "degraded",
|
|
126
|
+
run: async (sectionSignal) => {
|
|
127
|
+
observedSignal = sectionSignal;
|
|
128
|
+
const cancelled = Promise.withResolvers<void>();
|
|
129
|
+
sectionSignal.addEventListener("abort", () => {
|
|
130
|
+
cancelledDuringWork = true;
|
|
131
|
+
cancelled.resolve();
|
|
132
|
+
});
|
|
133
|
+
await cancelled.promise;
|
|
134
|
+
return "late value";
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
assert.equal(outcome.timedOut, true);
|
|
139
|
+
assert.equal(outcome.value, "degraded");
|
|
140
|
+
assert.equal(observedSignal?.aborted, true);
|
|
141
|
+
assert.equal(cancelledDuringWork, true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("recall section deadline returns the real value when the section finishes in time", async () => {
|
|
145
|
+
const outcome = await runRecallSectionWithinDeadline({
|
|
146
|
+
deadlineMs: 5_000,
|
|
147
|
+
fallback: null as string | null,
|
|
148
|
+
run: async (sectionSignal) => {
|
|
149
|
+
assert.equal(sectionSignal.aborted, false);
|
|
150
|
+
return "section body";
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
assert.equal(outcome.timedOut, false);
|
|
155
|
+
assert.equal(outcome.value, "section body");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("a zero deadline disables the bound rather than failing every section", async () => {
|
|
159
|
+
const released = Promise.withResolvers<void>();
|
|
160
|
+
const pending = runRecallSectionWithinDeadline({
|
|
161
|
+
deadlineMs: 0,
|
|
162
|
+
fallback: "degraded",
|
|
163
|
+
run: async () => {
|
|
164
|
+
await released.promise;
|
|
165
|
+
return "unbounded value";
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Two full event-loop turns: any 0ms timer would already have fired, so a
|
|
170
|
+
// still-pending section proves the bound was disabled rather than instant.
|
|
171
|
+
for (let turn = 0; turn < 2; turn += 1) {
|
|
172
|
+
const turned = Promise.withResolvers<void>();
|
|
173
|
+
setImmediate(turned.resolve);
|
|
174
|
+
await turned.promise;
|
|
175
|
+
}
|
|
176
|
+
released.resolve();
|
|
177
|
+
|
|
178
|
+
const outcome = await pending;
|
|
179
|
+
assert.equal(outcome.timedOut, false);
|
|
180
|
+
assert.equal(outcome.value, "unbounded value");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("a caller abort propagates into the section signal", async () => {
|
|
184
|
+
const caller = new AbortController();
|
|
185
|
+
let cancelledDuringWork = false;
|
|
186
|
+
|
|
187
|
+
const pending = runRecallSectionWithinDeadline({
|
|
188
|
+
deadlineMs: 5_000,
|
|
189
|
+
fallback: "degraded",
|
|
190
|
+
parentSignal: caller.signal,
|
|
191
|
+
run: async (sectionSignal) => {
|
|
192
|
+
const cancelled = Promise.withResolvers<void>();
|
|
193
|
+
sectionSignal.addEventListener("abort", () => {
|
|
194
|
+
cancelledDuringWork = true;
|
|
195
|
+
cancelled.resolve();
|
|
196
|
+
});
|
|
197
|
+
await cancelled.promise;
|
|
198
|
+
return "cancelled";
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
caller.abort();
|
|
203
|
+
const outcome = await pending;
|
|
204
|
+
|
|
205
|
+
assert.equal(cancelledDuringWork, true);
|
|
206
|
+
assert.equal(outcome.timedOut, false);
|
|
207
|
+
assert.equal(outcome.value, "cancelled");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("an already-aborted caller signal cancels the section before it does work", async () => {
|
|
211
|
+
const caller = new AbortController();
|
|
212
|
+
caller.abort();
|
|
213
|
+
|
|
214
|
+
const outcome = await runRecallSectionWithinDeadline({
|
|
215
|
+
deadlineMs: 5_000,
|
|
216
|
+
fallback: "degraded",
|
|
217
|
+
parentSignal: caller.signal,
|
|
218
|
+
run: async (sectionSignal) => {
|
|
219
|
+
assert.equal(sectionSignal.aborted, true);
|
|
220
|
+
return "observed";
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
assert.equal(outcome.value, "observed");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("a section rejection surfaces to the caller instead of being swallowed", async () => {
|
|
228
|
+
await assert.rejects(
|
|
229
|
+
runRecallSectionWithinDeadline({
|
|
230
|
+
deadlineMs: 5_000,
|
|
231
|
+
fallback: "degraded",
|
|
232
|
+
run: async () => {
|
|
233
|
+
throw new Error("section exploded");
|
|
234
|
+
},
|
|
235
|
+
}),
|
|
236
|
+
/section exploded/,
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("the default core section budget is capped below the remaining request budget", () => {
|
|
241
|
+
// Both settings default to 75s. Taken at face value the section deadline could
|
|
242
|
+
// never fire first, so degradation would be unreachable on default config.
|
|
243
|
+
assert.equal(
|
|
244
|
+
resolveRecallCoreSectionDeadlineMs({
|
|
245
|
+
configuredCoreDeadlineMs: 75_000,
|
|
246
|
+
remainingOuterMs: 75_000,
|
|
247
|
+
}),
|
|
248
|
+
60_000,
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("the core section budget shrinks with the request budget already spent", () => {
|
|
253
|
+
// A section that starts 20s into a 75s request must finish inside the 55s left,
|
|
254
|
+
// not inside the original ceiling — otherwise the request is aborted wholesale.
|
|
255
|
+
assert.equal(
|
|
256
|
+
resolveRecallCoreSectionDeadlineMs({
|
|
257
|
+
configuredCoreDeadlineMs: 75_000,
|
|
258
|
+
remainingOuterMs: 55_000,
|
|
259
|
+
}),
|
|
260
|
+
44_000,
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("a request already over budget degrades its sections immediately", () => {
|
|
265
|
+
assert.equal(
|
|
266
|
+
resolveRecallCoreSectionDeadlineMs({
|
|
267
|
+
configuredCoreDeadlineMs: 75_000,
|
|
268
|
+
remainingOuterMs: 0,
|
|
269
|
+
}),
|
|
270
|
+
1,
|
|
271
|
+
);
|
|
272
|
+
assert.equal(
|
|
273
|
+
resolveRecallCoreSectionDeadlineMs({
|
|
274
|
+
configuredCoreDeadlineMs: 75_000,
|
|
275
|
+
remainingOuterMs: -5_000,
|
|
276
|
+
}),
|
|
277
|
+
1,
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("an explicitly lowered core section budget is honored exactly", () => {
|
|
282
|
+
assert.equal(
|
|
283
|
+
resolveRecallCoreSectionDeadlineMs({
|
|
284
|
+
configuredCoreDeadlineMs: 5_000,
|
|
285
|
+
remainingOuterMs: 75_000,
|
|
286
|
+
}),
|
|
287
|
+
5_000,
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("a zero budget on either side keeps the configured value", () => {
|
|
292
|
+
assert.equal(
|
|
293
|
+
resolveRecallCoreSectionDeadlineMs({ configuredCoreDeadlineMs: 0, remainingOuterMs: 75_000 }),
|
|
294
|
+
0,
|
|
295
|
+
);
|
|
296
|
+
// No request ceiling means nothing to reserve headroom against.
|
|
297
|
+
assert.equal(
|
|
298
|
+
resolveRecallCoreSectionDeadlineMs({ configuredCoreDeadlineMs: 75_000, remainingOuterMs: null }),
|
|
299
|
+
75_000,
|
|
300
|
+
);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("a bounded core section resolves its budget from the budget left when it starts", async () => {
|
|
304
|
+
const metrics: RecallSectionMetric[] = [];
|
|
305
|
+
const clockMs = 1_000_000;
|
|
306
|
+
const runSection = createBoundedCoreSectionRunner({
|
|
307
|
+
configuredDeadlineMs: 75_000,
|
|
308
|
+
// 75s request that started 20s ago: 55s left, so the section gets 80% of 55s.
|
|
309
|
+
outerDeadlineAtMs: clockMs + 55_000,
|
|
310
|
+
record: (metric) => metrics.push(metric),
|
|
311
|
+
logger: { warn: () => {} },
|
|
312
|
+
now: () => clockMs,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await runSection("entityRetrieval", null as string | null, async () => "section body");
|
|
316
|
+
|
|
317
|
+
assert.equal(metrics[0]?.deadlineMs, 44_000);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("a bounded core section degrades on cancellation rather than rejecting", async () => {
|
|
321
|
+
// The phase this section belongs to is awaited through a race the caller's
|
|
322
|
+
// abort also wins, so a rejection here would be an unhandled rejection.
|
|
323
|
+
const metrics: RecallSectionMetric[] = [];
|
|
324
|
+
const caller = new AbortController();
|
|
325
|
+
const runSection = createBoundedCoreSectionRunner({
|
|
326
|
+
configuredDeadlineMs: 5_000,
|
|
327
|
+
outerDeadlineAtMs: null,
|
|
328
|
+
parentSignal: caller.signal,
|
|
329
|
+
record: (metric) => metrics.push(metric),
|
|
330
|
+
logger: { warn: () => {} },
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
caller.abort();
|
|
334
|
+
const value = await runSection("artifacts", [] as string[], async (sectionSignal) => {
|
|
335
|
+
throwIfAborted(sectionSignal, "artifact search aborted");
|
|
336
|
+
return ["should not be reached"];
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
assert.deepEqual(value, []);
|
|
340
|
+
assert.equal(metrics[0]?.section, "artifacts");
|
|
341
|
+
assert.equal(metrics[0]?.success, false);
|
|
342
|
+
assert.match(metrics[0]?.timing ?? "", /^cancelled\(\d+ms\)$/);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("a bounded core section still surfaces a non-abort failure", async () => {
|
|
346
|
+
const runSection = createBoundedCoreSectionRunner({
|
|
347
|
+
configuredDeadlineMs: 5_000,
|
|
348
|
+
outerDeadlineAtMs: null,
|
|
349
|
+
record: () => {},
|
|
350
|
+
logger: { warn: () => {} },
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
await assert.rejects(
|
|
354
|
+
runSection("artifacts", [] as string[], async () => {
|
|
355
|
+
throw new Error("artifact tier unreadable");
|
|
356
|
+
}),
|
|
357
|
+
/artifact tier unreadable/,
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("yieldToEventLoop lets a pending timer run", async () => {
|
|
362
|
+
// The guarantee corpus scans depend on: without a yield the timer that carries
|
|
363
|
+
// a section deadline cannot fire while the scan holds the loop.
|
|
364
|
+
let timerFired = false;
|
|
365
|
+
const timer = setTimeout(() => {
|
|
366
|
+
timerFired = true;
|
|
367
|
+
}, 0);
|
|
368
|
+
|
|
369
|
+
await yieldToEventLoop();
|
|
370
|
+
await yieldToEventLoop();
|
|
371
|
+
clearTimeout(timer);
|
|
372
|
+
|
|
373
|
+
assert.equal(timerFired, true);
|
|
374
|
+
});
|
package/src/recall-qos.ts
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
|
+
import { abortError, isAbortError } from "./abort-error.js";
|
|
1
2
|
import { log, type LoggerBackend } from "./logger.js";
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Hand the event loop one macrotask.
|
|
6
|
+
*
|
|
7
|
+
* A recall section deadline is a timer, and a timer cannot fire while a
|
|
8
|
+
* synchronous scan holds the loop — so a provider that iterates the whole corpus
|
|
9
|
+
* must call this periodically or its budget is unenforceable (issue #2291).
|
|
10
|
+
* `setImmediate` (check phase) is used rather than a `0ms` timer so the yield
|
|
11
|
+
* cannot be starved by the timer queue it exists to let run.
|
|
12
|
+
*/
|
|
13
|
+
export function yieldToEventLoop(): Promise<void> {
|
|
14
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
15
|
+
setImmediate(resolve);
|
|
16
|
+
return promise;
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
export type RecallSectionPriority = "core" | "enrichment";
|
|
4
|
-
|
|
20
|
+
/**
|
|
21
|
+
* `timeout` records a section that started, exceeded its deadline, and was
|
|
22
|
+
* degraded to its neutral value (issue #2291). It is distinct from `skip`
|
|
23
|
+
* (never ran: disabled or zero-limit) so operators can tell a configuration
|
|
24
|
+
* choice from a budget breach in the section metric log.
|
|
25
|
+
*/
|
|
26
|
+
export type RecallSectionSource = "fresh" | "stale" | "skip" | "timeout";
|
|
5
27
|
|
|
6
28
|
export interface RecallSectionMetric {
|
|
7
29
|
section: string;
|
|
@@ -31,6 +53,9 @@ function defaultTiming(metric: RecallSectionMetric): string {
|
|
|
31
53
|
if (typeof metric.timing === "string" && metric.timing.length > 0) {
|
|
32
54
|
return metric.timing;
|
|
33
55
|
}
|
|
56
|
+
if (metric.source === "timeout") {
|
|
57
|
+
return `timeout(${Math.max(0, Math.round(metric.durationMs))}ms)`;
|
|
58
|
+
}
|
|
34
59
|
if (metric.source === "skip") {
|
|
35
60
|
return "skip";
|
|
36
61
|
}
|
|
@@ -80,3 +105,215 @@ export function createRecallSectionMetricRecorder(options: {
|
|
|
80
105
|
return entry;
|
|
81
106
|
};
|
|
82
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Fraction of the request's REMAINING budget that a single core section may
|
|
111
|
+
* consume. The remainder is what makes degradation possible at all: time to
|
|
112
|
+
* assemble and return the sections that did arrive before the outer timeout
|
|
113
|
+
* kills the request outright.
|
|
114
|
+
*/
|
|
115
|
+
const CORE_SECTION_MAX_SHARE_OF_REQUEST = 0.8;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Effective per-section deadline for optional core recall providers.
|
|
119
|
+
*
|
|
120
|
+
* Two things make the configured value unusable as-is (issue #2291):
|
|
121
|
+
*
|
|
122
|
+
* - `recallCoreDeadlineMs` and `recallOuterTimeoutMs` both default to 75s, so a
|
|
123
|
+
* section budget taken at face value can never fire before the request ceiling
|
|
124
|
+
* that cancels everything — the caller loses the whole recall rather than one
|
|
125
|
+
* section.
|
|
126
|
+
* - Sections start *after* planning, namespace resolution, and mode selection.
|
|
127
|
+
* Capping against the original ceiling would let a section that starts 20s in
|
|
128
|
+
* still be running when the request is aborted, which is the same failure.
|
|
129
|
+
*
|
|
130
|
+
* So the cap is taken against `remainingOuterMs` — the request budget left when
|
|
131
|
+
* the section actually starts. An operator who lowers `recallCoreDeadlineMs`
|
|
132
|
+
* below that still gets exactly what they asked for.
|
|
133
|
+
*
|
|
134
|
+
* `remainingOuterMs === null` means the request is unbounded and there is nothing
|
|
135
|
+
* to reserve headroom against. A non-positive remainder means the request is
|
|
136
|
+
* already over budget, so the section degrades immediately rather than starting
|
|
137
|
+
* work that cannot be delivered.
|
|
138
|
+
*/
|
|
139
|
+
export function resolveRecallCoreSectionDeadlineMs(options: {
|
|
140
|
+
configuredCoreDeadlineMs: number;
|
|
141
|
+
remainingOuterMs: number | null;
|
|
142
|
+
}): number {
|
|
143
|
+
const configured = options.configuredCoreDeadlineMs;
|
|
144
|
+
if (!Number.isFinite(configured) || configured <= 0) return configured;
|
|
145
|
+
const remaining = options.remainingOuterMs;
|
|
146
|
+
if (remaining === null || !Number.isFinite(remaining)) return configured;
|
|
147
|
+
if (remaining <= 0) return 1;
|
|
148
|
+
return Math.min(
|
|
149
|
+
configured,
|
|
150
|
+
Math.max(1, Math.floor(remaining * CORE_SECTION_MAX_SHARE_OF_REQUEST)),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface RecallSectionDeadlineOutcome<T> {
|
|
155
|
+
/** The section's value, or `fallback` when the deadline was exceeded. */
|
|
156
|
+
value: T;
|
|
157
|
+
timedOut: boolean;
|
|
158
|
+
durationMs: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Run one recall section under a deadline, degrading to `fallback` instead of
|
|
163
|
+
* blocking the phase it belongs to (issue #2291).
|
|
164
|
+
*
|
|
165
|
+
* Before this existed, `deadlineMs` was recorded in the section metric but
|
|
166
|
+
* never enforced for core sections: a provider doing a multi-minute scan on a
|
|
167
|
+
* slow filesystem blocked the whole phase-one `Promise.all`, and kept scanning
|
|
168
|
+
* after the caller aborted. Two guarantees fix that:
|
|
169
|
+
*
|
|
170
|
+
* 1. The returned promise settles within `deadlineMs`, with `timedOut: true`
|
|
171
|
+
* and the caller's neutral value, so one slow optional provider degrades
|
|
172
|
+
* independently rather than holding the response.
|
|
173
|
+
* 2. `run` receives a signal that fires on the deadline AND on the caller's
|
|
174
|
+
* abort, so the section can stop its own work at its next checkpoint.
|
|
175
|
+
*
|
|
176
|
+
* `deadlineMs <= 0` (or non-finite) means unbounded, matching the config
|
|
177
|
+
* contract that a zero budget disables the limit. The section signal is still
|
|
178
|
+
* created and still tracks the parent, so cancellation propagates either way.
|
|
179
|
+
*/
|
|
180
|
+
export async function runRecallSectionWithinDeadline<T>(options: {
|
|
181
|
+
deadlineMs: number;
|
|
182
|
+
fallback: T;
|
|
183
|
+
parentSignal?: AbortSignal;
|
|
184
|
+
run: (sectionSignal: AbortSignal) => Promise<T>;
|
|
185
|
+
now?: () => number;
|
|
186
|
+
}): Promise<RecallSectionDeadlineOutcome<T>> {
|
|
187
|
+
const now = options.now ?? Date.now;
|
|
188
|
+
const startedAtMs = now();
|
|
189
|
+
const controller = new AbortController();
|
|
190
|
+
const parentSignal = options.parentSignal;
|
|
191
|
+
const onParentAbort = () => controller.abort();
|
|
192
|
+
if (parentSignal?.aborted) {
|
|
193
|
+
controller.abort();
|
|
194
|
+
} else if (parentSignal) {
|
|
195
|
+
parentSignal.addEventListener("abort", onParentAbort, { once: true });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const bounded = Number.isFinite(options.deadlineMs) && options.deadlineMs > 0;
|
|
199
|
+
let timer: NodeJS.Timeout | undefined;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
// Wrap in a tagged result rather than racing raw values against a sentinel:
|
|
203
|
+
// T is caller-chosen and must never be mistaken for the timeout marker.
|
|
204
|
+
const work = options.run(controller.signal).then(
|
|
205
|
+
(value) => ({ kind: "value" as const, value }),
|
|
206
|
+
);
|
|
207
|
+
if (!bounded) {
|
|
208
|
+
const settled = await work;
|
|
209
|
+
return { value: settled.value, timedOut: false, durationMs: now() - startedAtMs };
|
|
210
|
+
}
|
|
211
|
+
const deadline = Promise.withResolvers<{ kind: "timeout" }>();
|
|
212
|
+
// Deliberately NOT unref'd: the caller is awaiting this bound, so the timer
|
|
213
|
+
// must keep the loop alive until it fires or `finally` clears it.
|
|
214
|
+
timer = setTimeout(() => deadline.resolve({ kind: "timeout" }), options.deadlineMs);
|
|
215
|
+
const settled = await Promise.race([work, deadline.promise]);
|
|
216
|
+
if (settled.kind === "timeout") {
|
|
217
|
+
controller.abort(abortError("recall section deadline exceeded"));
|
|
218
|
+
// The abandoned work outlives this call; swallow its settlement so a
|
|
219
|
+
// late rejection is not reported as unhandled.
|
|
220
|
+
void work.catch(() => {});
|
|
221
|
+
return { value: options.fallback, timedOut: true, durationMs: now() - startedAtMs };
|
|
222
|
+
}
|
|
223
|
+
return { value: settled.value, timedOut: false, durationMs: now() - startedAtMs };
|
|
224
|
+
} finally {
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
parentSignal?.removeEventListener("abort", onParentAbort);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Runs one *optional* core recall section, bounded and accounted for.
|
|
232
|
+
*
|
|
233
|
+
* Binds a section's deadline, the caller's abort signal, and the QoS recorder
|
|
234
|
+
* once so each provider is one call (issue #2291). A breach degrades the
|
|
235
|
+
* section to `fallback`, warns with the section name and budget, and records the
|
|
236
|
+
* section as `timeout` — so a slow provider costs its own section, not the
|
|
237
|
+
* response.
|
|
238
|
+
*
|
|
239
|
+
* The budget is resolved when each section STARTS, against the request budget
|
|
240
|
+
* still left at that moment (`outerDeadlineAtMs`), not against the original
|
|
241
|
+
* configured ceiling: sections begin after planning and namespace resolution, and
|
|
242
|
+
* a section sized to the full request would otherwise still be running when the
|
|
243
|
+
* request itself is aborted.
|
|
244
|
+
*
|
|
245
|
+
* Cancellation degrades too, and must: the section signal fires on the caller's
|
|
246
|
+
* abort, and the phase this section belongs to is awaited through a race that the
|
|
247
|
+
* caller's abort also wins. A rejection here would therefore land on a promise
|
|
248
|
+
* nobody is awaiting any more — an unhandled rejection rather than a signal. The
|
|
249
|
+
* caller learns of its own abort from that race, not from a section result.
|
|
250
|
+
*
|
|
251
|
+
* Only sections whose absence leaves recall coherent belong here: the fallback
|
|
252
|
+
* IS the degraded contract.
|
|
253
|
+
*/
|
|
254
|
+
export function createBoundedCoreSectionRunner(options: {
|
|
255
|
+
configuredDeadlineMs: number;
|
|
256
|
+
/** Absolute time the whole request is aborted, or null when unbounded. */
|
|
257
|
+
outerDeadlineAtMs: number | null;
|
|
258
|
+
parentSignal?: AbortSignal;
|
|
259
|
+
record: (metric: RecallSectionMetric) => unknown;
|
|
260
|
+
logger?: Pick<LoggerBackend, "warn">;
|
|
261
|
+
now?: () => number;
|
|
262
|
+
}): <T>(
|
|
263
|
+
section: string,
|
|
264
|
+
fallback: T,
|
|
265
|
+
run: (sectionSignal: AbortSignal) => Promise<T>,
|
|
266
|
+
) => Promise<T> {
|
|
267
|
+
const logger = options.logger ?? log;
|
|
268
|
+
const now = options.now ?? Date.now;
|
|
269
|
+
return async <T>(
|
|
270
|
+
section: string,
|
|
271
|
+
fallback: T,
|
|
272
|
+
run: (sectionSignal: AbortSignal) => Promise<T>,
|
|
273
|
+
): Promise<T> => {
|
|
274
|
+
const startedAtMs = now();
|
|
275
|
+
const deadlineMs = resolveRecallCoreSectionDeadlineMs({
|
|
276
|
+
configuredCoreDeadlineMs: options.configuredDeadlineMs,
|
|
277
|
+
remainingOuterMs:
|
|
278
|
+
options.outerDeadlineAtMs === null ? null : options.outerDeadlineAtMs - startedAtMs,
|
|
279
|
+
});
|
|
280
|
+
let outcome: RecallSectionDeadlineOutcome<T>;
|
|
281
|
+
try {
|
|
282
|
+
outcome = await runRecallSectionWithinDeadline({
|
|
283
|
+
deadlineMs,
|
|
284
|
+
fallback,
|
|
285
|
+
parentSignal: options.parentSignal,
|
|
286
|
+
run,
|
|
287
|
+
now,
|
|
288
|
+
});
|
|
289
|
+
} catch (err) {
|
|
290
|
+
if (!isAbortError(err)) throw err;
|
|
291
|
+
const durationMs = now() - startedAtMs;
|
|
292
|
+
options.record({
|
|
293
|
+
section,
|
|
294
|
+
priority: "core",
|
|
295
|
+
durationMs,
|
|
296
|
+
deadlineMs,
|
|
297
|
+
source: "skip",
|
|
298
|
+
success: false,
|
|
299
|
+
timing: `cancelled(${durationMs}ms)`,
|
|
300
|
+
});
|
|
301
|
+
return fallback;
|
|
302
|
+
}
|
|
303
|
+
if (outcome.timedOut) {
|
|
304
|
+
logger.warn(
|
|
305
|
+
`recall section [${section}] exceeded its ${deadlineMs}ms core deadline ` +
|
|
306
|
+
`after ${outcome.durationMs}ms; recall continues without it`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
options.record({
|
|
310
|
+
section,
|
|
311
|
+
priority: "core",
|
|
312
|
+
durationMs: outcome.durationMs,
|
|
313
|
+
deadlineMs,
|
|
314
|
+
source: outcome.timedOut ? "timeout" : "fresh",
|
|
315
|
+
success: !outcome.timedOut,
|
|
316
|
+
});
|
|
317
|
+
return outcome.value;
|
|
318
|
+
};
|
|
319
|
+
}
|
package/src/storage.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import { appendFileSync, createReadStream, mkdirSync, readFileSync, statSync, type Dirent } from "node:fs";
|
|
17
17
|
import { createHash, randomUUID } from "node:crypto";
|
|
18
18
|
import { normalizeContent, computeContentHash } from "./content-hash.js";
|
|
19
|
+
import { selectArtifactMatches, type ArtifactSearchOptions } from "./artifact-search.js";
|
|
19
20
|
import path from "node:path";
|
|
20
21
|
import { log } from "./logger.js";
|
|
21
22
|
import { createMemorySnapshot } from "./memory-snapshot.js";
|
|
@@ -235,36 +236,6 @@ import {
|
|
|
235
236
|
// stripCitation import removed: legacy rebuild fallback was replaced by a
|
|
236
237
|
// skip-with-warning strategy (Finding 1 — Uhol). See ensureFactHashIndexAuthoritative.
|
|
237
238
|
|
|
238
|
-
const ARTIFACT_SEARCH_STOPWORDS = new Set([
|
|
239
|
-
"a",
|
|
240
|
-
"an",
|
|
241
|
-
"and",
|
|
242
|
-
"are",
|
|
243
|
-
"as",
|
|
244
|
-
"at",
|
|
245
|
-
"be",
|
|
246
|
-
"but",
|
|
247
|
-
"by",
|
|
248
|
-
"for",
|
|
249
|
-
"from",
|
|
250
|
-
"has",
|
|
251
|
-
"have",
|
|
252
|
-
"i",
|
|
253
|
-
"in",
|
|
254
|
-
"is",
|
|
255
|
-
"it",
|
|
256
|
-
"of",
|
|
257
|
-
"on",
|
|
258
|
-
"or",
|
|
259
|
-
"that",
|
|
260
|
-
"the",
|
|
261
|
-
"this",
|
|
262
|
-
"to",
|
|
263
|
-
"was",
|
|
264
|
-
"were",
|
|
265
|
-
"with",
|
|
266
|
-
]);
|
|
267
|
-
|
|
268
239
|
type SharedVersionKind = "memory-status" | "artifact-write" | "cold-write" | "memory-corpus" | "entity-mutation";
|
|
269
240
|
|
|
270
241
|
type OfflineSyncDigestCacheEntry = {
|
|
@@ -323,15 +294,6 @@ export interface MemoryLifecycleEventWriteOptions {
|
|
|
323
294
|
correlationId?: string;
|
|
324
295
|
}
|
|
325
296
|
|
|
326
|
-
function tokenizeArtifactSearchText(input: string): string[] {
|
|
327
|
-
return input
|
|
328
|
-
.toLowerCase()
|
|
329
|
-
.split(/[^a-z0-9]+/i)
|
|
330
|
-
.map((t) => t.trim())
|
|
331
|
-
.filter((t) => t.length >= 2)
|
|
332
|
-
.filter((t) => !ARTIFACT_SEARCH_STOPWORDS.has(t));
|
|
333
|
-
}
|
|
334
|
-
|
|
335
297
|
/**
|
|
336
298
|
* Validate a Memory Worth counter (`mw_success` / `mw_fail`) before we persist
|
|
337
299
|
* it. Rejects non-finite, non-integer, and negative values rather than silently
|
|
@@ -4389,23 +4351,17 @@ export class StorageManager extends TombstoneBlockedCaptureIndexHost {
|
|
|
4389
4351
|
return this.memoryReadStore.readAllArtifactsCached();
|
|
4390
4352
|
}
|
|
4391
4353
|
|
|
4392
|
-
async searchArtifacts(
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
if (score > 0) {
|
|
4404
|
-
hits.push({ score, memory });
|
|
4405
|
-
}
|
|
4406
|
-
}
|
|
4407
|
-
hits.sort((a, b) => b.score - a.score);
|
|
4408
|
-
return hits.slice(0, maxResults).map((h) => h.memory);
|
|
4354
|
+
async searchArtifacts(
|
|
4355
|
+
query: string,
|
|
4356
|
+
maxResults: number,
|
|
4357
|
+
options: ArtifactSearchOptions = {},
|
|
4358
|
+
): Promise<MemoryFile[]> {
|
|
4359
|
+
return selectArtifactMatches(
|
|
4360
|
+
() => this.readAllArtifactsCached(),
|
|
4361
|
+
query,
|
|
4362
|
+
maxResults,
|
|
4363
|
+
options,
|
|
4364
|
+
);
|
|
4409
4365
|
}
|
|
4410
4366
|
|
|
4411
4367
|
async writeEntity(
|