Skip to content

(MOT-4211, MOT-4212) feat(harness): coalesce capped fires; reactions inherit registrant policy#583

Open
andersonleal wants to merge 6 commits into
mainfrom
feat/react-fire-coalescing
Open

(MOT-4211, MOT-4212) feat(harness): coalesce capped fires; reactions inherit registrant policy#583
andersonleal wants to merge 6 commits into
mainfrom
feat/react-fire-coalescing

Conversation

@andersonleal

@andersonleal andersonleal commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

What

Follow-up to the rctest-k7m3 postmortem (the wall of reaction failed — fire-rate cap (10 per 60000ms) reached for this subscription; not spawning). Two changes:

1. Capped fires coalesce instead of dropping

The fire-rate breaker (loop breaker #3, 10/60s per subscription) silently discarded capped fires. For recompute-style reactions that's the worst choice: a burst's tail is exactly what gets capped, so aggregates freeze one step behind the source of truth. Observed live: 15 orders in ~30s → 17 dropped reactions → totals stuck at 4/3/3 vs truth 5/5/5, never reconciled.

Now a capped fire parks in the gate slot (newest event wins, drops accumulate) and one trailing task per key re-enters harness::react when the window frees, delivering the latest event stamped with __coalesced_fires: N. The reaction outcome records waiting ("coalescing — the latest event fires once the window frees (N collapsed so far)") instead of failed.

The loop-breaker property is preserved by construction: at most one pending event and one trailing task per key, so a true runaway costs at most MAX_FIRES_PER_WINDOW + 1 reactions per window. If the window is still hot when the trailing fire lands, it parks again and a fresh cycle takes over.

Known edge (accepted): a binding unregistered while holding a parked fire still delivers that one trailing reaction — consistent with the at-least-once delivery posture.

2. Registration advisory for key-less state bindings

The same run registered BOTH a keyed and a key-less state binding on one scope; the key-less one fires for EVERY key (done markers, completion signals, everything), doubling the budget burn. Registrations of state bindings without a key filter now return an advisory note (react and notify paths) naming the scope and the cost — same posture as the existing standing-binding / join-wiring advisories.

Verification

  • cargo test: 267 unit + 7 integration pass — new tests pin the coalescing contract (newest-wins, single scheduler per drain cycle, drop-count reset, GC never evicts parked fires) and the advisory shape (scope-wide vs global vs keyed vs non-state)
  • Full e2e suite: E2E-001…004 all pass against the pinned engine (~3.2s each)
  • clippy/fmt clean

Replaying rctest-k7m3 against this: the 17 capped fires collapse into 2 trailing reactions (one per binding) that fire ~60s in with the final events — totals converge to 5/5/5 — and both registrations would have carried the catch-all warning telling the agent to key-filter the second binding.

Summary by CodeRabbit

  • New Features
    • Added reaction fire-rate coalescing (newest-wins) with trailing dispatch support and burst drop reporting.
    • Reaction-triggered spawns now inherit and safely narrow function permissions from the registration context.
    • Expanded E2E coverage with probe-driven actions, plus new E2E scenarios for reaction-policy inheritance (E2E-005), state-worker sidecar (E2E-006), and coalesced-fire (E2E-007).
  • Bug Fixes
    • Prevented untrusted reaction metadata from overriding registered permissions.
    • Improved deferred scheduling, delay calculation, and drop-count accuracy under rate limits.
    • Added clearer subscription advisory when state bindings omit a key filter.
  • Tests / Documentation
    • Updated harness/evidence/probe tooling and refreshed E2E scenario registries and documentation.

…ate bindings

The fire-rate breaker (10/60s per subscription) DROPPED capped fires.
For recompute-style reactions that is the worst possible choice: a
burst's TAIL is exactly what gets capped, so aggregates freeze one
step behind the source of truth. Live run rctest-k7m3: 15 orders in
~30s across two overlapping bindings -> 17 dropped reactions, totals
stuck at 4/3/3 vs truth 5/5/5, and a wall of "reaction failed —
fire-rate cap" outcomes.

Capped fires now COALESCE: the newest event parks in the gate slot
(drops accumulate), one trailing task per key re-enters harness::react
when the window frees, delivering the latest event stamped with
__coalesced_fires. Bounded by construction — one pending event and one
trailing task per key, so a true runaway costs at most
MAX_FIRES_PER_WINDOW + 1 reactions per window and the loop-breaker
property is preserved. The capped-fire outcome records "waiting" with
the collapse count instead of "failed".

Second half of the same postmortem: the run registered BOTH a keyed
and a key-less state binding on one scope — the key-less one fires
for EVERY key (done markers, completion signals, everything), doubling
budget burn. New registration advisory (react + notify paths) warns on
state bindings without a key filter, naming the scope.
@vercel

vercel Bot commented Jul 23, 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:34am
workers-tech-spec Ready Ready Preview, Comment Jul 24, 2026 1:34am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

React subscriptions now stamp trusted registrant policies, coalesce rate-limited fires with newest-wins trailing delivery, apply policy subsets to spawned payloads, and add probe-driven E2E coverage for reaction, state-worker, and coalesced-fire flows.

React trigger behavior

Layer / File(s) Summary
Registrant policy propagation
harness/src/functions/subscribe.rs, harness/src/functions/react.rs
Caller dispatch policies are trusted, stamped into reaction metadata, carried into ReactSpec, and included in subscription advisories.
Fire-rate coalescing and deferred re-entry
harness/src/functions/react.rs
Capped fires park the newest event, accumulate drop counts, schedule one trailing drain, and re-enter the handler with coalescing metadata.
Spawn payload policy inheritance
harness/src/functions/react.rs
Spawn options inherit or narrow registrant function policies without escalation.

E2E and state-worker coverage

Layer / File(s) Summary
Probe-driven scenario execution
harness/tests/e2e/src/fixtures/*, harness/tests/e2e/src/scenarios/dsl.rs, harness/tests/e2e/src/scenario/phases/completion.rs
Scenario fixtures define probe actions, execute them after completion boundaries, expand identifiers, and update trace expectations.
Controlled-call evidence and environment wiring
harness/tests/e2e/src/probe.rs, harness/tests/e2e/src/evidence_data.rs, harness/tests/e2e/src/scenario/*, harness/tests/e2e/src/stack/supervisor.rs
Controlled calls are recorded in run evidence, awaited by scenarios, and configured with per-fixture environment variables.
Reaction, state-worker, and coalescing scenarios
harness/tests/e2e/src/scenarios/*, harness/tests/e2e/src/stack/config.rs, harness/Makefile, harness/tests/e2e/README.md
E2E-005, E2E-006, and E2E-007 verify policy inheritance, standalone state-worker delivery, and capped-fire coalescing, with matching worker and fixture registration.

Estimated code review effort: 5 (Critical) | ~100 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ScenarioRunner
  participant ScenarioProbe
  participant StateWorker
  participant ReactHandler
  participant ControlledFunction
  ScenarioRunner->>ScenarioProbe: dispatch state::set probe
  StateWorker->>ReactHandler: deliver state reaction with metadata
  ReactHandler->>ControlledFunction: dispatch recorder call
  ControlledFunction-->>ScenarioProbe: record target call
  ScenarioProbe-->>ScenarioRunner: satisfy call-count wait
Loading

Possibly related PRs

  • iii-hq/workers#436: Implements the standalone state-worker migration used by the new E2E coverage.

Suggested reviewers: ytallo

Poem

A rabbit watches fires race,
The newest hops into its place.
Policies guide each payload bright,
State workers thump through the night.
Coalesced carrots land just right!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main changes: capped-fire coalescing and registrant-policy inheritance for reactions.
Docstring Coverage ✅ Passed Docstring coverage is 97.22% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/react-fire-coalescing

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 andersonleal changed the title feat(harness): coalesce capped reaction fires + key-less state binding advisory (MOT-4211) feat(harness): coalesce capped reaction fires + key-less state binding advisory Jul 23, 2026
A trigger-fired reaction spawned parentless and landed on the read-only
baseline whenever the spec carried no options.functions. Live run
rctest-k7m3: the wrap-up reaction — delivered into the ORCHESTRATOR'S
OWN chat — was denied database::query (x6), state::set (x1), and
engine::unregister_trigger (x4) in the same session whose earlier turns
called all three freely. The reaction whose job was cleanup couldn't
unregister, completing the binding-leak cycle across runs.

The register interceptor now stamps the registering turn's dispatch
policy onto the react binding (__registrant_functions — harness stamp,
smuggled values stripped, both modes). At fire time the spawned turn
inherits it; explicit options.functions are subset against it (narrow,
never escalate — the in-turn child rule, subset_policy). Raw
engine-side registrations carry no stamp and keep the read-only
baseline fallback.

CallerModel (already threading the registrant's model for inherit_model)
now carries the policy from the same trusted TurnRecord source.
@andersonleal andersonleal changed the title (MOT-4211) feat(harness): coalesce capped reaction fires + key-less state binding advisory (MOT-4211, MOT-4212) feat(harness): coalesce capped fires; reactions inherit registrant policy Jul 23, 2026
@andersonleal

Copy link
Copy Markdown
Collaborator Author

Added registrant-policy inheritance (27695c64, MOT-4212): the register interceptor stamps the registering turn's dispatch policy onto react bindings (__registrant_functions — harness stamp, smuggled values stripped in both modes); at fire time the spawned reaction inherits it, and explicit options.functions are subset against it (narrow, never escalate — the in-turn child rule via subset_policy). Raw engine-side registrations keep the read-only-baseline fallback. Fixes the rctest-k7m3 run-3 failure where the wrap-up reaction delivered into the orchestrator's own chat was denied database::query/state::set/engine::unregister_trigger — including the cleanup that would have stopped the cross-run binding leak. 269 unit + 7 integration tests, E2E-001…004 green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@harness/src/functions/react.rs`:
- Around line 556-601: Gate the rate-limit warning and waiting outcome in the
!deps.react_gate.admit(...) branch on deferred.schedule_delay_ms.is_some(), so
only the first deferral in each cycle performs logging and
record_reaction_outcome. Keep defer, pending-event coalescing, trailing-task
scheduling, and the final return behavior unchanged for subsequent deferrals.

In `@harness/src/functions/subscribe.rs`:
- Around line 666-668: The join_canon canonicalization path must also remove the
__registrant_functions metadata stamp. Update join_canon to strip
REGISTRANT_FUNCTIONS_KEY alongside __subscription_id, __once,
__owner_session_id, and join.key, preserving predecessor fingerprints based only
on task/model/session/options.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0528ffe4-9fc7-4272-9811-60914a980521

📥 Commits

Reviewing files that changed from the base of the PR and between 0e4432c and 27695c6.

📒 Files selected for processing (2)
  • harness/src/functions/react.rs
  • harness/src/functions/subscribe.rs

Comment on lines 556 to 601
if !deps.react_gate.admit(&gate_key, now_ms) {
// Coalesce instead of dropping: react patterns are recompute-style,
// so the newest event subsumes the capped ones. Park it and let one
// trailing fire deliver it when the window frees — without this, a
// burst's TAIL is exactly what gets dropped and aggregates freeze
// one step behind the source of truth (rctest-k7m3 postmortem).
let deferred = deps.react_gate.defer(
&gate_key,
event.clone(),
raw_metadata.unwrap_or(Value::Null),
now_ms,
);
tracing::warn!(
subscription = %gate_key,
"harness::react: fire-rate breaker tripped ({MAX_FIRES_PER_WINDOW} fires/{FIRE_WINDOW_MS}ms); not reacting"
dropped = deferred.dropped,
"harness::react: fire-rate breaker tripped ({MAX_FIRES_PER_WINDOW} fires/{FIRE_WINDOW_MS}ms); coalescing"
);
if let Some(delay_ms) = deferred.schedule_delay_ms {
let deps = deps.clone();
let key = gate_key.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(delay_ms as u64)).await;
let Some(pending) = deps.react_gate.take_pending(&key) else {
return;
};
let mut event = pending.event;
if let Some(obj) = event.as_object_mut() {
// Tell the reaction it stands in for a collapsed burst.
obj.insert("__coalesced_fires".to_string(), json!(pending.dropped));
}
// Re-enters the gate: if the window is still hot the fire
// parks again and a fresh trailing task takes over.
if let Err(e) = handle_boxed(&deps, event, Some(pending.metadata)).await {
tracing::warn!(subscription = %key, error = %e,
"harness::react: trailing coalesced fire failed");
}
});
}
let note = format!(
"fire-rate cap ({MAX_FIRES_PER_WINDOW} per {FIRE_WINDOW_MS}ms) reached for this subscription; not spawning"
"fire-rate cap ({MAX_FIRES_PER_WINDOW} per {FIRE_WINDOW_MS}ms) reached; coalescing — \
the latest event fires once the window frees ({} collapsed so far)",
deferred.dropped
);
record_reaction_outcome(deps, &spec, &event, "failed", &note, None).await;
record_reaction_outcome(deps, &spec, &event, "waiting", &note, None).await;
return Ok(ReactResult::note(note));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Every capped fire writes an outcome record and a log line, not just the first per drain cycle.

record_reaction_outcome(..., "waiting", ...) and tracing::warn! run on every single call that hits admit() == false, regardless of whether this deferral is the first of the cycle (schedule_delay_ms.is_some()) or a mid-cycle overwrite (schedule_delay_ms.is_none()). During exactly the runaway-burst scenario this gate exists to bound, this reintroduces unbounded I/O/log volume proportional to event ingress rate — the doc comment's "MAX_FIRES_PER_WINDOW + 1 reactions per window" bound only covers spawned reactions, not the outcome-store writes and log lines on the drop path.

Consider gating the record/log to the first deferral of a cycle (deferred.schedule_delay_ms.is_some()), since the trailing fire's own outcome record (once it actually spawns/fails) already reflects the final state and the accumulated dropped count.

♻️ Suggested throttle
         let deferred = deps.react_gate.defer(
             &gate_key,
             event.clone(),
             raw_metadata.unwrap_or(Value::Null),
             now_ms,
         );
-        tracing::warn!(
-            subscription = %gate_key,
-            dropped = deferred.dropped,
-            "harness::react: fire-rate breaker tripped ({MAX_FIRES_PER_WINDOW} fires/{FIRE_WINDOW_MS}ms); coalescing"
-        );
+        if deferred.schedule_delay_ms.is_some() {
+            tracing::warn!(
+                subscription = %gate_key,
+                "harness::react: fire-rate breaker tripped ({MAX_FIRES_PER_WINDOW} fires/{FIRE_WINDOW_MS}ms); coalescing"
+            );
+        }
         if let Some(delay_ms) = deferred.schedule_delay_ms {
             ...
         }
         let note = format!(...);
-        record_reaction_outcome(deps, &spec, &event, "waiting", &note, None).await;
+        if deferred.schedule_delay_ms.is_some() {
+            record_reaction_outcome(deps, &spec, &event, "waiting", &note, None).await;
+        }
         return Ok(ReactResult::note(note));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if !deps.react_gate.admit(&gate_key, now_ms) {
// Coalesce instead of dropping: react patterns are recompute-style,
// so the newest event subsumes the capped ones. Park it and let one
// trailing fire deliver it when the window frees — without this, a
// burst's TAIL is exactly what gets dropped and aggregates freeze
// one step behind the source of truth (rctest-k7m3 postmortem).
let deferred = deps.react_gate.defer(
&gate_key,
event.clone(),
raw_metadata.unwrap_or(Value::Null),
now_ms,
);
tracing::warn!(
subscription = %gate_key,
"harness::react: fire-rate breaker tripped ({MAX_FIRES_PER_WINDOW} fires/{FIRE_WINDOW_MS}ms); not reacting"
dropped = deferred.dropped,
"harness::react: fire-rate breaker tripped ({MAX_FIRES_PER_WINDOW} fires/{FIRE_WINDOW_MS}ms); coalescing"
);
if let Some(delay_ms) = deferred.schedule_delay_ms {
let deps = deps.clone();
let key = gate_key.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(delay_ms as u64)).await;
let Some(pending) = deps.react_gate.take_pending(&key) else {
return;
};
let mut event = pending.event;
if let Some(obj) = event.as_object_mut() {
// Tell the reaction it stands in for a collapsed burst.
obj.insert("__coalesced_fires".to_string(), json!(pending.dropped));
}
// Re-enters the gate: if the window is still hot the fire
// parks again and a fresh trailing task takes over.
if let Err(e) = handle_boxed(&deps, event, Some(pending.metadata)).await {
tracing::warn!(subscription = %key, error = %e,
"harness::react: trailing coalesced fire failed");
}
});
}
let note = format!(
"fire-rate cap ({MAX_FIRES_PER_WINDOW} per {FIRE_WINDOW_MS}ms) reached for this subscription; not spawning"
"fire-rate cap ({MAX_FIRES_PER_WINDOW} per {FIRE_WINDOW_MS}ms) reached; coalescing — \
the latest event fires once the window frees ({} collapsed so far)",
deferred.dropped
);
record_reaction_outcome(deps, &spec, &event, "failed", &note, None).await;
record_reaction_outcome(deps, &spec, &event, "waiting", &note, None).await;
return Ok(ReactResult::note(note));
}
if !deps.react_gate.admit(&gate_key, now_ms) {
// Coalesce instead of dropping: react patterns are recompute-style,
// so the newest event subsumes the capped ones. Park it and let one
// trailing fire deliver it when the window frees — without this, a
// burst's TAIL is exactly what gets dropped and aggregates freeze
// one step behind the source of truth (rctest-k7m3 postmortem).
let deferred = deps.react_gate.defer(
&gate_key,
event.clone(),
raw_metadata.unwrap_or(Value::Null),
now_ms,
);
if deferred.schedule_delay_ms.is_some() {
tracing::warn!(
subscription = %gate_key,
"harness::react: fire-rate breaker tripped ({MAX_FIRES_PER_WINDOW} fires/{FIRE_WINDOW_MS}ms); coalescing"
);
}
if let Some(delay_ms) = deferred.schedule_delay_ms {
let deps = deps.clone();
let key = gate_key.clone();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(delay_ms as u64)).await;
let Some(pending) = deps.react_gate.take_pending(&key) else {
return;
};
let mut event = pending.event;
if let Some(obj) = event.as_object_mut() {
// Tell the reaction it stands in for a collapsed burst.
obj.insert("__coalesced_fires".to_string(), json!(pending.dropped));
}
// Re-enters the gate: if the window is still hot the fire
// parks again and a fresh trailing task takes over.
if let Err(e) = handle_boxed(&deps, event, Some(pending.metadata)).await {
tracing::warn!(subscription = %key, error = %e,
"harness::react: trailing coalesced fire failed");
}
});
}
let note = format!(
"fire-rate cap ({MAX_FIRES_PER_WINDOW} per {FIRE_WINDOW_MS}ms) reached; coalescing — \
the latest event fires once the window frees ({} collapsed so far)",
deferred.dropped
);
if deferred.schedule_delay_ms.is_some() {
record_reaction_outcome(deps, &spec, &event, "waiting", &note, None).await;
}
return Ok(ReactResult::note(note));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness/src/functions/react.rs` around lines 556 - 601, Gate the rate-limit
warning and waiting outcome in the !deps.react_gate.admit(...) branch on
deferred.schedule_delay_ms.is_some(), so only the first deferral in each cycle
performs logging and record_reaction_outcome. Keep defer, pending-event
coalescing, trailing-task scheduling, and the final return behavior unchanged
for subsequent deferrals.

Comment on lines +666 to +668
// Both modes: strips any smuggled registrant-policy stamp; inert for call
// reactions (they dispatch with worker authority, no spawned turn).
stamp_registrant_functions(&mut req.metadata, caller);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "fn join_canon" -A 40 harness/src/functions/react.rs

Repository: iii-hq/workers

Length of output: 5860


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== subscribe relevant section =="
sed -n '620,700p' harness/src/functions/subscribe.rs

echo
echo "== stamp_registrant_functions definition/usages =="
rg -n "fn stamp_registrant_functions|stamp_registrant_functions|__registrant_functions" harness/src -g '*.rs'

echo
echo "== react join_canon tests =="
sed -n '1720,1742p' harness/src/functions/react.rs

echo
echo "== deterministic semantic probe for current join_canon strip behavior =="
python3 - <<'PY'
from pathlib import Path
import re

src = Path('harness/src/functions/react.rs').read_text()
m = re.search(r'pub fn join_canon\(metadata: Option<&Value>\) -> Option<\(String, Value\)> \{(?P<body>.*?)^}', src, re.S|re.M)
print("join_canon snippet:")
start = src.find('pub fn join_canon')
end = src.find('\n/// Registration-time validation', start)
print(src[start:end])
PY

Repository: iii-hq/workers

Length of output: 6932


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== stamp_registrant_functions implementation =="
sed -n '288,320p' harness/src/functions/subscribe.rs

echo
echo "== subscription key constants =="
sed -n '150,170p' harness/src/functions/subscribe.rs
rg -n "OWNER_SESSION_KEY|owner_session|__owner_session_id|RegistrantModelPolicy|RegistrantFunctions|__" harness/src/functions/subscribe.rs harness/src/subscriptions*.rs harness/src -g '*.rs' | head -n 80

echo
echo "== dedup key if relevant =="
rg -n "registration_dedup_key" -A 60 harness/src/functions/subscribe.rs

echo
echo "== conflicting_join_spec shape =="
rg -n "conflicting_join_spec|join_canon" -A 35 harness/src -g '*.rs'

Repository: iii-hq/workers

Length of output: 7873


Strip __registrant_functions in join_canon.

join_canon currently only removes __subscription_id, __once, __owner_session_id, and join.key, so the newly added __registrant_functions stamp makes join predecessors of identical specs fingerprint differently when registered from sessions with different dispatch policies. Add REGISTRANT_FUNCTIONS_KEY (__registrant_functions) to the canonical strip path so join predecessor equivalence depends only on task/model/session/options.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness/src/functions/subscribe.rs` around lines 666 - 668, The join_canon
canonicalization path must also remove the __registrant_functions metadata
stamp. Update join_canon to strip REGISTRANT_FUNCTIONS_KEY alongside
__subscription_id, __once, __owner_session_id, and join.key, preserving
predecessor fingerprints based only on task/model/session/options.

… policy

Drives the rctest-k7m3 run-3 wrap-up failure through the real stack:
the scripted model registers a turn-completed reaction on its own
session with NO options, the turn completes, the binding fires, and
the reaction seeds a second terminal turn in the same session whose
scripted call to the controlled function must dispatch successfully —
possible only under the inherited policy (the read-only baseline
exposes no worker functions, so the pre-fix run fails both the tools
pin and the no-policy-denial verify). Verified meaningful: fails
against pre-inheritance sources (d34fc5f).

Uses the two-terminal-turn await (terminal_turn_statuses) from E2E-003
and pins the reaction turn's steps with turn_request_step. Reaction
turns run the sub-agent prompt, so those generations match the prompt
by presence, not the default sha.

Coalescing (the PR's other half) stays unit-pinned: its trailing fire
lands when the 60s window frees, which cannot fit the e2e deadline.
@andersonleal

Copy link
Copy Markdown
Collaborator Author

Added E2E-005 reaction-policy-inheritance: real-stack regression gate for the MOT-4212 half — a turn-completed reaction registered with no options seeds a second terminal turn in the registrant's session and successfully dispatches a controlled function that only the inherited policy exposes (the read-only baseline has no worker functions). Verified meaningful: fails against pre-inheritance sources. All five direct scenarios pass (~3.2s each). Coalescing stays unit-pinned — its trailing fire needs the 60s window to free, which can't fit the e2e deadline.

…ract docs

rctest-k7m2 exposed the coalescing contract's sharp edge: only the
newest event survives a collapse, and a reactor doing per-key upserts
from single events silently skipped a writer whose events were all
among the collapsed (totals converged for w1/w3, never w2). The
recompute-on-coalesce rule existed only in this repo's heads.

Three places now carry it, closest first:
- the trailing event itself: stamp_coalesced adds __coalesced_note
  ("stands in for N fires ... recompute from the source of truth")
  beside the machine-readable __coalesced_fires — guidance delivered
  at the exact moment the reacting model reads the event
- ReactSpec.task docs: tasks that process single events per key must
  recompute on a coalesced fire
- SubscribeRequest.metadata docs: the coalescing behavior is part of
  the subscription contract agents read at registration time

Non-object events cannot carry the stamp and pass through unchanged.
@andersonleal

Copy link
Copy Markdown
Collaborator Author

Follow-up from the rctest-k7m2 live run (7da6879f): coalescing worked mechanically (15 events → 10 admitted + 1 trailing, exactly per design) but its sharp edge cost a writer — a per-key upsert reactor missed w2 because all of w2's tail events were among the collapsed and the trailing event carried w3. The recompute-on-coalesce rule is now delivered in three places: a __coalesced_note stamped INTO the trailing event (read by the reacting model at exactly the right moment), ReactSpec.task docs, and the SubscribeRequest.metadata contract docs. Unit test pins the stamp; 270 unit + 7 integration + all five e2e scenarios pass.

The cross-worker sidecar path (MOT-4209/MOT-4213) couldn't be gated
deterministically: a reaction firing mid-turn races the strict-ordinal
scripted router. The probe hook fixes that — a scenario declares
`probe_after(n, function_id, payload)`, and the runner fires it once n
terminal turns are observed, so the reaction it trips runs while the
tracked session is idle (its turn is the only active one).

- ProbeAction on the fixture + `probe_after` DSL builder; the await
  phase waits to each action's boundary, fires it (expanding
  {{run_id}}/{{session_id}}), then awaits the rest.
- expected_traces for Direct is now 1 + probe_actions: a probe-tripped
  reaction is a fresh externally-initiated flow, so it traces
  separately (like a Playground external turn).
- The integration stack now runs the standalone `state` worker (the
  engine builtin disabled) so the `state` trigger type is owned exactly
  as in production — the fan-out seam under test.

E2E-006 (state-worker-sidecar): main turn registers a state-key
reaction and completes; the probe writes the key; the state worker
fans out to harness::react WITH the sidecar, seeding a second turn
that calls the recorder. Verified meaningful — reverting the state
worker to its pre-MOT-4209 sources times the scenario out (no sidecar
-> no reaction -> no second turn). 5/5 stable.

Coalescing's trailing-fire e2e stays deferred (MOT-4215): call
reactions leave no main-session trace and task reactions message-merge
when several target one session, so it also needs whole-run trace
evidence, not just the probe hook. Coalescing logic remains unit-pinned.
@andersonleal

Copy link
Copy Markdown
Collaborator Author

Implemented the probe post-completion hook (217b9f00) — unblocks the deterministic cross-worker-fire e2e that strict-ordinal routing made impossible before. A scenario declares probe_after(n, fn, payload); the runner fires it once N terminal turns complete, so the reaction it trips runs while the session is idle and scripts cleanly.

New scenario E2E-006 state-worker-sidecar: the integration stack now runs the standalone state worker (builtin disabled), and the scenario drives a reaction through it end-to-end — proving the fire-time metadata sidecar (MOT-4209/MOT-4213 class) crosses the worker boundary. Verified meaningful: reverting the state worker to its pre-fix sources times the scenario out (no sidecar → no reaction → no second turn). 5/5 stable; all six direct scenarios green.

Coalescing's trailing-fire e2e stays deferred to MOT-4215: it hits a second wall the probe hook doesn't clear (call reactions leave no main-session trace; task reactions message-merge when several target one session), so it also needs whole-run trace evidence. Coalescing logic remains unit-pinned + live-confirmed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
harness/tests/e2e/src/fixtures/loading.rs (1)

140-145: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

validate() never exercises probe-action payloads or bounds.

compiled() only carries scenario/script/system_prompt_template, so validate()'s expand_compiled_fixture sanity check never touches ProbeAction.payload. A malformed {{placeholder}} in a probe action's payload, or an after_turns outside [1, expected_terminal_turns), passes fixture.validate().unwrap() in unit tests and only surfaces as a real-engine E2E failure/timeout.

♻️ Suggested addition to `validate()`
         crate::expand::expand_compiled_fixture(
             &self.compiled(),
             "validate-run",
             "validate-session",
         )?;
+        for action in &self.probe_actions {
+            anyhow::ensure!(
+                !action.function_id.is_empty(),
+                "probe action has an empty function_id"
+            );
+            anyhow::ensure!(
+                action.after_turns > 0 && action.after_turns < self.expected_terminal_turns,
+                "probe action after_turns ({}) must be within [1, {})",
+                action.after_turns,
+                self.expected_terminal_turns
+            );
+            let mut payload = action.payload.clone();
+            crate::expand::Placeholders::new("validate-run", "validate-session")
+                .expand_value(&mut payload)?;
+        }
         Ok(())

Also applies to: 164-170

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness/tests/e2e/src/fixtures/loading.rs` around lines 140 - 145, Update the
fixture validation method around expand_compiled_fixture so it also expands and
validates every ProbeAction payload, including placeholder substitution and
payload bounds. Ensure after_turns values are rejected unless they fall within
[1, expected_terminal_turns), while preserving the existing compiled fixture
validation behavior.
harness/tests/e2e/src/scenarios/state_worker_sidecar.rs (1)

156-175: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicates message_text instead of reusing it.

This block manually re-extracts content block text from a function_result message, duplicating what crate::evidence_data::message_text already does — used for the identical check in reaction_policy_inheritance.rs (same cohort).

♻️ Suggested fix
-use crate::fixtures::ScenarioFixture;
+use crate::evidence_data::message_text;
+use crate::fixtures::ScenarioFixture;
             if msg.get("role").and_then(Value::as_str) == Some("function_result") {
-                let text: String = msg
-                    .get("content")
-                    .and_then(Value::as_array)
-                    .map(|blocks| {
-                        blocks
-                            .iter()
-                            .filter_map(|b| b.get("text").and_then(Value::as_str))
-                            .collect::<Vec<_>>()
-                            .concat()
-                    })
-                    .unwrap_or_default();
+                let text = message_text(&msg);
                 anyhow::ensure!(
                     !text.contains("not permitted"),
                     "a dispatch was denied: {text}"
                 );
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness/tests/e2e/src/scenarios/state_worker_sidecar.rs` around lines 156 -
175, Replace the manual content-block text extraction inside the transcript loop
with the existing crate::evidence_data::message_text helper, while preserving
the function_result role filter and the anyhow::ensure! denial check. Reuse that
helper’s result for the “not permitted” validation instead of constructing text
locally.
harness/tests/e2e/src/scenario/phases/completion.rs (1)

42-63: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Inconsistent deadline-expiry handling between the wait and the fire step.

The wait_for_completion_turns error path explicitly checks deadline.is_expired() and degrades gracefully (active.timed_out = true; return Ok(())), but the immediately following fire_probe_action(...).await? (Line 61) propagates any failure as a hard RunError regardless of whether the deadline expired. If the deadline lapses between the wait succeeding and the dispatch call, this turns what should be a graceful timeout into a hard CI failure.

♻️ Suggested fix
-                self.fire_probe_action(services, &action, deadline).await?;
+                if let Err(error) = self.fire_probe_action(services, &action, deadline).await {
+                    if deadline.is_expired() {
+                        active.timed_out = true;
+                        return Ok(());
+                    }
+                    return Err(error);
+                }

Please confirm call_with_deadline's error shape on deadline expiry to validate this is actually reachable.

Also applies to: 136-161

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness/tests/e2e/src/scenario/phases/completion.rs` around lines 42 - 63,
Update the probe-action dispatch path around fire_probe_action and its
additional occurrence so failures are checked for deadline expiry before
propagating. Confirm call_with_deadline’s deadline error shape, then mark
active.timed_out and return Ok(()) when the deadline has expired; preserve
RunError propagation for other failures and keep the existing
wait_for_completion_turns handling consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@harness/tests/e2e/src/fixtures/loading.rs`:
- Around line 140-145: Update the fixture validation method around
expand_compiled_fixture so it also expands and validates every ProbeAction
payload, including placeholder substitution and payload bounds. Ensure
after_turns values are rejected unless they fall within [1,
expected_terminal_turns), while preserving the existing compiled fixture
validation behavior.

In `@harness/tests/e2e/src/scenario/phases/completion.rs`:
- Around line 42-63: Update the probe-action dispatch path around
fire_probe_action and its additional occurrence so failures are checked for
deadline expiry before propagating. Confirm call_with_deadline’s deadline error
shape, then mark active.timed_out and return Ok(()) when the deadline has
expired; preserve RunError propagation for other failures and keep the existing
wait_for_completion_turns handling consistent.

In `@harness/tests/e2e/src/scenarios/state_worker_sidecar.rs`:
- Around line 156-175: Replace the manual content-block text extraction inside
the transcript loop with the existing crate::evidence_data::message_text helper,
while preserving the function_result role filter and the anyhow::ensure! denial
check. Reuse that helper’s result for the “not permitted” validation instead of
constructing text locally.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5e8314e6-adb4-492d-81cf-f813ac17870e

📥 Commits

Reviewing files that changed from the base of the PR and between 27695c6 and 217b9f0.

📒 Files selected for processing (13)
  • harness/Makefile
  • harness/src/functions/react.rs
  • harness/src/functions/subscribe.rs
  • harness/tests/e2e/README.md
  • harness/tests/e2e/src/fixtures.rs
  • 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/mod.rs
  • harness/tests/e2e/src/scenarios/reaction_policy_inheritance.rs
  • harness/tests/e2e/src/scenarios/state_worker_sidecar.rs
  • harness/tests/e2e/src/stack/config.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • harness/src/functions/subscribe.rs
  • harness/src/functions/react.rs

Call-mode reactions are invisible to the runner's two evidence surfaces
— they seed no turn (completions blind) and no session trace (trace
floor blind) — which is what kept the coalescing e2e deferred. Two
runner capabilities close that gap:

* probe-side WHOLE-RUN call evidence: the probe serves the controlled
  function, so it records every dispatch payload regardless of origin
  (RunEvidence::target_calls, expect_target_calls), and
* await_target_calls: the await phase holds until the controlled
  function served N calls — the only way to await a trailing coalesced
  fire that lands after the rate-cap window frees.

The fire gate's limits gain env overrides (III_HARNESS_MAX_FIRES_PER_
WINDOW / III_HARNESS_FIRE_WINDOW_MS, read once), and fixtures can pass
env to the harness under test plus override the floor's trace count
(call reactions form no extra trace tree).

E2E-007 bursts 5 state fires through a call-mode binding under a
3-per-2s gate: exactly cap + 1 dispatches, the first three unstamped
with distinct seqs, the trailing one stamped __coalesced_fires: 2 with
the self-describing recovery note, its event one of the CAPPED fires
(newest wins — one seq is never observed, by design). Gate check: with
the trailing-fire scheduling disabled (pre-MOT-4211 drop semantics) the
scenario times out; with it, 6/6 passes at ~4.6s. All 7 direct
scenarios pass.
@andersonleal

Copy link
Copy Markdown
Collaborator Author

MOT-4215 landed (34f6595a): E2E-007 coalesced-fire now gates the coalescing path end to end.

The two walls that kept it deferred are closed with runner capabilities rather than trace surgery:

  • probe-side whole-run call evidence — the probe serves the controlled function, so every dispatch is recorded regardless of origin (call-mode reactions included), exposed as RunEvidence::target_calls;
  • await_target_calls — the await phase holds until N dispatches were served, which is the only way to await the trailing coalesced fire (it seeds no turn and no session trace).

The fire gate gained once-read env overrides (III_HARNESS_MAX_FIRES_PER_WINDOW / III_HARNESS_FIRE_WINDOW_MS); the fixture shrinks it to 3-per-2s so the scenario runs in ~4.6s.

Assertions: 5-fire burst → exactly cap+1 dispatches; first three unstamped with distinct seqs; trailing one stamped __coalesced_fires: 2 + the self-describing recovery note; its event is one of the capped fires (newest wins — exactly one seq never observed, by design).

Gate check: with trailing-fire scheduling disabled (pre-MOT-4211 drop semantics) the scenario times out at the await; with it, 6/6 passes. All 7 direct scenarios green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@harness/tests/e2e/src/fixtures/loading.rs`:
- Around line 33-42: Update ScenarioFixture::validate() to reject
await_target_calls values of Some(0) and any positive await_target_calls when
scenario.target is absent. Preserve valid positive counts when a target exists,
and allow None as before so malformed fixtures fail before execution.

In `@harness/tests/e2e/src/scenarios/coalesced_fire.rs`:
- Around line 172-178: Update the trailing event assertion in the coalesced-fire
scenario to require trailing_seq to equal BURST exactly, preserving the existing
uniqueness check and error context so the test verifies newest-wins rather than
accepting any capped sequence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a8fbc104-fe85-448a-98e1-079355bc076f

📥 Commits

Reviewing files that changed from the base of the PR and between 217b9f0 and 34f6595.

📒 Files selected for processing (15)
  • harness/src/functions/react.rs
  • harness/tests/e2e/README.md
  • harness/tests/e2e/src/evidence_data.rs
  • harness/tests/e2e/src/fixtures/loading.rs
  • harness/tests/e2e/src/fixtures/tests.rs
  • harness/tests/e2e/src/probe.rs
  • harness/tests/e2e/src/scenario/floor.rs
  • harness/tests/e2e/src/scenario/phases/arm.rs
  • harness/tests/e2e/src/scenario/phases/completion.rs
  • harness/tests/e2e/src/scenario/phases/evidence.rs
  • harness/tests/e2e/src/scenarios/coalesced_fire.rs
  • harness/tests/e2e/src/scenarios/dsl.rs
  • harness/tests/e2e/src/scenarios/mod.rs
  • harness/tests/e2e/src/stack/supervisor.rs
  • harness/tests/e2e/tests/determinism.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • harness/tests/e2e/src/scenarios/dsl.rs
  • harness/tests/e2e/src/scenario/phases/completion.rs
  • harness/src/functions/react.rs

Comment on lines +33 to +42
/// Await this many controlled-function invocations (probe-side, whole-run)
/// after every terminal turn arrived. This is the only way to await work
/// that produces no session turn — a call-mode reaction's dispatch,
/// including a trailing coalesced fire.
pub await_target_calls: Option<usize>,
/// Session-trace-count override for the floor. `None` keeps the driver
/// formula. Needed when probe actions trip CALL-mode reactions, which
/// dispatch a function without seeding any session turn — no extra trace
/// tree ever forms.
pub traces_override: Option<usize>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate controlled-call wait configuration.

Line 37 permits Some(0), which later fails in wait_for_target_calls; a positive count without scenario.target instead waits until timeout. Reject both combinations in ScenarioFixture::validate() so malformed fixtures fail before execution.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness/tests/e2e/src/fixtures/loading.rs` around lines 33 - 42, Update
ScenarioFixture::validate() to reject await_target_calls values of Some(0) and
any positive await_target_calls when scenario.target is absent. Preserve valid
positive counts when a target exists, and allow None as before so malformed
fixtures fail before execution.

Comment on lines +172 to +178
let trailing_seq = seq_of(trailing)
.ok_or_else(|| anyhow::anyhow!("trailing fire has no seq: {trailing}"))?;
anyhow::ensure!(
(1..=BURST as u64).contains(&trailing_seq) && !seen.contains(&trailing_seq),
"the trailing event must be one of the CAPPED fires (newest wins), got seq \
{trailing_seq} vs delivered {seen:?}"
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the newest capped event exactly.

This accepts sequence 4 or 5; with CAP = 3, both are capped. A first-parked implementation would pass even though it violates newest-wins. Require the trailing event to be sequence BURST.

Proposed fix
             anyhow::ensure!(
-                (1..=BURST as u64).contains(&trailing_seq) && !seen.contains(&trailing_seq),
+                trailing_seq == BURST as u64 && !seen.contains(&trailing_seq),
                 "the trailing event must be one of the CAPPED fires (newest wins), got seq \
                  {trailing_seq} vs delivered {seen:?}"
             );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let trailing_seq = seq_of(trailing)
.ok_or_else(|| anyhow::anyhow!("trailing fire has no seq: {trailing}"))?;
anyhow::ensure!(
(1..=BURST as u64).contains(&trailing_seq) && !seen.contains(&trailing_seq),
"the trailing event must be one of the CAPPED fires (newest wins), got seq \
{trailing_seq} vs delivered {seen:?}"
);
let trailing_seq = seq_of(trailing)
.ok_or_else(|| anyhow::anyhow!("trailing fire has no seq: {trailing}"))?;
anyhow::ensure!(
trailing_seq == BURST as u64 && !seen.contains(&trailing_seq),
"the trailing event must be one of the CAPPED fires (newest wins), got seq \
{trailing_seq} vs delivered {seen:?}"
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness/tests/e2e/src/scenarios/coalesced_fire.rs` around lines 172 - 178,
Update the trailing event assertion in the coalesced-fire scenario to require
trailing_seq to equal BURST exactly, preserving the existing uniqueness check
and error context so the test verifies newest-wins rather than accepting any
capped sequence.

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