tootsies

a discord bot for the tootsies server. ask, recap, discuss, ship features by typing.


Project maintained by mejasonmejason Hosted on GitHub Pages — Theme by mattgraham

Observability: the full detail

CLAUDE.md carries the binding rules (the operability bar, the field-budget core, the ops-monitor summary); this file is the full narrative those rules compress – the complete field-budget history + tooling, and the ops monitor’s findings catalog. The DETAILED event ledger (the full table) lives in this file; utils/events.py’s module docstring carries the compressed quick index – keep both updated.

The event stream + field budget (full narrative)

Every metric-worthy thing emits a JSON event line via utils.events.emit(kind, **fields). Each line is prefixed with the literal EVENT so Railway log queries can isolate dashboard data from operational logs.

Existing event kinds – the DETAILED ledger (the compressed quick index lives in utils/events.py’s module docstring; keep BOTH updated when adding an event):

event source fields                                    
command utils/metrics.py (@track_command) cmd, user_id, guild_id, duration_ms, ok, error                                    
claude_api claude_client.py (_call) model, purpose, input_tokens, output_tokens, duration_ms, stop_reason, ok. On failure: error (the exception CLASS name) plus detail.api_error — the API’s own message, truncated to 300 chars (#2174), read with parse_json(detail)['api_error']. The class name alone is not diagnostic: a robots.txt refusal, an image the API could not download, a malformed tool schema and a blown context window all arrive as BadRequestError, so 22 such events over 30 days said a call failed and nothing about why. A 400 is deterministic and caller-caused — the body is the answer. Parity with openai_api, which already stored its response body, and it was that stored body that made #2171 solvable in minutes. On success, detail.cache_creation_input_tokens / detail.cache_read_input_tokens (#2659) — the Anthropic prompt-cache write/read token counts for this turn, read with parse_json(detail)['cache_read_input_tokens']. input_tokens on its own is the UNCACHED remainder ONLY (it excludes both), so before this pair shipped there was no way to tell a cache hit from a miss, or to add up what a call actually cost — the gap a low org-wide cache-hit-rate report (Console billing alert, 2026-08) surfaced: the mechanism (cache_control: ephemeral on the persona + per-surface system blocks, see ARCHITECTURE.md) was already live and, verified live against the running persona prompt, DOES cache warm for the high-volume Sonnet/Haiku surfaces (cache_read_input_tokens > 5000 on the very first probe call) — the org just had no per-call visibility into it. The low-volume Opus /ask surface (~3 calls/day) is a genuine, expected cold-cache surface: its call gaps run ~8h, past even the 1-hour TTL, so there is no TTL choice that would warm it — accept the miss rather than pay a pre-warm write nobody would read back. scripts/axiom_setup.py’s cache_hit_stat/cache_hit_purpose panels and the ops-monitor’s per-provider “LLM spend by provider” line (cache hit N% (reads / writes)) both read this pair; the cost panels/_spend_cost price a cache write at ~1.25x and a read at ~0.1x the model’s input rate, so the estimated spend actually reflects a cache hit’s savings now instead of silently excluding cached tokens from the sum entirely. On success, detail.image_retry (#backpage, 2026-08-30) – the API rejected one of the attached images, so _call dropped EVERY image and retried on the text alone. Read the rate per surface with ['tootsies'] | where event=='claude_api' | summarize calls=count(), retried=countif(tostring(parse_json(detail)['image_retry'])=='true') by purpose | extend pct=round(100.0*retried/calls,1). Match on the VALUE, never on the substringemit() keeps the null keys when it stringifies detail, so every claude_api row literally contains the text "image_retry":null and a detail contains 'image_retry' filter matches 100% of calls (measured 2026-09-03: 45,590 of 45,590 over 7 days, against a true 476). The same trap applies to every marker in this row. Measured correctly, curator_pick retried 475/2454 calls (19.4%) over the 7 days to 2026-09-03. It sat near 10% of curator_pick calls for months, climbed past 50% in August 2026, and nobody saw it, because the retry still reports ok=true. That is the fail-open trap: the vision fit-judge kept answering about images it could not see. A caller whose TASK is the images now passes images_required=True, which turns the retry off – the call FAILS instead, and the failure carries detail.api_error naming the image. So watch image_retry as a degradation rate, not as noise. Full account: ARCHITECTURE.md -> curator.py. detail.thinking_exhausted / detail.thinking_recovery (2026-09-03) – a thinking-enabled turn that stopped at max_tokens with NO text (every token went to thinking blocks, which are dropped) is stamped thinking_exhausted=true, and the one low-effort retry of that turn is stamped thinking_recovery=true. Before the retry existed the surface shipped its empty-answer fallback (“lost my train of thought”) and answer_length read as a normal 45-char reply, so the miss was invisible. Query ['tootsies'] | where event=='claude_api' | where tostring(parse_json(detail)['thinking_exhausted'])=='true' | summarize count() by purpose, model — match the VALUE, not the substring (see the image_retry note above: a detail contains filter matches every row, because the null keys survive into the stringified detail); a recovery turn that ALSO stops at max_tokens is the still-empty case — that is what the ops monitor’s reasoning_exhausted finding flags (#2934), and the llm_exhausted dashboard panel graphs both providers’ markers.                                    
openai_api utils/openai_chat.py (complete/create_response) model, purpose, duration_ms, ok; on success: input_tokens, output_tokens, stop_reason, response_chars, response_preview (Chat Completions); Responses round-trips carry responses=True; on failure: error (HTTP <status>/no_text_in_response/<exc>), detail, category (#973 — the failover class: provider/quota/auth/request/malformed; provider+quota fail over to Claude, the rest surface; category=quota = OpenAI out of credits, a 429/insufficient_quota NOT retried), detail — one OpenAI call, the optional OpenAI TEXT backend (#openapi-parity), the provider twin of claude_api for a surface routed to a GPT model (cost-spread / Claude-credit failover). Two paths: Chat Completions (tool-free) + the Responses API (tools/web_search, one event per round-trip). Ops-monitor keys these openai:<purpose> (parity with claude_api’s per-purpose split) so each GPT-routed surface’s cost/latency stays separately visible. On success, detail.reasoning_exhausted / detail.reasoning_recovery (#2869) — the openai_api twin of claude_api’s thinking_exhausted pair. A reasoning model (the gpt-5 family) spends its thinking out of the SAME output budget as the visible message, so a turn that spends it all comes back with no text — status="incomplete" on the Responses path, finish_reason="length" on Chat Completions — and the surface ships its fallback line. Such a turn is stamped reasoning_exhausted=true, and the one lower-effort retry of it is stamped reasoning_recovery=true. Query ['tootsies'] | where event=='openai_api' | where tostring(parse_json(detail)['reasoning_exhausted'])=='true' | summarize count() by purpose, model — match the VALUE, not the substring (see the image_retry note in the claude_api row); a recovery turn that is ALSO stamped exhausted is the still-empty case — the ops monitor’s reasoning_exhausted finding flags it (#2934) — and a sustained rate on one purpose says that surface’s budget floor is too low for what it asks the model to do                                    
openai_vision_fetch claude_client.py (_localize_openai_vision) purpose, count (images that downloaded to inline base64), ok (none dropped); dropped (# we couldn’t download) folds into detail — OpenAI’s Responses API can’t server-side-fetch an image URL (#1365), so the bot downloads it and passes bytes. An image that WON’T download is dropped (#2171): passing its URL made OpenAI reject the whole call with a 400 (Error while downloading file), and a 400 is category=request, so the router re-raises instead of failing over to Claude and the surface posts its canned error quip — one unreachable image killed the whole answer (the 2026-08-08 ask_mention outage: a reply to a message whose image had expired, host 401). Dropping costs that one image and keeps the reply; the paired [image N, posted by …] label block stays, so the model is not told an image is present when it isn’t. The drop raises NO exception (just a thinner prompt), so this ok-rate is the only signal our IP stopped reaching an image host — ops-monitored as integration vision_cdn. A low steady rate is NORMAL (an expired Discord attachment URL); only a blackout flags                                    
provider_fallback claude_client.py (_call) purpose, requested (the model that failed), fell_back_to, reason — one provider failed and _call failed over to the OTHER (#openapi-parity, now BIDIRECTIONAL). FORWARD (a GPT surface → Claude): fell_back_to = the GPT’s Claude sibling (gpt-5.6-sol→Opus, gpt-5.6-terra→Haiku; an unmapped id→the frontier Opus), reason = the failure CATEGORY (#973: provider/quota/unprovisioned — the FALLOVER classes; an auth/request/malformed OpenAI failure is NOT failed over, it re-raises so the surface’s own canned fallback handles it, the mirror of the reverse direction re-raising a Claude 400/401) — the graceful degradation that makes gpt safe as a per-surface menu choice. REVERSE (a Claude surface → OpenAI, #973): fell_back_to = the Claude’s OpenAI sibling (Opus→gpt-5.6-sol, Sonnet/Haiku→gpt-5.6-terra; an unmapped id → the frontier gpt-5.6-sol), reason=credit_exhausted (the Anthropic balance ran dry — a 400 naming the credit balance, permanent until topped up) or claude_unavailable (a persistent 529/5xx/429/timeout that survived the retry loop) — credit-exhaustion failover, so the bot keeps talking instead of going dark on every surface at once. Each direction calls the OTHER provider’s method DIRECTLY (never back through the router), so the two can’t ping-pong. A rising FORWARD rate = OpenAI flaking; a rising REVERSE rate = Anthropic out of credits / overloaded. (Vision is served on the Responses path now, so it no longer triggers a fallback by itself.)                                    
order_state cogs/order.py order_id, issue_number, guild_id, user_id, from, to                                    
order_served_ping cogs/order.py guild_id, user_id (the reporter), order_id, issue_number, delivered, reason (None on delivered; no_guild/no_channel/no_perms/bot_off/send_failed) — when a reconcile flips an order to SERVED, Toots pinged the original reporter back in the channel they filed it from (“completing the circle”). Fires once per order (guarded by orders.served_pinged_at); fail-open, honors the master kill switch (bot_off leaves it pending so it lands once she’s back on)                                    
rate_limit_hit utils/rate_limits.py scope, command, user_id, guild_id, count, cap                                    
deploy_event bot.py kind (boot/shutdown), guilds                                    
guild_join bot.py (on_guild_join) guild_id, ok, + folded detail (name, members, owner_id, inviter_id) — Toots was ADDED to a server. Captures the guild NAME + who added her (the facts that were unrecoverable once she’d left an unknown server), AND pages the bot MASTER in the MASTER guild’s #bot-logs (utils.permissions.MASTER_GUILD_ID) so an unrecognized add is surfaced the moment it happens, not discovered weeks later from a stale DB row. Fully fail-open                                    
guild_left bot.py (on_guild_remove) guild_id, ok, + folded detail (name, members, owner_id) — the mirror of guild_join: Toots was REMOVED from a server. A durable record of WHEN she left + a master #bot-logs page                                    
error cogs/* + bot.py error handler source (e.g. ask, order_preflight, undo), error (exception class), guild_id, user_id, optional context, traceback (last 3 frames), + folded detail.error_message — the exception’s own TEXT, capped at 300 chars, and OFF unless the call site passes include_message=True (#2816). The default is a data-minimization rule: emit_error is the catch-all around provider / HTTP / Discord / database calls, so str(exc) is UNCONTROLLED input — an asyncpg unique-violation prints the offending key VALUE, an aiohttp error prints the request URL with its query string, an LLM SDK error can print a response body — and none of that may reach a durable 30-day store. Opt in ONLY where the text is ours: a constructed exception, written to describe a fail-open miss, which has no traceback either, so its message is the only diagnosis it has. Seven such sites (billboard, pollstar_charts ×2, industry_feeds ×2, link_enrich, music_desk) are opted in. Query with parse_json(detail)['error_message']                                    
recap cogs/recap.py (produce_recap) guild_id, period (1h|1d|today), count (messages used), decision (ceiling|rate_limit|size|full) — one recap built, with its coverage. A non-full decision means the recap covers only the most-recent slice of the window, and the card’s header (and the @-mention reply) state the REAL span (“the last ~3 hours”) instead of the period label. The cause is recorded as ONE reason so the dashboard reads the ceiling signal alone: ceiling = the window held more than the per-period message ceiling (the fetch asks for one message past the ceiling, so a cap is only recorded once a further in-window message actually exists — never inferred from how close the span looks); rate_limit = a global Discord 429 cut the history walk short (recent_messagesinterrupted signal); size = the rendered blob overflowed the token budget (_MAX_BLOB_TOKENS, which drops the oldest messages — and clips a single oversized message — so a transcript-heavy channel degrades instead of hard-failing on a context-length error). Only a high ceiling rate on 1d/today says the ceiling is too low (the signal behind the “/recap today only recapped the last hour” regression fix); a size/rate_limit cap is a different problem. Dashboard: the recap_cov panel on the ask/conversation board breaks the rate out by cause. Not an error (a capped recap is honest, just partial), so no monitor.                                    
recap_deflected cogs/recap.py guild_id, user_id, period, channel_id, channel_name, reason (no_permission/no_messages), can_read_history, total_messages                                    
discourse_fallback cogs/discourse.py guild_id, user_id, category, source_count, recent_topic_count, reason                                    
wire_read utils/wire_sources.py (gather_wire), called by cogs/discourse.py and cogs/music_desk.py surface (discourse | music | music_desk), guild_id, category, count (handles asked for), kept (posts returned), ok, duration_ms — the direct read of the CODE-OWNED wire lists that a compose UNIONS with the guild’s feed channels (#wire), so a server with no MEE6 mirror still has linkable material. count is the breadth the surface’s tunable allowed (discourse_wire_accounts / music_wire_accounts; 0 = the direct read is off for that guild, and no event fires at all). The music DROP deliberately does NOT read this (PR #2202): a live A/B showed news material pulls the compose onto the news subject and it fabricates a track, so surface never carries music. The wire desks’ breaking EVENT poll (cogs/event_poster.py, #news-breaking-speed) also emits this kind with surface = the desk and category = breaking — the same fail-open read of the head wire handles, on the aligned 15-minute poll. The fail-open tripwire is kept: the fetch never raises (a dead handle, an exhausted twitterapi.io balance, and a missed deadline all return []), so a kept=0 with count>0 across many composes is the SILENT degradation to watch — the surface keeps posting off the feed channels alone and nothing errors. duration_ms covers the whole concurrent fan-out, not one handle — and is near-zero on a warm cache, since the desks’ own fetches serve most composes. The kind is surface-neutral on purpose: one shared integration read by three surfaces stamps surface rather than splitting into per-surface kinds, so one panel and one health branch cover all of them. Ops-monitor health: rolls into misc_health["wire_read_<surface>"] — keyed PER SURFACE so a healthy discourse cannot mask a music read that has gone dark — and scored on kept>0 rather than on ok for exactly the reason above.                                    
discourse_scored cogs/discourse.py guild_id, channel_id, score, reason, must_post, category, user_id, post_preview; on category="trending" also (#1272 silent-degradation tripwires): shipped (bool — the trending-clip reaction cleared its high solo bar and won the slot vs fell through to a news take), platform (the clip she reacted to), framed (# pool clips that carried a vision cover frame — a framed=0 while TikTok is in the pool means the frame plumbing broke, e.g. TikTok changed its cover-URL shape or Anthropic stopped fetching tiktokcdn, so she’s back to reacting caption-blind), pool (the balanced platform mix, e.g. instagram:2,tiktok:2,youtube:2, confirming the per-platform balance holds). #trending-visibility: it ALSO fires on the PRE-score DECLINES (shipped=False, score=0.0, reason=no_clips|model_declined|error) so EVERY trending attempt is countable — the ops-monitor renders a “Surface decisions” line (news vs trending shipped + the decline breakdown) and flags trending_dark when trending is attempted repeatedly but never ships on a fixable reason (error = the GPT-vision Responses 400 that took it dark; no_clips = the gather finds nothing on-theme; model_declined is by-design restraint, never flagged). The GPT-vision failure now degrades caption-only in react_to_trending (drop the frames + retry once) rather than falling through to a news take every slot.                                    
discourse_dedup cogs/discourse.py guild_id, channel_id, decision (similarity_gate), signal (same_link/text_similarity/shared_run/content_overlap), post_preview                                    
music_desk_scored (any phase, shipped=false) – the COOLDOWN NOTE cogs/music_desk.py (2026-09-06) no new event: a shipped=false row from _compose_board_unit, _milestone_candidate or _compose_record_unit now also stamps fail:<dedup key> in music_desk_events, and every lane skips that key for MUSIC_DESK_FAIL_COOLDOWN_HOURS (12). So a pick that fails reads as at most ~2 rows a day instead of one per slot (before: one radio board 165 rows in 7 days, one record card 61, one chart-peak card 73). Read a lane’s failure rate as dcount(matchup) alongside count() – the same failing pick no longer inflates the count. A skip emits nothing (it is derived state, and a per-slot skip row would re-inflate the very count this fixes)                                    
music_desk_scored phase=shape_reject cogs/music_desk.py guild_id, reason, matchup (the lane), post_preview — a composed line DROPPED by a deterministic guard before the self-gate. reason=self_correction means the response showed the model redrafting in the open (a draft, then “Wait, I can’t say that”, then another draft — the 0.6 gate scored that 0.90 and was right to, since it grades whether claims are GROUNDED, not the SHAPE of the response). reason=career_claim means a bare-row lane (chart / radio / called_shot / reconciliation / watch_chart / debut_sales) ranked a release against the artist’s other work, which its block can never support; measured over 40 hours of real posts, 2 of 38 desk posts leaked one from the chart lane and both reached the room. NOT applied to reveal / milestone, whose context stack can genuinely ground a catalogue fact. reason=decline_narration is the chart-POSITION lanes’ second shape rule (owner steer 2026-09-05 “never call it a slip just say where it is”, then 2026-09-06 “never say sliding down, big fall, dont narrate the downwards movement just say where it is”): a take on chart / watch_chart (the desk’s _POSITION_LANES, via _unshippable) or the newsroom’s chart lane (music_news_scored, via ship_guard.drop_decline_narration) narrated the DOWNWARD move (“sliding down from its #8 debut”, “drops four spots to #16”, “went from #8 to #16”) instead of stating where the title sits; the matched wording rides detail.phrases. It exists because the judge grades whether a claim is TRUE and a narrated decline usually is: the 2026-09-06 leak scored high for its “grounded context (debut peak included)” and shipped to X. Not on chart_exit, whose story is the departure. Measured on the real inputs with the real model, n=6 per arm: the old newsroom framing leaked 5/6, the cause reword 0/6 with or without a banned-word list (the list was cut as scar tissue), and the final framing plus the chart lane-hint reword 0/8, so expect a rate near zero; a sustained rate on one lane means that lane’s framing lost to its own context (the newsroom hands the model the debut and peak in its BACKGROUND block, which is where the path narrative comes from). The desk_shape_reject panel groups by reason, so the new reason appears with no dashboard change; no monitor, for the reason given below – a guard firing is the guard working. The board-shaped lanes (standings / chart_board / chart_proj) take only the ALL-TIME half of that check (has_alltime_claim): a ranked board hands the model every position an act holds and names the highest, so “her highest entry sits at #13” is read off the block, while “her highest-charting album yet” is still invented. reason=attribution_tag is the publication-card lanes’ credit guard: the take names its source in the banned press-release form (“, per HITS” — has_attribution_tag) and survived the one name-the-phrase re-ask (_detag_credit); the dry run measured the raw leak at 5/5 HITS-credited takes and the re-ask cleared all 5, so a nonzero steady rate here means the re-ask stopped working. reason=row_tally grounds rather than bans on the music desk’s board lanes (#2662): the guard rejects a count of a board’s rows because the block hands over each row alone and never a total, but the STANDING board states its top-10 count in the block and leads with it. Measured on four real composes, all four matched the flat ban and would have died before the judge, so the board would have shipped nothing. _drop_before_gate now takes the block and ungrounded_row_tally passes a tally the source states; a count it does not state still drops, and every lane that passes no source is unchanged. A rising rate here on watch_board means the compose started naming a top-10 count the board never handed it. reason=row_arithmetic is the board lanes’ own guard (#2031) — a take that SUMS the board’s rows (“more than the next two combined”). The board gives each act its own number and never a total, so a combined claim is arithmetic the model did itself, and the live dry run caught it doing one that was false (11 spots “more than Drake and Ella Langley combined”, off rows reading 7 and 5) while the 0.6 judge scored it 0.92 and printed the arithmetic in its own reason. Same shape as career_claim: every INDIVIDUAL number is grounded, so only a deterministic rule catches the derived claim. reason=self_correction now rides THIRTEEN more surfaces (#2755 + its class sweep). The checker was always shared, but only three cogs CALLED it – music_desk, cinema_desk and sports_boards, each through its own private _unshippable. Every other surface that composes model prose and cross-posts it to X ran no shape check at all, so a self-correction on any of them shipped. utils/ship_guard.py is the one wiring home, and it deliberately emits on the CALLER’s own <surface>_scored kind with this same phase=shape_reject reason=self_correction shape, so market_drop_scored, music_news_scored, pop_desk_scored, music_alert_scored, sports_desk_scored, cinema_news_scored, bet_alert_scored, bet_board_scored, bet_value_scored, discourse_scored and music_scored join the desk_shape_reject panel and the ops monitor’s NON_SCORE_PHASES rule with no dashboard change and no new field against the budget – every field it writes is already in SHARED_FIELDS. matchup carries the lane, or lane: subject where the surface tracks one. TWO cogs are deliberate exceptions and do NOT appear on that panel, because neither has a <desk>_scored event for the helper to emit on. x_mentions counts its declines by reason on the single x_mentions event, so it calls the checker directly and emits reason=self_correction there beside row_tally and declined. market_alert has no *_scored event at all, so each of its three compose lanes records the drop on the event that lane already uses: market_filtered reason=self_correction on the main alert lane (the same pre-ship drop event its #2547 number backstop uses), rt_reconcile on the reconcile lane, market_outcome on the outcome lane. Watch all three – a gap in one lane is the whole bug. discourse runs FOUR compose lanes and the ICEBREAKER is the fourth – its drop rides discourse_icebreaker reason=self_correction, not the panel, because that is where that lane’s other drops already are. discourse_scored DOES ride the panel, but note its drop does not silence the slot: a self-corrected line takes the same fallback path an empty compose does (discourse_fallback reason=self_correction, then the quip or the icebreaker), so the shape reject and the fallback are two events about one moment. Expect a rate near zero: over the full 30-day population of 10352 real composes on the helper-wired surfaces the guard fires 5 times. Four are market_drop and were already stopped downstream. The fifth SHIPPED, and it is the reason the class sweep mattered: on 2026-08-16 a discourse take carrying “Wait, let me pull the real link.” between two drafts scored 0.78 against the 0.6 floor and crosspost surface=discourse fired four seconds later. The one surface with a measured live leak was the one #2755 did not list. Read a sustained rate the same way as on the desk – a lane’s framing losing to its own context. This rate was NOT observable before the change, so the first weeks of it are a baseline being established, not a regression. The one shape to watch is a take that OPENS with a marker word. The line-leading arm anchors on ^ as well as a newline, so a take whose FIRST word is wait / hmm / correction / scratch that / hold on / on second thought / actually, no is read as a self-correction. On a chart desk that never happens; on the CONVERSATIONAL surfaces this change newly covers (discourse, music) a rhetorical “wait, …” opener is much closer to the persona’s real voice. Measured before shipping: an adversarial set of 20 hand-written legitimate lines flagged 8, but zero of the 10352 real composes in the 30-day window open with a marker word, so the risk is real in principle and has no observed incidence. If a compose-prompt change ever makes her open that way, this guard starts eating good posts and the tell is a shape_reject rate that climbs on discourse_scored / music_scored while the desks stay flat. No monitor, for the reason stated below: a shape_reject firing is the guard WORKING. reason=metric_word:<word> (#2161) is the same shape on the ALBUM-METRIC axis, and it is the one shape_reject reason music_alert_scored also carries (phase=shape_reject, the alert cog’s first use of this marker). One album carries a Pure Sales ladder AND an Album-Equivalent Units ladder at the same time, and the two figures differ by an order of magnitude — live on 2026-08-08 the Karol G album read ~3.1K pure sales and ~45.8K equivalent units in the same tracking week. A take that quotes its number exactly as given but names the OTHER metric (“3.1K units” off the Pure Sales ladder) reads as fully grounded, so the 0.6 gate ships it — it scored that exact take 0.72. output_checks.metric_foreign_word keys off the READING’s metric rather than the lane, which is why it sits beside _unshippable instead of inside it (_drop_before_gate only ever sees the story name). Measured against the 34 real shipped album-market posts in music_alert_state: it flagged the 2 wrong ones and left 32 alone. The desk_shape_reject panel (added in the same change) is where every reason on this phase is graphed, split by surface — until then nothing graphed shape_reject at all: desk_quality filters it out as a non-verdict and desk_declined counts it in neither column, so a take the model DID write and a guard refused was the one compose outcome with no dashboard home. No MONITOR yet, deliberately: a shape_reject firing is the guard WORKING, and there is no calibrated rate that separates “the guards are earning their keep” from “a prompt regressed” — the panel builds that baseline first. Expect a rate near zero — the live rate was 2 of 15 pure-sales alerts, so a SUSTAINED nonzero rate means a compose prompt started conflating the two metrics again, and a jump to most pure-sales posts means the metric label stopped resolving (check luminate.event_metric). A rising rate means a lane’s framing is losing to its own context. reason=ungrounded_number (#2081) is the same shape on the other figure surfacesmusic_news_scored, cinema_desk_scored, music_desk_scored (every number lane, #2543/#2559), music_alert_scored (both compose paths, #2547), market_drop_scored (#2547), sports_board_scored, sports_desk_scored, pop_desk_scored, bet_alert_scored, and (as market_filtered reason="ungrounded_number", its existing pre-ship drop event) market_alert (#2547) carry it, under the lane’s own phase/kind rather than shape_reject, with the flagged values in detail.numbers. On the music_desk REVEAL/tracking lane there is a SALVAGE step before the drop (_reask_ungrounded, owner call 2026-08-25): a production check found good debut cards (ENHYPEN, Stella Lefty) suppressed slot after slot because the model kept adding an invented comparison (“ahead of the 110K the chart accounts project”), so the lane now re-asks ONCE – naming the exact figure and asking for the same read without it – and ships the retry only when it comes back clean AND still carries a MARKET-SCALE figure the draft had (a grounded value >= 1000, so the retry cannot drop the headline units/streams number and ship a numberless take beside a number card; #2571 review) (verified salvaging 5/5 on a real-model dry run). A successful salvage emits its OWN event – music_desk_scored reason="ungrounded_salvaged", shipped=True, the flagged figures in detail.numbers (#2571 review round 4) – so the compose-fabrication rate the salvage fixes stays visible to the ops monitor instead of being masked. A music_desk_scored reason="ungrounded_number" on the reveal lane therefore means the re-ask ALSO failed, so a nonzero rate there is now a stronger signal; a rising ungrounded_salvaged rate means the compose prompt is inventing figures more often even though the card still ships. The market-family surfaces (market_drop / market_alert / betting_alert) and the desk surfaces ground through the shared output_checks.numeric_grounding — the source plus a bare copy of every digit-run it holds — so a compact token (“24h” paraphrased as “24 hours”, a milestone “50th” written as “50”) is not a false-positive drop; market_drop/market_alert also add the market title/topic. market_alert + betting_alert deliberately WITHHOLD the prior price from the blob (#1140), so the backstop there also deterministically enforces “no movement number the model was not handed” — a take narrating “cratered from 50%” drops. A real-model dry run over all five surfaces flagged 0 false positives. It fires when the composed line states a number the ground-truth block does not (output_checks.ungrounded_numbers). It exists because a fabricated figure that SHIPS is the one failure this ledger cannot otherwise report: it shipped because the judge scored it HIGH, so there is no low score to flag and no error to triage, and no after-the-fact check is possible either – these events carry post_preview but deliberately never the ground truth (data minimization), so the comparison exists only at compose time. Measured against the real #2069 and #2076 fabrications it caught 4 of 4, and against the real posts that shipped in both dry runs it flagged 0 of 13. A NON-ZERO rate is the interesting signal here: it means a compose invented a figure and the judge would have been the only thing standing in front of it. reason=career_ordinal (#2154) is its words-not-digits twin, on music_news_scored only, with the flagged counts in detail.career_ordinals. ungrounded_numbers reads DIGITS by design, so a career tally written as an ordinal was invisible to it – and the #2152 re-roll actively SELECTED for that phrasing by rejecting every digit-carrying take, which is how “Cardi’s fourth Diamond single and Megan’s first” shipped to X at a judge score of 0.92. The check is GROUNDING, not phrasing (output_checks.ungrounded_career_ordinals): a wire block that states “her sixth consecutive #1” grounds the take’s “sixth”. That distinction is load-bearing – banning the possessive-plus-ordinal SHAPE was measured against 900 real shipped takes and would newly block 23, all but the WAP post true and sourced, repeating the mistake has_alltime_claim had to undo in #2031. Shipped as a MEASUREMENT first and promoted to a gate on its own rule: it flagged 1 shipped take in 74 over 30h (~1.4%), the low-rate outcome that makes the gate cheap. Like the digit guard it marks the post SEEN, so the story is not re-rolled until a take omits the count. Expect a rate near zero; a sustained one means a compose lane started inventing career tallies. phase=chart_run is the per-release chart-run milestone that replaced a long-runners board (#2031): measured on the real Billboard 200, that board’s top ten was 10/10 identical week to week, so it was cut in favour of a rung that fires once per release. It rides the shared songstats_milestone_state store under a bbrun: entity prefix with metric=chart_weeks, so no new table; a rate of ~3/week across the Hot 100 + Billboard 200 is normal, and 0 for weeks on end means the seed pass never completed. phase=chart_board / phase=debut_sales / phase=chart_proj are the PUBLICATION card lanes (owner steer 2026-08-06, utils/chart_boards.py): the weekly top-10 + debut-sales cards, the individual first-week-units cards, and the four daily projection boards. They ride this same event with no new fields and join the per-lane lane_dark health automatically (music_desk is in LANE_PHASE_SURFACES). Expected cadence: chart_board ~4/week (three top-10s + one sales board), chart_proj 4/day (the HITS variants, each once daily, rotating least-recently-posted first; the owner’s “weekly and daily ofc” steer) — a silent chart_proj day means the HITS read is dark (check hits_fetch) or the document has no chart date. phase=lifetime_board is the LIFETIME record-boards lane (owner ask 2026-08-19, utils/lifetime_boards.py): the evergreen career-tally leaderboards (“most Rhythmic Airplay No. 1s”). It rides music_desk_scored/music_desk_posted with NO new event or field — the desk ship-rate and lane_dark panels group by phase and pick it up automatically. Its data is CURATED (a cited registry, guarded by lifetime_boards.validate), so there is no fetch to fail and no fail-open integration to watch — no dedicated MONITOR is warranted; the shared desk ship-rate covers a compose regression. Expect it QUIET: at most one board a slot, and a board posts ONCE on first sight, then never again until its RANKED ROWS change. The lane keeps a DURABLE per-(guild, experiment-stage) signature — a hash of the ranked artist:value rows (lifetime_boards.signature, which deliberately EXCLUDES as_of, so a date-only refresh does NOT re-post) — in kv_cache, and skips a board whose stored signature still matches. A real change (a new figure, a new leader, a new act) is a new signature and posts again. The state is written from the successful-delivery / intentional-dedup path, not at compose, and the STAGING stamp is separate from the PRODUCTION one, so a #bot-logs audition never suppresses the room after promotion. It is on _BOARD_LANES for the row-sum/row-tally guards but EXEMPT from has_alltime_claim (_ALLTIME_EXEMPT_LANES), because stating the all-time record IS the surface — the same carve record_card gets, but this board keeps the row checks.                                    
music_desk_scored phase=riaa_plaques reason=plaques_twin cogs/music_desk.py (2026-09-05) guild_id, shipped=false, score=0, matchup (the skipped act), detail{chart, twin} — a per-act NEW PLAQUES board skipped BEFORE compose because its signature already shipped under ANOTHER credited act of the same collaboration batch. awards_by_act hands every act on a shared credit the same rows, so “Intro” 7x + “Ganga” 4x boarded as Peso Pluma at 23:04Z on 2026-09-04 and again as Tito Double P at 00:02Z, one signature under two act keys; only the text dedup stopped the twin. The board now keeps a second fire-on-change state keyed on the signature alone (<guild>:<stage>:riaa_plaques:sig:<sig> in lifetime_board_state, value = the chart that shipped it, written on the same confirmed delivery or dedup close-out as the act’s own key). detail.twin names that chart. Expect it rare (a collaboration-only batch between two recognised acts); a steady rate is the guard working. The SAME night also showed the other way a plaques board repeats: #2957 renamed its chart key (riaa_plaques:<act> -> :single), so every open batch read as never posted and Nickelback’s board went to the room twice (15:07Z and 02:08Z, the same signature under both keys). No event marks that class; the tell is a music_desk_posted phase=riaa_plaques for one act twice inside the 3-day window with no new award between them.                                    
music_desk_scored phase=pollstar_board / phase=radio_format_board reason=stale_chart cogs/music_desk.py (2026-09-06) guild_id, shipped=false, score=0, matchup (N of M boards, dated <oldest chart date>), detail{charts (the stale chart keys), age_days} — ONE row per Pollstar lane per slot when a board’s chart is older than pollstar_boards.MAX_CHART_AGE_DAYS (10); the boards yield BEFORE compose. This is the freshness signal for a feed that stands still: pollstar_fetch reads ok=true because the data arrives (it is just old), so without this row a stuck upstream looked like a lane that was merely deduped. Measured 2026-09-06: every chart at Aug 14/17 for three weeks, and “this week” boards posted off the Aug 17 chart on Aug 30-31. A stale_chart count that persists across days USED to be read as “the upstream, not us; the lane resumes on its own when the feed moves”. That was wrong, and it cost 15 days of two beats (#3357). The lane could not resume: Pollstar publishes every weekly issue under its OWN chart id and never updates one, so the pinned registry ids kept serving 8/14 and 8/17 forever at HTTP 200. The reader now resolves the current issue off each chart’s config endpoint, so a sustained stale_chart count means the upstream genuinely has not printed. It is also no longer silent: the rows are counted by the ops monitor’s lane_stale_upstream (high) finding, and they are excluded from judge-quality stats (NON_SCORE_REASONS) — left in, 304 markers a week pulled the music desk’s measured mean from 0.83 to 0.74                                    
music_alert_scored / music_alert_posted phase=artist_market / phase=artist_market_move / phase=artist_market_leader / phase=artist_market_settle cogs/music_alert.py the alert’s ARTIST-MARKET lanes (#2800 line E, #2833), riding the alert’s own scored/posted events with no new field. artist_market is a NEW Kalshi event market about a watched act (listed inside 10 days, 50+ contracts, the leader priced 5%-95% by the shared kalshi_price rule); artist_market_move is the biggest week move on such a market (15+ points, 300+ contracts, off Kalshi’s public daily candlesticks read in ONE batch call per event – market_fetch query=candles_batch:<n>). Both ride the music_artist_markets experiment (STAGING by default: delivered=staged in #bot-logs) and dedup on Kalshi tickers in post_dedup_history (music_alert_artist_market, the event ticker, 1 year; music_alert_artist_market_move, <market>:<up|down>, 30 days; music_alert_artist_market_leader, <event>:<hero label>, 30 days), never the song reuse block. artist_market_leader is the STANDING card, the companion to a move: a shipped move post writes its board to the QUEUE (music_alert_artist_market_leader_queue, <event>:<moved market>, 5 days) and the standing lane – registered BEFORE the move lane, so it reads only an earlier TICK’s queue and the card lands on the NEXT SLOT, 10 minutes on – reports what that board says now. It withholds on a board whose leader is the leg the move card already reported, on a board that has left the open catalog or that the listing lane posted in the last 3 days, on a board whose raw read came back a full page (truncated, so unrankable), on a standing this board already reported last, and on any leader that is not BID-SUPPORTED. A cumulative DATE board (kalshi_ladder.is_date_ladder) states the soonest window, the first even-money window and the widest one instead of a bare ‘leader’. Expect each QUIET: at most one card a day per lane per guild, the standing lane only ever follows a board the move lane already posted, and the move lane fires only when a market about a watched act actually moves (measured 2026-09-02: 3 of 168 open act markets moved 20+ points in a week). A music_alert_scored row with phase=artist_market_move and a preview naming a price the blob did not state is the ungrounded_number guard’s job; a day with neither phase is normal. music_alert is NOT in ops_monitor.LANE_PHASE_SURFACES, so neither lane joins the per-lane dark rule; the silent-failure tell is market_fetch ok=false on query=candles_batch:* or raw_markets:*, which the integration-health rates already count. artist_market_settle (#2800 line E): the follow-through on a market the listing lane announced – its every market finalized/settled inside 7 days, the YES outcome (or a single market’s NO) as a big-number card; the roster is the listing surface’s event tickers (held a year), each reported once on music_alert_artist_market_settle (permanent), at most 12 event reads a tick; a resolved market whose title names no watched act is skipped. Expect it rarely (a listing resolves months later), so a quiet lane is normal while artist_market posts are young.                                  
music_desk_scored / music_alert_scored reason=ungrounded_age (score 0.0, shipped=false, any lane) cogs/music_desk.py + cogs/music_alert.py _drop_ungrounded (2026-09-06, owner ask “cut those last sentences on age of the record”) guild_id, phase (the lane), matchup, post_preview, + folded detail.claim (the phrase). The age-in-WORDS twin of reason=ungrounded_number (#2081): output_checks.ungrounded_age_claim flags a take that states how old the release is (“thirteen years deep”, “a track over a decade old”, “sixteen years after its release”) when NONE of its sources state an age. The digit check already dropped “14 years old”, so the model wrote the age in words; 20 watch-chart takes and 2 Spotify jump posts shipped that way in the 30 days to 2026-09-06, every one off a kworb row that carries no release date. PERMISSIVE by design: a source that states any age in any form (the Luminate head’s catalog, out 4.2 years, a dated debut blob’s came out 18 days ago, an Anniversary edition title, the oldest-songs board) stands the check down – it checks that the source spoke to age, not that the two ages agree. Measured over 30 days of shipped takes (n=5774): the claim pattern matches 67, of which the tracking + lookup_board lanes (18) are grounded and stand down; the rest (watch_chart 18, chart_run 15, milestone 12, chart 2) now drop. Root cause fixed in the same change: the shared _CONTEXT AGE / STAGE paragraph asserted “the head names the release’s milestone” as a fact about every post, and the chart lanes have no age in their blob, so the model supplied one (reworded to a condition; real-model n=6 dry run 3/6 -> 2/6, so the guard carries the rest). In NON_SCORE_REASONS with its twin, so it never reads as a judge score. Expect a low steady rate; a SUSTAINED rate on one lane means that lane’s blob should state the age (or the prompt regressed).                                    
<surface>_scored phase=declined utils/events.py (emit_decline), called by the five wire desks (cogs/pop_desk.py, cogs/sports_desk.py, cogs/cinema_news.py, cogs/music_news.py, cogs/cinema_desk.py) and, since #2086, by cogs/music_desk.py, cogs/market_drop.py, cogs/music_alert.py and the three betting surfaces guild_id, phase (declined), shipped (always False), score (always 0.0), reason (model_declined empty_after_link_strip too_short), matchup (the subject, else the lane), and on too_short also post_preview + chars — the compose model DECLINED to write a take, so nothing shipped (#2051). too_short is the one reason here that is a DEFECT rather than editorial restraint (#3160). It is emitted by the SPINE (cogs/scheduled_poster.py), not by a desk: the compose stopped mid-word and returned a fragment, and utils.output_checks.too_short_to_ship dropped it before the delivery walk. The floor is 15 visible characters, measured — over 30 days of shipped posts the shortest real scheduled post is 22 characters (“Clairo turns 28 today.”). A post carrying a LINK is exempt at any length, so the three curator surfaces, which ship a bare fixup link as the whole post, are untouched. The cause: on 2026-09-11 compose_pop_take returned the two characters “EM” (a cut-off EMPTY decline). is_empty_response compared against the whole sentinel word and passed it, and pop_desk_score scored the fragment 0.62 — over the 0.6 ship floor — with a stored reason that graded the SOURCE wire headline, because the take gave the judge nothing to grade. The fragment shipped to the pop channel and crossposted to X. The judge is not a reliable guard on this input: re-run live on the same fragment and blob it returned 0.2, so the same input scores either side of the floor. That is why the floor is deterministic. Since these ride the existing *_scored kind, the desk_declined panel and the compose_declined finding cover them with no new event kind and no new field names; compose_declined flags any reason other than model_declined/all_seen, so a run of too_short raises on its own. A decline is a real editorial decision: the model returns EMPTY for a charged or out-of-lane moment, or one with nothing to state. It used to be the ONLY outcome in the whole funnel that left no trace — the self-gate emits *_scored, the repeat check emits post_dedup/topic_dedup, the staleness judge emits news_stale, delivery emits *_posted, and this path just returned None. So “the desk never saw the story” and “the desk saw it and passed” looked identical from telemetry, and they need different fixes; that gap is what made the missed Drake posts (#2033) hard to diagnose. Rides the surface’s own *_scored event rather than a new kind, the same shape music_desk_scored phase=shape_reject already uses, so it costs nothing against the field budget. It is NOT a judge scorescore=0.0 here means “there was no take to grade”, so ops_monitor.NON_SCORE_PHASES excludes it and the desk_quality panel filters it out; counting it would report a surface that DECLINED as a surface whose writing is bad, and at a high rate would trip surface_dark and point triage at the judge instead of the compose prompt. surface_dark cannot cover this class at all: it reads judge scores, and a declined take never reaches the judge, so a desk refusing everything shows no scores and reads as a quiet news day — which is why the ops monitor gained its own compose_declined finding (medium, at 60% of at least 6 attempts; deliberately loose, because some declining is correct and only a lurch is a signal). Watch the desk_declined panel: a decline rate that jumps after a prompt change means the compose prompt started refusing real material. #2086 closed the class: the music desk’s nine lanes read is_empty_response(line) or self._unshippable(...), and or short-circuits, so an EMPTY response emitted neither this marker nor shape_reject — silent on the surface that scores more rows than any other. All nine now go through _drop_before_gate, which keeps the two drops as SEPARATE events because they need different fixes: declined means the model refused to write (fix the prompt or the material), shape_reject means it wrote something a deterministic guard forbids (fix the guard or the framing). bet_alert_scored joined ops_monitor.SCORED_KINDS in the same change — it was the one *_scored kind missing while both its siblings were in, so betting_alert also gains the self-gate quality monitoring it lacked. Still NOT emitting: cogs/market_alert.py (it has no *_scored event to ride) and the commentator / discourse opener paths (they carry their own decision events).                                
music_news_chart_lookup phase=first_week cogs/music_news.py ok (a projection was settled), phase (first_week), matchup (the subject), + folded detail: charts (hits_projection), chart (the document: midweek-20 / hits-top-50), pos (the projected rank), reason — the newsroom SETTLED a first-week projection claim against the HITS Daily Double document (#2049). The desk already read this document first-party for the called-shot lane, and it is cached, but the newsroom could not reach it — so a first-week projection was the one declared story kind the settler had to relay on account trust, and it is the kind most likely to be wrong, because a projection MOVES through the tracking week. A wire quoting Tuesday’s midweek number on Friday quotes a number the document has since changed. Settles on UNITS rather than a position, so it carries its own reading (ProjectedUnits) rather than a CHART_SOURCES row; latest_projection() is the same call the called-shot lane makes, so both lanes read one number. The extra refusal reason is not_first_week, and it is the WRONG-WEEK guard: an album stays on the document for many weeks, so matching on title and artist alone answers a first-week question with a later week’s number. Caught in the dry run on the real midweek document for 2026-08-15 — Olivia Rodrigo’s album sat at #3 with 75,000 units and last_week=1, while its actual first week read 79,863 on the building chart for 2026-08-08. A sustained not_first_week stream is the guard working. A sustained absent here means the document is not carrying what the wires talk about. STREAM claims settle here too (#2771). #1890 wired kworb into the CHART branch and left the MILESTONE branch reading the open web, where both legs (Perplexity + Grok) inherit aggregators that LAG Spotify – so a TRUE claim came back refuted. A wire said Rihanna had 8 songs over 2 billion Spotify streams; the judge answered “her highest is ‘Stay’ at 1.972B” and the story was dropped three times, while the artist page the bot already fetches ~20x a day listed exactly 8 over the rung, Stay at 2.027B and her highest at 2.648B. Drake’s “140 billion” was dropped four times over four days against a leaderboard reading 140,255,100,000. Rows carry phase=milestone, detail.source=kworb_streams, plus shape (catalog track career), rung, claimed, count and total. ok=False is NOT a refutation – it means the page was read and the claim did not bear out, which happens legitimately when a wire count is one ahead on the day a song crosses, so the reading is dropped and the web path decides. Scoped to a claim naming BOTH Spotify and a stream metric, so an Apple Music or YouTube claim is never answered with Spotify numbers. Watch the RATE of ok=True on this phase against music_news_filtered reason=refuted: the fix is working when refuted-stream drops fall and this rises; both flat means the settler is not being reached and the claim shapes have moved.                                
music_news_chart_lookup phase=chart cogs/music_news.py ok (a position was settled), phase (chart), matchup (the subject), + folded detail: charts (the chart keys read, comma-joined – charts_named off the claim’s own words), chart + pos (the settled reading), reason (ambiguous / absent / unresolvable / unavailable), is_debut (the DEBUT verdict off chart_presence for a live platform chart: false = established title, true = genuinely new, null = weekly chart / unknown title / memory down -> UNKNOWN, and since 2026-09-01 unknown no longer means the wire’s framing stands: the compose’s debut guard asks Billboard’s artist chart-history page and reports the bare position when that cannot confirm a debut either, see music_news_debut_framing) – the newsroom SETTLED a chart claim against the live chart before web research (#1890). The is_debut verdict stops a wire’s loose “debuts at #N” being relayed for an established title (ICEMAN by Drake, days on the Apple albums chart, shipped as “debuts at #2”): _settle_debut_framing reads the shared memory and, on false, hands the compose established_framing_note so a climb is told as a climb. charts covers EVERY chart we can read since #2258 (owner steer 2026-08-10, “should work like that for every chart”): the kworb platform dailies, all 21 Billboard charts, and the ten Mediabase radio panels (keys radio_*, read off the same hits.radio_building() document the desk’s radio lane uses). The table held 7 charts before, while the process already fetched ~38, so a claim about any of the other 31 kept the wire’s THRESHOLD wording – live on 2026-08-10, “enters Top 20 on US Rhythmic Radio (Mediabase)” shipped for Mac Miller and Ty Dolla $ign’s “Cinderella” against a real #19, and “Top 20 on US Urban Radio” shipped for Future’s “California Girls” against a real #14. WATCH the ok-rate by charts: a radio_* or Billboard key with a sustained absent means our title matching is missing that reader’s credit style, NOT that the songs fell off – those panels are weekly, so the staleness gate deliberately ignores them. A rising unavailable on radio_* is a HITS fetch problem.                                    
music_news_filtered cogs/music_news.py guild_id, phase (story kind), reason (not_a_story = the classifier’s settled no-story verdict, stamped seen; shortlist_full = the compose shortlist filled with count candidates still unclassified behind it – the newsroom’s one formerly silent loss, #2800 line D; ladder_rung/refuted/unverified_wire/chart_cut/cert_reported/milestone_reported/chart_debut_reported/chart_repost/novelty_distribution/lane_dark/charting/nochart_unverified; cert_batch_deferred = an act with more RIAA awards in the 3-day window than its cards per act per slot share: matchup the act, count the awards waiting for a later slot (not stamped seen; they age out of the window), detail.per_act the knob, detail.headline the act’s best; owner call 2026-09-03: diversity across acts, not a cap), the desk’s plaques board lists them all; not emitted when nothing is left to fold; cert_act_cooldown (2026-09-05) = the act took a cert card inside cert_act_cooldown_hours (detail.cooldown_hours, the /menu knob, default 24, 0 = off) so it sits this slot out: matchup the act, count the awards held (not stamped seen), detail.headline its best. The per-slot share alone let a catalog batch take one card of EVERY slot: Nickelback, 2026-09-04, 15 awards, one card composed an hour for twelve hours, five reached the room and six were stopped only by the text dedup. Watch it next to cert_batch_deferred: an act should appear under cert_batch_deferred once, then under cert_act_cooldown for the rest of the day; the same act under cert_batch_deferred every slot means the cooldown came unwired), matchup, source, + folded detail (claim, note, family, key, pos, prev, chart) — a wire numbers story was DROPPED before compose. The three no_chart reasons (phase=no_chart, #top-100-debuts-no-chart) fence the one lane that states a NEGATIVE about a real act — a tracked artist’s named release that missed a chart. reason=lane_dark = the guild has not opted the lane in: its own music_news_nochart experiment is OFF (the default), so the story is dropped and marked seen. A steady stream here is the lane being dark as designed, not a fault; it goes to zero for a guild once a mod flips the experiment on. reason=charting = the VETO fired — we read the named Billboard chart first-party and the release IS on it, so the “did not chart” claim is FALSE and nothing ships (detail.pos/detail.chart name where it actually charted). This is the “prefer absent over invented” fail direction inverted for a negative claim: a false miss is the fabrication to fear, and a first-party read that finds the song kills the story. A hit here is the veto working; marked seen. reason=nochart_unverified = the lane is FAIL-CLOSED — a no_chart story ships ONLY when the independent research CONFIRMS the miss (verify_music_claim verified=true), never on source trust like a positive number, because you cannot prove an absence on trust. NOT marked seen (the tracking week is still closing, so the research may confirm it a slot later). Watch the three together as the lane’s whole funnel: lane_dark should dominate until a guild opts in, then nochart_unverified should be the main drop (most feed “flop” chatter is an insult with no verifiable miss) and a charting hit means the veto caught a wrong claim. reason=unverified_wire (#2040) means the story came from a music WIRE — an account that REPOSTS numbers rather than measuring them (may_relay_number false) — and the research did not confirm the figure, so it is dropped rather than relayed. That is the standing rule for a wire’s hard number: verify-or-drop. It is NOT marked seen, unlike a refutation, because “the web has not caught up yet” is a state that changes and a wire often posts a number ahead of the outlets. An AUTHORITY’s unconfirmed number is a different case and still ships — she measures it first-party. Watch it as a share of the wire count in music_news_slate: a rate near 100% means the widened poll is spending verify budget on accounts whose claims the web never carries, and the account list should shrink rather than the rule loosen. reason=refuted (#2035) means the verify judge reported a DIFFERENT number for this claim, so nothing ships. The judge verdict used to be one boolean and the desk read every false as “we could not corroborate it”, which RELAYS the wire’s number in her own voice; that is right for a claim the research does not cover and wrong for one the research CORRECTS. Live: the wire said Drake had 6 songs in the top 11 single-week stream list, the judge answered “7 distinct songs, not 6”, and the post shipped reading 6 on Discord and on X. The self-gate cannot catch the class — it grades the take against the claim, and the claim is the wrong number. A hit here is the guard working. Watch it as a SHARE of music_news_verified verified=false, not on its own: only the contradicted subset drops (~1 in 4 of the false verdicts, by a 30-day read; the rest are “no corroboration found” or “that figure is a projection” and still ship). A refuted rate climbing toward the whole false population means the judge is calling missing corroboration a refutation, and the desk is going quiet instead of getting accurate. reason=ladder_rung = the wire quoted ONE RUNG of a Kalshi scalar ladder and we could not re-read the market ourselves to replace it with the ladder’s forecast (utils/kalshi_ladder.py); compare against market_filtered reason=ladder_rung on the same tickers. reason=chart_cut (#2127) = the story is about a chart the owner cut from the newsroom’s scope (music_news.CUT_PATTERNS); detail.family names which one. The newsroom was the only chart surface with no chart list, so it relayed whatever chart a trusted account posted about — 135 chart stories over 30 days named ~20 chart families, including ARIA, the U.K. Official Albums Chart and a K-pop fan tally. Watch detail.family=unnamed separately from the named families: a named family is the cut working as asked, but unnamed means the gate could not recognize the chart at all and failed CLOSED, so a real chart appearing there is a missing pattern, not a cut. It measured 1 of 135 (a preview-truncation artifact) before shipping; a rate climbing past a few percent means the wires moved to wording the patterns do not cover, and the desk is going quiet rather than getting narrower. Re-measure with scripts/dryrun_chart_filter.py. reason=deep_chart (owner cut 2026-08-07) = a PLATFORM-chart (Apple Music / Spotify / iTunes daily) position below the top-MUSIC_NEWS_CHART_POS_FLOOR (default 10) with no debut call in the wire wording, OR a debut call past the artist’s tier debut floor (music_news.deep_platform_cut with artist_watch.debut_depth_floor: none for WATCHED, top 50 KNOWN, top 25 UNKNOWN – added 2026-09-08 after a #199 Spotify “debut” by Ashe shipped on the unconditional carve-out; a re-entry is not a debut). detail.pos is the position tested, detail.settled says whether it was the live reading or the claim’s own number (kworb down), detail.debut whether the wire called a debut and detail.tier the artist’s recognition tier (a cut with debut=true is the tier floor firing; tier=known on every row means the recognition lists are empty and the floor relaxed to 50). Measured cause: popbase’s deep-chart chatter about one song shipped as hero cards two days running (#148, then #125). The desk’s watch lane is the intended supply for top-of-chart moves. A hit here is the floor working; a SUSTAINED zero alongside deep positions in music_news_posted previews means the gate came unwired. reason=chart_repost (#chart-repost) = the desk ALREADY reported this song’s position on this WEEKLY chart and it has not made a real move, so the re-report is dropped before the verify spend. A weekly chart (a radio Mediabase panel, a Billboard weekly) holds a position for the whole week, and the text dedup cannot hold it — Drake’s “Shabang” shipped at #1 on the US Rhythmic radio chart on Aug 15, 16, 19 AND 24, and Cardi B’s “AH HA” shipped at #18 then #14 (four spots) on the same chart. The high-water store (kv_cache namespace music_news_chart_hw, keyed music_news.chart_position_key = folded subject + pos + the chart key, per guild, 45-day INACTIVITY TTL — a suppressed repeat refreshes the row, so a 10–20-week #1 run cannot expire mid-streak and re-ship) holds the last position REPORTED; music_news.should_repost_chart_position re-reports only a CLIMB of MUSIC_NEWS_CHART_REPOST_MIN_CLIMB (default 5) spots, a NEW #1 (a crown the last report did not hold), or a RE-ENTRY (the song left the chart and came back — the owner steer “exits post right” — detected off the original wire wording via music_news.claims_reentry, since the settled rewrite strips “re-enters”). A plain FALL to a worse-or-equal position with no re-entry cue stays suppressed like a hold. detail.pos is the settled position, detail.prev the last reported one, detail.chart the chart key. It gates a SETTLED reading on ANY known chart — WEEKLY panels AND live DAILIES, because a held #1 repeats on both (Ariana Grande’s “hate that i made you love me” shipped ELEVEN times holding #1 on the global Spotify daily chart); the deep_chart floor runs FIRST and trims a deep daily position, so only a top-of-chart daily position reaches this gate. A genuinely new chart or a big jump still ships. Written on PRODUCTION delivery only, so a staging audition never raises the mark. Dropped BEFORE the compose spend, and the post is marked seen. A hit here is the gate working; a sustained zero alongside repeated same-song positions in music_news_posted previews means it came unwired. reason=cert_reported (#2153) means the desk ALREADY reported this exact certification: a certification settles, so it is news once, and the text dedup cannot hold that (its memory is bounded by rows, a 36h window and 72h retention, while the WAP Diamond repeat arrived 113h later from a third account). The folded key is the music_news.cert_story_key identity (the folded subject + the model’s cert_id, <multiplier>x<level>|<body>) matched against the music_news_cert rows in post_dedup_history. Like the milestone rung, the certification identity comes from the classifier, not a regex over the claim: cert_id (e.g. 7xplatinum|riaa) so a reworded repeat keys the same, empty when the post is not a clean certification. Dropped BEFORE the research spend, and the post is marked seen. reason=milestone_reported is the SAME fix for a STREAMING MILESTONE (a track crossing a fixed stream rung): a crossing settles once, and the text window could not hold it — the J. Cole “No Role Modelz” 3B card posted twice ~54h apart, past the 36h window, because the topic judge overturned a shape-only hit against a DIFFERENT milestone while the real twin sat out of window. The folded key is the music_news.milestone_story_key identity (the folded subject plus the model’s milestone_id) matched against the music_news_milestone rows in post_dedup_history (1-year retention). The rung comes from the MODEL, not a regex over the claim prose: classify_music_news already reads every post and returns milestone_id — the canonical value|platform|metric (e.g. 3000000000|spotify|streams) — so a reworded repeat (“3B” and “3 billion”, “streams on Spotify” and “Spotify streams”) keys the same. The ONE metric held is a cumulative all-time STREAM count, and a wire’s “plays” folds to “streams” so the same crossing keys the same whichever word the wire used. The model returns an EMPTY id for a figure that must not dedup — a year-to-date/annual/period total, a territory-scoped figure, an aggregate across platforms, a YouTube or other VIEW count (per-video, not per song), or a monthly-listener figure (a moving snapshot that rises and falls) — and a throwback (retro) also fails open (#2353 review). The key builder only validates the id’s shape and folds the subject; a regex over the prose could not hold the wording variance (grouped digits, hyphenated qualifiers, post-metric platforms, territories) and each broke a different branch (#2352). Dropped BEFORE the research spend, and the post is marked seen. reason=chart_debut_reported (#market-dedups) is the SAME fix for a WEEKLY-chart DEBUT, and the FIRST cross-surface one: a song enters a chart once, so “BbY WOW debuts at #73 on the Billboard Hot 100” is not news again days later — but it shipped from music_desk on Aug 19 and from the newsroom on Aug 22 (+53h) and Aug 23 (+85h), each past the ~14h effective wire window, and the topic judge even overturned a shape-only hit against a different Karol G debut still in the window. The folded key is the music_news.chart_debut_story_key identity (the folded subject + debut + the chart key) matched against the SHARED wire_chart_debut rows in post_dedup_history (1-year retention). It is shared because music_desk reads Billboard first-party while the newsroom relays the same debut off @billboardcharts — both surfaces write the key (on PRODUCTION delivery only, so a staging audition never suppresses a real post) and both read it. Only a DEBUT keys (chart_debut_story_key returns None for a re-entry or a move, and for a LIVE daily chart that churns), so a jump (“surges to #47”) still ships. music_desk runs the same key: it emits music_desk_scored shipped=false reason=chart_debut_reported when it suppresses a debut a wire surface already posted. Dropped BEFORE the compose spend, and the post is marked seen. reason=novelty_distribution means a novelty story was a per-work DISTRIBUTION, not a single unusual count, so it is dropped before the verify spend (music_news.novelty_is_distribution). A novelty is one number (“Kid Cudi hums 1,814 times across his albums”); a per-album breakdown has no single headline figure, so the classifier put a RANGE in the card’s hero slot (“47.0%—89.4%”) and a comma list in the claim, and the card heroed a meaningless range over a caption that overflowed. Live: “Kanye West’s lyric-credit share by album runs from 47% on DONDA all the way to 89.4% on 808s & Heartbreak” (2026-08-15, @hiphopnumbers, X-only). The two upstream gates missed it — the source is a trusted relay, so may_relay was true and verify-or-drop did not fire, and the slop self-gate passed it at 0.0. The guard fires on a range figure OR a claim listing three-plus percentages, and the post is marked seen. A SUSTAINED zero here is fine — the shape is rare; a rate climbing means the wires are posting more per-work breakdowns and the novelty lane is spending drops on them.                                  
music_news_slate cogs/music_news.py guild_id, count (the slate size), + folded detail: authority + culture (how many of each source kind the slate carries), authority_pool + culture_pool (the fresh, cued, unseen candidates each kind offered), artist_pull (proactive watched-artist pull candidates — see artist_news), new_releases (deterministic Apple album-drop candidates, leading the slate — see artist_news detail.mode=newrelease) — the per-slot CLASSIFY SLATE the newsroom built (#2031). The desk polls the whole music wire list (38 handles) rather than the 13 numbers authorities, because a chart stat only an aggregator carried could not otherwise reach it, and _MAX_CLASSIFY (12) is a PER-KIND budget so neither kind can take the other’s places. Either count persistently short of its budget while its pool is healthy means the split broke. culture_pool is the pool BEFORE the cue-strength filter, so a healthy culture_pool with culture at 0 means chart_cue_strength rejects everything, while both at 0 means the wire fetch is not reaching those handles (separable against curator_fetch). Watch culture against music_news_scored: the classify loop stops at _MAX_COMPOSE_ATTEMPTS (6) stories and the authorities lead, so a slate carrying music wires that never produce a scored story means the authorities are filling the shortlist first — measured at 6% of slots, and a materially higher rate is the signal to reserve compose places per kind.                                    
apple_releases_fetch utils/apple_releases.py (top_albums, #release-board) The releases board’s source: Apple Music US Top Albums, 100 rows ranked by current plays, each carrying its own release_date, in ONE keyless request. source=apple_releases, ok, count (rows parsed), duration_ms, error (fetch_failed after the retry layer | no_albums for an empty or changed body — an empty Top Albums feed is a failed read, not a real “nothing charting”). WHY IT EXISTS: the board read Deezer’s all-genres chart alone, that chart is LONG-TAIL, and the board went fully DARK — 35 runs between 2026-08-27 and 2026-09-04, 0 posts, every one phase=source_thin. On 2026-09-04 Deezer’s top 100 held three albums released that day and missed Beyoncé’s B’DAY (20th ANNIVERSARY DELUXE EDITION); Apple held four, Beyoncé’s among them. Fail-open like Deezer: an ok=False raises nothing (the board just reads fewer drops and skips the slot as a quiet night), so this rate is the ONLY sign the feed died — it feeds misc_health["apple_releases"] → the integration_unhealthy finding. On the outbound_calls and data_sources panels. All fields are shared vocabulary — zero new field names.                                    
spotify_fetch utils/spotify.py DORMANT since #2447, and never once emitted — the releases board moved off Spotify (/browse/new-releases is 403 for a new app) to Apple’s Top Albums feed (see apple_releases_fetch), so nothing imports the client and nothing emits this event; re-measured 2026-09-04 against the live credentials — the token call returns 200, the browse call 403; utils/spotify.py + this row await removal in a follow-up. Historical shape: ok, duration_ms, count (releases returned), action (new_releases), reason (on ok=False: unprovisioned = no keys | auth = creds present, token refused | fetch_failed = HTTP error) — one call to Spotify’s Web API /browse/new-releases, the releases board’s source (client-credentials auth). Fail-open: ok=False count=0 on any of those, and a real failure also emits emit_error(source=spotify\|spotify_token, recoverable=True). Joins the ops-monitor latency read AND its integration-health read (a dedicated branch feeds misc_health["spotify"] → the integration_unhealthy finding) — but reason=unprovisioned is EXCLUDED there, so a dark-by-default install (no SPOTIFY_CLIENT_ID/SECRET) is not perpetually flagged; a creds-present auth/fetch_failed rate over the window IS a real upstream/quota issue and flags. This is the ONLY sign of a silent Spotify outage, since a thin board is logged as source_thin (out of the quality/decline findings).                                    
music_releases_scored / music_releases_posted / music_releases_dedup cogs/music_releases.py (#release-board) The NEW-RELEASES BOARD — a scheduled roundup of the night’s fresh drops (artist - title, one card), the roundup sibling of the music DROP. Source: apple_releases_fetch – the board reads Apple Music’s US Top Albums feed and keeps the records that dropped in the window. It reads NO Deezer (owner steer 2026-09-05): Deezer’s chart alone left the surface dark for its whole recorded life (35 runs, 0 posts, every one source_thin – long-tail, missed the day’s biggest drops), and paired with Apple (#2950) its rows were the noise on the card; deezer_fetch and deezer.recent_releases were removed with it. (#2447; the Spotify /browse/new-releases source is 403 for this app, and the MusicBrainz date feed had no popularity signal so it was noise-or-thin.) music_releases_scored: guild_id, score, reason, shipped (bool >= the shared music-desk 0.6 floor – the board self-gates on music_desk_score(kind="release"), the numberless release rubric, so the ops-monitor’s 0.6 read is honest), count (fresh drops found, capped at the card’s 10 rows), category (new music friday | drops today for the board; biggest drops for the Friday-only TOP DROPS card — the two units one slot yields since #2956, each with its own floor, roster-dedup key and source_thin skip, so a per-unit read is a category filter), post_preview. A thin/empty skip rides this event with a marker phase + reason: phase=source_thin reason=thin (below the plan’s floor, NO compose attempted – a source/volume skip, kept out of BOTH the quality average and the compose-decline count, since the model never ran; an Apple outage is the apple_releases_fetch event’s job) vs phase=declined reason=empty (compose ran and the model returned EMPTY – a real compose decline the decline-rate finding counts). is_judge_score drops both 0.0s from the quality average. music_releases_posted: guild_id, channel_count (0 on a staged audition), delivered (room | staged), count, category, post_preview. music_releases_dedup: guild_id, channel_id, decision (similarity_gate), signal, post_preview. On the shared desk_quality/desk_posts panels (it is in _DESK_SCORED/_DESK_POSTED) and in ops_monitor.SCORED_KINDS – and because it ships at the 0.6 desk floor, the monitor’s 0.6 SCORE_FLOOR reads it honestly (no false surface_dark from sub-0.6 shipped boards). All fields are shared vocabulary — zero new field names. No dedicated MONITOR: it ships dark (the music_releases experiment defaults to STAGING) and the shared self-gate + decline-rate findings already cover it.                                    
watch_new_entry cogs/music_desk.py ok (the row was datable), reason (debut reentry charted no_date deep radio_entry), + folded detail: chart, pos, age_days, last_seen_days, title — the watch lane’s new-entry gate (#2045). kworb marks a catalog RE-ENTRY NEW exactly like a fresh drop on the rank-only charts, so chart_stories emits an unresolved new_entry and this dates the release before anything is claimed. The third lane to carry this gate, after apple_debut (#1887) and called_shot (#2038) — the same one bug in three places. charted is the CHART-PRESENCE rejection (utils/chart_presence.py, first-sighting semantics per #2139): the title is past its 24h first-sighting window on this chart (last_seen_days = days absent before its latest return; 0 = continuously charting), so kworb’s NEW flag is a tracked-range bounce or a re-surfacing recent release, and the date gate alone would have re-claimed a debut — the duplicate-debut fix (a week-old release’s age passes the window every time it bounces back). A charted row makes no post. Unlike the other two lanes, a reentry here is KEPT and posts as a re-entry: a catalog track returning to a platform chart after a real absence is real news (it is what the wires post as “‘ICEMAN’ has returned to being the #1 Hip-Hop album”), it just is not a debut. deep is a settled re-entry the DEPTH then cut: it landed below MUSIC_DESK_WATCH_REENTRY_MAX_POS (10). A new title is news wherever it lands, but an older one returning deep in a chart is churn — measured over the 14 days to 2026-08-08, all 14 re-entries the lane kept sat at #56–#99 and none reached the top 10, and the #97 one (“Where We Go”, Marshmello & Thomas Rhett, US iTunes) shipped to X. music_news.deep_platform_cut already cut this exact shape at the same depth for the wire lane; the desk lane had no floor. Debuts are exempt. radio_entry (2026-08-17) is a new_entry on a LEADING chart — radio airplay (_LEADING_CHARTS) — KEPT as a position rather than a debut. Radio builds a song’s spins over WEEKS after release, so a first appearance on the airplay chart is decoupled from the release date, and the release-age gate cannot tell a genuine debut from a song radio picked up late. A 2-week-old song entering radio at #72 passed the 45-day window and shipped as “‘petal’ debuts on the US radio-airplay chart at #72” (owner call). So the lane makes NO release claim for radio: it states only WHERE the song sits (“now on the US radio-airplay chart at #N”, kind radio_entry), and only when the entry lands inside MUSIC_DESK_WATCH_RADIO_ENTRY_MAX_POS (20, anchored to the per-format lane’s top-15 breakout cap). A deeper entry is deep-tail airplay churn and drops as deep (petal #72); a tracked-range bounce drops as charted; no release date is ever read for radio. The crown / top-10 / jump kinds carry no release claim and are untouched, and utils/radio.py still tells the “climbing on radio” story off the spins. So reentry + charted + deep + radio_entry streams are the gate working. Watch instead for a sustained no_date stream: that is Deezer failing, and the lane’s new entries are dark.                          
called_shot cogs/music_desk.py ok (the row is callable), reason (fresh stale no_date), + folded detail: rank, age_days, album — the HITS called-shot lane’s DEBUT gate (#2038). The projection document has no last-week position for a catalog RE-ENTRY either, so new_entries() mixes real debuts with albums that fell off the chart and came back; the album’s release date is what separates them, and the gate fails CLOSED (no_date refuses the claim). The HITS twin of apple_debut. Measured live on the building chart dated 2026-07-30: 4 of 7 no-last-week rows were re-entries — Drake’s VIEWS (2016) at #44, Taylor Swift’s FOLKLORE (2020) at #49, Pitbull’s GREATEST HITS at #50, Megan Moroney’s CLOUD 9 at #47 — and three sat inside the watch band (#2007) that reaches rank 100 for exactly those artists, so the lane could have called a ten-year-old album as a debut. Watch a sustained no_date stream with zero fresh: that is Deezer lookups failing and the lane going dark, which is otherwise indistinguishable from a quiet chart week.                                
proj_debut_gate cogs/music_desk.py ok (at least one row confirmed), count (rows kept), + folded detail: considered, fresh, stale, no_date, dropped — the DEBUT gate in front of the two PROJECTION BOARDS (_fresh_projection_entries, #3170). Same bug as called_shot, one lane later: projected_debut_rank and projected_debut_sales drew new_entries() raw, and that column reads the same for a catalog re-entry, so the card for the chart dated 2026-09-19 shipped nine rows under the hero “Projected Sales: New This Week” and eight were catalog — Kanye West’s GRADUATION (2007) at #32, Drake’s VIEWS (2016) at #50, Beyoncé’s B’DAY (2006) at #8, Dolly Parton’s greatest-hits comp at #21. Both boards now take the caller’s confirmed rows and are ABSENT without them (fail closed), under a floor of 2 confirmed rows. ONE event per evaluation, not one per row, and deliberately NOT in ops_monitor._DEBUT_GATE_SHIPS: the rows overlap the called-shot lane’s, so a per-row shape would double that lane’s counts, and a light release week legitimately confirms nothing — the gate_dark finding would fire on the gate working. The board’s own silence is already covered by lane_dark on phase=chart_proj, which the three RANKING boards keep alive (they make no claim about what is new and do not read this gate). Read the tally, not the count: a stale majority is the gate working (8 of 9 on the document dated 2026-09-10), a no_date majority means Deezer and iTunes stopped answering and both boards are dark for that reason.                                    
apple_debut cogs/music_alert.py reason (fired deep stale charted no_date), count (NEW chart entries evaluated), + folded detail: pos, age_days + tier (watched/known/unknown) on fired, charted (# rejected as already-charted), deep (# cut by the depth floor, on EVERY disposition), tiers (live/off — was the recognition list real?), song, chart (songs/albums) — the Apple Music DEBUT lanes’ gate decision, for the songs chart and the Top ALBUMS chart (#1887). The lane runs FOUR gates and each rejection has its own reason. kworb’s NEW marker also fires on a RE-ENTRY, so stale is the release-date rejection (every NEW entry resolved a date, all outside the fresh window) and no_date is Deezer resolving nothing — the fail-CLOSED direction, a lookup problem rather than a real “all old”. charted is the chart-presence rejection (utils/chart_presence.py, first-sighting semantics per #2139): the title is past its 24h first-sighting window on this chart, so the NEW flag is a tracked-range bounce. deep is the tiered DEPTH floor (#2207): the debut is real, and the lane cut it because it landed below the floor for its artist’s recognition tier — MUSIC_DEBUT_KNOWN_MAX_POS (50) or MUSIC_DEBUT_UNKNOWN_MAX_POS (25), with a WATCHED artist exempt. It was added after the albums lane posted, and crossposted to X, “Lil Tony Official’s ‘ELIJAH’ debuts at #98 on the Apple Music US albums chart” — the third-to-last rung of a top-100 chart by an artist on neither kworb ranking page. deep is reported ahead of stale because a depth cut always passes the date gate first, so without the ordering the cut would read as “the dates were old” when the lane made a policy choice. deep rides the fired event too, not only the terminal one: a row cut above a row that then shipped would otherwise leave no trace, and the depth panel would under-report. tiers is the health of the depth floor’s other half — off means the recognition lists came back empty, so every artist read as known and the floor silently relaxed to the looser bar (the tier_gate_off finding). A no-NEW day emits NOTHING (the quiet norm; a kworb fetch crater is caught by the chart_fetch health watch). So deep + charted + stale streams are the gates working. Watch instead for a sustained no_date stream with zero fired: that is Deezer failing and the lanes dark, which a quiet chart day is otherwise indistinguishable from — and that whole class is now a named finding (gate_dark), because no judge-score check can see it. entry + framing say WHICH WORDING SHIPPED (2026-08-09). The lane picks one of four phrasings for a fired debut and the choice turns on the ENTRY rank, which the event did not record — so a misfiring rule looked identical to an ordinary debut. framing is debut (still at its entry rank), ramp (released within MUSIC_DEBUT_RAMP_MAX_AGE_DAYS and risen since first sighting — takes the DEBUT wording, because on a realtime chart the first-sighting rank records when the bot polled, not where the title entered), climb (risen, but the release is older, so it is a real climb) or slip (fallen, which is never a ramp). entry rides along so a query can re-derive the branch without trusting the label: ['tootsies'] | where event == 'apple_debut' | extend d=parse_json(detail) | summarize count() by tostring(d['framing']). Watch for ramp on a chart where releases are rare, or climb never appearing at all — either says the age threshold is mis-set. Panels: desk_debut_gates (reason mix), desk_debut_ship_rate (per lane+chart, the one that separates songs from albums) and desk_debut_depth_cuts (what the floor cut, by tier and rank).                            
chart_crown cogs/music_alert.py reason (fired), + folded detail: chart (global_daily/apple_songs/apple_albums), pos, prev (the rank it came from), age_days (None = Deezer could not date the row), framing (ramp/climb), song — the shared #1 TAKEOVER lane’s wording decision (#2235). The lane fires only on pos_change > 0, so by default the title held a real prior rank and the blob says “climbs to #1, up from #N” and forbids “debut” — the #2140 fence, and this lane is where that bad post came from. The one exception is a RELEASE-DAY RAMP, the crown half of #2226’s debut-lane rule: kworb’s pages are near-realtime snapshots, so a title released today enters low and rises through the day as plays accumulate, and its earlier ranks are that ramp rather than a chart history. A #1 reached within MUSIC_DEBUT_RAMP_MAX_AGE_DAYS of release is a real #1 debut. framing says which wording shipped, because the branch turns on a release date the event would not otherwise carry — without it a misfiring ramp reads as an ordinary crown. Fail direction is CLIMB: an undatable row (age_days null) keeps the climb wording, because pos_change already proves a climb while “debut” is the claim needing evidence. Note the ramp blob never names prev — the number rides this event for diagnosis but is kept out of the compose’s context, so the model cannot cite a rank it was never given. Watch for a sustained all-ramp mix, which says the age threshold is mis-set. refreshed_at + refresh_age_s (#3191) are the DETECTION half of the lead-time pair: refreshed_at is the chart PAGE’s own refresh time (kworb’s last-modified), and refresh_age_s is how stale that was when the lane DETECTED the new #1. This event fires while the lane BUILDS the signal, before the daily gate, the self-gate and the repeat check — so a crown counted here may never have posted, and this is NOT the “how fast did users see it” number (Codex on #3193). That one rides music_alert_posted.refresh_age_s, which only a delivered post emits, and crown_lead_time reads it. Keep both: comparing them splits a slow READ (the cache) from a slow COMPOSE (the gates and the model). Both are empty/None when the client cannot report one: absent over invented, because stamping our fetch time there would read as the chart’s refresh time and be wrong by however long the cache held. The RADIO crown lane emits this event now too. It composes its own blob (an audience figure, not a rank) so it never rode _chart_crown, which left it the one crown lane with no event at all; framing is always climb there (radio adds take weeks, so a release-day ramp is impossible on that chart). Watch refresh_age_s for a creep back toward the old 3h: that means revalidation stopped working and the lanes are late again. Panels: desk_crown_framing (this event), crown_lead_time (the delivered twin on music_alert_posted).                                    
spotify_debut cogs/music_alert.py reason (fired stale no_date), count (fresh chart entries evaluated), + folded detail: pos, age_days (on fired), entry (the first-sighting rank, 0 = unknown), days_on_chart, song, chart (global_daily) — the Spotify RELEASES lane’s DEBUT gate, and the Spotify twin of apple_debut under the same fail-CLOSED contract. kworb’s days-on-chart column counts days on THAT chart, never days since release: a song released weeks earlier that only now crosses into the Global Daily top 200 reads days=1, identical to a same-day drop. Shipped in prod on 2026-08-04 as “Steve Lacy’s ‘oh yeah?’ opens with 8.4M Spotify streams day one” — the song came out 2026-07-17 and had charted in the US every day since; it entered the GLOBAL chart at #126 that morning, and the 8.4M was a running total, not a first day. The lane now confirms the Deezer release date (_debut_release_age, the gate the Apple debut lanes already ran) before it calls anything a new release. Watch a sustained stale / no_date stream with zero fired: that is the lane dark, which a quiet chart day is otherwise indistinguishable from. The gate runs BEFORE the Songstats lookup, so a rejected entry costs no metered slot. entry + days_on_chart are the #2224 fix. chart_debuts accepts a row up to MUSIC_DAILY_DEBUT_MAX_DAYS (2) days on the chart and the row carries its CURRENT rank, so the blob’s “entered the chart at #N” was stating a day-2 rank as an entry rank — #2140 on the lane that never got it. Measured live: Stray Kids’ “This & That” entered at #31 on 2026-08-08 and still counted as a debut at #82 on 08-09, so a post one day later would have been wrong by 51 places; it did not ship only because _daily_gate’s same-key rule blocks a REPEAT, which is dedup and not correctness. The lane now reads chart_presence under the shared sp_global key for the entry rank, falls back to the current rank only on a day-ONE row (where the two are the same by definition), and otherwise claims no entry rank at all. The post leads with the LATEST rank and carries the entry as context (“is at #82, down from a #31 entry inside a day”) — owner steer 2026-08-09, and chart_presence’s own #2139 rule that a consumer inside the window reports the CURRENT rank. The entry rank is named so the move can be stated and so the current rank is never read as a debut, which is the #2140 half; leading with the entry rank instead would report a number the chart no longer shows. The “inside a day” phrase is gated on info.new, so it is dropped once the row is past its 24h first-sighting window. Presence is read for the RANK ONLY here, never for suppression: the Spotify days-on-chart count is cumulative so a bounce never reads as day 1, and info.new is a 24h window while this lane allows 48h, so suppressing on it would drop every legitimate day-2 debut. days_on_chart also fixes the other number in the same sentence — the plays were narrated as “its first day ON THE CHART” on a day-2 row                                
radio_debut cogs/music_alert.py reason (fired crossover no_date), count (radio entries that matched a big Spotify hit), + folded detail: pos, age_days, song — the RADIO ENTRY lane’s age decision (#2077), the fifth #1887-class fix. Radio’s days column counts weeks on the RADIO chart, never days since release, so the lane composed “a BIG new song just DEBUTED” about whatever crossed to radio — live it called an 847-day-old Malcolm Todd track new. Unlike its siblings this is a FRAMING split, not a gate: the lane’s real picks measured BIMODAL (fresh releases reaching radio at 5–21 days, a summer single crossing at ~44–48, the 2.3-year outlier), and a months-old streaming hit finally reaching radio is a real story — arguably the better one. fired composes the NEW framing (the release age is stated in the blob); crossover composes the crossover framing with “new”/”debut” banned; no_date takes a NEUTRAL framing — both “new” AND “established hit” are banned, because an undatable song may be a drop-day release the catalogs have not indexed yet (#2125), so asserting either age is invented. Neither non-fired reason blanks the lane. The window is MUSIC_RADIO_ENTRY_NEW_MAX_AGE_DAYS (60), sized to radio’s measured lag — a streaming-lane 14d window would misframe most legitimate radio arrivals.                                
music_news_retired cogs/music_news.py guild_id, channel_id, reason (the dedup signal: text_similarity content_overlap topic_restated shared_run), phase (story kind), source (the @account), post_preview — the newsroom RETIRED a source tweet because the repeat check dropped its composed unit (#2037). The desk marked a post handled when it posted, when the self-gate failed it, and when the classifier called it a non-story — but not when the repeat check dropped it, so the post stayed a candidate for its full 30h freshness window and re-composed every slot, paying one Sonnet write plus one judge call each time. Measured over 7 days: 892 composes covering only 95 distinct subjects — 9.4 writes per story (petal 136, AH HA 102, Music/Fashion/Film 75) against 104 posts, so any read of the funnel off the compose count was ~9x inflated. Pair with post_dedup on the same reason to confirm every mechanical/topic drop retires its source; a post_dedup stream with no matching retire means the unit carried no external_id                              
dedup_overturned utils/dedup.py (arbitrated_match), emitted by cogs/scheduled_poster.py + cogs/discourse.py + cogs/chimein.py surface (music_news/music_desk/… plus discourse and chimein), guild_id, channel_id, signal (the mechanical signal overturned), reason (the judge’s words), post_preview — the topic judge OVERTURNED a mechanical dedup hit, so the post shipped (#2037). The mechanical gate compares WORDING, which on a chart desk is mostly boilerplate, and its hit used to be final: live against a real 20-post history it called a different artist’s identical-shape line a repeat, and the same song at a NEW chart position a repeat. The judge now arbitrates instead of only backstopping. A healthy rate is small and non-zero. Zero means the arbitration is not running, or the judge is unreachable — which fails CLOSED and leaves the mechanical verdict standing, so an outage looks like silence here rather than a flood of repeats. A rate approaching post_dedup on the same surface means the mechanical gate is firing on shape far more than on stories, and its thresholds want a look. Always read the two together. The arbitration is shared, but the CATCH direction is not (#2083). The decision lives once in utils.dedup.arbitrated_match, which the wire desks, discourse and chime-in all call. The desks pass catch=True and pay one judge call per post, because they write from a house style and restate each other constantly. Discourse and chime-in pass catch=False, so the judge runs only when the mechanical gate has already found a hit: their gates fired 9 times and 0 times in 30 days, against 705 mechanical drops across the desks in 7, so catching there would buy a handful of suppressions for thousands of calls. For those two surfaces the DROP side is not post_dedup — read discourse_dedup and chimein_evaluated decision=dedup_gate against this event instead.                                    
post_dedup cogs/scheduled_poster.py surface, guild_id, channel_id, reason, phase, post_preview — the MECHANICAL dedup gate (utils.dedup.duplicate_reason) discarded a unit that was already composed AND already through its self-gate. reason is the signal that tripped (same_link / text_similarity / shared_run / content_overlap); phase is the lane, so the loss is attributable. Why it exists: this gate ran with no event at all, so a composed-but-discarded unit cost a write call plus a judge call and left no trace. On 2026-08-04 the music desk composed 45 units and posted 5 in 140 minutes, and finding out why meant replaying real production lines through duplicate_reason by hand — 38 of 40 died here. Drop-rate = post_dedup events / units composed; a lane whose drop-rate approaches 1 is paying full price for posts nobody sees. Fires for EVERY ScheduledPoster surface, not only the topic-dedup opt-ins. A drop rate is not a material rate (#2037). A surface that does not CLOSE OUT the SOURCE of a deduped unit re-composes it every slot until the source ages out, so this event counts the loop, not the stories: the newsroom’s 892 composes over 7 days covered 95 distinct subjects, one written 136 times. Read post_dedup alongside dcount(matchup) on the surface’s *_scored event before concluding anything about volume — composes-per-distinct-subject is the number that says whether a surface is busy or stuck. _sched_on_dedup is where a surface closes the source out. The PUBLICATION lanes run a REDUCED signal set (#2098). chart_board / debut_sales / chart_proj / market_board are deduped on wording and links alone (_sched_dedup_wording_only), which drops shared_run and content_overlap (utils.dedup.FORMAT_SIGNALS) and the topic judge’s CATCH direction. Those two signals measure a post’s SHAPE, which on a recurring chart graphic is the FORMAT: a card names its chart, its week and its source every time, and “the Aug 15 Billboard 200, with HITS Daily Double” is a 40-char verbatim run before the post says anything of its own. Measured 2026-08-07 on the live Aug 15 week, the four projection boards all cleared the 0.6 gate and only ONE reached the room; after the opt-outs plus per-board compose steers, 4/4 did. So a post_dedup on one of those four phases should only ever read same_link or text_similarity — a shared_run or content_overlap there means the opt-out regressed, and is worth a look.                                    
history_gate utils/history_gate.py, taken by cogs/scheduled_poster.py + cogs/event_poster.py surface, guild_id, ok, reason, duration_ms — the per-guild POST-HISTORY GATE (#2758), which serializes the history READ -> judge -> history WRITE walk so two wire desks cannot both read a history that holds neither’s post yet. Emitted on TWO branches only, so the steady state is silent: reason=waited (ok=true) is a real collision the gate serialized instead of double-posting, duration_ms being how long the second desk queued; reason=wait_timeout (ok=false) is the gate giving up after GATE_WAIT_SECONDS and posting on a possibly-stale read. THE GATE FAILS OPEN: a timeout is never worse than no gate, but it is the old race, so any wait_timeout row means a deliver is stuck for minutes and wants a look. An uncontended take emits nothing (~120 desk posts a day would otherwise be ~120 events of no signal). WHY IT EXISTS: on 2026-08-31 sports_desk and pop_desk both composed Messi’s international retirement on one tick, judged at 15:31:43 and 15:31:46, and tweeted at 15:31:48.389 and 15:31:52.396 — pop_desk read the history ~2s before sports_desk’s row existed. The dedup gate itself was healthy: it blocked the same story twice later that hour (15:37:29, 15:47:25), once the row was there. Panel: desk_history_gate. NO MONITOR: a waited row is the gate working (an alert on it would page on success), and wait_timeout is bounded by the deliver path, which post_deliver_failed and the latency ceilings already watch — revisit if wait_timeout is ever non-empty.                                    
post_deliver_failed cogs/scheduled_poster.py surface, guild_id, channel_id, phase (the lane), post_preview — a gate-passed, non-deduped unit’s _sched_deliver returned False (#2063): a missing/unsendable channel or a failed send. This was the LAST silent branch of the ScheduledPoster spine — a dedup drop emits post_dedup, a delivered unit emits its surface’s *_posted event, and a failed deliver emitted nothing, so a gate-passed unit could vanish with no telemetry. With it the funnel closes as an identity: gate-passed = posted + post_dedup + this. That identity is how #2063’s ~890 “missing” newsroom units/month were diagnosed — they were dedup drops from before #2037 added the post_dedup emit, and the funnel has closed day-by-day since (measured 08-06..08-08: scored exactly = posted + deduped). Expected rate ~0. A sustained rate on one surface means its room config or send path is broken — the slot then re-fires next tick (nothing-delivered slots are not recorded), so a persistent failure also shows as repeated composes of the same subjects.                                    
slot_replan cogs/scheduled_poster.py (plan_shrank) surface, guild_id, channel_id, reason=plan_shrank, count (the stored posts_today), expected (slots due under the plan running NOW; folded into detail) — the daily-cap gate found today’s slot counter ABOVE the current plan’s elapsed-slot count and let the slot post anyway (#2921/#2926). Why the state is diagnostic on its own: the cap only lets a channel post while posts_today < expected, and expected only grows while the plan holds still, so posts_today > expected cannot happen under a stable plan. It means the plan SHRANK mid-day — a mod moved the surface to a lighter firehose cadence (scheduler_firehose_surfacesscheduler_firehose_hourly_surfacesscheduler_firehose_two_hourly_surfaces), changed the guild mood, or edited the /menu calendar. Before this escape the counter simply sat above the new elapsed count and the cap suppressed every remaining slot until the ET date rolled, returning BEFORE any log line and raising nothing: on 2026-09-03 music_desk posted 18 of its 30-min slots by 10:56 ET, a mod switched it to hourly 14 seconds after its last post, and the desk was dark for nine hours with zero error rows. surface_dark cannot see this class — that finding reads self-gate SCORES, and a surface stranded before compose produces none, so it drops out of the aggregation entirely rather than reading as a blackout. The escape is keyed on the most recent slot of the CURRENT plan, so it re-opens the gate once per new slot and the surface resumes the new cadence instead of draining the difference in a burst. Expected rate: a short burst on the day of a cadence change, then zero. A surface emitting it every day means its plan is being rewritten daily, or something other than the slot walk is bumping the counter.                                    
topic_dedup cogs/scheduled_poster.py surface, guild_id, channel_id, reason, post_preview — the semantic TOPIC-dedup gate (a cheap Haiku judge, claude_client.topic_duplicate) SUPPRESSED a post as a RESTATEMENT of something already shipped. Opt-in via ScheduledPoster.TOPIC_DEDUP on the proactive news/market surfaces (sports_desk/pop_desk/cinema_desk/cinema_news/music_desk/music_news/market_drop); runs AFTER the mechanical duplicate_reason and catches the same STORY re-angled that order-sensitive token-overlap misses (a signing posted 3 ways scored 0.33-0.46 overlap; “72 Hours” across sibling Netflix boards 0.47 — both real prod cases). Policy (owner steer “allow real updates only”): a restatement carrying no materially-new info is suppressed; a genuine DEVELOPMENT (a new number/party/resolution) or a different subject is ALLOWED (the judgment tokens can’t make). It now ARBITRATES a mechanical hit as well as catching what the mechanical gate missed (#2037). It used to run only when duplicate_reason found nothing, so a mechanical hit was final — and that gate compares WORDING, which on a chart desk is mostly boilerplate: live it called a different artist’s identical-shape line a repeat, and the same song at a NEW chart position a repeat. An overturn emits dedup_overturned. The verdict is TRI-STATE because the two jobs want opposite failure directions — catching fails OPEN (never invent a duplicate), overturning fails CLOSED (never ship a repeat because Haiku was down) — so None means could not judge and both leave the mechanical verdict alone. The judge is handed the MATCHED post first, so an arbitration never depends on where in the list the match sits. Measured with the real judge on real history: 12/12 repeats confirmed, 3/3 cross-subject collisions overturned. HOW DEEP IT SEES (#2189). The judge keeps the newest claude_client.TOPIC_DEDUP_MAX_RECENT posts of whatever list its caller hands it. That ceiling was 12 and the wire desks read 80 rows over 36h, so the judge silently discarded 68 of them — and a truncated prompt still returns a confident verdict, so a clean “not a duplicate” and a verdict on a list that never contained the duplicate looked identical from telemetry. Measured on 2026-08-09: six desks post ~112/day into one shared history, so the newest 12 rows spanned 1h18m against the caller’s 15h19m. What it cost: three surfaces posted the same fact (“Dai Dai” by Shakira is #1 on Spotify’s Global Daily chart) at 17:19, 21:27 and 01:02 UTC on 2026-08-08/09, and in the 80-row list the third one read, the first two sat at depth 24 and depth 40. The ceiling now MATCHES wire_lanes.WIRE_DEDUP_READ_LIMIT (a test pins the two together), and a caller that exceeds it gets a WARNING log naming the drop. Raised 80 -> 180 (#2493) as the desk volume kept climbing: measured over the 14 days to 2026-08-23 the pool posts a median of 119/day (busiest 174), so 80 rows had shrunk to ~11-16h of span – under the same-day window this read exists for – and a 12-24h-apart cross-lane restatement fell past the cap (the newsroom posted the SAME Weeknd UK-leg wrap twice ~12h apart, the first at row 78 by the time the second composed). 180 rows restores a full 24h of span even on the busiest day and reaches 36h at the median; recall holds with no precision cost at the larger size (the buried repeat caught 5/5 at a 120/160/180-row read, live Haiku on the real pool). Re-measure and move it if the surface set grows again. A story that settles PERMANENTLY (a cert, a streaming milestone) still gets an exact key instead (music_news.cert_story_key / milestone_story_key) rather than a bigger window; the window is for same-day evolving numbers. Dilution was measured rather than assumed (scripts/ablate_dedup_depth.py, 5 runs a cell on that production history): the repeat is caught 0/5 at 12 and 5/5 at 24/40/80, while a candidate on a subject absent from the list ships 5/5 at every cap including 80. music_alert runs this judge too, off the same shared read (cogs/music_alert.py:_topic_repeat) — #2163 gave it the WRITE half of WIRE_DEDUP_SURFACES only, so it logged its posts for the desks and never read theirs; it emits topic_dedup (the semantic kind, not the mechanical post_dedup) with phase=the lane, so it lands in the same desk_repeat_gate bucket as the desks’ judge verdicts and the suppress-rate math stays right. Emitted ONLY on suppression (its existence IS the suppression); the per-check count + latency ride claude_api purpose=topic_dedup, so suppress-rate = topic_dedup events / topic_dedup calls. FAIL-OPEN (a Haiku miss/error lets the post through, never swallows a real update). The mechanical content_overlap (#1681) stays the free deterministic first pass; this is the semantic backstop for the low-overlap re-angled cases it misses.                                    
discourse_skipped cogs/discourse.py guild_id, channel_id, reason (rate_limited/compose_error/empty)                                    
discourse_icebreaker cogs/discourse.py guild_id, channel_id, category (ranking|hot_take|this_or_that|confession), score, reason, shipped, manual, trigger (fallback|quiet_room|manual), has_memory, has_topical, has_market, post_preview (a linkless opener; material-only, so a slot with no source material skips with reason=no_material; reason=room_unanswered = the human-activity floor held it back, fewer than MIN_HUMAN_MSGS_BETWEEN_ICEBREAKERS (3) human messages have landed since Toots’ own last post in the channel, so a second unanswered opener can’t stack on the first (enforced once in _icebreaker_fallback via a live channel-history read, shared by the scheduled + quiet-room paths, restart-proof; manual bypasses it); market line reused from compose or sports-room-gated. trigger names the source: fallback = an otherwise-empty scheduled discourse slot fell back to one; quiet_room = cogs.chimein broke a silent room (the inverse of a chime-in: fired off the chime-in tick when a listen channel went QUIET_AFTER with no human message, riding the chime-in mood/hours/cooldown/cap gates, one opener per lull); manual = on-demand /discourse category:icebreaker (also manual=True). icebreaker is a permanent surface (no per-guild experiment): a ship-worthy opener (clears the quality floor) posts straight to the room — shipped=True; a gated/no-material slot is recorded by this event with shipped=False.)                                    
chimein_evaluated cogs/chimein.py guild_id, channel_id, decision (mood_off_gate/hours_gate/vibe_gate/cooldown_gate/daily_cap_gate/threshold_gate/reacted/empty_generation/dedup_gate/quality_gate), vibe, score, mood; situational: signal (same_link/text_similarity/shared_run/content_overlap, on dedup_gate), local_hour_et, quality_score, quality_reason, post_preview, voice_present (a voice note in the buffer lowered the worth-posting threshold — likelihood only, the quality gate is unchanged)                                    
chimein_posted cogs/chimein.py guild_id, channel_id, score, vibe, hook, mood, quality_score, quality_reason, voiced (shipped as a native voice note), sing (sung delivery), post_preview (the shipped take, POST_PREVIEW_CHARS — graded by the live-log eval pass), voice_present (a voice note in the buffer lowered the post threshold)                                    
quiet_room_decided cogs/chimein.py guild_id, channel_id, decision (break|hold), score, reason (the verdict on cold-opening a quiet room — the inverse of a chime-in. After the cheap silence/mood/hours/cooldown gates, an early deterministic human-activity pre-gate runs first — reason=room_unanswered (score=0.0, no model call) holds when she posted recently and fewer than MIN_HUMAN_MSGS_BETWEEN_ICEBREAKERS humans have spoken since, so the path skips the paid quiet_room_score/produce_icebreaker work; otherwise a Haiku quiet_room_score call (gated on the same mood-tuned threshold as chime-in, 0.7 chill / 0.5 yaps) decides whether the room is genuinely dead and worth reviving, weighing whether Toots was just mentioned/engaged. break = a quiet-room icebreaker is being produced (→ discourse_icebreaker with trigger=quiet_room, called with skip_activity_floor=True so the shared floor isn’t re-read), hold = left alone. Bias is toward hold; emitted once per lull)                                    
link_enrich utils/link_enrich.py platform, url_host, ok, duration_ms, cache_hit                                    
link_check utils/link_enrich.py platform, ok, alive (False only on confirmed 404/410), status, duration_ms — the dead-link HEAD guardrail’s per-URL liveness probe (distinct from link_enrich’s metadata fetch)                                    
github_api utils/github.py op (create_issue|get_issue|list_issues|close_issue|comment), ok, status, duration_ms — one GitHub REST call (the /order path + the feedback log)                                    
feedback cogs/order.py guild_id, user_id (the regular), logged; on logged=True: kind (request|suggestion|complaint|praise), issue_number; on logged=False: reason (daily_cap|github_error) — Toots logged SOFT feedback via the /ask note_feedback tool, recorded as a comment on one rolling feedback-log issue (label feedback, plain body so claude-code-action ignores it) for mods to triage. The lighter lane beside file_fix: no preflight/kitchen/pipeline gates, just a per-guild FEEDBACK_DAILY_CAP                                    
fix_update cogs/order.py guild_id, user_id (the regular), logged; on logged=True: issue_number, order_id; on logged=False: reason (unknown_order|terminal|daily_cap|github_error) — Toots appended NEW INFO from a regular’s follow-up reply onto an existing in-flight fix-order’s issue via the /ask update_fix tool (add_fix_update), as a plain attributed comment (no @claude, so it records context without spawning a duplicate PR run). The counterpart to file_fix (file opens the order, update adds detail); scoped to this guild’s non-terminal orders, runaway-guarded by the per-guild AUTO_ORDER_DAILY_CAP (shared with file_fix, separate counter)                                    
railway_api utils/railway.py op (list_deployments|redeploy|deployment_logs|build_logs|gql), ok, duration_ms — one Railway GraphQL call (the /undo rollback path AND the read-only check_deploys /ask diagnostics tool)                                    
tool_call cogs/ask.py + cogs/message_search.py + cogs/discord_info.py + cogs/playercard.py tool (the /ask tool that ran: break_down_board, lookup_player_props, scoreboard, api_sports, standings, matchup_preview, box_score, game_markets, match_highlights, list_sports, game_lookup, engagement, discord_lookup, look_at_image, find_image, find_media, listen_to_audio, read_media, search_messages, self_serve, check_deploys), ok, + the promoted shared dims where they apply (query, matched, count, reason, phase, sport, aspect, action, source, guild_id, user_id, host, url_host, error) + a demap’d detail dict for tool-specific fields — the UNIFIED on-demand /ask tool-run event (#1296: folded ~20 near-identical per-tool *_lookup kinds into one parameterized event keyed by tool, via utils.events.emit_tool; the tool-volume/hit-rate dashboards group by tool, tool-specific signals ride detail off the field budget). memory_search (recall dashboard), reference_lookup + catalog_lookup (integration-health), and social_search (timed latency) deliberately stay their OWN kinds                                    
auto_order cogs/order.py guild_id, user_id (the reporting regular), filed; on filed=True: issue_number, order_id; on filed=False: reason (gated = kitchen-closed/pipeline-red/in-flight cap/AUTO_ORDER_DAILY_CAP daily budget; plumbing/refused = preflight rejected; preflight_error/github_error = downstream failure) — Toots filed (or declined to file) a fix-order ON HER OWN via the file_fix /ask tool off a regular’s bug report. The autonomous counterpart to a mod’s /order; does NOT consume the server order budget (its own tighter daily cap) and still honors kitchen/pipeline/in-flight gates + the order preflight (protected paths)                                    
health cogs/health.py integration (the watched source: apple_music, the market/odds feeds api_sports/sgo/the_odds_api/polymarket/kalshi, plus the #968 app-backbone set embeddings/elevenlabs_tts/elevenlabs_stt/perplexity/twitterio/odds_divergence), action (filed|dedup|alert|usage_alert); crater actions also carry failure_rate + attempts (the numbers it judged on) + recent (#725: True = a sharp RECENT-tail crater the full-window average still hid — assess checks the last _RECENT_TAIL_EVENTS calls too, so a fresh throttle self-announces while it’s fresh instead of waiting ~15 min to dominate the window; this is why she stayed quiet on the SGO blip before). action=filed: a fix-order filing was attempted — ok=True with guild_id + issue_number + order_id on success, ok=False with reason (gated = every guild blocked by kitchen/pipeline/cap; plumbing|refused|preflight_error|github_error = the order text itself rejected) on a miss (the filing also emits auto_order). action=dedup: a crater was found but already filed within the 24h window, held back. action=alert: an owner-facing crater alert posted to #bot-logs (guild_id) with an @master ping, delivered=bot_logs (#968: #bot-logs ONLY, never a content room), gated by master switch + a per-(guild, integration) durable cooldown — deliberately NOT mood-gated (an ops alert isn’t proactive content). Fires for every crater, incl. the ops-only (notice="") backend watches that used to file a fix-order silently — the self-healing watch (#443) now also reaching the owner, closing the gap the drained twitterapi.io wallet exposed. delivered=suppressed (reason=backstop_healthy): the crater has a backstop integration (a Watch.backstop) that’s provisioned + healthy and carrying the load, so the owner alert is held (the fix-order still files for ops) — it only fires when the backstop is ALSO down. The SGO betting-lines alert rides this (#748): once the Odds API SGO-down backstop shipped, SGO going flaky no longer drops betting lines, so she stays quiet unless the_odds_api is degraded too (“everything flaky and down”). action=usage_alert: a metered-wallet early warning posted to #bot-logs (guild_id, integration=the source) with an @master ping — a prepaid wallet under its floor (twitterio/scrapecreators) or a capped source ≥85% of its limit, caught before the 402/429 wall (deduped per (guild, usage:<source>) on a 12h cooldown)                                    
db_query db.py (_run) op (coarse sql_op() label, never params), method (execute|fetch|fetchrow|fetchval), ok, duration_ms — one DB query through the central choke point (singleton queries; transactional paths bypass _run). Ops monitor keys these db:<op> so each query shape gets its own p99. SAMPLED: only emits for queries ≥ _DB_QUERY_EMIT_FLOOR_MS (50ms) or any failure, so the hot-path sub-ms majority doesn’t flood logs                                    
span utils/instrument.py (@timed / timed_span) name (function / flow-step label), ok, duration_ms — the catch-all latency primitive for heavy functions (feeds.recent_messages, markets.get_context, the memory write pipeline) and discrete flow steps (ask.parallel_fetch, ask.memory_context, ask.vector_recall, recap.produce_recap, …). Ops monitor keys these span:<name>; report-only (no per-span ceiling), but the trend gate catches a climbing tail                                    
pplx_<purpose> (pplx_ask/pplx_discourse/pplx_recap/pplx_chimein/pplx_music/pplx_icebreaker/pplx_trending/pplx_cinema_desk) utils/perplexity.py ok, duration_ms; on success: input_tokens, output_tokens, response_chars, hedged, source_count, context_size, recency; on failure: error. pplx_icebreaker is the idea-tuned conversation-starter hook fetched per quiet-room opener (surface="icebreaker": current debates/divisive/nostalgic angles to springboard off, never cited, always fresh, fail-open). pplx_trending (#1272) discovers the SPECIFIC current viral moments that fit a channel’s vibe (surface="trending", this-week), distilled by parse_trending_phrases into short searchable phrases the trending-clip gather searches — the fix for the raw-topic-string clip query                                    
link_stripped claude_client.py (discourse, ask, recap, music_post, chimein_post, discourse_icebreaker, commentate) purpose, reason (hallucinated | redundant | dead_link), count, urls (openers + live commentary are linkless, so any URL is stripped as hallucinated)                                    
discourse_relabel claude_client.py (react_to_trending) purpose (discourse_trending), reason (scaffolding_leak | unresolved_pick), ok — a trending clip react echoed its internal candidate label into the shipped text (“clip B out here selling…”, #clip3-leak; the [A]/[B] tag that binds a vision frame to a list entry, meaningless to a reader). The PICK was fine, only the wording leaked, so instead of discarding a good clip it re-reacts to JUST the picked clip (resolved by the link the reaction ended on) — a single clip carries no letter scaffolding, so the retry structurally can’t leak. ok=True = the clean single-clip retry shipped; ok=False = dropped (the slot falls through to a normal discourse take): reason=scaffolding_leak when the retry itself came back empty, reason=unresolved_pick when the leaked reaction carried no clip link to pin the pick to. Bounded to one extra call (_allow_retry), never a loop                                    
market_fetch utils/markets.py + utils/api_sports.py + utils/the_odds_api.py + utils/espn.py source (sgo/polymarket/kalshi/api_sports/the_odds_api/espn), query, ok, duration_ms, cache_hit, result_count, error, http_status (every sgo AND kalshi fetch miss classifies error off the HTTP code via _classify_fetch_errorrate_limited 429, forbidden 403, server_error 5xx, not_found 404, unreachable no response/a breaker short-circuit — instead of a bare fetch_failed, order #722/#73 for sgo, order #77 for kalshi: a 429 quota throttle, a 403 datacenter-IP block, and a plain timeout used to be indistinguishable in the same bucket). source=espn (#1113): the FREE, keyless ESPN scoreboard, the last-resort score/finals backstop for the SGO-only individual sports — tennis (only when SGO degraded) + MMA/UFC (only when API-Sports degraded); query=sport (tennis mma), result_count=matches parsed, error=all_feeds_failed. Reuses market_fetch so the ops-monitor’s per-source integration health + the ops-only espn health Watch cover it with no new wiring. Team-sport fallback (#full-espn-fallback): EspnScoreProvider now ALSO backstops the TEAM sports (soccer / NBA / WNBA / MLB / NHL / NFL) — live scores + just-finished FINALS off ESPN’s day scoreboard (source=espn_results, one market_fetch per league/day) — gated on api_sports.degraded, so an API-Sports outage (a suspended account, a crater) no longer darkens live commentary or bet settlement for those sports. The finals carry REAL scores, so the Bookie settles off them through the shared idempotent _settle_game (a level score PUSHES, never a wrong payout). Bookie pregame-odds fallback (#espn-odds-fallback): the SAME espn_results fetch now ALSO feeds the Bookie’s LAST-resort price fill (_supplement_with_espn_odds) — for an UPCOMING game still unpriced after SGO + the Odds API + the prediction markets, ESPN’s FREE scoreboard moneyline is folded on (odds_source='espn', American values), so the game is bettable instead of hidden. Fill-only-when-absent (a fresher SGO/Odds line always wins), pregame-only (ESPN has no in-play line, so the stale-line router reprices it off predictions if it goes live), and fold-only (it never INTRODUCES a game no settleable source surfaced, so it can never strand a bet). No new event — the fold’s ESPN reads are the existing espn_results market_fetch, so the ops-monitor integration health already covers it; the odds_source='espn' stamp on the bet is the per-bet audit trail. Verified vs the Odds API consensus on 15/15 live MLB games: favorite agreement 100%, implied-probability delta within ±1.5%. source=espn_summary (#espn-depth-parity): one game’s summary?event= payload – the boxscore + key events + rosters + season series that backstop the commentator’s per-game DEPTH (goal/card EVENTS, team STATS like possession/shots, and PREGAME formations + lineups + head-to-head) when API-Sports is degraded. EspnScoreProvider.game_events/game_team_stats/game_pregame parse it (one fetch shared per tick, cached per event); game_standings reads ESPN’s standings endpoint, and game_leaders reads the summary’s pre-computed leaders (basketball’s “who’s cooking”; soccer/baseball have no top-level leaders and stay empty rather than index-guess a stat off the raw box score). All display-only – the commentator degrades to the scoreline on a miss, never blocks a post. source=sgo reason=budget_shed (#786): the proactive budget governor (SportsGameOddsClient._shed_heavy_call) DROPPED a heavy league-wide read — the full game-board or league-wide player-props (the priciest calls on the tight rookie-tier monthly entity cap) — because the entity budget is near the cap; NO network call (cache_hit=True → excluded from integration-health like a no-op guard), result_count=0, ok=True, caller degrades fail-open (no board/props). The proactive complement to #1177’s scoreboard-cadence throttle: #1177 slows the CORE fetch, this drops the OPTIONAL depth first, reserving budget for the cheapest critical reads. source=api_sports errors (#still-no-art): API-Sports answers an ACCOUNT / PLAN / RATE problem with HTTP 200 + a populated errors object (a suspended account → {"access":"…suspended…"}, a plan gate → {"plan":…}, a throttle → {"rateLimit":…}) and an EMPTY response. _fetch used to hand that back as a healthy [], so a suspended account read as ok=True “nothing live” across every api_sports call (games, live scores, team logos) for days while sports GAME cards shipped art-less — the ops-monitor never saw it. _response_error now turns a populated errors into ok=False (the breaker opens + fallbacks take over), so the outage shows in the api_sports integration-health rate. source=espn_logos (#still-no-art): the KEYLESS ESPN team-crest backstop for the market card’s sports art – when api_sports.team_logo returns nothing (a down / suspended account), EspnClient.team_logos reads the crest off ESPN’s /teams endpoint (query={home} vs {away}, sport, result_count=2 on a matched pair else 0). Both sides must resolve inside ONE of the sport’s ESPN leagues or it returns None (the card keeps the branded floor, never a wrong crest); the team list is cached per league path (static), so this costs a fetch only on the first miss per league. Single-team crest (#full-espn-fallback): EspnClient.team_logo(name, sport) is the one-team analog — the deterministic floor-beater for a card whose subject is ONE team (a futures winner like “MLS Cup Winner” -> its leading club, a standings/award market), and the ESPN fallback inside the wire-desk team card (entity_image._team_fallback_card). It sits BELOW the vision photo search in the market ladder, so a real team PHOTO wins first and the crest only fills a card otherwise heading for the branded floor; exact-name-matched inside the sport’s leagues, so never a guessed crest                                  
billboard_fetch utils/billboard.py source (billboard), query (<chart>:<week\|current>), ok, duration_ms, count (rows parsed), cache_hit (two-speed durable cache: ~12h when current, ~20min while a flip is due); on a miss: reason + error + http_status — one FIRST-PARTY Billboard chart read (Hot 100 / Billboard 200 / Global 200 / Artist 100 / …, one parser for all). reason: blocked (403/451 — billboard.com REFUSING us; its robots.txt names anthropic-ai, an EXPECTED end-state — docs/BILLBOARD_SOURCES.md §5), shape_change (a 200 that parsed to near-nothing = a redesign, the SILENT failure), partial_credits (a 200 whose song/album rows carry an EMPTY artist — the page read MID-PUBLISH: Billboard lands the rows first and attaches the artist links on new entries minutes later; measured 2026-09-09, the Sep 12 Hot 100 one minute after its flip carried 15 of 16 debuts with no credit, was cached 12h, and posted as “Take Care leads 16 new entries … from 2 different acts”. Refused like a shape change: nothing served or cached, a cached week with a credit-less row is re-read too, the next poll re-reads, the block clears itself on the full page; dropped in detail = rows with no credit. Artist-unit charts are exempt — their artist cell is empty by design), http_error, breaker_open. There is deliberately NO stand-in source (owner call 2026-08-04: the GitHub-mirror fallback was removed — it was itself a once-daily crawler of the same site, so it added a hop and staleness, never data): a block yields nothing, loudly. blocked + shape_change + partial_credits also emit error (recoverable=False) on the first failure of a streak and every 10th after, and the ops monitor raises a source_blocked finding naming the action (high for a refusal / a sustained or still-dark run; medium for a partial or empty page that completed within the window)                                    
billboard_artist_history utils/billboard.py source (billboard), query (<slug>:<code>), ok, duration_ms, count (entries parsed = the artist’s entry count on that chart), cache_hit (~6h durable, its own namespace); on a miss: reason + error + http_status — one per-artist CHART-HISTORY read (/artist/<slug>/chart-history/<code>/), the CAREER-ENTRY count a weekly chart page cannot carry (#2154 follow-up). Only the two flagship charts are mapped (billboard200tlp, hot100hsi). reason: blocked (403/451, the same §5 refusal as billboard_fetch), unknown_slug (404 from a GUESSED slug — an ORDINARY fail-open miss, NOT a Billboard block), no_entries (a 200 that parsed to nothing — a soft-404 or a redesign), http_error, breaker_open. detail.slug_source (2026-09-15, #3331) says WHICH page was asked for: chart_row (the slug Billboard printed in the act’s own chart row, read out of the cached week) or guess (artist_slug, the fold of the credit string). The guess does not only 404 — it lands on ANOTHER ACT: BLACKPINK’s LISA is filed as lisa-of-blackpink, so “lisa” answered 200 with a 1980s act’s three Hot 100 entries from 1982-84, and the debut guard read that page as hers. Watch the guess share: it is the fraction of these reads that can still be another artist’s, and it rises when acts reach the read without a row on any chart week we hold (the slug is only remembered for the current week). Deliberately QUIET, unlike billboard_fetch: a miss suppresses one optional “their Nth entry” clause, never a post, so it does NOT touch the weekly-chart block_state() and does NOT emit error — a wrong-slug 404 is not Billboard refusing us. Watch the ok-rate as the source-health signal; a sustained blocked here (not unknown_slug) is the AI-agent block reaching the artist pages too                                    
music_news_debut_framing cogs/music_news.py ok (the debut was CONFIRMED), reason (presence | history | unconfirmed), guild_id, phase (chart), matchup (the subject), source (the wire account), + folded detail: chart (the chart the history was read on), entries (rows that page returned) — did anything CONFIRM that a chart story is a NEW ENTRY (owner steer 2026-09-01). The gap it closes: chart_presence settles debut-vs-established for the live PLATFORM charts only, so a weekly Billboard chart came back is_debut=null and the wire’s word rode through as Toots’ own claim. On 2026-09-01 both “Painted You Pretty debuts at No. 93” and “All My Exes … debuts at No. 92” shipped off @billboardcharts while our own Hot 100 read was waiting_for_flip — the site still served the 08-29 chart, so music_news_chart_lookup said absent and nothing had checked either claim. reason=history is the new confirmation: Billboard’s own artist chart-history page (billboard_artist_history) carries a debut DATE, and it updates EARLIER than the chart page — measured 2026-09-01, it already listed “All My Exes … 9/05/26 … 92”. The date is compared against the CURRENT chart week (billboard.expected_chart_date, the one definition of that), never a rolling window of days: Billboard dates a chart to a Saturday and reveals it the Tuesday four days before, so on reveal day LAST week’s chart date is only three days past and any window wide enough for this week would confirm a second-week title as a debut (Codex review). detail.rewrote says the story RECORD was rewritten too — news_compose_blob builds it from the claim and the self-gate grades the take against it, so a record still reading “debuts at No. 93” is an answer key that marks the debut correct; the claim becomes the bare position through the same settled_claim formatter a live reading uses. reason=unconfirmed hands the compose unconfirmed_debut_note and the take states the bare POSITION in the owner’s wording (“is at No. 93 on the Billboard Hot 100” – the note SHOWS that sentence, naming the position the story reports, because describing the wording got “sits at” instead): the wire’s number still ships, only the unverified claim about the title’s chart HISTORY is dropped — prefer absent over invented. It also vetoes the career-ENTRY ordinal (music_news_entry_ordinal claim=entry), so a post that may not say “debuts” cannot say “their fourth entry” either. Filed only when the wire framed the story as a debut or we confirmed one, so an ordinary climb story never files a row. WATCH the history share of the confirmed rows: it is the ONLY way a Hot 100 or Billboard 200 debut keeps its wording, and it rides a GUESSED artist slug — an act Billboard slugs differently 404s (Hudson Westbrook did) and reports flat. A history rate falling toward zero means the slug guess or the page parse broke, and every real weekly debut is being under-reported                                    
music_news_entry_ordinal cogs/music_news.py ok, phase (chart), matchup (subject), count (the artist’s entry count), + folded detail: chart (the CHART_SOURCES key), claim (peak | streak | weeks | entry) — the newsroom stamped a chart take with a grounded CAREER fact read off the chart’s own pages (billboard_artist_history for peak / entry, the weekly Billboard row for weeks, the kworb daily row’s (x?) days-at-peak column for streak), instead of a guess. detail.claim=entry (#2154) is the debut ORDINAL (“JUNGLE’s fourth Billboard 200 entry” instead of the guessed “first”); it fires only for a debut music_news_debut_framing CONFIRMED (the count is the title’s ordinal only when it is the newest entry, and a new-entry claim she may not state in words must not ship as an ordinal either) and its grounding converts a career_ordinal DROP into a ship. detail.claim=peak (#2587) is a career-BEST milestone (“their first #1”, “their highest-charting entry yet”) off the peak column + the CURRENT position, so it is fresh news and can fire on a CLIMB, not only a debut; it takes precedence over the entry ordinal (one clause ships). It fires only the week the position is REACHED (owner steer 2026-09-08): the settled row’s last_week / weeks_on ride ChartPosition, and a held position, a fall, or a re-entry yields no peak claim — “Choosin’ Texas … her first #1” had shipped off a row reading last week #1, 45 weeks on. detail.claim=weeks is the rung a HELD position gets instead (music_news.chart_run_fact, “in its 45th week on the Billboard Hot 100”, off the row’s own weeks column, a digit ordinal so the digit gate grounds it). detail.claim=streak is the daily twin (owner example 2026-09-08): “has 11 days at #1 on the global Spotify daily chart in total” for a held Spotify #1 that is its own peak, off music_news.chart_streak_fact (the day’s plays ride the settled reading itself: “#1 on the global Spotify daily chart with 5.4M streams on the day”). A held daily #1 reaches the compose only when its streak CROSSED a milestone bucket since the last report (music_news_filtered reason=chart_repost now folds detail.streak, the days-at-#1 count the gate saw; MUSIC_NEWS_STREAK_MILESTONE_DAYS, default 7), so on spotify_global / spotify_us expect a streak row about once a week per long-running #1 and chart_repost rows in between. music_news_filtered reason=streak_unstated is the streak-triggered repost whose composed line did NOT state the streak: dropped and marked seen rather than shipped as a bare repeat of the held #1, and the milestone is not consumed (detail.chart, detail.streak). Measured 0/25 in the dry runs (the compose stated the clause every time), so a rising rate here means the compose stopped folding the CHART HISTORY clause in. Watch the peak : weeks split on hot100: a peak row with the same subject in consecutive weeks is the stale-milestone bug back; weeks should be the common kind for a #1 story, peak the rare one. Both history kinds require the reported title on the page (the wrong-artist guard, music_news.entry_ordinal_fact / peak_history_fact), emit at SHIP time (after the gates + self-score), and only when the composed line ACTUALLY states the fact (music_news.career_clause_in_line) — so the rate is how often a grounded career fact shipped. Watch claim=entry against music_news_scored reason=career_ordinal (the drop): a rising entry rate with a falling drop rate is the fix working. Watch claim=peak on its own — a #1 / career-best is rarer and higher-value                                    
feed_fetch utils/industry_feeds.py source (the feed key: pollstar|iq|mbw|billboard_news), ok, duration_ms, count (items parsed), cache_hit (~15min durable); on a miss: reason (unreachable = no 200 | unparseable = a 200 with zero items, i.e. a feed shape change) + error — one trade-press RSS read (#reporting-oversight). The FIRST-PARTY news source, read directly so a number is in hand rather than chased through a web-confirm that lags a fast story (the R&B Tour gross case: the wire said $153.4M/20 shows, the web-confirm returned the stale $101.9M/13 shows, and Toots posted the stale one). One parser for all four feeds, namespace-driven. Each source has its OWN breaker (integration=industry_feeds, source=<key>) + limiter so one host refusing us does not blind the others. Fail-open (a miss → the desk keeps its X wire) with emit_error(recoverable=True); DARK until a desk consumes it. Per-source ok-rate is the health signal a single feed went quiet                                    
music_ranking (RIAA boards) cogs/music_desk.py reason=riaa_roster_partial (the Diamond boards wait: count acts of the roster read so far) | reason=riaa_timelines_partial (the fastest-to-Diamond board waits: count roads timed so far) | reason=riaa_diamond_list_partial (the Diamond singles and Diamond albums boards wait: the read of RIAA’s own Diamond list stopped short), ok=False — the RIAA record boards’ “absent over invented” rows; both trend to zero as the seven-day caches fill, and a count that stops rising is a finding. Measured 2026-09-03: #2862 put the genre loop between if complete: and the roster-partial else, so the row fired every slot (count=40 of 40) as a for/else; it is an if not complete now, with a test that records the emits.                                    
riaa_fetch utils/riaa.py source (riaa), query (recent | timeline | tallies | artist | awards | genre | diamond), ok, duration_ms, count (rows parsed | history points | tally rows | act rows | awards naming the act), cache_hit (recent ~2h, timeline ~7d, tallies + artist + awards ~7d, durable); on a miss: reason (unreachable = no 200 | unparseable = a 200 with zero rows, i.e. the table changed; the site’s empty end-of-list payload on the page after the last one is the list’s END, not a miss (list_ended, a one-page act read as unparseable before 2026-09-02) | capped = an awards walk hit AWARDS_MAX_PAGES with pages left, so the list is not the whole catalog and is not served) + error. A timeline read of a LATIN-program award is ok=true with reason=latin_program and count=0 (#2902), not a miss: RIAA’s Latin program has its own ladder (Oro / Platino / Diamante, 600,000 units a level against the main program’s Gold 500,000 / Platinum 1 million / Diamond 10 million), so parse_timeline deliberately maps none of it and returns an empty history – but the page itself is perfectly readable. That empty result used to fall through to unparseable, which meant a Latin award was never cached, cost a cold read out of the slot’s three-read budget on every attempt, and filed a recoverable riaa error each time, for a page that would never parse (measured 2026-09-03: award 453588, “DE LA NADA - REMIX (FT. DE LA ROSE)”, a Latin single certified that day). timeline_ladder tells the two apart, so unparseable goes back to meaning the one thing worth paging about: the site changed shape. The newsroom’s cert lane now also skips non-main-program awards outright, so the read is not requested at all — one read of RIAA’s own Gold & Platinum database (#2839, line B of #2800). The certification AUTHORITY read directly: measured 2026-08-20 to 09-01 the wire cert lane shipped 22 cards against ~180 awards RIAA issued (one in eight), and a Julia Michaels 2x Platinum reached no wire at all. recent walks the newest-first feed until a page’s oldest award is older than the caller’s window (the newsroom’s 3 days), capped at 8 pages of 30 (~15 awards land a day, 58 in one burst); timeline reads one award’s history so the take can state the previous level (count = history points, on a cache hit too). Rows for WATCHED/KNOWN acts become first-party SourcePosts (platform=riaa, handle riaa, relay_number=True) into the newsroom’s existing cert lane, so they classify, key (cert_story_key, <n>xplatinum|riaa) and dedup against the same certification from any wire. Gated on music_riaa_certsgraduated 2026-09-07 (master-only), so the gate is graduated_stage and resolves PRODUCTION in the master guild, OFF elsewhere (while it was a trial, anything but PRODUCTION routed its cards to #bot-logs whatever the parent newsroom’s stage, and such an audition never booked the settled cert key; audition=True can no longer occur). A recent read that stops short (a later page unreachable or unparseable) returns the rows it has, emits ok=false with that reason plus an error row naming the page, and is never cached; a timeline miss emits an error row too. One breaker (integration=riaa) + one limiter for the host. Fail-open (a miss → the cert lane keeps its wires) with emit_error(recoverable=True). Cloudflare refuses a bare Mozilla/5.0 (403) and accepts a browser-shaped user-agent; the client and the riaa probe send the same string, and the probe requires award rows in the body (a 200 challenge page without a table is a 502 on the sweep), so a future challenge page reads as a failure here and on the /debug/integrations sweep, never as a quiet cert lane. An RIAA audition’s #bot-logs header named music_riaa_certs as the flip that ships it (no longer reachable since the graduation). On the ops monitor as riaa in the misc integration health (_MISC_INTEGRATIONS, cache hits excluded) and on the data_sources dashboard panel. Expect ~12 recent reads a day per process (a 2h cache under the ~15-min desk tick) plus a timeline read per new watched-act award. diamond (#2915) is the Diamond boards’ own read and is NOT a roster read: it searches RIAA’s award database for every Diamond award of one format (awards[]=D), up to DIAMOND_MAX_PAGES (26) pages, held ~7d. Measured 2026-09-03 the singles list is 516 awards over 55 acts. Before this the two Diamond boards ranked only the 40 names of the streaming watch list, so they MISSED any Diamond holder outside it – the posted card omitted Post Malone (9, which is #2), Lil Wayne (6), Morgan Wallen (6) and Cardi B (4), and two X replies called the board wrong. A short read emits ok=false with capped, unreachable or unparseable and draws NO board (music_ranking ok=false reason=riaa_diamond_list_partial). The RIAA record boards (music_riaa_boards, graduated 2026-09-07 master-only alongside music_riaa_certs; utils/riaa_boards.py) add tallies (the top-albums list, 4 pages, ~7d cache) and awards (one act’s award SEARCH, every award naming the act, up to 8 pages, ~7d cache, at most _RIAA_TALLY_FETCH_PER_SLOT cold walks a desk slot – the diamond boards, the Platinum-singles board and the certified-units board all count features and collaborations off it, one read per act for every board); artist (one act’s awards-by-artist page) is parsed and cached but no board reads it today; a music_ranking ok=false reason=riaa_roster_partial row marks a slot where the diamond board waited for the roster (count = acts read so far), which is normal while a cold cache fills and a finding if it never clears. year_feed (#2871) is the YEAR board’s own read and is NOT a roster read: RIAA’s dated feed walked back to Jan 1, so the board ranks every act RIAA certified rather than the first 40 names of a streaming watch list. It is budgeted – one call takes at most YEAR_FEED_PAGES_PER_CALL (25) pages, resumes on the page it stopped on, and emits ok=false reason=filling until the walk reaches Jan 1, with music_ranking ok=false reason=riaa_year_feed_partial on the lane side and NO board drawn meanwhile (prefer absent: a partial walk under-counts every act). Measured end to end 2026-09-03 the cold walk is 61 pages / 1830 rows / ~9 minutes; a covered walk then tops up from page 1 every ~6h and costs one or two pages a day. It REPLACES nothing – the roster’s awards searches still run for the Diamond, Platinum-singles and certified-units boards, so the year board used to ride them for free and now pays its own way. Held a week (_YEAR_FEED_TTL), that is ~13 reads a day against the roster’s ~16, i.e. about a 45% ADDITION to the RIAA read budget. A 1-day TTL was the first choice and cost ~64 a day, because the row expiring forces the whole cold walk again. Rows merge by award id with the later certification date winning, so a re-certification upgrades its row instead of counting the title twice.                                    
music_desk_career cogs/music_desk.py (#3363) chart (hot100|billboard200), kind (entry — a settled debut’s ordinal projected_entry — the standing a HITS-projected debut WOULD set, #3363), matchup (title - artist), count (entries on the act’s Billboard chart-history page) — the chart lane attached a grounded CAREER STANDING (“her seventh entry on the Billboard Hot 100”) to a row whose own story is thin. Why it exists: measured over 1,034 of our posts (2026-09-08..15) a bare chart position is the worst-performing type on all 45 wire accounts we read (0.69x their own median) and 37% of our output, while a career standing is the best (1.30x) and 1% of ours. Why it is narrow: the A/B improved 4 of 4 debut/re-entry rows but REGRESSED a row that already had a hook (Luke Combs’ “a new peak 24 weeks into its run” 0.92 → “his 43rd career entry” 0.88), so it fires only on debut/reentry, and only when music_news.is_latest_entry confirms the title is the act’s newest (an ordinal on an older title implies a recency it lacks — Ariana Grande’s 99th of 110). Emitted only when a fact was produced, so the rate is how often the clause is available; fail-open, every miss ships the card unchanged. projected_entry is the forward twin on the projection lane: it swaps that framing’s flat career ban for the one grounded fact. The ban was not producing silence about a career — measured 2026-09-19 it produced a WRONG one, “a solid new entry with no chart history to lean on yet” about Dolly Parton, who has 51 Billboard 200 entries. Its guards refuse a title already on the history page (a catalog re-entry, not a debut) and an empty page (a guessed slug that 404s reads the same as a genuine first-timer)                                  
pollstar_fetch utils/pollstar_charts.py source (pollstar_charts), query (the chart key: global_concert_pulse|live75|new_tours|artist_power_index|mediabase_<format>, plus a <key>:issue row per ISSUE RESOLUTION — #3357, the config read that finds the current week’s chart id, kept separable from the body read so one failing does not hide the other; it carries folded detail.issue_id), ok, duration_ms, count (rows parsed), cache_hit (~6h durable, keyed by ISSUE so a new week busts the body cache at once); on a miss: reason (unreachable|unparseable|no_issue — the config did not parse, so the read falls back to the seed issue and the freshness fence takes it from there) + error — one Pollstar free-tier chart read (#reporting-oversight). The live-music numbers Billboard does NOT publish (worldwide active-tour ticket averages, avg boxoffice gross/city, new-tour on-sales, 24 Mediabase radio formats), off the public chart JSON at data.pollstar.com, anon-only — no auth is ever sent, and only the free-preview rows the endpoint returns are taken. ToS one-way-door (owner-accepted, epic issue): Pollstar’s terms forbid redistributing chart content and forbid stripping attribution, so every posted number MUST be attributed to Pollstar (PollstarChart.attribution + the chart link). Breaker integration=pollstar_charts. Fail-open with emit_error(recoverable=True); DARK until a desk consumes it                                    
artist_news cogs/music_news.py guild_id, count (synthetic candidates produced this slot), + folded detail{checked, hits, stale, top, tail} — one PROACTIVE watched-artist news pull ran (owner steer 2026-08-20). The newsroom’s reactive feed only reports what a followed account posts; this lane inverts it: each slot it rotates a bounded set of the top watched artists (top ~25 fast, the ~75 tail slow — the split is detail.top/detail.tail), asks Perplexity for each one’s biggest recent RELEASE / CERTIFICATION / TOUR update, and feeds any answer through the SAME classify → tier → verify → compose → gate pipeline as a synthetic SourcePost (platform=artistpull, source tag artist_watch, which may_relay_number reads as untrusted so the pull story ships ONLY when independently verified). checked is the artists swept, hits the ones whose pull returned FRESH real news, stale the ones dropped because the model’s stated event date was old or missing (#2488 — the pull’s recency gate; the pull has no source-tweet time, so a now-stamped synthetic post would otherwise defeat the _RECENCY_HOURS window, which is how a nine-month-old $1B tour record shipped as “breaking”). (count then trails hits after the seen-set dedup and the _ARTIST_PULL_MAX_STORIES slate cap.) Gated on the music_news_artistpull experiment (STAGING default; rides the parent newsroom’s staging-vs-production routing like the other overlays). count sitting near 0 while pplx_music_news stays green is the COMMON case — most swept artists have no news that sweep, which is correct; checked collapsing toward 0 while kworb’s chart_fetch stays green = the watchlist build broke. STREAMING milestones AND chart positions are dropped here (the desk owns both) — see music_news_filtered reason=artistpull_desk_owns. A SALES-unit milestone is a known gap (no ladder, no discovery, no dedup key), tracked as #2793. The pull’s shipped stories are normal music_news_scored/music_news_posted rows with the artist_pull lane tag. detail.mode distinguishes the two proactive lanes that emit this event: pull (Perplexity, certifications/tours) and newrelease (Apple catalog, album drops, owner steer 2026-08-21). The deterministic new-release lane (_gather_new_releases) reads each rotated watched artist’s Apple Music discography (keyless, free — a deterministic read the metered pull can miss; the top ~25 swept every slot are caught for sure, the tail best-effort), turns an ALBUM (singles/remixes filtered) dated within _NEW_RELEASE_WINDOW_DAYS into a release candidate with a STABLE newrelease:<collection_id> id (a drop posts once), and rides the SAME pipeline; gated on the music_news_newrelease overlay (OFF default — dark until a mod opts in, since it rides the parent stage for routing). Apple OWNS releases (the pull no longer asks for them). checked/hits read the same for both modes; count near 0 is the common case (a watched artist rarely drops an album in a given week)                                    
artist_watch cogs/music_desk.py guild_id, count, detail{checked, tracks_windowed} — one TOP-ARTIST WATCH milestone sweep ran (owner steer 2026-08-04): the watched artists’ free kworb Spotify metrics (career streams, monthly listeners, cumulative totals for their tracks on the Global Daily chart) through the SAME songstats_milestone_state rungs as the opportunistic Songstats checks, so the two detectors share one high-water baseline and can never double-fire. count is crossings composed this sweep (usually 0 — rungs are sparse by design), checked the rung checks run, tracks_windowed the watched charting tracks eligible for the bounded, hour-rotated track-page window. The watch’s other halves (lower chart/radio floors, the wider callable band for watched artists) ride the existing lane events — a watched chart move is a normal music_desk_scored phase=chart row. The sweep going silent = the watch is dark (kworb miss / empty watchlist); checked collapsing toward 0 while kworb’s chart_fetch stays green = the watchlist build broke detail.mode=tenure (#2800 line C) marks the CHART-TENURE pass on the same event: checked = watched rows on the Spotify daily charts that carry days, hits = rows at or past the first rung (100 days), count = tenure cards composed. hits near 0 while the chart sweep is healthy = the watchlist has no long-running songs, which is normal for a young roster; checked at 0 while watch_chart cards flow = the days column stopped parsing.                                    
milestone_source cogs/music_desk.py surface=”music_desk”, matchup (the artist), source=”kworb”, ok (do kworb and Songstats land on the SAME career rung? null when Songstats returned nothing), reason (resolved no_row no_total feed_out), detail{metric, kworb, songstats, gap_pct, matched} — one CAREER-STREAMS rung check resolved its source (_kworb_career_total, 2026-09-14). Why it exists: the career rung is the ONE milestone metric on which this repo’s two Spotify sources disagree. Measured live over 14 top acts: monthly listeners agreed to 0.00% on 8/8 and track totals to ~0.1%, while career totals ran 0.0%–4.3% apart with Songstats always higher, and 2 of the 14 sat in the gap band where Songstats clears a rung the kworb board does not. That gap shipped the Weeknd “crosses 100 billion career Spotify streams” card on 2026-09-14 — Songstats read 102.1B, kworb 97.9B, and the card’s own standing strip (kworb) printed #4 with 97.9B beside the 100B hero. The Songstats artist total also RESTATES DOWNWARD, which a high-water rung cannot represent (Bad Bunny posted “at 133.3B” on 09-13, the same field read 130.7B a day later). kworb now drives the rung alone and this event keeps the divergence visible after it stops driving the break. ok=false is the useful series — it counts the checks where the old path would have fired early, so a rate near zero means the sources have converged and a rate climbing means the gap is widening. A gap_pct turning NEGATIVE (Songstats BELOW kworb) or widening well past ~5% means kworb has started lagging and the source choice needs a re-look. reason is the funnel. no_row means no EXACT name match on the board. That covers an act the board does not carry AND a COLLAB credit: kworb’s strict match is a CREDIT test — right for a rank, wrong for a career total — and live it hands back Drake’s row for “Drake & 21 Savage” (his solo 141.0B) and Future’s for “Future & Metro Boomin”, so this path does not use it. detail.matched carries the board’s CANONICAL name, which is also the rung’s entity key: it is shared with _watch_milestone_units, and folded matching makes the spellings differ often (“Beyonce” → “Beyoncé”, “JAY Z” → “JAŸ-Z”, already in the live table as artist:jaÿ-z), so keying by the news subject would be a second state row over one figure. feed_out is the one to alarm on: the all-artists board came back EMPTY, which stops EVERY career milestone, so a run of it is an outage rather than a quiet day. no_row is an act off the board, normal below the ladder’s 50B floor. Panels: “Career-streams rung: how the kworb figure resolved” and “Career-streams source gap” on tootsies-desks. The event absent entirely = kworb is unwired, and NO career milestone can fire                              
catalog_sweep cogs/music_desk.py guild_id, count, detail{read, readings, roster, mode} — one CATALOG-COUNT sweep ran (detail.mode = total or daily, #2800: the same page counted twice, once per column; one event each) (#2772, owner question 2026-08-31 “why didn’t we proactively find different stats about artists and genres?”). Every other music lane fires on a printed chart week or on one scalar crossing a round number; this one asks the CORPUS a question. Each slot it rotates a bounded window of watched artists (MUSIC_DESK_CATALOG_FETCHES, default 6), reads each act’s kworb SONGS page, counts the rows at or above each rung in catalog_stats.CATALOG_RUNGS (1B and 2B), and upserts the counts into artist_catalog_counts — the GLOBAL, cross-guild field a rank is taken over. A count that TICKS UP breaks through the shared songstats_milestone_state rungs as metric catalog_count, so seed-on-first-sight holds and a rung breaks once. read is pages fetched this sweep, readings the BOUNDED counts stored, roster the watched acts carrying a kworb artist id. count near 0 is the NORMAL case — a song crossing 1B or 2B is rare, so the sweep’s job most slots is to fill the corpus, not to post. That makes count useless as a health signal: read read (0 = the sweep is dark, a kworb miss or an empty watchlist) and readings (collapsing while read holds = the songs pages truncated above the rung, so count_catalog marked the counts unbounded and none may be stated). The corpus ageing out is the quiet failure: catalog_counts_at_rung drops rows older than 30 days, so a sweep that stalls silently shrinks the ranking field until coverage_ok (90%) gates every rank — and because the lane refuses to compose without a ranking clause, the lane then goes SILENT rather than shipping bare counts. That is deliberate (a bare count lets the compose invent a record) but it means a stalled sweep looks exactly like a quiet one on count alone; read and readings are what separate them. The ROUND-COUNT path (owner ask 2026-09-10): a TOTAL count landing on a multiple of five with no rank clause goes to the candidate with the web qualifier REQUIRED, so its outcome is NOT in this event – read qualifier with phase == 'catalog_stat': verified is a card in flight, required_missing is the lane finding no reported ranking (detail.settled true = the rung advanced, no retry; false = a transient miss, retries next slot). Measured before the path shipped (2026-09-10): 224 total-mode sweeps in 14 days, 0 cards. Gated on the music_catalog_stats experiment (STAGING default)                                    
desk_art cogs/music_desk.py + cogs/music_news.py + cogs/cinema_desk.py + the three WIRE desks (cogs/pop_desk.py, cogs/sports_desk.py, cogs/cinema_news.py) surface, ok (art bytes returned), source (the rung that answered: artist_photo discography album_search song_search release release_discography catalog_cover artist_portrait tmdb_poster deezer_artist deezer_artist_exact wiki_logo wiki_portrait steam_art …). wiki_portrait is new (#deezer-403) — the Wikidata/Commons artist photo, the SECOND provider under Deezer’s artist picture. Until it existed every artist photo in the repo came from Deezer alone, and on 2026-09-14 Deezer began answering 403 to Railway’s IP: a music card whose title the Apple catalog does not carry (a new radio single is the normal case) then had no rung left at all and shipped bare, which is the David Banner “+8” most-added card. Read its SHARE the way artist_portrait’s is read, and with the opposite sign: wiki_portrait is a FLOOR, so a low steady share is health and a share that climbs says Deezer has gone quiet again. steam_art is new (#2741) — the Steam catalog rung for a VIDEO GAME subject, which the wiki_image rung structurally could not serve (pageimages excludes the non-free box art every game article leads with, so a game resolved NOTHING and the card fell to the branded floor). Watch it the way artist_portrait is watched: steam_art appearing where a game subject used to produce none is the fix working, and a game subject falling back to none again is the signal that Steam’s storesearch leg has gone quiet — a silent, fail-open degradation that raises no exception (the steam integration probe is the other half of that watch). music_desk’s single catalog label split into catalog_cover vs artist_portrait (#2274) when resolve_art_url gained a portrait floor: a cover is the record, a portrait is a stand-in for a record the catalog could not supply, and one label for both would let a rise in portraits read as healthy art coverage. Watch the portrait SHARE — it is the signal for a cover-lookup regression, which is otherwise silent because the card still ships a picture. It was HIGH on the song lanes until #2278 fixed the subject-kind title guard; live A/B moved 24 of 45 song rows from artist_portrait to catalog_cover, so the expected steady state is now mostly covers, with portraits only for a release the catalog genuinely lacks. A portrait share climbing back toward the covers is the regression signal. The desk_art panel groups by source dynamically, so it picked the new values up with no axiom_setup.py change, reason (on a miss: no_url = the resolver found no URL download_miss = a URL resolved but the image GET failed no_art no_subject error). music_desk’s _art_bytes now names which of the two it was (#deezer-403); it collapsed every miss to no_art, so the David Banner card could not be told apart from a CDN blip without replaying the whole ladder by hand. Its sibling _fetch_art always split them, and the two now match, phase (take_retry on the wire desks only) — a desk’s ART resolution resolved or missed. The art path used to be silent, so an art-blank card (“why didn’t it have art”, owner) could only be spotted by eye: a *_posted carded flag says a card was BUILT, never that it had art. The music newsroom now emits it for EVERY story, including the ones with no number (#wrong-missing-images): it used to return before resolving art whenever the story had no figure, so the whole RELEASE lane resolved no art and emitted no event — the lane’s art misses were invisible, and its posts shipped imageless in the room and on the timeline. Expect a small rise in surface == 'music_news' volume from that lane alone; a release story whose art misses is now a countable ok=false instead of silence. The three WIRE desks emit it only for their LAST rung (phase=take_retry): they resolve art BEFORE compose on the wire HEADLINE, and a headline that names no entity (a chart/roundup title) classifies to an empty subject list, so that pass has nothing to resolve — the composed TAKE is then the first text that names a subject with art, and the retry runs on it. Query phase == 'take_retry': ok=true counts posts that would have shipped bare, ok=false counts the ones that still do (the residual, also visible as the *_posted source=none rate). cinema_desk’s bare-text lanes now carry count (#individual-art) — the number of posters resolved for the take, one per show it names (the release “what’s out” roundup resolves several; a single-title news/awards take resolves one). count > 1 is the multi-poster path; count == 0 with ok=false is a lane that resolved no art at all. music_news also carries tour (#tour-announcement, folded into detail) — whether the card took the TOUR path (_claim_is_tour or _is_tour_subject). It is the queryable half of that fix: tour == true with source == 'deezer_artist' is the intended photo-first result, while tour == true with source == 'catalog_cover' is the BACKDROP net (no artist photo resolved, so the act’s own cover keeps the card). A backdrop share that climbs says the artist-photo rungs have gone quiet, which is silent otherwise because the card still ships a picture. music_news also carries bare (folded into detail, 2026-09-14) — a TRI-STATE: true shipped the picture with NO card drawn over it, false was a candidate for that rung and kept its card, and ABSENT was never a candidate. One rung takes it: a figureless TOUR announcement whose art is feed_photo, the image the publisher filed WITH the story, AND whose image the image_is_tour_admat judge says lists shows. It records what SHIPPED, never what was attempted (Codex review on #3291): the judge can say no, and download_image_bytes validates only the HTTP status and the body length, so a non-image body reaches build_bare_art_card and comes back None — in both cases the branded card ships, and a bare=true row there would be a lie told by the one signal this rung has. So read false as the judge’s refusals: a run of them is how a judge that has started refusing everything becomes visible, which is otherwise silent because the card still ships. Read it against delivered == 'room' (Codex review on #3291): the card is built before _sched_deliver branches on the stage, so a STAGING audition runs the same route and would otherwise inflate the shipped-bare share with posts that only reached #bot-logs — the same dimension desk_link carries, for the same reason. That image is the tour admat, so it already prints the tour name, the act and every date; the card repeated all three in its headline and its bottom scrim covered the date list (owner steer, the Malcolm Todd “Do That Again Tour” post — “this probably looks better without our markup on it”). Read bare == true as a SHARE of tour == true, never as a count: the rung is rare because feed_photo itself is rare. A bare share that climbs past the tour lane says the feed rung started answering stories it did not answer before, and those posts now ship unbranded — no wordmark, no tag, no source stamp — which nothing else in the ledger reports
release_art_stale cogs/music_news.py surface (always music_news), source (the rung that answered INSTEAD — a portrait rung, or none when no art resolved at all), reason (the REFUSED row’s release date, or undated), detail.topic (the story subject) — a RELEASE card refused a matched catalog row because the row is from a different era than the story, so its cover would have claimed the wrong record (_row_is_a_different_record). The case it comes from (#3171): Linkin Park’s “Faint [Live in São Paulo]” dropped 2026-09-11 off the UNSHATTER film soundtrack, the wire named only “Faint”, and resolve_music_row answered with the 2003 Meteora recording — catalog_art._rank_rows prefers the clean original over a live edition when the requested title does not name one, which is right for a cert or chart story and wrong for a drop. The card then stamped NEW RELEASE over a 23-year-old cover. The same shape shipped the 2006 “IRREPLACEABLE” cover on five B’Day 20th-anniversary cards and the 2009 “Fear” cover on “Fear of Missing Out”. Read source, not just the rate. The guard trades a wrong cover for the act’s PHOTO, so a healthy steady state is mostly portrait rungs; a rising source == 'none' share means it is trading covers for BARE cards instead, which is a worse post than the one it replaced and the signal to revisit the window (_RELEASE_ROW_MAX_AGE_DAYS, 365 days). Measured before shipping: over 30 days, 19 of 71 catalog-matched release cards would fire this, about 13 of them plainly wrong art. A rate far above that says the window or the throwback carve-out (retrospective.states_past_interval) is mis-calibrated, not that the bug got worse                                    
tmdb_calendar_filter utils/tmdb.py (releases_between) source (tmdb), op (discover_upcoming), count (rows KEPT), query (the window, <start>..<end>), + folded detail{dropped, undated, stale} — the coming-soon calendar DROPPED rows TMDB dated outside the window it was asked for (#3285). dropped is the stale-dated rows; undated is rows TMDB gave no date at all, counted apart because the two faults are different and one number for both reads as noise. TMDB matches a film on any US theatrical date in the window but returns the film’s PRIMARY date, so a re-release comes back in-window carrying its original date: Avengers: Endgame dated 2019-04-26 led the live 2026-09-15..2026-09-28 query, reached the card under the hero “Opening Next”, and the take called a 2019 film the most anticipated of the fortnight. The correct date is not in the response, so the row cannot be dated and is dropped — absent beats invented. This is a SILENT, fail-open drop: it raises nothing and only shrinks the board, so this event is the one place it shows. Emitted on EVERY build, not only on a drop: count is the starve signal, and a starve does not need a drop — TMDB returning six in-window rows is the same blackout as TMDB returning twenty of which fifteen are stale, and a drop-only event is missing exactly then. Steady state measured over five consecutive 14-day windows is 0–3 dropped against 17–20 kept, for a card that draws 10 and needs 5. WATCH dropped climbing while count falls: that is the calendar starving, and below 5 kept the board goes absent with no other signal. READ IT PER BUILD, never summed across a bin: the 5-row floor is a per-build threshold, so two starved builds that kept 3 rows each sum to 6 and read as healthy. The panel graphs min(count) (the bin’s worst build) against max(dropped), with count() alongside as the cadence                                    
<surface>_scored reason=retro_unframed / reason=retro_interval cogs/music_news.py, cogs/pop_desk.py, cogs/cinema_news.py, cogs/sports_desk.py guild_id, score (always 0.0), shipped=false, matchup (subject), post_preview, + folded detail: frame (the wire_retrospective frame the source post carried) — a THROWBACK take was composed and then DROPPED because it stated no look-back frame of its own (retrospective.frames_as_past). The case it comes from (#3270): @Genius posted “eight years ago today, 6LACK dropped ‘east atlanta love letter’”, the desk detected the throwback and told the compose to write a look-back, and the take that shipped to X read “East Atlanta Love Letter debuted at No. 3 on the Billboard 200 with 77,000 equivalent album units in its first week” — past tense, no time frame, so it reads as this week’s debut for a 2018 album. Nothing else catches the class: the self-gate grades whether the claims are GROUNDED and every word of that line is true, so it scored high and shipped. Read this rate against wire_retrospective on the same surface — that is the denominator (how many throwbacks the desk framed) and this is how many lost the frame on the way out. Expect it NEAR ZERO: the note that names the frame was measured on the production compose model with the real inputs at 12/12 framed, against 4/12 for the note it replaced. A sustained rate means the note stopped landing (check whether a block below it in the context turned present-tense — the X CROWD READ block, which says “what people on X are saying RIGHT NOW”, is what pulled the old note off in the dry run), and a rate at or near 100% on one surface means its compose never sees the note at all. reason=retro_interval is its twin on music_news: the take DID frame itself, but with an era the wire never gave it (“back in 1999” off an eight-year-old album), so the numbers inside its look-back clause are not the ones the source fixes (retrospective_values); detail.numbers carries the invented ones. Read it as a fabrication rate, not a framing rate — the self-gate cannot catch it, because the claim is true and only the date is wrong. Its FIRST live hit was a false positive, and the rate is how it was found (2026-09-15). One drop, on a @Genius “today in 2007” wire about “Crank That (Soulja Boy)”: the take quoted the source’s own year back, and the gate called 2007 an invented era because retrospective_values derived the interval 19 from a year frame and never listed the year itself. Read a retro_interval drop against the wire_retrospective FRAME on the same surface — a drop whose detail.numbers is a four-digit year on a today_in / this_day_in source is the take agreeing with its wire, not inventing an era. On music_news the drop marks the post SEEN, so a hit costs that anniversary; that is the trade the WAP re-roll bought (a re-roll pays verify + compose + score again for a phrasing lottery), and the lever that keeps anniversaries in the lane is the note, not a retry. Panel: desk_guard_drops (tootsies-desks), added with this reason — it graphs every deterministic refusal that rides the LANE’s own phase rather than shape_reject, so ungrounded_number and career_ordinal land on it too; before it, those reasons were documented here and graphed nowhere. NO monitor, the same call desk_shape_reject makes: a guard firing is the guard WORKING, and there is no calibrated rate yet that separates “the note is landing” from “a prompt regressed” — the panel builds that baseline first                                    
deezer_lookup / deezer_track utils/deezer.py (_get_json, via every read’s timed_event block) query (the search term or a artistpic: / albumcover: / chart: / track_ prefixed key, truncated), hit, ok, error, duration_ms — ONE Deezer read. ok told the truth only from 2026-09-15 (#deezer-403). _get_json is fail-open: it catches its own error and returns None, so a dead Deezer left the surrounding block through a clean exit and timed_event stamped ok=true. Measured over the 2026-09-14/15 outage, while Deezer answered 403 to every call, deezer_lookup reported ok=true on 402 of 402 reads. The only trace was an error event marked recoverable, which error triage discounts — the exact fail-open blind spot the integration-health rates exist to cover. _get_json now calls event().fail(...) on a failed read, and on an {"error": ...}-with-HTTP-200 body whose code is NOT 800. Code 800 (DataException, “no data”) is deliberately excluded: Deezer answers both outcomes through the same envelope, and 800 is a HEALTHY request for a row that does not exist (a delisted track id, an artist with no album), verified live against /track/999999999999. Counting it would report an outage every time a pooled clip id went stale — the same ok=False contamination catalog_search had to correct. A read that SUCCEEDS also clears an earlier failure in the same block, because fail() latches and several blocks read more than once (album_cover tries two search terms then the discography rung), so without it a block that missed once and then found the cover reported ok=false while the card shipped correct art. Both kinds are registered in ops_monitor._MISC_INTEGRATIONS under ONE deezer bucket, not split per query: the query field is a raw search string, so a per-query split would make one throwaway bucket per artist, and this failure is total anyway — the host refuses every call or none. Deezer is the repo’s ONLY source of an artist PHOTO and it is fail-open twice over (the client returns None, each caller reads None as “no match”), so a dark Deezer silently empties the portrait rung and the genre boards rather than raising. This rate is the only thing that says so.                                    
catalog_search utils/apple_music.py (search_catalog) source (always itunes_search), entity (song | album | artist, FOLDED into detail so it costs no column — query it as parse_json(detail)['entity'], which is what the panel does), ok, count (rows returned), query (the term as sent, truncated to 120) + limit (the clamped row cap) — with entity these are the TRIPLE that reaches iTunes, and therefore the measured cache KEY. limit is in it because callers pass 1, 5, 10, 50 and 200 and a cached 5-row answer cannot serve a 50-row request, so keying on the term alone counts incompatible calls as interchangeable and overstates the reachable hit rate (Codex review on #3207). query and limit keep their own columns while entity folds into detail, and the panel reads each from its own place — a live run is how that was learned. (calls - dcount(key)) / calls is the share a cache could have served, the panel’s reuse_pct; it is NOT calls-per-key, which reads 100 where the avoidable share is 99% and 1 where it is 0%. Truncation caveat: two terms sharing a 120-character prefix measure as one, which chart subjects and catalog titles never approach), duration_ms — ONE iTunes catalog search. The repo’s busiest music lookup, and until #3200 the only outbound step in the family with no event at all. resolve_music_row is built on it, so every desk card and every release link (#3188) passes through it — and song_release_date reads the same Search endpoint directly, matching on raw iTunes fields the normalizer drops, so it emits the same kind from its own call site rather than being left out of the count (Codex review on #3207); its siblings catalog_lookup and cover_art_fetch were already instrumented and it was not, so its latency, its fail rate and even its VOLUME were unknowable. That is what made “should these reads be cached” unanswerable from telemetry: the only numbers available were proxies (desk_art counts, apple_debut counts). ok is the TRANSPORT outcome and nothing else — False only when the read genuinely failed (retries exhausted, timeout, bad body), which _itunes_get reports by returning None rather than [] since #3207. A rowless answer from a working call is ok=true, count=0, reason=no_rows. Getting this split right took three passes and both wrong versions broke a shared panel: semantic-miss-as-failure told an operator to check the API key over a title iTunes does not carry, and then unconditional success would have made int_ok and media_ok read 100% healthy straight through an iTunes outage. Every success-rate consumer — ops_monitor._MISC_INTEGRATIONS, int_ok, media_ok, the last two off the shared _OUTBOUND_KINDS/_MEDIA_KINDS inventories — means one thing by ok=False, so this event means that thing too. Because that is what ok=False means, the kind IS registered in ops_monitor._MISC_INTEGRATIONS (as itunes_search), so a real outage on the repo’s busiest music lookup raises integration_unhealthy; it was left out while ok=False still meant a semantic miss, and leaving it out after the flip would have meant a total outage raised nothing there (Codex review on #3207). The rowless SHARE is the separate question and stays on the dedicated panel as empty_pct, over ANSWERED calls only (ok==true and count==0) — a raw empty COUNT is not comparable across entities or dashboard windows, since 100 of 10,000 and 100 of 100 render the same, and counting bare count==0 read an OUTAGE as “the catalog holds nothing” because a failed read carries count=0 too, so the panel also carries fail_pct in its own column (Codex review on #3207). Transport failures also still reach error triage via _itunes_get’s emit_error(source=apple_music), a 200 with no results list included. A dedicated ops-monitor finding on the ROWLESS share is the follow-on, once a baseline exists to set a floor against — the same reason #3199’s floor is deliberately loose. Watch count too: a drift toward thin answers shows before it becomes zero. Registered in the unified outbound accounting — axiom_setup._OUTBOUND_KINDS and events._VENDOR_BY_KIND (vendor apple_music) — so the overview’s outbound-calls table and the health board’s vendor rollup both count it; a kind missing from those is silently dropped from both (Codex review on #3207). An empty QUERY emits nothing — that is a caller bug, not a search.                                    
desk_link cogs/music_desk.py + cogs/music_news.py surface (music_desk | music_news), ok (a url resolved), phase (the lane — ktt2_chatter | release), reason (no_match = the catalog held no EXACT-name artist row; link_only | no_link | no_card = which way a newsroom RELEASE went), delivered (newsroom release only: room | staged) — a desk card’s outbound LINK resolved or missed. The link half of the desk_art watch, added with the forum-chatter card (#3035). It exists because the miss is SILENT in every other signal: the link is optional, so a miss raises nothing, drops no post, and changes no *_posted field — the card simply ships without its button, which nobody sees unless they remember there used to be one. Read the ok=False SHARE per phase, never the raw count: the chatter lane posts once or twice a day BY DESIGN, so one miss is noise and a run of them says the exact-match resolver (apple_music.resolve_artist_link) has gone quiet or the lane’s subjects have moved to acts iTunes does not carry under that spelling. It is deliberately NOT music_link_missing, which is /music’s HARD degradation (a drop with no link is recomposed and counts against the music surface in the ops monitor’s DEGRADATION_KINDS); a missing chatter link costs a button, not a post. music_news’s release phase is a DECISION, not only a health signal (2026-09-12). The newsroom now posts a new release as EITHER the music link OR the card, never both (owner steer: “new releases shouldn’t have a card and music link. only a music link, probably, if it exists. it’s one or the other”), so ok=true/reason=link_only counts the drops that shipped as the take plus the Apple Music link with no card, and ok=false/reason=no_link counts the ones whose catalog row held no link, so the card shipped instead. A THIRD reason, no_card, is the double miss: no link AND no art, so the take shipped as bare text and there is no card either — it is broken out because a no_link that silently meant “nothing shipped but words” would hide catalog-and-art degradation inside the healthy-looking side of the split. Read the three together as the lane’s split. Read the SHIPPED split off delivered == 'room' (2026-09-12). The card is built before _sched_deliver branches on the stage, so a STAGING audition — a post that only ever reaches #bot-logs — runs the same route and emits the same row; the decision is also emitted before the send, so a failed send counts too. Without that filter an auditioning guild’s decisions read as shipped posts. Read them ALSO against music_news_posted carded: a release that ships its link is carded=false and is NOT a degraded post, so the carded rate on phase=release no longer means what it means on the stat lanes. A no_link share that climbs says the catalog lookup (_artwork_and_link) has gone quiet, which is silent otherwise — the post still ships, just as a card                                    
music_ranking utils/ranking_strip.py (the ONE lookup home since 2026-09-08), gated per surface by cogs/music_desk.py, cogs/music_news.py, cogs/music_alert.py surface (music_desk | music_alert | music_news – the newsroom’s streaming-milestone card, added 2026-09-08 so a Spotify milestone keeps its rank | market_card), ok (did a standing strip attach), reason (attached = the artist strip | song_attached = a per-track milestone showed the SONG’s all-time most-streamed rank | daily_attached = a per-track milestone showed the song’s DAILY chart position | units_attached = a UNITS/SALES Luminate card showed its projected Billboard 200 rank | no_match = no ranking found (artist off kworb’s board, or the release not in the HITS document) | not_artist = an ALBUM streams reading, whose subject is not an artist | kworb_off | error), guild_id — a card TRIED to attach a ranking STANDING strip under the figure, so a reader can gauge WHERE the number ranks. A per-TRACK streams milestone runs a LADDER (owner ask 2026-08-27): the song’s all-time most-streamed rank (song_attached), else its daily Spotify chart position (daily_attached), else the artist strip; a UNITS/SALES card shows its projected Billboard 200 rank for the same chart week (units_attached, cb.projection_rank_standing off the freshest HITS document); every other metric keeps the artist’s Spotify standing (their most-streamed rank + monthly listeners, per kworb). (A radio-riser card also carries a rank strip — its position is on the kworb row, so it is deterministic and rides no event.) Attaches on BOTH the Luminate streams card AND the Spotify streaming-milestone big-number card (the one home is _artist_ranking_strip). The music ALERT lane attaches the SAME strips on its Luminate cards (surface music_alert) — the artist Spotify standing on a streams card (music_alert._streams_standing) and the projected Billboard 200 rank on a units/sales card (music_alert._units_standing) — it was missing both before, so a ‘cleared 12B streams on the year’ alert shipped a bare number card (owner report: missing ranks). The strip now also renders on the LIVE/last-call PROJECTION card, not just the settled number card: _projection_number_figure carried no standing slot, so #2636’s units rank never showed on the projection card (the common units card) on EITHER lane; it does now. Gated on the music_stream_ranking experiment, which is LIVE (default PRODUCTION) since 2026-08-25; the strip adds a PUBLIC claim that can mirror to X and can be wrong (a stale rank / a name-fold mismatch), so it stays FAIL-OPEN and a mod can still dial it OFF/STAGING on /menu (both leave the strip absent). The strip is FAIL-OPEN — a miss just drops the context and the card still ships the number — which is exactly why this event exists: an ok=False rate is the ONLY way a silent “the strip never attaches” surfaces (it raises no error, and the card looks fine). The MARKET-CARD lane attaches the SAME artist strip on a Kalshi artist COUNT market (surface market_card): a “: Highest daily YouTube view count in August 2026" card heroes a raw count for one act and said nothing about the act's scale, so `market_cards.build_market_card` now stamps `number_figure['standing']` = "#75 most-streamed artist · 94.2M monthly listeners" via `market_subject_image.artist_chart_standing` (owner ask 2026-08-30: add artist ranking). That lane is UNGATED -- it rides the card's own standing ladder (the video read first, the artist read second), not the `music_stream_ranking` experiment -- and it emits `attached` / `attached_youtube` / `no_match` / `feed_down` / `kworb_off` / `error`. `attached_youtube` is the rung that answered: on a YOUTUBE market the strip reads the act's YouTube ranking rather than its Spotify one, because the two platforms rank acts very differently (live 2026-08-30: Drake #1 Spotify / #58 YouTube, Morgan Wallen #52 / #263) and a Spotify rank under a YouTube count is the wrong scale. A YouTube market for an act that page does not carry falls back to Spotify and reports plain `attached`, so a persistent all-`attached` rate on YouTube markets means the YouTube page stopped answering. The emit lives INSIDE `artist_chart_standing`, not at its call site: every failure reaches the caller as the same None, so a caller that classified the outcome itself reported an outage as `no_match`, which reads here as a name-matching regression (Codex, #2702). A market outside the artist count family emits nothing -- it is not a miss. **`feed_down` is the reason to watch on ALL THREE lanes** (added #2702, Codex): kworb's `_fetch` NEVER raises -- a timeout, an IP block, an open breaker and a page-shape change all fail open to `[]` -- so an outage used to arrive as the same empty standing an unranked act gives and was counted `no_match`, i.e. an outage read here as a name-matching regression. `ArtistStanding.feed_ok` is False when BOTH ranking pages come back with no rows at all (a healthy kworb carries ~1000 on each), and all three lanes now report that as `feed_down`. So: `feed_down` = go look at `chart_fetch`; `no_match` = the name match really did break. Rank derived from kworb's `artist_standing`, which rides the shared `chart_fetch` health, so a `no_match`/`error` streak with `chart_fetch` green means the name match broke, not the feed. That match is now STRICT by default (`_find_by_artist(strict=True)`, the #2520 credit test): folded CONTAINMENT read 267 of kworb's live top 1000 acts as a DIFFERENT act (measured 2026-08-30 -- 'Enrique Iglesias' took Sia's #51, 'T.I.' took Justin Bieber's #5), so a `no_match` rise right after that change is a refused near-name, which is the intended trade (absent over a wrong public rank)                                    
qualifier cogs/music_desk.py surface (music_desk), ok (did a qualifier verify, or did the guard pass), reason, guild_id, matchup (subject), phase (the lane), post_preview (guard hits), detail{kind (ordinal/superlative/rank), stage, claims, enforced} — the VERIFIED RANKING QUALIFIER step for a milestone caption (owner ask 2026-08-24; _qualifier_block + _drop_for_ungrounded_ranking, pure core utils/milestone_qualifier.py). The qualifier is the RANK a milestone gives an artist (“the first female rapper to hit the mark this year”, “her highest debut”, “their tenth release past 100M”) — the engagement driver. Two sources, tried in order: a DETERMINISTIC catalog count off kworb (“Frank Ocean’s 9th song to pass 1 billion”, exact + free, no judge), else a WEB path — a Perplexity LOOKUP of the web and, when Grok is provisioned, a Grok LOOKUP of X (grok_search.qualifier_lookup_pulse, owner ask 2026-09-10: chart accounts post a ranking within hours, the web carries it a day or more later; it replaced the Grok claim_verify_pulse corroboration read), a second Perplexity read only when the web alone found a claim, then the claude.qualifier_verify judge. A NONE from both reads is the settled no_qualifier; a NONE beside a read that did not run is error (transient, retries). The judge is biased toward ADDING a real reported rank (one credible source is enough) and drops only a CONTRADICTED or fabricated claim (owner steer 2026-08-25: the second read SEARCHES for a qualifier to add, it is not a two-source drop-gate). Lookup reason: verified (attached at PRODUCTION) | unprovisioned | no_qualifier (the read said NONE) | unverified (judge said no) | error | the cached cached/cached_none. Guard reason: ungrounded_ranking (a caption stated a ranking claim the source did not carry; enforced says whether it was dropped) | off_chart (2026-09-06: a VERIFIED qualifier about ANOTHER chart than the chart-lane milestone’s – “his second top 20 Hot 100 hit” on a Global 200 peak card – dropped before the compose; post_preview the qualifier, detail.scope the chart it had to name). Required-path reason: required_missing (owner ask 2026-09-10, the catalog lane’s ROUND-COUNT path: a lane that composes ONLY with a reported ranking found none – phase the lane, detail.lookup the lookup’s own reason, detail.settled whether the rung was advanced without a card (a settled negative) or left open to retry (a transient miss), detail.rung the count). A required_missing run of weeks with settled=true means no outlet reports the count stats we detect – alive, no story; settled=false for days means the lookup itself is failing (read error). The NEGATIVE case (nothing verified) leaves the compose UNCHANGED — no “make no ranking claim” prompt (it primed the ranking words and suppressed the artist’s OWN grounded standing rank; _drop_for_ungrounded_ranking is the deterministic backstop). The guard also does NOT police the grounded standing rank (“the world’s 71st most-streamed artist”), whose number the number backstop grounds. Gated on the milestone_qualifiers experiment (default PRODUCTION, live): OFF changes nothing, STAGING looks up + emits WITHOUT attaching or enforcing, PRODUCTION attaches + enforces. The lookup is FAIL-OPEN, so an ok=False lookup rate is the ONLY signal the qualifier step went quiet; summarize count() by reason reads whether verification is converging                                    
called_shot_scored cogs/music_desk.py guild_id, phase (always reconciliation), matchup (the release, 80 chars), detail{chart, week, called, printed, verdict} — a first-week chart CALL was settled against the chart that printed (Lane D, docs/BILLBOARD_ALERTS.md). verdict is exact | close (within 2 rungs either way) | miss | uncharted, and printed is 0 when the release did not chart at all. Emitted ONCE per call, at the moment it resolves, so verdict summarized over time IS the lane’s public track recordsummarize count() by verdict answers “how good are our calls” without trusting any individual post. It is also the tripwire for the failure mode this lane cannot self-report: a verdict distribution with no miss or uncharted in it means we have started quietly dropping our losses, which destroys the only thing the lane is for. A call whose chart week could not be read emits NOTHING and stays unresolved — never a fabricated verdict                                    
hits_fetch utils/hits.py source (hits), query (chart slug | doc type | type:<slug> | genres), ok, duration_ms, count, cache_hit; on a miss: reason (unavailable) + error + http_status — one HITS Daily Double read off their public, unauthenticated Sanity GROQ API (no key, no quota; docs/BILLBOARD_SOURCES.md §2.2). This is the FORECAST half of the chart frontier: hits-top-50 (building chart — units + the pure-albums/TEA/SEA split + label marketshare) and midweek-20 (the earliest callable projection) land ~5 days before billboard.com flips, and the Mediabase add_chart/building_chart radio reads refresh multiple times daily. Fail-open (a miss → None/[]), so this ok-rate is the ONLY signal that the projection lane went quiet — nothing raises. Guarded by limiter + breaker (integration=hits) + retry + a durable cache keyed to each source’s REAL cadence (projections ~6h, radio ~90min). /debug/integrations carries a hits probe so a dataset that stops answering anonymously reads red before a lane silently empties                                    
billboard_block_cleared utils/billboard.py integration (billboard), reason (what the streak was), query (the chart that recovered), count (how many consecutive failures THAT CHART ran for) — one chart’s block streak RECOVERED. Emitted because a block that silently heals is as confusing as one that silently starts. Per chart since #2824: the client used to keep ONE streak counter, so a success on any chart reset it and this event reported count=1/2 all through a 40-minute outage of global200 on 2026-09-01 — the streak is a property of the chart, and the event now names it                                    
sports_board_scored cogs/sports_boards.py (#2268) guild_id, phase (the board kind: league_table | win_streaks | loss_streaks | leaders_<category> | title_odds | upcoming | players | game_props), league, score, reason, shipped, post_preview — one sports STATS BOARD composed and put through the 0.6 self-gate. The lane’s funnel: summarize count() by phase, shipped says which boards actually reach a room and which are dying at the gate. Watch for a board kind that is built often and NEVER ships: that is a wrong-RUBRIC failure, not a quality one. Every board left in the lane grades on drop_score’s LIVE rubric, because every one of them reads a state that is still moving. The lane used to pick between two rubrics off SportsBoard.settled, and the results board was the only board that ever set it — both went out together (#3237, the board got no engagement), so phase never reads results again and historical rows keep it. The upset board went the same way (owner, 2026-09-15: “no more upsets of the week”) — it was the BUSIEST kind in the lane when it went (Axiom, 30 days to 2026-09-15: 33 of 161 posts), so phase never reads upset again either and those slots go to the boards that remain. The measurement behind that old split is worth keeping if a settled board ever returns: on real lines 2026-08-10 a weekend-scores take scored 0.20 under the live rubric and 0.92 under the settled one, and a standings take scored the reverse. Since compose self-gates fail CLOSED, the wrong rubric silently kills a good board every time                                    
sports_board_posted cogs/sports_boards.py (#2268) guild_id, channel_id, phase (board kind), league, + folded detail: notability — a sports board card SHIPPED to a room. Paired with sports_board_scored it gives the lane’s full funnel; on its own it answers which leagues and which board kinds a guild actually sees                                    
sports_board_lookup cogs/sports_boards.py (#2268) source (espn_scoreboard | sgo_game_props), league, result_count (how many of the pool have a usable line — for sgo_game_props, the number of paired game+props cards built), count (the pool: tonight’s slate for espn_scoreboard, tonight’s props for sgo_game_props), ok — the odds boards’ per-build coverage read, one row per source. A board under its coverage floor posts NOTHING, raising no exception and leaving no trace in error triage. ok IS that gate, which makes a sustained False rate the only sign that the book stopped attaching odds. source=espn_scoreboard: the UPCOMING board’s read — how many of tonight’s games the book has priced. ESPN attaches odds to a day scoreboard only while a game is upcoming, so this costs no fetch of its own; ok is whether it priced anything at all, and a sustained False means ESPN stopped attaching odds and the board has gone quiet without raising. A third source, espn_pregame, went out with the upset board (2026-09-15); historical rows keep it.                                    
chart_lookup utils/chart_credits.py + utils/chart_ages.py (owner ask 2026-08-10) source (genius | musicbrainz), query (the chart key, e.g. hot100), count (rows RESOLVED for the week, cache hits + live + verified misses), ok (resolution reached the module’s COVERAGE FLOOR — 0.8 for credits, 0.85 for ages; False = the board is still cache-filling, the source degraded, or the token is unset), + folded detail: rows (chart length), fetched (live lookups this build), dated (ages only: rows carrying a real year) — one PER-BUILD coverage read for the lookup boards (producers/writers off Genius credit arrays; oldest-songs off MusicBrainz first-release dates). The per-CALL latency/errors ride reference_fetch (source=genius/musicbrainz); this event is what the boards actually gate on, so a sustained ok=False stream on one source is the ONLY sign that lane went quiet (the boards fail SILENT below the floor by design — a partial-chart producer count would be a whole-chart claim). Live lookups are budgeted (20/build Genius, 15/build MusicBrainz, ~1.1s paced) over durable per-song caches (kv_cache namespaces chart_credits/chart_ages), so steady state is a handful of fetched per week                                    
chart_fetch utils/kworb.py (#daily-releases, order #79) source (kworb), query (global_daily|us_daily|global_weekly|artists|listeners|radio_us|apple_us|itunes_us|apple_albums_us|yt_videos_global|yt_videos_anglo|yt_us_weekly|track_<id>|artist_songs_<id> — one event per page, each revalidated per path), ok, duration_ms, result_count; on a miss: reason + error + http_status — one kworb scrape (a Spotify/Apple/radio/YouTube chart or ranking page, a per-track streams lookup, or the per-artist songs page that backs the album-streams card). yt_us_weekly (owner ask 2026-08-31) is the US WEEKLY YouTube chart behind the us_video_top + us_video_new boards — a DIFFERENT page from the yt_videos_* pair and a different metric: those are worldwide daily VIEWS, this is US weekly STREAMS, and it is the only YouTube page we read that says whether a title is NEW. It backs both boards off one fetch, so a miss darkens the pair together. An INCOMPLETE read reports here, on this event (Codex #2753): both boards need the WHOLE chart — the top 10 draws positions in order and the debuts board counts every NEW row in it — so us_video_chart requires positions exactly 1..N for one of the sizes in kworb._US_VIDEO_CHART_SIZES and emits a SECOND chart_fetch row with ok=false, reason=shape_change and the first missing position when they are not. _fetch has already logged the HTTP read as ok=true by then, so routing the rejection to an unmonitored display-filter event would have left kworb’s integration health green while both boards went dark — the silent-degradation trap. THE SIZE SET IS WHY THAT CHECK HOLDS BOTH 20 AND 100 (2026-09-14): kworb flips this page between the two, and a single-value constant darkened the boards on each flip — 100→20 on 2026-09-06 (#3042), then 20→100, which failed 263 of 364 reads over Sep 7-14 with “expected 1..20, got 100 rows” while these were the only YouTube boards left. Both sizes are measured complete pages; a size we have never seen still darkens the boards, which is the right direction to fail. A yt_us_weekly shape_change rate climbing means kworb moved the page to a THIRD size — add it to the set. A yt_us_weekly ok-rate dipping is the signal that the page’s shape moved for the music new-release lane’s CHART ranks (“#2 US, #9 global, #3 this week”) + the /ask spotify_chart tool. reason (order #79, the same idiom as billboard_fetch): blocked (401/403/451 — kworb refusing us), shape_change (a 200 that parsed BELOW the chart/ranking floor — min_rows=1 on every chart/ranking page, since each carries hundreds of rows when healthy; a per-track lookup keeps min_rows=0, a genuine per-track miss being normal), http_error (any other non-200), breaker_open (the breaker itself short-circuited — no network call), unreachable (a raw connection failure/timeout survived every retry). Before order #79 a shape_change (kworb’s markup moving under the parser, or a soft-block/challenge page) recorded ok=True — silently reading as healthy — and an unreachable case escaped _fetch as a RAISED exception instead of degrading to [], breaking the fail-open contract for any caller with no try/except of its own. not_modified (#3191) is a SUCCESS, not a miss: the page was revalidated with a conditional GET and kworb answered 304, so the cached parse stands. It carries ok=true, http_status=304, cached=true and the cached result_count. Counting it as a failure would trip the breaker on a perfectly healthy page, so read kworb’s ok-rate with not_modified included as ok. superseded=true on a not_modified row means this 304 lost a race: a concurrent caller landed a fresh 200 for the same page while this conditional request was in flight, so the newer rows were kept and this read returned them unchanged (the generation guard, Codex on #3193). It is rare and healthy; a steady stream of it means two surfaces are revalidating the same page in lockstep and one of them could read from the other’s result instead. A HEALTHY tick is mostly not_modified rows with an occasional full 200; all-200 means the validators stopped being sent and we are re-scraping full bodies, while a sudden all-not_modified stretch on an intraday page (Apple, iTunes, the Spotify rankings, YouTube trending) means the page stopped refreshing upstream. Guarded by a gentle rate limiter + breaker (integration=kworb) + retry + a REVALIDATING cache (_REVALIDATE_AFTER_SECONDS, 2 min). Only whole-CHART and RANKING pages revalidate: a page serving no validator, and any PER-ENTITY read (track_*, artist_songs_*, artist_albums_* — 337 of 540 events over 24h, so the bulk of this event by volume), keep the old flat 3h hold. Fail-open (a miss → no chart line, the streams still lead), NEVER raises                                    
album_card cogs/music_alert.py (#daily-releases, #2376) surface (music_alert), ok (the card built), query (the album title, ≤80), result_count (rows on the card, on a build), recovered (tracks the Haiku fallback matched past the deterministic fold, #2582 follow-up) — the fresh-drop ALBUM-STREAMS card outcome: the NEW_RELEASE lane trying to build the album’s tracklist ranked by daily Spotify streams (the “album charts sorted by streams” surface, owner ask 2026-08-16). It rides the NEW_RELEASE post as that post’s CARD (via build_board_card), replacing the streams-hero number card, but ONLY in the release WEEK (_ALBUM_CARD_MAX_AGE_DAYS, default 7): kworb reports a per-track daily while a song is charting, which for an album is its first week — after that the deep cuts drop off the daily column and the card would silently omit half the tracklist. Data is free: the artist’s Spotify id rides the chart standing (ChartRow.artist_id), the tracklist comes from iTunes (apple_music.album_tracklist), and one kworb songs-page fetch (chart_fetch query artist_songs_<id>) gives every track’s daily. Fail-OPEN at every step — a skip is NOT an error, the lane falls back to the number card — so the ok=False RATE is the only signal the card quietly stopped building. reason on a skip: no_kworb (no client) | no_artist_id (the release isn’t charting yet, so no id to reach the songs page) | no_tracklist (iTunes had no album) | no_kworb_songs (the songs page missed) | thin_coverage (fewer than the row floor carry a live daily — the album is past its charting week). Track recovery (#2582 follow-up): before the board is built, the same Haiku closed-set matcher the lifetime board uses (_recover_album_aliaseschart_boards.recovered_aliases) recovers tracks the two sources spell differently (Apple Rusty Intro vs kworb Intro), so a spelling variant no longer drops a row — the recovered count says how many it added, and its latency/outcome ride the claude_api event (purpose=album_track_match). No monitor added: the lane is rare (one Kalshi-tracked debut at a time) and kworb reachability already rolls up under chart_fetch; a sustained no_tracklist/no_kworb_songs rate points at iTunes/kworb, which their own health covers                                    
album_total utils/stream_totals.py (#stream-discovery) surface names the lane — music_desk (the individual-artist board), x_mentions (the per-chart mention board), artist_sweep (the manual sweep) —, ok (a lifetime total resolved), query (the album title, ≤80), source (which of the two sources answered: kworb_albums | tracklist_sum), result_count (matched tracks, on a tracklist_sum hit; 1 on a kworb_albums hit), track_count (tracklist length) + recovered (tracks the Haiku fallback matched past the deterministic fold, #2582) — one album’s LIFETIME Spotify total on the individual-artist (catalog) board. A Billboard 200 row is an ALBUM, and kworb’s weekly page is songs-only, so the board used to show a sales rank alone (blank for a catalog album outside the sales top 50). This resolves each album’s total FREE, from TWO sources in order. First the artist’s kworb ALBUMS page (chart_fetch query artist_albums_<id>, one fetch per act), which publishes Spotify’s OWN cumulative total per release — chart_boards.album_page_total matches the Billboard title to it accent- and case-insensitively (Debi Tirar Mas FotosDeBÍ TiRAR MáS FOToS). Then the tracklist sum, the older path: the tracklist is one iTunes lookup (apple_music.album_tracklist), and one kworb songs-page fetch (chart_fetch query artist_songs_<id>, cached ~3h and shared across the act’s albums) gives every track’s cumulative total; chart_boards.album_total_streams sums the matched tracklist. The page total leads because it is the PUBLISHED figure while the sum only approximates it — a track’s own total carries the plays it took as a single, so summing over-counts a release that has one — and because iTunes does not return every act’s albums at all (measured 2026-08-28: an iTunes album search for Bad Bunny returns his singles and features, none of his albums, so his Billboard 200 rows resolved only once the albums page was added). source on each event says which one answered. It is a LIFETIME total, not a weekly one — the compose framing names it as such so the take never recasts it as “this week”. Fail-OPEN at every step — a skip leaves the album row on its sales rank alone — so the ok=False RATE is the only signal streams quietly stopped resolving for album boards. reason on a skip: no_kworb (no client) | no_artist_id (neither id source knows the act — see below) | no_tracklist (iTunes had no album) | no_kworb_songs (the songs page missed) | thin_coverage (the match covers less than the 0.6 tracklist floor, so the sum would UNDERCOUNT — prefer absent over invented). Track recovery (#2582): before the floor is read, a Haiku closed-set matcher (claude.match_album_tracks) recovers tracks the two sources spell differently (Apple Rusty Intro vs kworb Intro), under a plausibility bound (a recovered row cannot out-stream the album’s biggest matched track). The recovered count says how many it added; a HABIBTI-shape album that read 8/11 = 267M now reads 10/11 = ~328M. The matcher fails CLOSED (a Haiku outage recovers nothing, leaving the deterministic sum), and its own latency/outcome ride the claude_api event (purpose=album_track_match). The artist id has two sources (2026-09-02): the FREE kworb weekly rows (an act with a song on the Spotify Global Weekly top 200), then the all-artists leaderboard by exact name (StreamTotals.resolve_artist_id, the route the mentions lane always used). The weekly page alone left every CATALOG act blank — Future held three Billboard 200 albums on the 2026-09-05 chart with no global top-200 song, so the desk board shipped with an empty streams column while his kworb albums page carried all three totals — and that branch emitted NOTHING, so the 14-day ledger read 100% ok. Now an act neither source knows emits ok=False, reason=no_artist_id per album, from the desk lane (MusicDesk._catalog_artist_id) and the mentions lane (board_stream_totals) alike. No monitor added, re-judged when the X-mentions lane joined (2026-08-28): the volume is higher now, but a miss still degrades ONE COLUMN and never the post – a board with no totals falls back to its move markers – while iTunes + kworb reachability already roll up under their own health (chart_fetch, catalog_lookup) and a sustained thin_coverage rate points at kworb catalog coverage, not our code. The panel is the read                                    
song_total utils/stream_totals.py (#2430) surface names the lane (music_desk | x_mentions | artist_sweep), ok (a lifetime total resolved), query (the song title, ≤80), result_count (1 on a hit) — one SONG’s LIFETIME Spotify total on a per-artist board, the sibling of album_total. Three music-desk boards draw the column now: the individual-artist board, the debut class (2026-09-09) and the GENRE board (2026-09-12, owner report on the Karol G Hot Latin Songs card: “where did our streams go”). The genre board costs one songs-page read per board, because every row belongs to one act. It REPLACED the old song streams source (the kworb Spotify Global Weekly top 200), which left every US Hot 100 song ranking lower globally blank — the Karol G card showed a number on only 1 of 4 songs. Now each song matches its title against the artist’s kworb songs page (chart_boards.song_total_streams, the same free artist_songs fetch the album lane shares) and shows its cumulative total, so a currently-charting act fills every row. Fail-OPEN — a skip leaves the song row on its sales rank alone — so the ok=False RATE is the signal streams quietly stopped resolving. reason on a skip: no_kworb_songs (the songs page missed) | no_match (the song is not on the artist’s songs page) | no_artist_id (neither the weekly rows nor the leaderboard knows the act; emitted per song since 2026-09-02, the same two-source id lookup + miss album_total describes). No monitor added: the board fires only on the week an act moves, and kworb reachability rolls up under chart_fetch.                                    
plaque_facts cogs/music_desk.py (_plaque_facts, owner ask 2026-09-04) surface (music_desk), query (the act, ≤80), count (rows the board draws, at most 10), result_count (rows that resolved a RELEASE DATE), hit (kworb knew the act, so a daily figure was read off its songs/albums page), ok (at least one row dated), + folded detail: daily_rows (rows that carry a daily streams figure) — one event per NEW PLAQUES board built (riaa_boards.plaques_board, one board per act AND format since 2026-09-04, so an act with new singles and new albums emits two). The board’s row sub used to name the format (“single” / “album”), which the owner read as noise on the Disney card of 2026-09-04 and then ruled out outright (“no plain ‘single’ string”); it now carries the title’s release date and, where Spotify tracks the title day to day, its daily streams (“Jun 14, 2015 · 1.2M/day”), and is EMPTY when neither resolved – the format lives in the chart key and caption. The date is MusicBrainz’s first-release-date (chart_ages.first_release_date, one paced read per title behind the desk’s per-process memo, the reads on reference_fetch source=musicbrainz), looked up under the row’s own credit first and the act second, so a soundtrack cut RIAA files under a cast cell still gets a try under the act; it is NOT the Deezer/iTunes release_age resolver, which dated catalog titles to reissues on the live feed (see docs/ARCHITECTURE.md, riaa_boards.py). The daily comes off the act’s kworb page through StreamTotals.song_daily / album_daily (one fetch per act, the same cached page the lifetime totals read); an act the kworb leaderboard does not know (hit=false, a catalog act like Disney) draws dates alone. Fail-OPEN: no exception anywhere, so the ok=false RATE is the only sign the MusicBrainz path died — on the ops monitor as riaa_plaque_dates in the misc integration health (_MISC_INTEGRATIONS). A single ok=false on a soundtrack or kids’-channel batch MusicBrainz does not hold is normal (measured 2026-09-04: it dated 12 of 26 rows across seven batches, and none of the Disney rows); a sustained one is not. The board’s fire-on-change signature reads label + value only, so a daily figure moving never re-posts a board. No monitor beyond the health line: the board fires only on a batch, a few times a week.                                    
cert_batch cogs/music_news.py (_cert_batch_card, owner report 2026-09-12, #3211) surface (music_news), matchup (the act, ≤80), count (the awards RIAA’s own sweep holds), ok, guild_id; on a DRAW: kind (single|album, the format the board drew) + awards (rows on the card); on a SKIP: ok=false + reason (cold_cache|no_sweep|count_mismatch|other_authority|under_floor), + folded detail: batch (the count the WIRE claimed). One event per certification-BATCH story the newsroom cards. Why it exists: RIAA certifies an act’s whole catalogue on one day and the wires report the sweep as a TOTAL, so the classifier heroes a count and the room got “53 certifications / Future” with no titles under it. The desk’s own plaques lane could not supply the board — it reads awards certified in the last THREE days, and RIAA had dated that sweep 2026-07-16, 58 days before the wire noticed it (measured live 2026-09-12: 50 awards on 07-16, 4 on 07-15, 0 Future awards anywhere in the recent feed). So the card is drawn off the act’s OWN award search instead — the read the record boards already hold a week (riaa.awards_for_act), never a fresh fetch: one search is up to 16 paced pages (~100s measured) and a post cannot wait. Fail-OPEN: every skip ships the story’s number card, so no card is lost and no exception escapes – and for a LONG batch figure the cog restores the count in its short form first (53 new RIAA certifications is 26 characters and the card’s phrase guard clears anything over 24, which would hero nothing and, with no art, ship bare text; Codex review). The cold_cache RATE is therefore the silent-degradation signal — it means the roster walk (music_desk._riaa_candidates, RIAA’s own artist ranking at 3 acts a slot, cached 7 days) has stopped keeping the big acts warm, and a climb there turns every sweep back into a bare number with nobody seeing an error. count_mismatch is the prefer-absent guard, and it runs BOTH ways (Codex review): a take saying “53 certifications” over a three-row board tells a bigger story than the card shows, and a story about 3 new certifications over a 50-award sweep from two months back draws the wrong event entirely – the busiest date in the window is still the old one. The counts must agree within a factor of two in each direction. other_authority is the second fence: the board reads RIAA and wears its pill, so a BPI story is refused here and cert_batch_count matches no BPI figure either. Sweeps are MAIN-program only – the Latin ladder is different and award_name reads every row on the main one, so a Latin sweep would print “6x Platinum” over 360,000 units. No monitor: a sweep is a few cards a month and every failure degrades to a card that is merely less good, so the panel is the right weight.                                    
count_board cogs/music_news.py (_chart_count_card, owner report 2026-09-13, #3224) surface (music_news), matchup (the act, ≤80), count (the settled count), ok, guild_id, + folded detail: chart (the chart key the count settled on), top_n (the window the claim named); on a DRAW: detail.rows (rows drawn on the card); on a SKIP: ok=false + reason (other_figure|under_floor). One event per artist-level COUNT story the newsroom cards. Why it exists: a wire states a count about an ACT (“Lil Durk has 6 titles inside the top 100 on the US Apple Music albums chart”), the newsroom settles it against the live chart, and the card heroed the settled count – so the room and the timeline got a “6” over an artist photo with no titles under it (owner report 2026-09-13, the same shape as the certification sweep one day earlier). The board lists the titles instead. It costs no read: resolve_artist_count already keeps the (position, title) pairs it counted, _sched_compose stamps that reading on the unit, and the board draws those rows – so the board and the take come from ONE count and cannot disagree. That is why there is no counts-agree gate here, unlike cert_batch. Fail-OPEN: both skips ship the story’s number card, and no exception escapes. other_figure means the card’s settled hero is no longer that count (a market story heroes its own leg), so a chart list would sit under a take about something else; under_floor is the board’s MIN_COUNT_ROWS cut – one title is a position, and the number card states it better. Because nothing is fetched, a SUSTAINED ok=false rate means the count stopped reaching the card (a compose-side change), never an upstream fault. No monitor: every failure degrades to a card that is merely less good, so the panel is the right weight.                                    
catalog_board cogs/music_news.py (_catalog_board_png, owner rule 2026-09-13, #3225) surface (music_news), matchup (the act, ≤80), count (the titles at or above the rung), ok, guild_id, + folded detail: rung (the rung in streams), rows (rows drawn, on a draw); on a SKIP: ok=false + reason (under_floor). One event per catalog-milestone story the newsroom cards. Why it exists: “Drake has 30 songs past 1 billion Spotify streams” heroed a bare “30”. #3131 made that card say WHAT it counts (the block names the rung, the Spotify rank strip rides along) but it still never said WHICH thirty songs. The board is ADDITIONAL, not a replacement (owner steer: “can we make the board additional”) — the tuned card keeps its hero and the board is a second image in the SAME post: one MediaGallery with two items on Discord, two media in one tweet on X. That is the difference from count_board, which replaces its card: there the hero stood in for the list, here the hero is the crossing and the list is the evidence under it. It costs no readresolve_stream_claim already selects the titles and now keeps them on the reading, so the count in the take and the rows on the card come from one selection. Fail-OPEN twice over: a skip ships the number card unchanged, and the whole build is wrapped so an exception can never cost the post its card. A sustained ok=false is not an outage — under_floor means the act has one title past the rung, which is a crossing rather than a list. On X each image is gated SEPARATELY by the media coherence gate and a failing one is dropped without sinking the other, so a wrong-person card degrades to the board alone rather than to bare text. No monitor: every failure degrades to the card that shipped before this existed.                                    
genre_debut_tag cogs/music_desk.py (_genre_debut_groups, owner question 2026-09-05) source (itunes), query (<artist> - <album>), ok, duration_ms, count (1 = the Apple result matched the row on the album\|artist key), reason (tagged | other_genre | genre_disagree | genre_unconfirmed | no_match | breaker_open), genre (Apple’s primaryGenreName) and genre_tags (Deezer’s album genre names), both folded into detail — ONE iTunes album search that tags a TRUE DEBUT on the HITS Top 50 with the genre Apple publishes on the album, so a first-week album reaches the hip-hop / R&B genre board. The genre board’s membership rule is the PRINTED Billboard genre chart, and the HITS document projects the week AFTER it, so a debut is on no printed chart at all and missed both boards on its biggest week (measured live: Rod Wave’s “Don’t Look Down” projected #1 on ~100K units and sat on neither card). Only a row on NO row of the printed Billboard 200 is a candidate (chart_boards.genre_debut_candidates), capped at MUSIC_DESK_GENRE_DEBUT_TAG_CAP (8) per read. Apple’s tag alone is not enough (owner steer 2026-09-11, “the release isn’t the problem, it’s the genre”): Apple reads Hip-Hop/Rap on Beyonce’s 2006 “B’Day” record while reading Pop on that same album’s other editions, so Beyonce led the hip-hop sales board for the week of 2026-09-19 off its 20th-anniversary reissue. DEEZER’s album genre (deezer.album_genres) is the second, independent source, and the album joins a board only when both sources name the same group (chart_boards.confirmed_genre_group). Deezer is read only when Apple already names a kept group, so a pop or country debut costs no call. genre_disagree (Deezer names another group, or none — the B’Day case) and genre_unconfirmed (Deezer holds no genre for the album yet) both leave the album absent. A settled answer (tagged, other_genre, genre_disagree) is cached per album for the process, while no_match (a release-day album iTunes has not indexed yet, the common case), genre_unconfirmed and breaker_open are not cached, so the next read retries. ok=False ONLY for breaker_open (the iTunes breaker is open, the read never happened, and the cache is left alone so the next read retries); no_match, other_genre, genre_disagree and genre_unconfirmed are healthy outcomes. Fail-open: a miss drops one row, never the board. Ops monitor: misc health itunes_genre_tag — a sustained fail rate is the only sign the week’s debuts are silently missing. Dashboard: the data_sources table                                    
entries_board cogs/music_news.py (_entries_board_png, owner report 2026-09-15, #3331) surface (music_news), matchup (the act, ≤80), count (the act’s entries on the chart), ok, guild_id, + folded detail: chart (the chart key); on a DRAW: detail.rows (rows drawn); on a SKIP: ok=false + reason (under_floor). One event per career-entry milestone the newsroom cards. Why it exists: “LISA ties JENNIE with 7 Billboard Hot 100 entries” heroed a bare “7” over her photo with none of the seven titles on it — the third appearance of one bug, after count_board (#3224) and catalog_board (#3225). The board is ADDITIONAL, the catalog_board shape rather than the count_board one: the hero is the MILESTONE the story is about, not a stand-in for the list, so the number card keeps it and the board is a second image in the same post. It costs no read_live_entry_history settles the claim against Billboard’s own chart-history page and _sched_compose stamps the reading on the unit, so the count in the take and the rows on the card come from ONE reading. Fail-OPEN twice over: a skip ships the number card unchanged, and the build is wrapped so a bonus image can never cost the post its card. The wrong-artist guard lives UPSTREAM and is silent here: a career count names no title, so the check is the count itself (the page must hold exactly as many entries as the claim states) and a refusal lands in music_news_chart_lookup with phase=milestone + detail.reason (count_mismatch | unreadable_row), not in this event. So read the two together: this event going quiet for a whole act, with count_mismatch rows beside it, means the read is landing on the wrong artist page — check billboard_artist_history detail.slug_source. No monitor: every failure degrades to the card that shipped before this existed.                                    
artist_filter utils/kworb.py (is_functional_audio) source (kworb), kind (streams|listeners), count (rows dropped), reason (functional_audio), + folded detail: dropped (the dropped names, ≤10) — functional-audio accounts (white noise / sleep sounds / ASMR / lofi study loops) removed from the Spotify all-artists ranking (kind streams, name + stream-shape signals) AND the monthly-listeners ranking (kind listeners, NAME signal only — that page carries no stream-shape numbers, and a functional account self-labels by name) before any consumer sees it. Rides _fetch’s one-shot transform, so it fires on a FRESH page fetch only — a 3h cache hit returns the already-cleaned rows and never re-emits. The board artist_streams_board re-ranks the whole page by the DAILY column, and a white-noise account collects huge daily streams from overnight sleep-playlist autoplay, so one floated to #1 over Drake and shipped to X: “White Noise Radiance sits atop Spotify’s most-streamed artists … 211.3M clear of Drake” (the account is real on kworb — #658 by total, 7.1B total, 269.5M/day — it is just not a music artist). Two signals catch it, either one enough: a NAME marker phrase, or a STREAM-SHAPE anomaly (daily ≥ 1% of a total over 5B — no real catalog artist turns over that share of their whole history in a day; the live top sits near 0.04–0.10%). Emitted only when a row is dropped, so a non-zero count IS the signal. The watch is OVER-filtering: a genuinely huge, tiny-catalog real act wrongly held off a board is the silent degradation (the numeric floor is set 10x over the real max + a 5B total floor to make that near-impossible, but the event is where it would show). No monitor added — this is a display filter, not an integration; kworb reachability is already covered by chart_fetch, and an over-filter would surface as a named artist missing from the ranking rather than a rate crater                                    
rt_fetch utils/rotten_tomatoes.py (#2266 follow-up) op (scores_for), ok (a Tomatometer was resolved for the right film), duration_ms, count (search candidates returned), + folded detail: checked (film pages actually opened), matched_year (whether any candidate’s own release year matched the chart’s) — one lookup for the critics scoreboard’s ROTTEN TOMATOES column. Rotten Tomatoes publishes no API, so this reads their public pages (owner decision 2026-08-11): two GETs per film, a /search for candidate /m/ slugs then the film page for media-scorecard-json + the JSON-LD Movie block. It exists because OMDb carries an RT score for roughly a quarter of current releases (2 of 8 on a real weekend top 8, against 8 of 8 direct), so the board’s dash claimed “unreviewed” and meant “our source lacks it”. This is the fail-open case error triage cannot see: when RT changes a page nothing raises, the client simply stops matching and the column silently reverts to dashes — so the ok RATE is the only signal, rolled into the ops monitor’s integration health as rotten_tomatoes and split by failure step on the rt_scores dashboard panel (count=0 means the SEARCH page moved or RT blocked us; count>0 with matched_year false means the FILM page moved). Never ranks, only fills — the board still orders on Metacritic, so a bad day at RT changes which numbers appear, never which films. Gated per guild on the rt_scores experiment (default STAGING); rate limiter + breaker (integration=rotten_tomatoes) + retry + a 12h TTL cache keyed on title AND year                                    
market_filtered cogs/market_alert.py (alert pool) + utils/markets.py:get_events_for_series (Kalshi discovery, reason=non_leader_chart|netflix_views) guild_id, reason (the filter that cut it: horizon|unwatched_sport|trailing_leg|same_answer|forecast_cooldown|forecast_series|forecast_decided|non_leader_chart|netflix_views|soft_price_tier|young_ladder_cap – the young-board lane’s per-room cap (#3113): a scalar board with NO previous-day price (so no price_change, so never a mover) that the _YOUNG_LADDER_MAX_EVENTS volume cut dropped before its ladder was resolved + seeded; ticker is the EVENT, kind=forecast_move; rendered, never flagged, a steady count says the cap is too tight for that room – |pure_over_units – the music sweep’s sibling sanity check, #3069: a release’s pure-sales forecast read above its own album-equivalent-units forecast, impossible since pure sales are a component of units, so fetch_desk_readings drops the pure reading for every consumer; guild-less, ticker + topic in detail), kind (the shape it WOULD have fired as: swing|near_lock_crack|favorite_collapse|forecast_move), + the folded detail map: ticker, topic (≤80 chars), move (|price_change| in points), resolves_in_days (days to resolution per the best-available date) — a market that WOULD have alerted (a qualifying swing or conviction break, _would_alert) was cut by a market_alert pool filter before its move was ever examined — the observability for a silent candidate-exclusion filter (#world-cup-spike: the Men’s World Cup winner Spain leg spiked 20→55% but was dropped by the “within a week” horizon because Kalshi stamps a placeholder ~2yr close_time on open tournament legs, so no alert ever fired). The generalizable lesson: a filter that removes candidates from a pool is as load-bearing as the surface it feeds, so it needs the same instrumentation — verify the property that matters (does this date reflect reality?), not the convenient proxy (does the field exist?). Emitted per dropped would-alert market (bounded to the top few by move size so a wide pool can’t flood; a persistent market re-fires ~every 20min tick and the ops-monitor dedups by ticker). The ops-monitor renders a “Filtered markets” line every run + flags market_movers_dropped when ≥ MARKET_FILTERED_MIN (2) distinct hot markets are cut (horizon medium, unwatched_sport low); a dropped market always resolves >a week out (that’s why it was cut), so the sample’s resolves Nd is the tell — a human confirms whether that far date is REAL (a legit future) or another placeholder-date bug hiding a near-term market. The market_drop’s horizon exclusion is already observable via market_drop_skipped(reason=all_far_off) (#1092), so only the alert’s silent pool filters needed this. reason=trailing_leg (#netflix-race, made BLANKET + fail-closed in #1878) is a DELIBERATE routing choice, not a silent bug: a NON-leader leg of a multi-outcome Kalshi race (each movie in “Top US Netflix Movie” is its own yes/no leg) is dropped from the swing/conviction pool so a collapsing LOSER is never headlined on its own (“72 Hours sits at 18%, closes in 2 days” — owner: “why do we care who’s not leading… no narrative just a dry report”). Instead the moved leg’s whole EVENT is resolved into a leaderboard race (_resolve_racesget_event_markets_race_snapshot: the full legs, since a stable longshot is filtered out of the movers pool, folded into an {outcome: prob} map + per-leg chart_specs so the card draws the real crossover) and routed through the existing lead-change path (an OVERTAKE narrated as “A Toxic Love Story overtook 72 Hours for the top spot, now most likely at 74%”); only the leader’s own moves or a real switch make the feed. So the ops-monitor RENDERS trailing_leg in the “Filtered markets” line but NEVER flags it as market_movers_dropped (it’s intentional, not the #world-cup-spike class). reason=ladder_rung (#ladder-forecast) is the same shape of deliberate routing: a Kalshi SCALAR market is a LADDER of “Above N” rungs over ONE underlying number, and an at-the-money rung is a knife-edge that amplifies a tiny move in the underlying into a huge move in its own digital odds — live, the Spider-Man KXRT-SPI-90 rung swung 66%→48% while the figure Kalshi publishes for that market held flat at 90, and this surface posted “a favorite falling to a coin flip” about a market that hadn’t moved in two weeks, three times in three days. So rungs are dropped from the swing/conviction pool entirely (utils.kalshi_ladder.is_ladder_rung, the same test market_drop has used since #1011), leaving a scalar market a shape this surface simply doesn’t narrate. A RISING ladder_rung rate is HEALTHY — it counts the false alerts no longer posted — so the ops-monitor renders it and never flags it. What a ladder DOES forecast (its E[X], the figure Kalshi prints) alerts via the forecast_move kind (a material move in the forecast vs a durable per-event baseline, debounced) — and a ladder CLOSING soon posts through the ending-soon lane reframed onto its forecast (#forecast-card-copy, owner steer “the forecast is the story, the odds is a subtitle qualifier”): the take reads “X is forecasted to hit ~17M” (the close time gates the alert but stays OUT of the take — owner steer 2026-08-26, the settling clock is plumbing, not the story), the card heroes the forecast VALUE, and the rung’s odds ride only as the card’s subtitle qualifier (“75% to clear 16.5M”, _rung_qualifier) — the Bieber views board had shipped the inversion (75% heroed, the ~17M figure unmentioned) because the ending-soon lane never saw the swing pool’s rung filter; a closing rung of an UNREADABLE ladder (bucket/thin, no forecast) is dropped fail-closed with this same ladder_rung reason. (The same change fixed the forecast trigger having shipped DORMANT: ladder_forecast reads the raw book fields (yes_bid_dollars/yes_ask_dollars/last_price_dollars), which _kalshi_market_to_snapshot never threaded onto snapshot meta — so _resolve_ladders always saw None-book rungs and no forecast ever resolved; the mapper now carries them.) reason=unconfirmed_leg (#1878) is the ONLY leader-rule drop that is neither editorial nor a correctness guard, and the fail-CLOSED half of the leader rule: a named-outcome leg may be headlined only when its event’s own leg set CONFIRMS it either leads its race or isn’t in one (_event_leader over any event with ≥2 named priced legs — a lower bar than the ≥3-leg leaderboard fold, since a two-horse race has a loser too), and anything unconfirmed is SUPPRESSED rather than posted blind. The rule existed before but fail-OPEN, so every gap in resolution came out as a trailing leg headlined with the leader unmentioned: a Charli xcx album sank to 15% on the 26-album Billboard 200 board and got its own alert while Olivia Rodrigo led it at 71% — the room told who was LOSING a race it was never told anyone was winning. What left it unresolved was the per-tick resolve cap (3) being spent on scalar LADDERS, whose rungs carry named-looking labels and so passed the race candidate test (that tick, three Rotten Tomatoes / YouTube-views rungs moved 47-52 points and took every slot). So rungs are excluded from the race candidates (they have their own lane, making the two candidate lists disjoint and each event fetched once), candidates are ordered would-alert-FIRST (a conviction break fires BELOW the swing bar, so move size alone buries it), and the budget is split: every event that can FIRE (_RACE_MAX_EVENTS 50, sized to a live sweep — Sports 39, Entertainment 13, ≤20 elsewhere) plus a handful resolved only for lead-change reach (_RACE_CONTEXT_EVENTS 5), so a quiet day’s long tail of small movers costs nothing. Live-swept: 107 trailing legs suppressed vs 29 leaders still alertable, 5 lone binaries untouched. reason=same_answer (#1983) is the ANSWER gate, and it REPLACED the 10-point same-ticker move threshold that carried reason=repeat. That threshold asked how far the PRICE had travelled as a proxy for whether the next post would say anything new, and the proxy broke on the case the owner reported: Ransom Canyon Season 2 held the #2 Netflix spot from 31 July to 3 August while its price swung 44 -> 92 -> 90, so every repeat cleared 10 points honestly and the room read the same sentence four times. The price is volatile; the answer is not. So the gate now compares the CLAIM: for a race leg WHO leads (the leader rule already drops every non-leader, so the leader’s name IS the claim), for a lone binary the subject plus its side of even money. Stored per EVENT in market_alert_answers; a candidate asserting what we already published does not post, however far the price moved. Strictly better in BOTH directions – a real change ships at once with no cooldown to wait out, and a non-change never ships. Measured over 30 days of real shipped alerts it removes 47 of 240 posts, and removes ZERO from the per-game sports families (MLB game/spread/total, tennis): a new fixture each night is a different event with a different answer, so per-game markets are protected by construction rather than by a taxonomy. The answer field (folded into detail) carries the claim that repeated. A RISING rate is HEALTHY – it counts posts no longer made – so the ops-monitor renders it and never flags it. NOT applied to the scalar-ladder forecast_move lane: there the claim is a number that drifts, so “the answer changed” is true nearly every time and only the per-event time limit from #1977 bites. Because nearly EVERY Kalshi market is a named leg (299/299 would-alert markets on a live sweep), the event fetch is now load-bearing for the whole surface — so a sustained unconfirmed_leg rate means it’s going DARK, not quiet, and this is the one filter reason the ops-monitor FLAGS (medium, pointing at get_event_markets) rather than only rendering. That fetch must therefore be COMPLETE, and #1879-follow-up is why (_legs_complete): get_event_markets read ONE /markets page (100) and dropped the cursor, so any bigger field came back silently truncated — and a partial leg set doesn’t weaken this rule, it INVERTS it, crowning the best leg on page 1 while suppressing the real favourite as trailing. Live: the 146-runner Rocket Classic came back cut at 100 with Cameron Young (the true 26% favourite) missing entirely, so page 1 read Patrick Cantlay at 14% as the leader; 1 of 279 live events exceeded a page, but large fields (golf, a 32-team futures, a big candidate slate) recur weekly. So the fetch now follows the cursor (page size raised to Kalshi’s 1000 cap, so the extra round trip is only ever paid past 1000 markets — the golf field drains in one) and STAMPS event_legs_complete on every leg; an undrained walk (page budget spent, or a later page failing) marks the set partial and _resolve_races treats it exactly like a failed fetch — left out of the map, every leg unconfirmed_leg. Suppressing the true leader alongside the pretender is the accepted cost of not knowing, and is the same direction every other gap in this rule takes. reason=forecast_cooldown / reason=forecast_series (#1977) are the scalar-ladder forecast lane finally being PACED like every other trigger, and they carry kind=forecast_move. That lane is keyed on an EVENT ticker rather than a snapshot, so it never went through _blocked and asked neither pacing question — its whole bar was “did the forecast move ≥15% and hold two ticks”, which on a CONVERGING board is not the same question as “should we say it again”: the per-event baseline RESETS to each post’s value, so a ladder drifting one direction re-clears the bar indefinitely. Live, KXRT-ICE posted three times in fifteen hours (45.4 → 26.5 → 16.5, each hop an honest ~40% move), against the same KXRT family sitting at exactly one post a day for the ten days before the lane went live — the series gate was working, a new trigger had simply been routed around it. forecast_cooldown is the EVENT’s own 24h clock (market_forecast_alerts.last_alert_at, stored since the table shipped and read by nothing until now); forecast_series is the shared #1007 window, so a forecast can’t ride in behind a swing on a sibling of its family. Both are DELIBERATE pacing like repeat/ladder_rung — a RISING rate counts posts no longer made — so the ops-monitor renders them and never flags them, and both fail OPEN on a DB error (a pacing lookup must never silence a surface). The pair is also the tell for the residual asymmetry: the lane reads market_alert_state but does not WRITE to it, so forecast_series can fire while the reverse (a swing blocked by a forecast post) cannot — a sustained forecast_series rate alongside same-family swings is what that gap would look like in the data. reason=forecast_decided kind=forecast_move (#outcome-timing, 2026-08) is the STALE-forecast guard: a scalar ladder is a forecast only while its underlying number is UNKNOWN, and once the event happens (a video airs, a game ends) the survival curve collapses onto the now-known value — a near-PERFECT step, the whole drop from ~1 to ~0 packed into the single strike gap bracketing the value (utils.kalshi_ladder.ladder_is_decided, a shape test because there is no field for the real event date: Kalshi’s expected_expiration_time is a padded settle-by ceiling, so ends_within still reads a decided-but-open market as near-term). The move that fired the alert is then the reveal catching up, not news: the GTA VI “An Extended Look” runtime card shipped “the runtime forecast pulled back to ~28.3 minutes” after the video had already aired. The move lane emits this reason and suppresses (the ending-soon lane drops the same ladder’s rung fail-closed under ladder_rung). The band is tight (survival ≥0.92 → ≤0.05) to separate a decided board from a merely TIGHT live one — the Tyla album board (Above 7K ~0.11) stays a forecast because its “no” rung sits above the LO; validated against 634 live open exchange ladders, zero false positives. A RISING rate is HEALTHY (stale posts no longer made), so the ops-monitor renders it and never flags it. Posting the OUTCOME instead of suppressing is the reconcile lane’s job (today KXRT-only) — the #outcome-timing follow-up. reason=same_answer kind=bet_value (#2162) is the betting VALUE alert’s edge baseline, emitted from cogs/betting_value.py: the per-game cooldown is pacing, not a content gate, so during a long live game it lapsed while the edge stood still and the surface re-stated the SAME book-vs-market numbers ~50-115 min apart (4 times on one measured day). The edge signature (team book% market%) is stored on each fire (bet_value_alerts.last_edge) and an unchanged signature never re-fires; a moved number is a new signature and still posts under the cooldown. A rising rate is HEALTHY (posts no longer made) reason=non_leader_chart (2026-08) is a SEPARATE emit site — the Kalshi DISCOVERY boundary markets.get_events_for_series, not the alert pool — for the owner’s leader-only cut: a non-leader chart board that is Entertainment, names a #N/runner-up in its EVENT title, and is NOT music (_is_cut_runner_up_event) is dropped from every pool the discovery feeds (trending / movers / ask). Sports is a separate Kalshi category (kept), Elections/ballot measures are a separate category (kept), and a per-leg identifier #N (a video-game “Remake #3”) is kept because the event title carries no rank. Carries source=kalshi + category (the event category) + ticker/topic in detail. This is the WATCH for a BAD cut — the failure is silent (a dropped board raises nothing), so the cut is instrumented instead: the runner_up_cut dashboard panel lists the cut TITLES by name (steady state is the known handful — Netflix Movie/Show #2, Google #2 — so a new/unexpected title is the eyeball alarm), and the “runner-up cut over-cutting” monitor fires when the distinct-ticker count over 12h jumps past 20, i.e. a catalog/title change or a bug is over-cutting music or other boards that should stay. Verified against the live Kalshi catalog (6,000 open events): cuts exactly 5 boards, keeps every music runner-up + sports rank board + ballot measure. reason=netflix_views (2026-08-22) shares that same discovery emit site for a SURGICAL owner cut: only the Netflix VIEW-COUNT boards are dropped (_is_cut_netflix_views_event) — the “how many views will the #1 show/movie have this week” boards (KXNETFLIXTOPVIEWSTV, KXNETFLIXTOPVIEWSMOVIE), a dry number with no narrative. The Netflix RANK/leader boards (KXNETFLIXRANKSHOW “Top US Netflix Show this week”, which name the #1 title — e.g. Outer Banks leading at 98%) are KEPT: a real story and a rich card. Keyed on the NETFLIXTOPVIEWS ticker cue, with a narrow Entertainment-scoped title fallback (“how many views” + “netflix”), so a rank board is never caught and a Netflix STOCK market (tickered NFLX) is never swept in. The runner-up cut above only catches a #N board, and a view-count board names no rank, so it slipped past both; this cut removes it. Gated on the CUT_NETFLIX_VIEW_MARKETS env flag (default on) — set it falsey to let the view-count boards post again. Over-cut risk is near zero (the ticker cue is exact), so no dedicated over-cut monitor is earned — the generic market_filtered panel (grouped by reason) surfaces the netflix_views count. Carries the same source=kalshi + category + ticker/topic fields. reason=blocked_series (2026-08-26) shares the same discovery emit site for a whole-show BLACKLIST by ticker PREFIX (_BLOCKED_TICKER_PREFIXES + _is_blocked_ticker) — the Netflix cut generalized from one board type to whole series. A subcategory tag cut cannot do this job: Kalshi tags a show’s series INCONSISTENTLY, so a room whitelisting the broad Television tag re-admitted Big Brother no matter which tags it picked, and the S27 winner series (KXBIGBROTHER27) carries ONLY Television — no tag catches it. The STABLE key is the series ticker: every Big Brother series starts with KXBIGBROTHER, so one prefix drops eviction / winner / rank / S27 / Brazil alike. Big Brother has since MOVED into the per-guild menu (owner steer 2026-08-26), so _BLOCKED_TICKER_PREFIXES now ships EMPTY and blocked_series fires for NO show by default — Big Brother’s cut now emits muted_series (below) instead. The mechanism stays for a future all-surface block. When a global prefix IS set it cuts at FOUR sites so no discovery path bypasses it, each emitting this event: list_series_by_category (the PRIMARY drops/alerts/trending category pull — cut here so a blocked series is never ranked, which matters for the board path; the series never reaches get_events_for_series, so this is the emit site for that path), get_events_for_series (the series-arm fetch, before the /events call), and the EVENT-first arm the /ask + compare/combined tools use — kalshi_search (blocked events dropped from the FTS candidates before the last-mile picker) + get_event_markets (the direct event fetch, before the /markets call). All the skips return [] (clean skip, not a failure). Carries source=kalshi + ticker. reason=muted_series (2026-08-26) is the PER-GUILD sibling of blocked_series: a show a guild opted to mute on the /menu “mute shows” picker (markets.SHOW_CATALOG -> ticker prefixes, stored in muted_series). Unlike the global blacklist it is applied at the GUILD-AWARE layer – market_drop._drop_muted_shows filters the pick pool, and market_alert filters its candidate pool via _emit_filtered – so it carries guild_id too, and (in the alert path) only a WOULD-alert muted market is logged, like the other alert pool filters. Applies ONLY to the automated surfaces (drops + alerts), never to an explicit /ask. A RISING rate is HEALTHY (posts a guild chose not to see); the generic market_filtered panel grouped by reason surfaces it. Over-cut risk is near zero (an exact prefix), so no dedicated monitor is earned — the generic market_filtered panel grouped by reason surfaces the blocked_series count. reason=soft_price_tier (#ella-dandelion, 2026-08-29) is the market_DROP’s twin of the alert’s soft_price: a board (or a lone binary) STEPPED ASIDE because its top-priced leg carries no price the book’s own best bid stands behind (kalshi_price.bid_supported). The picker then takes the next candidate, so the slot still posts. Emitted once per skipped board/binary with the event ticker; without it the skip is invisible and reads exactly like a board that was simply never hot enough to pick. It is a SKIP and not a tier downgrade for a measured reason: gating only the drop’s conviction tiers left market_cards.leader_figure re-heroing the identical leg one layer down, which is the bad card minus two words. The shipped case it exists for: DANDELION’s 40% was an 18-hour-old print over a 6c bid, so “DANDELION at 40% is the market’s top pick for #2 on the Billboard 200” went out two minutes before the music desk posted the HITS projection with Dandelion at #3. Like trailing_leg and ladder_rung, the ops-monitor RENDERS it in the “Filtered markets” line and NEVER flags it – a rising rate counts claims we no longer make. No monitor is earned yet: the live rate is 7% of frontrunner boards and 15% of locked ones, so the shape to watch is the OPPOSITE direction (a rate falling to zero would mean the stamp stopped reaching the tiers), which the generic market_filtered panel grouped by reason already shows. decided_entity (2026-09-06) is the entity outcome lane CLAIMING a named field that has collapsed onto one outcome: the lock’s legs leave the swing pool so the result is confirmed and posted as a fact, never narrated as “tracking toward” (the Emmy incident); rendered, never flagged, and the count is the lane’s liveness tell – it fires whether or not the hunt confirmed. reason=soft_baseline (the Emmy “Tie” incident, 2026-09-07) is the lead-change lane RESETTING a stored leader baseline that names a leg the market never stood behind (a lone print over a zero bid, stamped soft_priced by the race fold) to the current leader instead of measuring a flip from it – the guard against a second wrong post (“Kate O’Flynn overtook Tie”) born from the first. Carries source=kalshi + ticker (the event) + guild_id. Expected to fire ONCE per poisoned row and then never; a sustained rate means a board whose leader keeps flipping into a soft print, worth a look. Rendered in the “Filtered markets” line, never flagged.                                
market_image utils/market_cards.py guild_id, surface (drop|alert), market_source (kalshi|polymarket), image_source (the ladder rung that answered: number|bare_art|chart|banner; number is the DEFAULT since bars went sports-only on 2026-08-08 – a non-sports card heroes its leading leg, so chart belongs to the bet_* surfaces and a Sports-category drop/alert), ok; on image_source=chart also: outcomes (# lines drawn), with_series (# with REAL price history vs a flat line) — a market post’s CARD IMAGE was resolved (the link-unfurl fix). kalshi.com links don’t unfurl (Vercel bot wall serves Discord’s crawler a “security checkpoint” instead of OG tags), so cogs/market_drop + cogs/market_alert build their OWN embed (clickable market link + her take as the embed description + image) instead of a bare link. The image is the self-rendered chart card (utils/market_chart.py — market name + each outcome’s price line + big %, co-branded “Tootsies on {source}”) on ~every post; the polymarket native image → committed floor banner are fallbacks only if the chart can’t render. This records which RUNG answered so the ops monitor watches the mix + catches the chart silently failing (a spiking banner rate) or the candle fetch degrading (a with_series=0 rate). ok is True (a card image always resolves — the banner is the floor); image_source + with_series are the real signals. bare_art (2026-09-15) is the rung for a WORD hero over real art on a NON-MUSIC lane: the picture ships with no card drawn on it, because the post’s heading and Toots’ take already say what the card would stamp (the Emmy frontrunner card). A number hero keeps number, and so does every music card whatever its hero. Measured at 92 of 912 heroed cards a month, all on alert / predictions / drop. Watch the two as a MIX: ANY bare_art on a music surface is a bug in is_music_lane, and a rising share on the market surfaces means a figure builder started handing words where it used to hand a rank                                    
rt_reconcile cogs/market_alert.py (the “final numbers” lane, #final-numbers) guild_id, shipped (cleared the 0.6 reconcile gate + posted), score/reason (the drop_score(reconcile=True) self-gate), + folded detail: event_ticker (the settled KXRT event scored), verdict (exact|close|miss), reason=void (settled with no usable expiration_value -> resolved + nothing posted) | ungrounded_number, post_preview — the RECONCILIATION half of the prediction surface: market_alert posts a Rotten Tomatoes score FORECAST card off a Kalshi KXRT-* scalar ladder (~38.9 . Rotten Tomatoes score), and once the film’s reviews land and the market SETTLES this lane scores the forecast against the real Tomatometer (read off the settled market’s expiration_value, utils/rt_reconcile.parse_settled_rt_score). The forecast we posted is logged at fire time in rt_forecast_calls; the hourly sweep (rt_reconcile_tick) reads the KXRT settled set once and scores each guild’s matching forecast, delivering a build_number_card “— Final” card through the shared staging/room/X spine. Honest by construction — the drop_score reconcile rubric HARD-FAILS a spun miss or an inverted direction, and the pure verdict is arithmetic (utils/rt_reconcile). Gated by the market_reconcile experiment (default STAGING, separate from market_alert so a new scorecard auditions on its own) + mood + master kill switch. Dashboard: a rt_reconcile panel (ship + verdict split); no monitor earned yet — films settle rarely, so the shared desk ship-rate + the tick liveness cover a regression                                    
rt_reconcile_tick cogs/market_alert.py duration_ms, ok, + folded detail: settled_events (KXRT settled-set size, read once per sweep) and posted (Final cards shipped this sweep) — the hourly reconciliation sweep’s liveness + latency (twin of market_alert_tick). A sweep that finds a settled forecast but never posts, or a settled_events that drops to 0 while KXRT is active, is the silent-degradation tell                                    
market_card_hero utils/market_cards.py (emit_card_hero, called from build_market_card’s number rung and from build_number_card) guild_id, surface, kind (figure|headline), source, + folded detail: figure, titled (outcome|question), chart (the accent-block chart tag, absent when the category ladder tagged the card), block (the accent block that was DRAWN, always), rung (which rung of card_block drew it: figure_tag|chart|event|category|sport|market_category|post_type|caller|bare_art|none) — WHAT A MARKET CARD ACTUALLY HEROED, one event per card, on every surface that draws one (the drop, the alert, the music newsroom, the cinema desk). The standing rule is that a card heroes the OUTCOME the market prices, never the market’s confidence in it (owner steer #1918): a percent answers “how sure is the market”, which is not the question the reader asked. kind names the MODE, and both healthy modes are here — headline is the outcome set big with the figure cleared, figure is a number the subject owns (#1, ~147K, 105M, 80+, NO). rung is bare_art (2026-09-15) when the card drew NOTHING: a WORD hero over real art, on a non-music lane, ships the picture bare, so the event records the hero that was decided and the rung that replaced the render. A bare_art row on any music surface is a bug in is_music_lane, not a mix shift. A bare percent in figure is the regression (where kind == 'figure' and figure matches regex @'^[0-9]{1,3}(\\.[0-9]+)?%$'). WHY IT EXISTS (the 2026-09-03 sweep): the hero had NO telemetry outside the music newsroom. Measured over 14 days, 261 events carried a card hero and every one came from music_news_verified — the drop and the alert, the two highest-volume market surfaces, logged nothing about theirs. So a hero rung could stop firing and ship percent heroes indefinitely with the ops monitor blind to it; the only detector was the owner seeing the post, which is exactly how the Olivia Dean “41% odds” card was found. Every field is already in the schema vocabulary, so the event costs nothing against the Axiom field budget. Dashboard: the card_hero panel (shape mix per surface), the rank-filtered card_chart, plus card_block (which RUNG drew the accent block, across every card, per surface). The two block panels answer different questions and neither subsumes the other. The BLOCK TEXT itself has no panel on purpose: detail.block carries it on every row, so a rung mix that moves is one Axiom query away from the words, and a third panel would have said little the other two do not. Monitor: the ops-monitor percent_hero finding (medium) — flagged per SURFACE by RATE, over PCT_HERO_RATE_CEILING (34%) with at least PCT_HERO_MIN_CARDS (6) cards, because ONE rank-less binary per window is the known remaining shape (#2812) and a third of a surface’s cards is a rung that regressed. Fail-open — the telemetry never breaks a card. block + rung are the FOURTH rule (#3263), and they are what makes the third one diagnosable. chart records what the chart/event rungs read and is absent otherwise, so a card tagged by the category ladder logged NOTHING about its block: the Miley Cyrus companion-projection card shipped ENTERTAINMENT over a first-week-units figure and no query could see it (#3261 — the owner found it by reading the post on X). block is the tag that was drawn, always; rung is which rung of market_cards.card_block answered. READ IT AS A MIX PER SURFACE, NEVER A LEVEL: category is the CORRECT block on most of the Entertainment board, so a level threshold would fire forever; what says something is a surface whose category / post_type share CLIMBS, which means a market family whose own words stopped being read. No ops-monitor finding, deliberately — the monitor’s deterministic core compares a window against a fixed ceiling, and there is no honest ceiling here for the reason just given; the panel is the right instrument until someone wants window-over-window comparison, which the monitor does not do today. titled is the SECOND rule on the same card (owner steer, 2026-09-06): the card titles itself with the OUTCOME, never with the market’s QUESTION. The rule reads the question’s subject off market_cards.outcome_split — a verb registry over Kalshi’s own prose, measured at 95% of the live board (scripts/dryrun_market_outcome_title.py) — and a question it cannot split KEEPS the title it always had, rather than ship a subject cut out of the middle of a name. That fallback is fail-open by design, which is exactly why it needs a rate: nothing raises, nothing looks broken, and the card simply goes back to asking instead of reporting. titled is stamped off the rendered title (a trailing ? is the whole test, so it cannot disagree with what shipped). Dashboard: the card_titled panel (outcome vs question per surface). Monitor: the ops-monitor question_title finding (LOW) — per SURFACE by RATE, over QUESTION_TITLE_RATE_CEILING (50%) with at least QUESTION_TITLE_MIN_CARDS (10) cards. The rate divides by cards CARRYING titled, never by every card the surface drew: the build_number_card path (music desk, newsroom, cinema desk) draws cards with no titled at all — correctly, they are not market cards — and dividing by all heroes padded the denominator enough that a surface could be entirely question-titled on its MARKET cards and never reach the ceiling (measured the day the field shipped: music_desk drew 9 cards in 6h, not one a market card). Both thresholds sit looser than the percent_hero pair on purpose: a question title is a safe card, not a broken one, so this flags a market FAMILY the registry has stopped reading (new exchange phrasing) and not an ordinary tail. The fix it names is to re-run the sweep and extend market_cards._OUTCOME_VERBS. chart is the THIRD rule on the same card (owner steer, 2026-09-08): the accent block names the CHART the market settles on (market_cards.chart_card_fields, the registry in market_chart_tag; measured 146 of 8,000 open Kalshi events tag, all Entertainment, plus the artist YouTube view-count family that the alert posts most). chart is the tag drawn; it is absent when the category ladder tagged the card, which is the safe fallback working (the take under the card carries the chart, so nothing is lost; the card has had no caption since 2026-09-10). Dashboard: the card_chart panel, which stays RANK-FILTERED on purpose (Codex review on #3268) — a chart-registry gap shows up only among cards that hero a #N, and in the all-cards card_block mix below those few are buried under the large, legitimate population of non-rank cards that use category correctly. It now reads detail.rung rather than inferring the fallback from an empty chart. No monitor: the fallback is deterministic and legible, so a rising no-chart share on a surface is a registry gap to read off the panel, not an incident.                                    
market_outcome cogs/market_alert.py (the decided-ladder outcome lane, #2655) guild_id, ok, reason, shipped, score, post_preview, + folded detail: event_ticker, verdict (exact|close|miss), question, web_value, x_value, market_now — a DECIDED ladder (ladder_is_decided: the event happened, the board collapsed, Kalshi not yet settled — the GTA VI runtime window) triggered a hunt for the real result. TRUST MODEL (owner steer 2026-08-28 “kill grok if you have to” — the first dry run showed X doesn’t carry niche numbers, 3 of 5 real outcomes declined on the X leg while the web read was exact every time): the WEB leg (Perplexity) must fully land — a non-X publisher citation + a number extracted by its own Haiku call (outcome_extract) — AND the confirmed value must match the collapsed board (the market’s own traders are the second independent authority; the tolerance is GRADED — ~1.5 rungs when X corroborated, the strict half-rung sources must meet when it didn’t, the live URBN catch: an uncorroborated web read confidently cited the COMPANY comp 8.4 for the BRAND board settled 6.2 and the loose bar would have posted it); GROK x_search is the BONUS corroborator — silent/absent proceeds (recorded in detail as x_miss: no_x_client|x_no_result|x_no_value, with corroborated true/false), but an X read that SPEAKS a different number still vetoes (disagree) (market_consistent — the wrong-subject guard: a decided board is near-certain, so a confirmed far number means the search answered a different question, never “the market was wrong”). The card grades the market’s PRE-collapse forecast (market_forecast_refs), not the collapsed E[X] (which copied the answer). ok=False + reason names the decline (no_web_client|web_no_result|web_uncited|web_no_publisher — the web leg must cite a real press outlet: an X mirror can be the very post Grok reads, and the prediction market’s own page (kalshi.com/polymarket.com) is the board grading itself|web_no_value|disagree — X spoke and contradicts the press|market_contradicted|no_forecast_ref — a stored PRE-collapse baseline is REQUIRED: without one the only quotable “forecast” is the collapsed board’s copied answer, a false forecast history, so the board gets no card; the move lane also never seeds a decided ladder’s baseline for the same reason); a decline keeps #2654’s plain suppression (absent over invented) and retries after a 6h deadline ENCODED IN the attempt state’s value (encode_retry/attempt_blocks — restart-proof: a DurableCache L2 hit re-enters L1 under the default TTL, so a TTL-only retry row would pin for 3 days after a redeploy). ok=True carries shipped/score (the drop_score(reconcile=True) gate on the ladder kind; on kind=entity the score is claude.winner_line_score, the lane’s persona-less faithfulness judge – claude_api purpose=winner_line_score) or reason=ungrounded_number. Bounds: the market_outcome experiment (default STAGING — #bot-logs audition), a durable per-(guild,event) attempt state (market_outcome_attempt kv namespace: posted/rejected terminal under a 30-day TERMINAL_TTL so a long-open collapsed event can’t re-post after the default row expires; declines retry after the encoded 6h deadline), and 1 paid hunt per guild TICK (a shared budget across every room’s movers + trending passes). The card’s source chip credits the RESULT’s actual publisher — the web read’s first cited domain (cited_host, e.g. vice.com) — never Kalshi (unsettled; it supplies only the sub’s forecast); an uncited read ships the neutral unverified ‘Reported’ label. The lane RIDES the market_alert tick (its pools are the collapse detector), so market_alert OFF or a spent alert daily cap idles the hunts — deliberate; the market_outcome experiment narrows within that, never widens past it. Dashboard: a market_outcome panel (decline-reason mix + ship rate); no monitor yet — decided ladders are rare (0 of 634 live boards on #2654’s sweep), so the panel + the market_filtered reason=forecast_decided count (which keeps firing either way) cover liveness. A rising disagree/market_contradicted rate is the extraction or the question-building drifting — that’s the number to eyeball kind=entity is the WHO-WINS twin (the Emmy incident, 2026-09-06; trial key market_outcome_entity, default STAGING). A named field decided by the real event – market_outcome.entity_is_decided: one leg a lock (>= 0.95, DECIDED_LOCK_PRICE == SETTLED_LEADER_PCT) the book’s own bid stands behind, every sibling <= 5%, and the lock NEW (its previous_price under the line, so a standing 97% category never hunts and the tell switches itself off a day after a real result) – hunts the winner’s NAME from the market’s CLOSED slate (claude.winner_extract returns an index or null, never a new name). Same decline vocabulary; reason=market_contradicted here means the press named a different candidate than the lock leg (names_match; live on the Creative Arts Emmys the Guest Actress in a Comedy field declined this way – board 0.96 Gilpin, press twice Cherry Jones – the honest fail-closed for a press/market disagreement); reason=market_talk (ok=True, shipped=False) is the composed line quoting a %, naming Kalshi or a market, or using the odds words after the one named re-ask also did – terminal, and a rising rate means the compose framing lost to its context; reason=void is the settle sweep closing a field Kalshi settled with no YES leg. On the sweep’s shipped row detail.winner is the lock the row recorded, detail.web_pick the leg Kalshi settled YES, and detail.flipped=true marks the two disagreeing (the settled leg posts as the official winner either way). detail carries winner / web_pick / x_pick / prior (the lock’s day-ago price, the card’s sub-line) / market_now; detail.authority=kalshi_settlement marks the sweep’s OFFICIAL final – the market_outcome_calls ledger row written at detection, read against Kalshi’s raw markets on reconcile_tick (settled_winner), posted only when the press hunt never shipped (an attempt state of posted closes the row silently: one result, one card; a live retry:hunting: / retry:settling: HOLD state makes the other delivery path stand down for up to 15 minutes; the sweep reads the whole field in one 1000-row page, least-recently-checked row first; the lane runs only on events Kalshi marks mutually_exclusive). A ladder row carries no kind. Dashboard: the same market_outcome panel, now split by kind. No monitor, for the parent’s reason (rare by construction); the liveness tell is market_filtered reason=decided_entity, which counts every claim whether or not the hunt confirmed. Expect it QUIET outside awards nights and election nights, and expect a burst on them: the 2026-09-06 Creative Arts Emmys would have produced two claims in one hour (Woodley, Harden Jr.). A rising market_contradicted rate means the extractor is answering a neighbouring category – read the web_pick beside the winner before touching the tell.                                    
market_projection cogs/market_drop.py (_project_lone_market, #2490/#2697) guild_id, ok (a projection was read), kind (threshold|outcome), reason (which rung answered, or why none did: units|rank|no_projection), + folded detail: ticker (the SOURCE event) — a LONE-leg Kalshi market tried to read a PROJECTION, so the card can report a number instead of the leg’s bare percent. The outcome kind has TWO rungs (#2700, owner steer “either say the projection or the rank”): the cross-series companion ladder’s first-week units, then the trade desk’s projected chart rank (reason names which one answered). Emitted on EVERY attempt, both kinds, which is the point: a rung that quietly stopped resolving — Kalshi renaming a ladder subject, HITS moving a field, a series retired — raises nothing and skips nothing, and reason=no_projection now DROPS the slot, so without this event the surface would just go quiet. reason=rank rising while units falls means the companion-ladder match is degrading even though cards still ship — the failure the panel exists to catch, and one no error rate would show. The market_projection dashboard panel groups ok by kind; no monitor is earned yet — the outcome family is 4 open markets at a time, so a 12h rate is too sparse to gate on without flapping, and the panel’s ok=false count is the eyeball alarm (on the live sweep all 4 open debut markets matched exactly one ladder, so steady state for outcome is ~0; a nonzero count is either a subject that stopped matching or the two-ladders-for-one-subject tie the reader refuses). Revisit if Kalshi grows the family.                                    
circuit_breaker utils/markets.py + utils/api_sports.py + utils/the_odds_api.py + utils/espn.py (via utils/circuit_breaker.py, #615 Kalshi / #725 SGO + API-Sports + Odds API / Polymarket / #1113 ESPN) integration (kalshi/sgo/api_sports/the_odds_api/polymarket/espn), from / to (closed/open/half_open), failure_rate, samples — an outbound-client circuit-breaker state change. Same mechanism, one instance per client. Kalshi/SGO crater from the datacenter IP (50%+ failure windows); API-Sports got its own breaker in #725 (it’s the live-scores + final-settlement backbone), and the Odds API got one too (it’s now load-bearing — the /bet slate’s SGO-down source AND the settlement backstop). Polymarket got one once it became load-bearing for the live Bookie reprice — at parity with Kalshi (breaker + bounded retry on every read), so a Polymarket crater short-circuits straight to Kalshi with no wasted round-trips. Once the failure rate over the window crosses 50% with enough samples the breaker trips OPEN and short-circuits every read — no network call — for a cooldown, so the bot stops hammering the downhost and degrades gracefully (a peer source answers). Two degraded signals are load-bearing: the API-Sports breaker routes the bookie’s settlement to the Odds API /scores backstop (payouts survive), and the Odds API breaker makes get_odds serve its stale-on-error cache (_STALE_FALLBACK_SECONDS, 10min) so /bet keeps showing recent games through a blip instead of blanking. After the cooldown one HALF_OPEN probe tests recovery: success → CLOSED, failure → re-OPEN. Each breaker is scoped to its client instance (a crater of one never stalls the others). Fail-open, pure + clock-injectable, telemetry-agnostic (the client wires this event as its transition hook)                                    
odds_health cogs/commentator.py (via utils/sportsdata/odds_health.py, #968) game_key, sport, sources (the source names compared), home_pcts ({source: home implied %}), max_divergence (max pairwise gap in home %, 0 if <2 sources), incoherent (sources whose sides don’t sum to ~100±vig) — a STATELESS cross-source odds snapshot for one LIVE game. With every source for the game already in hand (SGO/enriched line via game_sides_pct, The Odds API via event_sides_pct, Polymarket + Kalshi via prediction_sides_pctno extra fetch), the commentator emits each source’s home implied % so a source that’s stale/wrong (it diverges from the others) or mis-parsed (its sides don’t sum sanely) is catchable with no oracle — disagreement IS the signal (the 37%-on-a-93%-game bug reads as a ~56pp divergence between a frozen book line and the live prediction markets). #1458: a LIVE single-leg knockout ALSO gets a match_winner source — the full-MATCH (win-by-method) line via prediction_match_winner_sides_pct (Kalshi Method of Victory, summed per team; one extra fetch but the Kalshi pick is cached + shared with the Bookie reprice). This is the one comparison the regulation-only sources CAN’T provide: at 0-0 full time EVERY regulation line collapses to the same 50/50 (both win legs → ~0), so they AGREE on the wrong number and the cross-check saw no outlier — the match-winner line (Spain ~71%) is what makes the stale regulation line an outlier, self-flagging the 50%-at-0-0 bug this PR fixes even if the fold ever silently stops engaging. Emitted only with ≥2 sources (divergence needs a comparison); computed from THIS tick and forgotten (no storage, no history — owner steer). Pure helpers in utils/sportsdata/odds_health.py (summarize_odds_health + the per-source *_sides_pct extractors, all unit-tested); fully fail-open (observability never breaks commentary). Ops-monitor keys odds_divergence (gap ≥ ODDS_DIVERGENCE_PP 15pp → a source stale/wrong) + odds_incoherent (off-sum line → parse/mapping drift), and renders an “Odds health” line every run. Reactivity (did-it-move-on-a-goal) was deliberately dropped — it’s the one signal needing stored per-tick history; divergence already catches a frozen source as an outlier                                    
prediction_match cogs/commentator.py (via utils/sportsdata/odds_health.py:diagnose_prediction_match, measure-to-improve #kalshi) source (kalshi|polymarket), game_key, sport, matchup (away @ home, ≤80 chars), candidates (# raw snapshots the source returned before matching), stage (no_coverage = returned nothing / no coverage of this game/league; no_match = returned snapshots but none matched this game; mapped_fail = a snapshot matched but yielded no usable line; ok = produced a two/three-way line), reason (on mapped_fail: unmapped_labels = an outcome label didn’t resolve to home/away/draw, the cross-provider NAME gap; no_outcomes; incoherent_or_ambiguous = labels mapped but the line was rejected downstream), unmapped (up to 5 offending outcome LABELS on reason=unmapped_labels — the actual failing team names to fix, e.g. a nickname/abbreviation that didn’t substring-match the bookie’s full name) — the per-source game-line FUNNEL for ONE live game, turning “is Kalshi matching?” into a queryable number + the exact names that missed. Emitted per prediction source on every live game every tick (comprehensive coverage, alongside odds_health, from the SAME already-fetched snapshots — no extra fetch). Pure diagnose_prediction_match (unit-tested); fully fail-open. Ops-monitor aggregates a render-only “Prediction line funnel” per source (the no_coverage/no_match/mapped_fail/ok distribution + the top unmapped labels) so the Kalshi-vs-Polymarket gap and its cause are watchable every run                                    
bet_settle_backstop cogs/bookie.py (#725) source (the_odds_api | espn), bets_settled — the Odds API /scores PAYOUT BACKSTOP settled open bets because API-Sports (the primary final-score feed) was down (api_sports.degraded). The symmetric resilience to the /bet slate fix: a World Cup/NBA bet still pays out W/L through an API-Sports outage by settling off the Odds API final (matched by the cross-provider match_key, names aligned by canonical_team #739). Budget-safe: only for sports with open bets (db.open_bet_sports), reusing the shared idempotent _settle_game. The degraded gate is GONE and this is no longer an outage marker (#2465/#2466). The leg is ALWAYS-ON, so a row here means the backstop won the settlement on the merits, not that API-Sports was down – reading it the old way inverts what it says. It had to change because the failure was invisible to a breaker: the API-Sports baseball and american-football hosts sit on a free plan that reads only seasons 2022-2024 and answer a current-season query with HTTP 200 plus an empty body, while the healthy soccer/basketball hosts on the SAME shared breaker kept it CLOSED. So api_sports.degraded was never true, the backstop never fired, and current-season MLB/NFL bets would have voided at the 96h stale-void. Verified 2026-08-31: 7 bets settled over 45 days and 4 came through this leg, against zero in the 45 days before #2465. So it is no longer true that it normally never fires – on the current bet mix it carries the majority. A run of ZERO here while MLB/NFL bets are open is now the thing worth asking about. ESPN is now a SECOND backstop source on this event (#2466). The Odds-API leg is metered, so a quota lapse or a key failure would have left MLB and NFL with no settler at all. ESPN is free and keyless and carries the full final scoreline for both, so it is registered as the third layer and its settlements are counted here too. Read source to tell the layers apart: the_odds_api is the metered first backstop, espn the free second one. backstop_settled on bet_settle_tick now SUMS both legs rather than holding the Odds-API leg alone. The dashboard panel is Settlement BACKSTOP payouts by layer on the Bookie board. No dedicated monitor: a window of zero here is the healthy case whenever API-Sports settles everything, so it would alert on nothing, and the real alarm – settlement has stopped paying out – is already the bet_settle_tick stalled monitor plus the 96h stale-void                                    
bet_slate_narrowed cogs/betting_board.py (bettable_slate, #mlb-menu-visibility) guild_id, surface (bet_board | bet_alert), + folded detail: kept, dropped, sports (the cut sport keys), watched (the guild’s coverage picker) — the /menu COVERAGE picker cut games out of a betting POST surface’s slate. The two /menu sports pickers answer different questions: the BETTING picker says what /bet takes bets on, and the COVERAGE picker says what Toots covers at all. The post surfaces (the odds board + the line-move alert) now require BOTH, so a guild can keep taking MLB bets while its board never names MLB. The owner asked for this on 2026-09-14 after a morning board came back five-sixths MLB for a guild with no MLB in its coverage picker. This event exists because the narrowing is otherwise SILENT — it raises no error and writes no post, so a board that goes quiet because a mod unticked a row looks exactly like a dark feed. It is emitted ONLY when it actually cuts something, so a slate that loses nothing costs no events; the expected steady state on the master guild is one firing per 5-minute tick per surface while the betting picker holds a sport the coverage picker does not. kept=0 is the interesting value: the surface is dark for that guild, and detail.sports names the picker row to check. An unset coverage picker narrows NOTHING (storage cannot tell “never set” from “emptied”), and a coverage READ ERROR keeps the full slate and emits error source=bet_board_watched instead — a DB blip must not silence a surface. Graphed on the bet_slate_narrowed panel; no monitor, because a nonzero rate is the filter WORKING and the rate the owner would want flagged (kept=0) is already the surface_dark finding’s job.                                    
bookie_live_reprice cogs/bookie.py game_key, sport, source (polymarket|kalshi|none), repriced (bool), matched (bool), sides (# bettable sides after), line_type (moneyline|match_winner, only meaningful when repriced) — a LIVE game whose sportsbook line was stale / incoherent / absent was repriced off the prediction markets’ live implied %s. SGO is our only live sportsbook feed; when it’s down (its monthly entity cap recurs exhausted) the Odds API h2h fallback is a pre-match-style line that doesn’t reprice in play (the frozen Norway −205 / Draw +1000 served on a live 0-0 game — the screenshot bug), while the prediction markets move in real time. _supplement_with_live_predictions reprices a live game _sides finds no coherent line for, fetching Polymarket AND Kalshi CONCURRENTLY as a unified resilient enricher (#prefer-kalshi: KALSHI preferred — CFTC-regulated + US-web-bettable — Polymarket the fallback, and kept for a SOCCER game whose Kalshi line came back a draw-less 2-way, via the usable_prediction_line guard so a soccer draw is never dropped; feed list derived from the source-capability matrix utils/sportsdata/sources.py, each feed circuit-breaker + retry guarded) and pricing off the first that yields a coherent line (a sanity guard, three_way_coherent, rejects a single-digit-% draw / near-decided market before it reaches a bettor; an incoherent fold is cleared via clear_moneyline so no half-line lingers). matched=True, repriced=False is the defensive-parsing / silent-thinning signal: a market for the game EXISTED but yielded no coherent line (schema drift, e.g. a Polymarket label change, or a near-decided market) — distinct from matched=False (no coverage). ODDS-ONLY via prediction_line_odds/fold_prediction_as_line: maps outcomes to home/away/draw through our internal canonical_team mapping (the SAME key match_key/settlement use; fail-safe on an ambiguous name) and writes only game.odds — never home/away/title/meta, so reconciliation is provably unaffected. Fail-open per game + per feed. #968: also carries implied_pcts (a demap’d {side: pct} JSON string of the ACTUAL implied %s the fold wrote — ONE field, not three home_pct/draw_pct/away_pct fields, #1296; before this, repriced=true told us a fold happened but never WHAT it priced, so a stale 37%-on-a-93%-game fold was invisible in logs). MUST-WIN (single-leg knockout) games are priced off the MATCH-WINNER line, not the Regulation-Time moneyline (line_type=match_winner): a knockout is settled on who wins the MATCH (_winning_team pays the advancer via ET/penalties, #1120), but the regulation 1X2 market’s two win legs both COLLAPSE to ~0 at 0-0 full time and renormalize to a meaningless 50/50, then the near-resolved floor drops the line entirely — the Spain vs Argentina 2026 final, where bettors saw a 50% card then no odds heading to penalties, while Polymarket/Kalshi plainly showed Spain ~71%. So for a game that clears is_single_leg_knockout (the same predicate the draw-suppression uses), the reprice fetches Kalshi with KALSHI_INTENT_MATCH_WINNER (a prose intent — the Haiku picker lands on the per-game ‘Method of Victory’ market, NOT the Regulation moneyline; no ticker hardcoding, so it generalizes across competitions) and folds it via fold_prediction_match_winner/prediction_match_winner_odds, which SUMS each team’s win-by-method legs (Reg + Extra Time + Penalty Shootout) into a 2-way — the true full-time line that stays correct through ET/pens. It’s gated Kalshi-only-in-practice (Polymarket’s per-game event is Regulation-scoped, so its snapshot carries a Draw leg and the summed fold bails to the normal path), fully fail-open (no MoV market → the existing moneyline path runs, nothing regresses), and the summed fold normalizes by the total so an over-round leg set can’t crash the American converter. The reprice runs on the bettable-slate refresh (~60s) and fetch_prediction_snapshots is deliberately UNCACHED, so each fold uses genuinely fresh prediction odds; the bettor reads the slate cache, so a bet’s line is as fresh as that ~60s fold. The warm loop re-folds live games even when betting is idle (#968): during an active session it runs the full metered refresh, but when idle AND a game is live it does a CHEAP re-fold of just the live games’ odds off the FREE prediction markets (_supplement_with_live_predictions(force_refresh=True) — re-folds an already-prediction-sourced line, leaves an SGO self-priced one) — closing the gap where the commentator pulled fresh odds for a live game but the picker sat stale because no /bet was driving the slate refresh. There is deliberately no placement-time rejection guard (removed per owner: a stale lock is rare enough with the fresh fold that a re-quote isn’t worth the UX)                                    
usage_fetch utils/markets.py + utils/api_sports.py + utils/the_odds_api.py + utils/tts.py + utils/github.py + utils/twitterio.py (#727, #753, #1255) source (sgo/api_sports/the_odds_api/elevenlabs/github/twitterio), ok, duration_ms; on failure: error (+ http_status on an HTTP miss) — one metered-source usage read, the cheap authoritative call behind the quota poll. SGO /account/usage is quota-EXEMPT (it answers even while every other SGO read 429s on the entity cap — exactly when you need it); API-Sports /status is a free account read; the Odds API refreshes its free header budget via /sports; ElevenLabs /v1/user/subscription is a free read of the monthly character cap (#753; may 401 if the key lacks the user_read scope → ok=False); GitHub /rate_limit is free and doesn’t count against the limit (#753); twitterapi.io /oapi/my/info is the quota-exempt prepaid CREDIT-BALANCE read — NOT breaker/rate-limiter guarded, so it answers even while the paid reads fail, which is exactly when you need the balance (#1255). Highlightly, Giphy, and OpenAI have no usage endpoint, so they’re read passively off captured response headers (no usage_fetch)                                    
quota cogs/health.py (#727, #753, #1255) source (sgo/api_sports/the_odds_api/highlightly/elevenlabs/github/giphy/openai/twitterio), ok; on ok=True: tier, metric (month_entities/day_requests/credits/month_characters/hour_core_requests/day_searches/minute_requests/…), period, used, limit (None = source reports it unlimited), remaining, pct (used/limit, None when unlimited); on ok=False: error — a metered API’s usage vs its cap, emitted per (source, metric) by the health-watch usage poll (~30min) so the ops-monitor flags a quota nearing exhaustion (quota_low, high) before it 429s. #1255 added twitterio (credits, the curator’s X source): a PREPAID wallet with no fixed cap, so limit/pct are None and it can’t flag via the generic pct-based quota_low; instead the ops-monitor fires a dedicated balance-FLOOR finding (twitterio:budget, high) when remaining drops under TWITTERIO_CREDITS_LOW — the same prepaid-wallet model as the Odds API’s own budget finding. #753 extended the poll to ElevenLabs (month_characters, the voice-budget SGO-entities analog), GitHub (hour_core_requests, the REST-core hourly cap the /order filing path spends against), Giphy (day_searches, daily cap captured passively off search headers), and OpenAI (minute_requests, per-minute RPM captured passively off embed headers — period="minute" is INFORMATIONAL, rendered but never flagged since a rolling rate can’t fill ahead of time; the ops-monitor’s _INFORMATIONAL_QUOTA_PERIODS guard on QuotaReading.low enforces this). The early-warning that was missing when SGO’s monthly ENTITY cap (100k, rookie tier) silently filled and took /bet dark while API-Sports sat at 90% of its daily request cap. Rendered every run in the ops report’s “Metered-API usage quotas” table; on-demand via the token-gated GET /debug/usage. the_odds_api keeps its own real-time budget finding (off market_fetch credits_remaining, #734), so it’s excluded from the quota-driven finding to avoid a double-flag                                    
music_fallback cogs/music.py guild_id, channel_id, channel_name, reason                                    
music_scored cogs/music.py guild_id, channel_id, channel_name, score, reason, must_post, post_preview                                    
awards_posted cogs/awards.py (#1017) guild_id, period (week|month), ok; on ok=True also: channel_id (None when staged), awards (# categories handed out), winners (# people tagged), manual (a mod’s /awards vs the scheduler), delivered (room = posted to the awards channel; staged = auditioned in #bot-logs – awards graduated 2026-09-01 (#2796), so it always resolves PRODUCTION and staged can no longer occur), cards (# themed champion-card images rendered), hypes (# per-award memory HYPE-UPS that generated — claude.awards_hype on the per-guild awards model, default Opus; hypes=0 with winners is the silent-degradation signal that the memory/model came back empty across every award), rank (FOLDED into detail — query with parse_json(detail)['rank']; the first RANKED award’s real place on the board it was crowned off. ABSENT when the post has no ranked headline — a window with a besties pair but no reactions or replies omits MVP and Funniest, and besties is a relationship award with no board (rank 0); emitting that 0 would count the row in the audit’s n but never in countif(r > 1), making a headline-less post read as board-leading, so the field is omitted instead: 1 = the board leader, higher = the rolling anti-repeat skipped past recent winners to reach them, and AwardSpec.measure drops the superlative from the post’s wording to match. Added 2026-08-30 after an accuracy audit had to REPLAY 5 weeks of engagement snapshots to answer “was the crown the top of the board” — the answer was no in 3 of 5, once at rank 4, and no event recorded it. ['tootsies'] | where event == 'awards_posted' and ok == true and delivered == 'room' | extend r=toint(parse_json(detail)['rank']) | summarize off_top=countif(r > 1), n=count() by period is the standing check — delivered == 'room' is REQUIRED, since a STAGING audition also emits ok=true and deliberately bypasses idempotency, so repeated mod previews would otherwise swamp the real crowns this measures); on ok=False also: reason (no_channel = production but none set on /menu; no_window = no seed / no snapshot to diff against, run /stats period:; no_winners = the window was too quiet; empty = the voice call came back blank) — the weekly (single-award “Player of the Week”, same fanfare as the monthly, Sundays) / monthly (gallery led by the “{month} MVP” e.g. “June MVP” + funniest/besties, the 1st) engagement awards posted (weekly + monthly headline are the SAME engagement metric titled per cadence; NO OVR/”Player of the Month” award). Each award is the top of a WINDOWED board (seed − the period snapshot); pings the winners. Gated by the awards experiment (off skips, staging auditions in #bot-logs, production posts to the awards channel) + master kill switch + mood != off; idempotent per ISO-week / per-month with anti-repeat over the prior period’s winners                                    
curator_fetch utils/twitterio.py (#curator) phase (user|search|trends), handle, count (# posts / trends parsed), ok, duration_ms; on a phase=user timeline read also guild_id + stop_reason + pages (#curate truncation audit): the fetch is PER-GUILD (each guild passes its OWN seen-set as stop_ids, so the paged walk terminates at a different depth per guild — guild_id makes it splittable), and stop_reason records WHY the walk stopped — hit_seen (reached an already-posted tweet = fully CAUGHT UP, nothing dropped), page_end (endpoint has_next_page=false), cursor_end (has_next_page true but no next_cursor), max_pages (walked the whole _MAX_PAGES_PER_FETCH cap and STILL no seen id = unambiguous truncation, more backlog than we can fetch), limit (legacy no-stop_ids single-window cap), page_error (a later page failed, kept page 1). A page_end/cursor_end/max_pages (never hit_seen) with count at a full-page boundary is the truncation signature; a hit_seen means the drop is DOWNSTREAM (best-home routing / fit-judge, see curate_routing/curator_evaluated), not the fetch. pages (# pages walked) folds into detail. undated (also folded) is how many of the returned posts carried NO publish time, and it is the tripwire for a silent fail-open: utils.curate.is_fresh reads an absent stamp as fresh, so when a platform’s time field goes null upstream the 24h curation window stops filtering that platform and NOTHING raises. That is not hypothetical — Instagram’s v1 taken_at_timestamp and created_at both went null on every post of every handle, and the artist curator shipped a seven-month-old Beyonce post as a fresh pick (2026-09-15, fixed by moving the IG read to /v2/instagram/user/posts). Reported by EVERY timeline read on every platform – X (utils/twitterio.py), Instagram and TikTok (utils/scrapecreators.py) – so the tripwire covers the whole class, not just the provider that broke. undated equal to count on a phase=user read is that state: ['tootsies'] \| where event == 'curator_fetch' and phase == 'user' \| extend u=toint(parse_json(detail)['undated']) \| summarize dead=sum(u), posts=sum(count) by platform. On a miss: error (unprovisioned|no_handle|empty_query|HTTP |breaker_open|) — one twitterapi.io read for the content curator (the fresh-posts fetch from a seed account) OR, `phase=trends` (#1272), X's NATIVE trending topics (`/twitter/trends`) that source the discourse trending-clip surface. The fail-open X source; ops-monitor tracks its integration health (a silent crater = the curator quietly stops finding posts). The metered client is guarded like SGO (rate limiter + circuit breaker + retry); a crater trips its own `circuit_breaker` (`integration=twitterio`)                                    
curator_posted cogs/curator.py (#curator) + cogs/artist_curator.py guild_id, channel_id, delivered (room = posted to the curator channel; staged = auditioned in #bot-logs – the content curator graduated 2026-09-01 (#2796), so on it staged can no longer occur; the artist curator still routes on its own artist_curator stage, so staged there is a real audition), handle (the source account), external_id (the posted source id, the dedup key), engagement (the chosen post’s blended magnitude), source (in_channel|learned|roster|adjacent, the discovery tier — roster = the ARTIST CURATOR’s pick, a top artist off the kworb watchlist via the utils/artist_socials handle map; the artist curator (2026-09-07) emits this same kind stamped surface=artist_curator, the content curator carries no surface stamp – split them on surface), mode (image|text, which fit-judge ran, #1232 Phase 1.5), via_repost (#curator: True = the pick came from a seed’s REPOST, resolved to the endorsed original — watch this rate to see the repost-supply path landing, esp. for aggregator-seeded channels like #her) — the curator posted a fresh on-theme link into a curator channel (the SUCCESS half of the ship funnel). The pick cleared the structural gates (dedup + original-with-media) and a Haiku fit-judge (claude.pick_curator vision, or pick_curator_text) against the channel’s own recent posts, chosen from an account-diversified slate (no engagement pre-filter); posts the source permalink (fixupx-rewritten so it unfurls). Gated by the curator experiment + master kill switch + mood; deduped by the durable curator_seen set (never reposts an id)                                    
watch_link utils/watch_link.py (#music-video-linking, #trailer-lane) guild_id, surface, ok, query (the search phrase), count (hits weighed); on ok=True: source (source_link | youtube | tmdb_trailer | youtube_trailer | youtube_moment — WHICH rung answered; source_link is rung 0, the video the SOURCE POST itself linked, kind=source), url_host; on ok=False: reason (not_music_video|declined|no_results|no_fresh_results|no_link|unprovisioned|pick_error) — resolved (or declined to resolve) the real WATCHABLE link for a wire post that’s about a music video. A discourse take sourced from an X wire post ships to @tootsiesbar with NO link: its source link is stripped (an x.com link would render the quote card the owner banned for discourse) and the wire post carries only photos, so there’s nothing to re-host natively — a take about a music video shipped as a take + a stock press photo with nothing to click, the whole payoff missing. So this searches YouTube and links the REAL video, which X unfurls into a playable embed (shipped WITHOUT the photo so the link drives the embed — the same reason the video_link fallback drops its still). The claim comes from the SOURCE POST, not her take — that’s what makes it work: her take names the PEOPLE (“Travis Scott directing James Blake and Ludwig Göransson”) while the source post names the RELEASE (“The video for ‘When I’m Home’ was directed by…”); searching the take finds nothing. RUNG 0 — TRUST THE SOURCE’S OWN LINK (#watch-link-source, owner steer “trust the link”): before any classify, if the source post already links a video in its text (“Taylor Swift performs a medley … Watch: youtu.be/_9jaJtmraXA”), take THAT link and skip the rest. It is the source’s own pointer to the exact clip — no classifier can refuse it and no search can pick the wrong upload — and it is exactly the case the classify path is built to refuse, a LIVE PERFORMANCE the wire linked (the reported Taylor Swift miss: the classifier correctly declined not_music_video, so nothing was ever linked, while the youtu.be link sat in the post). The link comes from SourcePost.link_urls (the expanded entities.urls, since the tweet body carries only the t.co shortlink); source_video_link keeps only YouTube/TikTok/Instagram (the hosts X unfurls), never an x.com quote. Only when the source links no video does the classify path run. CLASSIFY, THEN ROUTE (owner steer), the same shape as the image resolver (entity_image.resolve_desk_image) including its deterministic-first discipline: claude.video_subject (Haiku) reads the post into a clean {kind, title, artist} subject, and the KIND routes to an authoritative source — film/tv → TMDB’s own /videos (tmdb.trailer_url, a LOOKUP: the studio’s curated trailer keys, so a hit is the real trailer by construction, nothing to pick between or gate on) and music_release → a YouTube search + pick (the one kind with no authoritative endpoint — nobody publishes “the official video for X” as a lookup — so search_query_for builds the query → “Kendrick Lamar squabble up” and claude.pick_music_video (Haiku, text-only) confirms the hit on SONG IDENTITY + PROVENANCE) — load-bearing rather than ceremonial, since the live search for this story returns the right song’s official upload alongside a DIFFERENT James Blake single, a soundtrack trailer, and three fan re-uploads. The two-stage split is what closed the leak the single-call design couldn’t: the POST-level question (“is this even announcing a video”) used to sit inside the picker beside two CANDIDATE-level tests, so it competed with them and a chart-position post linked 5/5 — and both attempts to strengthen the rule IN PLACE measured worse (one made the lead frame fight the rule, the other blew max_tokens on reasoning and started accepting fan channels). Moving the question to its own classifier is the restructure those rewrites were reaching for, and it deleted the brittle half too (the quoted-span regex, the capitalized-word scan, the stopword list). artist is a name to SEARCH WITH, not necessarily the credited act — measured: requiring the crediting artist declined the exact reported post 5/5, because a wire post names the title and the DIRECTOR (“The video for ‘When I’m Home’ was directed by Travis Scott”) and not the singer; anchoring on whoever the post DOES credit restored it at no off-lane cost. The name must appear IN THE POST — inferring it from model knowledge is the wrong-artist failure mode this codebase keeps re-learning. FAIL-CLOSED throughout — it only ever ADDS a link to a post that would otherwise ship without one, so every uncertain path leaves the post exactly as it was (a wrong video is far worse than none); not_music_video is the common, healthy outcome (most takes aren’t about a video). The question stays POST-LEVEL and that restraint IS the design (owner steer): “does a video exist for this subject” is true of nearly every film and song, so it would attach a link to almost every take — loud, and at X’s ~$0.20 link price against ~$0.015; “is this post ANNOUNCING a video” is what keeps the surface quiet. SPORTS is deliberately absent — Highlightly needs both team names AND the game date, which a wire post’s prose doesn’t carry (the commentator posts clips because it holds a live game object with those fields), so the case where we actually have a game is already served there. MEASURED (n=5/case end-to-end through the real resolver, live lookups, scripts/dryrun_watch_link.py): on-lane 29/30, off-lane 30/30 — every off-lane case declines at the SUBJECT stage before any lookup runs, including two that name a subject which really does have a video (a live performance; a casting note for a film with a trailer), the exact subject-level behaviour the post-level question refuses. The one on-lane miss is the reported post’s music lane, re-measured at 11/12 on its own (~92%, the hardest case — its anchor name is the DIRECTOR so the candidate list is genuinely ambiguous), not a regression; the earlier 5/5 reading was equally consistent with 92%, since n=5 can’t separate the two. Watch the pick_error rate (a BROKEN picker — the one reason that’s a problem); a climbing not_music_video share is just the classifier doing its job. COST: a resolved link ships as crosspost mode=tweet with has_link=true, which X prices at ~$0.20 vs ~$0.015 for a link-free post; bounded because most posts decline, but an ok=true rate approaching the surface’s crosspost rate means the picker has gone loose and is spending that ~13x broadly. #trailer-lane (2026-08-06) changed two things. (1) CALLERS. It was wired into discourse ONLY, while three wire desks that post about trailers every day never called it — the reported miss is a cinema-desk take reading “the Awarapan 2 trailer is out” that shipped a TMDB poster and nothing to watch, with no watch_link event at all because nobody asked. All three wire desks (cinema_news, pop_desk, sports_desk) now call it, each stamping its own surface, and only when the source post carries no clip of its own (a native mp4 re-share beats any link). (2) A SECOND RUNG under the film/TV lookup. TMDB is community-maintained, so its /videos list is empty exactly when the post is most current: at post time that title’s record held ONE clip, a six-week-old Teaser flagged official:false, which trailer_url correctly refuses — while the real trailer sat on the production house’s own YouTube channel. So a TMDB miss now falls to the same YouTube search-and-judge the music lane uses, with claude.pick_trailer (THREE candidate tests where the music picker has two: title identity, it-is-the-trailer, provenance — the third is earned, because a SONG off the film’s soundtrack is uploaded by the studio’s own channel and so passes identity AND provenance). Query shape is measured: "<title> trailer" puts the studio trailer in the top few, "<title> official trailer" demotes it and pulls in an AI-made farm upload, the bare title leads with soundtrack songs. The rung split is the thing to watch: source separates tmdb_trailer from youtube_trailer, so a deterministic rung that goes silent (TMDB auth dead, a shape change) shows up as its share collapsing while the lane still looks healthy #lane-commentary-video (2026-09-14) added a THIRD lane, moment (owner ask: “why not use youtube to find the actual videos for all lane commentary”). Before it the resolver answered only for a music video or a trailer, so the ordinary thing the three wire desks post — an interview answer, a press conference, a performance, a viral clip — had no video lane at all and those takes shipped whatever clip the WIRE account had attached, unchecked. The moment kind captures {anchor name, the moment in searchable words}, searches YouTube for both, and judges the hits with claude.pick_moment_clip (three tests: SAME OCCASION, it-is-the-footage-not-talk-about-it, it-is-that-moment-alone — a moment has no owning channel, so provenance cannot carry the judgment the way it does for a trailer). Its deterministic rung is a RECENCY filter, and it is the load-bearing half: fresh_moment_results drops every candidate over 14 days old — and every UNDATED one (fail-closed) — before the judge sees it, because a name-plus-words search returns old footage of the RIGHT person and that reads as a fine candidate to anything comparing titles. A moment a wire is reporting today cannot have been filmed years ago, so the filter settles that whole class without asking a model. no_fresh_results is its own reason: the search worked, the footage just isn’t up yet. Watch the moment lane’s declined + no_fresh_results share against youtube_moment: all-decline means the judge has gone too strict to ever fire, while youtube_moment rising toward the desks’ post rate means it has gone loose and is spending the ~13x link price broadly. MEASURED live end-to-end (scripts/dryrun_watch_link.py, n=5/case): the reported post resolves 5/5 to the New York Giants’ OWN channel — “Odell Beckham Jr.: ‘I just know I’m going to give my all’” — and the off-lane set holds 55/55, transfer / injury / written-statement / signing posts all declining at the subject stage before any search runs. The classifier wording is ablated, not guessed: a “report the FOOTAGE, not a fact about a person” rule refused the reported post 5/5 (a quote IS a fact about a person), a “would it read the same with no camera in the room” test measured completely inert (10/25 on-lane and 50/50 off-lane either way, so it is cut rather than kept), and rewording the kind’s definition to lead with something a person SAID OUT LOUD or DID is what took the press-conference case 0/5 → 5/5. Known miss: a red-carpet ARRIVAL with no quote and no action stays 0/5. A sports GAME HIGHLIGHT is still not a lookup (Highlightly needs both team names and the game date, which wire prose lacks); this lane searches by name instead, and its judge rejects a highlight compilation                                    
curate_run cogs/curate.py (#curate) guild_id, channel_id, delivered (room = posted to the curator channel; staged = auditioned in #bot-logs – the curate feed graduated 2026-09-01 (#2796) on the curator key, so it always resolves PRODUCTION and staged can no longer occur), handle (the source X account), external_id + platform (x, the shared curator_seen dedup key), engagement (the chosen post’s blended magnitude), mode (image|text, which fit-judge ran), via_repost (True = the pick was a repost), has_caption (True = a short Toots caption shipped on the marker line above the link — ARTIST curation only, i.e. a repost of a distinct artist; False = the bare 🔁 link, either not an artist repost, no caption composed, or it failed the short/no-link gate — the fail-CLOSED default), trigger (scheduled) — the scheduled curate-feed surface posted: it distributes a mod-configured X account’s posts + reposts (set on /menu) across the curator channels. Each due slot DRAINS all eligible on-theme posts for a channel, best-first (owner steer — “curate all eligible recents vs slow drip”), not one: the slot is the trigger, but on it the loop repeatedly composes the best UNSEEN eligible post via the curator’s fit-judge + delivers it (marking it seen, so the next compose slides to the next-best) until the judge declines or nothing eligible remains — bounded by _MAX_DRAIN_PER_SLOT per slot (the remainder carrying to the next slot; curator_seen makes that safe), so curate_run fires once PER drained post (multiple per slot). discord.py backs off + retries on a 429 so a batch never drops a post to rate limits. Rides the curator’s /menu calendar cadence + the curator’s experiment (one toggle for both); gated by that experiment + master kill switch + mood; dedup shared with the curator (curator_seen), so a tweet lands in one room and never double-posts. A drained-nothing slot (or the terminal compose that ends a drain) emits curator_evaluated (source=curate_feed) with the skip reason (no_home/none_fit/pick_error/no_candidates/no_posts) instead                                    
wire_identity utils/wire_identity.py (#wrong-person follow-up) ok, source (the desk surface), guild_id, count (replies read), query (the resolved name, on ok/no_web_check), reason (no_replies|no_id|no_web_check) — the thread-identity path ran on a wire moment about an UNNAMED person: the replies under the source post identify who’s in an unnamed viral clip (the @DailyLoud “Brazilian model is going viral” → Gabriela Moura case), where a web search on the post’s vague words matches the WRONG story (the Larissa Nery substitution). A Haiku gate (claude.wire_subject_named) stops the path on a named subject BEFORE the metered replies fetch (emitting nothing — only real identity attempts count); an unnamed one pools TWO reply pages (the newest slice + the post’s first EARLY_WINDOW_SECS minutes, where “who is this” gets answered), like-ranks them, extracts a CREDIBLE identification (claude.identify_from_replies — detail or cross-reply agreement, never a lone bare name/joke/lookalike gag, answer-first NAME:/NONE verdict), and web-verifies the name (a Perplexity bio check; a crowd ID alone never ships a name — fail-closed), then runs the PASS-biased Sonnet vision frame tripwire (claude.identity_frame_mismatch, reason=frame_mismatch on a reject: the wire’s frame plainly shows a DIFFERENT person the model recognizes — the crowd-misnames-a-famous-person case; an unknown/niche face passes, measured: Gabriela Moura passes 3/3, a Taylor Swift mis-claim rejects 3/3). A resolved identity replaces the vague-words confirm and lands in the compose blob via thread_identity_block (name + the wire’s view count as the moment’s hard number + a no-body-comment line; the compose still holds the final coheres-with-the-media judgment). Wired on pop_desk + sports_desk (the named-subject gate makes it near-free on sports, whose wires name their subjects); the ops-monitor renders the funnel every run (“Wrong-person guardrails”) and flags wire_identity_verify_down (medium) on a sustained no_web_check rate — the fail-closed verify leg breaking means identities silently stop shipping                                    
sports_diversity cogs/sports_desk.py (_diversified, via utils/sports_stories.apply_sport_diversity, #sports-news-diversity) guild_id, count (ranked stories in the batch), sport (the BEAT that leads AFTER the re-weight), + folded detail{recent (the beats of the desk’s recent posts, newest-first, ? for an unclassified one), top_penalty (the leader’s magnitude multiplier), leaders (<sport>:<penalty> for the top compose candidates)} — the sports desk re-weighted a ranked batch against the beats it POSTED recently, so one sport cannot own every slot on engagement alone. Emitted once per ranked batch, on BOTH the slot path and the off-slot breaking path. It exists because the problem it fixes is INVISIBLE in the ship events: sports_desk_posted records what shipped, so a desk quietly reverting to all-football reads exactly like a busy transfer window. This event records the DECISION. Read it against the sports_beats panel: top_penalty at 1.0 on nearly every batch means the desk is already alternating beats and the rule has nothing to do; a leader that keeps winning at 0.125 is a genuinely huge moment beating a full penalty, which is the rule WORKING. The failure to watch for is a sport column that returns to all-soccer while detail.recent is also all-soccer AND top_penalty reads 1.0 — that combination means the beat is not being counted at all (the sport column on sports_desk_events stopped being written, or the classifier stopped reading the handle), not that football is winning fairly. No MONITOR: there is no rate that separates a legitimately football-heavy news day (a transfer deadline) from a broken re-weight, and the whole point of the change is that a beat mix is a judgement call, not a threshold. The sports_beats panel builds the baseline first.                                    
media_coherence utils/x_crosspost.py (_media_gate + _clip_gate, #wrong-person follow-up, #lane-commentary-video) guild_id, surface, ok (False = flagged), mode (log|enforce), reason (mismatch|unconfirmed|verified_source|copy_mismatch), detail.kind (clip on a VIDEO verdict, absent on a photo one — the two halves stay separable in a query), detail.note (what the image showed), detail.source (the ART RUNG judged — on EVERY verdict, not only the exempt one, so a rung the judge keeps destroying is visible in aggregate; the gate_rung dashboard panel groups on it, and a mismatch row on a deterministically-verified rung is the #2238 false-positive shape repeating) — the DELIVERY-TIME media↔take coherence gate ran on an image crosspost: the LAST line of defense for the whole wrong-person class, at the ONE chokepoint every art resolver feeds (maybe_crosspost), so it catches the NEXT resolver bug regardless of which upstream path fails. A Sonnet vision tripwire, PASS-biased: our own charts/number/logo cards, artwork, unrecognized faces, and any uncertainty all PASS — the only reject is an image the model recognizes as a different specific person/work than the take names (measured on the shipped cases: the Jack Harlow cover on the Dua Lipa take flags 3/3 with a crisp note, every control incl. a real number card passes 15/15; the Larissa/Gabi clip does NOT flag — a niche unknown face structurally can’t, that class is owned by the upstream brief-discipline + thread-identity fixes). A copy_mismatch verdict NEVER drops the image (the owner’s “art failure” report, 2026-08-31). The judge returns three verdicts — YES / COPY / NO — and only NO (plainly a different person or work) is a drop. COPY means the image shows the RIGHT subject and only the take’s wording or numbers disagree with what the image displays; the image ships and the event records reason=copy_mismatch, because what is wrong there is the COMPOSE, not the art. The answer used to be binary, so the judge had exactly one token for “something here is off” and it put wording complaints in it: 10 of the 40 confirmed drops over 30 days named no wrong subject at all — “the graphic says 5.3B but the post copy says 5.2B”, “the chart shows five new entries, not four”, and the report itself, “the post calls #2 a ‘Dracula remix’ but the chart credits ‘Dracula - Jennie Remix’” (its own note said “Image matches the chart subject fine”). Each one deleted a CORRECT branded chart card and shipped the flawed prose alone, which is backwards: the card is rendered from the parsed rows, so dropping it removes the correction and keeps the error. Measured on the real Hot AC card, n=5 per case: the shipped take goes subject 5/5 → copy 5/5, a wrong-figure take and a miscount take both go subject 5/5 → copy 5/5, an exact take stays clean 5/5, and the true catches hold — a wrong-act take on the same card stays subject 5/5, a Shakira portrait under a Stray Kids take stays subject 5/5, and correct portraits stay clean 5/5. The ops monitor counts these APART from the wrong-art rate (media_copy_mismatch, medium) so the compose bug has an owner instead of being buried inside a drop. A verified_source flag is likewise not a drop and not wrong art that shipped: the gate KEPT it because a deterministic upstream identity check (_IDENTITY_VERIFIED_ART) already vouched for the art. It counts as overruled — still a judge flag for the rate, excluded from the “shipped wrong art” number, so a known judge false positive cannot raise the strongest wrong-art alarm on art we know is right (Codex review, #2748). mode=enforce is the DEFAULT in production (owner call #2219; re-affirmed 2026-08-22): a flagged image is DROPPED and the take ships as TEXT, and the ops-monitor flags media_mismatch medium (verify the drop). It ran log-only first to measure itself — 1025 checks over 14 days, 50 flagged (4.9%), notes reading as true positives (Julie Chen Moonves on a Big Brother CONTESTANT’s card four times, an “X-Men: Apocalypse” poster on an MCU X-Men post). #2216 is what tipped it: the gate caught a KAROL G album cover on a Drake market card, wrote the note, and the post shipped anyway 1.2s later because log mode does not drop. Enforce is opt-OUT (X_MEDIA_GATE_ENFORCE=0 returns to log-only), not opt-in — _media_gate_enforce. It used to be opt-in (“1” turns it on), with the Railway variable as the single revert point; that point failed silently — the variable was set to “1” on 2026-08-09, drifted back to “0” the same day (after the #2238 false positive), and was never turned on again once #2238 was fixed. The gate ran log-only for 13 days and shipped ~32 confirmed wrong-person images (a Mary J. Blige photo on a Chris Brown & Usher touring card among them); it CAUGHT every one, log mode just could not act. Enforce-by-default means a LOST or reset variable can never again silently return the timeline to shipping wrong art. NOTE the scope — it runs inside maybe_crosspost, so it guards the X send ONLY; the Discord room post is unchecked (#2219 item 2). Skipped for image-only posts; fully fail-open.

2026-09-14 — THE GATE’S BLIND SPOT IS THE OCCASION, and the live record says so outright. A pop_desk take — “Odell Beckham Jr. says his Giants return is about showing ‘who I am, not who I was.’” — shipped over a FOUND still from an old football-freestyle promo (pop_desk_posted 02:23:34Z, source=found). The gate ran on it in enforce mode and returned ok=true, with the note “Shows Odell Beckham Jr. in a Giants jersey during an ‘F2 vs Odell’ soccer trick shot video”. It SAW the wrong occasion, wrote it down, and passed — because its question is “a different specific person or work?” and the person was right, the work was right, the OCCASION was years off. This is a different failure mode from every incident above it: those are the gate destroying CORRECT art, this is the gate describing WRONG art and shipping it. No prompt wording fixes it — a wrong occasion genuinely is not a wrong person, so the question itself has to change, and the four exemption incidents above are what a widened photo-gate question costs when it misfires. So the occasion question ships FIRST on the path that has no gate at all.

The CLIP half (_clip_gate). A wire post’s own mp4 is re-shared natively and the photo gate is SKIPPED on that path (the photo is the clip’s own thumbnail and does not ship), so a wire account’s stale footage reaches the timeline unchecked. That path now gets its own judge (claude.crosspost_clip_mismatch) asking whether the frame could be from the occasion the take describes, with “the right person in the wrong situation is still NO” stated outright. It reads the clip’s still thumbnail — all vision can see of a clip, and enough for this class: a studio promo against a field mock-up does not look like a press-conference podium. MEASURED on the reported post’s own frame (scripts/dryrun_clip_gate.py, n=5): flags 5/5, note “This is a still from an ‘F2 vs Odell’ soccer freestyle/challenge video, not footage of Beckham discussing a Giants return.” A confirmed flag drops the clip AND its thumbnail (a frame of the same wrong footage), so the take ships as text. Same mode/reason vocabulary and the same fail directions as the photo half — enforce by default, a drop needs a second agreeing vote, a split verdict ships and records unconfirmed, an errored check never blocks a post — minus copy (a clip displays no numbers to disagree with) and verified_source (no rung verifies that a wire attached footage of the occasion it wrote about). The clip_gate dashboard panel reads these per surface, because the gate_rung panel groups on detail.source, which a clip verdict does not carry. Watch the flagged rate per wire desk: a rate that climbs is a wire account attaching stale footage to its posts.

Extending the occasion question to the FOUND-PHOTO path is the open follow-up, and it is deliberately not bundled here — see the watch_link moment lane above, which fixes the reported post from the other side by giving it the real interview clip, so the found still is never the media.

#2238 — the first enforce drop was a FALSE POSITIVE, which paused enforce; the fixes below make it safe, so enforce is back ON as the default (2026-08-22). A Taylor Swift Billboard post lost her own “The Life of a Showgirl” cover; the note read “That looks like Blake Lively … not Taylor Swift.” The art was correct: an artist-verified iTunes row, her own lead release. What the gate actually does is READ THE TITLE printed on artwork and match it against memory — Sonnet declines face identification outright, so it never judged her face. A release newer than the model’s knowledge has no match, and it filled the blank with a guess (“looks like a Kylie Minogue album”). Ablation, real images, 10 samples each: the full cover rejects 10/10, the SAME photo with the title text cropped away rejects 1/10, and 1989 (an album the model knows) rejects 0/10 — the title text is the whole trigger. Two conditions are needed: the post names only the ARTIST (no title in the text to match) and carries a SUPERLATIVE (a bare chart line does not make it reach). Two changes followed: an ABSTENTION sentence in the prompt (25 real cases × 5: false drops 8% → 0%, true catches unchanged at 75%; the cut-the-work-clause ablation fixed nothing and cost 8 points of recall), and a CONFIRM-ON-REJECT second vote in enforce mode only — the same card/take rejected 3 of 30 times at temperature 0, so one sample was never a safe basis for discarding art. A split verdict ships the image and records reason=unconfirmed. #2241 — the class sweep. The same protection now reaches every card-shipping surface, not just the music newsroom: MarketCard.art_source carries the ART RUNG (distinct from image_source, which names the card KIND), and the market / betting / pop / sports / cinema desks pass it through. sports_api joins the trusted set (a game card’s crests come from the structured game_teams payload the betting surfaces stamp, so no extraction can get the subject wrong). The exclusions are each backed by a real catch: tmdb_poster (the X-Men: Apocalypse poster), tmdb_person (“that’s Michelle Pfeiffer, not Michelle Hadley” — a namesake), deezer_artist (the LOOSE artist-name match only — see the 2026-08-29 entry below), and found/native/market_leg all keep the gate’s full authority (youtube_chart was on this list until 2026-09-09 — see below). A measured dead end, so nobody re-derives it: asking the model to CONFIRM its own flag by naming the subject — with an explicit “a guess is worse than UNKNOWN” — does not work. It does not know that it does not know. It answered “The Life of a Showgirl — Kylie Minogue” 5/5 rather than abstaining, while abstaining 4/5 on a BTS cover that was a REAL catch; 4/8 separation, wrong on exactly the cases that matter. Only an upstream deterministic check helps.

2026-08-29 — #2238 repeated on the rung it did not cover, so deezer_artist_exact is now exempt. A market_alert Kalshi card (“Daily Top Music Videos USA”, Taylor Swift at 16%) resolved her own official Deezer ARTIST portrait — the “The Life of a Showgirl” underwater press photo — and the judge flagged it “a woman submerged in water who does not appear to be Taylor Swift”. The second opinion agreed, so the drop was CONFIRMED and the post shipped as a bare number card (crosspost mode=number_fallback, 14:55Z). The art was verified correct: the cached bytes are pixel-identical to Deezer artist 12246 (the real Taylor Swift, 12.7M fans), and the take scored 0.92 and named the true frontrunner. Same artist and same era artwork as #2238 — the fix there listed rungs, and this rung was not on the list. entity_image._artist_rung now splits the portrait into deezer_artist_exact (Deezer’s row is NAMED what the take calls the artist — a deterministic identity check, exempt) and deezer_artist (loose token overlap, still gated, because it really does return a namesake: “Michelle Hadley” → “Michelle Marly”, live-checked). Watch reason=verified_source with detail.source=deezer_artist_exact — it counts portraits the judge would have destroyed.

2026-09-09 — #2238 repeated on a THIRD rung, so youtube_chart is now exempt. A market_outcome winner line (“Dai Dai won Top Daily Music Video Global on YouTube for September 8”, KXYTDAILYTOPVIDEOG-26SEP08, scored 0.95, Kalshi settled the leg YES) carried the leg’s own video thumbnail, resolved by video id off the chart the market settles on. The judge wrote “Image shows Shakira in a music video (World Cup themed styling with jerseys), not ‘Dai Dai’”, the second vote agreed, and the correct card shipped as a bare number (crosspost mode=number_fallback, 16:54Z). The art was verified correct: the cached bytes are the official Dai Dai (Shakira & Burna Boy) Vevo thumbnail. The cause is structural. A thumbnail is a video FRAME with no title printed on it, so the judge has no text to read; its only lever is recognizing a famous face, and a Kalshi leg is a bare song title with no artist, so a famous face it recognizes and a title it does not know read as a mismatch. That fires on exactly the biggest artists. Measured over 30 days: 9 verdicts on this rung, 0 true catches, 2 flags, both this false positive — and the same thumbnail PASSED 4 times on market_drop, whose take names Shakira because _youtube_grounding_block feeds it kworb’s upload title. The exclusion had been reasoned from the RESOLVER (most-viewed phrase match among several candidates), which is a residual the judge cannot see either. So a UNIQUE match is trusted like a catalog credit: the video id is the chart’s own identity for the leg. The trust stops there (Codex review on #3109): kworb.find_video_match reports whether exactly one charting video carried the phrase on a complete read of both kworb pages for a chart day within two days of today (a half read, or a dated market the settle sweep renders late, never reports unique), the resolver names that youtube_chart and a most-viewed PICK among several youtube_chart_pick, and the pick stays gated — the same split as deezer_artist_exact over deezer_artist. Live chart 2026-09-09: 9 of 15 open legs resolved, 7 unique (“Dai Dai” among them), 2 picks (“Golden”, “Shararat”). A wrong TAKE on this family stays gated upstream and deterministically (take_references_leg on the drop, winner_line_score on the outcome lane). Watch reason=verified_source with detail.source=youtube_chart.



2026-09-13 — #2238 repeated on a FOURTH rung, so an UNPICKED source_photo is now exempt. A cinema_news post (“Buddy” passes $20 million domestically…) carried @DiscussingFilm’s own photo of the film’s title character, a costumed orange unicorn. The judge answered “not the dog from ‘Buddy’”, the second vote agreed, and the take shipped bare on X (crosspost mode=tweet, 22:02Z). There is no dog: TMDB 1514026 is “a magical, singing orange unicorn holds his cast hostage in a surreal TV-show dimension”, and Toots’ own desk had posted “Buddy the Unicorn” nine days earlier. Take right, art right. Two days before, the same rung dropped a sports_desk photo of Mastantuono celebrating in the Fiorentina kit because “Mastantuono plays for Real Madrid, not Fiorentina” — API-Sports has him at Fiorentina for 2026. Both reasons are assertions about the POST’S subject out of the model’s own stale or absent world knowledge, and neither NAMES the person or work in the picture. The rung’s one TRUE catch in 30 days does name it: “This is Christian Coulson … not Billy Barratt”, where the wire attached BOTH the newly-cast actor and a 2002-film reference still and pick_subject_image took the reference. That is the whole separator, and it is deterministic: Buddy and Mastantuono each carried ONE wire photo (nothing to choose), Barratt carried TWO. gated_source_photo now returns source_photo for a lone photo (exempt) and source_photo_pick for a chosen one (still gated) — the same split as deezer_artist_exact and youtube_chart. The exemption covers art that cleared gated_source_photo and only that: the music newsroom’s first-party FEED image is downloaded and stamped with no vision check, so it now carries its own feed_photo rung and stays gated (Codex review on #3247 — sharing the source_photo label would have handed an unchecked image the checked rung’s exemption). Coverage over 30 days of wire posts: 624 single-photo against 273 multi-photo, so the gate keeps authority over roughly a third of the rung. Watch reason=verified_source with detail.source=source_photo. Two measured dead ends from this incident, so nobody re-derives them. (1) REWORDING the judge prompt does not work: requiring a positive NAME in the NO definition, and telling the judge a title does not reveal what a work looks like, were ablated on 14 real cases (7 correct-art, 7 wrong-art, all three drops above included) — neither moved false drops outside noise, both cost recall (the Julie Chen catch fell 10/10 → 4/10), and two runs disagreed on sign. (2) The CONFIRM-ON-REJECT second vote is far weaker than it reads: the judge is NON-STATIONARY, returning 15/20, then 10/10, 9/10, then 0/10 NO on byte-identical input over ~20 minutes, so both votes land in the same regime a second apart rather than sampling independently. _JUDGE_TEMPERATURE is also inert on this call — claude-sonnet-5 is not in _SAMPLING_MODELS, so the 0.0 is dropped before the request and every “at temperature 0” claim in this ledger predates that. A drop rate that looks stable in one hour may not be.

Reading a flag’s NOTE is not verification — open the image. A fabricated note is as fluent and specific as a real catch; that is exactly how this class survived 14 days of log-mode review (it flagged 3 times and was read as correct)

A mismatch here can mean the TAKE is wrong, not the image (#youtube-take-fabrication). On KXYTDAILYTOPVIDEOG-26AUG23 the note read “Shakira … not Stray Kids”: the art was the CORRECT “Dai Dai” (Shakira & Burna Boy) thumbnail and the TAKE had invented “After You by Stray Kids”, so the gate dropped the right art and the post shipped as a bare number. A YouTube-video market’s take now has a DETERMINISTIC upstream gate (market_drop decline reason=leader_fabrication, on market_drop_scored phase="declined"), so a media_coherence mismatch on that family should be rare — a rising rate there means the upstream gate is not holding. Query the two together to tell a wrong IMAGE from a wrong TAKE.
                                   
curate_routing cogs/curate.py (#curate ops) guild_id, candidates (# fresh posts routed this tick), routed (# assigned a best-home room), dropped (# routed NOWHERE — fits no room / route classifier failed) — the best-home ROUTE funnel, the DROP signal: a high dropped/candidates rate = the feed is sending ~everything away, so a curate feed dropping everything is watchable + flaggable (ops-monitor renders a Curate-feed health line + flags curate_dark when it ships ~nothing over enough attempts, dominant-reason-guided like curator_dark; source=curate_feed skips are kept OUT of the curator’s funnel so the two don’t cross-contaminate) instead of silent. A route-classifier API failure also emits error (source=curate_route) so it hits error-triage                                    
crosspost utils/x_crosspost.py guild_id, surface (music|market_drop|discourse|market_alert|bet_board|bet_alert|bet_value|cinema_desk — which drop surface sourced it; cinema_desk mirrors the movie/TV big-number card (take + card image + Kalshi/TMDB link), the cinema sibling of the market_drop crosspost; the curator is deliberately excluded (owner steer) so its reposts of OTHER accounts don’t flood the timeline and drown out Toots’ own tweets — crossposting is reserved for her original-voice surfaces), mode (tweet|quote|repost|media|video|video_link — an X-origin link in the drop (a discourse trending-X reaction, auto-detected from the line via x_fetch.x_tweet_id) QUOTE-tweets when she added a take, else NATIVE-REPOSTS the bare link; a chart rides as media; video (#video-upload) is a WIRE DESK (pop_desk/sports_desk/cinema_news) re-sharing the source post’s OWN clip on her standalone take by fetching the source X post’s mp4 and uploading it as NATIVE media (utils.video_fetch.fetch_clip_bytesXPoster.upload_video), so the video plays inline on her own broadcast — NOT a quote/link to the wire account (the #video-detection-failure fix). A found photo passed alongside is DROPPED once the clip uploads (owner steer “we didn’t need the image beside it, it’s of the same video” — the desks’ still is usually a frame of the same clip, so the earlier video+photo mixed-media tweet doubled the same moment; the photo is FALLBACK media in the ladder below, never a companion, and the _media_gate vision check now runs only when the photo actually ships). The clip fetch prefers the highest-quality rendition ≤1080p (X re-encodes 4K, so it’s wasted bytes; x_fetch._largest_mp4), capped by X_CLIP_MAX_BYTES (64 MB). video_link is the FALLBACK when the clip can’t be downloaded (an undownloadable/HLS-only clip, a CDN block from the datacenter IP, an oversize file, or a clip over X’s native-video ceiling whose TRIM then failedutils.video_fetch.exceeds_x_video_limit / X_MAX_VIDEO_SECONDS, default 120s: the @tootsiesbar account tier refuses a longer video with 403 "This user is not allowed to post a video longer than 2 minutes.", and it refuses at POST time, AFTER the multi-MB download + chunked upload, then eats a second identical 403 on the video-only retry — and too late for this very fallback, which only fires when the UPLOAD failed, so the take was dropped from the timeline entirely (6 of 61 video crossposts over 14 days). fxtwitter reports the duration on the resolve, so fetch_clip_bytes knows the length BEFORE the download; the TikTok path reads the same ceiling off aweme_detail.video.duration. An UNKNOWN duration is never blocked — fail-open, better to spend the call than silently downgrade a clip that would have posted. Emits x_clip_too_long. The over-ceiling clip is now CUT, not dropped (#video-trim): the ceiling used to send the take straight to the deep-link, and the deep-link embeds only sometimes, so the reported Drake post shipped as a bare truncated link with nothing to play (a 153s source, mode=video_link, 11 ceiling hits over 30 days). utils.video_trim.trim_clip downloads the clip and cuts the first x_trim_target_secs() seconds — the ceiling less X_TRIM_MARGIN_SECONDS (default 2), because a cut lands on a frame boundary and a 118s cut measures 118.07s. The cut is a STREAM COPY (-c copy, no re-encode): both feeds already serve h264/yuv420p/aac in mp4, so the output is X-ready by construction and the cut costs under a second where a libx264 pass would cost minutes of container CPU. Verified live end-to-end on that source post → a 118.07s 1080x1080 h264/yuv420p/aac mp4 that decodes clean. Emits clip_trim; a FAILED cut still lands here on video_link, so this fallback’s old behavior is intact): rather than settle for a still, it appends the source .../status/<id>/video/1 deep-link IN-TEXT (_video_deep_link, host-normalized to x.com), which X SOMETIMES embeds as the playable video (owner steer “it works sometimes”) — a shot at the real video beats the photo, so it’s posted WITHOUT the photo so the link can drive the embed (and it’s the link-priced post, has_link=True). The whole thing is fully fail-open along a LADDER: native video re-upload → video_link deep-link → the found photo (mode=media) → a link-free text take — never a dead in-text link. It takes the media slot over the found photo (video wins); the take is kept clean of any x.com link so the quote auto-detect stays off and it rides the plain-tweet path. She LIKES the quoted/reposted post (a fail-open courtesy that rides the same quote, never touching the quote that already shipped). A borrow-the-audience REPLY under the quoted post was tried (#1732) and REMOVED: @tootsiesbar’s X API tier 403s any reply to a post where she isn’t mentioned/author (“You can only reply to or quote posts where you are mentioned or are the author”), so a reply under someone else’s tweet can’t land — and unlike the quote (which survives that same limit via the in-text-link card fallback) a reply has no equivalent workaround. So there’s no reply crosspost path; the quote + like are the amplification. delivered (posted = tweeted to @tootsiesbar’s X timeline | staged = auditioned in #bot-logs, the x_crosspost experiment in STAGING | skip), reason (None on posted/staged; unprovisioned = production but the 4 X_* OAuth env vars aren’t set; post_rejected = X rejected the tweet; surface_off = the per-guild picker disabled this surface; over_length = the take overshoots X’s ceiling; dedup = another guild already tweeted this content; error = the call raised — a no_art skip floor was tried here and REMOVED per owner steer “if the post is good don’t skip”: a good take with genuinely no art still SHIPS as a text tweet; art coverage is the desks’ multi-layer resolution (alternate entities + the composed-take retry), not a boundary filter) — a drop was MIRRORED to the @tootsiesbar X timeline (the outbound half of the X integration). Fires at each surface’s real production delivery (a STAGED drop never crossposts) — normally AFTER the room send, or INSTEAD of it on the X-ONLY sentinel lane (#2137, the Discord/X separation: a surface whose /menu CHANNEL picker is EMPTY stops posting to Discord but keeps composing for X when the guild would actually tweet it — utils.x_crosspost.x_only_active + the X_ONLY_CHANNEL_ID sentinel channel; the surface’s own *_posted event then reads delivered=x_only + channel_count=0. With X off in any way an unconfigured surface stays fully off, the old behavior) — ONLY for a guild that OPTED IN (the per-guild x_crosspost experiment != off), and only if not excluded by the optional X_CROSSPOST_GUILD_ID single-guild lock. The cog-level disposition; the API call itself emits tweet_posted (client latency/outcome), mirroring the image_reply / image_generated split. Fail-open: a crosspost is a courtesy mirror of a post that already shipped, so any miss never touches the Discord delivery. has_link reads False on the market surfaces since the exchange-link strip (strip_market_links, owner steer “kalshi links on discord, not twitter” + “for both predictions, poly too”): the tweet carries the take + the chart image, the Discord card keeps its market button. So a drop in the link-priced share of crosspost is expected from that change, not a compose regression — the strip is host-scoped (_MARKET_LINK_HOSTS: kalshi.com, polymarket.com), so a market post that still carries another link (a cinema_desk TMDB link, a sportsbook link) keeps has_link=True                                    
tweet_posted utils/x_poster.py ok, duration_ms, chars (tweet length) / bytes (media upload), mode (tweet|quote|reply|media|repost|like|upload|upload_video), delivered (posted|skip), tweet_id (on success, folded into detail); on a miss: error (unprovisioned|empty_text|post_rejected|no_user_id|no_media_id|too_large|init_failed|processing_failed|HTTP <status>|breaker/exception class), reject_detail (X’s OWN one-line reason for a refused write, parsed off the error body by the pure _reject_detail; folded into detail). That sentence IS the diagnosis: three unrelated bugs hide behind one HTTP 403 — a video over the account’s 2-minute ceiling, duplicate content, and the third-party quote/reply limit — and the body was previously only LOGGED, so telling them apart meant grepping Railway rather than reading the telemetry — one X API v2 write call (POST /2/tweets, .../retweets, .../likes, or v1.1 media/upload; OAuth 1.0a user-context as @tootsiesbar). The outbound half of the X integration (the read side is curator_fetch/x_fetch), provisioning-gated on the 4 X_* env vars; posts/quote-tweets/in-thread replies (reply, #1497 — the “correct” reply under a game winner) + likes the post she amplifies (like — the courtesy favorite that rides alongside a crosspost quote/repost, maybe_crosspost, fail-open) + uploads chart media (upload) or a chunked async video (upload_video, #1497 — INIT/APPEND/FINALIZE/STATUS, since X rejects bare audio so an iTunes preview clip ships as tweet_video); guarded by a circuit breaker (integration=x_poster) + rate limiter + retry, fail-open. Its integration health is rolled up as x_poster in the ops-monitor (a silent all-fail — bad creds / X down / write-cap wall — flags integration_unhealthy, benign unprovisioned/empty_text excluded), the p99 gated by the tweet_posted latency ceiling                                    
x_filter_rule utils/x_filter_rules.py (#1497) op (add|update|delete|get), ok, duration_ms; add carries rule_id on success, update/delete carry rule_id + (update) is_effect, get carries count; on a miss: error (unprovisioned|no_rule_id|HTTP <status>|exception class) — one twitterapi.io tweet-filter RULE call, the “no polling” answer-detection for the X guess-the-song game. A game round registers a to:tootsiesbar filter rule (add_rule → inactive → update_rule is_effect=1 to activate), twitterapi.io POSTs matching replies to /x/webhook, and the round delete_rules it at close. Reads (get) use retry_http; rule WRITES stay single-attempt (a retried add could duplicate the rule). Same prepaid TWITTERIO_KEY credits as the read XProvider; fail-open. The precise conversation-id match is done in-handler, so the rule grammar stays coarse/robust                                    
x_webhook utils/healthcheck.py /x/webhook (#1497) ok, rule_tag, tweets (# in the payload), dispatched (routed BY rule_tag: xgame_* → the XGame cog, xmentions → the XMentions cog, both via handle_webhook_tweets); on a miss: reason (unauthorized — X-API-Key mismatch | bad_json | dispatch_error) — a twitterapi.io filter-rule webhook POST was received (a game reply OR a mention). Verified on the echoed X-API-Key shared secret (TWITTERIO_KEY); always 200s on a valid authorized POST so the sender never greys out the endpoint (parsing/dispatch fully fail-open). The route exists independently of any cog, so a missing cog is a clean no-op                                    
x_mentions cogs/x_mentions.py (#x-mentions) kind (rule|scan|boards|prediction|reply), ok, guild_id. rule: reason (created|removed|create_failed) as the standing @tootsiesbar filter rule is reconciled to the gate (a 15-min loop; OFF removes it, so no read spend while dark – x_mentions graduated 2026-09-01 (#2796) to MASTER-ONLY, so OFF now means a non-master guild). scan: reason (hit|no_artist|rate_capped|scan_capped), query = the matched artist; a hit resolved off HER OWN parent post (the mention itself named no act but ASKED a question – “what about the debut?” under a Miley post; a reaction like “Temazo!!” stays no_artist) folds via=parent (owner steer 2026-09-08: the parent is fetched keyless via FixTweet only when the mention replies to her post, x_mentions.reply_parent_id; the gate is x_mentions.asks_question – a “?”, a leading question word, or a request-shaped phrase like “update on the debut”, never a bare noun; the parent is fetched only once that gate passes; the second Haiku read claims a second scan allowance, so scan_capped bounds model calls, not tweets; and a parent-derived act feeds ONLY the reply lane, never boards/prediction – PR #3081 review P1). reply (2026-09-08): the HAND-POSTED answer under the mention – reason (shipped|no_facts = no parent, no chart row, no market, so nothing to answer from|declined = the compose returned EMPTY|send_failed), delivered (bot_logs|skip), query = the act. The lane gathers the grounded facts (x_mentions.format_mention_facts: her parent post, then EVERY live Kalshi ladder for the release each named by its metric – readings_for_subject(max_lead_days=None), the window lifted, the metric read off the question by luminate.metric_from_text and applied only when a live ladder carries it, one release’s ladders only with siblings joined on the event-ticker release key; the parent rides only when it is about the resolved act – then the act’s strongest chart blobs; market lines come first and are never cut, a line over budget is dropped whole, and the chart rows take only the room left), composes with claude.draft_mention_reply (purpose x_mention_reply), and queues TWO messages in #bot-logs (the x_reply_draft header + the mention’s fixup link, then the bare draft) for the owner to post by hand; the draft text is cached under the x_reply_text ns so the nightly x_reply_edit join covers it; bot_logs.post now returns whether it SENT, a dropped post is send_failed, never shipped, and the x_reply_text entry is written only after delivery. It NEVER posts to X (the standalone boards/prediction posts stay automated; the reply is manual for now). Watch: where event == 'x_mentions' and kind == 'reply' | summarize count() by reason – a run of no_facts on hits means the chart/market reads are dark for the acts people ask about. boards/prediction: reason (shipped, or a skip cause: weekly_dupe (boards; the once-a-week-per-(artist,chart) gate)|no_boards (boards; the act has no 2+-title presence on any chart)|row_tally (boards; the take named a top-10 count the board’s own block does not state – the grounded-tally backstop the desk’s board path runs, wired here in #2662 because the standing board made this lane COUNT-LED and the 0.6 judge scores a wrong-but-grounded-looking tally 0.95)|cooldown (prediction)|no_market|declined|below_floor|topic_dupe|unprovisioned|fetch_failed), delivered (staged = #bot-logs audition | room = posted to the music channels | x_only = crossposted, no room | skip), score — an @tootsiesbar MENTION naming an artist gets that act’s PER-CHART cards (BOARDS lane: a chart_boards.watch_artist_board where the act holds 2+ titles on a chart, else a single-entry number card for exactly one — across the Billboard Hot 100 / 200 / Global 200 via utils.billboard + the live Spotify/Apple charts via kworb; UP TO 3 per mention, strongest chart first, deduped WEEKLY per (artist, chart)) + their live Kalshi first-week forecast (PREDICTION lane, watched acts only, music_markets.reading_for_subjectprojection_figure). Detection: a deterministic watchlist scan (x_mentions.scan_watched) + a Haiku extractor for aliases + the song title, recognition BROADENED to any literally-named charting act (the boards lane confirms by chart presence). SINGLETON (one @tootsiesbar) → master guild only; gated by the x_mentions experiment (OFF default, ships dark) + master switch; per-tweet dedup + the boards weekly-per-(artist,chart) gate + the prediction per-(artist,lane) cooldown + cross-surface topic dedup. Crossposts on the x_mentions picker (opt-in). Fail-open                                    
x_game_round cogs/x_game.py (#1497) action (open|close), ok, guild_id; on open ok: delivered (room = posted to the @tootsiesbar X timeline | staged = auditioned in #bot-logs, no post to X), tweet_id + has_rule (production); on open miss: reason (no_song|no_clip|unprovisioned|upload_failed|post_failed); on close: delivered + won (did anyone guess it) — an X guess-the-song round opened or closed. Toots posts an urban-90s+ song CLIP to @tootsiesbar, a to:tootsiesbar filter rule catches the replies (webhook → handle_webhook_tweets), the first correct guess ends the round with a single board reply (a music-drop-toned reveal + a light nod to the winner + the running leaderboard) under the clip + a music-drop quote of the clip with the answer’s Apple Music link, else a NO-WINNER round skips the board/leaderboard entirely and just quote-reveals the answer (no “nobody got it” complaint). ACCOUNT-global (one round open at a time across guilds); gated by the x_game experiment (OFF default — posts to a public external timeline like x_crosspost) + master switch + mood. Song pool reuses the /guess game’s generate_song_pool (urban bundle + 90s-onward era), each pick iTunes-verified to a real preview + art. Fail-open                                    
x_game_answer cogs/x_game.py (#1497) ok, won=True, username (the X handle, folded into detail), count (their new win total) — the FIRST correct guess in an X guess-the-song round. Fires once per round (guarded by the atomic x_game_claim_winner first-winner claim, so a race between two correct replies still yields one winner + one point); the win ENDS the round with the single board reply (crown + reveal + leaderboard) under the clip, and a leaderboard point. A wrong guess emits nothing                                    
curator_evaluated cogs/curator.py (#curator ops) + cogs/artist_curator.py (stamped surface=artist_curator; there no_seeds = the watchlist was down or no artist on it has a mapped handle) guild_id, channel_id, reason (no_seeds = channel has no source accounts, post links | no_history = channel empty/unreadable | no_posts = the source fetch returned nothing | no_candidates = every fetched post is already seen or non-postable (reply/repost/no-media) | none_fit = the fit-judge LOOKED and declined every candidate | pick_error = the fit-judge ERRORED (it choked, or the API rejected an attached image) | no_channel), and for source=curate_feed also detail.dropped (how many posts the judge passed over while the drain WALKED the pool – a decline advances the walk instead of ending the slot, and the slot still reports exactly ONE skip event so the walk cannot inflate the curate_dark denominator), mode (image|text, when known) — the curator ran a slot but posted NOTHING (the SKIP half of the ship funnel). With curator_posted it makes a surface going quietly DARK visible: the ops-monitor renders a Curator health line (post rate + skip-reason funnel + source/mode split) every run and flags curator_dark (high when none_fit-dominated = users see silence; medium when no_seeds/no_history = channels just need links). pick_error was split OUT of none_fit (2026-08-30). Both used to emit none_fit, so the funnel read “the judge rejects everything” while the judge was in fact FAILING on an image it could not load – every slot, for days, in #backpage. The curate feed already separated the two, and that separation is what made the outage findable. A judge error and a judge decline need different fixes, so they need different reasons. The curator has no 0-1 score event (the judge is pick-or-skip), so this is its surface-dark telemetry                                    
comment_read utils/comments.py (#1272 follow-up) url_host (≤40 chars), count (# comments after like-rank), ok; on a miss: error (unprovisioned|unsupported_url|fetch_failed|bad_shape) — the read_comments /ask tool ran: the audience REACTION on a specific TikTok/YouTube/Instagram/Reddit/X (Twitter) post (top comments ranked by likes), returned as TEXT for the model to synthesize (“read the room”, no links; the crowd’s-eye complement to read_media’s transcript). TikTok/YouTube/Instagram/Reddit ride ScrapeCreators (bot.scrapecreators); X REPLIES ride twitterapi.io (bot.xprovider_primaryXProvider.fetch_tweet_replies over /twitter/tweet/replies — ScrapeCreators has no tweet-replies endpoint, so the X reply thread is the “comments” there). Per-platform mappers normalize to one Comment(text, likes) shape; NSFW-agnostic (comment text only). Offered when EITHER client is provisioned; fail-open. Pure mappers + formatter in utils/comments.py (unit-tested); handler cogs/ask.py:_read_comments                                    
social_lookup utils/social_profile.py (#1272 follow-up) platform (tiktok|instagram|youtube|twitter, normalized from x/ig/yt/tt aliases), handle (≤40 chars), ok; on a miss: error (unprovisioned|bad_platform|no_handle|fetch_failed|not_found) — the social_profile /ask tool ran: ONE account’s PROFILE (follower count, verified badge, post/video count, bio, account age) on TikTok/Instagram/YouTube/X-Twitter via ScrapeCreators (/v1/tiktok/profile, /v1/instagram/profile, /v1/youtube/channel, /v1/twitter/profile). The structure complement to social_search (which FINDS posts) — the model relays the real stats, never invents a follower count. Per-platform pure mappers normalize to one SocialProfile shape; provisioning-gated on bot.scrapecreators; fail-open. Pure mappers + formatter in utils/social_profile.py (unit-tested); handler cogs/ask.py:_social_profile. Distinct from search_socials (find posts) and discord_lookup (this server’s members)                                    
reddit_search utils/reddit.py (#1272 follow-up) query (≤80 chars; a subreddit scope folds in as r/<sub>: <query>), count (# posts after score-rank + NSFW filter), ok; on a miss: error (unprovisioned|empty_query|fetch_failed|bad_shape) — the search_reddit /ask tool ran: Reddit DISCUSSION/opinion on a topic via ScrapeCreators (/v1/reddit/search, or /v1/reddit/subreddit/search when scoped), returned as TEXT for the model to SYNTHESIZENO links surfaced (owner steer: Reddit is an INPUT source we work into her own voice, never a URL she pastes — “not reddit-nerdy”; RedditPost has no permalink field, AND url_guardrail.strip_urls removes any link from the post’s own selftext – #2232, where the field-level guarantee was walked past by a url inside the author’s body text; the same one-line fix covers comment_read and ktt2_fetch). Each post carries title + subreddit + the discussion text + a traction signal (score + comments); NSFW (over_18) posts are dropped, results score-ranked. Provisioning-gated on bot.scrapecreators; fail-open. Pure mapper + formatter in utils/reddit.py (unit-tested, incl. a “never links” assertion); handler cogs/ask.py:_search_reddit. For opinion/discussion, distinct from web_search (breaking news)                                    
music_desk_scored phase=ktt2_chatter cogs/music_desk.py (#2230) guild_id, score, reason, shipped, matchup (<artist>: <n> replies), post_preview — the FORUM-CHATTER lane’s self-gate. Two things make this row unlike every other desk phase. (1) The score comes from discourse_score, not music_desk_score: measured on the live board, the desk judge scored real chatter takes 0.00–0.20 and said why — “forum chatter, not a music number” — which is correct, because this lane never reports a chart position or a consumption figure. The discourse judge grades whether a post earns a reply, which is what this lane is for, and scored the same story 0.62–0.78. (2) The lane reports DISCOURSE about a named real person. The take NAMES what the room is arguing about — the thread title + opening post, handed over as the argument’s SUBJECT (utils/ktt2_chatter.context_block + FRAMING) — and gives one honest read, but never states the allegation as a thing that happened (the constitution’s THE ROOM IS NOT A SOURCE + STAY IN YOUR LANE). It composes through claude_client.compose_forum_chatter, ISOLATED from the numbers-desk compose_market_drop (#2230 quality fix): that composer’s task is “lead with the number”, and the reply count was the only number, so the count became the headline (“569 replies deep on Drake at 24 an hour”) — a dead post that said nothing about the actual chatter. The isolated composer leads with the argument and treats the count as texture. A/B on the live Drake thread (Sonnet + Opus): the old path led with the count every sample; the new path led with the subject every sample and stayed hedged (“allegedly”), never ratifying the claim. Expect this phase to be QUIET — measured yield is ~1–2 watched-artist threads a day clearing the heat floors, heavily Drake-weighted, and 3 of 5 composes shipped in the dry run. A phase that goes silent for days is NORMAL; a phase that suddenly posts several times a day means a floor stopped biting. Gated by its own ktt2_chatter experiment (STAGING by default) and its own opt-in X toggle                                    
music_desk_scored phase=watch_board cogs/music_desk.py (#2321, #2662) guild_id, score, reason, shipped, matchup (<board.kind>: <act>), post_preview — the WATCHED-ARTIST BOARD’s self-gate. One act’s whole presence on ONE kworb chart, replacing the per-move cards the watch sweep used to ship in a burst. matchup is what tells the two board kinds apart, because both ride this one phase: watch_artist: <act> is the board several NOTABLE MOVES in one sweep make, and watch_standing: <act> is the board the act’s TOP-10 PRESENCE makes on its own (chart_boards.MIN_TOP10_STANDING, TWO or more of the chart’s top 10 since the owner steer of 2026-09-05; it was three, and the kind was called watch_takeover). A row rejected BEFORE the judge (the empty-response and number-backstop guards) carries the bare act name instead, since those guards see only the subject. The watch_boards panel (Content Desks, #2662) splits this phase by that prefix and buckets the un-prefixed rows as pre_gate_reject. It exists because the STANDING board’s failure mode is SILENCE, and silence is exactly how its absence hid: on 2026-08-28 Rod Wave held 7 of the Apple Music US songs top 10 all day, only one of his titles moved per sweep, so no board ever formed and the desk posted single-song climb cards eight hours apart — nothing counted the board that did not happen. Read it this way, and read it HARDER after 2026-09-05: a watch_standing count of zero across a week while watch_artist keeps firing means the presence path stopped forming boards. That column should now be the BUSIER of the two, for two reasons at once — the floor came down to two of the top ten, and the STANDING PASS seeds a board off the chart page with no mover at all, where the board used to need one. So the old expectation (quiet, bursty, one per slot) is the REGRESSION signal now: a return to the pre-2026-09-05 rate means the standing pass stopped seeding. Still no MONITOR, and the reason is unchanged — a standing is event-driven and genuinely bursty (an act with an album out holds the page for days, then nobody does for weeks), so no rate separates a quiet chart from a broken lane; the panel builds the new baseline first, and a monitor is worth revisiting once a week of the new rate is on it. The board is still capped at one per slot and still deduped on the top-10 COUNT it states, so a standing that holds steady posts once and not again. The sibling CARD lane (phase=watch_chart) gained two story kinds in the same steertop_climb and top_fall, a move of any size inside a chart’s top ten, where every kind before them needed an event. They ride music_desk_scored phase=watch_chart with no new event and no new field; matchup carries the chart and the title as before, and the kind is visible in the card’s eyebrow rather than the event. Expect this phase’s VOLUME to rise and then settle: the dedup key carries the position, so a title posts once per position it reaches inside the 9-day window, and _MAX_TOP10_MOVES_PER_SLOT (1) holds the share of each slot. Read a watch_chart rate that does NOT rise as the signal that the new kinds are not deriving – most likely the charts’ pos_change column stopped parsing, which would also flatten the jump and top<N> kinds beside them                                    
ktt2_fetch utils/ktt2.py (#ktt2) source (ktt2), query (the section slug — music by default), count (threads parsed off the page), ok, duration_ms, cache_hit; on a miss: reason (unavailable|bad_shape) + error (HTTP <status>|breaker_open|no_next_data|no_threads) + http_status — one KTT2 forum section read, the source behind BOTH the search_ktt2 (discourse) and ktt2_breaking /ask tools. KTT2 has no API and no key: every section page server-renders its Apollo GraphQL cache into __NEXT_DATA__, so this parses JSON off a page rather than calling an endpoint (access reasoning + the three pre-ship checks are in the module docstring). Fail-open (a miss → an empty section), so this ok-rate is the ONLY signal either tool went quiet — nothing raises. error=no_next_data / no_threads is the SCHEMA TRIPWIRE and the row to watch: the page answered 200 but carried no data, which is what a KTT2 redesign or a bot-block looks like. It does NOT self-heal (the reader stays dark until utils/ktt2.py is updated) and /debug/integrations CANNOT see it — the ktt2 probe still reads green, because the page is still returning 200. That gap is why this event carries its own monitor (KTT2 reader broken, >3 shape failures in an hour) while the ok-rate deliberately does not: both tools are ON-DEMAND, so a quiet hour means nobody asked, and a low-volume ok-rate would be noise. Guarded by limiter (6/min) + breaker (integration=ktt2) + retry + a 10min durable cache, so a burst of tool calls in one answer collapses onto one fetch. Panels: KTT2 forum reads (health by section + error) and KTT2 schema tripwire on the Media dashboard; the two tools themselves ride the generic tool_vol/tool_ok panels on the Ask dashboard                                    
social_search utils/social_search.py (#1272) query (≤80 chars; the search terms, #<tag> for hashtag, or trending:<REGION> for the trending tool), platform (all|tiktok|youtube|instagram), count (# merged hits returned), ok; on a miss: error (unprovisioned|empty_query|bad_platform) — the search_socials / discover_trending / search_hashtag /ask tool ran: unified cross-platform social DISCOVERY via ScrapeCreators, MERGED + engagement-ranked, so the model FINDS fresh social content uniformly (the “search all social platforms uniformly” ask) instead of a per-platform tool. search_socials is a keyword SEARCH (TikTok /v1/tiktok/search/top + YouTube /v1/youtube/search + Instagram reels /v2/instagram/reels/search); search_hashtag is the #-tag sibling (TikTok /v1/tiktok/search/hashtag + YouTube /v1/youtube/search/hashtag); discover_trending is the no-keyword TRENDING feed (TikTok /v1/tiktok/get-trending-feed region-scoped + YouTube /v1/youtube/shorts/trending). Each hit carries the platform, caption/title, author (with a ✓ verified badge), engagement (likes/plays/views), duration, and the LINK (Discord unfurls it). Provisioning-gated on SCRAPECREATORS_API_KEY (bot.scrapecreators); fail-open per platform (a source miss contributes nothing). Pure mappers + orchestrators in utils/social_search.py (unit-tested); the handlers are cogs/ask.py:_search_socials/_discover_trending/_search_hashtag. Full field-coverage ledger in docs/INTEGRATIONS.md. Distinct from read_media (read ONE known URL) and web_search (a general web fact)                                    
music_dedup cogs/music.py guild_id, channel_id, channel_name, decision (similarity_gate/song_gate), signal (same_link/text_similarity/shared_run/content_overlap/same_song), post_preview — a composed drop was too similar to a recent one (the shared take-text/link dedup, similarity_gate) OR it re-picked a SONG already dropped in the last ~30 days (the durable song-identity reuse block, song_gate/same_song — the “Folded keeps re-dropping” fix; scheduled drops only, one retry then skip the slot)                                    
music_link_missing cogs/music.py guild_id, channel_id, channel_name, must_post, attempt, post_preview                                    
music_house_gate cogs/music.py guild_id, channel_id, channel_name, ok, reason (off_genre/new_release/watched/certified/unknown_artist/unresolved/unparsed_track), attempt, must_post, artist, genre, watched, certified — a composed drop was graded against the HOUSE POLICY (utils/music_policy.py, owner steer 2026-09-14 after CMAT’s “EURO-COUNTRY” reached @tootsiesbar): the bar plays rap, R&B and pop, and a record that is not a new release must be by an act on the kworb watch list or one RIAA has certified. One row per graded attempt, and the ops monitor counts every attempt, retries included — counting only the first would make gate_dark report a dark channel in the window where every first attempt is refused and every retry passes, so read the rate as attempt quality rather than slot quality. ok=false refuses the drop, which triggers the same single retry the link and song gates use and then skips the slot. A refusal emits nothing else: it deliberately does NOT fall into music_dedup, which would file every policy rejection as a same_song duplicate that never happened. Read reason as the tuning signal, not just the health signal. off_genre carries the iTunes tag in genre, and an IN-HOUSE tag appearing there is the list being too narrow (iTunes files old records under catch-alls — measured 2026-09-14, Prince’s “Kiss” is tagged Soundtrack), so the fix is to add the tag to music_policy._IN_HOUSE_TAGS, not to loosen the rule. A climbing unknown_artist share says the compose prompt is fishing outside the room’s artists; a climbing unresolved share says the catalogue read is degrading, not that the picks got worse. watched=true on an unknown_artist row is impossible by construction and would mean the verdict and the fetch disagree.                                    
commentary_posted cogs/commentator.py guild_id, channel_count (rooms sent to; 0 on a staged audition), trigger (pregame|goal|halftime|period|final|interval|clutch_interval), sport, game_key, delivered (room = posted to the sports channel(s), live_scores experiment in production; staged = the line auditioned in #bot-logs, experiment in staging, room kept quiet), post_preview (the shipped line, POST_PREVIEW_CHARS; graded by the live-log eval pass on delivered=room), depth_legs (a comma-joined list of which optional depth blocks landed on this post — odds #476, props #390, team_stats/pregame/standings #432, payout #432 Epic E, market_edge #622, bets #623 — folding the former 8 has_* booleans into ONE field, #1296; leg names are non-overlapping so depth_legs contains '<leg>' reads each leg’s landing rate), depth_shed (#779: API-Sports near its daily-request cap so the OPTIONAL per-game depth — leaders/team-stats/pregame/standings, all API-Sports — was SKIPPED to preserve budget for the load-bearing live scores + bookie settlement, reverting to score+events only; read from bot.usage_status at the 0.85 LOW_PCT bar, fail-open). The live sports commentator posted a line for a live game; gated by the live_scores experiment + mood + master kill switch, paced by the pure utils.sportsdata.cadence decision. Ops-monitor watches the SHIPPED depth mix (a “Live sports commentary depth” line: room-post count + trigger spread + each depth_legs leg’s landing rate) so a silently-broken depth leg — the standings fetch dying, final/pregame never firing — is visible every run, backed by the api_sports market_fetch integration-health rate                                    
commentary_scored cogs/commentator.py guild_id, game_key, trigger, sport, score (0-1), reason, shipped (>= the 0.6 ship floor), post_preview — the commentator’s self-gate (commentate_score, Haiku) scored a composed line before sending. Parity with discourse_scored/music_scored: a sub-floor line is dropped (no must_post here), and the ops monitor watches it for low_quality. Fail-open: a scorer exception ships the line anyway                                    
commentary_deferred cogs/commentator.py guild_id, game_key, trigger, sport, reason (stagger) — cross-game spacing (#502) held a routine read back so concurrent live games don’t post a wall to one channel. The guild already hit its per-tick post budget (_MAX_ROUTINE_POSTS_PER_TICK); the deferred game’s PostState is left untouched so the cadence re-fires it next tick. MILESTONE triggers (goal/final) are never deferred (a reaction loses value if held) but count toward the budget so routine reads space around them                                    
highlight_fetch utils/highlightly.py sport, query (away @ home + date), ok, result_count (# VERIFIED clips), duration_ms; on a miss: error (no_key|unmapped_sport|HTTP <status>|<exc>) — a Highlightly /highlights API call, the commentator’s post-game video-clip source (#432, covers soccer + NBA). Key-gated on HIGHLIGHTLY_API_KEY, fail-open to []                                    
highlight_posted cogs/commentator.py guild_id, game_key, sport, delivered (room = posted to the sports channel(s); staged = auditioned in #bot-logs, live_scores experiment in staging), source (the clip’s aggregation source, e.g. youtube/twitter), ok, channel_count (room) — the commentator posted a post-game highlight clip for a completed game. Once per game (durable dedup via sports_post_state.highlight_posted_ts); clips lag the whistle 0-48h so a completed game is re-checked on a pace (_HIGHLIGHT_RECHECK_SECS, 60min) until a VERIFIED clip lands. Gated by the live_scores experiment + mood + kill switch (#432)                                    
abuse_warned utils/abuse_tracker.py guild_id, user_id, violations                                    
abuse_silenced utils/abuse_tracker.py guild_id, user_id, violations                                    
bot_power cogs/settings.py guild_id, user_id, enabled (the master kill switch / “complete off button” was flipped on /menu. enabled=False takes Toots fully dark in the guild: no mentions, voice, chime-ins, scheduled posts, memory writes, or slash commands; only /menu stays reachable so a mod can flip her back on. Distinct from mood=off, which only mutes the proactive surfaces. Per-guild servers.bot_enabled flag, default on, checked at every entry point via db.is_bot_enabled: the listeners/schedulers gate inline and the slash surface is gated globally by bot._GatedTree.interaction_check)                                    
scheduler_firehose cogs/calendar_view.py guild_id, user_id, enabled (≥1 surface on firehose), count (# surfaces across all three cadences), cadence (which cadence the mod just edited — half_hour / hourly / two_hourly; folded into detail, so query it as parse_json(detail)['cadence']) — the PER-SURFACE, PER-CADENCE scheduler firehose set was changed on the /menu calendar (the 🔥 button → a surface-picker sub-view, _FirehoseView, one multi-select per cadence). A surface in one of the guild’s three firehose SETS fires at EVERY working-hours slot OF THAT CADENCE (its per-slot calendar assignment ignored); the rest of the grid still governs the other surfaces — resolved in the single chokepoint schedule_calendar.calendar_hours (surface in one of the three sets → firehose_slot_times(working_hours, cadence), else the stored calendar). The three sets are disjoint on write, so a surface has exactly one cadence; calendar_hours reads them lightest-first. cadence is what makes a slot_replan correlate: that finding says a surface’s plan SHRANK mid-day, and this row says which cadence a mod moved it to and when. Per-surface, not all-or-nothing (the affordable shape per the #firehose cost pull: firehose the cheap token-only surfaces curator/discourse — no metered wall — and leave the metered market_drop/bet_board, which are SGO-capped and already resource-bound, on the normal calendar). Still thinned by each surface’s channel config + dedup + the 0.6 self-gate + rate limits; never overrides a muted (OFF) mood; grid stays fully editable. Off by default                                    
reaction_added utils/reactions.py + cogs/starboard.py source (chimein/starboard/…), guild_id, channel_id, message_id, emoji                                    
starboard_report cogs/starboard.py guild_id, user_id, period (1h|1d|1w), threshold, channels_swept, messages_scanned, eligible (# cleared-but-not-promoted), wall_known — the /star sweep listed messages that cleared a board but aren’t on the wall, as jump links for a human to react to (MEE6 ignores the bot’s own reaction, so only a person reacting re-triggers MEE6; an earlier bot re-react mode was removed for that reason). Runs in the background and posts the list when done. Filters out already-promoted messages via the jump-link ids on the wall’s MEE6 posts                                    
walloffame_leaderboard cogs/starboard.py guild_id, user_id, entries (total bangers across members), contributors (# members with a banger; 0 = no seed yet) — the bangers board ran (in the /leaderboard stats hub): ranks all-time BANGERS per member (posts that CLEARED the wall bar, 10+ 😂/😭) read STRAIGHT FROM the member_engagement seed (no scan). The seed’s would-be-wall count — a SUPERSET of MEE6’s actual wall promotions (the seed doesn’t capture those; the card’s separate “wall bangers” flex still reads the wall). All-time, uncapped; names resolve via the guild / identity map                                    
reaction_leaderboard cogs/starboard.py guild_id, user_id, contributors (# members with reactions; 0 = no seed yet) — the reactions board ran (in the /leaderboard stats hub): ranks all-time reactions pulled per member, read STRAIGHT FROM the member_engagement seed (no scan; the windowed 6mo/1w/1d live scans were dropped when it moved to the seed). All-time, uncapped; embed-fixer reposts folded onto the real user, MEE6/other bots skipped; names resolve via the guild / identity map                                    
funniest_leaderboard cogs/starboard.py guild_id, user_id, contributors (# members with laughs; 0 = no seed yet) — the funniest board ran (in the /leaderboard stats hub): ranks all-time laughs pulled per member by laugh_reactions (😭/😂), read STRAIGHT FROM the member_engagement seed (no scan) — the comedy-VOLUME twin of the bangers board (wall-clearing posts) and the leaderboard mirror of the awards’ Funniest. A public board in the shared toggle (alongside bangers + reactions); all-time, uncapped; names resolve via the guild / identity map                                    
card_leaderboard cogs/starboard.py (built off cogs/playercard.py) guild_id, user_id, contributors (# active members ranked; 0 = no seed yet) — the card OVR board ran (in the /leaderboard stats hub): active members ranked by their /card OVR off the SAME member_engagement seed the card reads (so a member’s board OVR matches their card), rendered through the shared _board_pages. Rows show the OVR number. Gated on the playercard experiment (PRODUCTION=everyone, STAGING=mods-only preview, OFF=hidden — the same gate /card uses); it joins the toggle picker (bangers / reactions / card / besties) only in PRODUCTION, so the toggle never exposes the card/besties surfaces to a non-mod during the staging preview (a mod previewing in staging still gets the card board surfaced by run_stats)                                    
besties_leaderboard cogs/starboard.py (off utils/social_graph.py) guild_id, user_id, scope (everyone|current — the show: membership filter, defaults current), pairs (# pairs ranked; 0 = no seed yet / none cleared the floor) — the besties board ran (in the /leaderboard stats hub): the server’s closest member PAIRS ranked by Ochiai closeness (== cosine similarity) over the engagement seed’s reply+mention graph, activity-normalized so it surfaces genuine duos not the loudest hubs. Reads the same seed /card does (no scan); playercard-experiment-gated. A pair board (A ↔ B · % rows) IN the shared /leaderboard toggle picker (no separate command). scope is the UNIVERSAL show: filter (here-now / everyone) that applies to every leaderboard board, defaulting current: keeps only pairs where both are still in the guild; everyone includes departed members (resolved to real names via the identity map). Pure closeness core in utils.social_graph.closest_pairs (unit-tested)                                    
engagement_snapshot cogs/stats.py (#1017) guild_id, members (# rows snapshotted), trigger (scheduled = after the daily catch-up fold; bootstrap = the first snapshot on cog-load so windows aren’t empty until the next daily run; backfill = a /stats period: reconstructed snapshot stamped at a PAST day so the windowed board reads accurately immediately) — a SNAPSHOT of a guild’s engagement seed was frozen into engagement_snapshots so the windowed (weekly/monthly) /leaderboard stats boards can diff against it (window = current seed − snapshot from N days ago). The daily one is a ~1-line server-side copy of member_engagement.stats (NOT a re-scan), idempotent per UTC day; the backfill one is a reconstructed (current − scanned window) blob (reconstruct_snapshot) past-dated to today−N. Retention bounded by prune_engagement_snapshots. One daily snapshot serves BOTH weekly + monthly by subtraction — no extra scan jobs                                    
memory_write cogs/memory.py guild_id, tier (hourly|daily for the room’s attributed memory; self_hourly|self_daily for Toots’s own-takes pyramid), ok, chars|skipped (low_activity|empty|truncated), message_count, channel_count, rolled_up (# of hourlies synthesized into a daily), backfill                                    
memory_forget cogs/memory.py guild_id, user_id, notes_deleted, facts_deleted (consented user_facts rows wiped alongside the observational notes)                                    
user_fact cogs/ask.py guild_id, user_id, stored (True = a new consented fact saved; False = blank/duplicate/db_error), reason (None on store; duplicate|db_error on skip) — Toots stored (or skipped) a fact a regular told her to remember about THEMSELVES via the remember_about_you /ask tool. Consent-based, per-(guild, user), storage uncapped with the per-reply read-back bounded to the newest USER_FACTS_READBACK; the fact only personalizes her replies to that user (read back via the <about_them> block), never disclosed to others; /forget wipes it                                    
poll cogs/polls.py guild_id, channel_id, user_id (the asker; absent on anonymous reads), action (create|list|read|update|end), ok, reason (None on success; invalid|no_permission|not_found|forbidden|not_mod|already_closed|error); create also: options (count), duration_hours, multiple, emojis (# answers given an emoji); list also: count — Toots created or managed a native Discord poll on a regular’s behalf via the create_poll/list_polls/read_poll/update_poll/end_poll /ask tools (#888). create_poll can attach a sanitized emoji per answer; reads are GUILD-WIDE (every readable channel, current first — Discord has no guild-poll API) and read_poll surfaces who voted each option (public votes). Create/list/read are open to anyone (poll-create rides the ask bucket); end/update are mod-gated. Discord can’t edit a live poll, so update CLOSES it + reposts fresh (votes reset). Fail-open: a bad request / missing permission / discord blip degrades to a soft status line                                    
versuz cogs/versuz.py (#2874) guild_id, channel_id, action (offer|start|prefill|song|queued|catalog|button|poll_open|revote|poll_close|round|end|card|recap|resume|reattach|score_edit|round_fix|first_to_edit|repeat|merge|reopen|round_add|poll_link|archive|cue|anon|opener_dm|void), ok, round (the round number), reason, count, duration_ms, hit, source, error, mode, user_id, message_id — Toots hosting a Versuz night. song is the voice-chat listener: ok=true with reason jockie (a Started-playing line credited to a player), shorthand (typed A: song) or fix (the card’s Fix songs form set the slot: a link read by the catalog, or Title - Artist) or clear (an emptied box on the Fix a round page dropped the slot’s song, owner ask 2026-09-09; the card waits on that player’s next song); ok=false with bad_link (the Fix songs form got a link no catalog could read: the slot stays, the reply says what to paste) or unattributed (a track started with no recent play command from either player: Jockie changed its shape, a queue replay, or the players are not who the host named) or not_a_player, or no_track (#2878: a player’s play command got no Started line inside the attribution window, Jockie down, out of the channel or another instance answered; the card says so once per play; since #2967 a command the bot answered with an Added Track line, the track waiting in the queue, is never no_track) or extra_a / extra_b (the same player’s next queued track started before the other player played, past the redo window: their pick stays, the card notes the extra track). offer: two different people summoned the music bot in a voice chat with no match on it inside 5 min (a typed command or a slash /play in that chat, or a command typed in any other channel once the bot’s Started-playing line lands in the chat), and Toots posted the prefilled Set up button there (one per voice channel per 30 min; versuz_offer error when the send failed). ok=true reason = voice (posted in the voice chat) | home (Toots could not post there, so it went to the last channel a versuz ran in; that channel is prefilled on the form either way); ok=false names why an offer that had two players did NOT post (once per 30 min cooldown, not once per play): off (the surface is off for the guild), no_send (Toots cannot post in that voice chat and no versuz has run in the guild yet, so there is no fallback channel), no_home (she cannot post in the voice chat and cannot post in the last match channel either), no_member (a player is not in the guild), stage_error (the stage read failed), no_voice (two people played from text channels inside the window and neither sits in a voice channel, so there is nowhere to offer; channel_id is the text channel, once per guild per cooldown; 2026-09-03 23:40 UTC: Jockie bound to the text chat, both players playing from it); 2026-09-03: two people played and nothing posted, because the send check treated a voice channel as unpostable, and nothing said so. start reason = quick (/versuz @a @b opened the match with no form: no name, no first-to, the voice channel the host sits in; owner ask 2026-09-11) | form (every other door: the Set up form, the auto-start offer’s prefilled form). A blank name box is not a miss – the match titles itself <A> vs <B>. reattach (owner ask 2026-09-03, resiliency): the match was picked back up from resumable_sessions, reason db = a card button pressed while the match was not in memory (before the on_ready restore, or after one that failed: the on_interaction listener resumes it and handles the press), command = /versuz or the Set up form run again on a channel whose match is on (a fresh card at the bottom, no second match). score_edit: the Edit score form set the counts by hand, reason = <old a>-<old b>-<old ties>><new a>-<new b>-<new ties>; the rounds keep their own results (the tie count is the tied rounds plus a tie_adjust the form sets, so a night that started before Toots was hosting can show its ties). repeat (owner idea 2026-09-03): a song that already ran this match started again, round = the round it started in, count = the round it first ran in, user_id = the player; the card and an H3 line say so, the pick stands. round_fix (#2904): the Fix a round form corrected a SCORED round (a song replaced, the result set), reason = <old winner>><new winner> (- when the result did not change), count = songs replaced; the running score follows the result and the round’s versuz_rounds row is replaced. first_to_edit (owner ask 2026-09-05): the First to box on the round page changed the match length mid-game, reason = <old>><new> (- = no limit), round = the round in play; a first-to the leading score already reaches is refused in the ephemeral reply and emits nothing. button: one control-card press, reason = the button (open|close|revote|round|recap|anon|end; fix|score from a card printed before the three correction forms merged into Fix a round, 2026-09-03), duration_ms = the handler’s time (Discord shows “This interaction failed” past ~3 s), ok=false + a versuz_button error when the handler raised. queued: Jockie’s “Added Track” embed named a player as the requester of a track now on the queue (it starts later and is credited by title, see the Jockie quirks in ARCHITECTURE). poll_open reason auto (both played, second song ended) | clock (the poll timer: both in and the second song versuz_poll_seconds into its play, default 60 s (120 until 2026-09-04, owner ask), 0 = wait for the end; owner steer 2026-09-03; a versuz_clock error means the timer task died) | manual (Open poll now) | revote (Re-vote round: the round that just closed runs back, a tie or an End poll tapped too early; the revote action right before it carries the result it undid as reason a|b|tie and the point comes off the score) | invalid | forbidden | error. poll_close reason manual (End poll) | auto (the versuz_poll_close_seconds knob ended it, owner ask 2026-09-09; 0 = off) | linked (a hand-made poll linked on the round in play had already ended, so it was tallied at once) | end (closed by End versuz) | expired (Discord finalised the poll without the End poll tap: a mod ended it from the poll’s own menu, or the hour ran out; the message-update listener tallies it within a second, a 5 s watcher is the fallback, a versuz_poll_watch error means the watcher died) | void (a correction after the poll went up, not scored; the poll message is deleted, owner ask 2026-09-03, or ended when the delete is refused; no chat line since 2026-09-05, owner ask) | not_found | error, count = votes, duration_ms = the wait for Discord to finalise the counts. round reason a|b|tie. end reason manual|first_to|idle, count = rounds closed. card (#2877): the drawn scoreboard PNG posted at the finish (reason = final) or by the Recap button (reason = recap), ok=false + reason send_failed when the text board went instead (a render exception is an error with source versuz_card). card reason=buried (owner ask 2026-09-04) is a different thing under the same action: the CONTROL card re-posted at the bottom of the match channel because versuz_card_messages messages (count = the knob, default 10, 0 = off) buried it while no poll was open; at most once per 20 s. prefill (owner ask 2026-09-04): the match opened with the songs the two of them traded in that voice chat BEFORE it was set up, count = songs seeded (1 to 5: the last two complete pairs, plus the newest song when it has no answer yet, which sits in the round in play waiting on the other player; since 2026-09-05, when the complete-pairs rule seeded nothing for one played song and one queued one), round = the current round after the seed; the seeded pairs wait for Open poll now. A track the music bot’s Added Track line named a player for BEFORE the match (_pre_queued) is carried into the match’s queue book at the start, so its Started line minutes later is credited by title, not unattributed. recap (owner ask 2026-09-03): the Recap button posted the board of the match so far, count = rounds scored. merge (owner ask 2026-09-08): the Merge earlier match button on the /versuz pickup reply folded the ended match(es) of the night in this channel into the live one, count = rounds absorbed, round = the round in play after the renumbering; a versuz_merge error with recoverable=false means the state merged in memory but the standings rows were not fixed (the leaderboard counts the night twice until they are). reopen (owner ask 2026-09-09): the Reopen versuz button on the wrapped card, or the Reopen offer on the /versuz reply, brought the channel’s last ended match of the night back at its next round, count = rounds scored, round = the round in play; its standings rows came back off in the same transaction (versuz_reopen error = the read or the write failed, nothing changed). round_add (owner ask 2026-09-09): the add-a-round entry in the Fix a round picker appended a fresh round for the host to fill by hand, round = its number (an empty round in play is reused and emits nothing). poll_link (owner ask 2026-09-09): the Poll link box on the round page attached a hand-made two-answer poll to a round, message_id = the poll, reason = open (the round in play took it as its open poll; the bot watches it) | final (it had already ended: tallied at once, a poll_close reason=linked follows) | scored (a scored round’s result re-read from it, count = votes; a changed result moves the score like round_fix). Each answer is matched to a slot by the player’s name or the song’s title; when neither matches, nothing is linked and the host is asked which player the top answer belongs to (two buttons); the pick links it. archive (owner ask 2026-09-09): the Archive button on the /versuz reply and its picker posted a past match’s drawn board into the channel, reason = final (an ended match) | recap (the live one) | send_failed, count = rounds scored; a versuz_archive error is the listing or the round read failing. catalog (owner steer 2026-09-03): the lead artist + features lookup for one registered song (utils/versuz_catalog: Deezer’s track contributors, then iTunes billing, then Genius’s featured_artists for a cast both leave blank), hit = a catalog credit replaced the Jockie/typed text, source = deezer|itunes|genius on a hit (genius, #2885: both catalogs left the cast blank and Genius’s featured_artists named it; the catalog’s lead and link stay), duration_ms = the whole lookup (bounded at 8 s), ok=false only on an exception (a miss is ok=true hit=false: fail-open, the song keeps its text and the regex feature). The ops monitor reads the hit rate as the versuz_catalog integration-health line. A card edit or send, a poll post and an End poll ride one retry on a transient Discord error (_twice: a 5xx, a 429, a timeout); a card edit that still fails is a versuz_card_edit / versuz_card_send error, a poll post that still fails notes it on the card (“tap Open poll now to try again”), and a state save that fails is a versuz_remember error (the state a redeploy would lose). void (owner ask 2026-09-12, “delete a versuz, so it doesn’t count against stats”): a MOD deleted a wrapped night from the Delete a night picker on the /versuz reply. The match row goes to status = 'hidden' with end_reason = 'voided', its game_scores rows are removed (count = how many, normally 2 – one per player), and the crown its finish gave comes off the winner’s streak (best_streak stays, the rule versuz_reopen and versuz_merge already follow). message_id = the match id. The ROUND rows stay, so a night voided by mistake can be rebuilt. All three doors back into a match – the archive picker, Reopen versuz, Merge earlier match – filter on ended/live, so hidden removes it from every one. A failed write emits no void event and a versuz_void error with recoverable=false: the mod was told nothing changed, and it is a transaction, so nothing did. Watch: versuz_void errors mean a mod asked to unscore a night and it did not happen, so the leaderboard still counts it. anon (owner ask 2026-09-12): the control card’s Anonymous toggle turned the match’s blind mode on or off, reason = on|off, round = the round in play. While it is on, the poll answers and the card’s slot lines read pick 1 / pick 2 instead of the player names, the two run in TITLE order rather than play order, and the card holds both songs back until the round is set; the tally reveals the map when the round closes. The button is refused while a poll is up (Discord cannot rewrite the answers it already sent, so a flip mid-vote would half-reveal the round). It rides the match row, so Reopen versuz brings a blind night back blind. Watch: nothing new – anon is a host action on an existing surface, not a rate, so it earns a row in the versuz_actions panel (which counts every action) and no monitor of its own. opener_dm (owner ask 2026-09-12): an anonymous match sent the drawn first player to the two players by DM, because the room is not told who plays next. One ok=false event per player who could not be reached (reason = the exception class, user_id = who), then a rollup with count = how many of the two landed and reason = sent (both) | partial. It is FAIL-OPEN: a player with DMs closed just settles the order with the other player, and the match runs either way. Watch: a guild where every opener_dm rollup reads partial means the DM path is not reaching anyone there, so the draw is effectively not being delivered. cue (owner ask 2026-09-09): one cue in the match’s voice channel, reason = open (the poll posted) | warn (versuz_cue_warn_seconds before the auto-close) | close (End poll tapped), mode = speech (Toots says the line in her ElevenLabs voice, owner ask 2026-09-10) | tone (the fallback: no key, or the synthesis failed; a versuz_cue_speech error carries a synthesis exception, the tts_synthesize event the call itself) | staging (the versuz_voice experiment is STAGING: the line went to #bot-logs, she did not join; OFF emits nothing), round = the round, duration_ms = the connect (first cue of the night) + the whole play (the ok event fires when the player finishes, not when it starts); ok=false + error = no_voice (the match has no voice channel) | stage (a stage channel, not handled) | no_permission (no Connect or Speak) | no_deps (PyNaCl / davey missing) | no_ffmpeg | busy (another live match in the guild holds the guild’s one voice connection; plays again once it ends) | timeout (the connect) | ffmpeg_exit_ (ffmpeg ended with an error mid-tone; the player reports no error for that, so the exit code is read at completion) | an exception class name (the connect, or the player failing mid-tone; a `versuz_cue` error carries the traceback). The first five turn the rest of the match's cues off, so they show once per match; the ops monitor reads the ok rate as the `versuz_cue` health line with no_voice, stage and busy excluded. **Watch:** `song ok=false` at any rate means the listener stopped understanding the music bot; `poll_close ok=false` means rounds are not being scored; `versuz_remember` errors mean a redeploy loses the match; `cue ok=false error=timeout` on every cue means the voice connection is not coming up (a Railway egress or a voice-gateway change).                                    
wheel cogs/wheel.py (#3300) guild_id, channel_id, user_id, action (build|panel|generate|regenerate|spin|save|render|button), ok, mode (manual|theme), count (slices on every action; saved wheels on list), query (the THEME, <=80 chars), reason on a miss, duration_ms (render only) — the random wheel. Slice LABELS are never logged: they are user content, and the theme is the one piece that says what a wheel was about. There is ONE command, /wheel, which opens the builder page; every other door is a control on that page or on the posted card (owner steer 2026-09-14). build opens the page (reason new = a fresh draft, saved = opened on a wheel from the shelf). Opening a saved wheel does NOT copy it, so posting it straight back posts the wheel ITSELF and its spins keep counting; the copy happens only when something actually changes, because a saved wheel may already have cards posted in the room. panel is one action on the page (reason add|drop|open|forget|post; not_yours when somebody who neither saved it nor mods tries to take a wheel off the shelf – which un-saves rather than deletes, so the page and any posted card stay alive). generate is a themed draw from either door, the page’s carrying reason panel, and regenerate a redraw from the card (both model calls, whose own latency rides claude_api purpose=wheel_items). Miss reasons: too_few (a typed list under 2 slices), empty (the model’s draw came back with nothing usable — the fail-open path, see the health line below), rate_limited (the per-user daily bucket, a refusal by design), not_found (no wheel under that name), not_yours (a delete by someone who neither saved it nor mods), taken (a save onto a name another member’s wheel holds – the same rule as a delete), gone (a button pressed on a wheel whose row is deleted or pruned — expected, a card outlives its row), wrong_guild. render is the ONLY action carrying duration_ms, and it IS the whole latency of a spin: no model call is involved, so the render is the wait between the tap and the picture. It carries mode: still is the plain PNG (measured ~0.4 s) and spin is the animated GIF a PULL draws (#3347) – 20 frames plus the still as a held last frame, measured 1.8 s at two parts and 2.55 s at twenty-four, the worst case. The ceiling went 4 s -> 6 s for that, because 4 s sat directly on the animated p99 and would have gated on ordinary spins. The dashboard panel splits p95 BY MODE: one line over both averages two unrelated things, hiding a still that had quietly tripled and reading a shift in the still/spin mix as a regression. Only a real pull animates; a card posted at rest, or repainted for any other reason, stays a PNG. A render that raises is a wheel_card error with recoverable=true and the spin posts the slice list instead. Health: wheel_generate counts generate + regenerate, ok against the total, excluding rate_limited — the draw is fail-open, so an ok rate falling is the only sign the model half stopped working (a prompt that no longer lands, or the narration filter turned too strict and now eats every line). Watch: wheel_generate under the floor; a render p99 past its ceiling means the render is starving for the thread pool – read it per mode, since the two modes are ~6x apart by design.                                    
scheduled_event cogs/scheduled_events.py guild_id, user_id (the asker; absent on anonymous reads), action (create|list|read|update|end|delete|start), ok, reason (None on success; invalid|no_permission|not_found|forbidden|not_mod|already_over|not_scheduled|error); create also: has_location, has_description; list also: count — Toots created or managed a Discord scheduled event via the create_event/list_events/read_event/update_event/end_event/delete_event/start_event /ask tools (#888). read surfaces who’s interested (top names, public); end leaves a cancelled/over tombstone, delete removes the event entirely, start flips a not-yet-started event live early. List/read open to anyone; create/update/end/delete/start are mod-gated AND need the bot’s Manage Events permission (checked in the cog). External events (free-text location); fail-open throughout                                    
memory_search cogs/ask.py guild_id, query, hits, vector (True = a semantic embedding cosine hit answered it; False = the keyword/FTS path did, and aliases/expanded/fell_back only apply to that path), aliases (rename-aware alias terms OR’d into the query), expanded (thin literal match → the concept was expanded to keyword terms via Haiku and re-searched), fell_back (still thin → backfilled with the durable daily arc so conceptual recall like “messiest thing” gets real material to reason over), detail (True = the recall_detail hour-grained drill-down), proactive (True = the always-on [relevant memory] recall Ask._relevant_memory runs on EVERY answer keyed on the incoming message, not the on-demand search_memory tool; hits is how many notes were injected after dedup)                                    
memory_reindex cogs/memory.py guild_id, and ONE of: tagged (# of legacy notes given concept keywords) OR embedded (# given a semantic-recall vector) — the keyword and embedding /remember reindex passes each emit their own                                    
embedding utils/embeddings.py ok, model, duration_ms; on success: chars, dims, batch (# inputs on an embed_batch, absent for single embed); on failure: error (empty_input|no_embedding_in_response|HTTP <status>|<exc>) — one OpenAI text-embedding call (a memory note at write/reindex time, a recall query, or the Kalshi D3 catalog/query embed)                                    
kalshi_shortlist utils/markets.py ok, duration_ms, chars; on success: hits (# shortlisted), catalog (index size); on a query-embed miss: error (query_embed_miss) — the D3 (#480) semantic Kalshi discovery ranked the FULL series catalog (cosine over the SeriesVectorIndex) for a query and returned the top-k shortlist handed to pick_kalshi_series in place of the volume-top-1000 title list (finds an on-topic but low-volume series, and shrinks the stage-1 prompt). Fail-open: an empty shortlist / no index / no OPENAI_API_KEY falls back to the title list                                    
kalshi_lexical_search utils/markets.py ok, duration_ms, chars; on success: hits (# events returned), catalog (event-index size, ~7k) — the EVENT-level LEXICAL (BM25) Kalshi discovery (KalshiClient.search_events, utils/lexical_index.py). Kalshi has no search API and team/player names live at the EVENT (“Spain vs Saudi Arabia: Goalscorer”) + MARKET (“Lamine Yamal: 1+”) level, NOT the series title (“World Cup Assists”); a first semantic event index leaked badly on proper nouns (“Spain Saudi Arabia” matched Israel–Saudi geopolitics / OPEC / WTA, never the soccer game — entities want exact tokens, not embeddings) and was reverted for this BM25 lexical index. Built once per hourly refresh over the ~7k open-event docs (each doc = event title + sub_title + every nested market label, captured for free by flipping the existing open-events walk to with_nested_markets=true); pure/in-process/no embeddings, prices re-fetched live per query (get_event_markets via /markets?event_ticker=X, the filter that works — /events?event_ticker is silently ignored). Routes BOTH the general kalshi tool (event-first, series-index as fallback) and kalshi_player_props (matched matchup events, team-token + _is_player_prop_event guards, per-player collapse). Last-mile SELECT (#kalshi-line): the FTS ranks a matchup’s ~25 markets by lexical relevance, so the game moneyline is rarely the top hit (measured: KXWCGAME ranked #4 for “England Argentina”, behind first-team-to-score/correct-score/advance), and the old event-first path took events[0] blindly — grabbing “First Team to Score” over the moneyline (the ARG@SUI live-bet outage: the /bet board offered a game whose _sides() was empty because the wrong Kalshi market was folded). The unified MarketsManager.kalshi_search(query, *, intent, n, k, expand) now does (optional OR-expand for recall) → FTS → ClaudeClient.pick_kalshi_events (a Haiku last-mile pick of 1..N events matching the caller’s stated intent), so FTS stays the cheap recall narrowing and Haiku is the precision selector over the small candidate set (no whole-catalog model call, no external search service). Callers pass their intent: KALSHI_INTENT_MONEYLINE (the game-winner line — bookie reprice + commentator via fetch_prediction_snapshots, the betting surfaces, and the /ask game-market tools) vs KALSHI_INTENT_GENERIC (the open kalshi tool). The selection is cached per (folded_query, intent, n, sport) so the ~60s/live-game reprice+commentator paths collapse to ~one Haiku pick per game (prices stay fetched fresh); fail-open to the top FTS hit if the picker errors / isn’t wired. Emits claude_api purpose=kalshi_event_pick. SPORT-SCOPING + richer candidate metadata (#kalshi-line): an OR-rewritten matchup query (db._or_search_terms, #1325 — needed since Kalshi indexes the CITY only, so a full-team-name AND-query found nothing) floods on a common city token — “Los Angeles” pulled 361 candidates across SIX sports (MLB/MLS/NBA-summer/WNBA/T20-cricket/NFL) and pushed the MLB moneyline to rank 21, out of the old k=12. Two levers fix it: the index now captures Kalshi’s product_metadata.competition/competition_scope + a derived sport column (_kalshi_competition_to_sport: ‘Pro Baseball’→baseball, ‘FIFA World Cup’/’MLS’→football, …), so kalshi_search(sport=...) (the caller’s known game.meta['sport'], threaded through fetch_prediction_snapshots) SCOPES the FTS to that sport (WHERE sport = $) and the cross-sport flood drops out; and k is widened to 40 (_KALSHI_SEARCH_K) so a buried moneyline is still retrieved. A sport-scoped fetch that returns EMPTY (stale/unmapped sport) falls back to the UNSCOPED fetch, so scoping only ever narrows. The picker also now SEES the richer per-candidate metadata (subtitle matchup+date · competition·scope market-domain/type · market_labels outcome legs, via _format_kalshi_candidate) so it tells a game moneyline from a season/futures/prop market of the same name. Dry-run-validated 8/8 across soccer/MLB/NBA/NFL + generics with a real Haiku pick (scripts/dryrun_kalshi_matrix.py); the sport/competition/scope columns fill on the next hourly index refresh after deploy. Fail-open to []                                    
catalog_lookup utils/apple_music.py + cogs/ask.py Emitted at two layers: (1) the apple_music raw-call event — query, entity (song|album|artist|artist_songs), results, ok, duration_ms (the iTunes-call latency telemetry); (2) the cogs/ask.py FAILOVER event (#847) — query, entity, source (itunes|deezer|none = which feed answered after the iTunes→Deezer failover), ok (source≠none). The cog event has no duration_ms (stays out of the latency table); its source field makes the failover observable — a spiking deezer rate = iTunes throttling, a none rate = both feeds down. The lookup_catalog tool now routes through the utils.music facade: Apple Music / iTunes primary (exact durations / tracklists / discographies / the catalog-wide artist_songs longest-song scan), falling back to Deezer’s public catalog API on an empty-or-throttled iTunes result (song/album/artist full parity; artist_songs degrades to the discography with a (via Deezer; iTunes was throttling) note), and on a both-miss a throttle PROBE (apple_music.probe_throttled) distinguishes a real (catalog feeds are throttling...) DOWN message from a genuine (no catalog match for that) — the catalog twin of the markets DOWN-vs-empty fix                                    
feed_status cogs/ask.py guild_id, degraded (the source keys whose circuit breaker is OPEN — sgo/the_odds_api/api_sports/polymarket/kalshi), near_cap (the source keys at/over the LOW_PCT usage threshold from the health cog’s cached snapshot) — a proactive LIVE FEED STATUS block was injected into an /ask answer (#847): the model was shown which data feeds are DOWN / near-cap right now AND where to route instead (failover from utils.sportsdata.sources.CAPABILITIES), so it never reads a downed feed as “no games” or fabricates a slate (the proactive complement to the reactive per-handler DOWN-vs-empty messages). Built from in-memory breaker + usage flags only (utils.feed_status.build_feed_status, NO network); the usage snapshot is cached on bot.usage_status by the health cog’s ~30min poll. Emitted ONLY when the block is non-empty (a degraded/near-cap feed exists); the healthy case injects + emits nothing                                    
reference_lookup utils/reference.py + cogs/ask.py also emitted by the standalone song_credits /ask tool (#847 — Genius promoted into its own tool, emitting this SAME event with source=Genius\|none so its integration-health telemetry survives bypassing the reference dispatcher). query, aspect, source (Genius|MusicBrainz|Wikidata|Wikipedia|none = the source that ANSWERED), tried (the source names SELECTED by intent, in order — a source in tried but NOT source matched the intent but returned None and fell through; source=Wikipedia + tried=[Genius, Wikipedia] = Genius was selected and came back empty, the signal for a silently-dead keyed source like Genius vs one merely never triggered), result_kind (chart|article|music|factoid|credits|samples|annotation|song|none), url, entries (chart kind), cache_hit (served from the durable reference cache, namespace reference), ok, duration_ms, error (empty_query|no_result on a miss) — the lookup_reference /ask tool ran: an authoritative citable fact from the reference library, routed by intent across sources — Genius (song credits, samples/interpolations, annotations/meaning, identify-by-lyric; link only, token-gated), MusicBrainz (release metadata), Wikidata (date/age factoids), Wikipedia (Billboard Hot 100 chart-table peaks #292/#335, or the general WIKI READER: resolves the article and hands the model its body prose + tables AND, for a detail/list question, follows the most relevant SUB-article (the filmography, the awards list, the electoral history) and renders its tables too - any topic, not just music)                                    
mention_routed cogs/ask.py guild_id, user_id, intent (recap|discourse|icebreaker|music), period (1h|1d|today for recap, else null). intent=ask + reason=other_channel (2026-09-03): the router said recap but the message pings another channel, so the cog overrode it to ask (recap reads the current channel only).                                    
reply_suppressed cogs/ask.py guild_id, channel_id, user_id, reason (broadcast) — someone replied to one of Toots’ own BROADCASTS (a bookie/market card, commentary line, discourse/music/icebreaker post, chime-in) WITHOUT @-mentioning her, so she stayed silent. The reply-restriction firing: she only answers a no-mention reply when it’s a reply to her actual conversational answer (MessageType.reply); a reply to a broadcast (top-level, MessageType.default) is suppressed. Emitted ONLY for this narrow case (not the firehose of every unaddressed message); an explicit @-mention bypasses the skip (she answers) so only genuine suppressions log                                    
links_copied cogs/clipboard.py guild_id, user_id, channel_id, period (1h|1d|1w|1mo|6mo), buffer (the buffer name), count (total links in the buffer after the merge), added (# this copy added; 0 = an idempotent no-op, the buffer already held them all), messages_scanned (# messages walked), hit_ceiling (the scan hit _MAX_SCAN_MESSAGES so the reply warned older links may be missing — no SILENT loss) — /copy scooped a channel’s links (over the window, bot posts NOT skipped so embed-fixer webhook reposts are caught, deduped in original order) into a named per-user buffer. Re-copying MERGES (union, deduped, order-preserving) rather than overwriting: every prior link is kept (no loss) and a re-copy of the same window adds nothing (idempotent). URLs are extracted as the history streams, so only the link set is held in memory. Mod-only; the scan runs in the background                                    
links_pasted cogs/clipboard.py guild_id, user_id, channel_id, buffer (the buffer name), count (links in buffer), delivered (# actually sent), via (always bot — posted as Toots via channel.send, no webhook), ok (all links sent) — /paste posted a saved link buffer into a channel (possibly in a different server), one link per message (each unfurls), paced + run in the background                                    
links_distributed cogs/clipboard.py guild_id, user_id, buffer (the buffer name), count (links in the buffer), routed (# a room was found for), dropped (# fit no room → left in the buffer), delivered (# actually posted), channel_count (# rooms it placed into) — /paste out:curate sorted a buffer’s links across the guild’s curator channels by best fit (/paste run through curation): each link enriched (utils.link_enrich) → routed to its single best-fit room by claude.route_curator (the curate feed’s best-home judge, reused) → posted there fixup-rewritten so it unfurls; a link fitting no room stays in the buffer. Mod-gated, per-run bounded (_DISTRIBUTE_MAX), background task                                    
ask_answered cogs/ask.py guild_id, channel_id, question (the asker’s prompt — the message she answered, not the wider room buffer), post_preview (HER FULL answer, untruncated). Logged so the live-log eval pass grades the real QUESTION+ANSWER pair for fabrication (#315), same framing as the synthetic eval. Internal ops telemetry for the judge — carries the whole answer + the question, an owner-approved departure from the 120-char/no-content preview convention (answer is her own output; question can carry the asker’s info, so logged consciously)                                    
answer_length cogs/ask.py + cogs/voice.py guild_id, user_id, chars, sentences — measures a reply’s length on the way out (a typed @mention via cogs/ask.py, or a spoken voice-note summons via cogs/voice.py’s shared deliver; the typed-mention-voiced case records in ask.py, so the voice cog only emits for user_sent_voice to avoid a double count), observation only, it never trims or rewrites her words. Length is owned by the model: the ask prompt carries a declarative LENGTH POLICY case dictionary (a take/opinion/banter = two sentences; a real how-it-works explainer / a requested list / code runs as long as it must; a deflection = one line) and she matches her answer to the case and adheres. This event records what she actually shipped so a drift back toward wordy takes (the Spielberg-films report) surfaces as a climbing length distribution (a rising p90 sentences/chars) — the early-warning that the policy prompt needs tightening (a prompt edit, never a length value). Deliberately trust + observe, not a regex enforcement guard (owner steer on #676): the live dry run showed she self-adheres to a clean case dictionary, so a hard backstop is only added back if the telemetry proves she drifts                                    
tts_synthesize utils/tts.py ok, mode (speak|sing); on success: chars, bytes, duration_ms; on failure: error (+ chars/duration_ms/detail)                                    
stt_transcribe utils/stt.py ok; on success: bytes, chars, duration_ms, plus (when the upstream returns them) language (detected language code) and speakers (# distinct speakers diarized — a multi-speaker clip comes back as a speaker-attributed transcript, surfaced via client.last_language); on failure: error (+ bytes/duration_ms/detail)                                    
video_transcribe utils/video_fetch.py ok, source (captions|audio|x_text|scrapecreators|none|unavailable); on success: chars, video_secs, duration_ms, lang (captions only), has_meta; on failure: reason (no_info|no_captions_no_stt|too_long|transcribe_failed|no_text_no_video) (+ video_secs/has_meta). x_text = an X/Twitter post resolved to its tweet-text floor via fxtwitter (#491); audio covers a YouTube clip OR an X post’s mp4; scrapecreators = a TikTok/Instagram transcript (+ TikTok metadata) resolved via ScrapeCreators (#1272) — yt-dlp resolves those two hosts poorly from a datacenter IP, so fetch_video routes them there first when provisioned, a miss falling through to yt-dlp. A long/foreign transcript is then summarized to English by a claude_api call with purpose=video_summary                                    
x_fetch utils/x_fetch.py ok, duration_ms; on a hit: has_text, has_video; on a miss: error (no_tweet_id|HTTP |no_tweet|empty_tweet|) — an X/Twitter post resolved via the public fxtwitter (FixTweet) JSON API, the durable replacement for yt-dlp's dead guest-token Twitter extractor (#491). Public content only, fail-open (None → caller degrades to no transcript)                                    
x_media_fetch utils/video_fetch.py (_download_media) ok, duration_ms; on a hit: http_status, bytes; on a miss: error (HTTP |too_large|empty|) — the X CDN mp4 download for a tweet's video (plain aiohttp, no yt-dlp/ffmpeg, #491). Fail-open (a miss → the X post keeps its tweet-text floor, `source=x_text`), so this success rate IS the open datacenter-IP reachability signal for `video.twimg.com` (ops-monitor integration health + `integration_unhealthy` finding)                                    
clip_trim utils/video_trim.py (trim_clip, #video-trim) ok, duration_ms, limit (the ceiling the cut must stay under, secs), target (the cut length ACTUALLY applied), method (snap = moved back to a shot boundary | no_boundary | detect_failed | fixed = snapping off), count (shot boundaries found in the window), source_bytes (the downloaded clip), video_secs (the SOURCE clip’s length); on success: clip_bytes (the cut mp4); on failure: error (empty_input|ffmpeg_exit_<rc>|no_output|<exc>), detail (ffmpeg stderr, truncated) — the ffmpeg cut that brings an over-long wire clip under X’s native-video ceiling. The cut END snaps to a shot boundary (#2215): a fixed cut lands wherever the clock lands, so the clip ended mid-shot; a second short ffmpeg pass (select='gt(scene,N)' + metadata=print, X_TRIM_SCENE_THRESHOLD default 0.25) reports the shot changes in the X_TRIM_SNAP_WINDOW_SECONDS (default 10s) before the cut, and the LATEST one at or before the ceiling wins. Measured on the clip that prompted the trim: boundaries at 106.63 / 111.00 / 115.50 / 118.07 against a 118s cut → cut at 115.5s, giving up 2.5s to end on a real cut; the pass costs ~0.4s wall over a 10s window. -copyts is what makes the reported timestamps absolute rather than rebased to the seek. Deliberately NOT vision: Claude vision reads stills, and stills carry neither motion nor audio — the two things that mark where a moment ends — so 30 sampled frames (~45k input tokens) would buy a worse answer than the free scene score. A failed snap is NOT a failed trim: detect_failed / no_boundary keep the fixed cut and the clip still ships, so ok stays True and only method moves. Watch method for a detect_failed share climbing off zero (the ffmpeg scene filter or the stdout shape changed); it is cosmetic, not user-impacting, which is why it earns a dashboard panel and NOT an ops-monitor finding so it uploads as NATIVE media instead of riding a /video/1 deep-link X embeds only sometimes. Always follows an x_clip_too_long (the ceiling hit) and an x_media_fetch (the download it cuts). A STREAM COPY (-c copy), not a re-encode: both feeds that reach it (the X CDN mp4, TikTok’s play_addr_h264) already serve h264/yuv420p/aac in mp4, so the cut is X-ready by construction and costs under a second. Fail-open, and that is why it needs watching: a failed cut raises nothing — the crosspost just drops back to mode=video_link and the take ships with nothing to play — so error-triage never sees it and this ok rate is the only signal (ops-monitor misc_healthclip_trim, integration_unhealthy finding; the plausible causes are ffmpeg missing from the image after a Dockerfile change, no writable temp space, or a source container ffmpeg cannot copy)                                    
clip_pick utils/clip_pick.py (clip_youtube, behind POST /debug/clip, the music-drop skill) ok, duration_ms, limit (the ceiling, secs), signal (most_replayed = the peak of YouTube’s replay heatmap via ScrapeCreators /v1/youtube/video | loudness = the window with the highest mean ebur128 momentary loudness | start | given = the caller pinned start), start (where the clip opens in the source, secs), target (the cut length applied), method + count (the END snap, the same _snap_to_shot as clip_trim), source_bytes (the yt-dlp download), video_secs (the source length); on success: clip_bytes; on failure: error (missing_url|download_miss|ffmpeg_exit_<rc>|no_output|<exc>), detail — the moment pick + cut for a YouTube video the owner hands the skill (a music video’s chorus, a trailer’s climax). Runs on the bot because a session’s egress cannot fetch YouTube media (measured 2026-09-08: CDN 403 from a session, 100-190 successful YouTube audio downloads a day from Railway). The START also snaps back to a shot change (CLIP_START_SNAP_WINDOW_SECONDS, default 4) so the clip opens on a cut. A manual surface with no monitor: a failed cut is seen by the session that asked for it. Dashboard: the clip_pick panel (cuts by signal).                                    
media_edit utils/media_edit.py (run_edit, the <media op=...> tag on a typed @mention) op (gif|mp4|trim|speed|frame|mute|reverse|resize|rotate|flip|convert), ok, duration_ms, mode (the INPUT kind: video|image|unknown), source_bytes (the upload), video_secs (the source clip’s length, from ffprobe); on success: bytes (the finished file), detail.duration_secs (the output window), detail.retried (the first encode missed the room’s upload limit, so it re-ran at a smaller shape – a FOLDED field, so a panel reads it as parse_json(detail)['retried']); on failure: error (empty_input|input_too_big|unsupported_op|image_edit_failed|no_command|ffmpeg_failed|empty_output|output_too_big|<exc>) — one mechanical edit of an attached file: the model classifies the ask in the SAME call that composes the reply (the tag is the classifier, so there is no second pass), and this runs it on ffmpeg + Pillow. The field is mode, not kind: @instrument passes the event’s fields as keywords into safe_emit(kind, **fields), so a field literally named kind collides with that positional and raises out of the emit. LOCAL work — no API money — so the cost signal is duration_ms, not a quota. Measured on a 60s 720p clip: a 6s gif 1.2s, an accurate 30s-to-40s trim 1.5s, an mp4 that needed the retry 13.3s (two encodes). Three ceilings bound it: MAX_INPUT_BYTES (25MB) refuses a big upload before any work, each op carries its own output-seconds ceiling, and the guild’s own upload limit decides whether the result can ship. Fail-open, so it needs watching: every failure returns None and the reply degrades to text, raising nothing error-triage would see, so this ok rate is the only signal. Watch error: a jump in ffmpeg_failed means ffmpeg left the image after a Dockerfile change or temp space ran out; a jump in unsupported_op means the model is naming ops for the wrong kind, which is a PROMPT problem (claude_client._media_directive is keyed per kind); a steady output_too_big share means the ceilings are set too generously for the rooms using it. Ops monitor: the media_edit health line (ops-monitor misc_health -> media_edit, integration_unhealthy finding), with empty_input, input_too_big, unsupported_op and output_too_big EXCLUDED (_MEDIA_EDIT_BENIGN_ERRORS) – those are what somebody uploaded or what the model chose, not a broken edit path, and counting them would false-flag an ordinary day. Dashboard: the media_edit panel (edits by op, ok rate, p95 latency, retry share) + the expr reply-delivery panel, which now counts media_reply beside voice/image/gif.                                    
media_reply cogs/ask.py (_deliver_media) guild_id, user_id, op, delivered (media = the edited file shipped to the room | text = it did not, and the caption went instead), reason (edit_failed|send_failed) on a text fallback, has_caption on a delivered one — the room-facing half of the pair: media_edit says whether the EDIT worked, this says whether the ROOM got it. The same split as image_generated / image_reply. A send_failed with a successful media_edit is the one case that points at Discord rather than at us (usually a file over the guild’s real limit, which the edit already checks, so it should be rare). No daily cap to read here, unlike image_reply: the edit costs container CPU and no money, so the per-user mention limit is its only budget.                                    
voice_note_sent utils/discord_voice.py channel_id, bytes, duration_secs, is_reply                                    
voice_reply cogs/voice.py (shared deliver, fired for voice-note replies AND typed @mention answers the model nominated for voice — a plain text mention with no <voice>/<sing> tag skips deliver entirely and emits nothing) guild_id, user_id, delivered (speak|sing|text; text = voice unprovisioned, a gate veto, or a synth/send fallback), gate_reason (reply_in_kind = a voice note; model_candidate = the model nominated voice on a typed mention; no_trigger/disabled/too_long = stayed text), model_candidate, sing — voice is a permanent surface: it speaks whenever ElevenLabs is provisioned (bot.tts) and the gate clears                                    
image_generated utils/image_gen.py ok, model; on success: chars, bytes, duration_ms, revised (bool: OpenAI returned a safety-rewritten revised_prompt, surfaced full-text to the caller via client.last_revised_prompt but only flagged — not logged — here, since the revised text is user-derived); on failure: error (empty_prompt|too_long|no_image_in_response|HTTP <status>|<exc>), plus chars/duration_ms/detail where available (chars only, never the prompt text)                                    
image_reply cogs/ask.py guild_id, user_id, delivered (image|capped|text; image = PNG + caption sent to the room; capped = per-guild daily image budget spent, caption went out as text; text = the model nominated an image but generation/send failed so the caption went out as a plain reply), has_caption (on a delivered image), reason (generate_failed|send_failed|unsupported_source|daily_cap, on a text/capped fallback). Image is a permanent surface: it posts whenever OpenAI is provisioned (bot.image), bounded by the per-guild image_daily_cap budget (#326)                                    
gif_search utils/gifs.py query, ok, results (# candidates), duration_ms, error (on failure) — a Giphy search ran (GIPHY_API_KEY-gated client)                                    
x_reply_draft cogs/x_reply_draft.py guild_id, channel_id (the room the content shipped in; None for scout), ok, delivered (channel|bot_logs|skip), reason (experiment_off|dedup|daily_cap|fetch_failed|declined|send_failed|error), trigger (listener|reply|banger) — Toots drafted the X reply she’d leave under a sourced tweet and posted the draft to the owner’s working queue — master guild only, and with NO ping — with the tweet’s BARE fixup link so Discord unfurls it inline under the draft; the OWNER posts the reply on X by hand. The surface graduated 2026-09-01 (#2796) to MASTER-ONLY, so the master-guild gate owns on/off (it replaced the per-guild experiment stage of 2026-08-06): a non-master guild stops drafting entirely (reason=experiment_offthe string is UNCHANGED so the Axiom panel keeps working, but it now means “not the master guild”, not “a mod set the experiment off”), and STAGING can no longer occur, so delivered=bot_logs in the master guild is now purely the FAIL-OPEN (no drafts channel picked, or she cannot post there). Historically STAGING queued to #bot-logs (delivered=bot_logs, the DEFAULT because it was exactly what the surface did before the knob existed), PRODUCTION sends to the drafts channel picked on the /menu “on X” page (delivered=channel; an unset or unreachable channel falls back to #bot-logs rather than dropping the draft, so the queue can’t go silent by omission). The header’s @-mention of the master users was removed in the same change: the surface delivers ~120 drafts a day, so the ping sent ~120 notifications a day for a queue the owner reads in batches; both messages now send with AllowedMentions.none(). Watch delivered=channel after a guild flips to PRODUCTION — a PRODUCTION guild still reading bot_logs means the channel picker is unset or she can’t post there — the hand-off loop that scales replies past the API tier’s third-party-reply 403 (#1732, epic #1793; measured: 12 owner replies out-viewed 48 originals ~13×). Two triggers, one path: listener = a tweet she linked in a Discord post; reply = the wire desks’ THIRD lane (owner steer: alongside biggest + breaking, named “reply”) — each pop_desk/sports_desk/cinema_news slot drafts the freshest-hottest story (pick_reply_stories: the same order_for_breaking ranking on a wider 3h X_REPLY_WINDOW_HOURS) the posting lanes did NOT claim (those get listener drafts on delivery), riding the same slot + wire fetch via the fire-and-forget kick_reply_lane (never delays desk delivery; X_REPLY_PER_SLOT=3, and 0 turns the lane off). Grounded on the fetched tweet (fxtwitter, keyless) + the live context beat (utils.context_beat.gather_context_beat, Grok X-crowd + Perplexity, framing="breaking", purpose=x_reply_draft — the draft USES the context/angle the beat surfaces (that’s what it’s paid for), with only its NUMBERS fenced from being quoted as her own stat; fail-open to a tweet-only draft) + steered to ADD not echo (the owner rejected auto-quotes as dead-similar); the model is the per-guild x replies Models-page knob (tunables.SETTING_X_REPLY_MODELclaude_client.x_reply_model_id, resolved live so a /menu flip needs no restart), defaulting SONNET (a deliberate COST call, #1839 — see the model note below); it gets its OWN row rather than borrowing discourse’s or market drop’s, because those families share their donor’s OUTPUT SHAPE while a reply draft is vision-heavy (~2.8k input tokens/call) and leans on WORLD KNOWLEDGE — a different axis — so a guild must be able to run discourse on Sonnet while replies run Opus; self-declining (EMPTY → reason=declined, a path that has never actually fired — deliberate, since the owner is the filter and a weak draft still surfaces the reply TARGET). De-hollowed (#1834, owner report “her x replies are hollow”): a real-wire dry run scored only 13% of drafts substantive — the rest captions for the photo (“that artwork is doing something”). The cause was three clauses that left captioning as the only legal move — a hard brevity wall (under 15 words, no comma-chained clause) that forbade carrying a specific AND a read, the chatter block being fetched then BANNED from use (compose_market_drop’s framing reused on a surface with no grounding blob to quote instead — so the one source of outside substance was paid for and thrown away), and “react to what you actually SEE” inviting the caption directly. Rewording the three (never adding a “be substantive” rule, per docs/PROMPT_OPTIMIZATION.md) took substance 13% → 38% — that reword is what carries this surface, and it is model-independent. The MODEL was a second, optional lever, tried and then REVERTED on cost (#1839, owner steer “move back to sonnet opus is too expensive”): on the same rebuilt prompt and identical material Opus scored 66% vs Sonnet’s 34% (n=50 each, +32pp, z≈3.4) — it brings real world knowledge where Sonnet reaches for the visual (it clocked a “puss puss” caption as Swedish for “kiss kiss” and the panicking repliers as wrong; it caught that a July 30 Harry Potter birthday post is a day early since the 30th is Neville’s) — so it briefly shipped on Opus (#1834), but the owner priced that ~26pp against the spend and chose Sonnet — and the gap is CONCENTRATED, which is what makes Sonnet-by-default a fair trade rather than a flat loss: on tweets carrying their OWN facts (a transfer clause, a meeting, a date) both models score 5/5, and the whole premium buys the tweets needing outside knowledge (a foreign-language caption, an NBA read). So expect any residual hollowness on the pop/celebrity image posts, not the sports/transfer wire. This is a SETTLED tradeoff, not an oversight — don’t “helpfully” route it back to Opus; it’s now a /menu Models-page pick (x replies), so re-buying the gap is a dropdown, not a code change. Ablations that came back a WASH and were therefore dropped: cutting the image clause entirely (40% vs 43% — the residual captioning is the image’s salience in vision, not the instruction) and adding explicit “say something only you’d know” / “not a caption” blocks (50% vs 43%, inside noise). Scoped to an ALLOWLIST of the channels explicitly configured for the X-post surfaces (the union of the /menu channel sets — discourse/music/music-news/music-market/cinema/cinema-news/pop/sports-desk/prediction/betting, via _CHANNEL_SOURCES; so curator rooms, #bot-logs, and any unwired room are out by construction, and the scope tracks the /menu config with no code edit — owner steer); durable per-tweet dedup (x_reply_draft kv ns, 7d) + a runaway daily cap (X_REPLY_DRAFT_DAILY_CAP, 0 = off by default on the owner’s call “remove the draft cap just let it run” — set it above 0 to re-arm the backstop for a bulk-op burst) + master kill switch + the experiment stage + staging-audit/own-tweet/draft-message guards; fully fail-open                                    
x_reply_tic_guard claude_client.py (draft_x_reply) ok (the retry came back clean and replaced the draft), count (tics on the first pass), kind (the first tic) — the verbal-tic guard (#1975, owner report “these idiosyncrasies in her prose are horrible”: a Discord search showed 21 hits for “flex” and walls of “the real X” / “nobody’s talking about”). utils.x_reply_draft.tic_hits is the deterministic detector, calibrated against BOTH real corpora — it trips 39% of a live week’s drafts against 6% of the replies the owner actually hand-posted, so it catches a habit rather than her voice. On a hit the compose re-asks once, NAMING the offending phrase; that naming is the load-bearing part, because the obvious alternative (hand the model a block of its own recent drafts and say “vary it”, the block the wire desks already use for TOPIC repeats) was measured and made the tics worse — any-tic 9/35 → 19/36, register flags 1/35 → 6/36, the primer effect (docs/PROMPT_OPTIMIZATION.md). Fail direction: the retry is taken ONLY if it comes back clean, so a degraded rewrite can never replace a good line and a retry failure is a no-op. Cost is one extra compose call on the drafts that trip. Watch the ok rate — a collapse means the re-ask stopped working and the tics are shipping again                                    
banger_lane cogs/x_reply_draft.py (_banger_lane) surface, ok, duration_ms, count (search hits weighed — a TOP-LEVEL SHARED_FIELDS column, so query it as count, not parse_json(detail)['count'], which returns null); folded detail: kept (cleared the floors → drafted); ok=False reason (not_master|unprovisioned|experiment_off|no_terms) — the BANGER lane (#1972), the reply surface’s third trigger and the first that finds targets by HEAT rather than by account. The reply lane can only ever draft under the code-owned wire accounts, because that is the only pool the desks fetch; the owner’s own reply wins say the pool is wider — of her top 20 hand-posted replies (2026-08-03) four landed under accounts the wire list has never carried (@Rap, @DrewPavlou, @THEUN1VRSE, @Kurrco), and all 20 sat under parents of 300K–17M views (the 11.7K-like winner under a 17M-view post with 862 replies), while a 1.1M-view viral quote-tweet from a mid-size film account was invisible to us at any temperature. So each pop/sports/cinema_news slot, after the posting + reply lanes, takes the search terms from its OWN top stories (utils.banger_lane.search_terms — quoted/ALL-CAPS titles and capitalized runs, so the query stays inside her lanes for free), runs ONE advanced_search (queryType=Top, min_faves + lang:en + -filter:replies + since_time, every operator verified live against twitterapi.io’s passthrough), and drafts under what clears the floors (pick_bangers: inside the 3h reply window, over BOTH a view and a like floor, an ORIGINAL not a reply/repost, not her own account, not piracy/engagement-farm spam, ranked by engagement velocity — climbing beats merely big, since a forming reply section is the whole point — then capped at one per author). Drafts ride the SHARED path (produce_draft, trigger=banger), so per-tweet dedup, the daily cap, the kill switch and the model’s EMPTY decline all apply unchanged; fire-and-forget so it never delays a desk post. Bounds are env: X_BANGER_PER_SLOT (2, 0 disables the lane), X_BANGER_MIN_VIEWS (50K), X_BANGER_MIN_LIKES (500), X_BANGER_STORIES (2), X_BANGER_SEARCH_LIMIT (25). The health line to watch is kept: a sustained kept=0 with count>0 means the floors have drifted above what the search actually returns — the fail-open miss that raises no error and would otherwise be invisible, since the lane simply draws nothing. Watch reason=no_terms FIRST, though — it is the earlier and quieter failure, because a lane that never searches never reports a count at all, so the kept watch above cannot see it. The lane shipped in #1972 reading story.title/story.text; no desk story type has either field (they carry headline + subject), so getattr(story, "title", "") returned "" for every slot, the query was empty, and the lane failed no_terms on 100% of its 73 sweeps across pop_desk/cinema_news/sports_desk for its whole first two weeks in production — it never ran a single search. The field read now lives in the pure banger_lane.story_terms, typed against the WireStory Protocol and tested against the three real story dataclasses. A sustained no_terms now means the desks genuinely produced nameless headlines; any spike right after a story-model change means the field read broke again                                    
x_audience cogs/x_audience.py ok; folded detail: followers, following, statuses, count (sample size), tweets_24h/replies_24h (bot output + owner reply pace), median/total views + likes split by replies vs originals; ok=False reason (fetch_failed|error) — the nightly X audience sweep (epic #1793 item 13): one user/info + one raw recent-tweets page via bot.xprovider_primary, folded by the pure utils.x_audience.audience_summary, one event per UTC day (daily 08:00 UTC + an idempotent boot run, kv-stamped so the every-merge redeploys never dup). The follower curve + the replies-vs-originals split the growth experiment steers by. Bot-wide (not guild-gated), X-source-provisioned, fail-open                                    
gif_reply cogs/ask.py guild_id, user_id, query, ok, delivered (room = shipped to the channel; no_result = search whiffed; vision_skip = she looked and none fit; cooldown = the per-channel gif gap held it back; daily_cap = the per-guild daily gif budget is spent), vision (picked = chosen by looking at the candidate frames; fallback = the vision pick errored so the top relevance hit was used; none_fit = skipped; no_candidates) — the model nominated a gif on a typed @mention via a <gif: query> tag (utils.gif_signal), the inverse-of-voice taste signal: a gif rides ALONGSIDE the text rather than replacing the delivery channel. gif is a permanent surface: it ships whenever Giphy is provisioned (bot.gifs), paced by the per-channel cooldown + per-guild daily cap. She vision-picks which candidate to send (claude.pick_gif, the outbound twin of the inbound gif vision) rather than sending the top text-relevance hit                                    
x_follow_change cogs/x_audience.py (hourly churn sweep) ok, kind (gained|lost|sweep), reason, count; folded detail: handle, tenure_hours, followers, statuses, low_signal (per-account rows) / gained, lost, lost_low_signal (the sweep row) — the X follower-churn diff (#x-churn). x_audience records only the follower TOTAL, so it nets follows against unfollows: a day with 5 follows and 3 unfollows reads +2 and the 3 departures never appear. This walks /twitter/user/followers hourly, diffs against the x_followers snapshot table (keyed on the X user id, so a rename is not a departure plus an arrival), and names each loss. A lost row is confirmed before it is called an unfollow — reason=unfollow means the profile is still alive (real feedback about what we post), account_gone means deactivated/suspended (not feedback), unconfirmed means the per-sweep confirm cap or a failed read. The refusal path is load-bearing: ok=False with reason=incomplete|low_coverage|empty means the follower walk was truncated, and the sweep wrote NOTHING and diffed NOTHING — diffing a short list reports every follower past the cut as an unfollow, which is worse than no data. reason=seeded is the first run. HOURLY because the cadence sets the attribution precision: a departure lands between two sweeps, so an hourly window holds the 1-3 posts that could have caused it, where a daily window holds ~20. Losses also post to the master guild’s #bot-logs. Ops-monitor health, two complementary paths: the API read itself is curator_fetch with phase=followers, so it already rolls into misc_health["twitterio"] via _MISC_INTEGRATIONS with no new wiring; the SWEEP-level refusals are a separate branch feeding misc_health["x_follow_sweep"] (the per-account gained/lost rows are payload and never counted, and reason=seeded is excluded so a fresh deploy cannot flag). That second one exists because a refused sweep is the fail-open trap: it raises nothing and posts nothing, so a broken follower walk looks exactly like a quiet week while the net follower curve carries on looking healthy. Known limit — correlation, not proof: X publishes no per-viewer data to any API (twitterapi.io has no likers endpoint — /twitter/tweet/likers and /twitter/tweet/favoriters both 404, verified 2026-09-14), so this narrows a departure to the posts in its window and cannot prove the account read any of them; the reply/mention/retweet record is the only per-account evidence of a real read. Correlate to the posts in the window (tweet_posted is in the same dataset): ['tootsies'] \| where event == 'x_follow_change' and kind == 'lost' \| extend d=parse_json(detail) \| project _time, tostring(d['handle']), toreal(d['tenure_hours']), reason
x_reply_edit cogs/x_audience.py (nightly sweep) ok, post_preview (the owner-POSTED reply); folded detail: pure_cut (bool — the KPI: the posted text is a contiguous slice of the draft, i.e. the owner KEPT a beat instead of rewriting; measured 2026-08-02 over 223 pairs, pure-cut posts out-viewed rewrites ~2.7×), words_draft, words_posted, slop_score — the owner-edit-fidelity join (#ai-register): the reply cog retains each draft’s text by target tweet id (kv ns x_reply_text, 7d, cogs/x_reply_draft.py), and the nightly x_audience sweep matches the timeline page’s replies by inReplyToId (utils.x_audience.reply_edit_metrics, pure + unit-tested), emitting ONCE per posted reply (kv-stamped x_reply_edit ns, 14d). Metrics only — the draft text is never emitted. A reply with no retained draft (hand-written, pre-feature, >7d late) is silently skipped. Weekly cut-rate APL: ['tootsies'] \| where event == 'x_reply_edit' \| summarize cut_rate=avg(toint(parse_json(detail)['pure_cut'])), med_posted=percentile(toint(parse_json(detail)['words_posted']),50) by bin(_time, 7d)

The slop_score auto-stamp (#ai-register, owner steer “every post every surface”): emit() itself stamps slop_score (the deterministic AI-register score from utils/slop_score.py — vendored antislop lexicon + not-X-but-Y tells, 0..1) onto ANY event carrying a post_preview, before the fold — so every shipped post on every surface (discourse/music/chimein/commentary/market drops/ask/…) is scored with zero per-surface wiring, and a surface added tomorrow is covered by construction. Folded into detail (zero field-budget cost): query parse_json(detail)['slop_score']. An explicit caller-passed slop_score is respected (the x_reply_edit join passes it literally so the schema scan sees the field’s in-source emit). Baseline at ship: 0.00 across every live surface. Consumed by the ops monitor’s slop_high finding (below) and, on the eval cadence, scripts/eval_ai_register.py.

Adding a new event: call emit("your_kind", key1=..., key2=...) and add a row to the table above + the events.py docstring. Use snake_case for kinds and fields. Don’t include full message content (data minimization, per the constitution).

Operability is part of the feature (full original text)

A feature isn’t done until it’s observable. Every new capability — especially a new outbound integration (an API client, a model call, an external fetch) — ships its instrumentation in the SAME change, not as a follow-up. This is mandatory and unprompted on every change going forward: apply the full bar below to anything that adds or touches a surface, integration, or outbound call — every time, without waiting to be asked. “Do we have full instrumentation for this?” should already be yes by the time the PR opens — never a question the owner has to raise. The bar, in order:

  1. Latency + outcome, per distinct call. Wrap the outbound call with @instrument("your_kind") (or timed_event for a block, @timed/timed_span for a heavy flow step) so it emits one event with ok + duration_ms. Never hand-roll a stopwatch — utils/instrument.py owns the clock. Any event carrying duration_ms is auto-picked-up by the ops monitor’s p50/p95/p99 table; add a ceiling to LATENCY_CEILINGS_MS (or a LATENCY_CEILING_PREFIXES rule) for each one if a blown tail should gate, not just render. Instrument EACH distinct external step as its own event, not one event for the whole feature — if a flow does an API resolve, then a CDN download, then an STT call, that’s three events, so the three failure modes stay separable in the logs (the X path’s x_fetch + x_media_fetch + stt_transcribe split is the worked example, #491). One bundled event hides which leg actually broke. Naming convention: an event KIND names WHAT was called (market_fetch, claude_api); a SHARED integration hit from many surfaces (SGO/Kalshi/Perplexity/Claude from bet/commentator/ask/discourse/music) also needs WHO called it. Don’t rename the kind (it’s the stable dashboard contract) — instead wrap the surface’s outermost entry method in @scoped_surface("commentator") (or with surface_scope(...)), and utils/instrument.py auto-stamps every nested event/span with surface= + a hierarchical path (surface.kind[.source], e.g. commentator.market_fetch.sgo) so the latency + error tables group by the CALL SITE, not just the integration. Additive + fail-open (no scope → no surface field, byte-identical to before). SPAN names (@timed) are already hierarchical (surface.function) — keep that. Wire a new surface at its one outermost entry method, never per call. Field budget (the Axiom schema cap): every DISTINCT field NAME across ALL events counts against the dataset’s per-dataset field limit (the tootsies dataset is on the Axiom Cloud plan: 1,024 fields, up from the free plan’s 256 that #1296 hit; the whole EVENT+log stream shares this one dataset, so it’s one budget), and AT the cap a never-seen field name SILENTLY DROPS the whole event at ingest (a 400 naming the field, or a 200-with-failures partial) — invisibly degrading the exact observability the ops-monitor + health cog depend on (#1296, how curator_posted went missing on the old 256 cap). The 1,024 cap stopped the dropping, but the owner may downgrade back to free (256), so the discipline below is kept precisely so free stays a viable fallback — after the #1296 “full maps push” the footprint is ~146, well under 256 (a downgrade would be clean), and the tripwire in tests/test_event_schema.py ratchets it further DOWN as consolidation continues; don’t bump it up. utils/event_schema.py is the single source of truth for the field vocabulary, split into three ledgers: SHARED_FIELDS (the reusable cross-event vocabulary — reuse a name from here when the meaning matches: source/kind/reason/ok/count/query/phase/delivered, …, before inventing mycog_specificthing), _EVENT_SPECIFIC (the top-level fields a dashboard / ops-monitor / consumer actually READS — these keep a real Axiom column), and _FOLD_FIELDS (the ~124 diagnostic singletons that no consumer reads: emit() auto-folds any of these into the ONE shared detail map wherever emitted — the “specific events use maps” model, so they cost NO top-level column and can never hit the cap; query them with parse_json(detail)['<name>'], see the _FOLD_LOG for the transition-window coalesce bridge). To ADD a field: reuse a SHARED name if one fits; else, if a consumer must read it top-level, add to _EVENT_SPECIFIC; else fold it (add to _FOLD_FIELDS). tests/test_event_schema.py fails CI on an unregistered field (the conscious reuse-or-add-or-fold gate) and ratchets the footprint DOWN toward the 256 free fallback. Two more runtime guards back the registry so a leak can’t slip past the static lint: emit() JSON-stringifies any dict field value (_demap), so a variable-key dict can NEVER flatten into per-key Axiom fields (home_pcts.<source>/genre_tags.<genre>/context.<key> were exactly this — put high-cardinality / variable-key data in ONE stringified dict field, not separate fields named after the keys), and utils/axiom.py inspects the ingest response’s failed/failures and logs (rate-limited) when Axiom rejects rows — a silent drop becomes a visible signal. When the count nears the cap: python -m scripts.axiom_setup --fields audits live-vs-registry (dead + drop-risk fields), --vacuum reclaims dead-field slots (renamed/removed events), and a wide/separable event family can route to its OWN dataset via an AXIOM_*_DATASET env (off the main budget). The dataset is currently on the paid Cloud plan ($25/mo, 1,024-field cap) which lifted the immediate wall; the footprint is now under the free 256 (via the fold), so a downgrade is clean — the consolidation levers above stay the primary tools to keep it there.

    Folding a field breaks every CONSUMER that reads it top-level (#2254). In the monitor the break is silent: ev.get("kept") returns None, nothing raises, and the branch reports 0. In APL it takes one of two forms, both measured against the live dataset. If the name is nowhere in the dataset schema the query FAILS – the wire panel returned HTTP 400 invalid field: "kept". If the name IS in the schema, because another event writes it top-level or because this one did before the fold, the query RUNS and reads null on every row: sum(source_count) returned 0 for all four X surfaces, and by league grouped every row under a null key. The second form is the dangerous one and the more likely after a fold, because folding leaves the old column in the schema.

    Five monitor branches and five panels shipped this way. Healthy wire_read rows carrying kept=157 counted as an outage, and the curate routing funnel read 0/0 on a lane routing 467 of 492 posts. So when you fold a field, GREP for its readers first. In the monitor, read it with _ev_folded(ev, name); in APL, read it with parse_json(detail)['<name>'] and alias it. Two guard tests hold the line: tests/test_ops_monitor.py parses the monitor and fails on any ev.get("<folded name>") or ev["<folded name>"], and tests/test_axiom_setup.py scans every shipped panel and monitor query for a folded name used as a column.

  2. Errors. Real failures go through emit_error(source=..., exc=..., recoverable=...) so error-triage groups them. Set recoverable honestly (user-impacting → False). At framework boundaries, emit the application exception rather than a generic transport wrapper. The global slash-command handler unwraps discord.py’s CommandInvokeError, so Axiom groups the real cause and the bot-log classifiers can recognize database and prompt failures.
  3. Silent / fail-open degradation is the trap. If the feature is fail-open (a miss → degraded-but-not-crashed, like embeddings → keyword fallback, X enrichment dying, a market source going dark), an ok=False raises NO exception, so error-triage never sees it. Wire it into the ops monitor’s integration-health rate tracking (an OkRate keyed by its dimension + an integration_unhealthy-style finding) so the silent degradation surfaces. This is the single most-missed step.

    “Keyed by its dimension” is the whole load-bearing part (#3040). One event kind that carries many independent reads must NOT share one rate, or a healthy sibling hides a dead feed. Measured on #3042: kworb cut its US YouTube page from 100 rows to 20, the yt_us_weekly read failed on every attempt for a full day, and the monitor said nothing — because chart_fetch was one bucket for every kworb page. Over that window the pooled rate read 4.8% failing (28/578) against a 60% gate, while the dead feed alone read 92% (24/26). The telemetry was already perfect; only the bucketing was wrong. _PER_SOURCE_MISC and _PER_QUERY_MISC in scripts/ops_monitor.py are where a kind declares which dimension splits it, and health_query_key collapses opaque per-entity ids (track_<id>track) so the map stays bounded — 227 distinct chart_fetch query values over 7 days become 17 real feeds.

  4. Document it. Add the event row to the table above + the events.py docstring, and (if it changes what the QA routine watches) update the ops monitor section. The scripts/ops_monitor.py deterministic core is pure and unit-tested — add a test for any new finding/health branch.

The litmus test: if this surface started silently failing in production at 3am, would a graph or a flagged finding show it by the next ops-monitor run? If not, it isn’t instrumented yet.

Field coverage is part of the feature too. When you add or change an outbound integration, hand the model every answer-valuable field the upstream returns (the data-minimization rule governs telemetry + her replies, NOT the in-context input), and make each surfaced field self-describing in its rendered block or tool description (a legend/label, so the model knows what the value IS). docs/INTEGRATIONS.md is the per-API coverage ledger: it lists, for every integration, the fields we surface and the fields we deliberately omit (with the reason). The rule is 100% of the answer-valuable fields, or an explicit entry there saying what’s missing and why — keep it in sync when you touch an integration.

Railway dashboard queries: filter logs for the EVENT prefix, then parse the JSON suffix. Typical queries: count of event=command per minute, p95 of duration_ms where event=claude_api, sum of output_tokens where purpose=ask for cost tracking.

Axiom time series render: counts are BARS, everything else is a LINE (owner steer). scripts/axiom_setup.py sets the variant per panel automatically: a TimeSeries panel renders as bars when EVERY summarize output column is a bare count() or countif(...); a p99 (or any sum, percentile, gauge, or computed rate) keeps the default line. The variant rides on query.queryOptions.aggChartOpts – a JSON map keyed by the column descriptor ({"alias":"count_","op":"count"} for a bare count) valued {"variant":"bars"}. _bar_variant_opts derives it from the APL, so a NEW count panel gets bars with no extra wiring; tests/test_axiom_setup.py pins the count/line split by panel id.

Ops monitor (the single QA interface, full detail)

scripts/ops_monitor.py + .github/workflows/ops-monitor.yml are the bot’s one automated QA routine, running twice daily (cron: 0 8,20 * * *, + manual dispatch). A deterministic pass pulls the last ~12h of Railway EVENT logs (GraphQL, via RAILWAY_API_TOKEN/RAILWAY_SERVICE_ID) and flags both halves of QA:

The report also renders an unconditional Quality spot-check (recent posts + their self-gate scores per surface) so the judge eyeballs voice/quality even on an otherwise clean run — this is what makes it a quality eval, not just a regression alarm.

claude-code-action then judges the flagged samples and files deduped auto-eval issues (it searches open auto-eval issues first, so no spam; “All clear” files nothing). The deterministic core (parse/aggregate/evaluate/render) is pure and unit-tested in tests/test_ops_monitor.py; the Railway I/O is integration-only (pragma: no cover).

This replaces the standalone Railway log-monitor routine — error monitoring now lives here, one interface and one cadence. There is no separate log-monitor workflow.

deploy_failed / deploy_stale — the change that never landed (#2088)

The only ops-monitor finding that does NOT read the event stream, because the question it asks cannot be answered from there. Every other blackout check here reads what the bot EMITTED, so all of them assume the bot is running the code we think it is. A failed deploy breaks that assumption in the one way nothing else can see: main carries the change, Railway keeps serving the PREVIOUS build, and because that build works fine every surface looks healthy — no dark lane, no error, no latency blip. The change simply never happened and nothing said so. (_recent_deployment_ids was already asking Railway for status and discarding it.)

fetch_deploy_findings asks Railway what is actually running and compares it to the commit Actions checked out (GITHUB_SHA, which on the schedule is the default branch’s tip; git rev-parse HEAD is the local fallback). The pure core is deploy_health and is unit-tested.

finding fires when why it is separate
deploy_failed (high) the NEWEST deployment has status FAILED names the stage — no image digest means it died in the BUILD, which is a different fix from a crash or a failed healthcheck
deploy_stale (high) the live SUCCESS deployment’s commit is not the checked-out commit, and that commit is older than DEPLOY_STALE_AFTER_MIN (45m) catches a build failure Railway has already tidied away — the FAILED row can be gone while the old SUCCESS row still serves, so status alone reads clean. Only the COMMIT comparison sees it

Both fail OPEN: no Railway creds, a GraphQL error, or a git command that will not answer all return no findings. A deploy check that broke the ops run would be worse than the gap it closes. The 45m allowance exists because Railway builds in a few minutes — flagging a commit that landed seconds ago would make the check cry wolf twice a day.

Verified against live Railway both ways: silent when production served 005acd7 and main was 005acd7; fired with the correct detail when handed a main SHA production had not reached.

scope_unnamed — the newsroom’s chart list narrowing on us (#2127)

The newsroom used to relay whatever chart a trusted account posted about. It was the only chart surface with no chart list: the movement lane reads MUSIC_DESK_CHART_LANE, the boards read MUSIC_DESK_STANDINGS_CHARTS, the genre boards read billboard.GENRE_CHARTS, and the newsroom read the ACCOUNT and the number cue only. Over the 30 days to 2026-08-07 its 135 chart stories named about 20 chart families, including ARIA, the U.K. Official Albums Chart, Billboard Japan, Bubbling Under, Kid Albums, Top Movie Songs and a Dabeme fan tally of “Top 100 Male K-Pop Vocalists”. The owner reviewed that list and cut most of it; music_news.KEPT_PATTERNS / CUT_PATTERNS is the reviewed scope, applied by _chart_in_scope right after classify so an out-of-scope story costs no verify and no compose.

What this finding watches is NOT the cut. A drop that names its family (family=uk_official) is the owner’s decision working, and it never raises anything, however often it fires. The gate fails CLOSED, so it also drops a story naming no chart it recognizes, as family=unnamed — and that bucket is a different thing entirely: the patterns stopped matching a chart we meant to KEEP.

That failure is invisible to every other check here. It raises no exception. It fails no integration, so no OkRate moves. The story dies at the inbound edge, before the compose and before the self-gate, so surface_dark and low_quality — which read judge scores — see a normal week with slightly fewer posts. The desk simply gets quieter, and the quieter it gets the more it looks like a slow news week.

The finding fires at medium when unnamed is at least CHART_SCOPE_UNNAMED_RATE (25%) of at least CHART_SCOPE_MIN_DROPS (4) scope drops in the window. The floor is far above the measured baseline: 1 of 135 stories before the gate shipped, and that one was an artifact of the 120-character preview the measurement read (the live gate reads the claim plus the whole source post, so it names the chart more often). The minimum count is low on purpose — the gate drops roughly one story a day, so a higher bar would never fire.

The fix is a row in KEPT_PATTERNS, not a threshold change. Re-measure against real posts with python -m scripts.dryrun_chart_filter (it reads the last 30 days of shipped chart stories out of Axiom and prints kept / cut / unnamed with the unnamed lines in full). Dashboard panel: desk_chart_scope on the Content Desks board.