From 345829decac9f33c3c06055bfe9c7ae1ddfae68f Mon Sep 17 00:00:00 2001 From: Drew Stone Date: Fri, 17 Jul 2026 20:20:26 -0600 Subject: [PATCH] fix(improvement): keep promoted run reopening read-only --- package.json | 2 +- src/kb-improvement.ts | 38 ++++++++++++++++++++++-------------- tests/kb-improvement.test.ts | 26 ++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 82b9972..8be44de 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tangle-network/agent-knowledge", - "version": "3.2.0", + "version": "3.2.1", "description": "Source-grounded, eval-gated knowledge growth primitives for agents.", "homepage": "https://github.com/tangle-network/agent-knowledge#readme", "repository": { diff --git a/src/kb-improvement.ts b/src/kb-improvement.ts index 2d64453..158f653 100644 --- a/src/kb-improvement.ts +++ b/src/kb-improvement.ts @@ -841,25 +841,33 @@ async function improveKnowledgeBaseInRun( if (state.status === 'promoted' && !promotedCandidate) { throw new Error('promoted knowledge state has no promoted candidate') } - await withKnowledgeMutation(options.root, () => undefined) - await ensureBaselineSnapshot(runDir, options.root, state.baseHash) - if (state.status === 'promoted') { const promoted = promotedCandidate! - return await applyKnowledgeCandidateTarget( - { - root: options.root, + const promotedState = state + return withKnowledgeMutation(options.root, async () => { + const currentHash = await hashKnowledgeBase(options.root) + if (currentHash !== promoted.candidateHash) { + throw new Error( + `promoted knowledge base changed: expected ${promoted.candidateHash}, got ${currentHash}`, + ) + } + const evidence = await assertCandidateEvidence( runDir, - state, - candidateRef: candidateRefFor(runId, state, promoted), - leaseTtlMs: options.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS, - assertRunOwned: lease.assertOwned, - now, - onState: options.onState, - }, - 'candidate', - ) + candidateRefFor(runId, promotedState, promoted), + ) + return { + runId, + state: promotedState, + candidate: promoted, + evaluation: evidence.evaluation, + promoted: true, + blocked: false, + } + }) } + await withKnowledgeMutation(options.root, () => undefined) + await ensureBaselineSnapshot(runDir, options.root, state.baseHash) + if (state.status === 'blocked') { return { runId, state, promoted: false, blocked: true } } diff --git a/tests/kb-improvement.test.ts b/tests/kb-improvement.test.ts index dbb8cd4..bfcb723 100644 --- a/tests/kb-improvement.test.ts +++ b/tests/kb-improvement.test.ts @@ -260,6 +260,32 @@ describe('improveKnowledgeBase', () => { }) }) + it('does not reapply a promoted candidate when the improvement run is reopened', async () => { + await withEmptyRoot(async (root) => { + const options = { + root, + goal: 'Keep candidate generation separate from activation', + runId: 'reopen-promoted-run', + updateKnowledge: async ({ candidateRoot }: { candidateRoot: string }) => { + await writeFile(join(candidateRoot, 'knowledge', 'candidate.md'), '# Candidate\n') + return { applied: true, summary: 'created candidate' } + }, + evaluate: passingMetric, + } + const staged = await improveKnowledgeBase(options) + const candidate = knowledgeImprovementCandidateRef(staged) + await promoteKnowledgeCandidate({ root, candidate }) + await rm(join(root, 'knowledge', 'candidate.md')) + const liveBeforeReopen = await hashKnowledgeBase(root) + + await expect(improveKnowledgeBase(options)).rejects.toThrow(/promoted knowledge base changed/) + expect(await hashKnowledgeBase(root)).toBe(liveBeforeReopen) + await expect(readFile(join(root, 'knowledge', 'candidate.md'), 'utf8')).rejects.toMatchObject( + { code: 'ENOENT' }, + ) + }) + }) + it('rejects evaluator results without provenance', async () => { await withKb(async (root) => { await expect(