Skip to content

Stop waiting for replication confirmation on writes that replicate nothing (audit: false, localOnly, copy-apply) - #3423

Merged
kriszyp merged 12 commits into
mainfrom
fix/appendedto-only-when-audit-appended
Oct 27, 2026
Merged

kriszyp merged 12 commits into
mainfrom
fix/appendedto-only-when-audit-appended

Conversation

@kriszyp

@kriszyp kriszyp commented Oct 21, 2026 •

Copy link
Copy Markdown
Member
An engineer mistakes a stay-home envelope for a broadcast

⊙ Problem

With replicatedConfirmation requested, unaudited, local-only and RocksDB copy-apply writes can wait until timeout (504) despite sending nothing to peers. This affects put, create, patch, invalidate, relocate, and tracked deletes on both engines.

🤔 Should these writes succeed rather than refuse the confirmation?

Yes: nothing about them replicates, so there is nothing to confirm. A 400 would break callers that set one count for every write. The catch: a successful confirmation no longer always means a peer acknowledged the write.

💡 Solution

Unaudited, local-only and copy-apply writes now resolve without peer checks. Audited writes still wait, including mixed commits. The encoder records whether staging appended a replicable audit entry; confirmation uses that result on both engines, including the LMDB position selector from #3392.

🤔 Should a write that lost to an unreplicated entry stop waiting too?

Not here. A skipped write, or a RocksDB validation no-op, appended nothing and confirms on the entry that beat it. When that entry is itself unaudited or local-only, the wait still times out. Fixing it means judging the survivor's replicability, a separate rule: #3387.

⚖️ Alternatives

  • Set skipReplicationConfirmation in each commit handler — the decision would be repeated in seven handlers. The two earlier fixes of this kind each missed sibling paths.
  • Count appends in the RocksDB log store — covers only one engine; LMDB's audit store has no wrapper to count in.
  • Count appends in a module counter sampled around the handler — a nested write staged by a custom index callback would be counted against the outer write.

🤔 Should replicate: false tables be exempt too?

Not here. harper-pro's sender skips them by policy core cannot see; its confirmationFeasibility hook should exempt or refuse them (#3399).

🤔 What about a commit handler that appends without reporting its write?

It would skip confirmation. None exists: all seven managed handlers pass their write, and the two direct appenders are LOCAL_ONLY or opted out; DESIGN.md states the rule. A typed handler return would enforce it, but changes every handler's signature for no current case.

🔧 Changes

✅ Verification

  • New unreplicatedWriteConfirmation.test.js runs on both engines; with reloadMarker.test.js: 32 passing on RocksDB, 27 passing + 5 engine skips on LMDB. On the old base, all 12 non-replicating cases hang on both engines; reverting only the LMDB selector filter fails the local-only put case. reloadMarker now asserts confirmation with an audited put.
  • test:unit:resources:core at 37eaf6a's code: RocksDB 4135 passing, 0 failing; LMDB 3227 passing, 2 failing in replicationDirectiveAuthorization.test.js (setReplicationConfirmation is not a function). Main fails the same 2 since Make replicatedConfirmation wait for peers on LMDB #3392 removed that LMDB export.
  • Build and Prettier pass. test:unit:main and integration not rerun locally; CI covers them.
  • PR QA: not queued @ 37eaf6a — rebase maintenance on a draft; queue at Ready.
  • Docs: needed at Ready — replicatedConfirmation resolves without peer acknowledgement for writes that replicate nothing.
Test cases
  • Positive controls hold a confirmation promise open and check that the commit has not settled until it is released. The retry case fails without its guard.
  • does not wait: unaudited put / create / patch / invalidate / relocate; unaudited delete on a delete-tracking table; local-only put / patch / invalidate / relocate / delete / publish; copy-apply row (RocksDB); an ERR_BUSY recommit after enableAuditing(), which appends nothing (RocksDB)
  • still waits: audited put / patch / invalidate / relocate / delete; a mixed commit with an unaudited write and with a local-only write, each in both orders; publish on an unaudited table (always audited); a write restaged after a runtime enableAuditing()
  • residency naming only this node (RocksDB): unaudited and local-only puts are not refused; an audited put still is

🤔 Pin the entry-less fallback branch directly?

