Skip to content

fix(cron): honest fallback-chain alerts + drift-guard alert-once - #85011

Closed
victor-kyriazakos wants to merge 5 commits into
NousResearch:mainfrom
victor-kyriazakos:fix/cron-fallback-alert-honesty-drift-alert-once
Closed

fix(cron): honest fallback-chain alerts + drift-guard alert-once#85011
victor-kyriazakos wants to merge 5 commits into
NousResearch:mainfrom
victor-kyriazakos:fix/cron-fallback-alert-honesty-drift-alert-once

Conversation

@victor-kyriazakos

Copy link
Copy Markdown
Contributor

Cron failure alerts around the fallback chain were lying in three ways, all field-reported from an enterprise fleet running ~40 jobs:

  1. Every provider failure said "Fallback chain was exhausted or unavailable" even when no chain was configured, sending operators to debug a fallback that never ran.
  2. The scheduler's own inactivity watchdog (a stuck tool call) was reported as a "provider timeout" — wrong system entirely.
  3. A fleet-wide config change made every unpinned job re-alert on every tick: 40 jobs, one identical drift alert each, every tick, until each was hand-pinned.

What changed

  • Cherry-picked fix(cron): stop mislabeling inactivity timeouts as provider timeouts + fix fallback-chain wording #81579 (@georgell-ceo, authorship preserved): _fallback_chain_phrase() distinguishes "no chain configured" from genuine exhaustion, and inactivity timeouts are classified before the generic timeout match. 6 tests came with it.
  • Empty-chain alert names the fix: hermes fallback add, or the cron.model + cron.model_provider fleet defaults. The exhausted branch stays terse — the chain is intact there, no config command applies.
  • Drift-guard skips alert once per job, not per tick: reuses the exact alert-once shape the blocked-config preflight already established — a persisted drift_alerted bit, a :silent marker variant that suppresses re-delivery, cleared on the next successful run so a future drift re-alerts. Only the drift branch consults the bit; every other failure still alerts per tick.
  • Drift alert delivered untruncated: the generic summarizer's 180-char cap was cutting the alert off before the pin command. The drift branch formats its own delivery, so the one alert the operator gets contains the actual fix. (Found by mutation-probing the delivery path during review.)

Verification

  • 12 new tests across three files, all asserting delivered content through run_one_job + _deliver_result, not helper internals.
  • Mutation checks: gutting the chain check fails 2 tests; removing the inactivity branch fails 1; disabling the silent suppression fails the alert-once test.
  • Full cron suite via scripts/run_tests.sh tests/cron/: 47 files, exit 0.

Not in this PR (parked as follow-ups)

  • hermes cron doctor with bulk re-snapshot/re-pin verbs (a read-only base exists in feat: add cron doctor health check #43729).
  • Setup/onboarding prompt for fallback configuration — product UX call first.
  • No retry machinery added: retry-with-backoff across the chain already exists in the conversation loop once a chain is configured.

Alexey (CTO) and others added 4 commits August 13, 2026 03:00
_summarize_cron_failure_for_delivery() unconditionally said 'Fallback
chain was exhausted or unavailable.' on every provider failure, even
when fallback_providers is empty (the default -- confirmed empty on
both the root and cto profile config.yaml). That phrasing implies a
fallback was attempted and failed, which sent the operator debugging
the wrong thing.

Add _fallback_chain_phrase(): reads the effective chain via
get_fallback_chain(load_config()) and returns 'No fallback chain
configured.' when it's empty, or the original wording when a chain
exists. Fails open to the original wording on any config read error.

The scheduler's own inactivity-watchdog mislabeling (idle-timeout
reported as provider timeout) was already fixed in a prior commit on
this branch; this closes the second half of t_29b8da55.

Data pull requested by the task (grep errors.log across profiles +
root for 'Provider has been unresponsive' + model=, 2026-07-21 to
2026-08-06): 9 stall events total, 5 on claude-sonnet-5, 4 on
claude-haiku-4-5, spread across 6 different cron jobs. No material
haiku-specific instability -- sonnet-5 stalls at least as often on the
cron path in this sample. Reporting per acceptance criteria; not
worth a routing change on this evidence.
…lert

A cron that dies on a provider timeout with no fallback chain configured
now tells the operator exactly how to fix it: `hermes fallback add` for a
personal chain, or the cron.model + cron.model_provider fleet defaults for
operator-managed fleets. The exhausted-chain branch stays terse — the chain
is intact there and no config command applies.

Field-reported: users hitting the empty-chain failure could not self-serve
from the alert text alone.
A fleet-wide inference config change previously produced one 'Skipped to
prevent unintended spend' alert per unpinned job per tick — 40 jobs meant
40 alerts every tick until each was re-pinned (Coatue field report,
2026-08-11). The NousResearch#44585 guard now reuses the NousResearch#73506 alert-once shape the
preflight path already established: a persisted drift_alerted bit on the
job record, a [drift_skip:silent] marker on repeat ticks that suppresses
delivery, and the bit clears on the next successful run so a future drift
re-alerts. Only the drift branch consults the bit — every other failure
keeps alerting per tick.

The alert text also now says it is sent once, so operators know the job
stays skipped silently until pinned or restored.
The generic failure summarizer caps unrecognized errors at 180 chars,
which cut the drift alert off mid-sentence before the pin command. The
drift branch now formats its own delivery from the guard's full message,
so the one alert the operator gets actually contains the fix.
@alt-glitch alt-glitch added type/bug Something isn't working comp/cron Cron scheduler and job management P2 Medium — degraded but workaround exists sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Aug 13, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Merged via PR #85508 (rebase-merge) — your commits were cherry-picked onto current main with your authorship preserved in git log, including the embedded #81579 cherry-pick from @georgell-ceo.

On top of your branch we reconciled the drift-alert delivery with #72056 (@sashmatash, the earliest submission of that half — lifecycle-aware remediation for consumed finite one-shots), widened the scheduler-internal timeout classification to the TERMINAL_CWD lock-wait path (#79768), and scrubbed some environment-specific comment references.

Thanks for the thorough work — the mutation-probed tests and the alert-once reuse of the preflight shape made this a clean salvage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cron Cron scheduler and job management P2 Medium — degraded but workaround exists sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants