@remnic/core 9.48.1 → 9.49.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 -26
- package/dist/access-audit.js +2 -2
- package/dist/access-authorization-probe.d.ts +6 -5
- package/dist/access-authorization-probe.js +28 -27
- package/dist/access-boundary.d.ts +6 -5
- package/dist/access-boundary.js +27 -26
- package/dist/access-cli.js +64 -63
- 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 -26
- 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 -26
- package/dist/access-http.d.ts +6 -5
- package/dist/access-http.js +35 -34
- package/dist/access-identity-continuity-surface.d.ts +5 -4
- package/dist/access-identity-continuity-surface.js +27 -26
- package/dist/access-lcm-surface.d.ts +6 -5
- package/dist/access-lcm-surface.js +27 -26
- package/dist/access-mcp.d.ts +6 -5
- package/dist/access-mcp.js +33 -32
- package/dist/access-namespace-preflight.js +27 -26
- package/dist/access-observe-write-surface.d.ts +6 -5
- package/dist/access-observe-write-surface.js +27 -26
- package/dist/access-offline-manifest.d.ts +5 -4
- package/dist/access-operations-batch.js +29 -28
- package/dist/access-operations.d.ts +6 -5
- package/dist/access-operations.js +32 -31
- package/dist/access-recall-concurrency.d.ts +6 -5
- package/dist/access-recall-concurrency.js +27 -26
- package/dist/access-recall-response.d.ts +6 -5
- package/dist/access-recall-response.js +27 -26
- package/dist/access-recall-surface.d.ts +6 -5
- package/dist/access-recall-surface.js +27 -26
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-andNjwfD.d.ts → access-service-DvXOzCrM.d.ts} +3 -3
- package/dist/access-service-helpers.d.ts +6 -5
- package/dist/access-service.d.ts +6 -5
- package/dist/access-service.js +27 -26
- package/dist/access-surface-catalog.d.ts +6 -5
- package/dist/access-wearables-meetings-surface.d.ts +3 -2
- package/dist/bootstrap.d.ts +5 -4
- package/dist/briefing.d.ts +2 -1
- package/dist/briefing.js +7 -6
- package/dist/buffer.d.ts +2 -1
- package/dist/{capsule-crypto-7FJQINUR.js → capsule-crypto-YO5QJ6L3.js} +2 -2
- package/dist/{capsule-merge-Q6AR3XL7.js → capsule-merge-6GFHWECG.js} +4 -4
- package/dist/causal-consolidation.js +8 -7
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-FMCHFSZF.js → chunk-2RIWJUSQ.js} +6 -6
- package/dist/{chunk-S55OAS5P.js → chunk-2VHLLPTX.js} +4 -4
- package/dist/{chunk-GWHPQOUC.js → chunk-3R6D33UF.js} +2 -2
- package/dist/{chunk-PELT6ADC.js → chunk-5AGR7SAI.js} +3 -3
- package/dist/chunk-5GINB5LC.js +13 -0
- package/dist/chunk-5GINB5LC.js.map +1 -0
- package/dist/{chunk-FJ2ND6CZ.js → chunk-5JRC6BYS.js} +10 -10
- package/dist/{chunk-A2TFK5PG.js → chunk-ABR2HEJO.js} +2 -2
- package/dist/{chunk-NXUJRXSF.js → chunk-B6KGGRUL.js} +2 -2
- package/dist/{chunk-PRVJH7CY.js → chunk-BMCHFWGX.js} +2 -2
- package/dist/{chunk-2NLLXCJG.js → chunk-BXLOS5AJ.js} +2 -2
- package/dist/{chunk-ANCC3JJH.js → chunk-C3KVFJH5.js} +2 -2
- package/dist/{chunk-JYPO7L3G.js → chunk-CNQQMB67.js} +2 -2
- package/dist/{chunk-X7Y7WX73.js → chunk-DQEMWVMT.js} +1 -1
- package/dist/{chunk-2EJNXE53.js → chunk-ENEA47UC.js} +2 -2
- package/dist/{chunk-PLCZD3LG.js → chunk-EY64JLOM.js} +66 -31
- package/dist/chunk-EY64JLOM.js.map +1 -0
- package/dist/{chunk-4WBGZYZZ.js → chunk-EYAIZBLM.js} +4 -4
- package/dist/chunk-EYAIZBLM.js.map +1 -0
- package/dist/{chunk-YNDLCWXS.js → chunk-EZ25VE3G.js} +4 -4
- package/dist/{chunk-HAYSYUAB.js → chunk-FMXJ555A.js} +2 -2
- package/dist/{chunk-VANZKHTN.js → chunk-G5OB4XZX.js} +4 -4
- package/dist/{chunk-DUEEQCTT.js → chunk-I3YHVVT4.js} +2 -2
- package/dist/{chunk-3JUYWPFJ.js → chunk-I4TQST3L.js} +2 -2
- package/dist/{chunk-HIDEBEAN.js → chunk-IPBNC77A.js} +8 -8
- package/dist/{chunk-45OTV7FZ.js → chunk-IXPKESVP.js} +11 -11
- package/dist/{chunk-KSGEGHYA.js → chunk-IZANPXMV.js} +34 -34
- package/dist/{chunk-IP3DHDU2.js → chunk-JFLOOTXU.js} +3 -3
- package/dist/{chunk-XL3TATMJ.js → chunk-JHMAYABJ.js} +4 -4
- package/dist/{chunk-HRMEYQRO.js → chunk-JXZ7NLYH.js} +2 -2
- package/dist/{chunk-53AJILXF.js → chunk-K7VKAHFE.js} +4 -4
- package/dist/{chunk-IJ7HK452.js → chunk-KMAP7SNQ.js} +2 -2
- package/dist/{chunk-D5NGH2EW.js → chunk-L6DCVUCA.js} +2 -2
- package/dist/{chunk-M4DMO67I.js → chunk-LQL7ARIC.js} +3 -3
- package/dist/{chunk-3NDN5P55.js → chunk-MXPB6PJQ.js} +2 -2
- package/dist/{chunk-RSPSHRZA.js → chunk-MZZR3J6M.js} +2 -2
- package/dist/{chunk-YLHM5BQQ.js → chunk-OLX2HJFW.js} +4 -4
- package/dist/{chunk-NIS256NW.js → chunk-QKD65RGL.js} +2 -2
- package/dist/{chunk-A4JMGKCD.js → chunk-R5IGA2NH.js} +4 -4
- package/dist/chunk-RBOJCG4T.js +77 -0
- package/dist/chunk-RBOJCG4T.js.map +1 -0
- package/dist/{chunk-7YKVM5H3.js → chunk-RHTO4HTL.js} +2 -2
- package/dist/{chunk-GD7UPIBG.js → chunk-RJV6Q2FT.js} +2 -2
- package/dist/{chunk-5L7PTX2Q.js → chunk-SHDZNSK4.js} +2 -2
- package/dist/{chunk-WOEVJKKQ.js → chunk-SMPVHGHG.js} +3 -3
- package/dist/{chunk-7K5Q6COX.js → chunk-TVVEYCNW.js} +4 -4
- package/dist/{chunk-52I3YZZ6.js → chunk-ULWHRQEQ.js} +2 -2
- package/dist/{chunk-KQAFEZQX.js → chunk-VDX2J7OX.js} +2 -2
- package/dist/{chunk-2P3GQVM2.js → chunk-YQIVWS7Y.js} +50 -41
- package/dist/chunk-YQIVWS7Y.js.map +1 -0
- package/dist/{chunk-G2RBWVFQ.js → chunk-Z4U6U3M3.js} +2 -2
- package/dist/{chunk-EZNXW27Q.js → chunk-Z5UC3OKB.js} +6 -6
- package/dist/{cli-p_WmVo2h.d.ts → cli-PyAUaozE.d.ts} +3 -3
- package/dist/cli.d.ts +7 -6
- package/dist/cli.js +52 -51
- package/dist/compounding/engine.d.ts +2 -1
- package/dist/compounding/engine.js +7 -6
- package/dist/connectors/codex-materialize-runner.js +7 -6
- package/dist/connectors/index.js +7 -6
- package/dist/consolidation-provenance-check.d.ts +2 -1
- package/dist/consolidation-provenance-check.js +4 -4
- package/dist/consolidation-undo.d.ts +2 -1
- package/dist/consolidation-undo.js +3 -3
- package/dist/contradiction/index.d.ts +2 -1
- package/dist/convergence-refresh.d.ts +2 -1
- package/dist/convergence-refresh.js +9 -8
- package/dist/corpus-read-cancellation.d.ts +38 -0
- package/dist/corpus-read-cancellation.js +9 -0
- package/dist/corpus-read-cancellation.js.map +1 -0
- package/dist/corpus-watermark.js +16 -15
- package/dist/dashboard-runtime.js +2 -2
- package/dist/entity-retrieval.d.ts +2 -1
- package/dist/entity-retrieval.js +7 -6
- package/dist/explicit-capture.d.ts +5 -4
- package/dist/external-wiki-access.d.ts +6 -5
- package/dist/external-wiki-access.js +28 -27
- package/dist/external-wiki-mcp-tools.d.ts +6 -5
- package/dist/extraction.js +8 -7
- package/dist/in-flight-reads.d.ts +46 -2
- package/dist/in-flight-reads.js +5 -3
- package/dist/index.d.ts +11 -10
- package/dist/index.js +68 -67
- package/dist/lcm/index.js +3 -3
- package/dist/maintenance/memory-governance.js +7 -6
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +2 -1
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -6
- package/dist/maintenance/rebuild-memory-projection.d.ts +2 -1
- package/dist/maintenance/rebuild-memory-projection.js +8 -7
- package/dist/{maintenance-BfcuwfJS.d.ts → maintenance-j7sfNQdK.d.ts} +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +6 -5
- package/dist/memory-corpus-version.js +2 -2
- package/dist/memory-worth-outcomes.d.ts +2 -1
- package/dist/namespaces/migrate.d.ts +2 -1
- package/dist/namespaces/migrate.js +15 -14
- package/dist/namespaces/search.js +7 -7
- package/dist/namespaces/storage.d.ts +2 -1
- package/dist/namespaces/storage.js +7 -6
- package/dist/operator-doctor-corpus.js +17 -16
- package/dist/operator-toolkit.d.ts +2 -1
- package/dist/operator-toolkit.js +22 -21
- package/dist/orchestration/compression-guideline-coordinator.d.ts +2 -1
- package/dist/orchestration/maintenance.d.ts +3 -2
- package/dist/orchestration/maintenance.js +12 -11
- package/dist/{orchestrator-B7nLo4z1.d.ts → orchestrator-Haa9xVPV.d.ts} +3 -3
- package/dist/orchestrator.d.ts +5 -4
- package/dist/orchestrator.js +64 -63
- package/dist/page-versioning.js +3 -3
- package/dist/search/factory.js +6 -6
- package/dist/search/index.js +11 -11
- 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 +8 -7
- package/dist/semantic-rule-promotion.js +7 -6
- package/dist/semantic-rule-verifier.js +7 -6
- package/dist/{service-B5W5IYCx.d.ts → service-Da_BI4S8.d.ts} +1 -1
- package/dist/source-agent-qualifier.js +7 -6
- package/dist/{storage-COL907QF.d.ts → storage-D1apCiCS.d.ts} +4 -3
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +6 -5
- package/dist/temporal-supersession.d.ts +2 -1
- package/dist/tier-migration.d.ts +2 -1
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +5 -5
- package/dist/verified-recall.js +7 -6
- package/package.json +2 -2
- package/src/corpus-read-cancellation.ts +42 -0
- package/src/entity-retrieval.ts +4 -4
- package/src/in-flight-reads.ts +119 -6
- package/src/orchestration/namespace-read-fanout.ts +13 -2
- package/src/orchestration/recall-internal-deps.ts +2 -0
- package/src/orchestration/recall-internal.ts +4 -1
- package/src/orchestration/recall-search-pipeline.ts +2 -0
- package/src/orchestration/recall-search-prefilter.ts +5 -1
- package/src/orchestrator.ts +5 -5
- package/src/storage/entity-store.ts +9 -2
- package/src/storage/memory-read-store.ts +33 -4
- package/src/storage-contract/corpus-read-cancellation.test.ts +408 -0
- package/src/storage.ts +61 -19
- package/dist/chunk-2P3GQVM2.js.map +0 -1
- package/dist/chunk-4WBGZYZZ.js.map +0 -1
- package/dist/chunk-PDJTHRAW.js +0 -36
- package/dist/chunk-PDJTHRAW.js.map +0 -1
- package/dist/chunk-PLCZD3LG.js.map +0 -1
- /package/dist/{capsule-crypto-7FJQINUR.js.map → capsule-crypto-YO5QJ6L3.js.map} +0 -0
- /package/dist/{capsule-merge-Q6AR3XL7.js.map → capsule-merge-6GFHWECG.js.map} +0 -0
- /package/dist/{chunk-FMCHFSZF.js.map → chunk-2RIWJUSQ.js.map} +0 -0
- /package/dist/{chunk-S55OAS5P.js.map → chunk-2VHLLPTX.js.map} +0 -0
- /package/dist/{chunk-GWHPQOUC.js.map → chunk-3R6D33UF.js.map} +0 -0
- /package/dist/{chunk-PELT6ADC.js.map → chunk-5AGR7SAI.js.map} +0 -0
- /package/dist/{chunk-FJ2ND6CZ.js.map → chunk-5JRC6BYS.js.map} +0 -0
- /package/dist/{chunk-A2TFK5PG.js.map → chunk-ABR2HEJO.js.map} +0 -0
- /package/dist/{chunk-NXUJRXSF.js.map → chunk-B6KGGRUL.js.map} +0 -0
- /package/dist/{chunk-PRVJH7CY.js.map → chunk-BMCHFWGX.js.map} +0 -0
- /package/dist/{chunk-2NLLXCJG.js.map → chunk-BXLOS5AJ.js.map} +0 -0
- /package/dist/{chunk-ANCC3JJH.js.map → chunk-C3KVFJH5.js.map} +0 -0
- /package/dist/{chunk-JYPO7L3G.js.map → chunk-CNQQMB67.js.map} +0 -0
- /package/dist/{chunk-X7Y7WX73.js.map → chunk-DQEMWVMT.js.map} +0 -0
- /package/dist/{chunk-2EJNXE53.js.map → chunk-ENEA47UC.js.map} +0 -0
- /package/dist/{chunk-YNDLCWXS.js.map → chunk-EZ25VE3G.js.map} +0 -0
- /package/dist/{chunk-HAYSYUAB.js.map → chunk-FMXJ555A.js.map} +0 -0
- /package/dist/{chunk-VANZKHTN.js.map → chunk-G5OB4XZX.js.map} +0 -0
- /package/dist/{chunk-DUEEQCTT.js.map → chunk-I3YHVVT4.js.map} +0 -0
- /package/dist/{chunk-3JUYWPFJ.js.map → chunk-I4TQST3L.js.map} +0 -0
- /package/dist/{chunk-HIDEBEAN.js.map → chunk-IPBNC77A.js.map} +0 -0
- /package/dist/{chunk-45OTV7FZ.js.map → chunk-IXPKESVP.js.map} +0 -0
- /package/dist/{chunk-KSGEGHYA.js.map → chunk-IZANPXMV.js.map} +0 -0
- /package/dist/{chunk-IP3DHDU2.js.map → chunk-JFLOOTXU.js.map} +0 -0
- /package/dist/{chunk-XL3TATMJ.js.map → chunk-JHMAYABJ.js.map} +0 -0
- /package/dist/{chunk-HRMEYQRO.js.map → chunk-JXZ7NLYH.js.map} +0 -0
- /package/dist/{chunk-53AJILXF.js.map → chunk-K7VKAHFE.js.map} +0 -0
- /package/dist/{chunk-IJ7HK452.js.map → chunk-KMAP7SNQ.js.map} +0 -0
- /package/dist/{chunk-D5NGH2EW.js.map → chunk-L6DCVUCA.js.map} +0 -0
- /package/dist/{chunk-M4DMO67I.js.map → chunk-LQL7ARIC.js.map} +0 -0
- /package/dist/{chunk-3NDN5P55.js.map → chunk-MXPB6PJQ.js.map} +0 -0
- /package/dist/{chunk-RSPSHRZA.js.map → chunk-MZZR3J6M.js.map} +0 -0
- /package/dist/{chunk-YLHM5BQQ.js.map → chunk-OLX2HJFW.js.map} +0 -0
- /package/dist/{chunk-NIS256NW.js.map → chunk-QKD65RGL.js.map} +0 -0
- /package/dist/{chunk-A4JMGKCD.js.map → chunk-R5IGA2NH.js.map} +0 -0
- /package/dist/{chunk-7YKVM5H3.js.map → chunk-RHTO4HTL.js.map} +0 -0
- /package/dist/{chunk-GD7UPIBG.js.map → chunk-RJV6Q2FT.js.map} +0 -0
- /package/dist/{chunk-5L7PTX2Q.js.map → chunk-SHDZNSK4.js.map} +0 -0
- /package/dist/{chunk-WOEVJKKQ.js.map → chunk-SMPVHGHG.js.map} +0 -0
- /package/dist/{chunk-7K5Q6COX.js.map → chunk-TVVEYCNW.js.map} +0 -0
- /package/dist/{chunk-52I3YZZ6.js.map → chunk-ULWHRQEQ.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-VDX2J7OX.js.map} +0 -0
- /package/dist/{chunk-G2RBWVFQ.js.map → chunk-Z4U6U3M3.js.map} +0 -0
- /package/dist/{chunk-EZNXW27Q.js.map → chunk-Z5UC3OKB.js.map} +0 -0
|
@@ -18,6 +18,8 @@ import { type BufferSurpriseEvent, type CompressionGuidelineOptimizerState, type
|
|
|
18
18
|
import { RECALL_FALLBACK_DIRS } from "../utils/category-dir.js";
|
|
19
19
|
import { isErrnoCode } from "../utils/errno.js";
|
|
20
20
|
import { assertPathInsideRoot } from "../utils/path-containment.js";
|
|
21
|
+
import { isAbortError } from "../abort-error.js";
|
|
22
|
+
import { checkCorpusReadAbort, type CorpusReadOptions } from "../corpus-read-cancellation.js";
|
|
21
23
|
import { isValidTranscriptDate } from "../wearables/day-store.js";
|
|
22
24
|
import {
|
|
23
25
|
isValidBufferSurpriseEvent,
|
|
@@ -147,7 +149,8 @@ export class MemoryReadStore {
|
|
|
147
149
|
return days;
|
|
148
150
|
}
|
|
149
151
|
|
|
150
|
-
async readAllArtifactsCached(): Promise<MemoryFile[]> {
|
|
152
|
+
async readAllArtifactsCached(options?: CorpusReadOptions): Promise<MemoryFile[]> {
|
|
153
|
+
checkCorpusReadAbort(options);
|
|
151
154
|
if (
|
|
152
155
|
this.deps.artifactIndexCache &&
|
|
153
156
|
Date.now() - this.deps.artifactIndexCache.loadedAtMs <= this.deps.storageManagerClass.ARTIFACT_INDEX_CACHE_TTL_MS &&
|
|
@@ -162,6 +165,10 @@ export class MemoryReadStore {
|
|
|
162
165
|
try {
|
|
163
166
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
164
167
|
for (const entry of entries) {
|
|
168
|
+
// Per-entry, so a tree of any shape has a checkpoint: this scan has no
|
|
169
|
+
// in-flight dedup, so every abandoned recall used to start another
|
|
170
|
+
// full walk of it (issue #2307).
|
|
171
|
+
checkCorpusReadAbort(options);
|
|
165
172
|
const fullPath = path.join(dir, entry.name);
|
|
166
173
|
if (entry.isDirectory()) {
|
|
167
174
|
await readDir(fullPath);
|
|
@@ -172,7 +179,10 @@ export class MemoryReadStore {
|
|
|
172
179
|
if (!memory) continue;
|
|
173
180
|
artifacts.push(memory);
|
|
174
181
|
}
|
|
175
|
-
} catch {
|
|
182
|
+
} catch (err) {
|
|
183
|
+
// A cancelled scan must surface, not be swallowed as "directory doesn't
|
|
184
|
+
// exist yet" — otherwise a partial walk would be cached as complete.
|
|
185
|
+
if (isAbortError(err)) throw err;
|
|
176
186
|
// Directory doesn't exist yet
|
|
177
187
|
}
|
|
178
188
|
};
|
|
@@ -183,10 +193,14 @@ export class MemoryReadStore {
|
|
|
183
193
|
const MAX_REBUILD_RETRIES = 2;
|
|
184
194
|
let latestArtifacts: MemoryFile[] = [];
|
|
185
195
|
for (let attempt = 0; attempt <= MAX_REBUILD_RETRIES; attempt += 1) {
|
|
196
|
+
checkCorpusReadAbort(options);
|
|
186
197
|
const versionBefore = this.deps.getArtifactWriteVersion();
|
|
187
198
|
const artifacts = await scanArtifacts();
|
|
188
199
|
const versionAfter = this.deps.getArtifactWriteVersion();
|
|
189
200
|
latestArtifacts = artifacts;
|
|
201
|
+
// A signal that fired during the last file read of the walk would otherwise
|
|
202
|
+
// publish this scan and return it as if uncancelled (issue #2307 review).
|
|
203
|
+
checkCorpusReadAbort(options);
|
|
190
204
|
if (versionAfter === versionBefore) {
|
|
191
205
|
this.deps.artifactIndexCache = { memories: artifacts, loadedAtMs: Date.now(), writeVersion: versionAfter };
|
|
192
206
|
return artifacts;
|
|
@@ -210,6 +224,7 @@ export class MemoryReadStore {
|
|
|
210
224
|
private async collectContainedMarkdownPaths(
|
|
211
225
|
startDirs: readonly string[],
|
|
212
226
|
propagateReadErrors = false,
|
|
227
|
+
options?: CorpusReadOptions,
|
|
213
228
|
): Promise<string[]> {
|
|
214
229
|
const filePaths: string[] = [];
|
|
215
230
|
|
|
@@ -226,6 +241,7 @@ export class MemoryReadStore {
|
|
|
226
241
|
}
|
|
227
242
|
|
|
228
243
|
const collectPaths = async (dir: string): Promise<void> => {
|
|
244
|
+
checkCorpusReadAbort(options);
|
|
229
245
|
// Directory-level guard, isolated from per-entry handling: skip symlinked
|
|
230
246
|
// or non-directory category dirs and assert the resolved dir stays inside
|
|
231
247
|
// the memory root before reading. A failure here means the whole subtree
|
|
@@ -264,6 +280,10 @@ export class MemoryReadStore {
|
|
|
264
280
|
|
|
265
281
|
const subdirs: string[] = [];
|
|
266
282
|
for (const entry of entries) {
|
|
283
|
+
// Outside the per-entry try/catch below, which deliberately swallows
|
|
284
|
+
// non-propagatable errors — a cancellation must never be swallowed with
|
|
285
|
+
// them (issue #2307).
|
|
286
|
+
checkCorpusReadAbort(options);
|
|
267
287
|
// Never follow symlinked entries out of the store.
|
|
268
288
|
if (entry.isSymbolicLink()) continue;
|
|
269
289
|
const fullPath = path.join(dir, entry.name);
|
|
@@ -297,10 +317,15 @@ export class MemoryReadStore {
|
|
|
297
317
|
for (const dir of startDirs) {
|
|
298
318
|
await collectPaths(dir);
|
|
299
319
|
}
|
|
320
|
+
// A signal that fired during the last directory read would otherwise return a
|
|
321
|
+
// truncated path list as if it were the whole corpus (issue #2307 review).
|
|
322
|
+
checkCorpusReadAbort(options);
|
|
300
323
|
return filePaths;
|
|
301
324
|
}
|
|
302
325
|
|
|
303
|
-
async collectActiveMemoryPaths(
|
|
326
|
+
async collectActiveMemoryPaths(
|
|
327
|
+
options?: { propagateReadErrors?: boolean } & CorpusReadOptions,
|
|
328
|
+
): Promise<string[]> {
|
|
304
329
|
// Scan EVERY supported memory category directory, not just the legacy four
|
|
305
330
|
// (facts/procedures/reasoning-traces/corrections). Issue #1497: the QMD
|
|
306
331
|
// filesystem-fallback recall path (orchestrator `recent_scan` ->
|
|
@@ -330,6 +355,7 @@ export class MemoryReadStore {
|
|
|
330
355
|
return this.collectContainedMarkdownPaths(
|
|
331
356
|
RECALL_FALLBACK_DIRS.map((dir) => path.join(this.deps.baseDir, dir)),
|
|
332
357
|
options?.propagateReadErrors ?? false,
|
|
358
|
+
options,
|
|
333
359
|
);
|
|
334
360
|
}
|
|
335
361
|
|
|
@@ -342,10 +368,13 @@ export class MemoryReadStore {
|
|
|
342
368
|
* of the recall fallback corpus (collectActiveMemoryPaths), whose scan is
|
|
343
369
|
* intentionally unchanged.
|
|
344
370
|
*/
|
|
345
|
-
async collectColdMemoryPaths(
|
|
371
|
+
async collectColdMemoryPaths(
|
|
372
|
+
options?: { propagateReadErrors?: boolean } & CorpusReadOptions,
|
|
373
|
+
): Promise<string[]> {
|
|
346
374
|
return this.collectContainedMarkdownPaths(
|
|
347
375
|
[this.deps.resolveTierRootDir("cold")],
|
|
348
376
|
options?.propagateReadErrors ?? false,
|
|
377
|
+
options,
|
|
349
378
|
);
|
|
350
379
|
}
|
|
351
380
|
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #2307 — the shared full-corpus read primitives honour cancellation.
|
|
3
|
+
*
|
|
4
|
+
* Scenario matrix, applied to every primitive that walks a whole memory tree:
|
|
5
|
+
* 1. no signal → unchanged behaviour
|
|
6
|
+
* 2. signal that never fires → unchanged behaviour
|
|
7
|
+
* 3. signal already aborted → throws, nothing read
|
|
8
|
+
* 4. signal fires mid-scan → throws, and NO partial corpus is cached
|
|
9
|
+
* 5. coalesced read with a joiner → the starter's abort must not cancel it
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
15
|
+
import os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
|
|
18
|
+
import { StorageManager } from "../storage.js";
|
|
19
|
+
import { resetStaticCaches } from "./harness.js";
|
|
20
|
+
|
|
21
|
+
const isAbort = (err: unknown) => err instanceof Error && err.name === "AbortError";
|
|
22
|
+
|
|
23
|
+
async function withStorage(
|
|
24
|
+
prefix: string,
|
|
25
|
+
body: (sm: StorageManager, dir: string) => Promise<void>,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), prefix));
|
|
28
|
+
try {
|
|
29
|
+
resetStaticCaches();
|
|
30
|
+
const sm = new StorageManager(dir);
|
|
31
|
+
await sm.ensureDirectories();
|
|
32
|
+
await body(sm, dir);
|
|
33
|
+
} finally {
|
|
34
|
+
resetStaticCaches();
|
|
35
|
+
await rm(dir, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Counts disk scans so "nothing was cached" is observable rather than assumed. */
|
|
40
|
+
function installScanSpy(sm: StorageManager): { scans: () => number } {
|
|
41
|
+
const spy = sm as unknown as { _readAllMemoriesFromDisk: (...args: unknown[]) => Promise<unknown> };
|
|
42
|
+
const orig = spy._readAllMemoriesFromDisk.bind(sm);
|
|
43
|
+
let count = 0;
|
|
44
|
+
spy._readAllMemoriesFromDisk = async (...args: unknown[]) => {
|
|
45
|
+
count += 1;
|
|
46
|
+
return orig(...args);
|
|
47
|
+
};
|
|
48
|
+
return { scans: () => count };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Hold the first disk scan open so a joiner provably attaches while it is in
|
|
53
|
+
* flight, and guarantee teardown: a failing assertion must not leave the patched
|
|
54
|
+
* method installed or a scan blocked for the rest of the run.
|
|
55
|
+
*/
|
|
56
|
+
function holdFirstScan(sm: StorageManager): {
|
|
57
|
+
reached: Promise<void>;
|
|
58
|
+
release: () => void;
|
|
59
|
+
restore: () => void;
|
|
60
|
+
} {
|
|
61
|
+
const scanReached = Promise.withResolvers<void>();
|
|
62
|
+
const releaseScan = Promise.withResolvers<void>();
|
|
63
|
+
const inner = sm as unknown as {
|
|
64
|
+
collectActiveMemoryPaths: (...args: unknown[]) => Promise<string[]>;
|
|
65
|
+
};
|
|
66
|
+
const orig = inner.collectActiveMemoryPaths.bind(sm);
|
|
67
|
+
let held = false;
|
|
68
|
+
inner.collectActiveMemoryPaths = async (...args: unknown[]) => {
|
|
69
|
+
if (!held) {
|
|
70
|
+
held = true;
|
|
71
|
+
scanReached.resolve();
|
|
72
|
+
await releaseScan.promise;
|
|
73
|
+
}
|
|
74
|
+
return orig(...args);
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
reached: scanReached.promise,
|
|
78
|
+
release: () => releaseScan.resolve(),
|
|
79
|
+
restore: () => {
|
|
80
|
+
releaseScan.resolve();
|
|
81
|
+
inner.collectActiveMemoryPaths = orig;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
test("readAllMemories: an unfired signal changes nothing", async () => {
|
|
87
|
+
await withStorage("remnic-2307-memories-inert-", async (sm) => {
|
|
88
|
+
await sm.writeMemory("fact", "inert signal memory");
|
|
89
|
+
const memories = await sm.readAllMemories({ abortSignal: new AbortController().signal });
|
|
90
|
+
assert.ok(memories.some((m) => m.content === "inert signal memory"));
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("readAllMemories: an already-aborted caller reads nothing at all", async () => {
|
|
95
|
+
await withStorage("remnic-2307-memories-preabort-", async (sm) => {
|
|
96
|
+
await sm.writeMemory("fact", "pre-abort memory");
|
|
97
|
+
sm.invalidateAllMemoriesCacheForDir();
|
|
98
|
+
const spy = installScanSpy(sm);
|
|
99
|
+
const aborted = new AbortController();
|
|
100
|
+
aborted.abort();
|
|
101
|
+
|
|
102
|
+
await assert.rejects(sm.readAllMemories({ abortSignal: aborted.signal }), isAbort);
|
|
103
|
+
assert.equal(spy.scans(), 0, "an abandoned caller must not start a disk scan");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("readAllMemories: a mid-scan abort caches nothing", async () => {
|
|
108
|
+
await withStorage("remnic-2307-memories-midscan-", async (sm) => {
|
|
109
|
+
await sm.writeMemory("fact", "mid-scan memory");
|
|
110
|
+
sm.invalidateAllMemoriesCacheForDir();
|
|
111
|
+
|
|
112
|
+
// Abort once the walk has produced paths: the next per-batch checkpoint in the
|
|
113
|
+
// parse phase is a real boundary, not a synthetic one.
|
|
114
|
+
const aborted = new AbortController();
|
|
115
|
+
const inner = sm as unknown as {
|
|
116
|
+
collectActiveMemoryPaths: (...args: unknown[]) => Promise<string[]>;
|
|
117
|
+
};
|
|
118
|
+
const origCollect = inner.collectActiveMemoryPaths.bind(sm);
|
|
119
|
+
inner.collectActiveMemoryPaths = async (...args: unknown[]) => {
|
|
120
|
+
const paths = await origCollect(...args);
|
|
121
|
+
aborted.abort();
|
|
122
|
+
return paths;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
await assert.rejects(sm.readAllMemories({ abortSignal: aborted.signal }), isAbort);
|
|
126
|
+
|
|
127
|
+
// The publish step is downstream of the checkpoint, so a truncated corpus must
|
|
128
|
+
// never have reached the cache: a fresh read has to scan again.
|
|
129
|
+
inner.collectActiveMemoryPaths = origCollect;
|
|
130
|
+
const spy = installScanSpy(sm);
|
|
131
|
+
const after = await sm.readAllMemories();
|
|
132
|
+
assert.equal(spy.scans(), 1, "the aborted read must not have published a cache entry");
|
|
133
|
+
assert.ok(after.some((m) => m.content === "mid-scan memory"), "the corpus is intact");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("readAllMemories: a second reader protects the coalesced scan from the starter's abort", async () => {
|
|
138
|
+
await withStorage("remnic-2307-memories-joiner-", async (sm) => {
|
|
139
|
+
await sm.writeMemory("fact", "joined memory");
|
|
140
|
+
sm.invalidateAllMemoriesCacheForDir();
|
|
141
|
+
|
|
142
|
+
const hold = holdFirstScan(sm);
|
|
143
|
+
const spy = installScanSpy(sm);
|
|
144
|
+
const starterAbort = new AbortController();
|
|
145
|
+
const starter = sm.readAllMemories({ abortSignal: starterAbort.signal });
|
|
146
|
+
// Observe the starter now: its rejection lands the instant it aborts, and this
|
|
147
|
+
// test awaits the joiner in between, so an unobserved promise would surface as
|
|
148
|
+
// an unhandled rejection rather than the assertion below.
|
|
149
|
+
const starterOutcome = starter.then(
|
|
150
|
+
(value) => ({ ok: true as const, value }),
|
|
151
|
+
(error: unknown) => ({ ok: false as const, error }),
|
|
152
|
+
);
|
|
153
|
+
try {
|
|
154
|
+
await hold.reached;
|
|
155
|
+
const joiner = sm.readAllMemories();
|
|
156
|
+
|
|
157
|
+
starterAbort.abort();
|
|
158
|
+
hold.release();
|
|
159
|
+
|
|
160
|
+
const joined = await joiner;
|
|
161
|
+
assert.ok(
|
|
162
|
+
joined.some((m) => m.content === "joined memory"),
|
|
163
|
+
"a joiner must never be handed a cancelled or truncated read",
|
|
164
|
+
);
|
|
165
|
+
// The joiner keeps the scan alive, so it completes — but the starter still
|
|
166
|
+
// gets its own AbortError, because it asked to be cancelled.
|
|
167
|
+
const outcome = await starterOutcome;
|
|
168
|
+
assert.equal(outcome.ok, false, "the starter must not be handed data it cancelled");
|
|
169
|
+
assert.ok(!outcome.ok && isAbort(outcome.error));
|
|
170
|
+
assert.equal(spy.scans(), 1, "the joiner reused the in-flight scan rather than starting its own");
|
|
171
|
+
} finally {
|
|
172
|
+
hold.restore();
|
|
173
|
+
await starterOutcome;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("readAllMemories: a joiner arriving after the starter aborted starts a fresh scan", async () => {
|
|
179
|
+
await withStorage("remnic-2307-memories-late-joiner-", async (sm) => {
|
|
180
|
+
await sm.writeMemory("fact", "late joiner memory");
|
|
181
|
+
sm.invalidateAllMemoriesCacheForDir();
|
|
182
|
+
|
|
183
|
+
// The starter's scan is doomed the instant its signal fires while it is the
|
|
184
|
+
// sole waiter. Withdrawing it from the registry first is what stops a late
|
|
185
|
+
// joiner inheriting that AbortError for a request it never cancelled.
|
|
186
|
+
const hold = holdFirstScan(sm);
|
|
187
|
+
const starterAbort = new AbortController();
|
|
188
|
+
const starter = sm.readAllMemories({ abortSignal: starterAbort.signal });
|
|
189
|
+
try {
|
|
190
|
+
await hold.reached;
|
|
191
|
+
starterAbort.abort();
|
|
192
|
+
|
|
193
|
+
// Joins only AFTER the abort — must not attach to the doomed scan.
|
|
194
|
+
const lateJoiner = sm.readAllMemories();
|
|
195
|
+
hold.release();
|
|
196
|
+
|
|
197
|
+
await assert.rejects(starter, isAbort);
|
|
198
|
+
const joined = await lateJoiner;
|
|
199
|
+
assert.ok(
|
|
200
|
+
joined.some((m) => m.content === "late joiner memory"),
|
|
201
|
+
"a late joiner must get its own scan, not the starter's AbortError",
|
|
202
|
+
);
|
|
203
|
+
} finally {
|
|
204
|
+
hold.restore();
|
|
205
|
+
await starter.catch(() => {});
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("readAllMemories: a joiner honours its own signal without cancelling the shared scan", async () => {
|
|
211
|
+
await withStorage("remnic-2307-memories-joiner-abort-", async (sm) => {
|
|
212
|
+
await sm.writeMemory("fact", "shared scan memory");
|
|
213
|
+
sm.invalidateAllMemoriesCacheForDir();
|
|
214
|
+
|
|
215
|
+
const hold = holdFirstScan(sm);
|
|
216
|
+
const starter = sm.readAllMemories();
|
|
217
|
+
try {
|
|
218
|
+
await hold.reached;
|
|
219
|
+
const joinerAbort = new AbortController();
|
|
220
|
+
const joiner = sm.readAllMemories({ abortSignal: joinerAbort.signal });
|
|
221
|
+
joinerAbort.abort();
|
|
222
|
+
|
|
223
|
+
// The joiner stops waiting immediately, before the scan is even released.
|
|
224
|
+
await assert.rejects(joiner, isAbort);
|
|
225
|
+
|
|
226
|
+
hold.release();
|
|
227
|
+
const starterResult = await starter;
|
|
228
|
+
assert.ok(
|
|
229
|
+
starterResult.some((m) => m.content === "shared scan memory"),
|
|
230
|
+
"a joiner's cancellation must never reach the shared scan",
|
|
231
|
+
);
|
|
232
|
+
} finally {
|
|
233
|
+
hold.restore();
|
|
234
|
+
await starter.catch(() => {});
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("readAllMemories: the scan is cancelled once its LAST waiter leaves", async () => {
|
|
240
|
+
await withStorage("remnic-2307-memories-last-waiter-", async (sm) => {
|
|
241
|
+
await sm.writeMemory("fact", "last waiter memory");
|
|
242
|
+
sm.invalidateAllMemoriesCacheForDir();
|
|
243
|
+
|
|
244
|
+
// Two cancellable readers overlap and both give up. Neither may cancel alone,
|
|
245
|
+
// but with nobody left the scan must not run on for no one — that is the
|
|
246
|
+
// abandoned I/O this whole change removes.
|
|
247
|
+
const hold = holdFirstScan(sm);
|
|
248
|
+
const starterAbort = new AbortController();
|
|
249
|
+
const joinerAbort = new AbortController();
|
|
250
|
+
const starter = sm.readAllMemories({ abortSignal: starterAbort.signal });
|
|
251
|
+
try {
|
|
252
|
+
await hold.reached;
|
|
253
|
+
const joiner = sm.readAllMemories({ abortSignal: joinerAbort.signal });
|
|
254
|
+
|
|
255
|
+
// First to leave must NOT cancel: the other reader still wants the result.
|
|
256
|
+
joinerAbort.abort();
|
|
257
|
+
await assert.rejects(joiner, isAbort);
|
|
258
|
+
starterAbort.abort();
|
|
259
|
+
hold.release();
|
|
260
|
+
|
|
261
|
+
await assert.rejects(starter, isAbort);
|
|
262
|
+
|
|
263
|
+
// With the scan cancelled rather than completed, nothing was published: a
|
|
264
|
+
// fresh read has to scan again.
|
|
265
|
+
hold.restore();
|
|
266
|
+
const spy = installScanSpy(sm);
|
|
267
|
+
const after = await sm.readAllMemories();
|
|
268
|
+
assert.equal(spy.scans(), 1, "the abandoned scan must not have published a cache entry");
|
|
269
|
+
assert.ok(after.some((m) => m.content === "last waiter memory"));
|
|
270
|
+
} finally {
|
|
271
|
+
hold.restore();
|
|
272
|
+
await starter.catch(() => {});
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("readAllMemories: a sole waiter's abort does stop the scan", async () => {
|
|
278
|
+
await withStorage("remnic-2307-memories-sole-", async (sm) => {
|
|
279
|
+
await sm.writeMemory("fact", "sole waiter memory");
|
|
280
|
+
sm.invalidateAllMemoriesCacheForDir();
|
|
281
|
+
|
|
282
|
+
const hold = holdFirstScan(sm);
|
|
283
|
+
const aborted = new AbortController();
|
|
284
|
+
const pending = sm.readAllMemories({ abortSignal: aborted.signal });
|
|
285
|
+
try {
|
|
286
|
+
await hold.reached;
|
|
287
|
+
aborted.abort();
|
|
288
|
+
hold.release();
|
|
289
|
+
await assert.rejects(pending, isAbort);
|
|
290
|
+
} finally {
|
|
291
|
+
hold.restore();
|
|
292
|
+
await pending.catch(() => {});
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("readAllEntityFiles: an already-aborted caller reads nothing and caches nothing", async () => {
|
|
298
|
+
await withStorage("remnic-2307-entities-", async (sm) => {
|
|
299
|
+
await sm.writeEntity("Cancelled Person", "person", ["Cancelled Person owns this test."]);
|
|
300
|
+
|
|
301
|
+
let entityFileReads = 0;
|
|
302
|
+
const inner = sm as unknown as {
|
|
303
|
+
readStorageSecureFile: (filePath: string) => Promise<string>;
|
|
304
|
+
};
|
|
305
|
+
const origRead = inner.readStorageSecureFile.bind(sm);
|
|
306
|
+
inner.readStorageSecureFile = async (filePath: string) => {
|
|
307
|
+
entityFileReads += 1;
|
|
308
|
+
return origRead(filePath);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const aborted = new AbortController();
|
|
312
|
+
aborted.abort();
|
|
313
|
+
await assert.rejects(sm.readAllEntityFiles({ abortSignal: aborted.signal }), isAbort);
|
|
314
|
+
assert.equal(entityFileReads, 0, "an abandoned caller must not read entity files");
|
|
315
|
+
|
|
316
|
+
// Nothing was published, so the corpus still reads correctly afterwards.
|
|
317
|
+
const entities = await sm.readAllEntityFiles();
|
|
318
|
+
assert.ok(entities.some((e) => e.name === "Cancelled Person"));
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("readAllEntityFiles: a mid-scan abort publishes no partial entity cache", async () => {
|
|
323
|
+
await withStorage("remnic-2307-entities-midscan-", async (sm) => {
|
|
324
|
+
await sm.writeEntity("First Person", "person", ["First Person owns this test."]);
|
|
325
|
+
await sm.writeEntity("Second Person", "person", ["Second Person owns this test too."]);
|
|
326
|
+
|
|
327
|
+
// Abort once the first entity file has been read: the post-loop checkpoint is
|
|
328
|
+
// what stops that partial batch reaching setCachedEntities.
|
|
329
|
+
const aborted = new AbortController();
|
|
330
|
+
const inner = sm as unknown as {
|
|
331
|
+
readStorageSecureFile: (filePath: string) => Promise<string>;
|
|
332
|
+
};
|
|
333
|
+
const origRead = inner.readStorageSecureFile.bind(sm);
|
|
334
|
+
let reads = 0;
|
|
335
|
+
inner.readStorageSecureFile = async (filePath: string) => {
|
|
336
|
+
reads += 1;
|
|
337
|
+
aborted.abort();
|
|
338
|
+
return origRead(filePath);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
await assert.rejects(sm.readAllEntityFiles({ abortSignal: aborted.signal }), isAbort);
|
|
342
|
+
assert.ok(reads >= 1, "the scan started before it was cancelled");
|
|
343
|
+
|
|
344
|
+
inner.readStorageSecureFile = origRead;
|
|
345
|
+
const entities = await sm.readAllEntityFiles();
|
|
346
|
+
assert.deepEqual(
|
|
347
|
+
entities.map((e) => e.name).sort(),
|
|
348
|
+
["First Person", "Second Person"],
|
|
349
|
+
"the aborted scan must not have cached a partial entity set",
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("searchArtifacts: an already-aborted caller never reads the artifact tier", async () => {
|
|
355
|
+
await withStorage("remnic-2307-artifacts-preabort-", async (sm) => {
|
|
356
|
+
await sm.writeArtifact("deploy runbook rollback quote", { sourceMemoryId: "src-1" });
|
|
357
|
+
|
|
358
|
+
let artifactReads = 0;
|
|
359
|
+
const inner = sm as unknown as {
|
|
360
|
+
readMemoryByPath: (filePath: string) => Promise<unknown>;
|
|
361
|
+
};
|
|
362
|
+
const origRead = inner.readMemoryByPath.bind(sm);
|
|
363
|
+
inner.readMemoryByPath = async (filePath: string) => {
|
|
364
|
+
artifactReads += 1;
|
|
365
|
+
return origRead(filePath);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const aborted = new AbortController();
|
|
369
|
+
aborted.abort();
|
|
370
|
+
await assert.rejects(
|
|
371
|
+
sm.searchArtifacts("deploy runbook", 5, { abortSignal: aborted.signal }),
|
|
372
|
+
isAbort,
|
|
373
|
+
);
|
|
374
|
+
assert.equal(artifactReads, 0, "an abandoned caller must not walk the artifact tier");
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("searchArtifacts: a mid-walk abort caches nothing", async () => {
|
|
379
|
+
await withStorage("remnic-2307-artifacts-midwalk-", async (sm) => {
|
|
380
|
+
await sm.writeArtifact("deploy runbook rollback quote", { sourceMemoryId: "src-1" });
|
|
381
|
+
await sm.writeArtifact("second deploy runbook quote", { sourceMemoryId: "src-2" });
|
|
382
|
+
|
|
383
|
+
// Abort from inside the walk, at the per-entry checkpoint.
|
|
384
|
+
const aborted = new AbortController();
|
|
385
|
+
const inner = sm as unknown as {
|
|
386
|
+
readMemoryByPath: (filePath: string) => Promise<unknown>;
|
|
387
|
+
};
|
|
388
|
+
const origRead = inner.readMemoryByPath.bind(sm);
|
|
389
|
+
let reads = 0;
|
|
390
|
+
inner.readMemoryByPath = async (filePath: string) => {
|
|
391
|
+
reads += 1;
|
|
392
|
+
aborted.abort();
|
|
393
|
+
return origRead(filePath);
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
await assert.rejects(
|
|
397
|
+
sm.searchArtifacts("deploy runbook", 5, { abortSignal: aborted.signal }),
|
|
398
|
+
isAbort,
|
|
399
|
+
);
|
|
400
|
+
assert.ok(reads >= 1, "the walk started before it was cancelled");
|
|
401
|
+
|
|
402
|
+
// A partial walk must not have been published as the artifact index: the next
|
|
403
|
+
// search sees every artifact.
|
|
404
|
+
inner.readMemoryByPath = origRead;
|
|
405
|
+
const matches = await sm.searchArtifacts("deploy runbook", 5);
|
|
406
|
+
assert.equal(matches.length, 2, "the aborted walk must not have cached a partial tier");
|
|
407
|
+
});
|
|
408
|
+
});
|