Skip to content

fix(notifications): re-arm expired Telegram topic leases for owned trusted attachments (#4647) - #4648

Merged
probepark merged 1 commit into
devfrom
fix/issue-4647-telegram-lease
Aug 18, 2026
Merged

fix(notifications): re-arm expired Telegram topic leases for owned trusted attachments (#4647)#4648
probepark merged 1 commit into
devfrom
fix/issue-4647-telegram-lease

Conversation

@Yeachan-Heo

@Yeachan-Heo Yeachan-Heo commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Closes #4647

Reproduction (from source, current origin/dev)

Ordinary interactive Telegram session with Threaded Mode:

  1. Attachment starts with logicalSessionIdTrusted = false; #leaseAllows returns true there, so createForumTopic + identity header send fine.
  2. Router replay flips logicalSessionIdTrusted = true. From then on #leaseAllows requires an authorized recovery lease plus a non-expired topic host lease (leaseExpiresAt > now, TTL = HEARTBEAT_TTL_MS = 20s).
  3. renewActiveTopicLeases() only renewed sessions that already passed #leaseAllows — once the host lease expired, renewal was skipped forever (deadlock).
  4. handleSessionMessage then rejected every later turn_stream/context_update/tool frame pre-send with Telegram publication rejected before send: trusted attachment lease is stale.

Result: topic opens, identity (:1:2) is the only delivered publication, topic frozen. Exactly the reported 0.14.0 incident (daemon gen 169; incident record persisted with authorityState: \"disconnect_grace\").

A discriminating regression suite reproduces this: with the fix reverted, 3 of 6 tests fail (post-TTL publish, heartbeat re-arm, grace resume).

Fix (smallest correct, fail-closed)

Split authority ownership from host-lease TTL in #leaseAllows via an explicit { allowLeaseRearm: true } mode, used at exactly two renewal sites:

  • renewActiveTopicLeases() (ownership heartbeat)
  • once in handleSessionMessage before the pre-send gate, then the gate is re-checked

The mode admits only what TopicRegistry.acquireLease itself admits for a live owner:

  • an expired-but-still-owned active lease (leaseOwner === installationHostId), or
  • a same-owner resume inside the disconnect-grace window (disconnect_grace + unexpired disconnectGraceExpiresAt) — which also covers the incident's persisted record.

Everything else still fails closed: dropped sessions, closed endpoint bindings, unauthorized/pending/rejected recovery leases, exact-logical-owner mismatch (#logicalSessionOwners), foreign leaseOwner, archive_pending/archive_exhausted/inactive/legacy_quarantined states, bindingMalformed, and chat-binding mismatch. After a successful re-arm the ordinary gate re-runs unchanged, so truly unauthorized/stale attachments still get the pre-send rejection. Socket/Router authority is untouched; #renewTopicLeasetopics.acquireLease remains the single mutation path (another unexpired host is still never displaced).

The reporter's suggested mechanism ("renew an owned topic lease even after expiry, then re-check") was correct but incomplete: the incident's durable record sits in disconnect_grace, and #leaseAllows would still reject an owned-but-grace topic even with TTL ignored. The implemented invariant is exactly acquireLease's admission rule (owned-active OR owned-grace-in-window), documented in-code.

Regression coverage (notifications-telegram-topic-lease-renewal.test.ts, 6 cases)

  • identity → TTL expiry → later frame publishes into the same topic (555) with a fresh lease
  • ownership heartbeat re-arms an expired owned lease with no publication in flight
  • foreign host (leaseOwner: \"another-installation\") is never re-armed; pre-send rejection; durable record untouched
  • replaced socket (successor attachment): predecessor fails closed after TTL
  • grace-window record is resumed (back to active, fresh lease) by its still-attached owner
  • archive-fenced topic is never re-armed; stays archive_pending

Validation

  • bun test packages/coding-agent/test/notifications-telegram-topic-lease-renewal.test.ts — 6 pass (3 fail pre-fix)
  • Hermetic focused suites at this exact head: notifications-telegram-topic-lease-renewal.test.ts 6/6 (3 fail with the fix reverted), notifications-topic-registry + notifications-live-stream + session-manager-resident-cache green (89 pass across the four), daemon + fault-containment + baseline-manifest 44 pass
  • Rebased over merged fix(session): stop resume listing paying one read syscall per 4 KiB #4616 (session-manager overlap inspected: no shared hunks; CHANGELOG adjacency resolved preserving both entries)
  • telegram-daemon-generation-guard: v51 required generation bump verified against 27afb732b3...ee1896b8f7 (DAEMON_GENERATION 169→170)
  • bun run --cwd=packages/coding-agent check:types clean; biome check clean on touched files; git diff --check clean
  • verify-gjc-state-writers --fail (0 unsanctioned writes) and changelog-history-guard (no released sections removed) PASS

Binary diff SHA-256

  • base 27afb732b3d25632d44176687d5bdd78d3419bb3
  • head ee1896b8f737c5ff3142f77ca88d1c53bad6f3dd (three-dot binary diff SHA-256 b12f972b2dacf3ebf0c2796f9b29463603db253f1feb2a03d373edde77399fa9)

GJC verdict

gajae.pr-review-verdict.v1 merge-approved sha256:b12f972b2dacf3ebf0c2796f9b29463603db253f1feb2a03d373edde77399fa9 reviewer:human reviewer-id:probepark evidence:exact-head-ee1896b8-independent-maintainer-approval-https://github.com/Yeachan-Heo/gajae-code/pull/4648#pullrequestreview


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

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4647-telegram-lease branch from 06bc1a6 to 2adf6f7 Compare August 18, 2026 00:47
@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Review request — exact head 2adf6f7d79c1de822193e0709128cc0e00f98207 (base 44d7b6ee07, contains immutable event base; exact binary diff SHA-256 aff666c82a9dd82e56d77ed7f70e7eba2a1cf460d18e68372d0db7473c6079dd).

@probepark @snowykr — independent exact-head review requested. This PR closes #4647 (Telegram forum topic opens, identity posts, then every later frame is rejected pre-send with trusted attachment lease is stale once the 20s topic-host lease TTL lapses).

What needs review (one-item change, telegram-daemon.ts + tests + generation manifest):

  • #leaseAllows gains an explicit allowLeaseRearm mode admitting only what TopicRegistry.acquireLease itself admits for a live owner: an expired-but-still-owned active lease, or a same-owner resume inside the disconnect-grace window (the incident record persisted disconnect_grace).
  • Used at exactly two sites: the ownership heartbeat and once before the pre-send gate, after which the ordinary gate re-runs unchanged.
  • Still fails closed: dropped sessions, closed endpoint bindings, unauthorized/pending/rejected recovery leases, exact-logical-owner mismatch, foreign leaseOwner, archive/inactive/quarantined/malformed bindings, chat mismatch.
  • Regression suite notifications-telegram-topic-lease-renewal.test.ts (6 cases, discriminating: 3 fail with the fix reverted — post-TTL publish, heartbeat re-arm, grace resume; foreign-owner/replaced-socket/archive cases stay denied).

Local verification at this exact head: 266 focused Telegram tests pass (lease-renewal, daemon, topic-registry, fault-containment, owner-postmortem, rich-draft, rich-redteam, live-stream, reapers, baseline-manifest, reference, adoption-intent), telegram-daemon-generation-guard v51 bump verified (169→170), coding-agent check:types clean, biome clean on touched files, G002 gate PASS.

Current PR verdict is needs-human (reviewer-id pending) — it will be promoted to merge-approved with this exact digest only after an independent exact-head APPROVED review exists. Contract red on run 32085983168 is the intentional needs-human block, not a product failure.


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

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

CI triage at exact head 2adf6f7d79 (run 32085982782):

  • Every job that executed passed, including Telegram daemon generation guard (v51 bump verified 169→170), Windows Telegram daemon safety, the topic-registry shard, the sdk-production-host shard, and the 1-of-8 coding-agent shard.
  • Three shard jobs (test:notifications-telegram-topic-lease-renewal.test.ts, test:session-manager-resident-cache.test.ts, test:notifications-live-stream.test.ts) were cancelled by a runner infrastructure stall: their Install system deps step sat on azure.archive.ubuntu.com apt Ign from 00:57:56Z until the 90-min job cancel at 02:27:03Z (job 95559767903 log). No test or product code ran in them.
  • Affected path validation then failed fail-closed on required affected shards did not succeed — the designed consequence of the cancelled shards, not an independent failure.
  • PR contract bootstrap red is the intentional needs-human verdict block (independent review pending; see review request above). Not a product failure.

Action: rerun of the failed/cancelled jobs has been triggered. The lease-renewal suite itself is green locally at this head (6/6) and was green in the earlier CI attempt's sibling shards.


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

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4647-telegram-lease branch from 2adf6f7 to ffd3a20 Compare August 18, 2026 02:46
@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Rebased delivery — exact head ffd3a20de398264dc5c25499a49c8dab977e31f4 on live dev 7265a61c8e (contains base; three-dot binary diff SHA-256 ec6bb8bf0b04d1a7305e41a2d2dfcd8fc5bbfedfcb4a94ca751406e55eb2fbc6). PR body verdict and digest updated to this head; earlier 2adf6f7d evidence is superseded (head was based on pre-#4616 dev).

Rebase notes: #4616 touched session-manager.ts (no overlap with this change's telegram-daemon.ts/topic-registry surface; no shared hunks). Only conflict was CHANGELOG [Unreleased] adjacency — resolved preserving both sibling entries. Original authorship and message preserved.

Hermetic validation at this exact head:

  • notifications-telegram-topic-lease-renewal.test.ts 6/6 (discriminating: post-TTL publish, heartbeat re-arm, grace-resume fail without the fix; foreign-owner / replaced-socket / archive-fenced stay denied)
  • topic-registry + live-stream + session-manager-resident-cache: 89 pass across four files; daemon + fault-containment + baseline-manifest: 44 pass
  • telegram-daemon-generation-guard v51: required generation bump verified (169→170) vs 7265a61c...ffd3a20d
  • coding-agent check:types clean, biome clean, git diff --check clean
  • verify-gjc-state-writers --fail: 0 unsanctioned writes; changelog-history-guard: no released sections removed

@probepark @snowykr — fresh exact-head review requested at ffd3a20d. Verdict stays truthful needs-human (reviewer-id:pending) until an independent exact-head APPROVED review exists; only then does it become merge-approved with this digest. Fail-closed invariants unchanged: only the exact still-owned authorized attachment re-arms through topics.acquireLease; foreign lease owner, replaced socket, malformed/quarantined/archived/inactive bindings remain rejected; the ordinary post-renewal gate re-runs; socket/Router authority untouched.


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

@probepark probepark left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Independent maintainer review — merge blocked (production change is correct; the new test violates the repo contract).

minor — test file breaks AGENTS.md conventions

packages/coding-agent/test/notifications-telegram-topic-lease-renewal.test.ts:54-69 (and repeated later) introduces any types and readFileSync/writeFileSync.

AGENTS.md: no any unless absolutely necessary; file I/O goes through Bun.file() / Bun.write(). Define narrow fixture types and switch to the Bun APIs.

otherwise

The renewal deadlock is repaired at its source and the ordinary lease gate is rechecked after mutation. Foreign owners, replaced sockets, grace, and archive fences are all covered, and the expiry/heartbeat/grace tests genuinely fail without the production change.

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Exact-head CI complete at ffd3a20de398264dc5c25499a49c8dab977e31f4 (run 32093149158): all product jobs greennotifications-telegram-topic-lease-renewal.test.ts, session-manager-resident-cache, notifications-live-stream, notifications-topic-registry, sdk-production-host-isolated, coding-agent shard-1-of-8, cli-smoke, Telegram daemon generation guard (v51 bump verified 169→170), Windows Telegram daemon safety, native build, cargo/gjc-sdk build, and all four gjc-state-gates. The single red is the intentional needs-human verdict block in PR contract bootstrap — no product failure.

Status: OWNER_CONFIRMATION_REQUIRED — merge is blocked only on an independent exact-head approval. @probepark @snowykr: a fresh review at ffd3a20d (re-requested) is the one remaining gate; on APPROVED the verdict is promoted to merge-approved with digest ec6bb8bf…b2fbc6, the contract job reruns, and this merges immediately. The delivery itself — fix, 6-case discriminating regression suite, generation manifest, CHANGELOG — is final at this head.

Earlier infra note for the record: the first CI attempt had three shard jobs cancelled by a runner apt-mirror stall (azure.archive.ubuntu.com Ign for ~90 min during Install system deps, no product code ran); all reran green at this head.


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

@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4647-telegram-lease branch from ffd3a20 to 0d02b1d Compare August 18, 2026 04:54
@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Review feedback applied — new exact head 0d02b1deba2ca3053f1b22685157ec0dbaa505ff (three-dot binary diff SHA-256 05f00c7313df68a0916b22c65a0b9940576736a8e6f57c76e47c6c05080dd38f; PR verdict updated).

@probepark's findings addressed in full:

  • any types removed from notifications-telegram-topic-lease-renewal.test.ts: narrow PersistedTopic/PersistedTopics/RecordedCall fixture types now describe the registry records and Bot API bodies the suite asserts over.
  • Sync readFileSync/writeFileSync replaced with Bun.file().json() reads and a mutatePersistedTopics helper writing through Bun.write() (only temp-dir mkdtempSync/rmSync remain, matching the sibling Telegram suites' temp-lifecycle convention).

Re-verified at this head: lease-renewal 6/6 (still discriminating), 130 tests green across lease-renewal + topic-registry + live-stream + daemon + fault-containment + baseline-manifest, telegram-daemon-generation-guard v51 bump verified, coding-agent check:types clean, biome clean, git diff --check clean, fast gate 0 unsanctioned writes. Production change untouched (one-line manifest/contract/gen artifacts identical to the reviewed ffd3a20d).

@probepark @snowykr — fresh exact-head review requested at 0d02b1deb.


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

@Yeachan-Heo
Yeachan-Heo requested a review from probepark August 18, 2026 04:56
…ts expired topic lease

After Router replay flips logicalSessionIdTrusted=true, every later frame
required #leaseAllows, which demands a non-expired topic host lease. But
renewActiveTopicLeases only renewed sessions that already passed that same
gate, so once the 20s HEARTBEAT_TTL_MS lapsed the lease was permanently
unrenewable: identity published, then all later frames died pre-send with
"trusted attachment lease is stale" and the forum topic froze (#4647).

Split authority ownership from lease TTL: #leaseAllows gains an explicit
allowLeaseRearm mode used only at the two renewal sites (ownership heartbeat
and once before the pre-send gate). It admits exactly what acquireLease
admits — an expired-but-still-owned active lease, or a same-owner resume
inside the disconnect-grace window (the incident record was persisted in
disconnect_grace) — and nothing else. Dropped sessions, closed endpoints,
foreign lease owners, archive-fenced/inactive/quarantined topics, malformed
bindings, chat-binding mismatches, and cross-session owner checks all still
fail closed. Daemon generation bumped 169→170 via the generation guard.

Lore-id: 8f2c1d47
Constraint: must not weaken cross-session or recovery ownership fencing
Constraint: socket/Router authority boundaries unchanged
Rejected: dropping the TTL check from #leaseAllows entirely | would admit foreign expired owners
Rejected: re-arm only in the heartbeat | a frame can arrive first after a long idle batch
Confidence: high
Scope-risk: narrow
Reversibility: trivial
Tested: 6-case lease renewal regression suite; 230+ focused Telegram suites green; discriminating (fails 3/6 without the fix)
Not-tested: live Bot API end-to-end (hermetic BotApi contract)
Closes: #4647
@Yeachan-Heo
Yeachan-Heo force-pushed the fix/issue-4647-telegram-lease branch from 0d02b1d to ee1896b Compare August 18, 2026 05:10
@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Dev advanced again (#4585 merged), so the delivery is rebased once more: exact head ee1896b8f737c5ff3142f77ca88d1c53bad6f3dd on base 27afb732b3, three-dot binary diff SHA-256 b12f972b2dacf3ebf0c2796f9b29463603db253f1feb2a03d373edde77399fa9. PR verdict/digest updated. This head carries both the review-fix (no any, Bun file I/O) and the new base; CHANGELOG adjacency resolved preserving the #4603 sibling entry.

Re-verified at ee1896b8f7: lease-renewal 6/6, 130 tests green across the six focused Telegram suites, generation guard v51 bump verified, types/biome/git diff --check clean, fast gate 0 unsanctioned writes.

@probepark @snowykr — review requested at this exact head; verdict remains truthful needs-human until an independent exact-head APPROVED review exists.


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

@Yeachan-Heo

Copy link
Copy Markdown
Owner Author

Replacement CI terminal at exact head ee1896b8f737c5ff3142f77ca88d1c53bad6f3dd (run 32101899116): all product jobs greennotifications-telegram-topic-lease-renewal.test.ts, notifications-topic-registry, notifications-live-stream, sdk-production-host-isolated, coding-agent shard-1-of-8, cli-smoke, root-check, native-build, Telegram daemon generation guard (v51, 169→170), Windows Telegram daemon safety, and all four gjc-state-gate shards + aggregate. The single red remains the intentional needs-human verdict block in PR contract bootstrap — no product failure at this head. (The cancelled run 32101873414's evidence-producer/aggregate reds are fail-fast cancellation propagation, not failures.)

Status: OWNER_CONFIRMATION_REQUIRED@probepark's prior CHANGES_REQUESTED targeted old head ffd3a20d; both requested findings are fixed at this head (any types removed, Bun file I/O) and a fresh exact-head re-review from @probepark (with @snowykr also requested) is the one remaining gate. On APPROVED: verdict → merge-approved with digest b12f972b…399fa9, contract rerun, immediate squash-merge into dev, #4647 closes via the PR body linkage.


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

@probepark probepark left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Independent maintainer review at exact head ee1896b8approved. My prior blocking findings are fixed and the lease logic holds under adversarial reading.

prior findings resolved

packages/coding-agent/test/notifications-telegram-topic-lease-renewal.test.ts no longer contains any, readFileSync, or writeFileSync — zero occurrences at this head.

the re-arm gate is genuinely narrow

Re-arm requires all of: a current trusted attachment, an authorized recovery lease, exact logical-session ownership, matching chat/binding, the same installationHostId, a non-malformed durable record, and either active authority or an unexpired disconnect_grace. Untrusted and unowned attachments cannot reach either re-arm callsite through any branch.

TOCTOU — checked, and it is defended

The attachment predicate is evaluated before the write rather than atomically with it, which is where I expected to find the bug. It survives because the actual mutation is TopicRegistry.acquireLease, which is serialized, rechecks durable authority itself, and CAS-retries against the winning registry generation. Another unexpired host cannot be displaced, and the ordinary publication gate is re-run after the mutation before anything is sent.

Worth stating plainly for the record: installationHostId is host-scoped, so a same-host split-brain is not distinguished by the topic lease at all. Correctness there still rests entirely on the daemon ownership lock and orphan fencing. This PR does not weaken that, but it also does not extend protection to that case — don't let this change be read as covering it.

expiry boundary

Wall-clock Date.now() via NotificationOperatorRuntime.now, not monotonic. Active leases are stale at leaseExpiresAt <= now; same-owner re-arm works both exactly at expiry and after an arbitrarily large delay. Grace re-arm requires disconnectGraceExpiresAt > now, so equality is rejected — one tick stricter than acquireLease itself, which rejects only < now. Stricter-than-the-primitive is the safe direction, so this is fine.

generation manifest is consistent

scripts/telegram-daemon-generation-manifest.json is structurally current for this diff: the three changed protected AST declarations are exactly DAEMON_GENERATION, TelegramNotificationDaemon.#leaseAllows, and handleSessionMessage; renewActiveTopicLeases is not in the protected inventory. Generation 170 and its exact fixture assertion were both updated, which is why the generation guard is green.

nit — ReturnType<> in the new harness

test/notifications-telegram-topic-lease-renewal.test.ts:103 uses ReturnType<typeof fakeBot>. AGENTS.md says never; a named result type shared by fakeBot and LeaseHarness is the intended shape. I am explicitly not blocking on it — the pattern already appears 227 times across packages/*/src and packages/*/test with no mechanical gate, so blocking one new instance would be inconsistent enforcement, not a standard. Fix it while you're in the file.

coverage

Three of the six new behavioral tests fail with the daemon change reverted and genuinely pin the fix: expiry-then-publish (150-178), post-expiry ownership heartbeat extending without publishing (180-198), and in-grace resume to active (250-278). The foreign-host, replaced-attachment, and archive-fence tests pass on base — they are safety guards, not pins, which is fine as long as nobody counts them as evidence the fix works.

Reviewed by @probepark — method: detached worktree at ee1896b8, full read of the changed regions of telegram-daemon.ts and telegram-daemon-contract.ts, ownership-predicate trace from gate to acquireLease CAS, expiry-boundary comparison against the primitive, manifest/protected-inventory cross-check, per-test would-this-fail-on-base analysis. Tests not executed.

gajae.pr-review-verdict.v1 merge-approved sha256:b12f972b2dacf3ebf0c2796f9b29463603db253f1feb2a03d373edde77399fa9 reviewer:human reviewer-id:probepark evidence:exact-head-ee1896b8-independent-maintainer-review-rearm-gate-and-acquirelease-cas-verified

@probepark
probepark merged commit d519c3e into dev Aug 18, 2026
68 of 89 checks passed
pull Bot pushed a commit to nenyatech-mirror/gajae-code that referenced this pull request Aug 18, 2026
…usted attachments (Yeachan-Heo#4648)

An owned topic lease that expired could never be re-armed, so trusted attachments silently stopped publishing until the daemon restarted. Re-arm is now gated on the full attachment predicate and executed through TopicRegistry.acquireLease, whose serialized CAS retry keeps another unexpired host from being displaced.

Closes Yeachan-Heo#4647

Reviewed-by: probepark
Verdict: merge-approved sha256:b12f972b2dacf3ebf0c2796f9b29463603db253f1feb2a03d373edde77399fa9
Confidence: high
Scope-risk: narrow
Tested: expiry-then-publish, post-expiry ownership heartbeat, in-grace resume to active
Not-tested: same-host split-brain (still covered only by the daemon ownership lock)
(cherry picked from commit d519c3e)
pull Bot pushed a commit to nenyatech-mirror/gajae-code that referenced this pull request Aug 18, 2026
…oes not ship

Cherry-picking Yeachan-Heo#4648 hit a CHANGELOG conflict whose union resolution also
pulled in the neighbouring `gjc team` worker-runtime-state bullet from
Yeachan-Heo#4603. That commit is deliberately excluded from the 0.14.1 pick set, so
the bullet advertised a fix the release does not contain.

Lore-id: 4c1f9a02
Constraint: Unreleased bullets must describe exactly the approved pick set
Confidence: high
Scope-risk: narrow
Reversibility: trivial
Tested: re-audited every Unreleased bullet against branch commit provenance
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