Skip to content

(MOT-4217) fix(harness): let reaction sessions unregister their run's subscriptions - #586

Merged
andersonleal merged 5 commits into
mainfrom
fix/react-lineage-unregister
Jul 24, 2026
Merged

(MOT-4217) fix(harness): let reaction sessions unregister their run's subscriptions#586
andersonleal merged 5 commits into
mainfrom
fix/react-lineage-unregister

Conversation

@andersonleal

Copy link
Copy Markdown
Collaborator

Why

rctest5-K7mQ ended in a cleanup deadlock. The repair reactor tried to tear down the run's subscriptions and hit subscription belongs to a different session ×3: engine::unregister_trigger is owner-session-scoped, and the owner — the orchestrator — was parked waiting on a report state notification that its own children could no longer satisfy. Result: 3 leaked armed subscriptions and a permanently sleeping session.

A reaction cleaning up its own run is the legitimate teardown path when the registrant is parked.

What

  • spawn_reaction records child session → registrant session lineage (from the trusted __owner_session_id stamp) in the ephemeral SubscriptionRegistry.
  • The unregister ownership check accepts the owner itself or any session whose lineage chain reaches the owner (transitive, hop-bounded so re-targeted-session cycles terminate).
  • Lineage entries are purged on session::deleted in both directions, keeping the map bounded by live sessions.

Deliberately narrow: strangers and the reverse direction (registrant "cleaning up" a child's subs it doesn't own) are still refused.

Tests

  • transitive grant (orchestrator → reactor → repair), reverse/stranger refusal
  • self-edge ignored; A↔B cycle terminates without granting unrelated ownership
  • forget_lineage purges entries pointing at and from the deleted session

267 harness tests pass; clippy clean.

Closes MOT-4217.

@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview, Comment Jul 24, 2026 1:43pm
workers-tech-spec Ready Ready Preview, Comment Jul 24, 2026 1:43pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@andersonleal, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 545018c1-0518-4ecd-b403-2716ba348087

📥 Commits

Reviewing files that changed from the base of the PR and between e7c581f and e01db52.

📒 Files selected for processing (14)
  • harness/src/functions/on_session_deleted.rs
  • harness/src/functions/react.rs
  • harness/src/functions/subscribe.rs
  • harness/src/subscriptions/registry.rs
  • harness/tests/e2e/README.md
  • harness/tests/e2e/src/fixtures/loading.rs
  • harness/tests/e2e/src/fixtures/tests.rs
  • harness/tests/e2e/src/scenario/phases/completion.rs
  • harness/tests/e2e/src/scenarios/dsl.rs
  • harness/tests/e2e/src/scenarios/late_join_replay.rs
  • harness/tests/e2e/src/scenarios/mod.rs
  • harness/tests/e2e/src/scenarios/reaction_unregisters_run.rs
  • harness/tests/e2e/src/scripted_router.rs
  • harness/tests/e2e/src/types/script.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/react-lineage-unregister

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 48 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

@andersonleal

Copy link
Copy Markdown
Collaborator Author

