Skip to content

Carry forward _forget_mn_key and the KF kf_action diagnostics - #352

Merged
jehanazad merged 2 commits into
mainfrom
chore/carry-forget-mn-key-and-kf-diagnostics
Sep 10, 2026
Merged

jehanazad merged 2 commits into
mainfrom
chore/carry-forget-mn-key-and-kf-diagnostics

Conversation

@jehanazad

Copy link
Copy Markdown
Contributor

Why

#303 and #305 were closed on 2026-09-10: their policy changes (the orphan rule, the KF hold and uncertainty-sourced R) were measured against their own live captures and against what main did since (#310, #329, #331) and lost. Each carried one piece that is worth having regardless of policy. This is those two pieces, written against current main rather than rebased through the conflicts.

What

What this buys: a capture on today's main can say how each published dark position came about — how many bad joins on established keys arrive via manoeuvre_rescued vs reanchored vs smoothed — without flipping any policy. That is the measurement #305's cap20 could not make, and the one any future outlier policy should be designed from.

Verified

  • tests/test_track_filter.py — new TestKfActionStamp (4): every path stamped, the ten-kilometre re-anchor carries its breaching d2 and ~10 km innovation, the manoeuvre rescue is stamped as such with d2 > gate, off mode does not stamp.
  • tests/test_mn_lifetime.py — new TestForgetMnKey (2) and TestHistoryRecordKfAction (2).
  • Targeted set (test_track_filter test_mn_lifetime test_solver_anchor test_solver_stats test_dark_follow test_dark_key_track_continuity test_mlat_history test_feed_multinode test_known_track_hold test_known_track_follow): 405 passed, 1 skipped (Stone-Soup oracle, not installed locally), exit 0.
  • pre-commit run on the four changed files: all five hooks pass.
  • Submodule pins unchanged.

🤖 Generated with Claude Code

The two reusable pieces of the closed drafts #303 and #305, rebased onto
main without either PR's policy change.

From #303: `_forget_mn_key(old_key)` factors the four-store removal
(entry, anomaly hex, smoother position history, Kalman state) that the
supersession loop did inline. No behaviour change; the loop now calls it.

From #305: every return path of `track_filter._smooth_kf` stamps
`kf_action` / `kf_d2` / `kf_innov_m`, and `_record_solve_history` copies
them onto the history record. Extended for #310: a gate breach the
manoeuvre retry accepts is stamped `manoeuvre_rescued`, distinct from
`smoothed` and `reanchored`, and `kf_d2` is the base-Q surprise that
triggered the retry. `_predict_update` also returns the innovation
magnitude for the stamp. The dark-lane kf_held/kf_reanchored counters
from #305 are dropped: `filter_stats()` already counts re-anchors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@claude

This comment has been minimized.

@claude

claude Bot commented Sep 10, 2026

Copy link
Copy Markdown

Claude finished @jehanazad's task in 1m 23s —— View job


Todo

  • Gather context (PR diff, description, CLAUDE.md)
  • Read changed files (solver.py, track_filter.py, tests) against origin/main
  • Review _forget_mn_key extraction for behavior parity
  • Review kf_action/kf_d2/kf_innov_m stamping across _smooth_kf return paths
  • Review test coverage additions
  • Check contract/regeneration implications (CLAUDE.md gate)
  • Post final review

Summary

Re-reviewed against origin/main (this synchronize only merged in unrelated main history — git diff between the feature commit and HEAD on the four changed files is empty, so the substantive diff is unchanged from the last review). Confirms the same conclusion: both pieces do what the description says, no correctness issues found.

_forget_mn_key (backend/services/tasks/solver.py:1820-1836) — a straight extraction of the four-store removal (multinode_tracks, anomaly_hexes, _MN_POS_HISTORY, track_filter.drop_key) the supersession loop previously did inline. Verified the single call site (solver.py:3943) sits inside the with _MN_TRACKS_LOCK: block opened at solver.py:3769, matching the docstring's claim that the caller holds _MN_TRACKS_LOCK and that _MN_POS_HISTORY_LOCK is taken inside — consistent with the lock order documented at solver.py:3776-3778 and used everywhere else in the module. No behavior change.

