Skip to content

fix(alerts): one notification per incident, none on recovery - #1539

Merged
EtanHey merged 1 commit into
mainfrom
fix/notif-one-per-incident
Oct 28, 2026
Merged

EtanHey merged 1 commit into
mainfrom
fix/notif-one-per-incident

Conversation

@EtanHey

@EtanHey EtanHey commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Why

Etan's Notification Center (2026-10-28) was flooded via Script Editor (osascript display notification):

  • "BrainLayer calendar 2026-10-27 abandoned during index …" posted 4× at 00:07;
  • a stream of "BrainLayer recovered".

Root cause: maintenance_calendar._abandonment_alerts maps one abandoned run to one alert key per incomplete step, all with the same text, and job_alerts.report() notified once per key. Every clear also notified "recovered".

What

report() still sets/clears every key in job-alerts.json (BrainBar's dashboard reads it) and still logs the health event, but posts a macOS notification only for a new failure whose text no other active key already carries, and never on recovery. drive-consent's re-notify-on-new-reason rule is unchanged.

Tests

  • tests/test_job_alerts.py (30 passed), tests/test_brainbar_auto_activation.py + tests/test_maintenance_calendar.py::test_abandoned_run_sets_every_step_alert_but_posts_one_notification (43 passed); ruff clean.

Record

🤖 Generated with Claude Code


Note

Low Risk
Alert persistence and logging are unchanged; only macOS notification deduplication and suppression on recovery are adjusted, with broad test coverage.

Overview
Stops macOS Notification Center spam from job_alerts.report() while keeping job-alerts.json and health logging behavior for BrainBar.

Failure notifications now fire only when the failure text is not already present on another active alert key, so one abandoned calendar run that fans out the same message to index, backup-daily, etc. triggers one osascript notification instead of one per key. drive-consent still re-notifies when the reason escalates.

Recovery clears still update the alert file and emit health event … state=recovered logs, but never post a “BrainLayer … recovered” notification.

Tests were tightened across job alerts, BrainBar activation, and maintenance calendar to match the new notification rules.

Reviewed by Cursor Bugbot for commit e0fb873. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Suppress duplicate and recovery notifications in job_alerts.report

  • Changes the alert reporter in job_alerts.py so recovery still clears the alert state and logs a health event, but sends no notification
  • Suppresses failure notifications when another active alert key already has the same reason; the alert state is still written and reported as changed
  • Updates and adds tests across test_job_alerts.py and test_brainbar_auto_activation.py, plus an integration test in test_maintenance_calendar.py covering one notification for an abandoned run with multiple same-reason step alerts
  • Behavioral Change: an active alert reason no longer notifies again under a different key until the first alert is cleared; recovery events no longer notify at all

Macroscope summarized e0fb873.

An abandoned nightly fanned one incident out to one alert key per
incomplete step, each with the same text, so Etan got the same
"calendar ... abandoned" notice 4x at 00:07. Every clear also posted
"BrainLayer <key> recovered".

report() still sets/clears every key in job-alerts.json (BrainBar reads
them) and still logs the health event, but posts a macOS notification
only for a new failure whose text no other active key already carries,
and never on recovery.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@EtanHey EtanHey added the size:S Tight-loop PR size: 51-150 hand-written lines added label Oct 28, 2026
@coderabbitai

coderabbitai Bot commented Oct 28, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 46 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 00f6b105-01a7-4376-87b1-713bb29fb794
Commits

Reviewing files that changed from the base of the PR and between 1253f60 and e0fb873.

Files selected for processing: 4
  • src/brainlayer/job_alerts.py
  • tests/test_brainbar_auto_activation.py
  • tests/test_job_alerts.py
  • tests/test_maintenance_calendar.py
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@cursor

cursor Bot commented Oct 28, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: 877675cc-3815-405d-a92d-9f770233a03c)

@github-actions

Copy link
Copy Markdown

BrainLayer ratchet

Every Value below was measured by this run. A row this machine cannot measure says n/a — <reason> instead of a number; baselines in Notes name their own machine, method and date and were not measured here.

Hosted success gives no verdict on mapped bytes, search p50/p95, idle CPU, or fallback replay debt.

Row Status Value (measured by this run) Method Notes
commit provenance 🟢 GREEN measured e0fb873a8e13 == PR head · checkout e0fb873a8e13 commit graph + live PR head · in-process · runner Which commit this whole table is about. On a pull_request event the checkout is GitHub's synthetic merge ref, whose sha is not on the PR — #759's table printed 13fa724278bf while that PR's head was 4632f979 — so this row names the PR-head parent instead, the sha a reviewer can actually see. The comparison sha is read live from repos/{owner}/{repo}/pulls/{n} when the table is collected, not taken from the event payload, because the payload cannot know the run has been overtaken. Residual window, stated rather than papered over: a push landing between that read and the comment being posted is not caught here — the run for that push refreshes the table.
baseline attestation 🟢 GREEN baseline f421d1a7c5e6 matches the main attestation (run 40663077231 · main 1253f606dc59 · 2026-10-28T09:17:53Z) main attestation artifact via Actions API · in-process · runner What every comparison is measured AGAINST, and who says so. The baseline fields of tests/fixtures/sprint_gate/corpus.json (queries, latency_baseline_ms, thresholds) are compared to the ratchet-attestation artifact of the latest successful push or (no-input) workflow_dispatch run of ratchet-attest.yml on main, fetched through the Actions API — a PR run cannot write to another run's artifacts. A field that differs is RED unless that main run measured the new value. The calibrated socket collector can license p50/p95; every absent measured path stays locked, so missing collection never passes as permission for a hand edit. Boundary: the comparator is this PR's checkout of ci_ratchet_table.py, diff-reviewable, not tamper-proof.
provenance 🟢 GREEN stamped e0fb873a8e13 == HEAD, tree clean wheel stamp · in-process · runner Sha half of #749 keg-mode provenance: a keg built from this wheel can answer __build_sha__. The helper-age and served-process predicates need a running BrainBar and are measured only by scripts/sprint_gate.py on an installed Mac. The sha here is the checkout's — the explicit PR head in ratchet.yml — because that is what publish.yml stamps at release time; the PR-head sha this table describes is the one in commit provenance above.
fallback replay debt ⚪ n/a n/a — no fallback queue on this machine: the pending memories live in ~/Gits/*/docs.local/decisions, and docs.local/ is gitignored, so a runner checkout has no copy of them to count docs.local walk · machine with the fallback queue local-only; not enforced by hosted CI. intended_brain_store: true with no chunk_id means a memory reached disk and never reached the DB, so it answers no brain_search. Budget: 0. Any pending or unparseable file is a finding, never a band -- 122 of these sat from 2026-06-28 to 2026-09-05 because nothing counted them where a reader would look. Measured by walking the tree, so it is only ever measured on a machine that HAS the tree.
pre-push: delete-only push runs no suite 🟢 GREEN 0 suite starts · exit 0 real checkout hook · delete-only stdin · sentinel suite runner Bug SHA: 44ecbf5ba6f1ada3b965797559eee90c94b45595 (RED replay, 2026-10-06). Fix: #1084; measured at e0fb873a8e138dfae37aa892a479566281958dc0. Ceiling: 0 suite starts; hook must exit 0 and announce the delete-only skip.
mapped bytes ⚪ n/a n/a — no BrainBar daemon at /tmp/brainbar.sock: this row needs the daemon, its hybrid helper and the indexed corpus running together, and no GitHub-hosted runner has them (macOS included) — only a self-hosted Darwin/arm64 runner on an installed Mac would socket · installed Mac local-only; not enforced by hosted CI. Baseline 26.2 GB — installed Mac, socket, 2026-09-03, after R2 drained 15,070 → 0. Up from 16.8 GB because the drain left more vectors mapped under the same cap: the change is the drain, not a leak. Not measured by this run.
search p50/p95 ⚪ n/a n/a — no BrainBar daemon at /tmp/brainbar.sock: this row needs the daemon, its hybrid helper and the indexed corpus running together, and no GitHub-hosted runner has them (macOS included) — only a self-hosted Darwin/arm64 runner on an installed Mac would socket · installed Mac local-only; not enforced by hosted CI. Margin p50: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Margin p95: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Calibrated on MacBook-Pro.local at 2026-09-01T08:42:22Z under active_sprint_load (tests/fixtures/sprint_gate/corpus.json). Not measured by this run.
idle CPU ⚪ n/a n/a — no BrainBar daemon at /tmp/brainbar.sock: this row needs the daemon, its hybrid helper and the indexed corpus running together, and no GitHub-hosted runner has them (macOS included) — only a self-hosted Darwin/arm64 runner on an installed Mac would ps sampling · installed Mac local-only; not enforced by hosted CI. Ceiling: average CPU < 30% over a 60 s window (resource_budget in scripts/sprint_gate.py), ratified and kept as a hard budget. Margin daemon: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Margin helper: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Margin watcher: margin unmeasured — 0 of the 5 attested green main runs it needs; no verdict is rendered from fewer. Needs the BrainBar daemon, helper and watcher actually running. Not measured by this run.
signature_valid ⚪ n/a n/a — the macOS signature-parity job is trigger-gated and did not run on this PR: it touches no release or signing path (pyproject.toml, scripts/release-*, scripts/brainlayer-version-check.sh, publish.yml, ratchet.yml) and carries no ratchet:signatures label — a GitHub macOS runner bills at ~10× Linux minutes and rebuilds the keg venv from source codesign · installed keg scripts/release-verify-signatures.sh <keg> codesign-verifies every *.so/*.dylib under libexec/venv. The macOS parity job installs the published tap formula (etanhey/layers/brainlayer), so this row measures the release path — formula, published sdist and Homebrew's relocation — and not this PR's tree. Release-time baseline for the same keg on a different machine: 442 valid / 0 invalid — installed Mac (M4 Max), brew --prefix brainlayer 1.5.11, 2026-09-03.
BrainBar renders no enrichment status 🟢 GREEN 19 actual-view captures; active pending-store/replay retained macOS DEBUG binary · AppKit actual views · Vision OCR + icon RGBA comparison · private synthetic state Missing render capability/evidence is RED. Historical data is retained; this is source-build proof, not installed-app proof.
Watcher heartbeat freshness and recovery ⚪ n/a n/a — macOS native probe trigger-gated: no affected path or ratchet:heartbeat label private Python producer -> compiled Swift consumer Preserves genuine silence; independent agent stores cannot revive the watcher. Not a load or installed-UI measurement.
maintenance quiesce: services stay down under a reviving watchdog ⚪ n/a n/a — macOS replay trigger-gated: no affected path or ratchet:quiesce label real launchd · GitHub macOS runner · real maintenance + fleet-watchdog Fix: #1076 (bc6d690f4331387485678d0247e19e14e6ef8d97). Ceiling: 0 revivals; supervisor held, both resumed, no hold marker.
no cloud model call reachable anywhere 🟢 GREEN 508 source files; 444 installed imports; 3 native tests; wheel c3b64f8045da; 0 candidate attempts immutable installed wheel (default + dev) + AST source + actual native BrainBar Synthetic private fixtures; SDK absence gated on merged R10b. Exact-head candidate proof, not deployment.

🟢 GREEN measured, within budget · 🔴 RED measured, out of budget — a finding to clear before merge · ⚪ n/a not measurable on this machine, never guessed.

No RED rows.

Measured on Linux/x86_64 · measured e0fb873a8e13 · PR head e0fb873a8e13 · checkout e0fb873a8e13 · run · updated 2026-10-28 10:21:27 UTC

@EtanHey
EtanHey merged commit f6a044b into main Oct 28, 2026
26 of 42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S Tight-loop PR size: 51-150 hand-written lines added

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant