Skip to content

fix(telegram): fence delete settlement to its dispatched epoch and keep rollback route-atomic - #3617

Closed
twoimo wants to merge 1 commit into
Yeachan-Heo:devfrom
twoimo:fix/telegram-settle-fence-epoch
Closed

fix(telegram): fence delete settlement to its dispatched epoch and keep rollback route-atomic#3617
twoimo wants to merge 1 commit into
Yeachan-Heo:devfrom
twoimo:fix/telegram-settle-fence-epoch

Conversation

@twoimo

@twoimo twoimo commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Successor to #3610, addressing both P1 findings from that review. Thank you for the precise diagnosis — both races were real, and the one-line happy-path repair was indeed incomplete.

P1-1 — settlement is now fence-epoch bound

settleDelete(sessionId, topicId) checked only topic id plus delete_pending, so a held E1 delete could settle a newer E2 fence for the same session/topic and release the newer quarantine.

settleDelete now takes dispatchedAuthorityEpoch — the epoch the caller held when it dispatched the remote delete — and requires equality against both the record's authorityEpoch and the registry's current authorityEpoch(sessionId). The second comparison is what actually closes the race: beginDelete bumps epochs and the record together, so a superseded E1 holder fails the registry-current check.

All four daemon call sites capture the epoch before dispatch:

Site Location Epoch source
A revoked-lease compensation in the getOrCreateTopic create callback fenceAcceptedCreateForLease's returned record
B catch-path compensation after a failed initial commit same, from its own fenced record
C deleteTopic, user_created topic (no remote dispatch) shared capture after beginDelete
D deleteTopic, after deleteForumTopic returns definite same shared capture

P1-2 — rollback is now route-atomic

restoreDeleteFence reinstated the pending collision but used a narrow byTopic.delete(...) that only ran in the record-present branch — never in the absent-record branch, which is exactly the branch settlement creates. It also never re-added the id to the ambiguity set, so inbound routing stayed open despite the restored fence.

It now calls rebuildInboundRoutes() after the epoch write, matching the pattern restoreDeleteAuthority already used.

Verification

Three regressions in test/notifications-topic-settle-fence-epoch.test.ts, each proven to fail without its specific repair:

  • Base fix — quarantine released on a settled delete.
  • P1-1 — out-of-order E1/E2: with only the two guard lines removed, expect(reg.settleDelete("A", "42", dispatchedEpochE1)).toBe(false) receives true.
  • P1-2 — persist-failure active+pending collision: with only restoreDeleteFence reverted to the narrow form, expect(reg.sessionForTopic("42")).toBeUndefined() receives "B" — verbatim the open inbound route you described.

All three fail on unmodified dev and pass with the fix. Wider sweep: 568 pass / 0 fail across the topic-registry, new, and main telegram daemon suites; 704 pass / 0 fail across all 15 telegram/topic files; generation guard 41 pass; tsc and biome clean.

Rebased onto dev 7f5167c86. Tests use only the exported TopicRegistry surface — no private access, no casts. Happy to keep iterating if either fence needs to be stricter.

…ep rollback route-atomic

settleDelete() previously matched only topic id plus delete_pending, so a
held generation's definite remote result could settle a newer re-fenced
delete for the same session and release its topic-id quarantine. It now
requires the caller's dispatched authority epoch to equal both the
record's epoch and the registry's current epoch, and all four daemon call
sites capture that epoch before dispatching the remote delete.

restoreDeleteFence() reinstated a pending collision without rebuilding the
derived routing tables, and its narrow byTopic.delete never ran on the
absent-record branch that settlement creates. It now rebuilds inbound
routes after writing the epoch, so a restored fence re-quarantines the
topic id instead of leaving inbound routing open to the collision partner.

Settlement also rebuilds routes on success so a settled topic id becomes
adoptable and a surviving colliding record becomes routable immediately.
@twoimo

twoimo commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current dev 7f5167c86; exact-head CI is 14 success / 0 failure / 5 skipped.

This is the successor to #3610 and implements both P1s from your review exactly as specified, rather than re-proposing the one-line happy-path change:

P1-1 — settleDelete now takes dispatchedAuthorityEpoch and requires equality against both record.authorityEpoch and the registry-current authorityEpoch(sessionId). The second comparison is what actually closes the race you described, since beginDelete bumps epochs and the record together. All four daemon call sites capture the epoch before their remote delete is dispatched.

P1-2 — restoreDeleteFence now calls rebuildInboundRoutes() after the epoch write, replacing the narrow byTopic.delete. That narrow form only ran in the record-present branch and never in the absent-record restore branch, which is precisely the branch settlement creates, and it never re-added the id to the ambiguity set.

Both regressions you asked for are included — out-of-order E1/E2 settlement, and persist-failure active+pending collision. I verified each fails for its own blocker by reverting only that guard, not just with everything stashed, so neither passes for the wrong reason.

Ready for review at your convenience.

@Yeachan-Heo Yeachan-Heo left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Exact-head hostile review: REQUEST_CHANGES

Reviewed head: 2596b98b28df8177fda3eab3d6f092d1f47c285d
Exact/current base: 7f5167c864f938558840084ad218d48132f0a67a

P0: 0. P1: 4.

  1. Epoch advancement can exceed Number.MAX_SAFE_INTEGER; E1/E2 then collapse to the same IEEE-754 value and stale settlement can clear the newer fence. Use checked advancement/saturation and reject unsafe dispatched epochs.
  2. restoreDeleteFence can overwrite a newer E2 with an old E1 snapshot after stale E1 settlement/persist failure. Rollback must CAS the exact expected post-settlement state, and refused settlement must not restore.
  3. Settlement rebuilds/routes before the final clear is durable. During a held write, a colliding survivor becomes routable; a later persistence failure re-quarantines too late. Keep a transient clear-pending quarantine or publish the route only after durable commit.
  4. The first revoked-lease compensation path marks remote compensation complete before registry clear persistence. If that write fails, outer recovery skips fence supervision, leaving live memory cleared while disk retains delete_pending. Treat remote success + settle + durable clear transactionally.

Exact CI is terminal green (17 success / 5 skipped / 0 bad), but lacks overflow, stale rollback CAS, held-write inbound, and first-compensation persistence failure coverage. Normal E1/E2 checks and route rebuild-on-restore are improved, but these remaining authority/durability windows are material.


[repo owner's gaebal-gajae (clawdbot) 🦞]

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.

2 participants