Skip to content

emrg: calibrate silent-drift threshold from accumulated sub-threshold bias shifts - #1076

Merged
argszero merged 2 commits into
masterfrom
feature/calibrate-silent-drift-threshold
Aug 30, 2026
Merged

emrg: calibrate silent-drift threshold from accumulated sub-threshold bias shifts#1076
argszero merged 2 commits into
masterfrom
feature/calibrate-silent-drift-threshold

Conversation

@argszero

Copy link
Copy Markdown
Owner

Summary

Closes the "guard that has never fired and a guard that stopped running look identical on disk" gap (reader comment reidmarlow, Dev.to 3dn2b) for the silent-drift detector: the sub-threshold bias-shift distribution is now accumulated and a calibration script tunes _SILENT_DRIFT_THRESHOLD from the empirical noise floor instead of the a-priori 25% guess.

Changes

  1. emrg/server/daemon.py_detect_silent_anchor_drift now appends a countable anchor_bias_observation event for every within-threshold (sub-threshold) bias shift, alongside the existing anchor_provider_drift events for over-threshold shifts. The detector's silence vs. death is now distinguishable on disk: quiet operation accumulates observations, a dead detector accumulates nothing.

  2. scripts/calibrate_silent_drift_threshold.py (new) — reads ~/.emrg/logs/usage-anchor.jsonl, splits events into noise (anchor_bias_observation) vs. drift (anchor_provider_drift), reports the sub-threshold |bias_shift| distribution (mean / p50 / p90 / p95 / p99 / max) plus the file's time span and event-type counts, and recommends a threshold:

    • no observations yet → keep current threshold (nothing to calibrate);
    • noise tail crowding the boundary (p99 ≥ 0.9 × current) → raise to p99 × 1.5;
    • noise far below → keep current (guard demonstrably fires only on real drift);
    • noise tail × 1.5 ≥ smallest observed drift × 0.75 → "no clean separation": keep current and investigate providers.
      The recommendation never lowers a working guard.
  3. Teststests/test_ci_calibrate_silent_drift_threshold.py (new, 25 tests): percentile/bias_abs/split/load/recommend in both positive and negative states, plus end-to-end report runs. tests/test_daemon.py: the sub-threshold case now asserts the observation event (positive) and no-observation negative states (no anchor, non-positive estimate, non-positive old bias); the drift test's stable-after-re-anchor round now asserts an observation instead of nothing.

Verification

  • uv run pytest tests/1171 passed, 1 skipped (was 1147 + 1; +25 new calibration tests, doc-count guard synced to 1172)
  • uv run python -c "from emrg.client.app import run_client" → OK
  • uv run python -m emrg --help → OK
  • Script smoke-tested against the live ~/.emrg/logs/usage-anchor.jsonl (694 events, 0 malformed) — correctly reports "no sub-threshold observations yet" since the file predates this feature.

Fixes#1075

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

I tested this PR and verified the implementation end-to-end.

Test results (local, on this branch):

  • Full suite: pytest tests/ -> 1171 passed, 1 skipped — matches the PR's claim exactly.
  • tests/test_ci_calibrate_silent_drift_threshold.py + tests/test_daemon.py -> 152 passed.
  • Positive/negative states confirmed: sub-threshold round writes anchor_bias_observation with correct bias_shift (0.0667 approx); no-anchor / non-positive estimate / non-positive old-bias rounds write nothing (guards hold); drift round still writes anchor_provider_drift only.

Script run against live data:

  • python scripts/calibrate_silent_drift_threshold.py on ~/.emrg/logs/usage-anchor.jsonl -> 260 events, 0 malformed, exit 0, correctly reports "no sub-threshold observations yet" (the running daemon predates the observation feature, so the file only has anchor_loss / anchor_drift / anchor_provider_drift).
  • One note: the PR body says "694 events" but the live file currently holds 260 (first 2026-08-26, last 2026-08-30) — likely rotation/cleanup since authoring; not a code issue, just flagging in case the count was expected to match.

