Skip to content
This repository was archived by the owner on Jul 16, 2026. It is now read-only.

feat(bridges): recover orphan .sending files on startup (Phase 5.11) - #79

Merged
jgluck96 merged 1 commit into
dev-clientfrom
josh/phase-5.11-telegram-bridge
Jun 2, 2026
Merged

jgluck96 merged 1 commit into
dev-clientfrom
josh/phase-5.11-telegram-bridge

Conversation

@jgluck96

@jgluck96 jgluck96 commented Jun 2, 2026

Copy link
Copy Markdown

Summary

Phase 5.11 of the OSS→private sync. Pure-additive port of _recover_orphan_sending_files from OSS sutando to both stando bridges (telegram + discord), closing a bug class that silently dropped proactive notifications when the bridge crashed mid-send.

The bug class

Both bridges use atomic-claim-by-rename in proactive delivery: results/proactive-*.txt is renamed to proactive-*.sending before the send call, preventing same-tick double-deliveries between concurrent poll iterations. But if the bridge crashes BETWEEN the rename and the actual send (SIGKILL, OOM, unhandled exception), the .sending file is orphaned forever — no poll iteration looks at .sending suffixes.

OSS already had a fix in both bridges. Stando inherited the .sending claim pattern but never inherited the recovery. This PR closes that.

The fix

File Change Where it runs
src/telegram-bridge.py +56 lines (new function + 1-line call) main() after _single_instance_acquire, before polling loop
src/discord-bridge.py +59 lines (new function + 1-line call) on_ready() after Phase 5.10 auto-seed, before create_task(poll_*)

Function bodies are byte-identical to OSS upstream. Pattern: scan RESULTS_DIR for proactive-*.sending files, rename each back to *.txt, log per-file recovery + total count. Fail-open on per-file errors so a single bad file doesn't block bridge startup.

Audit notes — what was NOT ported

OSS addition Decision Why
send_reply(task_id: str | None = None) parameter Skipped Telemetry plumbing that benefits OSS's outbox_log.append(...) but doesn't fit stando's _emit_channel_metric cloud pipeline. Stando is architecturally ahead here.
from task_archive import find_task_file Skipped Dependency of OSS's claim_task rename pattern. Stando doesn't use claim_task. Inert if ported.

These are revisited if the underlying systems converge later.

Minimal-divergence accounting

  • Both function bodies are byte-identical to OSS — zero conflict surface against future OSS pulls.
  • Pure additive change: 115 line inserts, 0 line edits to pre-existing code. No risk of unrelated regression.
  • Net effect: stando's bridges are now byte-closer to OSS on a ~115-line gap that previously had no equivalent code on the stando side.

Test plan

New test

  • tests/bridge-orphan-sending-recovery.test.py — 9 cases covering both bridges:
    1. Behavior parity (telegram + discord both recover 2 orphan files end-to-end).
    2. Drift guards — both bridges DEFINE the function AND call it from startup.
    3. Selectivity — only proactive-*.sending is touched; task-*.sending, atomic-write temps, and existing .txt files pass through.
    4. Collision safety — if <name>.sending AND <name>.txt both exist, the .sending is left in place.
    5. Idempotency — second call returns 0; missing RESULTS_DIR also returns 0.

Regression sweep (all green)

  • 27 Python tests: discord-bridge-, dm-result-, discord-config, send-allowlist, bridge-marker-no-leak, result-markers, discord-chunker, single-instance, telegram-bridge-* (5), agent-api-result-archive, health-check-phase-57, plus this PR's new test.
  • pnpm typecheck clean.

Manual verification (post-merge)

On the next bridge restart, watch the startup output. If the previous process left orphan .sending files behind, you'll see lines like:

  [startup] recovered orphan proactive-1234.sending → proactive-1234.txt
  [startup] recovered 1 orphan .sending file(s)

And the proactive notification will deliver on the very next poll iteration instead of being silently dropped.

Made with Cursor

