Skip to content

feat(harness): reactive trigger bridge (harness::react) with join fan-in, wire hardening, and spawn console view - #401

Merged
andersonleal merged 10 commits into
mainfrom
feat/harness-react-bridge
Jul 3, 2026
Merged

feat(harness): reactive trigger bridge (harness::react) with join fan-in, wire hardening, and spawn console view#401
andersonleal merged 10 commits into
mainfrom
feat/harness-react-bridge

Conversation

@andersonleal

@andersonleal andersonleal commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

What

Ports the engine's trigger/notify primitives into a harness-native reactive sub-agent bridge (harness::react), plus the lifecycle, wire-shape, and console work that live testing of the reactive pipeline demanded.

Reactive bridge (harness::react)

  • Sub-agent spec fires on engine triggers (turn events, state, cron, stream); join fan-in with an expect array, fire-once semantics with optional rearm, durable join accumulation, auto-unregister of predecessor subscriptions.
  • Loop breakers: self-edge suppression (a subscription never fires for the sub-agent it spawned), reactive-chain depth cap, per-subscription spawn rate limit.
  • Ownership enforcement on delivery targets (registrant session or descendant, fail-closed), read-only baseline policy for trigger-fired sub-agents, model validation at registration.
  • Startup reconcile rebuilds the sub_ → engine-id mapping for durable bindings and GCs zombie notify bindings.

Wire-shape hardening (two live 400 classes from the notification workload)

  • Displaced tool results (fix(providers)): a notification injected into an open call window lands between function_call and function_result in the durable transcript; all four provider wire mappers only repaired missing results, so the pair split reached the API — Anthropic 400 tool_use ids were found without tool_result blocks immediately after, turn permanently wedged. Shared reorder_displaced_results in llm-router now runs first in every mapper. Wedged sessions self-heal.
  • Trailing-assistant prefill (fix(harness)): a user entry appended while a step generates lands before that step's assistant entry; the steering re-generate then ends the context with a call-less assistant — newer models reject it as prefill (This model does not support assistant message prefill). rotate_mid_generation_users presents mid-generation arrivals after the reply they interrupted. Hardened by adversarial review: watermark advances only after router.chat returns (redelivered steps keep their rotation window), and rotation runs on the final assembled values so compaction bookkeeping stays in log order.

Prompts

  • Every spawned child gets a readable session_id (job slug + random suffix, e.g. fetch-headlines-b4k9) instead of engine-minted UUIDs in the console tree; explicitly not parent-prefixed and scoped to direct spawns (react metadata.session_id keeps its delivery semantics). Fan-in doctrine aligned across all five prompt variants.

Console

  • Dedicated chat view for harness::spawn (request/response panes, parsers, stories, fixtures).

Testing

  • cargo test --lib green across harness (161), llm-router (45), provider-anthropic (79), provider-openai (65), provider-xai (72), provider-openai-codex (44).
  • Both wire fixes were reproduced with failing tests first (exact live 400 shapes), then fixed; regression tests in all four providers, the shared helper, and the turn loop (7 rotation tests including the redelivery and compaction-clamp schedules).
  • Rotation/watermark fix passed a 3-lens adversarial verification round (correctness, crash-resume, compaction) plus a re-verification round after addressing its two confirmed findings.

Summary by CodeRabbit

  • New Features

    • Added support for a new chat harness spawn flow with a dedicated preview and result display.
    • Introduced reactive subscriptions for event-driven follow-up actions and fan-in/join behavior.
  • Bug Fixes

    • Improved handling of tool/function results so they stay aligned with the originating action in chat transcripts.
    • Added safer parsing and clearer handling for incomplete or invalid spawn inputs.
  • Documentation

    • Expanded harness guidance to better explain trigger-based workflows and reactive callbacks.

@vercel

vercel Bot commented Jul 3, 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 3, 2026 7:28pm
workers-tech-spec Ready Ready Preview, Comment Jul 3, 2026 7:28pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 32 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