Restacked onto feat/react-fire-coalescing (PR #583) and extended with the e2e that gates this fix: E2E-008 reaction-unregisters-run — a standing binding's reaction, pinned to a separate session, unregisters the registrant's subscription; the router matcher demands the unregister result with is_error: false and removed: true. Reverting the ownership check to pre-lineage semantics times the scenario out; with the fix it passes (all 8 direct scenarios green). Also adds the router's serve-time capture primitive ([[cap:…]] — required because the sub_… id is runtime-generated) and an armed_wake_advisory on one-shot state-key wakes (the rctest5 orchestrator-parked-forever wiring gap).

@andersonleal

Copy link
Copy Markdown
Collaborator Author

Third commit: level-triggered join predecessors (+ E2E-009 late-join-predecessor-replay).

Live rctest5 iteration exposed the registration-vs-completion race: a rejected divergent sibling (MOT-4210's check, working as designed) forced a re-registration round, the watched writer sessions finished inside that window, and the re-armed edge-triggered turn-completed join predecessors starved forever — finalizer never spawned, run parked.

  • Registering a join predecessor on a turn-completed binding whose filtered session is already terminally completed (durable status completed, no armed wake) now delivers a catch-up fire shaped like the real completion event, stamped __late_subscription_replay. Joins only (per-key accumulator makes rare double-delivery idempotent); fail-open on status-lookup failure.
  • E2E-009 proves the completion is strictly in the past via a call-mode completion witness (recorder call feat: add sandbox-docker and sandbox-firecracker workers #1), then a probe-steered turn registers the late join — the joined downstream's recorder call feat: add proof worker — AI-powered browser testing #2 exists only through the replay. Gate check: disabling the replay times the scenario out. Runner gains ProbeAction.after_target_calls.

Live verification: with this stack deployed (this PR + #583 + #585), the rctest5 orchestration prompt now completes fully green end to end — self-verifying pass report, totals == GROUP BY (5/5/5), 15/15 events, cited trigger-spawned reactor session, zero leaked subscriptions, autonomous cleanup. All 9 direct e2e scenarios pass.

… subscriptions

rctest5-K7mQ ended deadlocked: the repair reactor hit `subscription
belongs to a different session` trying to clean up the run, because
unregistration is owner-session-scoped and the owner — the orchestrator
— was parked waiting on a report notification its children could no
longer satisfy. Three armed subscriptions leaked.

Reaction spawns now record child → registrant lineage in the ephemeral
subscription registry, and the unregister ownership check accepts any
session whose lineage chain reaches the owner (transitively, hop-bounded
against re-targeted-session cycles). Lineage entries are purged on
session::deleted along with the session's subscriptions.
…e captures; armed-wake advisory

Three pieces from the rctest5 live-run iteration:

* Router serve-time captures: a generation can capture a runtime value
  (regex over its matched request) and later frames echo it via
  [[cap:name]] — the only way a static fixture can call
  engine::unregister_trigger with a runtime-generated sub_… id.
  Validation requires declared-before-referenced and a capture group.

* E2E-008 reaction-unregisters-run: a STANDING binding's reaction, pinned
  to a separate session, unregisters the registrant's subscription. The
  gate is gen4's matcher demanding the unregister function_result with
  is_error:false AND removed:true (once:false is load-bearing — a
  one-shot binding retires itself first and the check never runs). The
  await phase now also drains the script after the awaited target call,
  so collection can't race an untracked session's turn tail. Gate check:
  reverting the ownership check to pre-lineage semantics times the
  scenario out.

* armed_wake_advisory: a one-shot state-key wake registration now warns
  that nothing fires it automatically and the session sleeps forever
  unless a registered task explicitly sets that scope/key — the exact
  wiring gap that left the rctest5 orchestrator parked with its cleanup
  pending after every row landed correctly.
rctest5 attempt 4 exposed the registration-vs-completion race: a
rejected sibling (the MOT-4210 divergent-spec check, working as
designed) forced a re-registration round, the watched writer sessions
finished inside that window, and the re-armed edge-triggered
turn-completed join predecessors starved forever — finalizer never
spawned, report never written, orchestrator parked.

A completion BARRIER is only correct level-triggered: registering a
join predecessor on a turn-completed binding whose filtered session is
ALREADY terminally completed (durable status completed, no armed wake)
now delivers a catch-up fire shaped like the real completion event,
stamped __late_subscription_replay. Joins only — their per-key
accumulator makes a rare double-delivery idempotent. Fail-open: a
status lookup failure keeps edge semantics. The registration response
carries a note naming the replay.

E2E-009 late-join-predecessor-replay gates it end to end: a worker
session completes (proved past via a call-mode completion witness —
recorder call #1), then a probe-steered turn registers the late join;
the joined downstream's recorder call #2 exists only through the
replay. Gate check: disabling the replay times the scenario out.
Runner: ProbeAction gains after_target_calls (an untracked session's
only observable milestone is a controlled-function call).

Verified live: rctest5 attempt 5 completed fully green — report row
self-verifying pass, totals == GROUP BY (5/5/5), 15/15 events, cited
trigger-spawned reactor, zero leaked subscriptions, no polling.
…alls

Same authoring-mistake class CodeRabbit flagged on the await knob: a
zero after_target_calls fails at runtime and a positive one without a
controlled function waits until the deadline burns. Reject both in
ScenarioFixture::validate.
@andersonleal
andersonleal force-pushed the fix/react-lineage-unregister branch from 07cfbb5 to e01db52 Compare July 24, 2026 13:42
@andersonleal
andersonleal merged commit 0003636 into main Jul 24, 2026
16 checks passed
@andersonleal
andersonleal deleted the fix/react-lineage-unregister branch July 24, 2026 14:03
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