Stando's telegram-bridge and discord-bridge both use atomic-claim-by-
rename for proactive-delivery — `results/proactive-*.txt` is renamed
to `proactive-*.sending` before the actual send to prevent same-tick
double-deliveries between concurrent poll iterations. But if the
bridge process crashes between the rename and the send (SIGKILL,
OOM, unhandled exception in the send path), the `.sending` file is
orphaned in `results/` — no poll iteration looks at `.sending`
suffixes, so the owner's proactive notification is silently dropped
until manual intervention.

This PR ports OSS's startup recovery sweep
(`_recover_orphan_sending_files`) to both bridges. On bridge boot,
scan `results/` for orphan `proactive-*.sending` files and rename
them back to `*.txt` so the next poll iteration picks them up.

Changes
-------
- src/telegram-bridge.py: add `_recover_orphan_sending_files` (37
  lines) before `main()`; call it from `main()` after
  `_single_instance_acquire`, before the polling loop.
- src/discord-bridge.py: add the same function (47 lines, with the
  richer docstring from OSS) before `on_ready`; call it from
  `on_ready` after the Phase 5.10 auto-seed block, before the
  poll-task creation.

Functions are byte-identical to OSS sutando's versions (modulo the
"Mirrors discord-bridge.py" / "Mirrors telegram-bridge.py" docstring
swap, which points each at the other for cross-bridge consistency).
Pattern matches the OSS comment chain.

Tests (tests/bridge-orphan-sending-recovery.test.py, 9 cases)
-------------------------------------------------------------
1. Behavior parity — both bridges recover 2 orphan files end-to-end.
2. Drift guards — both bridges DEFINE the function AND call it from
   their startup paths (regex-checked against `main()` and
   `on_ready()`).
3. Selectivity — only `proactive-*.sending` is touched; `task-*.sending`,
   `proactive-*.txt.tmp`, and existing `.txt` files pass through.
4. Collision safety — if `<name>.sending` AND `<name>.txt` both exist,
   the .sending is left in place (no clobber).
5. Idempotency — second call with no orphans returns 0; missing
   RESULTS_DIR also returns 0 (boot before any results materialize).

Minimal-divergence accounting
-----------------------------
- Diff impact on telegram-bridge.py: -0 +56. Pure additive.
  Closes a stando→OSS divergence point: OSS had this function,
  stando didn't.
- Diff impact on discord-bridge.py: -0 +59. Pure additive.
  Closes a stando→OSS divergence point: OSS had this function,
  stando didn't.
- Function bodies are byte-identical to OSS upstream. Zero conflict
  surface against future OSS pulls.
- Net effect: stando's bridges are now byte-closer to OSS on a
  ~115-line gap that previously had no equivalent code on the
  stando side.

Verification
------------
- python3 -m py_compile on both modified bridges: OK.
- New tests: 9 cases, all green.
- Full Phase 5 regression sweep: 27 Python tests green
  (discord-bridge-{allowlist, file-markers, file-send-allowlist,
  pending-replies, prose-marker, reply-chunks, state-prefetch,
  access-tier, discord-state-detection, welcome-on-first-post,
  reply-directive}, dm-result-{send-dm, upload}, discord-config,
  send-allowlist, bridge-marker-no-leak, result-markers,
  discord-chunker, single-instance, telegram-bridge-{allowlist,
  access, tofu, forward-attribution, proactive-owner-resolution},
  agent-api-result-archive, health-check-phase-57, plus this PR's
  new test).
- `pnpm typecheck` clean.

Phase 5.11 of the OSS->private sync. The other OSS additions to
telegram-bridge.py audited in this phase (the `task_id` parameter
on `send_reply` and the `task_archive.find_task_file` import) were
NOT ported: the first is an additive enhancement that mostly
benefits OSS's outbox_log telemetry rather than stando's
_emit_channel_metric pipeline, and the second is a dependency of
the `claim_task` rename pattern stando does not use. Both can be
revisited if the underlying systems converge later.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jgluck96
jgluck96 merged commit 0e4ea11 into dev-client Jun 2, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant