Skip to content

fix(telegram): reap abandoned publication staging temps - #3614

Closed
twoimo wants to merge 1 commit into
Yeachan-Heo:devfrom
twoimo:fix/telegram-staging-temp-leak
Closed

fix(telegram): reap abandoned publication staging temps#3614
twoimo wants to merge 1 commit into
Yeachan-Heo:devfrom
twoimo:fix/telegram-staging-temp-leak

Conversation

@twoimo

@twoimo twoimo commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Hello, and thank you for the review time on this one.

What this fixes

The Telegram notification self-heal reaper never claimed .tmp staging files, so every failed publication left one unreachable file in the agent notifications/ directory permanently.

writeJsonAtomic stages a sibling <name>.<pid>.<epoch-ms>.<suffix>.tmp and renames it over the destination. If the staging write or the rename fails, or the process dies between the two, that temp is never published and never read again. No prefix in NOTIFICATION_LEAK_ARTIFACT_PREFIXES matched it, so reapStaleNotificationArtifacts — the one thing that exists to clean this directory — walked straight past it.

Reachable today on dev through the public registerNotificationRoot entry point, and equally through daemon ownership state, callback aliases, seen-update ids, and the topic registry snapshot. Most visible where a rename-blocking condition persists: a Windows EPERM from an antivirus or indexer holding a handle, EACCES, EIO, ENOSPC.

Why the reaper, not the writer

I first wrote this as a try/catch unwind inside writeJsonAtomic. Your generation guard correctly rejected that: writeJsonAtomic is a protected lifecycle declaration and the change demanded a strictly higher DAEMON_GENERATION, which lives in telegram-daemon-contract.ts — owned by the active #3596. I reverted it rather than contend for that file.

Fixing it in the reaper turned out to be strictly better anyway:

  • It also reclaims temps orphaned by a crash or power loss, which no writer-side unwind can ever reach.
  • writeJsonAtomic stays byte-identical to dev, so no DAEMON_GENERATION bump and no manifest edit. The guard passes unchanged, and scripts/telegram-daemon-generation-manifest.json is untouched.

Safety

Reaping is shape-matched (/^.+\.\d+\.\d+\.[0-9a-z]+\.tmp$/) and still bounded by the existing five-minute mtime grace window, so a temp that an in-flight publication is still staging is never removed. A published notification file is never matched.

Verification

Reproduced the defect before fixing it, on this host (darwin arm64):

On unmodified dev source — 5/5 runs, deterministic:

(fail) the notification reaper reclaims staging temps abandoned by a failed publication
(fail) the notification reaper leaves a staging temp younger than the grace window alone
 1 pass  2 fail

With the fix — 8/8 runs, deterministic:

 3 pass  0 fail  14 expect() calls

I ran it eight times each way on purpose: my first draft of the staleness assertion was itself flaky (with graceMs: 0, a temp written in the same millisecond can carry a fractional mtimeMs slightly ahead of an integer Date.now(), reading as negative age and being skipped as still-staging). The test now advances the reaper's clock past the grace window instead, which is deterministic.

Also green:

  • notifications-telegram-daemon.test.ts + -2960 + -2960-redteam + -self-heal + this file — 575 pass / 0 fail
  • telegram-daemon-generation-guard --validate-current-tree — exit 0
  • telegram-daemon-generation-guard.test.ts41 pass / 0 fail
  • tsc -p tsconfig.json --noEmit — exit 0
  • biome check — exit 0

Rebased onto the current dev tip. Three files: the reaper rule, one regression test, one changelog bullet. No native bindings, and nothing in #3596's file set.

Happy to adjust anything — please take whatever time you need.

@twoimo
twoimo force-pushed the fix/telegram-staging-temp-leak branch from 31f78f1 to 4207eb4 Compare July 31, 2026 00:08
@twoimo twoimo changed the title fix(telegram): remove the staging temp file when publication fails fix(telegram): reap abandoned publication staging temps Jul 31, 2026
The notification self-heal reaper never claimed `.tmp` staging files, so every
failed publication left one unreachable file in the agent notifications
directory permanently.

`writeJsonAtomic` stages a sibling `<name>.<pid>.<epoch-ms>.<suffix>.tmp` and
renames it over the destination. If the staging write or the rename fails, or
the process dies between the two, that temp is never published and never read
again. No prefix in NOTIFICATION_LEAK_ARTIFACT_PREFIXES matched it, so the
reaper walked past it. This accumulates across the roots registry, daemon
state, callback aliases, seen-update ids, and the topic registry snapshot, and
is most visible where a rename-blocking condition persists (Windows EPERM from
an antivirus or indexer handle, EACCES, EIO, ENOSPC).

Reaping is shape-matched and still bounded by the existing five-minute mtime
grace window, so a temp an in-flight publication is still staging is never
removed. Fixing it in the reaper rather than the writer also reclaims temps
orphaned by a crash, which no writer-side unwind can reach, and leaves the
protected `writeJsonAtomic` lifecycle declaration byte-identical so no
DAEMON_GENERATION bump is required.
@twoimo
twoimo force-pushed the fix/telegram-staging-temp-leak branch from 4207eb4 to 03b5768 Compare July 31, 2026 00:15
@twoimo

twoimo commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current dev 7f5167c86, so exact-head CI covers the current tip: 14 success / 0 failure / 5 skipped.

A note on scope, since it changed after the first push. My initial version unwound the staged temp inside writeJsonAtomic, but that declaration is protected by the generation guard, which then required a strictly higher DAEMON_GENERATION — and that constant lives in telegram-daemon-contract.ts, which #3596 owns. Rather than touch owned code, I moved the repair into the notification self-heal reaper, which is not a protected declaration. That turned out strictly better: it also reclaims temps orphaned by a crash or power loss, which no writer-side unwind can reach. writeJsonAtomic and the semantic manifest are both byte-identical to dev in the current head.

Reaping is shape-matched and still bounded by the existing five-minute mtime grace window, so a temp an in-flight publication is still staging is never removed; one of the three tests pins exactly that.

Ready whenever you have time to look.

@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: 03b57684b9a94efd196166b610ee0f219fb01800
Exact/current base: 7f5167c864f938558840084ad218d48132f0a67a

P0: 0. P1: 2.

  1. The shape+mtime reaper is not publisher-generation/liveness bound. A live or blocked writeJsonAtomic publisher can remain staged past five minutes; startup/periodic reaping cannot distinguish it from crash residue and can delete its in-flight file. Encode a finite destination plus publisher generation/acquisition identity and reap only proven-dead claims; this protected writer change requires a daemon generation bump and guard regeneration.

  2. Cleanup is pathname-only stat then awaited unlink. stat follows symlinks and no regular-file/single-link identity is bound through deletion, so a readdir/stat→replacement ABA can delete a fresh generation's staged file; hardlinks, reparse points, and Windows busy-file semantics are unfenced. Use no-follow identity capture plus an identity-bound removal primitive and cover replacement/symlink/hardlink/Windows sharing races.

Exact Actions evidence is terminal green (17 success / 5 skipped / 0 bad), including the targeted test and Windows Telegram safety, but does not cover these authority races. The required repair overlaps #3596's exact native/generation authority and must be coordinated rather than merged as a shape-only cleanup.


[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