Skip to content

iter-137: daemon-mode parity — frame targets, RPC queueing, honest timeouts - #176

Merged
ractive merged 2 commits into
mainfrom
iter-137/daemon-mode-parity
Aug 9, 2026
Merged

iter-137: daemon-mode parity — frame targets, RPC queueing, honest timeouts#176
ractive merged 2 commits into
mainfrom
iter-137/daemon-mode-parity

Conversation

@ractive

@ractive ractive commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

Everything iteration 129 shipped worked only with --no-daemon. In the default connection mode frame enumeration returned zero targets, so click --frame, the cross-origin frame scan and consent accept all silently degraded. It went green because every iter-129 live test passed --no-daemon — the tests and the iteration's own dogfood_path disagreed, and the tests won.

  • Theme A — frame targets through the daemon proxy. The plan's stated root cause (an event sink installed too late) was wrong; two real causes, both confirmed on the wire against Firefox 153:

    1. watchTargets is not repeatable on a connection — ParentProcessWatcherRegistry.watchTargets only adds the target type to the watcher's session data, so the daemon (which subscribes once at startup) makes every proxied client's watchTargets("frame") a no-op and its drain window empty by construction. Without isServerTargetSwitchingEnabled: true the daemon received no target-available-form at all (daemon status reported target_count: 0 for whole sessions).
    2. navigate was tearing the daemon's subscription down — its three unwatchTargets("frame") teardown calls landed on the shared connection, and under server-side target switching unwatchTargets destroys every target, top level included. Captured in the daemon log: 2 target-available-form immediately followed by 4 target-destroyed-form after each navigation.

    Fix: the daemon records every raw target form (SharedState::frame_targets), serves it over a new {"to":"daemon","type":"frame-targets"} request, and drops client unwatchTargets frames (is_client_target_teardown — safe, the method is oneway). The CLI replays the packets through the new ff_rdp_core::target_events_from_packets (the same add/replace/remove rules the direct drain uses) from one shared entry point, commands/frame_targets.rs::fetch_frame_targets, that click and consent both call.

  • Theme B — concurrency. claim_rpc_slot_queued replaces iter-101's instant refusal with a Condvar queue (RPC_QUEUE_BUDGET, 2 s daemon-queued keep-alives so a waiting client's socket read window keeps restarting); daemon_busy now reports waited_ms and names the cap. AppError::from(ProtocolError::Timeout) reports the socket read deadline instead of a fabricated after_ms: 0, and Display never renders a zero as an elapsed duration.

  • Theme C — network source. New network --source auto|watcher|performance-api pins the capture source so both connection modes return the same rows; meta.source_reason is always present and states which rule applied. --since with performance-api is refused (since_requires_watcher_source) rather than silently ignored.

  • Theme D — the discipline hole. tests/no_daemon_live_test_guard.rs fails any live suite that uses --no-daemon without a //! daemon-parity: declaration, with a shrink-only grandfather list. It caught this iteration's own new suite before it was annotated.