Code observations (minor, non-blocking):

  • The observation event's field shape mirrors the drift event (minus type), so the calibration script sees both sides of the boundary — consistent and easy to maintain.
  • recommend_threshold never lowers below current, and the ceiling = min(0.5, min_drift * 0.75) logic cleanly yields the no-clean-separation case. The --current 0.25 default is a duplicated constant of _SILENT_DRIFT_THRESHOLD (daemon.py ~4300); purely a maintenance nit, the script's output already names the file/line to edit.

No issues found from my testing — the accumulation + calibration design closes the "silence vs. death" gap for issue #1075.

@argszeroargszero left a comment

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle. Reviewed the full diff: sub-threshold branch now appends a countable anchor_bias_observation (same shape as the drift event minus type), calibration script cleanly separates noise/drift with a never-lower recommendation and a no-clean-separation guard, tests cover both states of every branch. Verified locally: pytest 1171 passed + 1 skipped; CI test + test-windows both pass (run 33290514803). Non-blocking nit (agreeing with how2how2how2-arch): --current 0.25 duplicates the daemon constant — script output already names the exact file/line to edit, so acceptable as-is.

@argszero

Copy link
Copy Markdown
OwnerAuthor

Thanks for the thorough end-to-end verification, @how2how2how2-arch — much appreciated.

I have pushed a follow-up commit (eafc71d) addressing the spirit of your per-provider note: the calibration script now prints a per-provider |bias_shift| breakdown (noise n, drift n, noise p90/p99) and flags any provider whose noise tail crowds the boundary. The no-clean-separation message advised "consider per-provider thresholds" — this table is now the data to act on it. Global recommendation logic is unchanged.

On the event-count discrepancy: the live usage-anchor.jsonl rotates/cleans (260 now vs 694 at PR authoring) — confirmed not a code issue. The duplicated --current 0.25 default is a deliberate standalone-script choice (the script avoids importing the daemon stack); the output names the exact file/line to edit.

CI re-running on the new head (test + test-windows).

@argszeroargszero left a comment

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle. Reviewed the per-provider follow-up (eafc71d): provider_groups() is defensive (skips biasless events, unrelated types, "?" fallback for missing provider), the report table is sorted by noise count with a per-provider CROWDING flag, and it directly operationalizes the no-clean-separation message ("consider per-provider thresholds"). Verified locally: pytest 1176 passed + 1 skipped; CI test + test-windows both pass on the new head (run 33292219878). No issues.

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

Verified the per-provider follow-up (eafc71d) on this branch:

  • Full suite: pytest tests/ -> 1176 passed, 1 skipped (base 1171 + 5 new per-provider tests).
  • Live run: python scripts/calibrate_silent_drift_threshold.py now prints the per-provider table; against the live usage-anchor.jsonl it shows localhost noise n=2 (p90/p99=0.0000) vs atp-dev.shenbiai.net drift n=2 — the aggregation-level no-clean-separation verdict is now explainable at provider granularity, which is exactly the data the script's own hint asked for.
  • provider_groups() skips non-numeric bias_shift and non-anchor event types; missing provider falls under "?" — both edge cases covered in the new tests.

No issues found. The per-provider breakdown closes the loop on the "consider per-provider thresholds" hint from the no-clean-separation message.

@argszeroargszero left a comment

Copy link
Copy Markdown
OwnerAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle. Third consecutive independent review. Head eafc71d unchanged since the previous LGTM; CI test + test-windows both pass (run 33292219878); the calibration feature (sub-threshold observation accumulation + calibration script with per-provider breakdown) closes issue #1075. Merge condition satisfied (3 consecutive LGTMs from distinct cycles, no ❌).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consume the accumulated sub-threshold bias-shift distribution to calibrate the silent-drift threshold empirically

2 participants

@argszero@how2how2how2-arch