feat(history): record the candidate search into the canonical ledger - #673
Merged
Conversation
tangletools
approved these changes
Aug 21, 2026
tangletools
left a comment
Contributor
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — ace153e1
This PR was opened by the trusted drewstone account.
This approval is provisional and was applied by the local stand-in because the pr-reviewer webhook host is unreachable (2026-08-21). CI on this head is fully green. The full PR reviewer audit re-runs via the resweep when the service returns and will publish findings if it detects issues.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reconcile
git grep -n "openSearchLedger(\|searchLedger" origin/main -- srcoutsidesearch-ledger*.tsreturns nothing: the ledger has no caller in its own package, exactly as #633 states.git grep "search-plan-extended" origin/mainreturns nothing. Nothing in the last 60 merged PRs emits ledger events. This PR wires the existingFileSearchLedger; it does not add a second lineage record.Why
FileSearchLedgerimplements a hash-chained, fsynced, replayable search audit with multi-parent lineage and a hard generation invariant — and had no caller in its own package.runOptimizationemitted nothing, so the search loop and the lineage recorder never met, anddocs/search-history-receipts.md:67("first-party optimizer adapters should do this automatically") was unfulfilled: no first-party method returnedsearchHistory, socompareOptimizationMethods({ searchHistoryPolicy: 'require-complete' })refused every one of them. A downstream consumer that tried to adopt the ledger for a rolling search hit four refusals, one of which — no plan extension — has no workaround inside one ledger.What
runOptimization({ searchLedger })andselfImprove({ searchLedger })record the search and returnsearchHistory: SearchHistoryReceipt. One recorder (src/campaign/search-ledger-recording.ts) emits the plan (slots = generations x populationSize, one candidate-generation operation per generation, one selection operation, one task per designed scenario-replicate cell), one registration per candidate carrying the exact parent surface it mutated (PR feat(optimization): let a policy choose the parent surface each generation #669'sselectParentdecides which), one attempt per scored cell with the cell's own outcome and accounting, one decision per candidate, and the terminal event.in-progress, and the receipt reports the exact gap rather than claiming a closed search.agent,proposer,search,model). A measured value wins wherever execution reported one; a cell that ran a moving model alias is refused rather than recorded as an immutable identity.search-plan-extendedevent appends candidate slots and operations. Replay merges the first plan with every extension, the generation invariant continues across rounds, and the planless refusal is untouched. The planned task denominator does not extend — that would reopen candidates that already closed theirs. One ledger per open-ended search stays the rule; an external parent enters as a generation-0 registration whose surface artifact references the prior ledger. The loop itself uses the extension when a proposer returns more candidates than the planned population.recordCandidatePopulationSearch()turns GEPA's candidate-population artifact (its parent graph and per-scenario selection scores) into the same event stream, wired behindgepaOptimizationMethod({ searchLedger: { identity } }). This is the artifact becoming a source for the canonical ledger, not a parallel lineage.docs/search-history-receipts.mdgains the loop recipe, the identity contract, and the rolling-search rules. Version 0.152.0 -> 0.153.0 with the Python client and both source digests in lockstep.Proof
pnpm typecheck,pnpm typecheck:examples,pnpm typecheck:scripts,pnpm lint(Biome, 723 files): clean.pnpm test(full): every failing file re-run serially passes except the environment-bound set that also fails on cleanorigin/main(GEPA/SkillOpt Python-bridge suites without theuvvenv CI installs, git-worktree suites, macOS/private/vartmpdir symlink). The full parallel run on this machine reports ~13 timeout failures at the 5s per-test limit that pass individually — includingsrc/campaign/search-ledger.test.ts(37 tests, 5.5s alone). One genuine regression the serial re-run caught is fixed in this branch: a version bump had overwrittenANALYST_BENCHMARK_EVIDENCE_DEPENDENCY_LOCK_SHA256, a historical constant, which brokesrc/analyst/benchmark-reference-result.test.ts; only the live dependency-lock digest moves now, and that suite is 5/5 green.pnpm build: clean (tsdown + OpenAPI emit), exit 0.pnpm verify:package: clean, exit 0 (publint, attw, packed-export checks, source digests, evidence index fresh).vitest run src/campaign/search-ledger.test.ts src/campaign/presets/run-optimization-search-ledger.test.ts src/campaign/presets/compare-optimization-methods-history.test.ts— 120 tests, 118 passed and the 2 failures were the digest regression above, now green.Simplification:
FileSearchLedgergains its first caller instead of a second lineage recorder — GEPA's candidate-population artifact now feeds the SAME ledger throughrecordCandidatePopulationSearch(), so the package has one lineage record with two sources, not two records. The projector's four scatteredplanEvent.plan.candidateSlots.find(...)/.operations.find(...)scans collapse into two effective-plan maps that plan and extension both write, so a rolling search cannot diverge from the plan-only path.Net: +1766 / -217 lines, 20 files; 4 ad-hoc plan scans -> 2 maps; 0 new lineage formats.
Tests: +6 (rolling-round replay from durable bytes; the planless refusal the downstream consumer hit; an extended slot left open still blocks completion; the loop's five event kinds with a complete, digest-verified receipt; a failed cell reporting the exact gap instead of a closed search;
require-completeaccepting a first-party method), -3 deleted (twoselectParentunit assertions that restated the type, and aselfImproveforwarding change-detector whose 2-generation run also pushed its file over the 10s limit).Not done here: the ledger records one attempt per cell with
maxAttempts: 1; #670's in-run cell retry (merged after this branch started) means a retried cell now has attempts the ledger sees only as its final result. Recording each retry attempt is a follow-up that belongs with the retry owner.Closes #633