No. The fallback reads getEntry(key)?.version, so a write with no stored entry is passed over. Only a skipped write on an already-missing key reaches it; that fixture costs more than the one optional chain it would cover.

Refs replicatedConfirmation of a write that lost to an unreplicated entry waits until timeout (the skipped-write and validation no-op cases) and replicatedConfirmation on a table the sender never replicates waits until timeout (replicate: false)

🤖 Generated by Claude Code and OpenAI Codex; posted via @kriszyp.

Release target: main — fixes confirmation behavior on main; no customer report asks for a backport
Related PRs: #3392 overlaps (merged; this PR is rebased onto it and filters its LMDB selector with confirmsNothing), #3411 overlaps (merged skipped-write fallback), #3429 overlaps (merged prose pruning reused here), #3430 overlaps (prunes the same guide), #3506 overlaps (open; new _writeUpdate early returns must pass their write or mark skipped), #3527 overlaps (open; new commit handlers must pass their write), 7 others independent
Complexity: complicated

Deferred findings

Dispatch: task harper-appendedto-only-when-audit-appended · queued by unknown · ran by claude/opus/high · worker kzyp-xps-1

Late Grok findings

7 surviving (highest major) — Grok reviewed efae636
  • major — resources/DatabaseTransaction.ts:2307 — A no-change patch on an unaudited RocksDB table can still request confirmation of an unreplicated stored version
  • major — resources/RecordEncoder.ts:1166 — An audited replicate:false write remains confirmable; if the sender excludes its table without advancing acknowledgements, confirmation times out
  • major — resources/DatabaseTransaction.ts:1981 — LMDB put(A) followed by skipped B can confirm B’s stale position before peers receive A
  • major — resources/DatabaseTransaction.ts:2371 — A losing write over an unaudited or local-only survivor regains confirmation eligibility and can wait on unreplicated data
  • nit — unitTests/resources/reloadMarker.test.js:176 — The trailing unaudited delete is filtered before entry lookup, leaving entry-less fallback regressions undetected
  • nit — resources/DatabaseTransaction.ts:764 — Successful confirmation without peer acknowledgement deserves a release-facing caveat for unaudited writes
  • nit — resources/DatabaseTransaction.ts:702 — Trim staging narration and overlapping explanation, including resources/DatabaseTransaction.ts:1970-1976 and unitTests/resources/reloadMarker.test.js:167-168

Not standing: none. Grok ran 20m (20m from launch to report); the fold adjudication (cursor-grok@efae6363e6fe-361a26b6, codex) took 4m.

Late Grok findings

7 surviving (highest major) — Grok reviewed a30a2cd
  • major — resources/DatabaseTransaction.ts:2321 — A non-exempt handler appending without reporting its write bypasses confirmation; the named built-in control writers explicitly opt out
  • major — resources/DatabaseTransaction.ts:2295 — An empty patch on an unaudited RocksDB row remains confirmation-eligible and can wait on an unreplicated version
  • major — resources/DatabaseTransaction.ts:2359 — A skipped write over an unreplicated surviving row regains eligibility and can wait for acknowledgement that cannot arrive
  • major — resources/RecordEncoder.ts:1193 — An audited replicate:false write remains eligible; if the sender excludes that table, confirmation can time out
  • minor — resources/DESIGN.md:967 — Unaudited, local-only and copy-apply writes now succeed without requested peer acknowledgements; the supplied explanation is internal
  • nit — unitTests/resources/reloadMarker.test.js:174 — The mixed-write test bypasses the entry-less fallback, leaving that branch unpinned
  • nit — resources/DatabaseTransaction.ts:707 — Added staging comments repeat implementation details and overstate the runtime-auditing rationale

Not standing: none. Grok ran 17m (17m from launch to report); the fold adjudication (cursor-grok@a30a2cd9f38b-454dca39, codex) took 5m.

Review-Coverage: authored=codex; ran=cursor-composer,gemini,claude; adjudicated=domain; declined=cursor-grok,muse; rounds=12; full=4 @ 37eaf6a

Review-Attention: study ~20m (critical: DatabaseTransaction.ts, LMDBTransaction.ts +2; decisions: do-less-alternative, confirmation-without-replicable-work) @ 37eaf6a

