basquetWi + New ticket
llmmsg-srv MSG-397

hub: stuck-agent alert re-fires at base 30min cadence on an unchanged subject+cause - back it off with a ceiling

Done high clcoder-llmmsgsrv-cc

Measured 2026-09-11 on my own inbox: 32 [stuck-agent] alerts in 7 days, of which 24 in the last 48h name the SAME two subjects, nw-whey-cc and nw-venus-cc, repeating a defect already diagnosed and written up. The first alert on a subject is information. Alerts 2 through 32 are not - they re-assert an unchanged subject and an unchanged cause every 10 minutes, and the recipient has no way to say so. Root cause is the one already recorded: the counter measures read_unread CALLS, not delivery, so an agent that is injected with a message, acts on it and answers it without ever calling read_unread stays flagged indefinitely. MSG-390 fixed the SENDER side of the same detector - emitter traffic no longer counts toward the unread tail - and this is the RECIPIENT-of-the-alert side, which 390 does not touch. Fix shape: re-alert on backoff rather than on a fixed 10-minute tick, keyed on subject plus cause. Unchanged subject and unchanged cause means the interval grows; a CHANGED cause, or the counter clearing and re-arming, resets it. Do not silence the class and do not add a dismiss verb - a stuck agent that is genuinely wedged must still escalate, so backoff must have a ceiling that keeps re-alerting, just not at ten-minute cadence for four hours. Gate condition, from the recurring failure this project keeps hitting: the harness must contain a case where the cause CHANGES on the same subject and the alert MUST fire at full cadence. A suite that only proves suppression proves the feature is capable of going silent, which is the regression, not the fix. NOT to be widened into the paired recovery-confirmation traffic: 12 of my 7d inbound are agents telling me a flagged agent drained fine. Those are agent-authored and out of this WI. The right fix there is that the recovering agent should not have to report a false positive at all, which this WI removes at the source.

Sub-tickets

No sub-tickets.
+ Add sub-ticket

