Skip to content

Emit the anchor-bias heartbeat from the round loop with an explicit anchored:false reason #1078

Description

@pm25coder

Emit the anchor-bias heartbeat from the round loop with an explicit anchored: false reason, so a skipped round is distinguishable from a detector that never saw it

Source

Reader comment 3dn6k by vinhnguyenthanhdn on "A guard that has never fired and a guard that stopped running look identical on disk" (https://dev.to/pm25coder/a-guard-that-has-never-fired-and-a-guard-that-stopped-running-look-identical-on-disk-177f), 2026-08-30T05:53Z.

Reader's insight

The anchor-bias-heartbeat (added in #1073, consumed by #1076's calibration) is emitted per anchored round FROM INSIDE _detect_silent_anchor_drift — so anything that stops anchored rounds entirely yields zero heartbeat lines, byte-identical to the detector having stopped:

"that is the same ambiguity you removed one level down with anchor_loss, reappearing one level up at the call site."

Current state (master 981f037)

  • Heartbeat logged at emrg/server/daemon.py:3128-3135, inside _detect_silent_anchor_drift (L3094) — AFTER the early returns at L3114 (if not old or old[1] <= 0 or estimate <= 0: return) and L3119 (if old_bias <= 0: return).
  • Call site at L2594-2606: if final_usage:pt = final_usage.get("prompt_tokens")if pt:_detect_silent_anchor_drift(session, pt, estimate). The detector is only reached when the LLM response carries prompt_tokens AND a previous anchor exists.
  • Failure modes that go fully silent:
    • final_usage missing/empty (config flag, upstream change, stream edge case) → detector never called → no heartbeat;
    • prompt_tokens missing/zero → same;
    • no anchor yet / anchor invalid (first round, post-compact, post-switch, provider stopped returning usage) → early return before the heartbeat.
  • _warn_missing_usage_anchor (L2998-3050) covers only the auto-compact projection gate, fires once per session, and does not emit the drift heartbeat.

Proposed change

Move the heartbeat to the round loop so every round emits exactly one anchor-bias-heartbeat line with an explicit state:

  1. Keep the existing inside-detector heartbeat as the anchored: true case (it already logs bias_shift / threshold / old_bias / new_bias).
  2. In the round loop at the usage-processing point (L2594-2606), log anchored: false with a reason when the detector is skipped:
    • reason=no_usagefinal_usage missing/empty;
    • reason=no_prompt_tokensfinal_usage present but prompt_tokens missing/zero;
    • reason=no_anchor — no previous anchor (not old or old[1] <= 0);
    • reason=invalid_estimateestimate <= 0;
    • reason=invalid_biasold_bias <= 0 (kept as-is, reachable only via the detector's early return).
  3. Keep the greppable marker anchor-bias-heartbeat and debug level unchanged so existing grep "anchor-bias-heartbeat" workflows keep working; only the reason field is new.

This preserves "log unconditionally, alert conditionally" one level higher: a skipped round is now a first-class, labeled observation instead of an absence.

Acceptance (self-verifiable)

  • A round with no final_usage emits anchor-bias-heartbeat ... anchored=false reason=no_usage.
  • A round with final_usage but no prompt_tokens emits anchored=false reason=no_prompt_tokens.
  • A round with no previous anchor emits anchored=false reason=no_anchor.
  • A normal anchored round still emits anchored=true with bias_shift (existing behavior unchanged).
  • Tests assert each state (positive + negative per the repo's verification rules); CI test + test-windows green.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions