-
Notifications
You must be signed in to change notification settings - Fork 7
fix(watchdog): alert-framing — page once per wedge episode, not per kickstart #602
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
8a9ddfc
8771ec2
d3350ea
96ef4b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,11 @@ | |
|
|
||
| DEFAULT_WATCH_LABEL = "com.brainlayer.watch" | ||
| DEFAULT_NOTIFY_ENDPOINT = "http://localhost:3847/notify" | ||
| # Alert-framing (orc law, stalker postmortem): after a wedge episode is paged once, | ||
| # the operator is re-notified for the NEXT episode only — i.e. after this many | ||
| # consecutive healthy (progressing) ticks clear the latch. Prevents a chronic | ||
| # wedge sawtooth from paging on every kickstart. | ||
| _EPISODE_RESET_TICKS = 3 | ||
| DEFAULT_COMMAND_TIMEOUT_SECONDS = 15 | ||
| KICKSTART_TIMEOUT_SECONDS = 45 | ||
|
|
||
|
|
@@ -491,11 +496,21 @@ def run_once( | |
| elif config.dry_run: | ||
| result.action = "would_kickstart" | ||
| else: | ||
| try: | ||
| alert_fn(config, result) | ||
| except Exception as exc: | ||
| result.alert_error = str(exc) | ||
| print(f"throughput-watchdog alert failed: {exc}", file=sys.stderr) | ||
| # Alert-framing: page on the FIRST wedge of an episode only — never | ||
| # per-kick of a chronic sawtooth. The latch clears after a sustained | ||
| # healthy run (episode reset below). Recovery still runs every tick. | ||
| # The latch is set ONLY when the page actually sent — a transient | ||
| # notify failure on the first wedge must NOT silence the whole | ||
| # episode (page-once must never become page-zero); the next tick | ||
| # retries the alert instead. | ||
| episode_alerted = bool(state.get("episode_alerted")) | ||
| if not episode_alerted: | ||
| try: | ||
| alert_fn(config, result) | ||
| episode_alerted = True | ||
| except Exception as exc: | ||
| result.alert_error = str(exc) | ||
| print(f"throughput-watchdog alert failed: {exc}", file=sys.stderr) | ||
| attempt_state = dict(state) | ||
|
macroscopeapp[bot] marked this conversation as resolved.
|
||
| attempt_state.update( | ||
| { | ||
|
|
@@ -512,6 +527,7 @@ def run_once( | |
| "last_action": "recovery_attempt", | ||
| "last_restart_epoch": checked_at, | ||
| "restart_attempt_count": int(state.get("restart_attempt_count", 0)) + 1, | ||
| "episode_alerted": episode_alerted, | ||
| } | ||
| ) | ||
| _atomic_write_json(config.state_path, attempt_state) | ||
|
|
@@ -549,6 +565,25 @@ def run_once( | |
| next_state["last_alert_error"] = result.alert_error | ||
| else: | ||
| next_state.pop("last_alert_error", None) | ||
|
|
||
| # Episode reset (alert-framing): clear the first-wedge page latch after a | ||
| # SUSTAINED healthy run, so the next genuine wedge pages again without | ||
| # re-paging every cycle of a chronic sawtooth. "Healthy" = real watcher | ||
| # progress by EITHER measure — chunk highwater OR liveness rowid — because | ||
| # a DB without watcher_liveness_events reports liveness_rowid==0 forever | ||
| # while chunks still advance (else the latch would never reset there). A | ||
| # kickstart tick is never healthy: its own state pre-advances both rowids to | ||
| # the current values, so neither comparison is strictly-greater on that tick. | ||
| prev_chunk = int(state.get("watcher_highwater_rowid", 0) or 0) | ||
| prev_liveness = int(state.get("watcher_liveness_highwater_rowid", 0) or 0) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium The episode-reset block calls 🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
||
| progressed = current_highwater > prev_chunk or current_liveness_highwater > prev_liveness | ||
| if result.stalled_ticks == 0 and progressed: | ||
| healthy_ticks = int(state.get("healthy_ticks", 0)) + 1 | ||
| next_state["healthy_ticks"] = healthy_ticks | ||
| if healthy_ticks >= _EPISODE_RESET_TICKS: | ||
| next_state["episode_alerted"] = False | ||
| else: | ||
| next_state["healthy_ticks"] = 0 | ||
| _atomic_write_json(config.state_path, next_state) | ||
| return result | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fresh evidence since the earlier alert-latch comment: the default
_best_effort_alertstill swallows delivery failures (for exampleurlopenexceptions, and nonzeroosascriptexits do not raise), so when the first wedge only manages to append the log but fails to notify the operator,alert_fnreturns normally and this line marks the episode as alerted. Subsequent recovery attempts in the same wedge then skip the alert until three progress ticks occur, turning a transient notification outage into a page-zero episode despite the intended retry behavior.Useful? React with 👍 / 👎.