Skip to content

fix: stop emailing RESOLVED after every successful sync - #68

Merged
RyanMorash merged 2 commits into
mainfrom
fix/resolved-email-every-sync
Sep 16, 2026
Merged

RyanMorash merged 2 commits into
mainfrom
fix/resolved-email-every-sync

Conversation

@RyanMorash

@RyanMorash RyanMorash commented Sep 16, 2026

Copy link
Copy Markdown
Member

Problem

With an email transport configured (alert.transport = "smtp" or "mailgun"), the daemon sent a RESOLVED email after every successful reconcile cycle — not just after recovering from an alert.

orchestrator.reconcile() calls alert.clear() at the end of every successful cycle, and clear() dispatched RESOLVED whenever alert_config was set, unconditionally. path.unlink(missing_ok=True) swallowed the "no flag was there" case, so nothing distinguished a real recovery from an ordinary healthy cycle.

Fix

RESOLVED is now edge-triggered on the alert flag file, which is already the project's presence = active alert signal:

  • path.unlink() without missing_ok; FileNotFoundError means no alert was active → return silently, no email.
  • An OSError on removal (stuck flag) logs as before and also returns without mailing — that isn't a transition, and mailing there would reproduce the spam on every subsequent cycle. The retry happens next cycle, and RESOLVED goes out when the removal actually succeeds.

ALERT is unchanged. Every failing cycle mails, including consecutive failures on the same condition — a halt that repeats is a sync that is still not happening, and deduping it would drop that signal.

Cycle outcome Email
Fails (safety halt or crash) ALERT, every time — two failures in a row send two
Succeeds, alert was active RESOLVED, once
Succeeds, no alert active nothing

Testing

uv run pytest — 420 passed. Five tests added to tests/test_alert.py:

  • healthy cycle with no flag present sends nothing
  • raise → clear → clear → clear produces exactly [ALERT, RESOLVED]
  • a flag that cannot be removed sends nothing
  • two identical failures then a different one → 3 ALERTs, flag holds the latest reason
  • repeated failures then several successful cycles → [ALERT, ALERT, RESOLVED]

uv run ruff check ., uv run ruff format --check ., and uv run pyrefly check are clean.

Deployment note

alert.py ships in the package, so this needs an OTA / .deb update to reach a device. No config change required — no new keys.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Alert emails are now sent on every failing sync cycle, including consecutive failures.
    • Recovery notifications are sent only once, when an active alert is successfully cleared.
    • Healthy sync cycles no longer generate repeated recovery emails.
    • Failed or missing alert-state cleanup no longer triggers incorrect recovery notifications.
  • Documentation

    • Updated alert transport documentation to clarify failure and recovery notification behavior.

alert.clear() runs after every successful reconcile, and it dispatched a
RESOLVED email whenever an email transport was configured — regardless of
whether an alert had ever been raised. path.unlink(missing_ok=True)
swallowed the "no flag was there" case, so nothing distinguished a real
recovery from an ordinary healthy cycle, and an operator with SMTP or
Mailgun configured got mail every reconcile.

Make RESOLVED edge-triggered on the flag file, which is already the
project's "presence = active alert" signal: unlink() without missing_ok,
and treat FileNotFoundError as "no alert was active, say nothing". A
failure to remove an existing flag is not a transition either, so it logs
and returns rather than mailing on every subsequent cycle until the path
is fixed.

ALERT is unchanged: every failing cycle mails, including consecutive
failures on the same condition. Deduping it would drop the signal that a
sync is still not happening.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 16, 2026 00:49
@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 222c4a55-b744-4a20-9284-ec5d61d0910d

📥 Commits

Reviewing files that changed from the base of the PR and between 8255638 and ecf68f1.

📒 Files selected for processing (3)
  • docs/configuration.rst
  • src/door_sync/alert.py
  • tests/test_alert.py

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The alert transport now sends an ALERT for every failing cycle. It sends one RESOLVED notification only when an active flag is successfully cleared. Tests cover repeated failures and recovery behavior.

Changes

Alert transition semantics

Layer / File(s) Summary
Alert state behavior
src/door_sync/alert.py, docs/configuration.rst
raise_ documents repeated ALERT delivery. clear() sends RESOLVED only after successful removal of an active flag. Missing or failed flag removal sends no notification.
Alert transition validation
tests/test_alert.py
Tests cover inactive clears, successful recovery, failed removal, repeated ALERT messages, and one RESOLVED message after recovery.

Priority: ⚪ Pending latest changes

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant Daemon
  participant alert.raise_
  participant FlagFile
  participant Mailgun
  Daemon->>alert.raise_: report failing cycle
  alert.raise_->>FlagFile: store latest failure reason
  alert.raise_->>Mailgun: send ALERT
  Daemon->>alert.clear: process recovery cycle
  alert.clear->>FlagFile: remove active flag
  alert.clear->>Mailgun: send RESOLVED
Loading

Merge Risk: ⚪ Minimal · up to ecf68

The alert transition change has no remaining actionable merge risk.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 2 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: preventing repeated RESOLVED emails after successful sync cycles.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 2 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/resolved-email-every-sync

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-code-quality

github-code-quality Bot commented Sep 16, 2026

Copy link
Copy Markdown

Code Coverage Overview

Languages: Python

Python / code-coverage/pytest

The overall line coverage in commit f2edbc4 in the fix/resolved-email-e... branch remains at 94%, unchanged from commit 8255638 in the main branch.


Updated September 16, 2026 00:53 UTC

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Preserve a pending resolution or retry mechanism so failed recovery notifications are not permanently lost.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Updates alert recovery notifications to avoid repeated RESOLVED emails while preserving per-failure ALERT notifications. A remaining issue is that recovery notifications may be lost if dispatch fails after the alert flag is deleted.

Changes:

  • Makes resolution notifications edge-triggered.
  • Adds tests for recovery, repeated failures, and stuck flags.
  • Updates configuration documentation.
File summaries
File Description
tests/test_alert.py Tests new alert and recovery behavior.
src/door_sync/alert.py Implements edge-triggered resolution emails.
docs/configuration.rst Documents notification semantics.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/door_sync/alert.py
Copilot flagged that a RESOLVED whose dispatch fails is never retried: the
flag is unlinked first, so every later healthy cycle returns early on
FileNotFoundError. The observation is correct, but a pending-resolution
state file is the wrong answer for a best-effort courtesy email, and the
obvious alternative -- dispatch first, unlink only on success -- is worse:
a failed send would leave the flag raised, telling external monitoring the
system is still halted after it recovered.

The flag, not the email, is the signal monitoring reads, so the recovery
is not lost. What was actually missing is the ability to tell which
notification got dropped: the failure log named the transport but not the
subject. Include it, and record the no-retry reasoning in the clear()
docstring so it does not get re-litigated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@RyanMorash
RyanMorash merged commit 101932a into main Sep 16, 2026
5 checks passed
@RyanMorash
RyanMorash deleted the fix/resolved-email-every-sync branch September 16, 2026 00:54
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