Skip to content

feat: durable inbound receipts, and a /cleanup that can clean (#61, #67) - #70

Merged
TerrifiedBug merged 1 commit into
masterfrom
fix/receipts-and-partial-cleanup
Aug 23, 2026
Merged

feat: durable inbound receipts, and a /cleanup that can clean (#61, #67)#70
TerrifiedBug merged 1 commit into
masterfrom
fix/receipts-and-partial-cleanup

Conversation

@TerrifiedBug

Copy link
Copy Markdown
Owner

Closes #61. Closes #67.

Two independent fixes, both surfaced by the same incident, both in the "something was recorded and nothing could read it" family.


#61 — a durable record that an inbound message arrived

watchRoute reads the spooled payload, unlinks it, then hands it to the session. After a successful delivery there was no on-disk evidence anything arrived: route/<id>/ was left empty and the only surviving copy was inside the receiving agent's transcript, in that agent's private format.

A supervisor asking the reasonable question — did the operator's reply arrive? — had to parse another program's log. When omp-conductor did exactly that, it broke on a host whose session directory and cwd disagreed: two arming attempts, 300s each, both failed while the replies were delivered correctly and on time. The plugin had the truth on disk and had just deleted the only copy.

Now, one bounded receipt per route:

{ "messageId": 12345, "date": 1700000000, "fromId": 555,
  "chatId": 100, "messageThreadId": 7,
  "textSha256": "9f86d081…", "receivedAt": 1700000000123 }
decision why
written before onMsg the case a receipt exists for is a consumer that died; one written afterwards records only deliveries that already succeeded
a hash, not the text the payload already lives in the consuming agent's transcript; a supervisor verifying a challenge knows what it sent, and a hash cannot leak a message to anything that did not already know it
one file per route, replaced in place bounded by construction — the existing purgeRouteDir is the only reaper needed
excluded from the watcher by name it lives in the watched dir and ends in .json, so it would otherwise be consumed as a payload

docs/guide.md documents it as a stable contract, since the value of the feature is that another process may rely on it.

The test that matters. #61 names the likely fake — writing the receipt after onMsg — and an in-process throw cannot catch it, because the throw is caught and execution continues. My first attempt made exactly that mistake and passed under the fake. The real test spawns a child that hard-exits inside onMsg:

receipt before handoff → 1 pass
receipt after handoff  → Expected: 43, Received: undefined   ← fails

#67/cleanup could not clean the mess it exists for

staleThreads was !alive(entry.pid) and nothing else. In a DM host isDmChat selects deleteForumTopic, so cleanup deletes irreversibly. The offered remedy for 83 topics minutes old would also have permanently destroyed an unrelated project topic from eight days earlier — and there was no way to accept part of it. That is why the incident was resolved by scripting deletions by id outside the tool.

Classification, from evidence rather than a guess. An entry recording a sessionFile that does not exist belonged to a process that claimed a topic and died without writing one line of transcript — the crash-loop signature. A claim with no recorded session file is an older-format claim, not a crash: absence of evidence must not become evidence of a crash, or legacy topics get swept up. Pinned by its own test.

The preview now says which is which:

#9549  veltrosecurity — …/veltrosecurity/worktrees (139h ago)
#10071 conductor      — …/conductor/worktrees (1m ago, recent) ⚠ session never ran
#10073 conductor      — …/conductor/worktrees (1m ago, recent) ⚠ session never ran
…

And selection is possible:

/cleanup                      preview
/cleanup go                   everything stale
/cleanup go 10071 10073       only those
/cleanup go never-ran         only the crash-loop set

Unparseable arguments print usage rather than falling through to all — falling through deletes irreversibly. Selection always applies to a freshly derived set, so naming an id that has since resumed selects nothing rather than acting on a remembered snapshot.

Replayed against the real registry from the incident:

stale by the OLD predicate (dead pid only): 13
/cleanup go never-ran takes:                11
PRESERVED: #9549 veltrosecurity, #9551 conductor

Exactly the hand-written script's outcome, from the tool.


Proof

bun run check   → 314 pass, 0 fail   (was 307)

New tests: receipt fields and hash-not-text; survives a consumer that dies mid-handoff; bounded across two deliveries; never consumed as a payload; DM route covered; removed by purgeRouteDir; a corrupt receipt reads as absent. Cleanup: the grammar; bad input refused; ids select exactly those; a resumed id selects nothing; never-ran picks the right set; a claim with no session file is history; the preview labels and ages correctly.

Not changed

Delete-vs-close in a DM host. I checked, and closing is not available there — reopenForumTopic/closeForumTopic both return Bad Request: the chat is not a supergroup forum for a private chat, so deleteForumTopic is the only operation Telegram offers. Selection is therefore the remedy for irreversibility, not a gentler verb.

**#61 — a durable record that an inbound message arrived.**

The spooled payload is unlinked before the handoff, so after a successful
delivery the only surviving copy lived inside the receiving agent's transcript,
in that agent's private format. A supervisor asking "did the operator's reply
arrive?" had to parse another program's log — and when it did, it broke on a
host whose session directory and cwd disagreed.

One bounded receipt per route now, written BEFORE `onMsg`: message id, date,
sender, chat, thread, and a sha256 of the text. Before the handoff because the
case a receipt exists for is a consumer that died; a receipt written afterwards
records only the deliveries that already succeeded. A hash rather than the text
because the payload already lives in the consuming agent's transcript, a
supervisor verifying a challenge knows what it sent, and a hash cannot leak a
message to anything that did not already know it.

Bounded by construction — one file per route, replaced in place — so the
existing route purge is the only reaper needed. Documented in docs/guide.md as
a stable contract, since the whole point is that another process may rely on it.

The receipt lives in the watched directory and ends in `.json`, so the watcher
now excludes it by name: evidence, never a payload.

**#67 — /cleanup could not clean the mess it exists for.**

`staleThreads` was `!alive(pid)` and nothing else, and in a DM host cleanup
*deletes*. So the only offered remedy for 83 topics minutes old would also have
permanently destroyed an unrelated project topic from eight days earlier. There
was no way to accept part of it, which is why the incident was resolved by
scripting deletions by id outside the tool.

Stale topics are now classified. The discriminator is evidence, not a guess: an
entry recording a session file that does not exist belonged to a process that
claimed a topic and died without writing one line of transcript. A claim with no
recorded session file at all is an older-format claim, not a crash — absence of
evidence must not become evidence of a crash.

The preview says which is which and how old. And selection is possible:
`/cleanup go <id>…` for exact topics, `/cleanup go never-ran` for the crash-loop
set. Unparseable arguments print usage instead of falling through to "all",
because falling through deletes irreversibly. Selection always applies to a
freshly derived set, so naming an id that has since resumed selects nothing.

Replayed against the real registry from the incident: `never-ran` takes exactly
the 11 spam topics and preserves both legitimate ones. The old predicate took
all 13.

Each test was checked against the old behaviour: moving the receipt after the
handoff turns "survives a consumer that dies mid-handoff" red (a subprocess that
hard-exits inside onMsg — an in-process throw is caught and cannot distinguish
the two orderings).

bun run check: 314 pass, 0 fail.
@TerrifiedBug
TerrifiedBug merged commit 1387374 into master Aug 23, 2026
1 check passed
@TerrifiedBug
TerrifiedBug deleted the fix/receipts-and-partial-cleanup branch August 23, 2026 08:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant