fix(notifications): clear the disconnect-grace deadline when a topic is archived - #3910
Conversation
|
Exact-head evidence for review — head
Self-approval is BLOCK per the PR template, so this needs an independent architect/critic/human verdict. Flagging priority: the failure this fixes is permanent once persisted — the daemon can no longer parse its own registry, so every subsequent |
a365515 to
ebba2ca
Compare
|
Rebased onto current New exact head
Still needs an independent architect/critic/human verdict (self-approval is BLOCK). |
…is archived `parseTopicRegistryState` rejects a record that carries `disconnectGraceExpiresAt` in any state other than `disconnect_grace`, but only the grace-to-active transitions deleted it. Every archive transition (`beginArchive`, `restoreArchiveFence`, `scheduleArchiveRetry`, the exhausted-to-pending retry, `settleArchive`, and the restore path that retires a rebound topic) left it in place, so the daemon published a snapshot its own parser refuses. The failure is permanent, not transient: after the poisoned publish, every `loadTopics` and every `compareAndSet` throws `shared topic authority unavailable`, which on a shared-authority daemon exits the process. On a live installation three consecutive daemons died that way within three hours, and the orphaned topic stayed in `archive_pending` for hours with no owner to archive it or answer in it. Both halves are needed: transitions out of grace now go through one setter that drops the deadline, and a registry that already carries one loads with it normalized away instead of failing every publish forever. The reverse inconsistency — a `disconnect_grace` record missing its own deadline or orphan observation — is genuinely ambiguous and stays fatal. Lore-id: 7e41c0b8 Constraint: a settled authority state must round-trip through its own parser Rejected: relax the parser for both directions | a grace record without a deadline is ambiguous and must stay fatal Rejected: repair on write only | already-poisoned installations would never start a daemon again Confidence: high Scope-risk: narrow Reversibility: easy Tested: grace to archive_pending to inactive round-trips through parseTopicRegistryState; the real poisoned on-disk record loads normalized; a grace record without its deadline still throws
ebba2ca to
a62cd9a
Compare
|
Closing this contribution under maintainer direction. Do not open further PRs or issues in this repository without explicit maintainer approval. — |
What
Every transition out of
disconnect_gracenow clears the record'sdisconnectGraceExpiresAt, and a registry that already carries one loads with it normalized away instead of failing every publish.Why
parseTopicRegistryStaterejects a topic record that carriesdisconnectGraceExpiresAtin any state other thandisconnect_grace:Only the grace-to-active transitions deleted it (
acquireLease,clearOrphaned, and the endpoint-restore path). Every archive transition left it:beginArchive,restoreArchiveFence,scheduleArchiveRetry, the exhausted-to-pending retry insidearchivePendingSessionIds,settleArchive, and the restore path that retires a rebound topic. So the moment an orphaned topic began archiving, the daemon published a snapshot its own parser refuses.The resulting failure is permanent, not a transient blip. After that publish,
loadTopicsand everycompareAndSetthrow,persistTopicsconverts it toshared topic authority unavailable, and on a shared-authority daemon that rejection exits the process.Observed on a live installation, not inferred.
gjc-crash.logrecorded three consecutive daemons dying on the same line within three hours:Running
parseTopicRegistryStateagainst that installation'stelegram-topics.jsonthrowsmalformed Telegram topic state, and bisecting the record isolates one field:The record is
authorityState: "archive_pending"and still holdsdisconnectGraceExpiresAt: 1785971807162. The topic sat inarchive_pendingfor hours with no owner to archive it or answer in it — an empty Telegram thread that never goes away.#3907 stopped that rejection from killing the daemon. This is the reason the rejection happened at all; the two are independent and both are needed. Without this fix a #3907 daemon survives but still cannot ever publish registry state.
Why the parser is relaxed in only one direction. A
disconnect_gracerecord missing its own deadline or orphan observation is genuinely ambiguous about how long the grace runs, so that stays fatal. The reverse is not ambiguous: the authority state is explicit and the stale deadline is inert. Keeping it fatal permanently bricks every installation that already published one, since the daemon cannot load, cannot repair, and cannot publish. Normalizing on load un-bricks those installations and preserves the round-trip invariantserialize -> parsethat compare-and-set depends on.Testing
Three new tests in
notifications-topic-registry.test.ts, verified failing ondev(the first two) and passing here:grace -> archive_pending -> inactiveround-trips throughparseTopicRegistryStateat each step, and the deadline is gone from both settled snapshots.compareAndSetvalidates.disconnect_gracerecord without its own deadline still throwsmalformed Telegram topic state.Other verification:
bun test packages/coding-agent/test/notifications-topic-registry.test.ts packages/coding-agent/test/notifications-telegram-daemon.test.ts→ 611 pass, 0 fail.new TopicRegistry(parsed).serialize()re-parses, and the offending field isundefinedon the settled record.bun scripts/telegram-daemon-generation-guard.ts→v43 no protected changes(noDAEMON_GENERATIONbump owed; the wire contract is unchanged).tsc --noEmit -p packages/coding-agent/tsconfig.jsonclean;biome checkclean on both touched files.GJC verdict
devbun checkpasses (fullbun checknot run locally; focused suites, typecheck, biome, and the daemon generation guard are green — CI covers the rest)