Failures and retries
Watcher assumes things break. The design question at every layer is the same: can this failure lose you an alert? Where it could, there is a retry; where it cannot, the failure is tolerated and recorded.
The summary table
Section titled “The summary table”| What broke | What happens | Do you lose an alert? |
|---|---|---|
| One source | Skipped, error recorded, other sources continue | No |
| Every source | Run ends collection-failed, health alert sent, watch stays due |
No — retried next opportunity |
| Page extraction | Falls through to the next extraction tier | No |
| The model returned unparseable output | Retried, then failed over, then treated as a non-fatal transient | No — re-evaluated next cycle |
| A model key hit its rate limit | The key cools down and the pool rotates to another | No |
| A model key is invalid | Marked dead for the process, next key is used | No |
| The primary model is down | The second-opinion model answers instead, capped at the weak tier | No |
| The embedding service is down | Semantic deduplication is skipped for that run | No — but a duplicate may slip through |
| One delivery channel | Recorded; the run still counts as notified if another channel worked | No |
| Every delivery channel | Alert is stored and replayed on the next run | No — see below |
| The whole run crashed | Logged, health alert sent, other watches continue | No — state advances only at persist points |
Undelivered alerts are retried
Section titled “Undelivered alerts are retried”This is the guarantee worth stating precisely, because a monitoring system that silently drops the one alert you cared about is worse than useless.
When an alert passes every check but every configured channel fails to deliver it:
-
The run ends with the
notify-failedoutcome. -
The formatted alert — summary, evidence quote, evidence URL, date, confidence, tier, and the event’s embedding — is written to a durable pending-delivery queue on the watch’s stored state, along with an attempt count.
-
The event is not marked as notified, so it cannot be considered “already alerted” while it is still owed to you.
-
On the next run of that watch, before any collecting happens, the queue is replayed on the watch’s current channels. No source is fetched and no model call is spent — the stored verdict is sent verbatim.
-
On success, the alert is finally recorded as notified: its fingerprint, its embedding and its ledger entry all advance exactly as a first-time delivery would, so it will not be sent twice.
-
On failure, the attempt count increases and the alert stays queued for the run after that.
The limits
Section titled “The limits”| Limit | Value | Why |
|---|---|---|
| Maximum attempts per alert | 5 (the original failed send counts as the first) | Bounded, so a permanently broken channel cannot retry forever |
| Maximum queued alerts per watch | 10 (oldest dropped first) | Bounded state |
| Model cost of a retry | zero | The verdict is replayed, never recomputed |
Model failures
Section titled “Model failures”Watcher distinguishes two kinds:
“The model could not produce a valid answer.” Free-tier models occasionally emit malformed structured output. The evaluator re-asks a couple of times, then the cascade fails over to the other family. If nothing produces a verdict, the run is reported as evaluation unavailable — logged and health-alerted, but not treated as a hard failure, because no state was persisted and the next cycle will simply try again.
“The key is exhausted or invalid.” This is a real failure worth waking up for. Rate-limited keys are put on a cooldown (using the provider’s own retry hint when it sends one, otherwise 60 seconds) and the pool rotates. Invalid keys are marked dead for the life of the process. When a pool runs out of usable keys, the run fails loudly.
Collection failures
Section titled “Collection failures”A single failing source is not an error — it is expected, and the reason to give important watches more than one source. The engine records what went wrong per source and continues.
Only when every source fails does the run end as collection-failed. In that
case the last-run timestamp is deliberately not written, so the watch stays
due and retries at the very next opportunity instead of waiting for its next cron
slot.
Crash and restart
Section titled “Crash and restart”Watcher keeps nothing important in memory between runs. Everything it needs is in the database, so a crashed process, a killed container or a restarted runner simply picks up at the next run.
The one state that is lost on restart is per-process key health: a key that was cooling down will be tried again after a restart. That is harmless — it either works or it cools down again.
Going deeper
Section titled “Going deeper”- Run outcomes — the vocabulary above, defined.
- Alerts are not arriving — the diagnosis path.
- Model and quota errors — key pools in practice.