Skip to content

🪲 Answer from a finished holder's publication when elected late - #665

Merged
taras merged 1 commit into
mainfrom
fix-am19-recovery-race
Aug 30, 2026
Merged

taras merged 1 commit into
mainfrom
fix-am19-recovery-race

Conversation

@taras

@taras taras commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Why

AM19 failed intermittently under Bun on main at 879c4ff (run 33318388159, attempt 1), opening #663. The identical revision passed on re-run, and the commit touched nothing under packages/acp, so this is the flake tracked by #664 — but the cause is a real ordering gap in the recovery election, not a defective assertion.

What changes

Before:

A recovery attempt waiting behind a live ticket polled the published tree only between election passes, while a holder marked its ticket done only after publishing. When the publication and the release both landed between one poll and the next election pass, the waiter was elected and entered the recovery cleanup a second time. The in-lock re-check removed nothing, so durable state stayed correct — but beforeRecoveryCleanup observed a second entry, violating the module's contract that an attempt which did not do the recovery answers from what the holder published.

After:

An elected attempt checks the published tree before entering the cleanup. Ticket order guarantees a finished holder's publication is visible by election time — done is written only after the rename that publishes — so being elected behind a finished holder means the answer is already on disk, and the attempt settles on it without entering the cleanup.

How it works

recover() election pass → "held" → published(snapshot)? → settled, no cleanup
                                  → not published       → cleanup as before

What must stay true

  • Exactly one attempt performs any given recovery — enforced by the ticket election plus the new pre-cleanup published check, and checked by AM19 (statistical) and AM20 (deterministic).
  • A recovery never deletes a tree that appeared after its decision — the in-lock re-check after beforeRecoveryCleanup is unchanged, and AM8 still proves it.
  • A waiter behind a live or foreign ticket still never mutates anything — AM8b, AM13, AM14, AM17 unchanged and passing.

How to verify it

  • AM20 plants a live ticket ahead of the attempt, then — through the new beforeRecoveryElection observer — publishes a donor tree and finishes that ticket in the exact window after the attempt's last published poll and before its next election pass. It proves the elected attempt answers from the publication (cleanups stays 0, donor bytes intact) and fails with cleanups = 1 when the pre-cleanup check is removed (mutation-checked locally).
  • The full packages/acp/tests/adapter-materialization.test.ts file (22 tests) passes under all three runtimes at a744280:
    • deno task test packages/acp/tests/adapter-materialization.test.ts
    • bun test --timeout=300000 packages/acp/tests/adapter-materialization.test.ts
    • pnpm exec tsx --tsconfig tsconfig.node.json --test packages/acp/tests/adapter-materialization.test.ts
  • deno task lint and deno task check pass.

Scope

Included

  • The pre-cleanup published check in recover().
  • The beforeRecoveryElection test observer and AM20.

Intentionally unchanged

  • The in-lock published re-check inside the cleanup: publication can still appear after election (a rename can replace an empty target directory), so the pre-check does not subsume it.
  • The election itself, ticket issuance, and dead-holder finishing.

New abstractions

  • EmbeddedAdapterObservers.beforeRecoveryElection exists because the racing window cannot otherwise be produced deterministically — the same reason beforeRecoveryCleanup exists, and AM20 is its consumer.
  • Each new abstraction has multiple concrete uses or a clear justification.
  • No speculative functionality is included.

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

Closes #664

https://claude.ai/code/session_015PmPc8yC6AJKQiu9F9d2ru

A recovery attempt waiting behind a live ticket polls the published tree
only between election passes, while a holder marks its ticket done only
after publishing. When the publication and the release both landed in
the window between one poll and the next election pass, the waiter was
elected and entered the recovery cleanup a second time — harmless to
durable state, because the in-lock re-check removed nothing, but a
violation of the contract that an attempt which did not do the recovery
answers from what the holder published, and the intermittent failure
AM19 caught under Bun (#664).

An elected attempt now checks the published tree before entering the
cleanup: ticket order guarantees a finished holder's publication is
visible by election time, so being elected behind one means the answer
is already on disk. AM20 produces the exact window deterministically
through a new election-pass observer and fails without the fix.

Closes #664

Claude-Session: https://claude.ai/code/session_015PmPc8yC6AJKQiu9F9d2ru
@github-actions

Copy link
Copy Markdown

PR #665: 🪲 Answer from a finished holder's publication when elected late

2 files, +67 / -1

Scope

✅ PR scope looks good.

Structural

Oxlint structural signals:

  • no-empty-function ×2: packages/acp/src/adapter-snapshots.ts

Slop

✅ Slop indicators look low.

Static Analysis

Oxlint: 5 diagnostics across 1 file (3 rules)
Density: 0.075 violations/added-line

no-array-sort (2): packages/acp/src/adapter-snapshots.ts
no-empty-function (2): packages/acp/src/adapter-snapshots.ts
no-unnecessary-template-expression (1): packages/acp/src/adapter-snapshots.ts

Correctness

adapter-snapshots.ts, signal cluster, multiple Oxlint rules fired, Are the lint violations in this file clustered due to generated or unreviewed code?
adapter-snapshots.ts, speculative abstraction, fewer than 3 consumers, Is beforeRecoveryElection justified as a first-use abstraction?

@taras
taras merged commit 5375c29 into main Aug 30, 2026
30 checks passed
@taras
taras deleted the fix-am19-recovery-race branch August 30, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🪲 AM19 races a holder's release: a second attempt is elected and enters cleanup after publication

1 participant