fix(telegram): release the topic-id quarantine once a delete settles - #3610
fix(telegram): release the topic-id quarantine once a delete settles#3610twoimo wants to merge 1 commit into
Conversation
`TopicRegistry.settleDelete()` removed the `delete_pending` record but never recomputed the derived routing tables (`byTopic`, ambiguous topic ids), so the quarantine that the fenced record installed outlived the record itself. The topic id then stayed unroutable and unadoptable for the life of the daemon process, even though the load path rebuilds those tables and would have released it on the next restart. Reachable on dev: a crash mid-close leaves `delete_pending` on disk; `loadTopics()` quarantines the id; `reconcilePendingTopicDeletes()` settles a `user_created` record locally with no remote delete; the user's topic still exists in Telegram and the next adoption of it is refused. Recompute the routes after the delete, matching what `restoreDeleteAuthority()` already does. Records that remain `delete_pending` are re-added by the rebuild, so the deliberate fail-closed fence is unchanged.
8176c12 to
257e3ef
Compare
|
Just a small note for whenever you have time — no rush at all. This one is now rebased onto To make review as cheap as possible, the whole functional change is a single line: It follows the pattern already established two methods away in the same file: I also want to be explicit about the two things I deliberately did not do, in case either is the wrong call:
The fail-closed fence itself is unchanged: records still in Thank you for the thorough reviews on the earlier PRs — the pane-id narrowing and dispatch-fence feedback in particular made that change materially better. |
Yeachan-Heo
left a comment
There was a problem hiding this comment.
Exact-head hostile review: REQUEST_CHANGES
Reviewed head: 257e3eff5139fbd9141f239a98aca6c36c3064aa
Exact PR base: 0433be445278113a0b5ca6ab24983a486bf1df71
Current dev checked: 29ddfe08f51039b1bf4e8e373ba06c68d7005d44
P0: 0. P1: 2.
-
Settlement is not fence-epoch bound.
TopicRegistry.settleDelete(sessionId, topicId)checks only topic ID plusdelete_pending. A held E1 delete can race a scan/close-started E2 delete for the same session/topic; E1's definite result can delete E2 and release the newer quarantine. Capture the dispatch epoch and require record/current fence equality at settlement; update all four daemon call sites and add an out-of-order E1/E2 regression. -
Rollback does not rebuild derived routes. The new settlement rebuild can make a surviving colliding record routable. If the final topic-state persist fails,
restoreDeleteFencereinstates the pending collision but does not rebuildbyTopic/ambiguity, leaving inbound routing open despite the restored fence. Rebuild on successful fence restoration (or make settle/rollback route-atomic) and add a persist-failure active+pending collision regression.
The one-line happy-path repair is valid but incomplete under the required generation/owner and rollback races. Exact-head CI is terminal green (16 success / 6 skipped), but it does not cover either blocker. Current-dev overlap is only the Unreleased changelog insertion; #3596 overlap is also changelog-only, and issue #3587 remains separate daemon process/poller lifecycle ownership.
—
[repo owner's gaebal-gajae (clawdbot) 🦞]
Hello, and thank you for the review time on this one.
This is a single-line behavioural fix to
TopicRegistry.settleDelete(), carved out of a larger Telegram topic-authority change so it can be judged on its own. It deliberately avoids every file currently owned by #3596.The defect
A
delete_pendingrecord quarantines its topic id so routing fails closed.settleDelete()removes that record once a remote deletion is definite — but it never recomputes the derived routing tables (byTopic, the ambiguous-topic-id set). The quarantine therefore outlives the record that installed it, and the topic id stays unroutable and unadoptable for the remaining life of the daemon process.The load path already rebuilds those tables, so a restart releases the id. That divergence is what makes the in-memory state unambiguously stale rather than a second valid interpretation:
load()is the authoritative definition of routing, andsettleDelete()was leaving memory disagreeing with it.Why it is reachable on current dev
authorityState: "delete_pending"on disk.TelegramDaemon.loadTopics()→TopicRegistry.load()→rebuildInboundRoutes()adds the topic id to the ambiguous set.reconcilePendingTopicDeletes()→deleteTopic()settles atopicOrigin === "user_created"record locally, with no remote delete call.topic adoption refused: intent chat/binding/topic unavailable, and a session that had collided on that id stops receiving inbound replies until the daemon is restarted.settleDeleteis also reached from three other daemon call sites, so the stale quarantine is not confined to the reconcile path.The change
One functional line —
this.rebuildInboundRoutes()after the record is removed — plus an expanded doc comment explaining why. This matches the existing pattern in the same file, whererestoreDeleteAuthority()already rebuilds the routes after mutating authority state.The fail-closed fence is unchanged: records that are still
delete_pendingare re-added to the quarantine by the rebuild, exactly as the load path does. No new fields, no new API, no widened surface.TopicRegistry.delete()has the same stale-set shape, but it is documented as local/test cleanup and has no daemon caller, so I left it alone rather than change an unreachable path.Verification
Both tests were confirmed to fail against byte-exact dev source and pass with the fix (source reverted with
git checkout upstream/dev --, re-run, restored, re-run):Suites run on this exact head:
notifications-topic-registry-settle-quarantine.test.ts+notifications-topic-registry.test.ts+notifications-telegram-daemon.test.ts— 567 pass / 0 failnotifications-telegram-daemon-2960.test.ts,-2960-redteam.test.ts,-self-heal.test.ts— 15 pass / 0 failtsc -p packages/coding-agent/tsconfig.json --noEmit— cleanbiome check— cleanThe new tests use only the exported surface (
TopicRegistry,sessionForTopic,isTopicIdAvailable,settleDelete,get,getOrCreateTopic,endpointAuthority,serialize); no private access and noas any. The second test pins live routing against post-reload routing, so this specific divergence cannot silently return.Ownership
Touches
topic-registry.tsand one new test file only. Neither appears in #3596's file set, and I have kept clear offile-lock.ts,session/**, the natives crate,telegram-daemon-contract.ts, and the generation manifest.Happy to adjust anything here, and thank you again for the careful reviews — they have been genuinely useful in narrowing this down to a provable defect.