@tangle-network/agent-knowledge 23.0.0 → 23.0.2

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.
@@ -194,7 +194,8 @@ async function listRegularFileMetadataWithinRoot(root, relativeDirectory) {
194
194
  stat.size,
195
195
  stat.mtimeNs,
196
196
  stat.ctimeNs
197
- ].join(":")
197
+ ].join(":"),
198
+ changedNs: stat.ctimeNs
198
199
  });
199
200
  }
200
201
  }
@@ -1210,6 +1211,10 @@ var KnowledgeLockLostError = class extends Error {
1210
1211
  function isKnowledgeMutationHeld(root) {
1211
1212
  return activeRoots.getStore()?.get(resolve(root))?.active === true;
1212
1213
  }
1214
+ /** Whether this context already reads or mutates `root`, so it must never wait on another reader. */
1215
+ function isKnowledgeRootHeld(root) {
1216
+ return isKnowledgeMutationHeld(root) || activeReadRoots.getStore()?.has(resolve(root)) === true;
1217
+ }
1213
1218
  async function withKnowledgeMutation(root, mutate, options = {}) {
1214
1219
  const resolvedRoot = resolve(root);
1215
1220
  const existing = activeRoots.getStore()?.get(resolvedRoot);
@@ -2354,25 +2359,127 @@ async function initKnowledgeBaseUnlocked(root) {
2354
2359
  return layout;
2355
2360
  }
2356
2361
  async function loadKnowledgePages(root, options = {}) {
2357
- return withKnowledgeRead(root, () => loadKnowledgePagesUnlocked(root, options));
2362
+ const { views } = await readKnowledgePageViews([root], options);
2363
+ return views[0].pages.map((page) => structuredClone(page));
2358
2364
  }
2359
- async function loadKnowledgePagesUnlocked(root, options) {
2365
+ const MAX_CACHED_ROOTS = 8;
2366
+ const MAX_CACHED_SOURCE_BYTES = 67108864;
2367
+ const RACY_NS = 1000000000n;
2368
+ const cachedViews = /* @__PURE__ */ new Map();
2369
+ const cachedGroups = /* @__PURE__ */ new Map();
2370
+ const refreshes = /* @__PURE__ */ new Map();
2371
+ /**
2372
+ * Internal. Each root's pages under one pages directory; a file is read and parsed again only when
2373
+ * its identity (device, inode, size, mtime, ctime) changed, so a read of an unchanged store reads
2374
+ * no page. `pagesRead` counts the files this call's refreshes read from disk. Beyond a root and byte
2375
+ * bound, the least recently read roots are evicted, never one read with this call's roots.
2376
+ */
2377
+ async function readKnowledgePageViews(roots, options = {}) {
2360
2378
  const pagesDirectory = normalizePagesDirectory(options.pagesDirectory);
2361
- let files;
2362
- try {
2363
- files = await listRegularFilesWithinRoot(root, pagesDirectory);
2364
- } catch (error) {
2365
- if (isMissingFile(error)) return [];
2366
- throw error;
2379
+ const keys = roots.map((root) => JSON.stringify([resolve(root), pagesDirectory]));
2380
+ const loaded = await Promise.all(roots.map((root, index) => refreshView(keys[index], resolve(root), pagesDirectory)));
2381
+ if (keys.length > 1) for (const key of keys) cachedGroups.set(key, keys);
2382
+ const live = new Set(keys.flatMap((key) => cachedGroups.get(key) ?? [key]));
2383
+ let bytes = 0;
2384
+ for (const view of cachedViews.values()) bytes += view.bytes;
2385
+ for (const [key, view] of cachedViews) {
2386
+ if (cachedViews.size <= MAX_CACHED_ROOTS && bytes <= MAX_CACHED_SOURCE_BYTES) break;
2387
+ if (live.has(key)) continue;
2388
+ cachedViews.delete(key);
2389
+ cachedGroups.delete(key);
2390
+ bytes -= view.bytes;
2367
2391
  }
2368
- const pages = [];
2369
- for (const file of files) {
2370
- const rel = file.path.replace(/\\/g, "/");
2371
- if (!isKnowledgePagePath(rel)) continue;
2372
- pages.push(knowledgePageFromMarkdown(rel, file.bytes.toString("utf8"), pagesDirectory));
2392
+ return {
2393
+ views: loaded.map((item) => item.view),
2394
+ pagesRead: loaded.reduce((sum, item) => sum + item.pagesRead, 0)
2395
+ };
2396
+ }
2397
+ function refreshView(key, root, pagesDirectory) {
2398
+ const start = () => loadView(root, pagesDirectory, cachedViews.get(key)).then((loaded) => {
2399
+ cachedViews.delete(key);
2400
+ cachedViews.set(key, loaded.view);
2401
+ return loaded;
2402
+ });
2403
+ if (isKnowledgeRootHeld(root)) return start();
2404
+ const state = refreshes.get(key) ?? {};
2405
+ refreshes.set(key, state);
2406
+ state.next ??= (state.running ?? Promise.resolve()).catch(() => void 0).then(() => {
2407
+ state.next = void 0;
2408
+ const running = start().finally(() => {
2409
+ if (state.running === running) state.running = void 0;
2410
+ if (!state.running && !state.next) refreshes.delete(key);
2411
+ });
2412
+ state.running = running;
2413
+ return running;
2414
+ });
2415
+ return state.next;
2416
+ }
2417
+ function loadView(root, pagesDirectory, previous) {
2418
+ return withKnowledgeRead(root, async () => {
2419
+ const recent = BigInt(Date.now()) * 1000000n - RACY_NS;
2420
+ const listed = await listRegularFileMetadataWithinRoot(root, pagesDirectory).catch((error) => isMissingFile(error) ? [] : Promise.reject(error));
2421
+ const trusted = (file) => file.changedNs < recent ? file.generation : "";
2422
+ const generation = listed.every((file) => trusted(file) !== "") ? JSON.stringify(listed.map((file) => [file.path, file.generation])) : void 0;
2423
+ if (generation !== void 0 && previous?.generation === generation) return {
2424
+ view: previous,
2425
+ pagesRead: 0
2426
+ };
2427
+ const files = /* @__PURE__ */ new Map();
2428
+ const changed = [];
2429
+ for (const file of listed) {
2430
+ const known = previous?.files.get(file.path);
2431
+ if (known?.generation === file.generation) files.set(file.path, known);
2432
+ else if (isKnowledgePagePath(file.path)) changed.push(file);
2433
+ }
2434
+ const parse = (file, bytes) => {
2435
+ const page = knowledgePageFromMarkdown(file.path, bytes.toString("utf8"), pagesDirectory);
2436
+ const known = previous?.files.get(file.path)?.page;
2437
+ const same = known?.text === page.text && JSON.stringify(known.frontmatter) === JSON.stringify(page.frontmatter);
2438
+ files.set(file.path, {
2439
+ generation: trusted(file),
2440
+ page: same ? known : freezeView(page)
2441
+ });
2442
+ };
2443
+ let pagesRead = changed.length;
2444
+ if (changed.length > listed.length / 2) {
2445
+ const wanted = new Map(changed.map((file) => [file.path, file]));
2446
+ const walked = await listRegularFilesWithinRoot(root, pagesDirectory);
2447
+ for (const file of walked) if (wanted.has(file.path)) parse(wanted.get(file.path), file.bytes);
2448
+ pagesRead = walked.length;
2449
+ } else await mapFileIo(changed, async (file) => {
2450
+ try {
2451
+ parse(file, (await readRegularFileWithinRoot(root, file.path)).bytes);
2452
+ } catch (error) {
2453
+ if (!isMissingFile(error)) throw error;
2454
+ }
2455
+ });
2456
+ const pages = [...files.values()].map((file) => file.page).sort((a, b) => a.path.localeCompare(b.path));
2457
+ const bytes = listed.reduce((sum, file) => sum + file.bytes, 0);
2458
+ if (previous?.pages.length === pages.length && pages.every((p, i) => p === previous.pages[i])) return {
2459
+ view: Object.assign(previous, {
2460
+ generation,
2461
+ bytes,
2462
+ files
2463
+ }),
2464
+ pagesRead
2465
+ };
2466
+ return {
2467
+ view: {
2468
+ pages: Object.freeze(pages),
2469
+ generation,
2470
+ bytes,
2471
+ files
2472
+ },
2473
+ pagesRead
2474
+ };
2475
+ });
2476
+ }
2477
+ function freezeView(value) {
2478
+ if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
2479
+ for (const child of Object.values(value)) freezeView(child);
2480
+ Object.freeze(value);
2373
2481
  }
2374
- pages.sort((a, b) => a.path.localeCompare(b.path));
2375
- return pages;
2482
+ return value;
2376
2483
  }
2377
2484
  /** True when a root-relative path names authored Markdown rather than scaffolding. */
2378
2485
  function isKnowledgePagePath(path) {
@@ -4154,25 +4261,22 @@ function prepareKnowledgeValidation(index) {
4154
4261
  * the same report on every run.
4155
4262
  */
4156
4263
  function detectNearDuplicatePages(pages, options = {}) {
4157
- const threshold = finiteUnitInterval(options.threshold ?? .82, "threshold");
4158
- const minCharacters = nonNegativeInteger(options.minCharacters ?? 80, "minCharacters");
4159
- const wordShingleSize = positiveInteger(options.wordShingleSize ?? 5, "wordShingleSize");
4160
- const characterShingleSize = positiveInteger(options.characterShingleSize ?? 16, "characterShingleSize");
4161
- const maxDocumentFrequency = positiveInteger(options.maxDocumentFrequency ?? 64, "maxDocumentFrequency");
4264
+ const { threshold, minCharacters, wordShingleSize, characterShingleSize, maxDocumentFrequency } = settings(options);
4162
4265
  const maxCandidatePairs = positiveInteger(options.maxCandidatePairs ?? 25e4, "maxCandidatePairs");
4163
4266
  const maxReportedPairs = nonNegativeInteger(options.maxReportedPairs ?? 1e3, "maxReportedPairs");
4164
4267
  const prepared = pages.map((page) => preparePage(page, wordShingleSize, characterShingleSize)).filter((item) => item !== null && item.normalized.length >= minCharacters).sort((left, right) => left.page.path.localeCompare(right.page.path));
4165
4268
  const candidateKeys = /* @__PURE__ */ new Set();
4166
4269
  const exactGroups = /* @__PURE__ */ new Map();
4167
4270
  const byShingle = /* @__PURE__ */ new Map();
4271
+ const group = (groups, key) => groups.get(key) ?? groups.set(key, []).get(key);
4168
4272
  for (const [index, item] of prepared.entries()) {
4169
- exactGroups.set(item.normalized, [...exactGroups.get(item.normalized) ?? [], index]);
4170
- for (const shingle of item.shingles) byShingle.set(shingle, [...byShingle.get(shingle) ?? [], index]);
4273
+ group(exactGroups, item.normalized).push(index);
4274
+ for (const shingle of item.shingles) group(byShingle, shingle).push(index);
4171
4275
  }
4172
4276
  let truncated = false;
4173
4277
  const addPair = (left, right) => {
4174
4278
  if (left === right) return true;
4175
- const key = pairKey(Math.min(left, right), Math.max(left, right));
4279
+ const key = Math.min(left, right) * prepared.length + Math.max(left, right);
4176
4280
  if (candidateKeys.has(key)) return true;
4177
4281
  if (candidateKeys.size >= maxCandidatePairs) {
4178
4282
  truncated = true;
@@ -4191,8 +4295,9 @@ function detectNearDuplicatePages(pages, options = {}) {
4191
4295
  const pairs = [];
4192
4296
  let duplicatePairCount = 0;
4193
4297
  let comparedPairCount = 0;
4194
- for (const key of [...candidateKeys].sort(pairKeyOrder)) {
4195
- const [leftIndex, rightIndex] = parsePairKey(key);
4298
+ for (const key of [...candidateKeys].sort((left, right) => left - right)) {
4299
+ const leftIndex = Math.floor(key / prepared.length);
4300
+ const rightIndex = key % prepared.length;
4196
4301
  const left = prepared[leftIndex];
4197
4302
  const right = prepared[rightIndex];
4198
4303
  comparedPairCount += 1;
@@ -4232,6 +4337,94 @@ function detectNearDuplicatePages(pages, options = {}) {
4232
4337
  pairs
4233
4338
  };
4234
4339
  }
4340
+ function settings(options) {
4341
+ return {
4342
+ threshold: finiteUnitInterval(options.threshold ?? .82, "threshold"),
4343
+ minCharacters: nonNegativeInteger(options.minCharacters ?? 80, "minCharacters"),
4344
+ wordShingleSize: positiveInteger(options.wordShingleSize ?? 5, "wordShingleSize"),
4345
+ characterShingleSize: positiveInteger(options.characterShingleSize ?? 16, "characterShingleSize"),
4346
+ maxDocumentFrequency: positiveInteger(options.maxDocumentFrequency ?? 64, "maxDocumentFrequency")
4347
+ };
4348
+ }
4349
+ const sketches = /* @__PURE__ */ new Map();
4350
+ /**
4351
+ * Every pair between a candidate and another page of `pages` (which holds the candidates) at or
4352
+ * above the threshold. A pair qualifies as in `detectNearDuplicatePages`, an exact normalized copy
4353
+ * or a shared shingle held by at most `maxDocumentFrequency` pages, but no pair cap applies: only
4354
+ * the candidates' shingles index the corpus, as sorted 32-bit shingle hashes kept per frozen page.
4355
+ */
4356
+ function detectCandidateNearDuplicates(candidates, pages, options = {}) {
4357
+ const config = settings(options);
4358
+ const key = `${config.wordShingleSize}:${config.characterShingleSize}`;
4359
+ const cache = sketches.get(key) ?? sketches.set(key, /* @__PURE__ */ new WeakMap()).get(key);
4360
+ const sketchOf = (page) => {
4361
+ let sketch = cache.get(page);
4362
+ if (sketch === void 0) {
4363
+ const item = preparePage(page, config.wordShingleSize, config.characterShingleSize);
4364
+ const sorted = Uint32Array.from(item?.shingles ?? [], hash32).sort();
4365
+ sketch = item && {
4366
+ length: item.normalized.length,
4367
+ exact: hash32(item.normalized),
4368
+ hashes: sorted.filter((value, index) => index === 0 || sorted[index - 1] !== value)
4369
+ };
4370
+ if (Object.isFrozen(page)) cache.set(page, sketch);
4371
+ }
4372
+ return sketch && sketch.length >= config.minCharacters ? sketch : null;
4373
+ };
4374
+ const eligible = pages.flatMap((page) => {
4375
+ const sketch = sketchOf(page);
4376
+ return sketch ? [{
4377
+ page,
4378
+ sketch
4379
+ }] : [];
4380
+ }).sort((left, right) => left.page.path.localeCompare(right.page.path));
4381
+ const position = new Map(eligible.map((entry, index) => [entry.page, index]));
4382
+ const holders = /* @__PURE__ */ new Map();
4383
+ for (const candidate of candidates.filter((page) => position.has(page))) {
4384
+ const sketch = eligible[position.get(candidate)].sketch;
4385
+ for (const hash of [~sketch.exact, ...sketch.hashes]) holders.set(hash, []);
4386
+ }
4387
+ for (const [index, { sketch }] of eligible.entries()) {
4388
+ holders.get(~sketch.exact)?.push(index);
4389
+ for (const hash of sketch.hashes) {
4390
+ const group = holders.get(hash);
4391
+ if (group && group.length <= config.maxDocumentFrequency) group.push(index);
4392
+ }
4393
+ }
4394
+ const text = (page) => normalizePageText(`${page.title}\n${page.text}`);
4395
+ const pairs = candidates.flatMap((candidate) => {
4396
+ const self = position.get(candidate);
4397
+ if (self === void 0) return [];
4398
+ const { sketch } = eligible[self];
4399
+ const matches = new Set(holders.get(~sketch.exact));
4400
+ for (const hash of sketch.hashes) {
4401
+ const group = holders.get(hash);
4402
+ if (group.length <= config.maxDocumentFrequency) for (const index of group) matches.add(index);
4403
+ }
4404
+ return [...matches].flatMap((index) => {
4405
+ const matched = eligible[index];
4406
+ if (index === self || index < self && candidates.includes(matched.page)) return [];
4407
+ const exact = matched.sketch.exact === sketch.exact && text(matched.page) === text(candidate);
4408
+ const { similarity } = sortedJaccard(sketch.hashes, matched.sketch.hashes);
4409
+ if (!exact && similarity < config.threshold) return [];
4410
+ return [{
4411
+ candidate,
4412
+ matched: matched.page,
4413
+ similarity: exact ? 1 : round(similarity),
4414
+ exact
4415
+ }];
4416
+ });
4417
+ });
4418
+ return {
4419
+ threshold: config.threshold,
4420
+ pairs
4421
+ };
4422
+ }
4423
+ function hash32(value) {
4424
+ let hash = 2166136261;
4425
+ for (let index = 0; index < value.length; index += 1) hash = Math.imul(hash ^ value.charCodeAt(index), 16777619);
4426
+ return hash >>> 0;
4427
+ }
4235
4428
  function preparePage(page, wordShingleSize, characterShingleSize) {
4236
4429
  const normalized = normalizePageText(`${page.title}\n${page.text}`);
4237
4430
  if (normalized.length === 0) return null;
@@ -4268,25 +4461,26 @@ function jaccard(left, right) {
4268
4461
  const [small, large] = left.size <= right.size ? [left, right] : [right, left];
4269
4462
  let intersectionSize = 0;
4270
4463
  for (const item of small) if (large.has(item)) intersectionSize += 1;
4271
- const unionSize = left.size + right.size - intersectionSize;
4464
+ return overlap(left.size, right.size, intersectionSize);
4465
+ }
4466
+ function sortedJaccard(left, right) {
4467
+ let shared = 0;
4468
+ for (let i = 0, j = 0; i < left.length && j < right.length;) {
4469
+ if (left[i] === right[j]) shared += 1;
4470
+ const step = left[i] - right[j];
4471
+ if (step <= 0) i += 1;
4472
+ if (step >= 0) j += 1;
4473
+ }
4474
+ return overlap(left.length, right.length, shared);
4475
+ }
4476
+ function overlap(left, right, intersectionSize) {
4477
+ const unionSize = left + right - intersectionSize;
4272
4478
  return {
4273
4479
  similarity: unionSize === 0 ? 1 : intersectionSize / unionSize,
4274
4480
  intersectionSize,
4275
4481
  unionSize
4276
4482
  };
4277
4483
  }
4278
- function pairKey(left, right) {
4279
- return `${left}:${right}`;
4280
- }
4281
- function parsePairKey(value) {
4282
- const [left, right] = value.split(":");
4283
- return [Number(left), Number(right)];
4284
- }
4285
- function pairKeyOrder(left, right) {
4286
- const [leftA, leftB] = parsePairKey(left);
4287
- const [rightA, rightB] = parsePairKey(right);
4288
- return leftA - rightA || leftB - rightB;
4289
- }
4290
4484
  function finiteUnitInterval(value, name) {
4291
4485
  if (!Number.isFinite(value) || value < 0 || value > 1) throw new Error(`${name} must be a finite number in [0, 1]`);
4292
4486
  return value;
@@ -4573,11 +4767,12 @@ const toolViews = /* @__PURE__ */ new WeakSet();
4573
4767
  function isKnowledgeToolChain(chain) {
4574
4768
  return toolViews.has(chain);
4575
4769
  }
4576
- const MAX_CACHED_ROOTS = 8;
4577
- const MAX_CACHED_SOURCE_BYTES = 67108864;
4578
4770
  /** Internal tool path. Custom stores retain their existing loadChain contract. */
4579
- function loadKnowledgeToolChain(stores, runId) {
4580
- return toolViewLoaders.get(stores)?.(runId) ?? stores.loadChain(runId);
4771
+ async function loadKnowledgeToolChain(stores, runId) {
4772
+ return await toolViewLoaders.get(stores)?.(runId) ?? {
4773
+ chain: await stores.loadChain(runId),
4774
+ timing: {}
4775
+ };
4581
4776
  }
4582
4777
  function createRunScopedStores(options) {
4583
4778
  if (!options || typeof options !== "object") throw new TypeError("createRunScopedStores options are required");
@@ -4629,118 +4824,14 @@ function createRunScopedStores(options) {
4629
4824
  },
4630
4825
  lineage: resolveLineage,
4631
4826
  async loadChain(runId) {
4632
- assertRunId(runId);
4633
- const out = [];
4634
- const readInto = async (root, origin) => {
4635
- let pages;
4636
- try {
4637
- pages = await loadKnowledgePages(root, { pagesDirectory });
4638
- } catch (error) {
4639
- if (isMissingFile(error)) return;
4640
- throw error;
4641
- }
4642
- for (const page of pages) out.push({
4643
- page,
4644
- origin
4645
- });
4646
- };
4647
- await readInto(storePath(runId), "here");
4648
- for (const ancestor of await resolveLineage(runId)) await readInto(storePath(ancestor), `inherited:${ancestor}`);
4649
- if (options.sharedRoot) await readInto(options.sharedRoot, "shared");
4650
- return out;
4651
- }
4652
- };
4653
- const roots = /* @__PURE__ */ new Map();
4654
- let current;
4655
- const loadRoot = (root, previous) => withKnowledgeRead(root, async () => {
4656
- let listed;
4657
- try {
4658
- listed = await listRegularFileMetadataWithinRoot(root, pagesDirectory);
4659
- } catch (error) {
4660
- if (!isMissingFile(error)) throw error;
4661
- listed = [];
4662
- }
4663
- const generation = JSON.stringify(listed.map((file) => [file.path, file.generation]));
4664
- if (previous?.generation === generation) return previous;
4665
- const files = /* @__PURE__ */ new Map();
4666
- const changed = [];
4667
- for (const file of listed) {
4668
- const known = previous?.files.get(file.path);
4669
- if (known?.generation === file.generation) files.set(file.path, known);
4670
- else if (!isKnowledgePagePath(file.path)) files.set(file.path, {
4671
- generation: file.generation,
4672
- page: void 0
4673
- });
4674
- else changed.push(file);
4675
- }
4676
- const parse = (path, generation, bytes) => files.set(path, {
4677
- generation,
4678
- page: freezeView(knowledgePageFromMarkdown(path, bytes.toString("utf8"), pagesDirectory))
4679
- });
4680
- if (changed.length > 64) {
4681
- const wanted = new Map(changed.map((file) => [file.path, file.generation]));
4682
- for (const file of await listRegularFilesWithinRoot(root, pagesDirectory)) {
4683
- const generation = wanted.get(file.path);
4684
- if (generation !== void 0) parse(file.path, generation, file.bytes);
4685
- }
4686
- } else await Promise.all(changed.map(async (file) => {
4687
- try {
4688
- parse(file.path, file.generation, (await readRegularFileWithinRoot(root, file.path)).bytes);
4689
- } catch (error) {
4690
- if (!isMissingFile(error)) throw error;
4691
- }
4692
- }));
4693
- const pages = Object.freeze([...files.values()].flatMap((file) => file.page ? [file.page] : []).sort((a, b) => a.path.localeCompare(b.path)));
4694
- return {
4695
- generation,
4696
- bytes: listed.reduce((sum, file) => sum + file.bytes, 0),
4697
- files,
4698
- pages
4699
- };
4700
- });
4701
- const refreshes = /* @__PURE__ */ new Map();
4702
- const readRoot = (root) => {
4703
- let state = refreshes.get(root);
4704
- if (!state) {
4705
- state = {};
4706
- refreshes.set(root, state);
4707
- }
4708
- const slot = state;
4709
- const start = () => {
4710
- const running = loadRoot(root, roots.get(root)).then((loaded) => retain(root, loaded)).finally(() => {
4711
- if (slot.running === running) slot.running = void 0;
4712
- if (!slot.running && !slot.next && refreshes.get(root) === slot) refreshes.delete(root);
4713
- });
4714
- slot.running = running;
4715
- return running;
4716
- };
4717
- if (slot.next) return slot.next;
4718
- if (!slot.running) return start();
4719
- const next = slot.running.catch(() => void 0).then(() => {
4720
- slot.next = void 0;
4721
- return start();
4722
- });
4723
- slot.next = next;
4724
- return next;
4725
- };
4726
- const retain = (root, loaded) => {
4727
- const existing = roots.get(root);
4728
- if (existing?.generation === loaded.generation) return existing;
4729
- roots.delete(root);
4730
- roots.set(root, loaded);
4731
- const live = new Set(current?.identities.map((identity) => JSON.parse(identity)[0]));
4732
- live.add(root);
4733
- let bytes = 0;
4734
- for (const part of roots.values()) bytes += part.bytes;
4735
- for (const [candidate, part] of roots) {
4736
- if (roots.size <= MAX_CACHED_ROOTS && bytes <= MAX_CACHED_SOURCE_BYTES) break;
4737
- if (live.has(candidate)) continue;
4738
- roots.delete(candidate);
4739
- bytes -= part.bytes;
4827
+ const { scopes, views } = await readChain(runId);
4828
+ return views.flatMap((view, i) => view.pages.map((page) => ({
4829
+ page: structuredClone(page),
4830
+ origin: scopes[i].origin
4831
+ })));
4740
4832
  }
4741
- return loaded;
4742
4833
  };
4743
- toolViewLoaders.set(stores, async (runId) => {
4834
+ const readChain = async (runId) => {
4744
4835
  assertRunId(runId);
4745
4836
  const scopes = [{
4746
4837
  root: storePath(runId),
@@ -4754,31 +4845,35 @@ function createRunScopedStores(options) {
4754
4845
  root: options.sharedRoot,
4755
4846
  origin: "shared"
4756
4847
  });
4757
- const identities = scopes.map(({ root, origin }) => JSON.stringify([root, origin]));
4758
- const parts = await Promise.all(scopes.map((scope) => readRoot(scope.root)));
4848
+ return {
4849
+ scopes,
4850
+ ...await readKnowledgePageViews(scopes.map((scope) => scope.root), { pagesDirectory })
4851
+ };
4852
+ };
4853
+ let current;
4854
+ toolViewLoaders.set(stores, async (runId) => {
4855
+ const { scopes, views: parts, pagesRead } = await readChain(runId);
4856
+ const origins = scopes.map((scope) => scope.origin);
4759
4857
  const latest = current;
4760
- if (latest && identities.length === latest.identities.length && identities.every((id, i) => id === latest.identities[i] && parts[i] === latest.parts[i])) return latest.chain;
4761
- const chain = Object.freeze(parts.flatMap((part, i) => part.pages.map((page) => Object.freeze({
4762
- page,
4763
- origin: scopes[i].origin
4764
- }))));
4765
- toolViews.add(chain);
4766
- current = {
4767
- identities,
4768
- parts,
4769
- chain
4858
+ if (latest?.parts.length !== parts.length || parts.some((part, i) => part !== latest.parts[i] || origins[i] !== latest.origins[i])) {
4859
+ const chain = Object.freeze(parts.flatMap((part, i) => part.pages.map((page) => Object.freeze({
4860
+ page,
4861
+ origin: origins[i]
4862
+ }))));
4863
+ toolViews.add(chain);
4864
+ current = {
4865
+ origins,
4866
+ parts,
4867
+ chain
4868
+ };
4869
+ }
4870
+ return {
4871
+ chain: current.chain,
4872
+ timing: { pagesRead }
4770
4873
  };
4771
- return chain;
4772
4874
  });
4773
4875
  return stores;
4774
4876
  }
4775
- function freezeView(value) {
4776
- if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
4777
- for (const child of Object.values(value)) freezeView(child);
4778
- Object.freeze(value);
4779
- }
4780
- return value;
4781
- }
4782
4877
  /** File-backed authority used when the product has no separate run manifest. */
4783
4878
  function createFileRunLineageAuthority(runStorePath) {
4784
4879
  if (typeof runStorePath !== "function") throw new TypeError("createFileRunLineageAuthority requires a runStorePath function");
@@ -4893,46 +4988,26 @@ function assertKnowledgeWriteIntake(candidates, options) {
4893
4988
  page,
4894
4989
  origin: "here"
4895
4990
  }))];
4896
- assertNoUnrelatedDuplicate(candidatePaths, corpus, options.nearDuplicates ?? {});
4991
+ assertNoUnrelatedDuplicate(candidates, corpus, options.nearDuplicates ?? {});
4897
4992
  if (options.citations === false) return Object.freeze([]);
4898
4993
  return assertKnowledgeCitationsResolved(corpus, candidates.flatMap((candidate) => (candidate.cites ?? []).map((persisted) => parseKnowledgeCitationReference(persisted))));
4899
4994
  }
4900
- function assertNoUnrelatedDuplicate(candidatePaths, corpus, nearDuplicates) {
4901
- if (candidatePaths.size === 0) return;
4902
- const report = detectNearDuplicatePages(corpus.map((entry) => entry.page), nearDuplicates);
4903
- const byIdentity = /* @__PURE__ */ new Map();
4904
- for (const entry of corpus) {
4905
- const key = identityKey(entry.page.path, entry.page.id);
4906
- if (!byIdentity.has(key)) byIdentity.set(key, entry.page);
4907
- }
4908
- const offending = [];
4909
- for (const pair of report.pairs) {
4910
- const leftIsCandidate = candidatePaths.has(pair.leftPath);
4911
- if (!leftIsCandidate && !candidatePaths.has(pair.rightPath)) continue;
4912
- if (pair.leftPageId === pair.rightPageId) continue;
4913
- const left = byIdentity.get(identityKey(pair.leftPath, pair.leftPageId));
4914
- const right = byIdentity.get(identityKey(pair.rightPath, pair.rightPageId));
4915
- if (declaresRelation(left, right.id) || declaresRelation(right, left.id)) continue;
4916
- const candidate = leftIsCandidate ? left : right;
4917
- const matched = leftIsCandidate ? right : left;
4918
- offending.push({
4919
- candidatePageId: candidate.id,
4920
- candidatePath: candidate.path,
4921
- matchedPageId: matched.id,
4922
- matchedPath: matched.path,
4923
- similarity: pair.similarity,
4924
- exact: pair.exact
4925
- });
4926
- }
4927
- if (offending.length > 0) throw new KnowledgeDuplicateIntakeError(offending, report.threshold);
4928
- }
4929
- function identityKey(path, id) {
4930
- return `${path}\u0000${id}`;
4995
+ function assertNoUnrelatedDuplicate(candidates, corpus, nearDuplicates) {
4996
+ const { threshold, pairs } = detectCandidateNearDuplicates(candidates, corpus.map((entry) => entry.page), nearDuplicates);
4997
+ const offending = pairs.filter(({ candidate: c, matched: m }) => c.id !== m.id && !relates(c, m) && !relates(m, c)).map(({ candidate, matched, similarity, exact }) => ({
4998
+ candidatePageId: candidate.id,
4999
+ candidatePath: candidate.path,
5000
+ matchedPageId: matched.id,
5001
+ matchedPath: matched.path,
5002
+ similarity,
5003
+ exact
5004
+ }));
5005
+ if (offending.length > 0) throw new KnowledgeDuplicateIntakeError(offending, threshold);
4931
5006
  }
4932
- /** Whether `page` already relates itself to `targetId` through `cites` or `contradicts`. */
4933
- function declaresRelation(page, targetId) {
4934
- for (const persisted of page.cites ?? []) if (parseKnowledgeCitationReference(persisted).pageId === targetId) return true;
4935
- return (page.contradicts ?? []).includes(targetId);
5007
+ /** Whether `page` already relates itself to `target` through `cites` or `contradicts`. */
5008
+ function relates(page, target) {
5009
+ for (const persisted of page.cites ?? []) if (parseKnowledgeCitationReference(persisted).pageId === target.id) return true;
5010
+ return (page.contradicts ?? []).includes(target.id);
4936
5011
  }
4937
5012
  // src/write-protocol.ts
4938
5013
  const OPENER_LINE = /^---\s*FILE:\s*(.+?)\s*---\s*$/i;
@@ -5053,10 +5128,10 @@ async function applyKnowledgeWriteBlocks(root, proposalText, options = {}) {
5053
5128
  }
5054
5129
  if (intake) {
5055
5130
  const { inheritedPages = [], ...settings } = intake;
5056
- const here = await loadKnowledgePages(root, { pagesDirectory });
5131
+ const { views } = await readKnowledgePageViews([root], { pagesDirectory });
5057
5132
  assertKnowledgeWriteIntake(mutations.filter((mutation) => isKnowledgePagePath(mutation.path)).map((mutation) => knowledgePageFromMarkdown(mutation.path, mutation.content, pagesDirectory)), {
5058
5133
  ...settings,
5059
- visiblePages: [...originatedPages(here), ...inheritedPages]
5134
+ visiblePages: [...originatedPages(views[0].pages), ...inheritedPages]
5060
5135
  });
5061
5136
  }
5062
5137
  await commitKnowledgeFileMutations({
@@ -5157,4 +5232,4 @@ function explainKnowledgeTarget(index, target) {
5157
5232
  }
5158
5233
  export { loadSourceRegistry as $, writeFileDurableWithinRoot as $n, KNOWLEDGE_EVENT_TYPES as $t, validateKnowledgePage as A, finishKnowledgeFileTransaction as An, isScaffoldPath as At, CHECKABLE_RUNG_THRESHOLD as B, canonicalRelativeWithinRoot as Bn, KnowledgeEventSchema as Bt, immutablePageDigest as C, runInKnowledgeMutationScope as Cn, resolveKnowledgeCitation as Ct, normalizeVisibilityArtifact as D, applyKnowledgeFileTransaction as Dn, SCAFFOLD_PAGE_BASENAMES as Dt, nonEmpty as E, withKnowledgeRead as En, resolveRunScopedCitations as Et, detectNearDuplicatePages as F, recordLockOwner as Fn, WIKILINK_REGEX as Ft, gradeClaims as G, readRegularFileWithinRoot as Gn, KnowledgePageSchema as Gt, UncheckableClaimError as H, isMissingFile as Hn, KnowledgeGraphNodeSchema as Ht, normalizePageText as I, takeOverDeadHolder as In, extractWikilinks as It, verifyGradeableEvidence as J, renameDurable as Jn, ResearchClaimLedgerSchema as Jt, gradeFor as K, relativeWithinRoot as Kn, KnowledgeRelationSchema as Kt, prepareKnowledgeValidation as L, DEFAULT_PAGES_DIRECTORY as Ln, normalizeLinkTarget as Lt, verifyKnowledgeVisibilitySnapshot as M, prepareKnowledgeFileTransaction as Mn, layoutFor as Mt, visibilityIdentity as N, rollbackKnowledgeFileTransaction as Nn, loadKnowledgePages as Nt, normalizeVisibilityRef as O, assertKnowledgeMutationPath as On, initKnowledgeBase as Ot, visibilityIndexOf as P, RUN_LEASE as Pn, writeJson as Pt, addSourceText as Q, writeFileDurable as Qn, SourceRecordSchema as Qt, validateKnowledgeIndex as R, normalizePagesDirectory as Rn, DeepQuestionSchema as Rt, encodeKnowledgeVisibilitySnapshot as S, recoverPendingKnowledgeMutation as Sn, parseKnowledgeCitationReference as St, knowledgeVisibilityArtifactRef as T, withKnowledgeMutationLabel as Tn, resolveRunScopedCitation as Tt, assertGradeableEvidence as U, listRegularFilesWithinRoot as Un, KnowledgeIndexSchema as Ut, DEADLINE_EXIT_CODE as V, isKernelAnchoredPath as Vn, KnowledgeGraphEdgeSchema as Vt, claimCheckKey as W, readRegularFileNoFollow as Wn, KnowledgePageInvalidationSchema as Wt, writeKnowledgeIndex as X, withSafeDescendant as Xn, ResearchSourceVersionSchema as Xt, buildKnowledgeIndex as Y, syncDirectory as Yn, ResearchClaimRecordSchema as Yt, addSourcePath as Z, withSafeDirectory as Zn, SourceAnchorSchema as Zt, KNOWLEDGE_USE_RECEIPT_SCHEMA_VERSION as _, parseFrontmatter as _n, assertKnowledgeCitationsResolved as _t, applyKnowledgeWriteBlocksFile as a, claimEvidenceId as an, KB_CLAIM_LEDGER_DIR as at, decodeKnowledgeVisibilitySnapshot as b, isKnowledgeMutationHeld as bn, auditKnowledgeCitations as bt, KnowledgeDuplicateIntakeError as c, deepQuestionId as cn, KB_STORE_DIR as ct, createFileRunLineageAuthority as d, materializeRegisteredClaimEvidence as dn, buildKnowledgeGraph as dt, ClaimLedgerGoalConflictError as en, writeJsonDurable as er, snapshotSourceTextInput as et, createRunScopedStores as f, mergeClaimLedgers as fn, knowledgePageRelations as ft, KNOWLEDGE_RECEIPT_DIGEST_ALGORITHM as g, formatFrontmatter as gn, assertKnowledgeCitationAudit as gt, originatedPages as h, researchSourceVersionKey as hn, assertCurrentRunCitationsResolved as ht, applyKnowledgeWriteBlocks as i, assertTrackedClaimIntegrity as in, FileSystemKbStore as it, validateOrigin as j, knowledgeFileTransactionPlanHash as jn, knowledgePageFromMarkdown as jt, snapshotImmutableVisibility as k, commitKnowledgeFileMutations as kn, isKnowledgePagePath as kt, assertKnowledgeWriteIntake as l, emptyClaimLedger as ln, MemoryKbStore as lt, loadKnowledgeToolChain as m, normalizeClaimText as mn, KnowledgeCitationResolutionError as mt, inspectKnowledgeIndex as n, assertResearchClaimEvidenceIntegrity as nn, mediaTypeFor$1 as nr, writeSourceRegistry as nt, isSafeKnowledgePath as o, claimId as on, KB_EVENTS_PATH as ot, isKnowledgeToolChain as p, mergeTrackedClaims as pn, KnowledgeCitationAuditError as pt, verdictFor as q, removeDurable as qn, ResearchClaimEvidenceSchema as qt, stringMetadata as r, assertResearchClaimLedgerIntegrity as rn, textSourceAdapter as rr, ClaimLedgerMigrationRequiredError as rt, parseKnowledgeWriteBlocks as s, claimSourceHost as sn, KB_INDEX_PATH as st, explainKnowledgeTarget as t, assertDeepQuestionIntegrity as tn, writeJsonDurableWithinRoot as tr, sourceRegistryPath as tt, RUN_LINEAGE_BASENAME as u, linkClaimContradictions as un, assertClaimLedgerId as ut, assertSchemaVersion as v, acquireDurableFileLock as vn, assertRunScopedCitationsResolved as vt, knowledgePageDigest as w, withKnowledgeMutation as wn, resolveKnowledgeCitations as wt, digest as x, measureKnowledgeMutation as xn, formatKnowledgeCitationReference as xt, createKnowledgeVisibilitySnapshot as y, inspectPendingKnowledgeMutation as yn, auditCurrentRunCitations as yt, lintKnowledgeIndex as z, canonicalPathsEqual as zn, KnowledgeBaseCandidateSchema as zt };
5159
5234
 
5160
- //# sourceMappingURL=inspect-tl0P7I7w.js.map
5235
+ //# sourceMappingURL=inspect-DgLNFlsA.js.map