Run ID: a4994b8d-11f1-4c87-bfc0-7cc14d7b3a3a

📥 Commits

Reviewing files that changed from the base of the PR and between 5b99f21 and 9ab724c.

⛔ Files ignored due to path filters (2)
  • provider-anthropic/Cargo.lock is excluded by !**/*.lock
  • provider-openai/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (39)
  • console/web/src/components/chat/harness/SpawnView.tsx
  • console/web/src/components/chat/harness/__tests__/parsers.test.ts
  • console/web/src/components/chat/harness/index.tsx
  • console/web/src/components/chat/harness/parsers.ts
  • console/web/src/stories/fixtures/harness-fixtures.ts
  • console/web/src/stories/playground/Agent.stories.tsx
  • console/web/src/stories/playground/scenarios/harness-spawn.ts
  • console/web/src/stories/playground/scenarios/index.ts
  • harness/prompts/anthropic.txt
  • harness/prompts/cli.txt
  • harness/prompts/default.txt
  • harness/prompts/gpt.txt
  • harness/prompts/kimi.txt
  • harness/skills/SKILL.md
  • harness/src/clients/engine.rs
  • harness/src/clients/session.rs
  • harness/src/config.rs
  • harness/src/deps.rs
  • harness/src/events.rs
  • harness/src/functions/mod.rs
  • harness/src/functions/on_session_deleted.rs
  • harness/src/functions/react.rs
  • harness/src/functions/send.rs
  • harness/src/functions/spawn.rs
  • harness/src/functions/subscribe.rs
  • harness/src/main.rs
  • harness/src/subagent.rs
  • harness/src/subscriptions/mod.rs
  • harness/src/subscriptions/reconcile.rs
  • harness/src/subscriptions/registry.rs
  • harness/src/turn_loop.rs
  • harness/src/types/turn.rs
  • harness/tests/golden/schemas/harness.spawn.json
  • iii-permissions.yaml
  • llm-router/src/types/messages.rs
  • provider-anthropic/src/wire/messages.rs
  • provider-openai-codex/src/wire/messages.rs
  • provider-openai/src/wire/messages.rs
  • provider-xai/src/wire/messages.rs
📝 Walkthrough

Walkthrough

This PR introduces reactive sub-agent orchestration via a new harness::react trigger bridge with join/fan-in semantics, reactive metadata propagation through spawn/turn events, subscription reconciliation on restart and session deletion, chat UI rendering for harness::spawn, provider wire-message reordering to keep tool results adjacent to their calls, and corresponding permissions, prompts, docs, and schema updates.

Changes

Reactive spawn/react engine backend

Layer / File(s) Summary
Turn record and spawn contract
harness/src/types/turn.rs, harness/src/functions/spawn.rs, harness/src/functions/send.rs, harness/tests/golden/schemas/harness.spawn.json
Adds display_parent_session_id, spawned_by_subscription_id, reactive_depth fields to TurnRecord/SpawnRequest and golden schema.
Deps wiring
harness/src/deps.rs
Adds react_gate: Arc<FireGate> dependency.
harness::react implementation
harness/src/functions/react.rs, harness/src/functions/mod.rs
Implements FireGate, ReactSpec/JoinSpec, handle, join_edge fan-in accumulation, spawn_reaction, and model validation.
Turn event emission
harness/src/events.rs
Updates BindingFilter::matches, Binding metadata, register_trigger validation, ReactiveMeta, emit_started/emit_completed, fan_out.
Subscription registration
harness/src/functions/subscribe.rs, harness/src/subscriptions/registry.rs, harness/src/subscriptions/mod.rs
Adds harness::react registration path, idempotent dedup, join-wiring advisories, keyed dedup registry.
Reconciliation
harness/src/subscriptions/reconcile.rs, harness/src/functions/on_session_deleted.rs, harness/src/main.rs
Adds startup GC and session-delete sweep for orphaned bindings.
Subagent linkage and config
harness/src/subagent.rs, harness/src/config.rs, harness/src/clients/session.rs
Wires display-parent/reactive fields, default function policy, SessionClient::ensure returns bool.
Turn loop context assembly
harness/src/turn_loop.rs
Updates watermark handling, context rotation, policy_aid, and patch_orphaned_calls.
Permissions, prompts, docs
iii-permissions.yaml, harness/prompts/*.txt, harness/skills/SKILL.md, tech-specs/2026-06-agentic/harness.md
Documents harness::react, join semantics, and renames events to hyphenated form.

Chat harness spawn UI

Layer / File(s) Summary
Spawn parsers
console/web/src/components/chat/harness/parsers.ts, .../__tests__/parsers.test.ts
Adds Zod schemas, safeParseRequest, taskText, and tests.
SpawnView/SpawnPreview
console/web/src/components/chat/harness/SpawnView.tsx
Renders spawn task/result panes, chips, session links, JSON/markdown output.
Tool view wiring
console/web/src/components/chat/harness/index.tsx
Wires SpawnView/SpawnPreview into HarnessToolView.
Storybook fixtures/scenario
console/web/src/stories/fixtures/harness-fixtures.ts, .../playground/scenarios/harness-spawn.ts, .../playground/scenarios/index.ts, .../playground/Agent.stories.tsx
Adds spawn fixtures and harness-spawn playground scenario.

Provider wire message reordering

Layer / File(s) Summary
reorder_displaced_results integration
llm-router/src/types/messages.rs, provider-anthropic/..., provider-openai/..., provider-openai-codex/..., provider-xai/...
Adds shared reorder helper and integrates into all four provider wire builders with regression tests.

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

Sequence Diagram(s)

sequenceDiagram
  participant Agent
  participant Engine
  participant HarnessReact as harness::react
  participant IIIState as iii-state
  participant HarnessSpawn as harness::spawn

  Agent->>Engine: engine::register_trigger (turn-completed, metadata=ReactSpec)
  Engine-->>HarnessReact: fire trigger with event payload + metadata
  HarnessReact->>HarnessReact: validate_spec, FireGate, depth check
  alt join edge
    HarnessReact->>IIIState: record predecessor arrival/result
    IIIState-->>HarnessReact: accumulator state
    HarnessReact->>HarnessReact: fire-once guard
  end
  HarnessReact->>HarnessSpawn: spawn_reaction(task built from event/join)
  HarnessSpawn-->>HarnessReact: child_session_id, child_turn_id
  HarnessReact-->>Engine: ReactResult
Loading

Possibly related PRs

  • iii-hq/workers#204: Shares Storybook/Playground harness scenario utilities (scenarioStory) that the new harness-spawn story reuses.
  • iii-hq/workers#333: Overlaps in the harness subscription infrastructure (harness/src/subscriptions/mod.rs, subscribe/notify wiring) extended here for harness::react.
  • iii-hq/workers#338: Both modify harness/src/events.rs, particularly TurnEventTriggerHandler/TurnEvents signatures.

Suggested reviewers: sergiofilhowz, ytallo

Poem

A rabbit spawns a helper thread,
"Go react, dear child," it said.
Joins converge, one fire, no more,
Sessions swept from the restart floor.
Tool results hop back in place—
🐇 harness dances, full of grace!

🚥 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 accurately summarizes the main changes: a harness reactive trigger bridge, wire hardening, and spawn console UI support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/harness-react-bridge

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.

…-in and lifecycle hardening

Ports the engine's trigger/notify primitives into a harness-native reactive
sub-agent bridge and hardens the full registration/fire/teardown lifecycle
against gaps found in live testing.

- harness::react: sub-agent spec fires on engine triggers (turn events,
  state, cron, stream); join fan-in with an expect array, fire-once
  accumulator, and rearm for standing watchers.
- Interceptor pass-through (subscribe.rs): agent-issued
  engine::register_trigger calls get owner + subscription id stamped
  server-side into the react metadata, closing several trust gaps in the
  raw registration path.
- Idempotent registration (dedup by canonical request key) and a durable
  owner sweep on session::deleted, replacing two pipelines that could
  double-register the same reaction.
- Startup reconcile: GC react bindings whose owner session is gone and
  notify bindings unknown to the local registry; never GC on doubt.
- Loop breakers: self-edge drop, reactive-depth cap, per-subscription
  fire-rate limit.
- Join results deliver into the registering (owner) session by default
  instead of a detached, unread child session; parent nesting falls back
  from the event's session through the owner stamp to resolve_root.
- Registration advisories for turn-event filters naming a nonexistent
  session, and for a join key wired to the same event source as a
  sibling key.
- Policy aid: narrowed sub-agents are told their allowed/denied function
  surface directly in the system prompt instead of discovering it via a
  denied functions::list call.
- Heal dangling function_calls left by interrupted/compacted turns
  before the next generate step.
- Docs: tech spec, skill, and all prompt variants updated for the
  react/join doctrine.
Replace the raw-JSON fallback card with an instrument-panel view:
policy chips (model/mode/turns/thinking/output/allow/deny), the task
rendered as markdown, and the child's result as markdown, highlighted
JSON, or the direct-call child ids. Guard errors and failed children
route through the existing SandboxErrorView; the approval gate gets a
policy-first preview. Session ids link to the child conversation via
the sidebar's select when the console knows the session.

Includes Zod parsers for the spawn wire schema (excerpt-tolerant),
fixtures for all six card states, a gated-spawn playground scenario,
and parser tests locking envelope unwrapping and error-before-success
dispatch.
Upstream #388 added a test TurnRecord literal that predates this branch's
display_parent_session_id / spawned_by_subscription_id / reactive_depth
fields; the rebase merged clean but test compilation broke.
Every harness::spawn must pass session_id: a short readable job slug plus
a few random characters (fetch-headlines-b4k9), replacing the opaque
engine-minted UUIDs in the console tree. Never the parent session id as a
prefix; the random suffix carries the run-uniqueness guarantee instead
(a reused id silently resumes the old session). Scoped to direct spawn
calls only — in a react trigger's metadata a fixed session_id funnels
every firing into one session and re-aims join delivery. Fan-in doctrine
updated to the same naming across all five prompt variants.
A notification or steering user entry injected while a call window is open
(a parked harness::spawn holds one open for minutes) lands between
function_call and function_result in the durable transcript. Every wire
mapper only repaired MISSING results (orphan placeholder) — a DISPLACED
result survived to the wire as assistant(tool_use) / user(text) /
user(tool_result), which Anthropic 400s ('tool_use ids were found without
tool_result blocks immediately after') and OpenAI/xAI/Responses reject as
a user row between tool_calls and its tool rows. The durable transcript
replays the shape on every retry, permanently wedging the turn.

Fix: shared llm_router::types::messages::reorder_displaced_results runs
first in all four providers' to_wire_messages — each FunctionResult moves
directly after the assistant that emitted its call, order preserved,
orphan results untouched. Wedged sessions self-heal: the transcript
itself was never illegal, only the wire projection.

Repro test written first and failed with the exact live shape; regression
tests in all four providers plus unit tests on the shared helper.
…d reply

A user entry appended while a step is generating (or assembling — the
compaction/hook window) lands before that step's assistant entry in the
durable log. The steering check then re-generates, but the assembled
context ENDS with a call-less assistant message — a prefill request newer
Anthropic models reject ('This model does not support assistant message
prefill. The conversation must end with a user message.'), wedging the
turn on every retry. Older models silently accepted prefill, hiding this
path.

Fix: rotate_mid_generation_users presents arrivals after the previous
step's watermark AFTER the reply they interrupted — semantically exact,
the model answered without seeing them. Two invariants hardened by
adversarial review:
- the new watermark is assigned only after router.chat returns; the
  pre-generate put_turn persists the OLD one, so a redelivered step keeps
  its rotation window instead of re-issuing the rejected shape forever
- rotation runs on the FINAL assembled values, never on the candidate:
  compaction persists tail_start_entry_id as a log-order cursor indexed
  from the candidate, and rotating first would silently drop the rotated
  message from every future window

Also: has_user_after_watermark now loads include_custom=true, matching
the list the watermark comes from (a watermark landing on a custom entry
silently disabled the steering check).
Main's fs-scope refactor (#397) introduced the bare DispatchError struct;
the react-bridge reconcile pass logs it with %e, which needs Display —
an error type should carry one anyway.
@andersonleal
andersonleal force-pushed the feat/harness-react-bridge branch from 5b99f21 to e93fbe0 Compare July 3, 2026 19:08
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 31 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

@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: 4

🧹 Nitpick comments (6)
llm-router/src/types/messages.rs (1)

142-220: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider a regression test for the multi-call, multi-owner interleaving case.

Current tests cover single-call displacement and same-owner multi-call adjacency, but not the case where two different assistant messages each with their own call are separated by an interleaved user message, with only one result displaced (e.g., assistant(call t1) → result t1 → assistant(call t2) → user(notification) → result t2). Manual trace confirms current logic handles this correctly, but a test would lock in this guarantee given this is safety-net logic for a live 400 bug fix.

🧪 Suggested additional test
#[test]
fn multi_owner_interleaved_displacement_keeps_each_result_with_its_call() {
    let msgs = vec![
        assistant(vec![call("t1")]),
        user_text("[notification] progress"),
        assistant(vec![call("t2")]),
        result("t1"),
        result("t2"),
    ];
    let out = reorder_displaced_results(&msgs);
    assert!(matches!(out[0], AgentMessage::Assistant(_)));
    assert!(is_result(out[1], "t1"));
    assert!(matches!(out[2], AgentMessage::User(_)));
    assert!(matches!(out[3], AgentMessage::Assistant(_)));
    assert!(is_result(out[4], "t2"));
}
🤖 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 `@llm-router/src/types/messages.rs` around lines 142 - 220, Add a regression
test in the existing messages tests for the multi-owner interleaving case around
reorder_displaced_results. The current coverage in helper functions like
assistant, user_text, result, and call only verifies single-call displacement
and same-owner adjacency, so add a test that builds two Assistant messages with
separate function calls and an interleaved User notification, then asserts each
FunctionResult is reordered immediately after its matching Assistant. Keep the
test focused on the reorder_displaced_results behavior and use the existing
is_result helper for matching.
console/web/src/components/chat/harness/parsers.ts (2)

53-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider z.discriminatedUnion for outputContractSchema.

The two variants are tagged by type ('text' | 'json'); z.discriminatedUnion('type', [...]) gives clearer error messages and avoids Zod evaluating every branch.

♻️ Proposed refactor
-export const outputContractSchema = z.union([
+export const outputContractSchema = z.discriminatedUnion('type', [
   z.object({ type: z.literal('text') }),
   z.object({ type: z.literal('json'), schema: z.unknown().optional() }),
 ])
🤖 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 `@console/web/src/components/chat/harness/parsers.ts` around lines 53 - 57, The
`outputContractSchema` union in `parsers.ts` is a tagged union on `type`, so
replace the current `z.union([...])` with `z.discriminatedUnion('type', [...])`
using the same `text` and `json` object variants. Keep `OutputContract` inferred
from `outputContractSchema` so validation stays equivalent while improving
branch selection and error messages.

80-88: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider z.looseObject() over deprecated .passthrough().

Zod v4 deprecates .passthrough() (still functional, not removed) in favor of the top-level z.looseObject() constructor.

♻️ Proposed refactor
-export const taskMessageSchema = z
-  .object({
-    role: z.string().optional(),
-    content: z.array(z.unknown()).optional(),
-  })
-  .passthrough()
+export const taskMessageSchema = z.looseObject({
+  role: z.string().optional(),
+  content: z.array(z.unknown()).optional(),
+})
🤖 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 `@console/web/src/components/chat/harness/parsers.ts` around lines 80 - 88,
`taskMessageSchema` currently uses the deprecated `.passthrough()` on a
`z.object`, so update the schema definition to use `z.looseObject()` instead
while preserving the same fields (`role`, `content`) and the existing
`spawnTaskSchema`/`SpawnTask` behavior. Keep the refactor localized to the
parser definitions in `parsers.ts` so any unknown properties remain accepted as
before.
harness/src/subscriptions/reconcile.rs (1)

162-199: 🚀 Performance & Scalability | 🔵 Trivial

sweep_owner cost scales with total durable bindings per session delete.

Each session::deleted triggers a full list of react + notify bindings plus one engine::registered-triggers::info dispatch per binding. At a large standing binding count this makes chat deletion O(total bindings) engine round-trips. Fine at current scale; if binding counts grow, consider an owner-indexed listing (e.g. filter by owner metadata engine-side) or batching the info lookups.

🤖 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/subscriptions/reconcile.rs` around lines 162 - 199, The
sweep_owner path currently does a full list plus per-binding info dispatch for
every session delete, making session::deleted scale linearly with total durable
bindings. Update sweep_owner to avoid iterating every binding one-by-one: prefer
an engine-side owner-indexed lookup or filtering by owner metadata before
unregistering, and if that is not available, batch the registered-triggers::info
lookups/dispatches instead of issuing one round-trip per id. Keep the existing
ownership check and unregister_trigger behavior intact while reducing the number
of engine calls in sweep_owner.
harness/src/types/turn.rs (1)

264-266: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Round-trip test doesn't exercise the new fields with actual values.

turn_record_round_trips_through_json uses record(), which sets display_parent_session_id, spawned_by_subscription_id, and reactive_depth all to None. Since these are skipped when None, the test never verifies the fields actually serialize/deserialize correctly under their real key names — a future typo in a #[serde(rename)] or field name would slip through.

♻️ Suggested test strengthening
     #[test]
     fn turn_record_round_trips_through_json() {
         let mut r = record();
         r.calls
             .insert("a".into(), cp(CallState::Pending, Some("s_child")));
+        r.display_parent_session_id = Some("s_parent".into());
+        r.spawned_by_subscription_id = Some("sub_1".into());
+        r.reactive_depth = Some(2);
         let back: TurnRecord = serde_json::from_value(serde_json::to_value(&r).unwrap()).unwrap();
         assert_eq!(back, r);
     }

Also applies to: 330-337

🤖 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/types/turn.rs` around lines 264 - 266, The round-trip test for
TurnRecord is only using record() values where display_parent_session_id,
spawned_by_subscription_id, and reactive_depth are None, so it never verifies
their JSON keys. Update turn_record_round_trips_through_json to construct a
TurnRecord with actual non-None values for those fields, then assert it
serializes and deserializes correctly through serde using the TurnRecord type
and its round-trip test helper so the real field names are exercised.
harness/src/functions/react.rs (1)

338-360: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift

No TTL/reconciliation for orphaned join accumulator records.

Unlike react bindings (swept on startup and on session delete per subscriptions/reconcile.rs), a JOIN_SCOPE accumulator record has no equivalent cleanup path. If a predecessor never fires (its session/subscription is deleted before completion, or its metadata was malformed and silently ignored per the docs), the record sits in iii-state forever below expected, with no sweep to reclaim it.

Worth a follow-up: extend the startup/session-delete reconciliation to also GC stale harness::react_join records (e.g., by owner-session stamp or age), mirroring the existing react-binding reconciliation.

🤖 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 338 - 360, The JOIN_SCOPE
accumulator path in harness::react::react_join currently writes state via
state_update and never gets reclaimed if the join never completes. Add a
reconciliation/TTL cleanup path for stale harness::react_join records, similar
to the existing react binding sweep in subscriptions/reconcile.rs, so orphaned
join accumulators are garbage-collected on startup and/or session delete. Use
the join record identifiers (join.id, owner-session stamp, or age metadata) to
detect incomplete records and remove them during the reconciliation flow.
🤖 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/prompts/kimi.txt`:
- Around line 174-180: The fan-in session_id guidance in kimi.txt conflicts with
the shared spawn rules and the sibling prompt files. Update the wording around
the child-session naming guidance so it matches the canonical rule used in
SKILL.md and the other prompt files, and ensure the same instruction is applied
consistently in the kimi prompt section that references harness::spawn and react
trigger metadata.

In `@harness/src/functions/react.rs`:
- Around line 405-417: The join cleanup in react’s fire path can leave a stale
accumulator behind when state_delete fails, which permanently wedges rearmed
joins. Update the join handling around join_delivery_session,
gather_inputs_task, and state_delete so the accumulator is reliably removed
before the next cycle: either add a bounded retry/backoff around state_delete
and only warn after exhausting retries, or change the fire gating in the join
path from a fixed fire == 1 check to a generation-based guard so a leftover
record cannot block future firings.
- Around line 244-249: The fallback fire-gate key in the gate_key computation
currently hashes only model, task, and session_id, which causes all predecessors
of the same join to share one budget when subscription_id is missing. Update the
fallback in react.rs so the hash also incorporates the join key from the spec
(the same join identifier used by the join/predecessor flow), ensuring each join
predecessor gets a distinct gate key while preserving the existing
subscription_id path.

In `@harness/src/functions/spawn.rs`:
- Around line 65-74: SpawnRequest is exposing reactive metadata that should
remain internal, allowing model-reachable callers to spoof
spawned_by_subscription_id and reactive_depth and bypass the loop guards. Remove
these fields from the public SpawnRequest schema and ensure
spawn_child/seed_child only populate them on the react-internal path inside
harness::react, where the self-edge breaker and MAX_REACTIVE_DEPTH logic consume
them.

---

Nitpick comments:
In `@console/web/src/components/chat/harness/parsers.ts`:
- Around line 53-57: The `outputContractSchema` union in `parsers.ts` is a
tagged union on `type`, so replace the current `z.union([...])` with
`z.discriminatedUnion('type', [...])` using the same `text` and `json` object
variants. Keep `OutputContract` inferred from `outputContractSchema` so
validation stays equivalent while improving branch selection and error messages.
- Around line 80-88: `taskMessageSchema` currently uses the deprecated
`.passthrough()` on a `z.object`, so update the schema definition to use
`z.looseObject()` instead while preserving the same fields (`role`, `content`)
and the existing `spawnTaskSchema`/`SpawnTask` behavior. Keep the refactor
localized to the parser definitions in `parsers.ts` so any unknown properties
remain accepted as before.

In `@harness/src/functions/react.rs`:
- Around line 338-360: The JOIN_SCOPE accumulator path in
harness::react::react_join currently writes state via state_update and never
gets reclaimed if the join never completes. Add a reconciliation/TTL cleanup
path for stale harness::react_join records, similar to the existing react
binding sweep in subscriptions/reconcile.rs, so orphaned join accumulators are
garbage-collected on startup and/or session delete. Use the join record
identifiers (join.id, owner-session stamp, or age metadata) to detect incomplete
records and remove them during the reconciliation flow.

In `@harness/src/subscriptions/reconcile.rs`:
- Around line 162-199: The sweep_owner path currently does a full list plus
per-binding info dispatch for every session delete, making session::deleted
scale linearly with total durable bindings. Update sweep_owner to avoid
iterating every binding one-by-one: prefer an engine-side owner-indexed lookup
or filtering by owner metadata before unregistering, and if that is not
available, batch the registered-triggers::info lookups/dispatches instead of
issuing one round-trip per id. Keep the existing ownership check and
unregister_trigger behavior intact while reducing the number of engine calls in
sweep_owner.

In `@harness/src/types/turn.rs`:
- Around line 264-266: The round-trip test for TurnRecord is only using record()
values where display_parent_session_id, spawned_by_subscription_id, and
reactive_depth are None, so it never verifies their JSON keys. Update
turn_record_round_trips_through_json to construct a TurnRecord with actual
non-None values for those fields, then assert it serializes and deserializes
correctly through serde using the TurnRecord type and its round-trip test helper
so the real field names are exercised.

In `@llm-router/src/types/messages.rs`:
- Around line 142-220: Add a regression test in the existing messages tests for
the multi-owner interleaving case around reorder_displaced_results. The current
coverage in helper functions like assistant, user_text, result, and call only
verifies single-call displacement and same-owner adjacency, so add a test that
builds two Assistant messages with separate function calls and an interleaved
User notification, then asserts each FunctionResult is reordered immediately
after its matching Assistant. Keep the test focused on the
reorder_displaced_results behavior and use the existing is_result helper for
matching.
🪄 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

Run ID: 5b8f3c6f-c444-470b-a475-bbdf2574b760

📥 Commits

Reviewing files that changed from the base of the PR and between 62d5f42 and 5b99f21.

⛔ Files ignored due to path filters (2)
  • provider-anthropic/Cargo.lock is excluded by !**/*.lock
  • provider-openai/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (39)
  • console/web/src/components/chat/harness/SpawnView.tsx
  • console/web/src/components/chat/harness/__tests__/parsers.test.ts
  • console/web/src/components/chat/harness/index.tsx
  • console/web/src/components/chat/harness/parsers.ts
  • console/web/src/stories/fixtures/harness-fixtures.ts
  • console/web/src/stories/playground/Agent.stories.tsx
  • console/web/src/stories/playground/scenarios/harness-spawn.ts
  • console/web/src/stories/playground/scenarios/index.ts
  • harness/prompts/anthropic.txt
  • harness/prompts/cli.txt
  • harness/prompts/default.txt
  • harness/prompts/gpt.txt
  • harness/prompts/kimi.txt
  • harness/skills/SKILL.md
  • harness/src/clients/session.rs
  • harness/src/config.rs
  • harness/src/deps.rs
  • harness/src/events.rs
  • harness/src/functions/mod.rs
  • harness/src/functions/on_session_deleted.rs
  • harness/src/functions/react.rs
  • harness/src/functions/send.rs
  • harness/src/functions/spawn.rs
  • harness/src/functions/subscribe.rs
  • harness/src/main.rs
  • harness/src/subagent.rs
  • harness/src/subscriptions/mod.rs
  • harness/src/subscriptions/reconcile.rs
  • harness/src/subscriptions/registry.rs
  • harness/src/turn_loop.rs
  • harness/src/types/turn.rs
  • harness/tests/golden/schemas/harness.spawn.json
  • iii-permissions.yaml
  • llm-router/src/types/messages.rs
  • provider-anthropic/src/wire/messages.rs
  • provider-openai-codex/src/wire/messages.rs
  • provider-openai/src/wire/messages.rs
  • provider-xai/src/wire/messages.rs
  • tech-specs/2026-06-agentic/harness.md

Comment thread harness/prompts/kimi.txt
Comment thread harness/src/functions/react.rs
Comment thread harness/src/functions/react.rs
Comment thread harness/src/functions/spawn.rs
Restore tech-specs/2026-06-agentic/harness.md to main's version — the
react-bridge spec additions come out of this PR.
- react: include the join (id, key) in the fallback fire-gate hash —
  state-based join predecessors share the whole downstream spec except
  their key, so a wide join shared one 10-fires/min budget and tripped
  the breaker spuriously
- react: retry the join accumulator delete (3 attempts) — a failed
  delete left fire=1 behind, permanently wedging a rearmed join's
  fire-once guard; persistent failure on a rearmed join now logs at
  error level with the recovery path
- spawn: strip spawned_by_subscription_id / reactive_depth on the
  model-reachable dispatch path — react-internal bookkeeping a model
  could spoof to defeat the self-edge breaker and depth cap
- skills: align SKILL.md fan-in naming with the prompt doctrine (slug +
  random suffix, never the originating session id as a prefix)
- tests: multi-owner displaced-result reorder case; round-trip the
  react-bridge TurnRecord fields with real values
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.

2 participants