feat(run): heal mid-batch drift once and let the remaining rows reuse the fix - #26
Merged
Merged
Conversation
… the fix
`run --heal` only ever repaired the warm-up row. If a site changed at row 900 of
a 5,000-row batch — an A/B test flips a button label, a banner appears — every
row from there on returned drift, and each still paid for a Chrome launch. The
operator noticed later, ran `replay --heal` by hand, then `run --resume`.
Fan-out rows still replay strictly on the fast path. When one drifts under
--heal, a single worker takes a heal lock, repairs that row with the model, and
grafts the repair onto the shared template through the same graftHeals path the
warm-up already used (so {{var}} values survive). Other workers that drifted
against the stale template retry for free against the repair instead of each
spending a model run — whether the repair is still in flight or already landed.
Bounded by --max-heals (default 3): past the budget, drifted rows are reported
exactly as before and the reason is warned once, so a genuinely broken site
cannot turn into one model run per row. RunSummary gains `healed`, surfaced in
the summary line and the --json summary event.
Verified end-to-end against a real Chrome and real model repairs, on a local
fixture that renames its submit button mid-batch:
before (--max-heals 0): 6 rows -> 3 ok, 3 drift, 0 healed
after (--max-heals 3): 6 rows -> 6 ok, 0 drift, 1 healed
12 rows @ concurrency 4: 12 ok, 0 drift, 1 healed
248 tests (+5). Both new concurrency tests were confirmed to fail when the
free-retry check is disabled.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
josharsh
added a commit
that referenced
this pull request
Jul 30, 2026
Documents the mid-run heal feature merged in #26 and bumps pixelpi to 0.4.0 (minor: new user-visible behavior and a new flag, pre-1.0). The core/cdp/ai packages are unchanged at 0.2.0/0.2.2/0.1.0. Adds CHANGELOG.md (Keep a Changelog + semver), backfilled from git history and npm release dates for 0.1.0 through 0.4.0. CONTRIBUTING and the PR template now ask for an Unreleased entry on user-visible changes. README: the run section documents mid-run healing and --max-heals, and the "self-heals" bullet now says what it actually does (fix once at row 900, reuse for the rest) rather than implying it only worked on the warm-up row. packages/agent/README.md is a copy of the root README that npm renders, and it had to be kept in sync by hand. A prepack hook now copies it, so a published tarball can no longer ship a stale README — this release would have. Verified: build, typecheck, 248 tests, and the packed 0.4.0 tarball installed from disk and run end to end against a real Chrome — 12 rows at concurrency 4 over a site that renames its button mid-batch gave 12 ok, 0 drift, 1 heal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
What
run --healnow repairs a mid-batch drift once and lets the rest of the batch reuse the fix, instead of healing only the warm-up row.--heal, one worker takes a heal lock, repairs that row with the model, and grafts the repair onto the shared template via the samegraftHealspath the warm-up already used (so{{var}}values survive the write-back).--max-heals <n>(default 3). Past the budget, drifted rows are reported exactly as before and the reason is warned once, so a genuinely broken site can't become one model run per row.RunSummarygainshealed, surfaced in the summary line and the--jsonsummaryevent.No behavior change without
--heal, and none forreplay, the SDK's.over()(which passesheal: false), orrunwithout the flag.Why
run.tsfanned out every row withheal: false. If a site changed at row 900 of a 5,000-row batch — an A/B test flips a button label, a cookie banner appears, pagination moves — every row from 900 on returneddrift, and each one still paid for a Chrome launch. You'd notice later, runreplay --healby hand, thenrun --resume.That also made the README's claim on the
runsection ("a 5,000-row job costs one model run plus, at most, a handful of repairs") aspirational rather than literal. It's now literal.Self-healing at scale is the load-bearing claim of record → replay → run, and the repair machinery already existed one function away.
Verification
End to end, real Chrome + real model repairs. A local fixture site renames its submit button after N page loads ("Search now" → "Find results"); the recorded trace clicks the old label, so early rows replay clean and later rows hit the renamed button — the exact mid-batch drift an A/B test causes.
--max-heals 0(today's behavior)--max-heals 3--max-heals 3Every row returned its own correct output, and the healed trace kept its
{{query}}template (http://127.0.0.1:8731/?q={{query}}) while the act target moved"Search now"→"Find results".The first 12-row parallel run surfaced a real gap: 2 heals fired where 1 should have. A row that started before a repair and drifted after it landed found the lock already released, so it never noticed the newer template and burned its own heal. Fixed by checking the template generation before deciding to heal, not only after waiting on the lock — re-run gives 1 heal.
Tests: 248 (+5). Five new cases in
run.test.tscovering: one repair for a mid-batch change with later rows passing on their first strict attempt; a single heal lock under 4 concurrent drifting workers; the free-retry regression above;--max-healsexhaustion (budget respected, every row still reported, warning emitted); and no mid-run healing at all without--heal.Both concurrency tests were confirmed non-vacuous — they fail when the free-retry check is disabled.
Checklist
pnpm test(248 passing) andpnpm -r typecheckpass--max-healsis documented in--help; the README'srunsection needs a sentence on mid-run healing (happy to add it here or in a follow-up)🤖 Generated with Claude Code