kf_action/kf_d2/kf_innov_m stamping (backend/services/track_filter.py) — traced every return path of _smooth_kf:

  • init (first solve, gap re-init): stamped with d2=None, innov_m=None — nothing was computed.
  • passthrough (dt ≤ 0 dedup): same.
  • reanchored: stamped with the pre-branch (base-Q) d2_observed/innov_m — matches "a re-anchor carries the breaching d2."
  • manoeuvre_rescued/smoothed: action set on the retry-success branch, falls through to the single _stamp(smoothed, action, d2_observed, innov_m) at the bottom, still using the base-Q d2_observed — matches "a rescued update still shows the surprise."
  • off/ewma modes (track_filter.py:996-1003) return before ever calling _smooth_kf, so kf_action is absent for them and for rejects, consistent with _record_solve_history's r.get(...) copy in solver.py:2926-2929.

_predict_update's new fourth return value (innov_m) is correctly discarded (_) on the manoeuvre-retry call — only the base-Q attempt's innovation is stamped, matching the description.

TestsTestForgetMnKey (2) covers all-four-stores-cleared and unknown-key no-op. TestHistoryRecordKfAction (2) covers the published/reject history record. TestKfActionStamp (4) covers every _smooth_kf path, the ten-kilometre re-anchor's breaching d2/innovation, the manoeuvre rescue's base-Q d2 above gate, and off-mode's absence of the stamp. Assertions line up with the actual implementation (e.g. "smoother" not in out for the raw-passthrough re-anchor case).

CLAUDE.md gate — no /v1/nodes routes, models, or node_config.py bindings touched, so no contracts/nodes-v1.openapi.yaml regeneration is required.

Not independently verified: no pytest/pre-commit available in this sandbox (no approved tool access to invoke the interpreter or package manager), so this is static tracing of the diff, not a re-run of the suite. The PR body's reported results (405 passed, 1 skipped; all five pre-commit hooks pass) are plausible given the diff but unverified by me.

No changes requested.

@jehanazad
jehanazad merged commit f5810a0 into main Sep 10, 2026
16 checks passed
Babissimo added a commit that referenced this pull request Sep 14, 2026
…eploy

`deploy-staging` was the only job in the `staging-deploy` concurrency group.
`staging-smoke-tests` and `e2e-staging` carried no concurrency at all, so the
deploy was serialised while everything that verifies the deploy was not: a
following run's deploy was free to land between one run's smoke tests and its
E2E, restarting the box underneath a suite that was testing it.

Three consecutive merges failed that way on 2026-09-10 (#348, #351, #352), and
four runs within three minutes produced the same thing on 2026-08-21 (#223).
None of them failed for a fault in the change. The failure does not present as
a flake either: a test hitting a half-swapped application gets a specific,
plausible assertion failure, which reads as a content regression and costs
someone real time before they conclude otherwise. It also blocks releases, as a
red staging E2E skips `deploy-production`.

Adding the group to the two verification jobs does not close it. Concurrency is
acquired and released per job, so a deploy can still slot in between one run's
smoke tests and its E2E. The environment has to stay held across the sequence.

So the three jobs move to `staging-deploy-verify.yml` and ci.yml calls it from
a single `Staging` job that carries the group. A calling job is not complete
until every job in the called workflow has finished, so the group is held from
the first byte of the deploy to the last E2E assertion. The step bodies move
unchanged; only the job headers differ.

Two things the move forced:

- `env` does not cross a `workflow_call` boundary and `with:` cannot read the
  `env` context, so APP_DIR is re-exported as a `changes` output rather than
  restated in the new file, keeping one definition of the deploy directory.
- `playwright-image` joins the staging job's prerequisites. An unpublished
  image now fails the run before the droplet is touched rather than after a
  deploy and a smoke suite have landed on it.

Two comments that named the old job IDs move with them: the DNS-tolerance
notes in frontend/e2e/dashboard.spec.ts and deploy/staging-smoke-test.sh both
explained themselves by their job being a direct `needs:` of deploy-production,
which is now indirect through the calling job.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to 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.

1 participant