Activity

  • wi-cli-venus created · 5w ago
  • wi-cli-venus priorityChanged · 5w ago
    1
  • wi-cli-venus note · 5w ago
    ELAZAR GO relayed by pmmaster-evolutiva-cc 2026-09-11 20:22 ART, raised to P1, build next - takes the head of the coder queue ahead of MSG-392/381/383/384/389. Evidence cited: 24 of 32 stuck-agent alerts in 48h were the same two subjects, re-firing every 10 min on unchanged subject+cause. Backoff keyed on subject+cause with a ceiling that still escalates a genuinely wedged agent. GATE CONDITION, BINDING AND PM-ENFORCED: the harness MUST contain a case where the CAUSE changes on the same subject and the alert fires at FULL cadence. I will not gate this on a suite that only proves suppression - a suite that only proves suppression proves the feature can go silent, which is the regression this WI would introduce. Same shape as the standing rule that a control never seen to fail is a green line, not a control.
  • wi-cli-venus note · 4w ago
    PREMISE CORRECTION, verified in source before the gate - the item's two headline numbers were both wrong and the OLD TITLE is preserved here before the rename: "hub: stuck-agent alert re-fires every 10min on an unchanged subject+cause - back it off". (1) The re-alert floor was never 10min. It is SILENCE_REALERT_S = 1800 (30min) at hub.mjs:10567. The "10min" is STUCK_UNREAD_S = 600 (:10740) leaking out of the alert BODY text, where it is the saturation threshold and not a cadence. (2) "the SAME two subjects" is a routing artifact of the reading seat, not detector repetition: alertTo (:10925) sends every non-nw subject to nw-<host>-cc and only nw-* subjects to pm-llmmsgsrv-cc, so the filer's inbox holds nothing else by construction - 28 alerts / 3 subjects. Fleet-wide over the same 48h it is 115 [stuck-agent] rows, 106 hub-generated, 32 distinct subjects, 6 recipients; the other 9 are agent-authored prose relays from bin-whey-ca that no hub-side backoff touches. Neither correction weakens the item. The real defect is the one the fix targets: a moved max_unread_id plus 30min elapsed re-fires at full rate even when the diagnosis is identical, because the existing sameBatch dedup keys on max_unread_id and the alerting path is its only writer - two consecutive alerts sharing that key are impossible by construction, so measuring that they never collide proves the dedup is wired and nothing more.
  • wi-cli-venus titleChanged · 4w ago
    hub: stuck-agent alert re-fires at base 30min cadence on an unchanged subject+cause - back it off with a ceiling
  • wi-cli-venus completed · 4w ago
    SHIPPED AND VERIFIED LIVE. hub v2.59.19, head 93c2e10 (patch commit acc877d), restarted 2026-09-12 08:13:17 UTC, source-integrity state=clean ahead=0 behind=0 tree=clean. Authored by coder-llmmsgsrv-cc, gated and deployed by pm-llmmsgsrv-cc. WHAT SHIPPED: three new silence_state columns (cause_key, prev_cause_key, alert_level) in the idempotent boot migration. Cause key = multipid flag + filtered-tail flag. Unchanged cause grows the floor SILENCE_REALERT_S << min(level, STUCK_BACKOFF_MAX_LEVEL): 30m, 1h, 2h, 4h, then 4h forever - a ceiling, never silence. A CHANGED cause resets to base cadence. A FLAP back to an already-alerted cause does NOT reset (PM addition B). runStuckCheck routes all four of its writes through a new stmtStuckStateUpsert while stmtSilenceStateUpsert/Reset keep their arity and call sites (PM addition A). New constant STUCK_BACKOFF_MAX_LEVEL, default 3, range 0-12 via envIntInRange - the value is a SHIFT DISTANCE and 1800<<40 wraps negative, which would inverse the comparison and turn the backoff into a one-alert-per-sweep amplifier, so envIntAtLeast was the wrong helper. Level 0 is the kill switch: base cadence without disabling the detector. THE BINDING GATE CONDITION IS MET. g1: ladder seeded at the ceiling, cause changes, alert FIRES at base (un-backed-off SILENCE_REALERT_S) cadence, body says CAUSE CHANGED, row lands on rung 1. g2 is its inverse - unchanged cause at the ceiling still re-alerts, never permanently silent. "Full cadence" was ruled to mean UN-BACKED-OFF, not unfloored; immediate-fire on a cause change was explicitly out of scope, since an oscillating cause would then emit one alert per 5min sweep, strictly worse than the complaint. EVIDENCE, RE-RUN BY THE PM RATHER THAN TAKEN FROM THE HANDOFF: harness 21 assertions, 21 pass, 0 fail. Six mutants all red on distinct assertion sets (M6 floor-off-by-one 10, M1 flap-blind 4, M2 no-reset-on-change 3, M5 clearing-preserves-level 2, M3 no-ceiling 1, M4 suppression-advances-cause 1) - a substitution that matched nothing would have come back 21/21 green, so the spread is itself the proof they landed. g3c is a real control, not a green line: the SHIPPED hub was seen to FIRE on the same fixture where the patched hub suppresses. The SUSPENDED doc's "20/20" was stale by four minutes, not a discrepancy - the harness gained one assertion after that doc was written. MIGRATION REHEARSED ON REAL DATA BEFORE THE RESTART, not only on the g5 one-row control schema: VACUUM INTO copy of live /var/lib/llmmsg-srv/v2.sqlite, patched hub booted against it and served /health, all three columns landed on the real 290-row silence_state, and all 290 rows read ''/''/0 - exactly the never-alerted state, no backfill and no inherited backoff. Confirmed identically on the live DB after restart: 290 rows, 0 non-default. That the service is active is itself the migration proof, since stmtStuckStateUpsert is prepared at module scope against the three new columns and the hub would have thrown at boot had the ALTER trio not landed. ROLLBACK VERIFIED ADDITIVE: the shipped hub still boots against a migrated DB, so revert is a plain revert-and-restart with no schema surgery. NOT EXERCISED, stated rather than folded into the pass: no case drives a real multi-PID poll over HTTP, so the multipid component is exercised only as a stored string; the filtered half of the cause key is never produced live; no case runs more than one sweep, so the ladder is proved rung by rung from seeded state rather than observed climbing end to end (the alert->suppressed->alert weld is covered in two halves by g3b, which asserts the suppression branch writes the row back untouched except last_check_ts, plus the landed-row assertions in g1c/g2b); and there is no post-deploy alert-volume measurement - the 48h fleet figures above are a BEFORE picture. A 3-cycle A,B,C,A defeats one step of cause history; the cause space is four values, so that needs three distinct real state changes in a row, which is itself news. Resumed under the fleet-wide halt on Elazar's own CLI word, scoped to this agent and this item - NOT a fleet resume.
  • wi-cli-venus note · 4w ago
    POST-DEPLOY OBSERVATION, 24h window (deploy 2026-09-12 08:14Z to 2026-09-13 04:35Z), measured on the hub journal + silence_state in /var/lib/llmmsg-srv/v2.sqlite, not inferred. VOLUME: alerts/day went 74 (09-11), 46 (09-10), 36 (09-09) pre-deploy -> 8 in the ~20h since. Post-deploy alerts, all of them: coder04-mba-l-cc, coder02-mars-cc, bin-venus-cc (x2), nw-lezama-cc, bin-whey-cc, coder-mars-cc, bin-venus-ca. LADDER: every one of the 8 logged level=0->1 floor=1800s cause-changed. Not one reached rung 2. Every subject drained before its 1800s floor elapsed, which fires the REV3 drain-reset and clears cause_key, alert_level and last_alert_ts. Confirmed in the DB: across all 95 stuck:agent:* rows, ZERO carry a nonzero alert_level or a non-empty cause_key or prev_cause_key. The writer is wired correctly (stmtStuckStateUpsert at hub.mjs:11231 passes causeKey, prev.cause_key, nextLevel) - the columns are zero because the reset path keeps winning, not because nothing writes them. COVERAGE STATEMENT, stated as a gap not a pass: rung 2 and above, the STUCK_BACKOFF_MAX_LEVEL=3 ceiling, a live flap (causeIsFlap) and a cause change off a NONZERO level are all STILL UNEXERCISED on production. The suppression half is the only half observed. Per the standing rule, a control that has not been seen to fire is a green line, not a control - the 8x drop in volume is consistent with the backoff working AND with the fleet simply being quieter this window, and these 24h cannot separate them. NOTE, benign but worth recording so it is not misread later: 'cause-changed' is true by construction on every first fire, because the drain-reset empties prev.cause_key, so causeKey !== prev.cause_key always holds. The flag carries no information at level 0; it only means something on a re-alert off a nonzero level, which has not happened yet. Pre-existing anomaly, unrelated to this change, not actioned: silence_state row stuck:agent:android holds miss_count=102374 with last_alert_ts 2026-09-04 and has never been drained or reset.
perf
5w ago by wi-cli-venus
4w ago
2026-09-12 08:14