@tangle-network/agent-knowledge 1.12.0 → 2.0.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/index.js CHANGED
@@ -9,35 +9,53 @@ import {
9
9
  SourceAnchorSchema,
10
10
  SourceRecordSchema,
11
11
  WIKILINK_REGEX,
12
+ acquireDurableFileLock,
12
13
  addSourcePath,
13
14
  addSourceText,
15
+ applyKnowledgeFileTransaction,
14
16
  applyKnowledgeWriteBlocks,
15
17
  applyKnowledgeWriteBlocksFile,
18
+ assertKnowledgeMutationPath,
16
19
  buildKnowledgeGraph,
17
20
  buildKnowledgeIndex,
18
21
  explainKnowledgeTarget,
19
22
  extractWikilinks,
23
+ finishKnowledgeFileTransaction,
20
24
  formatFrontmatter,
21
25
  initKnowledgeBase,
22
26
  inspectKnowledgeIndex,
27
+ inspectPendingKnowledgeMutation,
28
+ isMissingFile,
23
29
  isSafeKnowledgePath,
24
30
  isScaffoldPath,
31
+ knowledgeFileTransactionPlanHash,
25
32
  layoutFor,
26
33
  lintKnowledgeIndex,
34
+ listRegularFilesWithinRoot,
27
35
  loadKnowledgePages,
28
36
  loadSourceRegistry,
29
37
  mediaTypeFor,
30
38
  normalizeLinkTarget,
31
39
  parseFrontmatter,
32
40
  parseKnowledgeWriteBlocks,
41
+ prepareKnowledgeFileTransaction,
42
+ readRegularFileWithinRoot,
43
+ recoverPendingKnowledgeMutation,
44
+ renameDurable,
45
+ rollbackKnowledgeFileTransaction,
33
46
  sourceRegistryPath,
34
47
  stringMetadata,
35
48
  textSourceAdapter,
36
49
  validateKnowledgeIndex,
50
+ withKnowledgeMutation,
51
+ withKnowledgeRead,
52
+ withSafeDirectory,
53
+ writeFileDurableWithinRoot,
37
54
  writeJson,
55
+ writeJsonDurableWithinRoot,
38
56
  writeKnowledgeIndex,
39
57
  writeSourceRegistry
40
- } from "./chunk-W6VWYTNH.js";
58
+ } from "./chunk-QID2QVR5.js";
41
59
  import {
42
60
  AgentMemoryHitSchema,
43
61
  AgentMemoryKindSchema,
@@ -101,7 +119,7 @@ import {
101
119
  scoreMemoryBenchmarkArtifact,
102
120
  scoreRetrievalArtifact,
103
121
  summarizeKnowledgeBenchmarkCampaign
104
- } from "./chunk-ULXZI235.js";
122
+ } from "./chunk-RIHIYCX2.js";
105
123
  import {
106
124
  reciprocalRankFusion,
107
125
  searchKnowledge,
@@ -133,7 +151,7 @@ async function fetchWithRetry(url, init, opts) {
133
151
  await res.text().catch(() => "");
134
152
  const backoff = opts.retryBaseMs * 2 ** attempt;
135
153
  const jitter = backoff * (0.75 + Math.random() * 0.5);
136
- await new Promise((resolve) => setTimeout(resolve, jitter));
154
+ await new Promise((resolve2) => setTimeout(resolve2, jitter));
137
155
  }
138
156
  if (lastRes) return lastRes;
139
157
  throw new RouterError(0, "fetchWithRetry produced no response");
@@ -1284,24 +1302,34 @@ function createKnowledgeEvent(input) {
1284
1302
  }
1285
1303
 
1286
1304
  // src/freshness.ts
1287
- import { mkdir, readFile, writeFile } from "fs/promises";
1288
- import { dirname, join } from "path";
1305
+ import { readFile } from "fs/promises";
1306
+ import { join } from "path";
1307
+ import { z } from "zod";
1308
+ var freshnessRecordSchema = z.object({
1309
+ workspaceId: z.string().min(1),
1310
+ sourceId: z.string().min(1),
1311
+ lastRefreshedAt: z.iso.datetime(),
1312
+ contentHash: z.string().min(1).optional()
1313
+ }).strict();
1314
+ var freshnessFileSchema = z.object({ records: z.record(z.string(), freshnessRecordSchema) }).strict();
1289
1315
  function createFileSystemFreshnessStore(options) {
1290
1316
  const path = join(options.root, ".agent-knowledge", "freshness.json");
1291
- let writeQueue = Promise.resolve();
1292
1317
  const read = async () => {
1293
- try {
1294
- const text = await readFile(path, "utf8");
1295
- const parsed = JSON.parse(text);
1296
- return parsed.records ?? {};
1297
- } catch {
1298
- return {};
1299
- }
1318
+ return withKnowledgeRead(options.root, async () => {
1319
+ try {
1320
+ return freshnessFileSchema.parse(JSON.parse(await readFile(path, "utf8"))).records;
1321
+ } catch (error) {
1322
+ if (isMissingFile(error)) return {};
1323
+ throw error;
1324
+ }
1325
+ });
1300
1326
  };
1301
1327
  const write = async (records) => {
1302
- await mkdir(dirname(path), { recursive: true });
1303
- await writeFile(path, `${JSON.stringify({ records }, null, 2)}
1304
- `, "utf8");
1328
+ await writeJsonDurableWithinRoot(
1329
+ options.root,
1330
+ ".agent-knowledge/freshness.json",
1331
+ freshnessFileSchema.parse({ records })
1332
+ );
1305
1333
  };
1306
1334
  return {
1307
1335
  async last(key) {
@@ -1310,17 +1338,16 @@ function createFileSystemFreshnessStore(options) {
1310
1338
  return record ? new Date(record.lastRefreshedAt) : null;
1311
1339
  },
1312
1340
  async mark(input) {
1313
- writeQueue = writeQueue.then(async () => {
1341
+ await withKnowledgeMutation(options.root, async () => {
1314
1342
  const records = await read();
1315
1343
  records[buildKey(input)] = {
1316
1344
  workspaceId: input.workspaceId,
1317
1345
  sourceId: input.sourceId,
1318
1346
  lastRefreshedAt: input.when.toISOString(),
1319
- contentHash: input.contentHash
1347
+ ...input.contentHash === void 0 ? {} : { contentHash: input.contentHash }
1320
1348
  };
1321
1349
  await write(records);
1322
1350
  });
1323
- await writeQueue;
1324
1351
  },
1325
1352
  async stale(input) {
1326
1353
  const last = await this.last(input);
@@ -2056,7 +2083,6 @@ function lensDistribution(set = investmentThesisSet) {
2056
2083
  }
2057
2084
 
2058
2085
  // src/investment-thesis-task.ts
2059
- import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
2060
2086
  import { join as join2 } from "path";
2061
2087
 
2062
2088
  // src/material-facts-metric.ts
@@ -2371,23 +2397,23 @@ function thesisSynthesisMessages(input, kbText) {
2371
2397
  ];
2372
2398
  }
2373
2399
  async function writeThesisPage(root, input, thesis) {
2374
- const { knowledgeDir } = layoutFor(root);
2375
- await mkdir2(knowledgeDir, { recursive: true });
2376
- const path = join2(knowledgeDir, `thesis-${input.ticker.toLowerCase()}.md`);
2377
- const body = [
2378
- "---",
2379
- `title: Investment thesis \u2014 ${input.company} (${input.ticker})`,
2380
- `ticker: ${input.ticker}`,
2381
- `cutoff: ${input.cutoff}`,
2382
- "kind: investment-thesis",
2383
- "---",
2384
- `# Investment thesis \u2014 ${input.company} (${input.ticker}), as of ${input.cutoff}`,
2385
- "",
2386
- thesis.trim(),
2387
- ""
2388
- ].join("\n");
2389
- await writeFile2(path, body, "utf8");
2390
- return path;
2400
+ return withKnowledgeMutation(root, async () => {
2401
+ const relativePath = `knowledge/thesis-${slugify(input.ticker)}.md`;
2402
+ const body = [
2403
+ "---",
2404
+ `title: Investment thesis \u2014 ${input.company} (${input.ticker})`,
2405
+ `ticker: ${input.ticker}`,
2406
+ `cutoff: ${input.cutoff}`,
2407
+ "kind: investment-thesis",
2408
+ "---",
2409
+ `# Investment thesis \u2014 ${input.company} (${input.ticker}), as of ${input.cutoff}`,
2410
+ "",
2411
+ thesis.trim(),
2412
+ ""
2413
+ ].join("\n");
2414
+ await writeFileDurableWithinRoot(root, relativePath, body, { encoding: "utf8" });
2415
+ return join2(root, relativePath);
2416
+ });
2391
2417
  }
2392
2418
  async function runInvestmentThesisTask(input, options) {
2393
2419
  const worker = createWebResearchWorker({ ...options.workerOptions, router: options.router });
@@ -2410,8 +2436,16 @@ async function runInvestmentThesisTask(input, options) {
2410
2436
  }
2411
2437
 
2412
2438
  // src/kb-improvement.ts
2413
- import { cp, mkdir as mkdir3, open, readdir, readFile as readFile2, rename, rm, stat, writeFile as writeFile3 } from "fs/promises";
2414
- import { dirname as dirname2, join as join3, relative } from "path";
2439
+ import { createHash } from "crypto";
2440
+ import { cp, lstat, mkdir, mkdtemp, rm, stat } from "fs/promises";
2441
+ import { tmpdir } from "os";
2442
+ import { dirname, isAbsolute, join as join3, relative, resolve, sep } from "path";
2443
+ import {
2444
+ canonicalJson,
2445
+ contentHash,
2446
+ validateRunRecord
2447
+ } from "@tangle-network/agent-eval";
2448
+ import { z as z2 } from "zod";
2415
2449
 
2416
2450
  // src/rag-eval.ts
2417
2451
  var defaultThresholds = {
@@ -3453,6 +3487,198 @@ function assertNotAborted(signal) {
3453
3487
  }
3454
3488
 
3455
3489
  // src/kb-improvement.ts
3490
+ var digestSchema = z2.string().regex(/^[a-f0-9]{64}$/);
3491
+ var runIdSchema = z2.string().min(1).max(2048);
3492
+ var safePathSegmentSchema = z2.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/);
3493
+ var improvementStatusSchema = z2.enum([
3494
+ "running",
3495
+ "candidate-ready",
3496
+ "promoted",
3497
+ "rejected",
3498
+ "blocked"
3499
+ ]);
3500
+ var runRecordSchema = z2.custom((value) => {
3501
+ try {
3502
+ validateRunRecord(value);
3503
+ return true;
3504
+ } catch {
3505
+ return false;
3506
+ }
3507
+ }, "invalid agent-eval RunRecord");
3508
+ var deterministicMetricProvenanceSchema = z2.object({
3509
+ evaluator: z2.string().min(1),
3510
+ version: z2.string().min(1),
3511
+ method: z2.literal("deterministic")
3512
+ }).strict();
3513
+ var measuredMetricProvenanceSchema = z2.object({
3514
+ evaluator: z2.string().min(1),
3515
+ version: z2.string().min(1),
3516
+ method: z2.enum(["sampled", "composite"]),
3517
+ corpusHash: digestSchema,
3518
+ runRecords: z2.array(runRecordSchema).min(1)
3519
+ }).strict();
3520
+ var modelMetricProvenanceSchema = z2.object({
3521
+ evaluator: z2.string().min(1),
3522
+ version: z2.string().min(1),
3523
+ method: z2.literal("model"),
3524
+ model: z2.string().min(1),
3525
+ corpusHash: digestSchema,
3526
+ runRecords: z2.array(runRecordSchema).min(1)
3527
+ }).strict();
3528
+ var improvementMetricSchema = z2.object({
3529
+ score: z2.number().finite().min(0).max(1),
3530
+ passed: z2.boolean(),
3531
+ dimensions: z2.record(z2.string(), z2.number().finite()).optional(),
3532
+ notes: z2.string().optional(),
3533
+ provenance: z2.discriminatedUnion("method", [
3534
+ deterministicMetricProvenanceSchema,
3535
+ measuredMetricProvenanceSchema,
3536
+ modelMetricProvenanceSchema
3537
+ ])
3538
+ }).strict().superRefine((metric, context) => {
3539
+ if (metric.provenance.method === "deterministic") return;
3540
+ const actualCorpusHash = contentHash(metric.provenance.runRecords);
3541
+ if (actualCorpusHash !== metric.provenance.corpusHash) {
3542
+ context.addIssue({
3543
+ code: "custom",
3544
+ path: ["provenance", "corpusHash"],
3545
+ message: "metric corpus hash must bind the complete RunRecord array"
3546
+ });
3547
+ }
3548
+ });
3549
+ var candidateRecordSchema = z2.object({
3550
+ iteration: z2.number().int().positive(),
3551
+ candidateId: safePathSegmentSchema,
3552
+ baseHash: digestSchema,
3553
+ candidateHash: digestSchema.optional(),
3554
+ evidenceHash: digestSchema.optional(),
3555
+ promotionPlanHash: digestSchema.optional(),
3556
+ status: improvementStatusSchema,
3557
+ createdAt: z2.iso.datetime(),
3558
+ updatedAt: z2.iso.datetime()
3559
+ }).strict();
3560
+ var KnowledgeImprovementRunStateSchema = z2.object({
3561
+ schemaVersion: z2.literal(1),
3562
+ runId: runIdSchema,
3563
+ root: z2.string().min(1),
3564
+ goal: z2.string().min(1),
3565
+ status: improvementStatusSchema,
3566
+ baseHash: digestSchema,
3567
+ createdAt: z2.iso.datetime(),
3568
+ updatedAt: z2.iso.datetime(),
3569
+ ownerId: z2.string().min(1).optional(),
3570
+ candidates: z2.array(candidateRecordSchema),
3571
+ promotedCandidateId: safePathSegmentSchema.optional(),
3572
+ blockedReason: z2.string().min(1).optional()
3573
+ }).strict().superRefine((state, context) => {
3574
+ const candidateIds = /* @__PURE__ */ new Set();
3575
+ for (const [index, candidate] of state.candidates.entries()) {
3576
+ if (candidateIds.has(candidate.candidateId)) {
3577
+ context.addIssue({
3578
+ code: "custom",
3579
+ path: ["candidates", index, "candidateId"],
3580
+ message: "candidate ids must be unique within an improvement run"
3581
+ });
3582
+ }
3583
+ candidateIds.add(candidate.candidateId);
3584
+ if (candidate.baseHash !== state.baseHash) {
3585
+ context.addIssue({
3586
+ code: "custom",
3587
+ path: ["candidates", index, "baseHash"],
3588
+ message: "candidate base hash must match its improvement run"
3589
+ });
3590
+ }
3591
+ if (candidate.status === "candidate-ready") {
3592
+ if (!candidate.candidateHash || !candidate.evidenceHash || !candidate.promotionPlanHash) {
3593
+ context.addIssue({
3594
+ code: "custom",
3595
+ path: ["candidates", index],
3596
+ message: "ready candidates require content, evidence, and promotion-plan identities"
3597
+ });
3598
+ }
3599
+ }
3600
+ if (candidate.status === "promoted" && (!candidate.candidateHash || !candidate.evidenceHash || !candidate.promotionPlanHash)) {
3601
+ context.addIssue({
3602
+ code: "custom",
3603
+ path: ["candidates", index],
3604
+ message: "promoted candidates require content, evidence, and promotion-plan identities"
3605
+ });
3606
+ }
3607
+ if (candidate.status === "promoted" && Boolean(candidate.evidenceHash) !== Boolean(candidate.promotionPlanHash)) {
3608
+ context.addIssue({
3609
+ code: "custom",
3610
+ path: ["candidates", index],
3611
+ message: "promoted candidate evidence and promotion-plan identities must appear together"
3612
+ });
3613
+ }
3614
+ if (candidate.status === "promoted" && state.status !== "promoted") {
3615
+ context.addIssue({
3616
+ code: "custom",
3617
+ path: ["candidates", index, "status"],
3618
+ message: "only a promoted run may contain a promoted candidate"
3619
+ });
3620
+ }
3621
+ }
3622
+ if (state.status === "promoted") {
3623
+ const promoted = state.candidates.filter(
3624
+ (candidate) => candidate.candidateId === state.promotedCandidateId
3625
+ );
3626
+ if (promoted.length !== 1 || promoted[0]?.status !== "promoted") {
3627
+ context.addIssue({
3628
+ code: "custom",
3629
+ path: ["promotedCandidateId"],
3630
+ message: "promoted state must identify exactly one promoted candidate"
3631
+ });
3632
+ }
3633
+ } else if (state.promotedCandidateId !== void 0) {
3634
+ context.addIssue({
3635
+ code: "custom",
3636
+ path: ["promotedCandidateId"],
3637
+ message: "only a promoted run may identify a promoted candidate"
3638
+ });
3639
+ }
3640
+ if (state.status === "candidate-ready" && state.candidates.filter((candidate) => candidate.status === "candidate-ready").length !== 1) {
3641
+ context.addIssue({
3642
+ code: "custom",
3643
+ path: ["status"],
3644
+ message: "candidate-ready state must contain exactly one ready candidate"
3645
+ });
3646
+ }
3647
+ if (state.status === "blocked" && state.blockedReason === void 0) {
3648
+ context.addIssue({
3649
+ code: "custom",
3650
+ path: ["blockedReason"],
3651
+ message: "blocked state must include a reason"
3652
+ });
3653
+ }
3654
+ });
3655
+ var KnowledgeImprovementEvidenceSchema = z2.object({
3656
+ schemaVersion: z2.literal(1),
3657
+ kind: z2.literal("knowledge-improvement-evidence"),
3658
+ runId: runIdSchema,
3659
+ candidateId: safePathSegmentSchema,
3660
+ iteration: z2.number().int().positive(),
3661
+ goalHash: digestSchema,
3662
+ baseHash: digestSchema,
3663
+ candidateHash: digestSchema,
3664
+ promotionPlanHash: digestSchema,
3665
+ validation: z2.unknown(),
3666
+ readiness: z2.unknown().nullable(),
3667
+ kbQuality: z2.unknown(),
3668
+ evaluation: improvementMetricSchema,
3669
+ lifecycle: z2.unknown().nullable()
3670
+ }).strict();
3671
+ var KnowledgeImprovementCandidateRefSchema = z2.object({
3672
+ schemaVersion: z2.literal(1),
3673
+ kind: z2.literal("knowledge-improvement-candidate"),
3674
+ runId: runIdSchema,
3675
+ candidateId: safePathSegmentSchema,
3676
+ goalHash: digestSchema,
3677
+ baseHash: digestSchema,
3678
+ candidateHash: digestSchema,
3679
+ evidenceHash: digestSchema,
3680
+ promotionPlanHash: digestSchema
3681
+ }).strict();
3456
3682
  var DEFAULT_LEASE_TTL_MS = 15 * 60 * 1e3;
3457
3683
  var UPDATE_PHASES = [
3458
3684
  "knowledge-acquisition",
@@ -3468,33 +3694,159 @@ function knowledgeImprovementRunId(root, goal) {
3468
3694
  return stableId("kimpr", `${root}:${goal}`);
3469
3695
  }
3470
3696
  function knowledgeImprovementRunDir(root, runId) {
3471
- return join3(layoutFor(root).cacheDir, "improvements", runId);
3697
+ const parsedRunId = runIdSchema.parse(runId);
3698
+ const safeRunId = safePathSegmentSchema.safeParse(parsedRunId);
3699
+ const runSegment = safeRunId.success ? safeRunId.data : `${slugify(parsedRunId).slice(0, 72)}-${sha256(parsedRunId).slice(0, 16)}`;
3700
+ const improvementsDir = join3(layoutFor(root).cacheDir, "improvements");
3701
+ const runDir = join3(improvementsDir, runSegment);
3702
+ const resolvedImprovementsDir = resolve(improvementsDir);
3703
+ const resolvedRunDir = resolve(runDir);
3704
+ if (!resolvedRunDir.startsWith(`${resolvedImprovementsDir}${sep}`)) {
3705
+ throw new Error("knowledge improvement run directory escaped its root");
3706
+ }
3707
+ return runDir;
3708
+ }
3709
+ async function withKnowledgeImprovementRun(root, runId, create, use) {
3710
+ const runDir = knowledgeImprovementRunDir(root, runId);
3711
+ const relativePath = descendantPath(root, runDir);
3712
+ if (!relativePath) throw new Error("knowledge improvement run directory escaped its root");
3713
+ return withSafeDirectory(root, relativePath, create, async (openedRunDir) => {
3714
+ const result = await use(openedRunDir);
3715
+ const openedIdentity = await stat(openedRunDir);
3716
+ const currentIdentity = await withSafeDirectory(
3717
+ root,
3718
+ relativePath,
3719
+ false,
3720
+ (currentRunDir) => stat(currentRunDir)
3721
+ );
3722
+ if (openedIdentity.dev !== currentIdentity.dev || openedIdentity.ino !== currentIdentity.ino) {
3723
+ throw new Error("knowledge improvement run directory changed during use");
3724
+ }
3725
+ return result;
3726
+ });
3472
3727
  }
3473
- async function loadKnowledgeImprovementState(root, runId, runDir = knowledgeImprovementRunDir(root, runId)) {
3728
+ async function loadKnowledgeImprovementState(root, runId) {
3729
+ const expectedRunId = runIdSchema.parse(runId);
3474
3730
  try {
3475
- return JSON.parse(await readFile2(statePath(runDir), "utf8"));
3476
- } catch {
3477
- return null;
3731
+ return await withKnowledgeImprovementRun(
3732
+ root,
3733
+ expectedRunId,
3734
+ false,
3735
+ (runDir) => loadKnowledgeImprovementStateFromRun(root, expectedRunId, runDir)
3736
+ );
3737
+ } catch (error) {
3738
+ if (isMissingFile(error)) return null;
3739
+ throw error;
3478
3740
  }
3479
3741
  }
3742
+ async function loadKnowledgeImprovementStateFromRun(root, runId, runDir) {
3743
+ const stateFile = await readRegularFileWithinRoot(runDir, "state.json");
3744
+ const raw = JSON.parse(stateFile.bytes.toString("utf8"));
3745
+ const state = KnowledgeImprovementRunStateSchema.parse(raw);
3746
+ if (state.runId !== runId) {
3747
+ throw new Error("knowledge improvement state does not match the requested run");
3748
+ }
3749
+ if (resolve(state.root) !== resolve(root)) {
3750
+ throw new Error("knowledge improvement state does not match the requested root");
3751
+ }
3752
+ for (const candidate of state.candidates) {
3753
+ if (candidate.status === "running") await assertCandidateWorkspace(runDir, candidate);
3754
+ }
3755
+ return state;
3756
+ }
3757
+ function knowledgeImprovementCandidateRef(result) {
3758
+ if (!result.candidate) throw new Error("knowledge improvement result has no candidate");
3759
+ return candidateRefFor(result.runId, result.state, result.candidate);
3760
+ }
3761
+ async function withKnowledgeImprovementCandidate(options, use) {
3762
+ assertExactCandidatePlatform();
3763
+ const candidateRef = KnowledgeImprovementCandidateRefSchema.parse(options.candidate);
3764
+ return withKnowledgeImprovementRun(options.root, candidateRef.runId, false, async (runDir) => {
3765
+ const state = await loadKnowledgeImprovementStateFromRun(
3766
+ options.root,
3767
+ candidateRef.runId,
3768
+ runDir
3769
+ );
3770
+ return withMeasuredCandidateSnapshot(
3771
+ options.root,
3772
+ runDir,
3773
+ state,
3774
+ candidateRef,
3775
+ (resolved) => withIsolatedCandidateCopy(
3776
+ resolved.root,
3777
+ candidateRef.candidateHash,
3778
+ (root) => use({
3779
+ root,
3780
+ candidate: candidateRef,
3781
+ evaluation: resolved.evidence.evaluation
3782
+ })
3783
+ )
3784
+ );
3785
+ });
3786
+ }
3787
+ async function promoteKnowledgeCandidate(options) {
3788
+ assertExactCandidatePlatform();
3789
+ const candidateRef = Object.freeze(
3790
+ KnowledgeImprovementCandidateRefSchema.parse(options.candidate)
3791
+ );
3792
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
3793
+ return withKnowledgeImprovementRun(options.root, candidateRef.runId, false, async (runDir) => {
3794
+ const lease = await acquireRunLease(runDir, {
3795
+ ownerId: options.ownerId ?? `pid-${process.pid}`,
3796
+ ttlMs: options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS
3797
+ });
3798
+ try {
3799
+ lease.assertOwned();
3800
+ const state = await loadKnowledgeImprovementStateFromRun(
3801
+ options.root,
3802
+ candidateRef.runId,
3803
+ runDir
3804
+ );
3805
+ return await promoteReadyCandidate({
3806
+ root: options.root,
3807
+ runDir,
3808
+ state,
3809
+ candidateRef,
3810
+ leaseTtlMs: options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS,
3811
+ assertRunOwned: lease.assertOwned,
3812
+ now,
3813
+ onState: options.onState
3814
+ });
3815
+ } finally {
3816
+ await lease.release();
3817
+ }
3818
+ });
3819
+ }
3480
3820
  async function improveKnowledgeBase(options) {
3821
+ assertExactCandidatePlatform();
3481
3822
  assertKnowledgeImprovementOptions(options);
3482
3823
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
3483
- const runId = options.runId ?? knowledgeImprovementRunId(options.root, options.goal);
3484
- const runDir = options.runDir ?? knowledgeImprovementRunDir(options.root, runId);
3485
- await initKnowledgeBase(options.root);
3486
- await mkdir3(runDir, { recursive: true });
3824
+ const runId = runIdSchema.parse(
3825
+ options.runId ?? knowledgeImprovementRunId(options.root, options.goal)
3826
+ );
3827
+ return withKnowledgeImprovementRun(
3828
+ options.root,
3829
+ runId,
3830
+ true,
3831
+ (runDir) => improveKnowledgeBaseInRun(options, runId, runDir, now)
3832
+ );
3833
+ }
3834
+ async function improveKnowledgeBaseInRun(options, runId, runDir, now) {
3487
3835
  const lease = await acquireRunLease(runDir, {
3488
3836
  ownerId: options.ownerId ?? `pid-${process.pid}`,
3489
- ttlMs: options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS,
3490
- now
3837
+ ttlMs: options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS
3491
3838
  });
3492
3839
  try {
3493
- let state = options.resume === false ? null : await loadKnowledgeImprovementState(options.root, runId, runDir);
3840
+ lease.assertOwned();
3841
+ let state = options.resume === false ? null : await loadKnowledgeImprovementStateFromRun(options.root, runId, runDir).catch((error) => {
3842
+ if (isMissingFile(error)) return null;
3843
+ throw error;
3844
+ });
3494
3845
  if (!state) {
3495
3846
  const baseHash = await hashKnowledgeBase(options.root);
3847
+ await createBaselineSnapshot(runDir, options.root, baseHash);
3496
3848
  state = {
3497
- version: 1,
3849
+ schemaVersion: 1,
3498
3850
  runId,
3499
3851
  root: options.root,
3500
3852
  goal: options.goal,
@@ -3508,29 +3860,65 @@ async function improveKnowledgeBase(options) {
3508
3860
  await saveState(runDir, state, options.onState);
3509
3861
  await appendLedger(runDir, { type: "run.created", runId, baseHash });
3510
3862
  }
3863
+ if (state.goal !== options.goal) {
3864
+ throw new Error("knowledge improvement state does not match the requested goal");
3865
+ }
3866
+ const promotedCandidateId = state.promotedCandidateId;
3867
+ const promotedCandidate = state.status === "promoted" ? state.candidates.find((candidate2) => candidate2.candidateId === promotedCandidateId) : void 0;
3868
+ if (state.status === "promoted" && !promotedCandidate) {
3869
+ throw new Error("promoted knowledge state has no promoted candidate");
3870
+ }
3871
+ const resumablePromotion = state.candidates.find(
3872
+ (candidate2) => (candidate2.status === "candidate-ready" || candidate2.status === "promoted") && candidate2.evidenceHash !== void 0 && candidate2.promotionPlanHash !== void 0
3873
+ );
3874
+ const resumableCandidateRef = resumablePromotion ? candidateRefFor(runId, state, resumablePromotion) : void 0;
3875
+ await withKnowledgeMutation(options.root, () => void 0, {
3876
+ resumeTransaction: resumableCandidateRef ? {
3877
+ purpose: promotionTransactionPurpose(resumableCandidateRef),
3878
+ validate: (transaction) => assertPromotionTransaction(transaction, resumableCandidateRef)
3879
+ } : void 0
3880
+ });
3881
+ await ensureBaselineSnapshot(runDir, options.root, state.baseHash);
3511
3882
  if (state.status === "promoted") {
3512
- return { runId, runDir, state, promoted: true, blocked: false };
3883
+ const promoted = promotedCandidate;
3884
+ return await promoteReadyCandidate({
3885
+ root: options.root,
3886
+ runDir,
3887
+ state,
3888
+ candidateRef: candidateRefFor(runId, state, promoted),
3889
+ leaseTtlMs: options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS,
3890
+ assertRunOwned: lease.assertOwned,
3891
+ now,
3892
+ onState: options.onState
3893
+ });
3513
3894
  }
3514
3895
  if (state.status === "blocked") {
3515
- return { runId, runDir, state, promoted: false, blocked: true };
3896
+ return { runId, state, promoted: false, blocked: true };
3516
3897
  }
3517
3898
  const maxCandidates = Math.max(1, options.maxCandidates ?? 1);
3518
3899
  let candidate = findActiveCandidate(state);
3900
+ let lastRejectedCandidate;
3901
+ let lastRejectedEvaluation;
3519
3902
  let lifecycle;
3520
- while (candidate?.status === "running" || !candidate && state.candidates.length < maxCandidates) {
3903
+ while (candidate || state.candidates.length < maxCandidates) {
3521
3904
  if (!candidate) {
3522
- const currentHash2 = await hashKnowledgeBase(options.root);
3523
- if (currentHash2 !== state.baseHash) {
3905
+ const currentHash = await hashKnowledgeBase(options.root);
3906
+ if (currentHash !== state.baseHash) {
3524
3907
  state = await blockRun(
3525
3908
  runDir,
3526
3909
  state,
3527
- `base changed before candidate creation: expected ${state.baseHash}, got ${currentHash2}`,
3910
+ `base changed before candidate creation: expected ${state.baseHash}, got ${currentHash}`,
3528
3911
  options.onState,
3529
3912
  now
3530
3913
  );
3531
- return { runId, runDir, state, promoted: false, blocked: true };
3914
+ return { runId, state, promoted: false, blocked: true };
3532
3915
  }
3533
- candidate = await createCandidateWorkspace(runDir, state, options.root, now);
3916
+ const activeState = state;
3917
+ candidate = await withBaselineSnapshot(
3918
+ runDir,
3919
+ activeState.baseHash,
3920
+ (baselineRoot) => createCandidateWorkspace(runDir, activeState, baselineRoot, now)
3921
+ );
3534
3922
  state.candidates.push(candidate);
3535
3923
  state.status = "running";
3536
3924
  state.updatedAt = now().toISOString();
@@ -3542,79 +3930,377 @@ async function improveKnowledgeBase(options) {
3542
3930
  iteration: candidate.iteration
3543
3931
  });
3544
3932
  }
3545
- lifecycle = await runCandidateLifecycle(runDir, runId, candidate, options, now);
3546
- candidate.status = "candidate-ready";
3547
- candidate.updatedAt = now().toISOString();
3548
- state.status = "candidate-ready";
3933
+ const measured = await measureCandidate(runId, runDir, state, candidate, options, now);
3934
+ candidate = measured.candidate;
3935
+ const evaluation = measured.evaluation;
3936
+ lifecycle = measured.lifecycle;
3937
+ if (evaluation.passed) {
3938
+ candidate.status = "candidate-ready";
3939
+ candidate.updatedAt = now().toISOString();
3940
+ state.status = "candidate-ready";
3941
+ state.updatedAt = now().toISOString();
3942
+ await saveState(runDir, state, options.onState);
3943
+ await appendLedger(runDir, {
3944
+ type: "candidate.ready",
3945
+ runId,
3946
+ candidateId: candidate.candidateId
3947
+ });
3948
+ break;
3949
+ }
3950
+ candidate.status = "rejected";
3951
+ state.status = "running";
3549
3952
  state.updatedAt = now().toISOString();
3550
3953
  await saveState(runDir, state, options.onState);
3551
3954
  await appendLedger(runDir, {
3552
- type: "candidate.ready",
3955
+ type: "candidate.rejected",
3553
3956
  runId,
3554
3957
  candidateId: candidate.candidateId
3555
3958
  });
3959
+ lastRejectedCandidate = candidate;
3960
+ lastRejectedEvaluation = evaluation;
3961
+ candidate = void 0;
3556
3962
  }
3557
3963
  if (!candidate) {
3558
3964
  state.status = "rejected";
3559
3965
  state.updatedAt = now().toISOString();
3560
3966
  await saveState(runDir, state, options.onState);
3561
- return { runId, runDir, state, promoted: false, blocked: false };
3562
- }
3563
- candidate = await evaluateCandidate(runDir, state, candidate, lifecycle, options, now);
3564
- state.updatedAt = now().toISOString();
3565
- await saveState(runDir, state, options.onState);
3566
- if (!candidate.evaluation?.passed) {
3567
- candidate.status = "rejected";
3568
- state.status = "rejected";
3569
- state.updatedAt = now().toISOString();
3570
- await saveState(runDir, state, options.onState);
3571
- await appendLedger(runDir, {
3572
- type: "candidate.rejected",
3967
+ return {
3573
3968
  runId,
3574
- candidateId: candidate.candidateId
3575
- });
3576
- return { runId, runDir, state, candidate, lifecycle, promoted: false, blocked: false };
3577
- }
3578
- if (options.promote === false) {
3579
- state.status = "candidate-ready";
3580
- state.updatedAt = now().toISOString();
3581
- await saveState(runDir, state, options.onState);
3582
- return { runId, runDir, state, candidate, lifecycle, promoted: false, blocked: false };
3583
- }
3584
- const currentHash = await hashKnowledgeBase(options.root);
3585
- if (currentHash !== state.baseHash) {
3586
- state = await blockRun(
3587
- runDir,
3588
3969
  state,
3589
- `base changed before promotion: expected ${state.baseHash}, got ${currentHash}`,
3590
- options.onState,
3591
- now
3592
- );
3593
- await appendLedger(runDir, {
3594
- type: "promotion.blocked",
3595
- runId,
3596
- candidateId: candidate.candidateId,
3597
- reason: state.blockedReason
3598
- });
3599
- return { runId, runDir, state, candidate, lifecycle, promoted: false, blocked: true };
3970
+ candidate: lastRejectedCandidate,
3971
+ ...lastRejectedEvaluation ? { evaluation: lastRejectedEvaluation } : {},
3972
+ lifecycle,
3973
+ promoted: false,
3974
+ blocked: false
3975
+ };
3600
3976
  }
3601
- await promoteCandidate(options.root, candidate.candidateRoot);
3602
- candidate.status = "promoted";
3603
- candidate.updatedAt = now().toISOString();
3604
- state.status = "promoted";
3605
- state.promotedCandidateId = candidate.candidateId;
3606
- state.updatedAt = now().toISOString();
3607
- await saveState(runDir, state, options.onState);
3608
- await appendLedger(runDir, {
3609
- type: "candidate.promoted",
3977
+ const evidence = await assertCandidateEvidence(runDir, candidateRefFor(runId, state, candidate));
3978
+ return {
3610
3979
  runId,
3611
- candidateId: candidate.candidateId
3612
- });
3613
- return { runId, runDir, state, candidate, lifecycle, promoted: true, blocked: false };
3980
+ state,
3981
+ candidate,
3982
+ evaluation: evidence.evaluation,
3983
+ lifecycle,
3984
+ promoted: false,
3985
+ blocked: false
3986
+ };
3614
3987
  } finally {
3615
3988
  await lease.release();
3616
3989
  }
3617
3990
  }
3991
+ async function promoteReadyCandidate(input) {
3992
+ const { candidateRef, runDir, state } = input;
3993
+ assertStateIdentity(input.root, candidateRef, state);
3994
+ const candidate = state.candidates.find((entry) => entry.candidateId === candidateRef.candidateId);
3995
+ if (!candidate || canonicalJson(candidateRefFor(candidateRef.runId, state, candidate)) !== canonicalJson(candidateRef)) {
3996
+ throw new Error("knowledge candidate approval does not match the measured candidate");
3997
+ }
3998
+ const purpose = promotionTransactionPurpose(candidateRef);
3999
+ return withKnowledgeMutation(
4000
+ input.root,
4001
+ async (mutationLock) => {
4002
+ input.assertRunOwned();
4003
+ const transactionRoot = mutationLock.transactionRoot;
4004
+ let pending = null;
4005
+ const currentHash = await hashKnowledgeBase(input.root);
4006
+ if (state.status === "promoted" && state.promotedCandidateId !== candidate.candidateId) {
4007
+ throw new Error(
4008
+ `knowledge run already promoted '${state.promotedCandidateId ?? "unknown"}'`
4009
+ );
4010
+ }
4011
+ if (state.status === "promoted" && currentHash !== candidateRef.candidateHash) {
4012
+ throw new Error(
4013
+ `promoted knowledge base changed: expected ${candidateRef.candidateHash}, got ${currentHash}`
4014
+ );
4015
+ }
4016
+ if (state.status !== "promoted" && state.status !== "candidate-ready") {
4017
+ throw new Error(
4018
+ `knowledge candidate is not ready for promotion: run=${state.status}, candidate=${candidate.status}`
4019
+ );
4020
+ }
4021
+ if (currentHash !== state.baseHash && currentHash !== candidateRef.candidateHash) {
4022
+ return await blockPromotion(
4023
+ input,
4024
+ candidate,
4025
+ `base changed before promotion: expected ${state.baseHash}, got ${currentHash}`
4026
+ );
4027
+ }
4028
+ if (currentHash !== candidateRef.candidateHash) {
4029
+ pending = await withMeasuredCandidateSnapshot(
4030
+ input.root,
4031
+ runDir,
4032
+ state,
4033
+ candidateRef,
4034
+ (resolved) => withBaselineSnapshot(runDir, state.baseHash, async (baselineRoot) => {
4035
+ const plan = await promotionPlanEntries(baselineRoot, resolved.root);
4036
+ if (knowledgeFileTransactionPlanHash(plan) !== candidateRef.promotionPlanHash) {
4037
+ throw new Error("knowledge candidate promotion plan changed after approval");
4038
+ }
4039
+ return prepareKnowledgeFileTransaction({
4040
+ root: input.root,
4041
+ transactionRoot,
4042
+ purpose,
4043
+ mutations: await promotionMutations(resolved.root, plan),
4044
+ includeUnchanged: true,
4045
+ now: input.now
4046
+ });
4047
+ })
4048
+ );
4049
+ if (!pending) {
4050
+ throw new Error("knowledge promotion plan unexpectedly contained no file changes");
4051
+ }
4052
+ try {
4053
+ assertPromotionTransaction(pending, candidateRef);
4054
+ } catch (error) {
4055
+ try {
4056
+ await rollbackKnowledgeFileTransaction({
4057
+ root: input.root,
4058
+ transactionRoot,
4059
+ transaction: pending,
4060
+ beforeCommit() {
4061
+ mutationLock.assertOwned();
4062
+ input.assertRunOwned();
4063
+ }
4064
+ });
4065
+ await finishKnowledgeFileTransaction({
4066
+ root: input.root,
4067
+ transactionRoot,
4068
+ transaction: pending,
4069
+ assertOwned() {
4070
+ mutationLock.assertOwned();
4071
+ input.assertRunOwned();
4072
+ }
4073
+ });
4074
+ } catch (cleanupError) {
4075
+ throw new AggregateError(
4076
+ [error, cleanupError],
4077
+ "invalid knowledge promotion transaction could not be removed"
4078
+ );
4079
+ }
4080
+ throw error;
4081
+ }
4082
+ }
4083
+ try {
4084
+ if (pending) {
4085
+ await applyKnowledgeFileTransaction({
4086
+ root: input.root,
4087
+ transactionRoot,
4088
+ transaction: pending,
4089
+ beforeCommit() {
4090
+ mutationLock.assertOwned();
4091
+ input.assertRunOwned();
4092
+ }
4093
+ });
4094
+ }
4095
+ mutationLock.assertOwned();
4096
+ input.assertRunOwned();
4097
+ if (await hashKnowledgeBase(input.root) !== candidateRef.candidateHash) {
4098
+ throw new Error("promoted knowledge content does not match the approved candidate");
4099
+ }
4100
+ await writeKnowledgeIndex(input.root);
4101
+ } catch (error) {
4102
+ if (!pending) throw error;
4103
+ try {
4104
+ mutationLock.assertOwned();
4105
+ input.assertRunOwned();
4106
+ } catch (ownershipError) {
4107
+ throw new AggregateError(
4108
+ [error, ownershipError],
4109
+ "knowledge promotion lost its lock and left the transaction pending"
4110
+ );
4111
+ }
4112
+ try {
4113
+ await rollbackKnowledgeFileTransaction({
4114
+ root: input.root,
4115
+ transactionRoot,
4116
+ transaction: pending,
4117
+ beforeCommit() {
4118
+ mutationLock.assertOwned();
4119
+ input.assertRunOwned();
4120
+ }
4121
+ });
4122
+ await finishKnowledgeFileTransaction({
4123
+ root: input.root,
4124
+ transactionRoot,
4125
+ transaction: pending,
4126
+ assertOwned() {
4127
+ mutationLock.assertOwned();
4128
+ input.assertRunOwned();
4129
+ }
4130
+ });
4131
+ await writeKnowledgeIndex(input.root);
4132
+ } catch (rollbackError) {
4133
+ throw new AggregateError(
4134
+ [error, rollbackError],
4135
+ "knowledge promotion failed and could not restore the previous files"
4136
+ );
4137
+ }
4138
+ throw error;
4139
+ }
4140
+ candidate.status = "promoted";
4141
+ candidate.updatedAt = input.now().toISOString();
4142
+ state.status = "promoted";
4143
+ state.promotedCandidateId = candidate.candidateId;
4144
+ state.updatedAt = input.now().toISOString();
4145
+ await saveState(runDir, state, input.onState);
4146
+ await ensurePromotionEvent(runDir, candidateRef);
4147
+ if (pending) {
4148
+ await finishKnowledgeFileTransaction({
4149
+ root: input.root,
4150
+ transactionRoot,
4151
+ transaction: pending,
4152
+ assertOwned() {
4153
+ mutationLock.assertOwned();
4154
+ input.assertRunOwned();
4155
+ }
4156
+ });
4157
+ }
4158
+ return promotionResult(input, candidate, true, false);
4159
+ },
4160
+ {
4161
+ staleMs: input.leaseTtlMs,
4162
+ resumeTransaction: {
4163
+ purpose,
4164
+ validate: (transaction) => assertPromotionTransaction(transaction, candidateRef)
4165
+ }
4166
+ }
4167
+ );
4168
+ }
4169
+ async function blockPromotion(input, candidate, reason) {
4170
+ await blockRun(input.runDir, input.state, reason, input.onState, input.now);
4171
+ await appendLedger(input.runDir, {
4172
+ type: "promotion.blocked",
4173
+ runId: input.candidateRef.runId,
4174
+ candidateId: candidate.candidateId,
4175
+ reason
4176
+ });
4177
+ return promotionResult(input, candidate, false, true);
4178
+ }
4179
+ function promotionResult(input, candidate, promoted, blocked) {
4180
+ return {
4181
+ runId: input.candidateRef.runId,
4182
+ state: input.state,
4183
+ candidate,
4184
+ ...input.lifecycle ? { lifecycle: input.lifecycle } : {},
4185
+ promoted,
4186
+ blocked
4187
+ };
4188
+ }
4189
+ function candidateRefFor(runId, state, candidate) {
4190
+ if (!candidate.candidateHash) {
4191
+ throw new Error(`knowledge candidate '${candidate.candidateId}' has no content hash`);
4192
+ }
4193
+ if (!candidate.evidenceHash) {
4194
+ throw new Error(`knowledge candidate '${candidate.candidateId}' has no evidence hash`);
4195
+ }
4196
+ if (!candidate.promotionPlanHash) {
4197
+ throw new Error(`knowledge candidate '${candidate.candidateId}' has no promotion plan hash`);
4198
+ }
4199
+ if (candidate.status !== "candidate-ready" && candidate.status !== "promoted") {
4200
+ throw new Error(`knowledge candidate '${candidate.candidateId}' is not ready`);
4201
+ }
4202
+ return Object.freeze({
4203
+ schemaVersion: 1,
4204
+ kind: "knowledge-improvement-candidate",
4205
+ runId,
4206
+ candidateId: candidate.candidateId,
4207
+ goalHash: sha256(state.goal),
4208
+ baseHash: candidate.baseHash,
4209
+ candidateHash: candidate.candidateHash,
4210
+ evidenceHash: candidate.evidenceHash,
4211
+ promotionPlanHash: candidate.promotionPlanHash
4212
+ });
4213
+ }
4214
+ async function withMeasuredCandidateSnapshot(liveRoot, runDir, state, candidateRef, use) {
4215
+ assertStateIdentity(liveRoot, candidateRef, state);
4216
+ const candidate = state.candidates.find((entry) => entry.candidateId === candidateRef.candidateId);
4217
+ if (!candidate) {
4218
+ throw new Error(`knowledge candidate '${candidateRef.candidateId}' does not exist`);
4219
+ }
4220
+ const expectedRef = candidateRefFor(candidateRef.runId, state, candidate);
4221
+ if (canonicalJson(expectedRef) !== canonicalJson(candidateRef)) {
4222
+ throw new Error("knowledge candidate approval does not match the measured candidate");
4223
+ }
4224
+ const evidence = await assertCandidateEvidence(runDir, candidateRef);
4225
+ const relativePath = join3(
4226
+ "candidates",
4227
+ candidate.candidateId,
4228
+ "snapshots",
4229
+ candidateRef.candidateHash
4230
+ );
4231
+ return withSafeDirectory(runDir, relativePath, false, async (root) => {
4232
+ if (await hashKnowledgeBase(root) !== candidateRef.candidateHash) {
4233
+ throw new Error("knowledge candidate snapshot changed after approval");
4234
+ }
4235
+ const result = await use({ root, candidate, evidence });
4236
+ if (await hashKnowledgeBase(root) !== candidateRef.candidateHash) {
4237
+ throw new Error("knowledge candidate snapshot changed during use");
4238
+ }
4239
+ return result;
4240
+ });
4241
+ }
4242
+ async function withIsolatedCandidateCopy(sourceRoot, expectedHash, use) {
4243
+ const isolationRoot = await mkdtemp(join3(tmpdir(), "agent-knowledge-candidate-"));
4244
+ const candidateRoot = join3(isolationRoot, "candidate");
4245
+ try {
4246
+ await copyKnowledgeWorkspace(sourceRoot, candidateRoot);
4247
+ if (await hashKnowledgeBase(candidateRoot) !== expectedHash) {
4248
+ throw new Error("isolated knowledge candidate does not match its approved content");
4249
+ }
4250
+ const result = await use(candidateRoot);
4251
+ if (await hashKnowledgeBase(candidateRoot) !== expectedHash) {
4252
+ throw new Error("knowledge candidate snapshot changed during use");
4253
+ }
4254
+ return result;
4255
+ } finally {
4256
+ await rm(isolationRoot, { recursive: true, force: true });
4257
+ }
4258
+ }
4259
+ async function assertCandidateEvidence(runDir, candidate) {
4260
+ const evidence = KnowledgeImprovementEvidenceSchema.parse(
4261
+ JSON.parse(
4262
+ (await readRegularFileWithinRoot(
4263
+ runDir,
4264
+ candidateEvidenceRelativePath(candidate.candidateId)
4265
+ )).bytes.toString("utf8")
4266
+ )
4267
+ );
4268
+ const actualHash = contentHash(evidence);
4269
+ if (actualHash !== candidate.evidenceHash) {
4270
+ throw new Error(
4271
+ `knowledge candidate evidence changed after approval: expected ${candidate.evidenceHash}, got ${actualHash}`
4272
+ );
4273
+ }
4274
+ if (evidence.runId !== candidate.runId || evidence.candidateId !== candidate.candidateId || evidence.goalHash !== candidate.goalHash || evidence.baseHash !== candidate.baseHash || evidence.candidateHash !== candidate.candidateHash || evidence.promotionPlanHash !== candidate.promotionPlanHash || evidence.evaluation.passed !== true) {
4275
+ throw new Error("knowledge candidate evidence does not match the approved candidate");
4276
+ }
4277
+ return evidence;
4278
+ }
4279
+ function assertStateIdentity(root, candidateRef, state) {
4280
+ if (state.runId !== candidateRef.runId) {
4281
+ throw new Error("knowledge candidate run identity does not match persisted state");
4282
+ }
4283
+ if (resolve(state.root) !== resolve(root)) {
4284
+ throw new Error("knowledge candidate root does not match persisted state");
4285
+ }
4286
+ if (sha256(state.goal) !== candidateRef.goalHash) {
4287
+ throw new Error("knowledge candidate goal does not match persisted state");
4288
+ }
4289
+ if (state.baseHash !== candidateRef.baseHash) {
4290
+ throw new Error("knowledge candidate base does not match persisted state");
4291
+ }
4292
+ }
4293
+ async function assertCandidateWorkspace(runDir, candidate) {
4294
+ await withCandidateWorkspace(runDir, candidate, () => void 0);
4295
+ }
4296
+ async function withCandidateWorkspace(runDir, candidate, use) {
4297
+ return withSafeDirectory(
4298
+ runDir,
4299
+ join3("candidates", safePathSegmentSchema.parse(candidate.candidateId), "workspace"),
4300
+ false,
4301
+ use
4302
+ );
4303
+ }
3618
4304
  function assertKnowledgeImprovementOptions(options) {
3619
4305
  if (options.step && options.knowledgeResearch?.step) {
3620
4306
  throw new Error("improveKnowledgeBase accepts either step or knowledgeResearch.step, not both");
@@ -3629,43 +4315,84 @@ function assertKnowledgeImprovementOptions(options) {
3629
4315
  );
3630
4316
  }
3631
4317
  }
3632
- async function runCandidateLifecycle(runDir, runId, candidate, options, now) {
3633
- const lifecycles = [];
3634
- if (shouldRunUpdateStage(options)) {
3635
- const updateLifecycle = await runRagKnowledgeImprovementLoop({
3636
- goal: options.goal,
3637
- acquireKnowledge: options.acquireKnowledge,
3638
- knowledgeResearch: candidateKnowledgeResearchOptions(candidate.candidateRoot, options),
3639
- updateKnowledge: candidateUpdateHook(runId, candidate, options),
3640
- enabledPhases: selectedStagePhases(options, UPDATE_PHASES),
3641
- requiredPhases: selectedStageRequiredPhases(options, UPDATE_PHASES),
3642
- signal: options.signal,
3643
- now
3644
- });
3645
- lifecycles.push(updateLifecycle);
3646
- recordLifecycle(candidate, "candidate-update", updateLifecycle);
3647
- }
3648
- if (shouldRunEvaluationStage(options)) {
3649
- const candidateIndex = await buildKnowledgeIndex(candidate.candidateRoot);
3650
- const evaluationLifecycle = await runRagKnowledgeImprovementLoop({
3651
- goal: options.goal,
3652
- retrieval: options.retrieval ? {
3653
- ...options.retrieval,
3654
- index: candidateIndex,
3655
- runDir: options.retrieval.runDir ?? join3(runDir, "retrieval", candidate.candidateId)
3656
- } : void 0,
3657
- diagnose: options.diagnose,
3658
- evaluateAnswers: options.evaluateAnswers,
3659
- promote: options.decidePromotion,
3660
- enabledPhases: selectedStagePhases(options, EVALUATION_PHASES),
3661
- requiredPhases: selectedStageRequiredPhases(options, EVALUATION_PHASES),
3662
- signal: options.signal,
3663
- now
4318
+ async function measureCandidate(runId, runDir, state, candidate, options, now) {
4319
+ return withCandidateWorkspace(runDir, candidate, async (candidateRoot) => {
4320
+ const currentCandidateHash = await hashKnowledgeBase(candidateRoot);
4321
+ if (candidate.status === "candidate-ready" && candidate.candidateHash === currentCandidateHash && candidate.evidenceHash !== void 0 && candidate.promotionPlanHash !== void 0) {
4322
+ const evidence = await assertCandidateEvidence(
4323
+ runDir,
4324
+ candidateRefFor(runId, state, candidate)
4325
+ );
4326
+ return { candidate, evaluation: evidence.evaluation };
4327
+ }
4328
+ clearCandidateMeasurement(candidate);
4329
+ const lifecycles = [];
4330
+ if (candidate.status === "running") {
4331
+ const updateLifecycle = await runCandidateUpdateLifecycle(
4332
+ runId,
4333
+ candidate,
4334
+ candidateRoot,
4335
+ options,
4336
+ now
4337
+ );
4338
+ if (updateLifecycle) lifecycles.push(updateLifecycle);
4339
+ }
4340
+ return withFrozenCandidateWorkspace(runDir, candidate, candidateRoot, async (snapshot) => {
4341
+ const evaluationLifecycle = await runCandidateEvaluationLifecycle(
4342
+ runDir,
4343
+ candidate,
4344
+ snapshot.root,
4345
+ options,
4346
+ now
4347
+ );
4348
+ if (evaluationLifecycle) lifecycles.push(evaluationLifecycle);
4349
+ const lifecycle = mergeLifecycleResults(options.goal, lifecycles);
4350
+ const measured = await evaluateCandidate(
4351
+ runDir,
4352
+ state,
4353
+ candidate,
4354
+ snapshot,
4355
+ lifecycle,
4356
+ options,
4357
+ now
4358
+ );
4359
+ return { ...measured, ...lifecycle ? { lifecycle } : {} };
3664
4360
  });
3665
- lifecycles.push(evaluationLifecycle);
3666
- recordLifecycle(candidate, "candidate-evaluation", evaluationLifecycle);
3667
- }
3668
- return mergeLifecycleResults(options.goal, lifecycles);
4361
+ });
4362
+ }
4363
+ async function runCandidateUpdateLifecycle(runId, candidate, candidateRoot, options, now) {
4364
+ if (!shouldRunUpdateStage(options)) return void 0;
4365
+ const lifecycle = await runRagKnowledgeImprovementLoop({
4366
+ goal: options.goal,
4367
+ acquireKnowledge: options.acquireKnowledge,
4368
+ knowledgeResearch: candidateKnowledgeResearchOptions(candidateRoot, options),
4369
+ updateKnowledge: candidateUpdateHook(runId, candidate, candidateRoot, options),
4370
+ enabledPhases: selectedStagePhases(options, UPDATE_PHASES),
4371
+ requiredPhases: selectedStageRequiredPhases(options, UPDATE_PHASES),
4372
+ signal: options.signal,
4373
+ now
4374
+ });
4375
+ return lifecycle;
4376
+ }
4377
+ async function runCandidateEvaluationLifecycle(runDir, candidate, candidateRoot, options, now) {
4378
+ if (!shouldRunEvaluationStage(options)) return void 0;
4379
+ const candidateIndex = await buildKnowledgeIndex(candidateRoot);
4380
+ const lifecycle = await runRagKnowledgeImprovementLoop({
4381
+ goal: options.goal,
4382
+ retrieval: options.retrieval ? {
4383
+ ...options.retrieval,
4384
+ index: candidateIndex,
4385
+ runDir: options.retrieval.runDir ?? join3(runDir, "retrieval", candidate.candidateId)
4386
+ } : void 0,
4387
+ diagnose: options.diagnose,
4388
+ evaluateAnswers: options.evaluateAnswers,
4389
+ promote: options.decidePromotion,
4390
+ enabledPhases: selectedStagePhases(options, EVALUATION_PHASES),
4391
+ requiredPhases: selectedStageRequiredPhases(options, EVALUATION_PHASES),
4392
+ signal: options.signal,
4393
+ now
4394
+ });
4395
+ return lifecycle;
3669
4396
  }
3670
4397
  function candidateKnowledgeResearchOptions(candidateRoot, options) {
3671
4398
  if (!options.step && !options.knowledgeResearch) return void 0;
@@ -3682,16 +4409,16 @@ function candidateKnowledgeResearchOptions(candidateRoot, options) {
3682
4409
  readiness: rest.readiness ?? options.readiness
3683
4410
  };
3684
4411
  }
3685
- function candidateUpdateHook(runId, candidate, options) {
4412
+ function candidateUpdateHook(runId, candidate, candidateRoot, options) {
3686
4413
  if (!options.updateKnowledge) return void 0;
3687
4414
  return (input) => options.updateKnowledge({
3688
4415
  ...input,
3689
4416
  runId,
3690
4417
  iteration: candidate.iteration,
3691
4418
  candidateId: candidate.candidateId,
3692
- root: candidate.candidateRoot,
4419
+ root: candidateRoot,
3693
4420
  baselineRoot: options.root,
3694
- candidateRoot: candidate.candidateRoot,
4421
+ candidateRoot,
3695
4422
  baseHash: candidate.baseHash
3696
4423
  });
3697
4424
  }
@@ -3716,20 +4443,6 @@ function selectedStagePhases(options, stagePhases) {
3716
4443
  function selectedStageRequiredPhases(options, stagePhases) {
3717
4444
  return (options.requiredPhases ?? []).filter((phase) => stagePhases.includes(phase));
3718
4445
  }
3719
- function recordLifecycle(candidate, stage, lifecycle) {
3720
- const record = {
3721
- stage,
3722
- phases: lifecycle.phases,
3723
- findingCount: lifecycle.findings.length,
3724
- retrievalWinnerConfig: lifecycle.retrieval?.winnerConfig,
3725
- answerQuality: lifecycle.answerQuality,
3726
- promotionDecision: lifecycle.promotion
3727
- };
3728
- candidate.lifecycle = [...candidate.lifecycle ?? [], record];
3729
- candidate.retrievalWinnerConfig = lifecycle.retrieval?.winnerConfig ?? candidate.retrievalWinnerConfig;
3730
- candidate.answerQuality = lifecycle.answerQuality ?? candidate.answerQuality;
3731
- candidate.promotionDecision = lifecycle.promotion ?? candidate.promotionDecision;
3732
- }
3733
4446
  function mergeLifecycleResults(goal, lifecycles) {
3734
4447
  if (lifecycles.length === 0) return void 0;
3735
4448
  return {
@@ -3749,54 +4462,88 @@ function lastDefined(values) {
3749
4462
  }
3750
4463
  return void 0;
3751
4464
  }
3752
- async function evaluateCandidate(runDir, state, candidate, lifecycle, options, now) {
3753
- const [baselineIndex, candidateIndex] = await Promise.all([
3754
- buildKnowledgeIndex(options.root),
3755
- buildKnowledgeIndex(candidate.candidateRoot)
3756
- ]);
3757
- const validation = validateKnowledgeIndex(candidateIndex, { strict: options.strict });
3758
- const readiness = readinessFor(options, candidateIndex);
3759
- const kbQuality = scoreKnowledgeBaseIndex(candidateIndex, {
3760
- strict: options.strict,
3761
- ...options.kbQuality
3762
- });
3763
- const candidateHash = await hashKnowledgeBase(candidate.candidateRoot);
3764
- const metric = options.evaluate?.({
3765
- runId: state.runId,
3766
- iteration: candidate.iteration,
3767
- root: options.root,
3768
- baselineRoot: options.root,
3769
- candidateRoot: candidate.candidateRoot,
3770
- baselineIndex,
3771
- candidateIndex,
3772
- baseHash: state.baseHash,
3773
- candidateHash,
3774
- validation,
3775
- readiness,
3776
- kbQuality,
3777
- lifecycle,
3778
- signal: options.signal
3779
- }) ?? defaultKnowledgeImprovementMetric(
3780
- validation,
3781
- readiness,
3782
- options.readinessSpecs,
3783
- kbQuality,
3784
- lifecycle
3785
- );
3786
- candidate.validation = validation;
3787
- candidate.kbQuality = kbQuality;
3788
- candidate.readinessBlockingMissing = readiness?.report.blockingMissingRequirements.length;
3789
- candidate.evaluation = applyLifecycleFailures(normalizeMetric(await metric), lifecycle);
3790
- candidate.candidateHash = candidateHash;
3791
- candidate.updatedAt = now().toISOString();
3792
- await appendLedger(runDir, {
3793
- type: "candidate.evaluated",
3794
- runId: state.runId,
3795
- candidateId: candidate.candidateId,
3796
- score: candidate.evaluation.score,
3797
- passed: candidate.evaluation.passed
4465
+ async function evaluateCandidate(runDir, state, candidate, snapshot, lifecycle, options, now) {
4466
+ return withBaselineSnapshot(runDir, state.baseHash, async (baselineRoot) => {
4467
+ const [baselineIndex, candidateIndex] = await Promise.all([
4468
+ buildKnowledgeIndex(baselineRoot),
4469
+ buildKnowledgeIndex(snapshot.root)
4470
+ ]);
4471
+ const validation = validateKnowledgeIndex(candidateIndex, { strict: options.strict });
4472
+ const readiness = readinessFor(options, candidateIndex);
4473
+ const kbQuality = scoreKnowledgeBaseIndex(candidateIndex, {
4474
+ strict: options.strict,
4475
+ ...options.kbQuality
4476
+ });
4477
+ const candidateHash = snapshot.hash;
4478
+ const metric = options.evaluate?.({
4479
+ runId: state.runId,
4480
+ iteration: candidate.iteration,
4481
+ root: options.root,
4482
+ baselineRoot,
4483
+ candidateRoot: snapshot.root,
4484
+ baselineIndex,
4485
+ candidateIndex,
4486
+ baseHash: state.baseHash,
4487
+ candidateHash,
4488
+ validation,
4489
+ readiness,
4490
+ kbQuality,
4491
+ lifecycle,
4492
+ signal: options.signal
4493
+ }) ?? defaultKnowledgeImprovementMetric(
4494
+ validation,
4495
+ readiness,
4496
+ options.readinessSpecs,
4497
+ kbQuality,
4498
+ lifecycle
4499
+ );
4500
+ const evaluation = applyLifecycleFailures(normalizeMetric(await metric), lifecycle);
4501
+ const measuredHash = await hashKnowledgeBase(snapshot.root);
4502
+ if (measuredHash !== candidateHash) {
4503
+ throw new Error(
4504
+ `knowledge candidate changed during evaluation: expected ${candidateHash}, got ${measuredHash}`
4505
+ );
4506
+ }
4507
+ candidate.candidateHash = candidateHash;
4508
+ candidate.promotionPlanHash = knowledgeFileTransactionPlanHash(
4509
+ await promotionPlanEntries(baselineRoot, snapshot.root)
4510
+ );
4511
+ const evidence = KnowledgeImprovementEvidenceSchema.parse(
4512
+ JSON.parse(
4513
+ JSON.stringify({
4514
+ schemaVersion: 1,
4515
+ kind: "knowledge-improvement-evidence",
4516
+ runId: state.runId,
4517
+ candidateId: candidate.candidateId,
4518
+ iteration: candidate.iteration,
4519
+ goalHash: sha256(state.goal),
4520
+ baseHash: candidate.baseHash,
4521
+ candidateHash,
4522
+ promotionPlanHash: candidate.promotionPlanHash,
4523
+ validation,
4524
+ readiness: readiness ?? null,
4525
+ kbQuality,
4526
+ evaluation,
4527
+ lifecycle: lifecycle ?? null
4528
+ })
4529
+ )
4530
+ );
4531
+ candidate.evidenceHash = contentHash(evidence);
4532
+ candidate.updatedAt = now().toISOString();
4533
+ await writeJsonDurableWithinRoot(
4534
+ runDir,
4535
+ candidateEvidenceRelativePath(candidate.candidateId),
4536
+ evidence
4537
+ );
4538
+ await appendLedger(runDir, {
4539
+ type: "candidate.evaluated",
4540
+ runId: state.runId,
4541
+ candidateId: candidate.candidateId,
4542
+ score: evaluation.score,
4543
+ passed: evaluation.passed
4544
+ });
4545
+ return { candidate, evaluation };
3798
4546
  });
3799
- return candidate;
3800
4547
  }
3801
4548
  function defaultKnowledgeImprovementMetric(validation, readiness, readinessSpecs, kbQuality, lifecycle) {
3802
4549
  const blockingMissing = readiness?.report.blockingMissingRequirements.length ?? 0;
@@ -3814,15 +4561,18 @@ function defaultKnowledgeImprovementMetric(validation, readiness, readinessSpecs
3814
4561
  const failedReasons = [
3815
4562
  validation.ok ? void 0 : "candidate validation failed",
3816
4563
  kbQuality.ok ? void 0 : "candidate KB quality check failed",
3817
- blockingMissing === 0 ? void 0 : `${blockingMissing}/${blockingTotal} blocking knowledge requirements still missing`,
3818
- lifecycle?.answerQuality && !lifecycle.answerQuality.passed ? "answer quality failed" : void 0,
3819
- lifecycle?.promotion && !lifecycle.promotion.promoted ? `promotion decision held: ${lifecycle.promotion.reason}` : void 0
4564
+ blockingMissing === 0 ? void 0 : `${blockingMissing}/${blockingTotal} blocking knowledge requirements still missing`
3820
4565
  ].filter((reason) => Boolean(reason));
3821
4566
  return {
3822
4567
  score: average2(Object.values(dimensions)),
3823
4568
  passed: failedReasons.length === 0,
3824
4569
  dimensions,
3825
- notes: failedReasons.length === 0 ? "candidate passed configured checks" : failedReasons.join("; ")
4570
+ notes: failedReasons.length === 0 ? "candidate passed configured checks" : failedReasons.join("; "),
4571
+ provenance: {
4572
+ evaluator: "@tangle-network/agent-knowledge/default-knowledge-improvement-metric",
4573
+ version: "1",
4574
+ method: "deterministic"
4575
+ }
3826
4576
  };
3827
4577
  }
3828
4578
  function applyLifecycleFailures(metric, lifecycle) {
@@ -3839,33 +4589,126 @@ function applyLifecycleFailures(metric, lifecycle) {
3839
4589
  };
3840
4590
  }
3841
4591
  function normalizeMetric(metric) {
3842
- if (!Number.isFinite(metric.score) || metric.score < 0 || metric.score > 1) {
3843
- throw new Error(`knowledge improvement score must be in [0, 1], got ${String(metric.score)}`);
3844
- }
3845
- return { ...metric, passed: Boolean(metric.passed) };
4592
+ return improvementMetricSchema.parse(metric);
3846
4593
  }
3847
4594
  async function createCandidateWorkspace(runDir, state, root, now) {
3848
4595
  const iteration = state.candidates.length + 1;
3849
4596
  const candidateId = stableId("kcand", `${state.runId}:${iteration}:${now().toISOString()}`);
3850
- const candidateRoot = join3(runDir, "candidates", candidateId, "workspace");
4597
+ const candidateRoot = candidateWorkspacePath(runDir, candidateId);
3851
4598
  await copyKnowledgeWorkspace(root, candidateRoot);
3852
4599
  const createdAt = now().toISOString();
3853
4600
  return {
3854
4601
  iteration,
3855
4602
  candidateId,
3856
- candidateRoot,
3857
4603
  baseHash: state.baseHash,
3858
4604
  status: "running",
3859
4605
  createdAt,
3860
4606
  updatedAt: createdAt
3861
4607
  };
3862
4608
  }
4609
+ function candidateWorkspacePath(runDir, candidateId) {
4610
+ return join3(runDir, "candidates", safePathSegmentSchema.parse(candidateId), "workspace");
4611
+ }
4612
+ function baselineSnapshotPath(runDir) {
4613
+ return join3(runDir, "baseline");
4614
+ }
4615
+ async function createBaselineSnapshot(runDir, root, expectedHash) {
4616
+ const target = baselineSnapshotPath(runDir);
4617
+ try {
4618
+ await assertBaselineSnapshot(runDir, expectedHash);
4619
+ return;
4620
+ } catch (error) {
4621
+ if (!isMissingFile(error)) throw error;
4622
+ }
4623
+ const preparation = await mkdtemp(join3(runDir, "baseline-prepare-"));
4624
+ let activated = false;
4625
+ try {
4626
+ await copyKnowledgeWorkspace(root, preparation);
4627
+ const actualHash = await hashKnowledgeBase(preparation);
4628
+ if (actualHash !== expectedHash) {
4629
+ throw new Error(
4630
+ `knowledge base changed while baseline was frozen: expected ${expectedHash}, got ${actualHash}`
4631
+ );
4632
+ }
4633
+ await renameDurable(preparation, target);
4634
+ activated = true;
4635
+ } finally {
4636
+ if (!activated) await rm(preparation, { recursive: true, force: true });
4637
+ }
4638
+ }
4639
+ async function ensureBaselineSnapshot(runDir, root, expectedHash) {
4640
+ try {
4641
+ await assertBaselineSnapshot(runDir, expectedHash);
4642
+ } catch (error) {
4643
+ if (!isMissingFile(error)) throw error;
4644
+ const liveHash = await hashKnowledgeBase(root);
4645
+ if (liveHash !== expectedHash) {
4646
+ throw new Error(
4647
+ "knowledge improvement baseline snapshot is missing and cannot be reconstructed"
4648
+ );
4649
+ }
4650
+ await createBaselineSnapshot(runDir, root, expectedHash);
4651
+ }
4652
+ }
4653
+ async function assertBaselineSnapshot(runDir, expectedHash) {
4654
+ await withBaselineSnapshot(runDir, expectedHash, () => void 0);
4655
+ }
4656
+ async function withBaselineSnapshot(runDir, expectedHash, use) {
4657
+ return withSafeDirectory(runDir, "baseline", false, async (baselineRoot) => {
4658
+ const actualHash = await hashKnowledgeBase(baselineRoot);
4659
+ if (actualHash !== expectedHash) {
4660
+ throw new Error(
4661
+ `knowledge improvement baseline changed: expected ${expectedHash}, got ${actualHash}`
4662
+ );
4663
+ }
4664
+ return use(baselineRoot);
4665
+ });
4666
+ }
4667
+ async function withFrozenCandidateWorkspace(runDir, candidate, candidateRoot, use) {
4668
+ const snapshotsPath = join3(
4669
+ "candidates",
4670
+ safePathSegmentSchema.parse(candidate.candidateId),
4671
+ "snapshots"
4672
+ );
4673
+ return withSafeDirectory(runDir, snapshotsPath, true, async (snapshotsDir) => {
4674
+ const preparation = await mkdtemp(join3(snapshotsDir, "prepare-"));
4675
+ let activated = false;
4676
+ try {
4677
+ await copyKnowledgeWorkspace(candidateRoot, preparation);
4678
+ const hash = await hashKnowledgeBase(preparation);
4679
+ try {
4680
+ const result = await withSafeDirectory(snapshotsDir, hash, false, async (existing) => {
4681
+ if (await hashKnowledgeBase(existing) !== hash) {
4682
+ throw new Error("knowledge candidate snapshot does not match its content identity");
4683
+ }
4684
+ return use({ root: existing, hash });
4685
+ });
4686
+ await rm(preparation, { recursive: true, force: true });
4687
+ activated = true;
4688
+ return result;
4689
+ } catch (error) {
4690
+ if (!isMissingFile(error)) throw error;
4691
+ }
4692
+ await renameDurable(preparation, join3(snapshotsDir, hash));
4693
+ activated = true;
4694
+ return withSafeDirectory(snapshotsDir, hash, false, (root) => use({ root, hash }));
4695
+ } finally {
4696
+ if (!activated) await rm(preparation, { recursive: true, force: true });
4697
+ }
4698
+ });
4699
+ }
4700
+ function clearCandidateMeasurement(candidate) {
4701
+ delete candidate.candidateHash;
4702
+ delete candidate.evidenceHash;
4703
+ delete candidate.promotionPlanHash;
4704
+ }
3863
4705
  function findActiveCandidate(state) {
3864
4706
  return [...state.candidates].reverse().find((candidate) => candidate.status === "candidate-ready" || candidate.status === "running");
3865
4707
  }
3866
4708
  async function copyKnowledgeWorkspace(sourceRoot, targetRoot) {
3867
4709
  await rm(targetRoot, { recursive: true, force: true });
3868
- await initKnowledgeBase(targetRoot);
4710
+ await mkdir(join3(targetRoot, "knowledge"), { recursive: true });
4711
+ await mkdir(join3(targetRoot, "raw", "sources"), { recursive: true });
3869
4712
  await copyIfExists(join3(sourceRoot, "knowledge"), join3(targetRoot, "knowledge"));
3870
4713
  await copyIfExists(join3(sourceRoot, "raw"), join3(targetRoot, "raw"));
3871
4714
  await copyIfExists(
@@ -3874,106 +4717,180 @@ async function copyKnowledgeWorkspace(sourceRoot, targetRoot) {
3874
4717
  );
3875
4718
  await writeKnowledgeIndex(targetRoot);
3876
4719
  }
3877
- async function promoteCandidate(root, candidateRoot) {
3878
- await copyIfExists(join3(candidateRoot, "knowledge"), join3(root, "knowledge"), { replace: true });
3879
- await copyIfExists(join3(candidateRoot, "raw"), join3(root, "raw"), { replace: true });
3880
- await copyIfExists(
3881
- join3(layoutFor(candidateRoot).cacheDir, "sources.json"),
3882
- join3(layoutFor(root).cacheDir, "sources.json"),
3883
- { replace: true }
4720
+ function promotionTransactionPurpose(candidate) {
4721
+ return `knowledge-promotion:${contentHash(candidate)}`;
4722
+ }
4723
+ async function promotionPlanEntries(baselineRoot, candidateRoot) {
4724
+ const [before, after] = await Promise.all([
4725
+ knowledgeHashEntries(baselineRoot),
4726
+ knowledgeHashEntries(candidateRoot)
4727
+ ]);
4728
+ const beforeByPath = new Map(before.map((entry) => [entry.path, entry]));
4729
+ const afterByPath = new Map(after.map((entry) => [entry.path, entry]));
4730
+ const paths = [
4731
+ .../* @__PURE__ */ new Set([...before.map((entry) => entry.path), ...after.map((entry) => entry.path)])
4732
+ ].sort((left, right) => left.localeCompare(right));
4733
+ return paths.map((path) => {
4734
+ assertKnowledgeMutationPath(path);
4735
+ const beforeEntry = beforeByPath.get(path);
4736
+ const afterEntry = afterByPath.get(path);
4737
+ return {
4738
+ path,
4739
+ beforeHash: beforeEntry?.transactionHash ?? null,
4740
+ afterHash: afterEntry?.transactionHash ?? null,
4741
+ ...beforeEntry ? { beforeMode: beforeEntry.mode } : {},
4742
+ ...afterEntry ? { afterMode: afterEntry.mode } : {}
4743
+ };
4744
+ });
4745
+ }
4746
+ async function promotionMutations(candidateRoot, plan) {
4747
+ return Promise.all(
4748
+ plan.map(async (entry) => {
4749
+ if (entry.afterHash === null) return { path: entry.path, content: null };
4750
+ const file = await readRegularFileWithinRoot(candidateRoot, entry.path);
4751
+ const actualHash = createHash("sha256").update(file.bytes).digest("hex");
4752
+ if (actualHash !== entry.afterHash || file.mode !== entry.afterMode) {
4753
+ throw new Error(`knowledge candidate file changed before promotion: ${entry.path}`);
4754
+ }
4755
+ return { path: entry.path, content: file.bytes, mode: file.mode };
4756
+ })
3884
4757
  );
3885
- await writeKnowledgeIndex(root);
3886
4758
  }
3887
- async function copyIfExists(source, target, options = {}) {
3888
- try {
3889
- await stat(source);
3890
- } catch {
3891
- return;
4759
+ function assertPromotionTransaction(transaction, candidate) {
4760
+ const actualPlanHash = knowledgeFileTransactionPlanHash(transaction.entries);
4761
+ if (actualPlanHash !== candidate.promotionPlanHash) {
4762
+ throw new Error(
4763
+ `knowledge promotion plan changed after approval: expected ${candidate.promotionPlanHash}, got ${actualPlanHash}`
4764
+ );
3892
4765
  }
3893
- if (options.replace) await rm(target, { recursive: true, force: true });
3894
- await mkdir3(dirname2(target), { recursive: true });
3895
- await cp(source, target, { recursive: true });
3896
4766
  }
3897
- async function hashKnowledgeBase(root) {
3898
- const entries = [];
3899
- for (const rel of ["knowledge", "raw"]) {
3900
- entries.push(...await hashTreeEntries(root, rel));
4767
+ async function ensurePromotionEvent(runDir, candidateRef) {
4768
+ if (await hasPromotionEvent(runDir, candidateRef)) return;
4769
+ await appendLedger(runDir, {
4770
+ type: "candidate.promoted",
4771
+ runId: candidateRef.runId,
4772
+ candidateId: candidateRef.candidateId,
4773
+ candidateHash: candidateRef.candidateHash,
4774
+ evidenceHash: candidateRef.evidenceHash,
4775
+ promotionPlanHash: candidateRef.promotionPlanHash
4776
+ });
4777
+ }
4778
+ async function hasPromotionEvent(runDir, candidateRef) {
4779
+ let matched = false;
4780
+ for (const row of await loadKnowledgeImprovementEventsFromRun(runDir)) {
4781
+ if (row.type !== "candidate.promoted" || row.candidateId !== candidateRef.candidateId) continue;
4782
+ if (row.runId !== candidateRef.runId || row.candidateHash !== candidateRef.candidateHash || row.evidenceHash !== candidateRef.evidenceHash || row.promotionPlanHash !== candidateRef.promotionPlanHash) {
4783
+ throw new Error("persisted knowledge promotion event conflicts with the approved candidate");
4784
+ }
4785
+ matched = true;
3901
4786
  }
3902
- const sourceRegistry = relative(root, layoutFor(root).sourceRegistryPath).replace(/\\/g, "/");
3903
- entries.push(...await hashFileEntry(root, sourceRegistry));
3904
- entries.sort((a, b) => a.path.localeCompare(b.path));
3905
- return sha256(JSON.stringify(entries));
4787
+ return matched;
3906
4788
  }
3907
- async function hashTreeEntries(root, relDir) {
3908
- const abs = join3(root, relDir);
4789
+ async function loadKnowledgeImprovementEvents(root, runId) {
4790
+ const parsedRunId = runIdSchema.parse(runId);
3909
4791
  try {
3910
- const s = await stat(abs);
3911
- if (!s.isDirectory()) return [];
3912
- } catch {
3913
- return [];
3914
- }
3915
- const out = [];
3916
- const entries = await readdir(abs, { withFileTypes: true });
3917
- for (const entry of entries) {
3918
- const rel = join3(relDir, entry.name).replace(/\\/g, "/");
3919
- if (entry.isDirectory()) out.push(...await hashTreeEntries(root, rel));
3920
- else if (entry.isFile()) out.push(...await hashFileEntry(root, rel));
4792
+ return await withKnowledgeImprovementRun(
4793
+ root,
4794
+ parsedRunId,
4795
+ false,
4796
+ (runDir) => loadKnowledgeImprovementEventsFromRun(runDir)
4797
+ );
4798
+ } catch (error) {
4799
+ if (isMissingFile(error)) return [];
4800
+ throw error;
3921
4801
  }
3922
- return out;
3923
4802
  }
3924
- async function hashFileEntry(root, rel) {
4803
+ async function loadKnowledgeImprovementEventsFromRun(runDir) {
4804
+ const events = [];
3925
4805
  try {
3926
- const bytes = await readFile2(join3(root, rel));
3927
- return [{ path: rel, hash: sha256(bytes.toString("base64")) }];
3928
- } catch {
3929
- return [];
4806
+ for (const file of await listRegularFilesWithinRoot(runDir, "events")) {
4807
+ const name = file.path.slice("events/".length);
4808
+ if (name.includes("/") || !name.endsWith(".json")) {
4809
+ throw new Error(`knowledge event store contains an unsupported entry: ${name}`);
4810
+ }
4811
+ events.push(parseKnowledgeImprovementEvent(JSON.parse(file.bytes.toString("utf8"))));
4812
+ }
4813
+ } catch (error) {
4814
+ if (!isMissingFile(error)) throw error;
4815
+ }
4816
+ const unique2 = /* @__PURE__ */ new Map();
4817
+ for (const event of events) {
4818
+ const { at: _at, ...semantic } = event;
4819
+ unique2.set(contentHash(semantic), event);
3930
4820
  }
4821
+ return [...unique2.values()].sort((left, right) => left.at.localeCompare(right.at));
3931
4822
  }
3932
- async function acquireRunLease(runDir, options) {
3933
- const path = join3(runDir, "run.lock");
3934
- const now = options.now();
3935
- const expiresAt = new Date(now.getTime() + options.ttlMs);
3936
- const payload = {
3937
- ownerId: options.ownerId,
3938
- acquiredAt: now.toISOString(),
3939
- expiresAt: expiresAt.toISOString(),
3940
- pid: process.pid
3941
- };
4823
+ function parseKnowledgeImprovementEvent(value) {
4824
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4825
+ throw new Error("knowledge improvement event is not an object");
4826
+ }
4827
+ const event = value;
4828
+ if (typeof event.at !== "string" || typeof event.type !== "string") {
4829
+ throw new Error("knowledge improvement event is missing at or type");
4830
+ }
4831
+ return event;
4832
+ }
4833
+ async function copyIfExists(source, target) {
4834
+ let sourceStat;
3942
4835
  try {
3943
- const handle = await open(path, "wx");
4836
+ sourceStat = await lstat(source);
4837
+ } catch (error) {
4838
+ if (isMissingFile(error)) return;
4839
+ throw error;
4840
+ }
4841
+ if (!sourceStat.isDirectory() && !sourceStat.isFile()) {
4842
+ throw new Error(`knowledge surface contains an unsupported filesystem entry: ${source}`);
4843
+ }
4844
+ await mkdir(dirname(target), { recursive: true });
4845
+ await cp(source, target, { recursive: sourceStat.isDirectory(), dereference: false });
4846
+ }
4847
+ async function hashKnowledgeBase(root) {
4848
+ return withKnowledgeRead(root, () => hashKnowledgeBaseUnlocked(root));
4849
+ }
4850
+ async function hashKnowledgeBaseUnlocked(root) {
4851
+ const entries = await knowledgeHashEntries(root);
4852
+ return sha256(JSON.stringify(entries.map(({ path, hash, mode }) => ({ path, hash, mode }))));
4853
+ }
4854
+ async function knowledgeHashEntries(root) {
4855
+ const entries = [];
4856
+ for (const rel of ["knowledge", "raw"]) {
3944
4857
  try {
3945
- await handle.writeFile(`${JSON.stringify(payload, null, 2)}
3946
- `, "utf8");
3947
- } finally {
3948
- await handle.close();
4858
+ for (const file of await listRegularFilesWithinRoot(root, rel)) {
4859
+ entries.push(knowledgeFileIdentity(file.path, file.bytes, file.mode));
4860
+ }
4861
+ } catch (error) {
4862
+ if (!isMissingFile(error)) throw error;
3949
4863
  }
4864
+ }
4865
+ const sourceRegistry = relative(root, layoutFor(root).sourceRegistryPath).replace(/\\/g, "/");
4866
+ try {
4867
+ const file = await readRegularFileWithinRoot(root, sourceRegistry);
4868
+ entries.push(knowledgeFileIdentity(sourceRegistry, file.bytes, file.mode));
3950
4869
  } catch (error) {
3951
- const code = error.code;
3952
- if (code !== "EEXIST") throw error;
3953
- const existing = await readLease(path);
3954
- if (existing && new Date(existing.expiresAt).getTime() > now.getTime()) {
3955
- throw new Error(
3956
- `knowledge improvement run is locked by ${existing.ownerId} until ${existing.expiresAt}`
3957
- );
3958
- }
3959
- await rm(path, { force: true });
3960
- return acquireRunLease(runDir, options);
4870
+ if (!isMissingFile(error)) throw error;
3961
4871
  }
4872
+ entries.sort((a, b) => a.path.localeCompare(b.path));
4873
+ return entries;
4874
+ }
4875
+ function knowledgeFileIdentity(path, bytes, mode) {
3962
4876
  return {
3963
- ownerId: options.ownerId,
3964
4877
  path,
3965
- async release() {
3966
- const current = await readLease(path);
3967
- if (!current || current.ownerId === options.ownerId) await rm(path, { force: true });
3968
- }
4878
+ hash: sha256(bytes.toString("base64")),
4879
+ transactionHash: createHash("sha256").update(bytes).digest("hex"),
4880
+ mode
3969
4881
  };
3970
4882
  }
3971
- async function readLease(path) {
3972
- try {
3973
- return JSON.parse(await readFile2(path, "utf8"));
3974
- } catch {
3975
- return null;
3976
- }
4883
+ async function acquireRunLease(runDir, options) {
4884
+ const path = join3(runDir, "run.lock.durable");
4885
+ const acquired = await acquireDurableFileLock(runDir, {
4886
+ lockfilePath: path,
4887
+ staleMs: options.ttlMs
4888
+ });
4889
+ return {
4890
+ ownerId: options.ownerId,
4891
+ assertOwned: acquired.assertOwned,
4892
+ release: acquired.release
4893
+ };
3977
4894
  }
3978
4895
  async function blockRun(runDir, state, reason, onState, now) {
3979
4896
  state.status = "blocked";
@@ -3983,24 +4900,51 @@ async function blockRun(runDir, state, reason, onState, now) {
3983
4900
  return state;
3984
4901
  }
3985
4902
  async function saveState(runDir, state, onState) {
3986
- await writeJsonAtomic(statePath(runDir), state);
4903
+ await writeJsonDurableWithinRoot(
4904
+ runDir,
4905
+ "state.json",
4906
+ KnowledgeImprovementRunStateSchema.parse(state)
4907
+ );
3987
4908
  await onState?.(state);
3988
4909
  }
3989
4910
  async function appendLedger(runDir, value) {
3990
- const row = { at: (/* @__PURE__ */ new Date()).toISOString(), ...value };
3991
- await mkdir3(runDir, { recursive: true });
3992
- await writeFile3(join3(runDir, "events.jsonl"), `${JSON.stringify(row)}
3993
- `, { flag: "a" });
4911
+ const type = value.type;
4912
+ if (typeof type !== "string" || type.length === 0) {
4913
+ throw new Error("knowledge improvement event requires a type");
4914
+ }
4915
+ const relativePath = join3("events", `${contentHash(value)}.json`).replace(/\\/g, "/");
4916
+ try {
4917
+ const file = await readRegularFileWithinRoot(runDir, relativePath);
4918
+ const existing = parseKnowledgeImprovementEvent(JSON.parse(file.bytes.toString("utf8")));
4919
+ const { at: _at, ...semantic } = existing;
4920
+ if (canonicalJson(semantic) !== canonicalJson(value)) {
4921
+ throw new Error("knowledge improvement event identity conflicts with durable content");
4922
+ }
4923
+ return;
4924
+ } catch (error) {
4925
+ if (!isMissingFile(error)) throw error;
4926
+ }
4927
+ await writeJsonDurableWithinRoot(runDir, relativePath, {
4928
+ at: (/* @__PURE__ */ new Date()).toISOString(),
4929
+ ...value
4930
+ });
3994
4931
  }
3995
- async function writeJsonAtomic(path, value) {
3996
- await mkdir3(dirname2(path), { recursive: true });
3997
- const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
3998
- await writeFile3(tmp, `${JSON.stringify(value, null, 2)}
3999
- `, "utf8");
4000
- await rename(tmp, path);
4932
+ function candidateEvidenceRelativePath(candidateId) {
4933
+ return join3("candidates", safePathSegmentSchema.parse(candidateId), "evidence.json").replace(
4934
+ /\\/g,
4935
+ "/"
4936
+ );
4937
+ }
4938
+ function descendantPath(root, path) {
4939
+ const value = relative(resolve(root), resolve(path)).replace(/\\/g, "/");
4940
+ if (value === "" || value === ".." || value.startsWith("../") || isAbsolute(value))
4941
+ return void 0;
4942
+ return value;
4001
4943
  }
4002
- function statePath(runDir) {
4003
- return join3(runDir, "state.json");
4944
+ function assertExactCandidatePlatform() {
4945
+ if (process.platform !== "linux") {
4946
+ throw new Error("exact knowledge candidate workflows require Linux directory descriptors");
4947
+ }
4004
4948
  }
4005
4949
  function average2(values) {
4006
4950
  const finite = values.filter(Number.isFinite);
@@ -4009,8 +4953,7 @@ function average2(values) {
4009
4953
  }
4010
4954
 
4011
4955
  // src/kb-store.ts
4012
- import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4 } from "fs/promises";
4013
- import { dirname as dirname3, join as join4 } from "path";
4956
+ import { z as z3 } from "zod";
4014
4957
  var MemoryKbStore = class {
4015
4958
  sources = /* @__PURE__ */ new Map();
4016
4959
  pages = /* @__PURE__ */ new Map();
@@ -4062,33 +5005,115 @@ var MemoryKbStore = class {
4062
5005
  return out.slice(-(query.limit ?? out.length)).map(clone);
4063
5006
  }
4064
5007
  };
4065
- var FileSystemKbStore = class extends MemoryKbStore {
5008
+ var knowledgeEventsSchema = z3.array(KnowledgeEventSchema);
5009
+ var FileSystemKbStore = class {
4066
5010
  constructor(dir) {
4067
- super();
4068
5011
  this.dir = dir;
4069
5012
  }
4070
5013
  dir;
5014
+ async putSource(source) {
5015
+ const parsed = SourceRecordSchema.parse(source);
5016
+ await this.updateIndex((index) => ({
5017
+ ...index,
5018
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5019
+ sources: [parsed, ...index.sources.filter((entry) => entry.id !== parsed.id)]
5020
+ }));
5021
+ }
5022
+ async getSource(id) {
5023
+ return withKnowledgeRead(this.dir, async () => {
5024
+ const index = await this.readIndex();
5025
+ return clone(index?.sources.find((source) => source.id === id) ?? null);
5026
+ });
5027
+ }
5028
+ async listSources() {
5029
+ return withKnowledgeRead(this.dir, async () => clone((await this.readIndex())?.sources ?? []));
5030
+ }
5031
+ async putPage(page) {
5032
+ const parsed = KnowledgePageSchema.parse(page);
5033
+ await this.updateIndex((index) => {
5034
+ const pages = [parsed, ...index.pages.filter((entry) => entry.id !== parsed.id)];
5035
+ return {
5036
+ ...index,
5037
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5038
+ pages,
5039
+ graph: buildKnowledgeGraph(pages)
5040
+ };
5041
+ });
5042
+ }
5043
+ async getPage(idOrPath) {
5044
+ return withKnowledgeRead(this.dir, async () => {
5045
+ const index = await this.readIndex();
5046
+ return clone(
5047
+ index?.pages.find((page) => page.id === idOrPath || page.path === idOrPath) ?? null
5048
+ );
5049
+ });
5050
+ }
5051
+ async listPages() {
5052
+ return withKnowledgeRead(this.dir, async () => clone((await this.readIndex())?.pages ?? []));
5053
+ }
4071
5054
  async putIndex(index) {
4072
- await super.putIndex(index);
4073
- await writeJson2(join4(this.dir, "index.json"), index);
5055
+ const parsed = KnowledgeIndexSchema.parse(index);
5056
+ await withKnowledgeMutation(
5057
+ this.dir,
5058
+ () => writeJsonDurableWithinRoot(this.dir, "index.json", parsed)
5059
+ );
4074
5060
  }
4075
5061
  async getIndex() {
4076
- try {
4077
- return JSON.parse(await readFile3(join4(this.dir, "index.json"), "utf8"));
4078
- } catch {
4079
- return await super.getIndex();
4080
- }
5062
+ return withKnowledgeRead(this.dir, () => this.readIndex());
4081
5063
  }
4082
5064
  async putEvent(event) {
4083
- await super.putEvent(event);
4084
- const events = await this.listEvents();
4085
- await writeJson2(join4(this.dir, "events.json"), events);
5065
+ const parsed = KnowledgeEventSchema.parse(event);
5066
+ await withKnowledgeMutation(this.dir, async () => {
5067
+ const current = await this.readEvents();
5068
+ const next = [...current.filter((entry) => entry.id !== parsed.id), parsed].sort(
5069
+ (a, b) => a.createdAt.localeCompare(b.createdAt)
5070
+ );
5071
+ await writeJsonDurableWithinRoot(this.dir, "events.json", next);
5072
+ });
5073
+ }
5074
+ async listEvents(query = {}) {
5075
+ return withKnowledgeRead(this.dir, async () => {
5076
+ let events = await this.readEvents();
5077
+ if (query.type) events = events.filter((event) => event.type === query.type);
5078
+ if (query.target) events = events.filter((event) => event.target === query.target);
5079
+ return clone(events.slice(-(query.limit ?? events.length)));
5080
+ });
5081
+ }
5082
+ async updateIndex(change) {
5083
+ await withKnowledgeMutation(this.dir, async () => {
5084
+ const current = await this.readIndex() ?? emptyIndex2(this.dir);
5085
+ const next = KnowledgeIndexSchema.parse(change(current));
5086
+ await writeJsonDurableWithinRoot(this.dir, "index.json", next);
5087
+ });
5088
+ }
5089
+ async readIndex() {
5090
+ return readJsonFile(
5091
+ this.dir,
5092
+ "index.json",
5093
+ KnowledgeIndexSchema
5094
+ );
5095
+ }
5096
+ async readEvents() {
5097
+ return await readJsonFile(this.dir, "events.json", knowledgeEventsSchema) ?? [];
4086
5098
  }
4087
5099
  };
4088
- async function writeJson2(path, value) {
4089
- await mkdir4(dirname3(path), { recursive: true });
4090
- await writeFile4(path, `${JSON.stringify(value, null, 2)}
4091
- `, "utf8");
5100
+ function emptyIndex2(root) {
5101
+ return {
5102
+ root,
5103
+ generatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
5104
+ sources: [],
5105
+ pages: [],
5106
+ graph: { nodes: [], edges: [] }
5107
+ };
5108
+ }
5109
+ async function readJsonFile(root, relativePath, schema) {
5110
+ try {
5111
+ const file = await readRegularFileWithinRoot(root, relativePath);
5112
+ return schema.parse(JSON.parse(file.bytes.toString("utf8")));
5113
+ } catch (error) {
5114
+ if (error?.code === "ENOENT") return null;
5115
+ throw error;
5116
+ }
4092
5117
  }
4093
5118
  function clone(value) {
4094
5119
  return value == null ? value : JSON.parse(JSON.stringify(value));
@@ -4330,11 +5355,11 @@ async function evaluateKnowledgeBaseReadiness(options) {
4330
5355
  // src/release.ts
4331
5356
  import {
4332
5357
  evaluateReleaseConfidence,
4333
- validateRunRecord
5358
+ validateRunRecord as validateRunRecord2
4334
5359
  } from "@tangle-network/agent-eval";
4335
5360
  function knowledgeReleaseReport(input) {
4336
5361
  const baselineRuns = input.baselineRuns ?? [];
4337
- const runRecords = [...input.candidateRuns, ...baselineRuns].map(validateRunRecord);
5362
+ const runRecords = [...input.candidateRuns, ...baselineRuns].map(validateRunRecord2);
4338
5363
  const scorecard = evaluateReleaseConfidence({
4339
5364
  target: "agent-knowledge-base",
4340
5365
  candidateId: input.candidateId,
@@ -4782,6 +5807,9 @@ export {
4782
5807
  KnowledgeEventSchema,
4783
5808
  KnowledgeGraphEdgeSchema,
4784
5809
  KnowledgeGraphNodeSchema,
5810
+ KnowledgeImprovementCandidateRefSchema,
5811
+ KnowledgeImprovementEvidenceSchema,
5812
+ KnowledgeImprovementRunStateSchema,
4785
5813
  KnowledgeIndexSchema,
4786
5814
  KnowledgePageSchema,
4787
5815
  KnowledgeProposalParseError,
@@ -4860,18 +5888,21 @@ export {
4860
5888
  initKnowledgeBase,
4861
5889
  innerHtmlById,
4862
5890
  inspectKnowledgeIndex,
5891
+ inspectPendingKnowledgeMutation,
4863
5892
  investmentThesisSet,
4864
5893
  isKnowledgeMemoryBenchmarkCase,
4865
5894
  isSafeKnowledgePath,
4866
5895
  isScaffoldPath,
4867
5896
  kbIndexToText,
4868
5897
  knowledgeBenchmarkJudge,
5898
+ knowledgeImprovementCandidateRef,
4869
5899
  knowledgeImprovementRunDir,
4870
5900
  knowledgeImprovementRunId,
4871
5901
  knowledgeReleaseReport,
4872
5902
  layoutFor,
4873
5903
  lensDistribution,
4874
5904
  lintKnowledgeIndex,
5905
+ loadKnowledgeImprovementEvents,
4875
5906
  loadKnowledgeImprovementState,
4876
5907
  loadKnowledgePages,
4877
5908
  loadSourceRegistry,
@@ -4888,10 +5919,12 @@ export {
4888
5919
  parseKnowledgeBenchmarkQrels,
4889
5920
  parseKnowledgeWriteBlocks,
4890
5921
  politeFetch,
5922
+ promoteKnowledgeCandidate,
4891
5923
  proposeFromFinding,
4892
5924
  proposeFromFindings,
4893
5925
  ragAnswerQualityJudge,
4894
5926
  reciprocalRankFusion,
5927
+ recoverPendingKnowledgeMutation,
4895
5928
  renderKnowledgeBenchmarkReportMarkdown,
4896
5929
  renderMemoryContext,
4897
5930
  resetRetrievalHoldoutRegistry,
@@ -4936,6 +5969,7 @@ export {
4936
5969
  triageSource,
4937
5970
  validateKnowledgeIndex,
4938
5971
  withCitedClaim,
5972
+ withKnowledgeImprovementCandidate,
4939
5973
  writeJson,
4940
5974
  writeKnowledgeIndex,
4941
5975
  writeSourceRegistry