Observability added along the way: daemon status gains live_target_count (targets alive now, vs. the cumulative target_count — the two diverging is this bug's signature), and the frame-targets reply carries watcher_ready so an unestablished subscription returns daemon_watcher_not_ready instead of an empty snapshot presented as fact.

Test plan

  • cargo fmt, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace -q — all clean
  • cargo run -p xtask -- check-iteration-ready --plan kb/iterations/iteration-137-daemon-mode-parity.md --base origin/main — 10/10 PASS (with FF_RDP_LIVE_TESTS=1); check-daemon-locks and check-oneway-conformance also pass
  • live_137_frame_targets_via_daemon — 2 frames via daemon and via --no-daemon, equal
  • live_137_click_cross_origin_via_daemontag: "A", meta.frame_url: "https://example.com/", no --no-daemon
  • live_137_concurrent_commands — 4/4 concurrent proxied commands succeed, no 0ms duration in any output
  • live_137_network_source_paritymeta.source, meta.source_reason and row count identical in both modes with --source performance-api
  • live_137_consent_accept_via_daemon (network-gated) — verified manually against theguardian.com: {"cmp":"sourcepoint","action":"accepted"} immediately after navigate, without --no-daemon
  • iteration-129 dogfood_path re-run verbatim: consent accept{"cmp":"sourcepoint","action":"accepted"}, click --frame → 3 frames listed (was 0 frame(s) available: ), 4/4 concurrent page-text
  • Full CLI live suite FF_RDP_LIVE_TESTS=1 cargo test -p ff-rdp-cli --test live -- --include-ignored --test-threads=1170 passed, 0 failed (regression check for enabling server-side target switching on the daemon)
  • Core live suite FF_RDP_LIVE_TESTS=1 cargo test -p ff-rdp-core -- --include-ignored --test-threads=1 — green (running it in parallel shows pre-existing cross-test interference, unrelated to this branch)
  • 12 new unit tests: daemon snapshot lifecycle/prune/request, replay-rule parity, RPC queue hand-off and refusal honesty, timeout non-zero rendering, and the three Theme D guard tests

Spec drift

None. The only crates/ff-rdp-core/src/actors/*.rs change is target_events_from_packets, a pure function over packets already received — it issues no RDP requests and introduces no undeclared spec fields.

🤖 Generated with Claude Code## Claims vs code
<generated 2026-08-09T22:08:22Z by ralph-loop>

  • the → ✅ matched in diff
  • SharedState::frame_targets → ✅ matched in diff
  • AppError::from → ✅ matched in diff
  • ProtocolError::Timeout → ✅ matched in diff
  • RPC_QUEUE_BUDGET → ✅ matched in diff
  • frame-target → ✅ matched in diff
  • target-available-form → ✅ matched in diff
  • frame-targets → ✅ matched in diff

ractive and others added 2 commits August 10, 2026 00:06
…nned network source

Everything iteration 129 shipped worked only with --no-daemon. In the default
connection mode frame enumeration returned zero targets, so `click --frame`,
the cross-origin frame scan and `consent accept` all silently degraded. Every
iter-129 live test passed --no-daemon, so nothing caught it.

Theme A — frame-target enumeration through the daemon proxy.
The plan's stated root cause (event sink installed too late) was wrong. Two
real causes, both confirmed on the wire against Firefox 153:

  1. `watchTargets` is not repeatable on a connection.
     ParentProcessWatcherRegistry.watchTargets only adds the target type to
     the watcher's session data, so the daemon — which subscribes once at
     startup — makes every proxied client's watchTargets("frame") a no-op and
     the drain window empty by construction. Without
     isServerTargetSwitchingEnabled: true the daemon also received no
     target-available-form at all (`daemon status` reported target_count: 0).
  2. `navigate` was tearing the daemon's subscription down. Its three
     unwatchTargets("frame") teardown calls landed on the shared connection,
     and under server-side target switching unwatchTargets destroys every
     target, top level included: two available forms followed by four
     destroyed forms after each navigation.

Fix: the daemon requests its watcher with server-side target switching on,
records every raw target form in SharedState::frame_targets (deduped by actor
id, removed on destroy, cleared on a real top-level switch), serves it over a
new {"to":"daemon","type":"frame-targets"} request, and drops client
unwatchTargets frames (is_client_target_teardown — safe, the method is oneway).
The CLI replays the packets through the new
ff_rdp_core::target_events_from_packets, the same add/replace/remove rules the
direct drain applies, from one shared entry point
(commands/frame_targets.rs::fetch_frame_targets) that click and consent both
use. `daemon status` gains live_target_count and the frame-targets reply
carries watcher_ready, so an unestablished subscription reports
daemon_watcher_not_ready instead of "0 frames".

Theme B — concurrency. claim_rpc_slot_queued replaces iter-101's instant
refusal with a Condvar queue (RPC_QUEUE_BUDGET, 2s daemon-queued keep-alives
so a waiting client's socket read window keeps restarting); daemon_busy now
reports waited_ms and names the cap. AppError::from(ProtocolError::Timeout)
reports the socket read deadline instead of a fabricated after_ms: 0, and
Display never renders a zero as an elapsed duration.

Theme C — `network --source auto|watcher|performance-api` pins the capture
source so both connection modes return the same rows; meta.source_reason is
always present and states which rule applied. --since with performance-api is
refused (since_requires_watcher_source) rather than silently ignored.

Theme D — tests/no_daemon_live_test_guard.rs fails any live suite that uses
--no-daemon without a `//! daemon-parity:` declaration, with a shrink-only
grandfather list. It caught this iteration's own new suite.

Verified live on Firefox 153: iteration-129's dogfood_path passes as written
(consent accept -> {"cmp":"sourcepoint","action":"accepted"} without
--no-daemon, click --frame lists 3 frames, 4/4 concurrent page-text). Full CLI
live suite 170/170 and core live suite green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All acceptance criteria landed and are verified against the diff
(ac-fidelity-check passes); no code changes needed from /review-pr —
the daemon-mode-parity implementation held up under a full local
review of the RPC-queueing, frame-target snapshotting, and network
source-selection logic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ractive
ractive merged commit f42b12b into main Aug 9, 2026
10 checks passed
@ractive
ractive deleted the iter-137/daemon-mode-parity branch August 9, 2026 22:19
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