@kriszyp kriszyp added this to the v5.4 milestone Oct 21, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors replication confirmation logic to avoid waiting on writes that append nothing to replicate, such as unaudited, local-only, or copy-apply operations. It introduces a stagedAudit property on TransactionWrite and a helper function confirmsNothing to determine eligibility, alongside updating transaction handling, table staging, and adding comprehensive unit tests. One review comment suggests ensuring robust null-entry guards when iterating over DatabaseTransaction.writes to prevent potential runtime TypeErrors.

Comment thread resources/DatabaseTransaction.ts Resolved
kriszyp and others added 11 commits October 26, 2026 06:17
…entry

An unaudited put/create/patch/invalidate/relocate, an unaudited delete on a
delete-tracking table, and a localOnly write append nothing a peer will
receive, yet staging stamped them appendedTo and left them confirmation-
eligible, so a replicatedConfirmation commit waited on a log key nothing
replicated until it timed out (504), on both engines.

recordUpdater, which alone decides whether an audit entry is appended and
whether it is LOCAL_ONLY, now records that on the owning write (passed as
its `write` option). Each fresh staging clears and re-derives it (RocksDB
save(), LMDB doWrite), stamping appendedTo only on an actual append and
excluding a write that replicates nothing from feasibility, target and
residency checks. The early residency refusal applies the same rule, and the
NoAudit-delete call-site flag is no longer needed.

Dispatch-Task: harper-appendedto-only-when-audit-appended
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ri2uUp4SAKFrCCL8nyxYpr
…staged

A commit handler may return a promise and append after an awaited dependency;
deriving replicatesNothing when the handler returned would then skip
confirmation for a write that did append. The fresh staging now resets
stagedAudit to 'none' and the confirmation checks read it directly.
recordUpdater notes the append after the audit put accepts it, and the test
waits for the confirmation hook instead of a fixed delay.

Dispatch-Task: harper-appendedto-only-when-audit-appended
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ri2uUp4SAKFrCCL8nyxYpr
…mment

Dispatch-Task: harper-appendedto-only-when-audit-appended
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ri2uUp4SAKFrCCL8nyxYpr
… a coordinated retry

A coordinated retry's put appends nothing, so it no longer reports an append,
and a fresh staging that was skipped clears stagedAudit instead of the check
reading skipped live, which a retry round can flip.

Dispatch-Task: harper-appendedto-only-when-audit-appended
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ri2uUp4SAKFrCCL8nyxYpr
Dispatch-Task: harper-appendedto-only-when-audit-appended
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ri2uUp4SAKFrCCL8nyxYpr
…ited put is not confirmed

The reload-marker test confirmed a put on an audit: false table, which
replicates nothing (on LMDB it confirmed undefined). Its point is that a
trailing write that replicates nothing does not suppress confirmation for a
replicable write staged before it, so the put now goes to the audited table.

Dispatch-Task: harper-appendedto-only-when-audit-appended
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ri2uUp4SAKFrCCL8nyxYpr
resources/DESIGN.md is already over its 1000-line budget on main; fold the
eligibility paragraph into the preceding one so this change adds no lines.

Dispatch-Task: pr-maint-355475e820ee481125db34713182ae84
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Use the shared staging predicate before LMDB reads candidate positions. Exercise the production confirmation registry and context count, including mixed local-only commits in both orders.

Dispatch-Task: pr-maint-355475e820ee481125db34713182ae84

Co-Authored-By: GPT-5 Codex <noreply@openai.com>
Dispatch-Task: pr-maint-355475e820ee481125db34713182ae84

Co-Authored-By: GPT-5 Codex <noreply@openai.com>
…tests

Dispatch-Task: pr-maint-355475e820ee481125db34713182ae84

Co-Authored-By: GPT-5 Codex <noreply@openai.com>
…ertion message

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dispatch-Task: pr-maint-355475e820ee481125db34713182ae84
@kriszyp
kriszyp marked this pull request as ready for review October 27, 2026 01:46
@kriszyp
kriszyp merged commit 579f9a7 into main Oct 27, 2026
80 of 85 checks passed
@kriszyp
kriszyp deleted the fix/appendedto-only-when-audit-appended branch October 27, 2026 01:46
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.

1 participant