fix(daemon): say why a no-effect claim was withheld - #1655
Conversation
A vetoed gestureNoEffect claim is invisible from outside: the response looks exactly like a gesture that worked. #1620 spent weeks unable to tell a cross-backend pair from capture drift from real movement for precisely this reason, and its own suggested next step was to log the corroboration inputs and re-run. Every veto on an accept-stale verdict now emits post_gesture_no_effect_vetoed with a reason: baseline_rebased (the pair is cross-backend, #1569) or surface_divergence carrying onlyInBaseline/onlyInCurrent/rectMismatched/shared from a new summarizeDiscriminatingSurfaceDivergence — one-sided keys are membership drift, rectMismatched is movement. Measured live against the seeded Bluesky fixture (167 nodes, depth-56, private-ax): inert scrolls corroborate and the warning fires 2/2, successful gestures never claim 2/2, zero rebases, and the one veto observed was rectMismatched:4 shared:158 onlyIn*:0 from ambient feed motion over an artificial 105s gap. #1620's truncation-drift half does not reproduce; membership is stable under the remembered depth cap. Also: marking no longer stores an empty baseline signature. '[]' is truthy, so an invented baseline was being rebased onto a post-gesture capture; 'no usable baseline' now has one representation, matching markPendingInteractionOutcome.
Size Report
Startup median (7 runs, lower is better):
Top changed chunks:
|
|
Reviewed exact head P2 — assert the diagnostic payload, not only that an event occurred. The new operator-facing behavior is the reason and divergence evidence carried by
Use the existing diagnostic scope/log helpers so the assertion pins the emitted NDJSON payload, not merely the pure summarizer. The empty-baseline fix and claim-preserving veto logic otherwise look sound. The current Select affected failure is an action-download 500/503 infrastructure flake; substantive CI is still pending. |
The claim tests counted `post_gesture_no_effect_vetoed` events by phase. The operator-facing behavior this PR adds is the *reason* and the divergence counts, and both survive an event tally: emitting the wrong reason, or dropping the counts entirely, keeps every count at 1. The four veto tests now assert the whole emitted payload, read back out of a diagnostics trace file so the assertion pins the serialized NDJSON line rather than an in-memory event. Backend rebase pins `reason: baseline_rebased` with no counts riding along (the pair is cross-backend; there is nothing comparable to count over). Both divergence cases pin `reason: surface_divergence` with exact onlyInBaseline/onlyInCurrent/rectMismatched/shared — the numbers that separate scope drift from movement, and the two membership-drift directions from each other. Verified red both ways the reviewer named: dropping the counts fails 2 tests, forcing the reason to surface_divergence fails 2. The old count assertions stayed green under both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Rva4YGtSCAKJqH5PbpcCU
|
Fixed at The four veto tests now assert the whole emitted payload, read back out of a diagnostics trace file rather than an in-memory array, so what is pinned is the serialized NDJSON line an operator actually reads (redaction included), via the existing
The last two are the point of asserting numbers rather than a reason string: they are the same veto with opposite drift direction, and an event tally cannot tell them apart. Red both ways you named: The old count assertions stayed green under both. One thing the payload assertions surfaced that I'd have missed: the scope-drift capture carries no
Generated by Claude Code |
|
Re-reviewed exact head 5c54ce3. The prior P2 is fixed: the production-path tests now read the serialized diagnostics trace and pin baseline_rebased separately from surface_divergence, including exact one-sided membership and rect-mismatch counts. The focused claim suite passes 7/7 and the full TypeScript build typecheck passes locally. The live Bluesky artifact remains applicable because the new commit changes tests only. No new code findings; the PR is merge-ready from review. CodeQL is queued, with no confirmed CI failure or conflict. |
Closes #1620. Also supplies the live-evidence artifact carried over from #1615.
The finding
#1620's premise does not hold: the warning fires. Measured on the seeded Bluesky fixture and on Settings, built from this branch. Neither hypothesised veto reproduces — the backend flip never occurs on a fully-hostile screen (the tree strategy fails before the first capture, so every capture including the baseline is already
private-ax, and there is nothing to flip), and truncation drift did not appear in a single sample: every veto observed hadonlyInBaseline: 0, onlyInCurrent: 0. Membership is stable under the remembered depth cap; only rects ever diverged.So this PR does not change the corroboration rule. It closes the reason the question stayed open for weeks: a withheld claim was indistinguishable from a gesture that simply worked — both render as a response with no warning. Every veto on an accept-stale verdict now says why.
The artifact (#1615 carry-over)
The exact repro from #1620's description, on this head:
Coordinates are retained, which is #1615's contribution — before it, the numeric-token heuristic reduced this to a contentless
swipe produced no visible change. That is the full condition #1615 was held on: a no-effect swipe warning retaining its coordinates, followed by a successful command proving the session remains usable.What the instrument showed
That is a correct veto, and it is only legible because of this PR: on Settings the drag rubber-banded and left exactly one rect of 103 shifted past tolerance. A control run — two idle snapshots, no gesture — showed 0 moved rects, confirming the screen is stable and the movement was gesture-caused rather than ambient. The other veto seen (Bluesky,
rectMismatched: 4 / shared: 158) came from feed content updating across a deliberately unrealistic 105-second gesture→capture gap.Both are the rule working as specified. Neither was diagnosable before.
Changes
post_gesture_no_effect_vetoedon every withheld claim, withreason: baseline_rebased | surface_divergence, and for divergence the counts above via a new puresummarizeDiscriminatingSurfaceDivergence— one-sided keys are membership drift,rectMismatchedis movement. This is what produced every number in this PR.[]is truthy, so a session with no pre-gesture snapshot had an invented baseline that the loop would then rebase onto a post-gesture capture. "No usable baseline" now has one representation — absent — matchingmarkPendingInteractionOutcome's existing rule.post-gesture-no-effect-claim.test.ts(fix(daemon): distrust post-gesture stability that matches the pre-gesture baseline #1563 convention), keeping the loop file at 525 lines.#1615's updated wording assertions move with them.No behaviour change to the verdict or the corroboration bar.
Verification
typecheck/lint/format:check/check:layeringgreen. Daemon suite 95 files / 711 tests. The five directly-related suites total 60 tests.check:affected --runis green except the documented provider-integration contention flake — three consecutive runs produced three different timeout sets ({android-lifecycle},{android-lifecycle, android-recording},{doctor}), each passing in isolation. All are Android; this change cannot reach Android at all, sincerequiresPostGestureBaselineDistrustis Apple-only and every new path is behind an accept-stale verdict that needs a baseline.Correction to my earlier comment on #1620
I attributed some earlier missing diagnostics to daemon takeover, using
daemon_startupin the client's--debugstream as the tell. That was wrong:daemon_startupis emitted on essentially every client request, including ones where the warning fired normally. The real discriminator in the corrupted samples wasios_runner_session_invalidated— the XCTest runner dying mid-capture (** TEST EXECUTE FAILED **indaemon.log), which loses the pending record. Cadence still matters when measuring, but for that reason rather than the one I gave.