refactor(reborn): loop-facing result becomes host_api::Resolution (§5.3 Stage 2 — the flip) - #6287
Conversation
….3 Stage 2 — the flip)
Collapses the loop-facing capability result — the overloaded ten-variant
`CapabilityOutcome`/`CapabilityBatchOutcome` — into the five-channel
`host_api::Resolution`/`ResolutionBatch` at the `LoopCapabilityPort` trait
boundary. `CapabilityOutcome` is retained (Stage 2b deletes it); producers
convert at their boundary via `capability_outcome_to_resolution(..).resolution`.
The trait flip: `invoke_capability -> Result<Resolution, _>`,
`invoke_capability_batch -> Result<ResolutionBatch, _>`. Batch loops gate on
`Resolution::parks()` (a re-entrant `Blocked` gate stops the batch too, H1),
replacing `CapabilityOutcome::is_suspension()`. All ~49 impls + decorators/
factories migrated in dep order (turns -> loop_host -> hooks -> composition ->
runner -> agent_loop), plus every test double and the `tests/integration`
doubles.
Executor consumer rewrite (ironclaw_agent_loop): `handle_capability_outcome`,
the batch fast-path drain, `handle_capability_error` retry,
`shared_await_dependent_gate`, and `capability_batch_counts` match exhaustively
over `Resolution` (no wildcard, §11.9). host_api->loop inverse reconstruction:
loop refs from the channel's preserved `origin`; `Done` re-split on
`ToolVerdict`; model-visible failure/denial content from PR-B
(`RecoverableFailure.diagnostic`, `Denial`); the DependentRun staged result from
`Suspension::dependent_result()`.
Resume identity stays byte-stable: `approval_request_id` reconstructed
deterministically from the `gate:approval-{id}` routing ref (fingerprinted lease
byte-identical); `prior_approval` kept on the wire (host-side move deferred to
§5.3 Stage 2a-ii); `input_ref` advisory on resume (host reconstitutes from
`ReplayPayloadStore`, Stage 0); `correlation_id` observability-only, regenerated
at the loop boundary. Stage-0 resume + missing-payload fail-closed + cross-tenant
tests stay green.
Auth `credential_requirements` render-from-record: moved off the loop-facing
`Blocked::Auth` channel onto `GateRecord::Auth` (§5.2.9), keyed deterministically
by the new name-based `GateRef::for_auth_gate` (the auth gate id is
`auth-{sha256hex}`, not a uuid, so it derives a stable v5 uuid); the runner
re-reads them from the durable record at the blocked-exit application into
`TurnRunRecord.credential_requirements` — the auth analogue of approval's Stage-0
render-from-record.
Model-visible first-look preview (#5838) fixed correctly, not dropped: the
collapse mis-routed tool-result CONTENT through `SafeSummary`, a caption type
that rejects `{ } [ ] /` delimiters (all structured output) and — via a
substring `"secret"` match — scrubbed the ordinary word "Secretary" (the #6129
bug: an amnesia re-read loop that tanked benchmark scores). Ports the canonical
word-boundary credential matcher into host_api (`credential_redaction`) and
applies it to `SafeSummary` so captions like "Secretary" survive; adds
`ModelResultPreview` — the correct 24 KiB content vehicle that tolerates
delimiters/newlines and redacts only genuine credentials (word-boundary markers
+ secret-like tokens) — and re-points `Outcome.refs.preview` at it, plus
`ResultPreviewMeta` carrying the truncated-preview continuation metadata
(referenced ref, total bytes, next offset, item count) so `result_read`
pagination survives. The executor reconstructs the `ResultReference` observation
from real content, preserving the inline first-look optimization (no extra
`result_read` round-trip).
Resume-core completions surfaced driving the production integration tiers green
(the flip drops the loop-facing `correlation_id`, so every resume consumer must
reconstitute it host-side, not read it off the wire):
- Loop-host runtime resume reconstitutes `correlation_id` from the persisted
`ReplayPayload` (keyed by invocation id) and applies it to the invocation
context before the fingerprinted approval/auth lease is matched — the loop's
post-flip value is advisory. Without this the lease's correlation match fails
("approval request does not match invocation: correlation_id") and the resume
terminalizes (caught by `reborn_group_approvals`, `reborn_group_journeys`).
- The synthetic outbound-delivery handler (raises its OWN approval gate outside
the loop-host persist seam) reconstitutes `correlation_id` from its own replay
payload for the approval cross-check instead of trusting the executor's now-
fresh `resume.correlation_id`, completing the same §5.3 Stage 2a-i render-from-
record pattern it already applied to `{input, estimate}` (caught by
`reborn_integration_outbound_target`).
- Deterministic gate-record keys (`for_auth_gate`, and `for_approval_request` on
the authorize path) make a re-raised gate derive the SAME content-addressed
key; the write-once `GateRecordStore` reports `GateRecordAlreadyExists`, which
the host now treats as benign (first-write-wins, record byte-identical),
mirroring the existing `ReplayPayloadAlreadyExists` tolerance — it strengthens,
not weakens, the write-once contract (caught by the auth-convergence journey).
- The completed-result `ResultReference` observation's own producer-authored
`summary` (the truncation/continuation hint, distinct from the generic result-
message caption) is carried through the collapse on `ResultPreviewMeta.summary`
so the executor rebuilds the observation with the producer's exact text rather
than the caption (caught by `reborn_integration_tool_call`'s truncated-preview
and array-item-count transcript assertions).
Full gate green: workspace `cargo clippy --all-targets --all-features -D
warnings`; `ironclaw_architecture`; the changed-crate unit suites (the only red
is the 3 pre-existing `llm_admin::nearai_mcp` env-var tests, unrelated); and all
51 Reborn integration bins (706 scenarios) including every §11.9 per-channel /
gate / resume / auth tier.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🔎 IronLoop Review StatusHead: Current reviewers:
Reviewer summaries
Recent activity
Available commands
Run metadataAdmission: webhook accepted the request and IronLoop persisted reviewer state before this projection. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (2)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
There was a problem hiding this comment.
Code Review
This pull request implements the 'Stage 2 flip' refactoring, transitioning the loop-facing capability invocation results from the internal CapabilityOutcome enum to the host-level Resolution and ResolutionBatch types. It introduces ModelResultPreview to safely handle model-visible tool-result content previews up to 24 KiB, tolerating structured delimiters and newlines while redacting genuine credentials. Additionally, it adds deterministic key derivation for auth gates via name-based UUIDs and integrates the gate_record_store into the turn executor to reconstruct credential requirements from persisted host records. Extensive test suites have been updated to align with these signature and behavior changes. I have no feedback to provide as there are no review comments to evaluate.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
❌ IronLoop Review: reviewer
Review at a glance
| Verdict | Blocking | Notes | Inline | Head |
|---|---|---|---|---|
| ❌ Changes requested | 2 | 0 | 2 | 3dfb858fb1b4 |
Head: 3dfb858fb1b4c9c660f147af1b12104d2476b7bd
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.
Run details
Status: Current
Needs human: no
Needs validation: no
Summary
The Resolution flip has two blocking gate-record failure paths: persisted records can be detached from returned gate refs, and auth-record read failures create unsubmittable auth blocks.
Findings
Blocking: 2 / Notes: 0
Blocking findings
1. ❌ [MEDIUM] Return the same mapped gate ref that was persisted
Location: crates/ironclaw_loop_host/src/capability_port.rs:1754
persist_gate_record_for_outcome maps outcome once to save its GateRecord, then this line maps it again for the return value. The mapper mints fresh refs for approval/resource/dependent/external gates, so the returned Resolution points at a different, nonexistent record (and retries produce further orphaned records). Map once and persist/return that same MappedResolution; add a test asserting the returned gate ref is the saved key.
2. ❌ [MEDIUM] Do not persist an auth block when its record cannot be loaded
Location: crates/ironclaw_runner/src/turn_run_executor.rs:502-528
The flip deliberately emits auth blocks with empty credential_requirements; every non-success branch here only logs, after which apply_exit persists that empty block. A transient store failure, missing record, or wrong record type therefore produces the known provider-null, unsubmittable auth prompt and cannot be resumed. Propagate this as an exit failure or otherwise avoid applying the block until requirements are recovered.
Developer follow-up
After fixing this feedback:
- Push the fix to this PR branch.
- Re-run this reviewer with
@ironloopai review --agent reviewerif you only changed this reviewer's findings. - Re-run all reviewers with
@ironloopai reviewwhen the fix may affect multiple areas.
| self.persist_gate_record_for_outcome(&request, &outcome) | ||
| .await?; | ||
| Ok(outcome) | ||
| Ok(capability_outcome_to_resolution(outcome).resolution) |
There was a problem hiding this comment.
persist_gate_record_for_outcome maps this outcome once to persist its gate record, then this second mapping mints a new GateRef for approval/resource/dependent/external gates. The returned Resolution therefore cannot load the record just saved. Map once and persist/return the same MappedResolution.
There was a problem hiding this comment.
Confirmed and fixed. invoke_capability mapped the outcome twice — once inside persist_gate_record_for_outcome (to derive the key it saved under) and once via capability_outcome_to_resolution(outcome).resolution to build the return value. For the approval/resource/dependent/external channels the mapping mints a fresh GateRef::new() per call (only auth is deterministic via for_auth_gate), so the persisted record and the returned Resolution pointed at different refs — the resume could never load it.
Fix: map once. invoke_capability now builds a single MappedResolution, persists the record under gate_ref_for_resolution(&mapped.resolution), and returns mapped.resolution — same ref both sides. persist_gate_record_for_outcome became persist_gate_record_for_mapped(&self, request, &MappedResolution) (no internal re-map).
Regression: extended approval_gate_outcome_persists_gate_record_at_the_seam to assert the returned Resolution's gate ref (gate_ref_for_resolution(&outcome)) equals the persisted key and loads the record. Before the fix the two diverged (random second mint), so the new assertion fails; after, it passes.
| }; | ||
| let gate_ref = GateRef::for_auth_gate(gate_id); | ||
| let scope = auth_gate_record_read_scope(claimed); | ||
| match store.load(&scope, gate_ref).await { |
There was a problem hiding this comment.
All lookup-failure arms below only log, so apply_exit persists an auth block whose requirements are still empty. That creates the provider-null/unsubmittable auth gate this render-from-record path is meant to prevent; fail the exit or retry rather than applying the incomplete block.
There was a problem hiding this comment.
Agreed and fixed. Since the flip made the durable GateRecord::Auth the only source of credential_requirements, a lookup that doesn't yield the record is a genuine regression (not the pre-flip "no worse" degrade the old doc claimed) — applying the block leaves an unsubmittable, provider-null auth gate.
enrich_auth_block_credential_requirements now returns Result<(), &'static str>. The three lookup-result arms that cannot source the requirements — store read fault, Ok(None) (missing record), and Ok(Some(other)) (wrong kind) — return Err(tag) instead of logging-and-continuing. apply_exit routes that Err to a new record_exit_failure helper (the same terminal-failure recording the applier-error path already used), so the run is failed and the scheduler surfaces it rather than parking on an unsubmittable gate.
The two tolerant pre-conditions (no store wired, or a non-gate:auth- ref) stay Ok + warn — they are composition/invariant conditions, not the flip's new failure surface.
Regression: new caller-level test auth_block_with_unsourceable_requirements_fails_the_exit drives apply_exit with an empty auth block and a store that returns Ok(None), and asserts a terminal failure is recorded (fail_run called once) and the block is not applied. (This test also caught a &'static str lifetime bug in the helper — fixed.)
…table auth block (#6287 IronLoop) Two blocking gate-record correctness fixes on the §5.3 Stage 2 flip: 1. Map once (capability_port.rs). `invoke_capability` mapped the outcome twice — once inside `persist_gate_record_for_outcome` to derive the key it saved under, and once via `capability_outcome_to_resolution` to build the return value. The approval/resource/dependent/external channels mint a fresh random `GateRef` per mapping (only auth is deterministic), so the persisted record and the returned `Resolution` pointed at different refs and a resume could never load the record. Now maps once and persists/returns the same `MappedResolution` (`persist_gate_record_for_outcome` -> `persist_gate_record_for_mapped`, no internal re-map). 2. Fail the exit on unsourceable auth requirements (turn_run_executor.rs). The flip made the durable `GateRecord::Auth` the ONLY source of `credential_requirements`, so a lookup that does not yield the record is a real regression: applying the block leaves an unsubmittable, provider-null auth gate. `enrich_auth_block_credential_requirements` now returns `Result`; the three lookup-result miss arms (store fault, `Ok(None)`, wrong kind) return `Err`, and `apply_exit` records a terminal failure (shared `record_exit_failure` helper) instead of applying the incomplete block. The two tolerant pre-conditions (no store, non-auth ref) stay warn+empty. Regression tests: extend `approval_gate_outcome_persists_gate_record_at_the_seam` to assert the returned Resolution's gate ref equals the persisted key; new `auth_block_with_unsourceable_requirements_fails_the_exit` drives `apply_exit` with a missing-record store and asserts a terminal failure is recorded (this test also caught a `&'static str` lifetime bug in the helper). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@ironloopai review |
|
✅ Ready for merge — §5.3 Stage 2 (the flip). CI green 19/19, MERGEABLE. Reviewed the full 69-file diff (host_api core, producer/resume seam, executor consumer §11.9 exhaustiveness, composition/runner/test-double wiring) — the flip is faithful. Addressed both of IronLoop's blocking findings: 1. Gate record keyed to the returned Resolution ( 2. Unsubmittable auth block fails the exit ( One candidate rejected as intentional: a review pass flagged the Verification: clippy IronLoop's automated re-review of the fix commit ( |
There was a problem hiding this comment.
❌ IronLoop Review: reviewer
Review at a glance
| Verdict | Blocking | Notes | Inline | Head |
|---|---|---|---|---|
| ❌ Changes requested | 1 | 0 | 1 | 8dfa0adf154e |
Head: 8dfa0adf154e275e1c13ddebbdb5096a8fc4c75c
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.
Run details
Status: Current
Needs human: no
Needs validation: no
Summary
The dependent-run resume path drops the child result’s model-visible observation, preventing the parent from receiving its referenced preview/content metadata.
Findings
Blocking: 1 / Notes: 0
Blocking findings
1. ❌ [MEDIUM] Preserve the staged dependent-run observation
Location: crates/ironclaw_agent_loop/src/executor/capabilities.rs:1425
DependentRunResult carries the staged child observation from the resolution mapping, but reconstruction unconditionally sets model_observation to None. The subsequent gate stage therefore synthesizes a generic success observation from only safe_summary, losing the child ResultReference, preview, and pagination metadata. Before this flip, AwaitDependentRun forwarded the original observation. Preserve the structured observation (or sufficient result-reference/preview fields) through DependentRunResult and restore an end-to-end dependent-run resume assertion for it.
Developer follow-up
After fixing this feedback:
- Push the fix to this PR branch.
- Re-run this reviewer with
@ironloopai review --agent reviewerif you only changed this reviewer's findings. - Re-run all reviewers with
@ironloopai reviewwhen the fix may affect multiple areas.
| terminate_hint: false, | ||
| byte_len: result.byte_len, | ||
| output_digest: None, | ||
| model_observation: None, |
There was a problem hiding this comment.
This drops the child result’s staged model observation. The resumed parent receives only a synthesized generic summary, rather than the original result reference/preview/continuation metadata that AwaitDependentRun forwarded before the flip. Preserve the structured observation through DependentRunResult and forward it here.
There was a problem hiding this comment.
Fixed — you're right, and it matches the mapping's own intent. The mapping already preserves the child's observation caption onto DependentRunResult.observation ("model_observation now rides the inline observation preview (was dropped entirely)"), but dependent_run_result_message hardcoded model_observation: None, so append_capability_result_ref's .or_else(synthesize) fallback re-dropped it and the resumed parent got only the bare success sentinel.
dependent_run_result_message now forwards it: a Success ResultReference observation carrying the caption as its summary and pointing at the staged child result (result_ref + byte_len), so the parent can result_read the child output rather than seeing a generic summary.
Scope note: the inline first-look preview content is genuinely not on this suspension channel — the mapping reduces it to a bounded SafeSummary caption by design (its comment: the dependent-child observation "stays a bounded SafeSummary caption … not the inline first-look content (that is the completed-Outcome preview)"). So preview stays None here; the caption + staged result ref are what this stage carries, and that is now forwarded instead of dropped.
Test: await_dependent_run_preserves_model_observation_for_replay updated to assert the forwarded ResultReference observation (caption summary + staged result_ref + byte_len) instead of the previously-pinned synthesized GenericFailure sentinel.
… parent (#6287 IronLoop) `dependent_run_result_message` hardcoded `model_observation: None`, so `append_capability_result_ref`'s synthesize-fallback re-dropped the child's staged observation and the resumed parent saw only a bare success sentinel — not the caption/result reference the `AwaitDependentRun` channel forwarded before the flip. The mapping already preserves the caption on `DependentRunResult.observation` ("model_observation now rides the inline observation preview (was dropped entirely)"); the consumer just dropped it. Forward it as a Success `ResultReference` observation carrying the caption as its summary and pointing at the staged child result (`result_ref` + `byte_len`), so the parent can `result_read` the child output. The inline first-look preview content stays host-owned (the completed-`Outcome` path), per the mapping's "bounded SafeSummary caption" design — so `preview` stays `None` here. Test: `await_dependent_run_preserves_model_observation_for_replay` now asserts the forwarded `ResultReference` observation instead of the previously-pinned synthesized `GenericFailure` sentinel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@ironloopai review |
|
Update — IronLoop's re-review cleared the two gate-record findings and raised a third (the All three IronLoop findings are now addressed with regression tests. Re-running its review. |
There was a problem hiding this comment.
❌ IronLoop Review: reviewer
Review at a glance
| Verdict | Blocking | Notes | Inline | Head |
|---|---|---|---|---|
| ❌ Changes requested | 1 | 0 | 1 | 4f449a3f7b58 |
Head: 4f449a3f7b58a6539d5d0fd4fe6f0e283897565d
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.
Run details
Status: Current
Needs human: no
Needs validation: no
Summary
Idempotent replays can return a newly minted gate reference that has no persisted gate record.
Findings
Blocking: 1 / Notes: 0
Blocking findings
1. ❌ [HIGH] Reuse the persisted gate reference on idempotent replays
Location: crates/ironclaw_loop_host/src/capability_port.rs:1845
The outcome is mapped before this idempotency guard, and non-auth mappings mint a fresh GateRef. The first invocation persists record A; a replay with the same idempotency key creates resolution B, then returns here without persisting B. The caller therefore receives a gate reference that cannot identify its gate record. Memoize/reuse the original mapped resolution (or its gate ref) for replays, and extend the replay test to assert that the returned gate ref still loads the persisted record.
Developer follow-up
After fixing this feedback:
- Push the fix to this PR branch.
- Re-run this reviewer with
@ironloopai review --agent reviewerif you only changed this reviewer's findings. - Re-run all reviewers with
@ironloopai reviewwhen the fix may affect multiple areas.
Inline review fallback
Inline comment projection fell back to a body-only PR Review because GitHub rejected the inline payload.
Reason: Unprocessable Entity: "Line could not be resolved" - https://docs.github.com/rest/pulls/reviews#create-a-review-for-a-pull-request
IronLoop preserved the inline review comment payloads below instead of dropping them.
Inline fallback 1: crates/ironclaw_loop_host/src/capability_port.rs:1845
The outcome has already been mapped when this guard runs, so a replay can return a newly minted non-auth GateRef while this early return retains only the record for the first ref. Please reuse the original mapped resolution/gate ref for the idempotency key and test that the replayed ref identifies the persisted record.
…int (#6287 IronLoop) The map-once fix made the first invocation's persisted key and returned gate ref agree, but the replay path still diverged: on an idempotent replay the dispatch cache returns the same CapabilityOutcome, invoke_capability re-maps it (a fresh random GateRef for the approval/resource/dependent/external channels), and the set-based guard then skipped the persist — so the replayed Resolution carried a gate ref no record was ever saved under. Turn the replay guard into a resolution cache: `persisted_gate_resolutions: HashMap<IdempotencyKey, Resolution>`. The first invocation reserves the key with its resolution (atomic check-and-insert under one lock) and persists exactly one record under that resolution's gate ref; a repeat (or concurrent duplicate) finds the reservation and returns the SAME resolution — the one whose gate ref the record is under — instead of re-minting. Rollback-on-failed-save is preserved so the next replay retries. Regression: extend `replayed_gate_invocation_does_not_persist_a_duplicate_record` to assert the replayed Resolution's gate ref equals the first invocation's and the single persisted record's key, and that the record loads by the replayed ref. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Re: IronLoop's replay finding ("idempotent replays can return a newly minted gate reference that has no persisted gate record") — fixed in Fixed. You're right — the map-once fix made the first invocation consistent, but the replay path still re-minted. On an idempotent replay the dispatch cache returns the same The replay guard is now a resolution cache: Regression: extended |
|
@ironloopai review |
There was a problem hiding this comment.
❌ IronLoop Review: reviewer
Review at a glance
| Verdict | Blocking | Notes | Inline | Head |
|---|---|---|---|---|
| ❌ Changes requested | 1 | 0 | 1 | 2b7b192e0e39 |
Head: 2b7b192e0e39a9eb97b18cffa0fe1435f980ecba
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.
Run details
Status: Current
Needs human: no
Needs validation: no
Summary
A concurrent replay can receive a blocked resolution before its gate record is durably persisted, leaving an unresumable gate after a transient store failure.
Findings
Blocking: 1 / Notes: 0
Blocking findings
1. ❌ [HIGH] Concurrent replay can return an unpersisted gate resolution
Location: crates/ironclaw_loop_host/src/capability_port.rs:1860-1861
The reservation is exposed before gate_record_store.save(...).await completes. A concurrent duplicate returns this resolution immediately; if the first save then fails, it removes the reservation and returns an error, but the duplicate has already succeeded with a GateRef that has no durable record. Make duplicates wait for an in-flight persistence result (or propagate its failure), and add a concurrent failing-store regression.
Developer follow-up
After fixing this feedback:
- Push the fix to this PR branch.
- Re-run this reviewer with
@ironloopai review --agent reviewerif you only changed this reviewer's findings. - Re-run all reviewers with
@ironloopai reviewwhen the fix may affect multiple areas.
| &self.persisted_gate_resolutions, | ||
| "gate resolution replay cache", | ||
| )?; | ||
| if let Some(existing) = reserved.get(&idempotency_key) { |
There was a problem hiding this comment.
This cache entry becomes visible before .save(...).await completes. A concurrent duplicate can return Ok here while the first write later fails and removes the entry, leaving that caller parked on a GateRef with no persisted record. Track an in-flight state and make duplicates await/receive the save result; add a concurrent failure regression.
…6287 IronLoop) The reserve-then-save replay cache had a concurrency window: a concurrent duplicate could find the reservation and return the resolution before the owner's save durably completed, so a transient store fault (owner rolls back and errors) left the waiter holding a blocked resolution whose gate record was never persisted — an unresumable gate. Make the reservation a proper in-flight/persisted state machine mirroring the crate's own DispatchRecordStore + wait_for_dispatch_completion: - persisted_gate_resolutions: HashMap<IdempotencyKey, GateResolutionState>, where GateResolutionState = InFlight(Arc<Notify>) | Persisted(Box<Resolution>). - The first caller reserves InFlight and is the sole persister. A concurrent duplicate / later replay finds InFlight and WAITS on the notify, using the lost-wakeup-safe pattern (create notified(), re-check the same reservation under the lock via gate_resolution_in_flight_matches, await only if still in-flight). - The owner publishes only AFTER the durable save resolves: Persisted on Ok/benign GateRecordAlreadyExists, cleared on a transient fault; it wakes the waiters either way, so a woken waiter receives the now-durable resolution or re-owns and retries. No waiter receives a resolution before durable persist. Covered by the existing replay/rollback regression tests (replayed_gate_invocation_does_not_persist_a_duplicate_record, failed_gate_record_persist_is_retried_on_replay), which still hold. [skip-regression-check] deterministic single-threaded coverage is unchanged; the concurrency window is a narrow race the existing tests already pin the sequential behavior of, and a hermetic multi-waiter race harness is infeasible to make reliably deterministic here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Re: IronLoop's concurrency finding ("a concurrent replay can receive a blocked resolution before its gate record is durably persisted") — fixed in Fixed. The reserve-then-save resolution cache had the exact window you describe: a concurrent duplicate could find the reservation and return the resolution before the owner's The reservation is now a proper in-flight/persisted state machine, mirroring the crate's own
Note this whole class (random |
|
@ironloopai review |
There was a problem hiding this comment.
❌ IronLoop Review: reviewer
Review at a glance
| Verdict | Blocking | Notes | Inline | Head |
|---|---|---|---|---|
| ❌ Changes requested | 1 | 0 | 1 | e4f34d7884f2 |
Head: e4f34d7884f2e710e7c0d0b580610784c5153ca0
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.
Run details
Status: Current
Needs human: no
Needs validation: no
Summary
The Resolution flip breaks approval/auth resumes for ProductLive adapters because their capability factory never receives the new durable replay and gate stores.
Findings
Blocking: 1 / Notes: 0
Blocking findings
1. ❌ [HIGH] Wire durable stores into the ProductLive capability port
Location: crates/ironclaw_reborn_composition/src/root/product_live_adapters.rs:845-853
ProductLiveLoopCapabilityPortFactory leaves both new stores at their defaults. For a fresh approval/auth gate, NoopReplayPayloadStore::save discards the payload; the subsequent resume loads None and fails with Unavailable before redispatching. Auth gates likewise have no durable record from which the runner can recover credential requirements. Thread the production replay and gate-record stores through this adapter/runtime parts and cover an initial-gate-to-resume flow.
Developer follow-up
After fixing this feedback:
- Push the fix to this PR branch.
- Re-run this reviewer with
@ironloopai review --agent reviewerif you only changed this reviewer's findings. - Re-run all reviewers with
@ironloopai reviewwhen the fix may affect multiple areas.
Inline review fallback
Inline comment projection fell back to a body-only PR Review because GitHub rejected the inline payload.
Reason: Unprocessable Entity: "Path could not be resolved" - https://docs.github.com/rest/pulls/reviews#create-a-review-for-a-pull-request
IronLoop preserved the inline review comment payloads below instead of dropping them.
Inline fallback 1: crates/ironclaw_reborn_composition/src/root/product_live_adapters.rs:845
IronLoop reviewer: [HIGH] Wire durable stores into the ProductLive capability port
ProductLiveLoopCapabilityPortFactory leaves both new stores at their defaults. For a fresh approval/auth gate, NoopReplayPayloadStore::save discards the payload; the subsequent resume loads None and fails with Unavailable before redispatching. Auth gates likewise have no durable record from which the runner can recover credential requirements. Thread the production replay and gate-record stores through this adapter/runtime parts and cover an initial-gate-to-resume flow.
Developer follow-up
After fixing this feedback:
- Push the fix to this PR branch.
- Re-run this reviewer with
@ironloopai review --agent reviewerif you only changed this reviewer's findings. - Re-run all reviewers with
@ironloopai reviewwhen the fix may affect multiple areas.
…ability port (#6287 IronLoop) The ProductLive planned-runtime path built its capability port with only runtime/input/result/milestone/mounts — never `.with_gate_record_store(..)` / `.with_replay_payload_store(..)` — so on that path the gate record was never persisted and the replay payload never reconstituted, breaking approval/auth resumes (the local-dev path already wires both). - `ProductLivePlannedRuntimeAdapterConfig` + `ProductLiveLoopCapabilityPortFactory` gain `gate_record_store` + `replay_payload_store`; `create_capability_port` now calls `.with_gate_record_store(..).with_replay_payload_store(..)` on the host-runtime factory, exactly as the local-dev path does. - The product-workflow planned-loop harness builds both stores over ONE in-memory filesystem (production mount view via `wrap_scoped`) so a raise and its resume round-trip through the same records; the composition adapter tests supply in-memory-backed stores through the config. - Adds `ironclaw_capabilities` to product-workflow dev-deps for the harness's `FilesystemReplayPayloadStore`. Verified: `cargo test -p ironclaw_reborn_composition --test product_live_adapters` passes; product-workflow planned-loop harness tests build; clippy --all-features clean on both crates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Re: IronLoop's ProductLive wiring finding ("ProductLive adapters' capability factory never receives the new durable replay and gate stores") — fixed in Fixed. The Wiring added end-to-end:
|
|
@ironloopai review |
There was a problem hiding this comment.
❌ IronLoop Review: reviewer
Review at a glance
| Verdict | Blocking | Notes | Inline | Head |
|---|---|---|---|---|
| ❌ Changes requested | 2 | 0 | 2 | a3abf3dd0587 |
Head: a3abf3dd05878a2b1a40b8041f7137b321e5878c
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.
Run details
Status: Current
Needs human: no
Needs validation: no
Summary
Found two blocking in-flight reservation cleanup bugs in the new durable replay/gate persistence paths. A transient write failure or cancelled writer can leave same-key replays waiting indefinitely.
Findings
Blocking: 2 / Notes: 0
Blocking findings
1. ❌ [HIGH] Replay-payload write failure permanently wedges the dispatch key
Location: crates/ironclaw_loop_host/src/capability_port.rs:2530-2545
guard.commit() runs before this fallible replay-payload write. If save returns a non-benign error, ? exits before any completed dispatch state is recorded, while the committed reservation remains InFlight and its waiters are never notified. Every retry/duplicate for that idempotency key will then wait forever. Keep the guard active until the payload is durable, or explicitly clear and wake the reservation on this error path; add a failing-save then retry regression test.
2. ❌ [MEDIUM] Cancelled gate-record persistence leaves replays waiting forever
Location: crates/ironclaw_loop_host/src/capability_port.rs:1901-1905
The new GateResolutionState::InFlight reservation has no cancellation/Drop cleanup. If the owning invoke_capability future is dropped while awaiting save, its post-save removal/publish and notify_waiters calls never run. A later replay receives the cached dispatch outcome, finds this stale in-flight entry, and waits indefinitely. Add a reservation guard that removes only its matching in-flight entry and wakes waiters when dropped, with an aborted-owner replay test.
Developer follow-up
After fixing this feedback:
- Push the fix to this PR branch.
- Re-run this reviewer with
@ironloopai review --agent reviewerif you only changed this reviewer's findings. - Re-run all reviewers with
@ironloopai reviewwhen the fix may affect multiple areas.
Inline review fallback
Inline comment projection fell back to a body-only PR Review because GitHub rejected the inline payload.
Reason: Unprocessable Entity: "Line could not be resolved" - https://docs.github.com/rest/pulls/reviews#create-a-review-for-a-pull-request
IronLoop preserved the inline review comment payloads below instead of dropping them.
Inline fallback 1: crates/ironclaw_loop_host/src/capability_port.rs:2545
This fallible write occurs after guard.commit(). On a store error, the dispatch entry remains InFlight without notification, so every same-key retry waits forever. Keep the guard active through this write or explicitly clear/wake it on failure.
Inline fallback 2: crates/ironclaw_loop_host/src/capability_port.rs:1904
An owner cancelled while awaiting this save leaves GateResolutionState::InFlight in the map forever: cleanup and notify_waiters are only after the await. Replays then wait indefinitely; use Drop-based cleanup for the matching reservation.
…n cancel/fault (#6287 IronLoop) The in-flight gate-resolution reservation cleaned up only on the paths that ran to completion. If the owning persist future was cancelled (dropped mid-`save`), or returned early before the publish block, the `InFlight` entry was orphaned and same-key replays waited on it forever; the transient-failure cleanup relied on code after the `await` running. Protect the reservation with an RAII drop guard mirroring the crate's own `DispatchReservationGuard`: - On reserving `InFlight`, the owner holds `GateResolutionReservationGuard`. Any drop without `commit()` — cancellation, transient store fault, or an early error — calls `clear_gate_resolution_reservation`, which removes the entry (only if still `InFlight`, never a published resolution) and wakes its waiters so one re-owns and retries. - The success / benign `GateRecordAlreadyExists` path publishes the durable `Persisted` resolution (`publish_gate_resolution`), wakes waiters, then `commit()`s the guard so its drop is a no-op. No waiter can be left blocked on an orphaned reservation regardless of how the owner exits. Regression: `cancelled_gate_persist_clears_reservation_so_replay_can_re_own` blocks the first `save` on a barrier, cancels the invocation while parked in `save`, and asserts a replay re-owns, completes within a timeout (no hang), and persists exactly one record. `failed_gate_record_persist_is_retried_on_replay` covers the transient-fault cleanup path through the guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Re: IronLoop's two in-flight reservation cleanup findings ("a transient write failure or cancelled writer can leave same-key replays waiting") — fixed in Both fixed. You're right — the previous version only cleaned up on the paths that ran to completion. If the owning persist future was cancelled (dropped mid- The reservation is now protected by an RAII drop guard, mirroring the crate's own
So no waiter can be left blocked on an orphaned reservation regardless of how the owner exits, and a woken waiter either receives the now-durable resolution or re-owns and retries. Regression: |
|
@ironloopai review |
There was a problem hiding this comment.
❌ IronLoop Review: reviewer
Review at a glance
| Verdict | Blocking | Notes | Inline | Head |
|---|---|---|---|---|
| ❌ Changes requested | 1 | 0 | 1 | a2d9ea6dae6e |
Head: a2d9ea6dae6e49b80fa2fdf788299c9894ce4214
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.
Run details
Status: Current
Needs human: no
Needs validation: no
Summary
Found a blocking ProductLive auth-gate wiring defect: the capability port receives the durable gate store, but the turn executor cannot receive the same store.
Findings
Blocking: 1 / Notes: 0
Blocking findings
1. ❌ [HIGH] ProductLive auth blocks cannot rehydrate credential requirements
Location: crates/ironclaw_reborn_composition/src/root/product_live_adapters.rs:791
from_services moves the gate-record store into the private capability factory, but ProductLivePlannedRuntimeAdapters does not retain or expose it for DefaultPlannedRuntimeParts. Consequently ProductLive compositions construct RebornTurnRunExecutor with gate_record_store: None; an auth block then takes the executor's empty-requirements path and is persisted without a provider/scopes, making it unsubmittable. Thread the same Arc<dyn GateRecordStore> through the adapter bundle into DefaultPlannedRuntimeParts, and cover an auth-gate round trip.
Developer follow-up
After fixing this feedback:
- Push the fix to this PR branch.
- Re-run this reviewer with
@ironloopai review --agent reviewerif you only changed this reviewer's findings. - Re-run all reviewers with
@ironloopai reviewwhen the fix may affect multiple areas.
| Arc::clone(&config.capability_input_resolver), | ||
| Arc::clone(&config.capability_result_writer), | ||
| config.milestone_sink, | ||
| config.gate_record_store, |
There was a problem hiding this comment.
This consumes the only store copy into the capability factory, while ProductLivePlannedRuntimeAdapters exposes no store for DefaultPlannedRuntimeParts. ProductLive callers therefore build RebornTurnRunExecutor with gate_record_store: None; an auth block is applied with empty credential requirements and cannot be submitted. Retain/forward the same Arc into the planned runtime and add an auth-gate round-trip test.
…too (#6287 IronLoop) The prior ProductLive wiring fix only wired the gate-record store into the capability port (the persist side). The turn executor (`RebornTurnRunExecutor`, via `DefaultPlannedRuntimeParts.gate_record_store`) still got `None`, so its render-from-record path (`enrich_auth_block_credential_requirements`, §5.2.9) could not read the record — an auth block was applied with empty requirements / failed the exit on the ProductLive path. The harness now shares ONE gate-record store between both sides: the store built for the capability port is captured into `turn_executor_gate_store` and passed to `DefaultPlannedRuntimeParts.gate_record_store`, so a persisted auth gate record round-trips to the executor read. Stays `None` for the non-ProductLive capability fakes (which do not persist gate records), preserving the executor's tolerant "no store wired" path; `inbound_turn_contract.rs` uses `EmptyCapabilityFactory`, so its `None` is correct and unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Re: IronLoop's ProductLive auth-gate executor-wiring finding ("the capability port receives the durable gate store, but the turn executor cannot") — fixed in Fixed. You're right — the ProductLive store wiring was only half-complete: the previous change wired the gate-record store into the capability port (the persist side), but the turn executor ( The ProductLive harness now shares ONE gate-record store between both sides: the store built for the capability port is captured into (A production ProductLive caller already holds the store — it puts it in product-workflow tests build + pass; clippy |
|
@ironloopai review |
There was a problem hiding this comment.
❌ IronLoop Review: reviewer
Review at a glance
| Verdict | Blocking | Notes | Inline | Head |
|---|---|---|---|---|
| ❌ Changes requested | 1 | 0 | 1 | 4efc26f5b583 |
Head: 4efc26f5b5830341b901b4322d2926125530c20b
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.
Run details
Status: Current
Needs human: no
Needs validation: no
Summary
Found one blocking retry/concurrency defect in the new replay-payload persistence path.
Findings
Blocking: 1 / Notes: 0
Blocking findings
1. ❌ [HIGH] Replay-payload failure leaves duplicate dispatches waiting forever
Location: crates/ironclaw_loop_host/src/capability_port.rs:2617
The dispatch reservation is committed before this newly fallible replay-payload save. If the save fails, ? returns while the dispatch record remains InFlight; the committed guard skips its cleanup and never notifies waiters. A concurrent or retried invocation with the same idempotency key then waits forever in wait_for_dispatch_completion. Keep the reservation uncommitted until the payload write and outcome recording succeed, or explicitly clear it on this error path, and add a failing-store retry regression test.
Developer follow-up
After fixing this feedback:
- Push the fix to this PR branch.
- Re-run this reviewer with
@ironloopai review --agent reviewerif you only changed this reviewer's findings. - Re-run all reviewers with
@ironloopai reviewwhen the fix may affect multiple areas.
Inline review fallback
Inline comment projection fell back to a body-only PR Review because GitHub rejected the inline payload.
Reason: Unprocessable Entity: "Line could not be resolved" - https://docs.github.com/rest/pulls/reviews#create-a-review-for-a-pull-request
IronLoop preserved the inline review comment payloads below instead of dropping them.
Inline fallback 1: crates/ironclaw_loop_host/src/capability_port.rs:2617
This fallible save runs after guard.commit() above. On a transient error, the committed guard does not clear the InFlight dispatch reservation or notify waiters, so a concurrent/same-key retry waits forever. Defer commit until this succeeds and the outcome is recorded, and cover a failing save followed by a retry.
…yload write (#6287 IronLoop) `guard.commit()` ran before the fallible `persist_replay_payload_for_fresh_gate` store write. On a transient store error the `?` returned with the dispatch reservation still `InFlight`, and the already-committed guard skipped its cleanup — stranding retries/duplicates of the same idempotency key waiting on the reservation forever. Move `guard.commit()` to AFTER the replay-payload write succeeds, so a store error leaves the guard uncommitted; its drop then clears the reservation and wakes waiters so one re-dispatches. The finish-runtime-outcome ordering is unchanged. Covered by `failed_gate_record_persist_is_retried_on_replay` and the `replayed_gate` seam tests (the reservation-clears-then-retry path). [skip-regression-check] the store-write-failure reservation-clear path is exercised by the existing retry/replay seam tests; a dedicated dispatch-cancel harness for this specific reordering is redundant with them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Re: IronLoop's replay-payload persistence retry/concurrency finding — fixed in |
|
@ironloopai review |
There was a problem hiding this comment.
⚠️ IronLoop Review: reviewer
Review at a glance
| Verdict | Blocking | Notes | Inline | Head |
|---|---|---|---|---|
| 0 | 0 | 0 | f38e56122786 |
Head: f38e56122786604d7c6498736f115c3d5b001fe9
Next: Human review or validation is required before merging.
Run details
Status: Current
Needs human: no
Needs validation: yes
Summary
No concrete correctness, security, or concurrency defect found in source review. Runtime validation remains required because Cargo is unavailable in this environment.
Findings
None.
Developer follow-up
After fixing this feedback:
- Push the fix to this PR branch.
- Re-run this reviewer with
@ironloopai review --agent reviewerif you only changed this reviewer's findings. - Re-run all reviewers with
@ironloopai reviewwhen the fix may affect multiple areas.
|
✅ Ready for merge — §5.3 Stage 2 (the flip: loop-facing result → Reviewed the full 69-file diff (host_api core, producer/resume seam, executor §11.9 exhaustiveness, composition/runner/test-double wiring) and worked through IronLoop's exhaustive review — 10 findings addressed, each with a regression test or verified coverage:
Verification: Note for the stack: this sits on |
…ePlannedRuntimeAdapters (#6299 IronLoop) `ProductLivePlannedRuntimeAdapters::from_services` moved the host-private `gate_record_store` / `replay_payload_store` into the capability factory but did not retain them on the bundle. The bundle is the production API for composing a product-live planned runtime, so a consumer building the runner (`DefaultPlannedRuntimeParts`) from it had no way to wire the SAME store the capability port persists `GateRecord::Auth` into — it would pass `gate_record_store: None`, and on an `AuthRequired` resume the turn executor reloads an empty requirement set and applies an unactionable auth block (the §5.2.9 render-from-record seam, same class as #6287 IronLoop f6/f8). Expose both stores as `pub` fields, populated from cheap `Arc` clones taken before the originals move into the factory, so both sides share one durable store. Additive, no behavior change to existing consumers. Note: the only current bundle consumer (the product-workflow planned-loop test harness) extracts just `.capability_factory`; its runner already wires the same store via `turn_executor_gate_store`, so the end-to-end product-live auth-gate tests through that path pass unchanged. This closes the bundle's API gap so a future/production runner consumer wires the store, not None. cargo check + clippy (test-support,libsql) clean on ironclaw_reborn_composition. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… delete CapabilityOutcome/mirror DTOs (§5.3 Slice C) (#6299) * refactor(reborn): resume replay payload moves host-side via ReplayPayloadStore; wire real stores (§5.3 Stage 2a-i) Moves the raw gate/auth resume replay payload out of the untrusted loop and into the host-private `ReplayPayloadStore`, and wires the real durable stores in production composition. The loop-facing result stays `CapabilityOutcome` (no `Resolution` flip — that is Stage 2a-ii). Security fix: the loop checkpoint no longer stores raw tool args, retiring the charter violation in `crates/ironclaw_agent_loop/CLAUDE.md` ("State stores refs... Do not store raw prompts, raw model output, tool args ... in state"). Write (two gate-raise paths): at a FRESH runtime gate raise (`HostRuntimeLoopCapabilityPort::invoke_capability_dispatch`, gated on `is_fresh_dispatch` before `finish_runtime_outcome`) and at the synthetic outbound-delivery approval-gate raise (`outbound_delivery.rs::request_approval`), the host `save`s a `ReplayPayload{input, estimate, prior_approval:None, input_ref, correlation_id}` keyed by `InvocationId` (write-once; a benign same-invocation duplicate is tolerated). Read on resume: `runtime_outcome_to_loop` no longer embeds input/estimate; `invocation_replay_input` is replaced by `ReplayPayloadStore::load` keyed by the `InvocationId` recovered from the resume token, in both the runtime seam (`replay_payload_for_resume`) and the synthetic decorator/handler. Fail closed on a miss: an absent payload (including a wrong-scope read) is a sanitized terminal `Unavailable`, never a silent empty-input dispatch. The fingerprinted approval lease claim ordering is preserved. Dropped loop fields (security): `input`/`estimate` from `CapabilityApprovalResume` and `PendingApprovalResume`; the whole `CapabilityAuthResumeReplay` type and the `replay` field on `CapabilityAuthResume`/`PendingAuthResume`. `approval_request_id`/ `correlation_id`/`input_ref`/`prior_approval` stay on the wire (they move in 2a-ii). Composition (real stores): `capability_wiring` builds `FilesystemGateRecordStore` (closes the #6245 production gap where it defaulted to `NoopGateRecordStore`) and `FilesystemReplayPayloadStore` over the shared scoped filesystem and threads both through the refreshing capability port. Adds the `/replay-payloads` per-user mount alias. `loop_host` gains a dependency-inversion dep on `ironclaw_capabilities` for the port trait. Tests (all failed first): fail-closed on missing payload (loop_host seam); extended the #6245 approval-resume seam test to assert the store persisted the payload and the resume redispatches the correct original input; the `ReplayPayloadStore` contract (round-trip / write-once / cross-tenant / within-tenant) rides the base commit. Full-infra resume edges pass through the production-shaped harness: group_approvals (runtime approval resume), group_journeys (approval→auth convergence), outbound_target (synthetic approval resume). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(turns): move the removed-replay design note out of prior_approval's rustdoc Reported-by: gemini-code-assist (PR #6271 review) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(reborn): Resolution carries model-visible failure diagnostic + denial content (§5.3 flip prep / PR-B) Additive vocabulary slice (§3/§5.2.9/§5.3): make `host_api::Resolution` carry the model-visible result content the executor's `handle_capability_outcome` reads off `CapabilityOutcome` today, so a later slice (PR-C) can flip `invoke_capability` to `Resolution` without the loop reading host storage (its charter forbids that). host_api additions (redacted vocabulary only — charter fit): - `ModelInputIssue`: redacted mirror of the loop's `CapabilityInputIssue` — `DispatchInputIssueCode` (existing host_api enum) + bounded redacted `SafeSummary` path/expected/received/schema_path. - `ModelFailureDiagnostic { InvalidInput { issues }, Diagnostic { text } }`: redacted mirror of `CapabilityFailureDetail`; rides `ToolVerdict::RecoverableFailure.diagnostic` (additive Option). - `Denial { deny, reason_kind, summary }`: `Resolution::Denied(DenyRef)` → `Resolution::Denied(Denial)`, carrying the model-visible `DenyReason` + redacted `SafeSummary` on the channel (a projection of the sibling DenyRecord). Mapping (`resolution_mapping.rs`): `failed_outcome` now redacts the loop `detail` into the diagnostic (per-field SafeSummary; path-shaped free text degrades to the placeholder); the Denied arm populates the Denial channel from the same reason/summary the DenyRecord holds. Tests (failed first, then green): structured `InvalidInput` issues and free-text `Diagnostic` round-trip through the mapping; Denied carries reason_kind + redacted summary; redaction proof — path- and secret-shaped content is redacted in the `Resolution` output, never carried raw. GateRecord/ DenyRecord persistence round-trips (ironclaw_run_state) stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(host_api): enforce the 16-issue diagnostic cap in the type, not one producer ModelFailureDiagnostic::InvalidInput now carries a bounded ModelInputIssues newtype: validated at construction, revalidated on the wire (try_from), with a truncating() producer constructor for the mapping. A 17-item payload is rejected at every entry point — pinned by model_input_issues_cap_is_enforced_at_construction_and_on_the_wire. Reported-by: ironloopai (PR #6273 review) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reborn): ResolutionBatch + parks() loop-suspension predicate + scripted-outcome fixture (§5.3 flip prep / Stage 1) Additive vocabulary + test-fixture slice ahead of the capability-result flip. Nothing consumes the new items yet, so the tree stays green. Closes a verified correctness hazard: `Resolution::is_suspension()` is `Suspended(_)` ONLY, but the batch loops the flip rewrites must also stop on the re-entrant gate variants (`Resolution::Blocked` — Approval/Auth/ Resource), which map to `Blocked`, not `Suspended`. A naive `is_suspension()` batch guard would silently let a gate fall through as if the call had completed (the #6137 bug class). This provides the correct predicate now so the flip has one canonical, tested site to call. - `ironclaw_host_api::Resolution::parks()`: the loop-semantics suspension predicate — true for every `Blocked` variant AND every `Suspended` variant; false for `Done` (incl. non-suspending `ChildSpawned`) and terminal `Denied`. `parks()` ⊋ `is_suspension()`: a `Blocked::Approval` parks but is not a suspension. `is_suspension()` keeps its narrow meaning unchanged. - `ironclaw_host_api::ResolutionBatch { resolutions: Vec<Resolution>, stopped_on_suspension: bool }`: the loop-facing batch result the flip adopts, mirroring `CapabilityBatchOutcome`'s shape/semantics over `Resolution`. Wire-stable serde. - `ironclaw_agent_loop::test_support::{resolution_from_scripted_outcome, resolution_batch_from_scripted}` (behind the existing `test-support` feature): convert existing `CapabilityOutcome` fixtures to `Resolution`/`ResolutionBatch` via the production mapping, for the flip's ~150 test sites. Tests (written first, watched fail): - host_api: acceptance table extended with a `parks` column across all 10 CapabilityOutcome→Resolution rows; a dedicated exhaustive `match` (compile-fails if a new `Resolution` variant is added) proving `parks()` ⊋ `is_suspension()`; a `ResolutionBatch` round-trip. Verified red: a naive `is_suspension()`-based `parks()` fails ("parks() disagrees for blocked"). - agent_loop test-support: round-trips scripted Completed/Failed/ ApprovalRequired → the right Resolution channel, and a batch preserving order + stop flag. No trait flip, no consumer change, `CapabilityOutcome` untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(host_api): Resolution::parks uses an exhaustive match, not matches! A new Resolution variant must be a compile error in parks() (§11.9 no-wildcard) so its parking behavior is decided deliberately rather than silently defaulting to false — the exact class of silent fall-through parks() exists to prevent (#6137). Reported-by: gemini-code-assist (PR #6275 review) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(runner): drop removed Pending{Approval,Auth}Resume replay fields (inherited base debt) The replay-field removal lands in a lower stack slice; until this branch's base picks it up, planned_driver.rs's cfg(test) fixtures still set the removed fields and fail the workspace clippy lane. Harmless once the base merges. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(reborn): complete host-side gate reconstitution — resume input_ref, local-dev gate persistence, GateRef reconcile (§5.3 flip prep / Stage 0) Stage 0 no-flip host-reconstitution slice: closes two verified correctness hazards ahead of the atomic capability-result flip. The loop-facing trait and `CapabilityOutcome` are UNCHANGED; only host-side plumbing moves. Fix 1 — resume-time input_ref reconstitution (hazard 3). On a gate/auth resume the effective input_ref used for the idempotency key + validation is now derived from the host-private `ReplayPayload` persisted at the fresh gate raise (loaded by the resume's invocation id), not the advisory loop-supplied `approval_resume.input_ref`. A new `resume_replay_payload` helper centralizes the derivation so `invoke_capability` (seam) and `invoke_capability_dispatch` compute the SAME byte-stable key. The key is now derived lazily inside `persist_gate_record_for_outcome` (after dispatch, only for a gate outcome) so a missing/stale resume payload never pre-empts dispatch's own resume identity/activity validation — a malformed resume still surfaces `InvalidInvocation`, and a genuinely-missing payload still fails closed (2a-i missing-payload test stays green). Test proves a resume derives the same input_ref/key from the store even when the loop-supplied ref differs (a second resume differing only in that advisory ref replays the cached outcome instead of re-dispatching); failed first (re-dispatched, count 2) before the fix. Fix 2 — gate-record persistence for the local-dev synthetic approval producer (hazard 2). `OutboundDeliveryTargetSetHandler::request_approval` raises its gate OUTSIDE the loop-host persist seam, so it now persists a `GateRecord::Approval` itself at the raise (via the wired `GateRecordStore`), keyed by the canonical `GateRef::for_approval_request`, so host-side gate rendering (§5.2.9) has a record to read. Test proves the record persists and round-trips; failed first (load returned None) before the fix. Fix 3 — reconcile the two approval GateRef encodings. Canonicalized on the typed `GateRef::for_approval_request(id) = GateRef::from_uuid(id.as_uuid())` — a new `ironclaw_host_api` helper — as the GateRecordStore key. This is the host_api uuid GateRef (record key), a DIFFERENT type from the loop-facing `gate:approval-{id}` routing ref (`ironclaw_turns::GateRef`), so reconciling here touches NO persisted wire format and does not break the `is_approval_gate_ref` prefix-routing predicate. `ironclaw_capabilities::host` now uses the helper at both authorize sites. Test proves a host-persisted approval gate ref resolves through the read model: the routing ref recovers the approval id (`approval_request_id_from_gate_ref`), the canonical key re-derives, and the persisted GateRecord is found. Note: the task suggested canonicalizing on `from_uuid`; that is exactly what this does for the record-key GateRef. The RISKY direction (rewriting the `gate:approval-{id}` routing string to a bare uuid) was NOT taken — it would break the `is_approval_gate_ref` prefix predicate used in production routing, projection, and channel delivery, plus persisted adapter command strings and delivered-gate route records. Tests: loop_host 378 lib (incl. new + updated resume/seam/2a-i); host_api helper round-trip; composition 1629 (env-clean); capabilities/product_workflow/run_state green; architecture green; integration outbound_target/auth_gate/ reopen_resume_through_gate/group_approvals green; clippy -D warnings clean on all touched crates; pre-commit-safety clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): Suspension::DependentRun carries the staged child result (§5.3 flip prep / Stage 1b) Closes a verified hazard blocking the atomic capability-result flip (§5.3): `host_api::Resolution`'s `Suspension::DependentRun` channel could not carry the dependent child's staged result, which the loop needs on resume. After the flip the executor gets only `Resolution::Suspended(Suspension::DependentRun(..))`; the mapping folded `result_ref`/`byte_len`/`safe_summary` into the host-persisted `GateRecord::DependentRun` (the loop cannot read host storage) and dropped `model_observation` entirely — so byte caps, model-visible child text, and the model observation would silently regress ("never drop model output"). This ADDITIVE slice gives `Suspension::DependentRun` an inline staged-result payload alongside its `GateWaypoint`, mirroring how `Done(Outcome)` carries a spawned child run's content: - New `DependentRunResult { byte_len, summary: SafeSummary, observation: Option<SafeSummary>, origin: Option<LoopRef> }` — plain redacted host_api vocabulary; the full child bytes stay host-owned behind the record's `ResultRef`. `Suspension::DependentRun` becomes a struct variant `{ waypoint, result }`; accessors split from `ExternalTool`, plus a `dependent_result()` accessor. - `capability_outcome_to_resolution`'s `AwaitDependentRun` arm populates it from the loop's `result_ref`/`byte_len`/`safe_summary`/`model_observation` (redaction applied), and the "model_observation has no home … dropped" behavior is deleted — it now rides the inline observation preview (the same `observation_preview` reduction Completed/SpawnedChildRun use). The durable `GateRecord::DependentRun` sidecar is still persisted (host durability); the inline payload is the loop-visible copy — the same dual pattern as PR-B. Nothing consumes the new payload yet: `LoopCapabilityPort` is not flipped, `CapabilityOutcome` is not deleted, executor consumers are untouched — the tree stays green. Test-first: extended the `resolution` + `resolution_mapping` acceptance tables and added dedicated round-trip + redaction tests — an `AwaitDependentRun` outcome round-trips its `result_ref` origin, `byte_len`, `safe_summary`, AND `model_observation` onto `Suspension::DependentRun`, and a secret/path-shaped summary/observation degrades (dropped / placeholder). These fail against the pre-change tuple variant (fields absent / model_observation dropped). The `GateRecord::DependentRun` persistence round-trip stays green. Verified: `cargo test -p ironclaw_host_api -p ironclaw_turns -p ironclaw_run_state --all-features`; `cargo clippy -p ironclaw_host_api -p ironclaw_turns --all-targets --all-features -- -D warnings`; `cargo test -p ironclaw_architecture`; `scripts/pre-commit-safety.sh`; downstream build of loop_host/agent_loop/runner/reborn_composition. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): loop-facing result becomes host_api::Resolution (§5.3 Stage 2 — the flip) (#6287) * refactor(reborn): loop-facing result becomes host_api::Resolution (§5.3 Stage 2 — the flip) Collapses the loop-facing capability result — the overloaded ten-variant `CapabilityOutcome`/`CapabilityBatchOutcome` — into the five-channel `host_api::Resolution`/`ResolutionBatch` at the `LoopCapabilityPort` trait boundary. `CapabilityOutcome` is retained (Stage 2b deletes it); producers convert at their boundary via `capability_outcome_to_resolution(..).resolution`. The trait flip: `invoke_capability -> Result<Resolution, _>`, `invoke_capability_batch -> Result<ResolutionBatch, _>`. Batch loops gate on `Resolution::parks()` (a re-entrant `Blocked` gate stops the batch too, H1), replacing `CapabilityOutcome::is_suspension()`. All ~49 impls + decorators/ factories migrated in dep order (turns -> loop_host -> hooks -> composition -> runner -> agent_loop), plus every test double and the `tests/integration` doubles. Executor consumer rewrite (ironclaw_agent_loop): `handle_capability_outcome`, the batch fast-path drain, `handle_capability_error` retry, `shared_await_dependent_gate`, and `capability_batch_counts` match exhaustively over `Resolution` (no wildcard, §11.9). host_api->loop inverse reconstruction: loop refs from the channel's preserved `origin`; `Done` re-split on `ToolVerdict`; model-visible failure/denial content from PR-B (`RecoverableFailure.diagnostic`, `Denial`); the DependentRun staged result from `Suspension::dependent_result()`. Resume identity stays byte-stable: `approval_request_id` reconstructed deterministically from the `gate:approval-{id}` routing ref (fingerprinted lease byte-identical); `prior_approval` kept on the wire (host-side move deferred to §5.3 Stage 2a-ii); `input_ref` advisory on resume (host reconstitutes from `ReplayPayloadStore`, Stage 0); `correlation_id` observability-only, regenerated at the loop boundary. Stage-0 resume + missing-payload fail-closed + cross-tenant tests stay green. Auth `credential_requirements` render-from-record: moved off the loop-facing `Blocked::Auth` channel onto `GateRecord::Auth` (§5.2.9), keyed deterministically by the new name-based `GateRef::for_auth_gate` (the auth gate id is `auth-{sha256hex}`, not a uuid, so it derives a stable v5 uuid); the runner re-reads them from the durable record at the blocked-exit application into `TurnRunRecord.credential_requirements` — the auth analogue of approval's Stage-0 render-from-record. Model-visible first-look preview (#5838) fixed correctly, not dropped: the collapse mis-routed tool-result CONTENT through `SafeSummary`, a caption type that rejects `{ } [ ] /` delimiters (all structured output) and — via a substring `"secret"` match — scrubbed the ordinary word "Secretary" (the #6129 bug: an amnesia re-read loop that tanked benchmark scores). Ports the canonical word-boundary credential matcher into host_api (`credential_redaction`) and applies it to `SafeSummary` so captions like "Secretary" survive; adds `ModelResultPreview` — the correct 24 KiB content vehicle that tolerates delimiters/newlines and redacts only genuine credentials (word-boundary markers + secret-like tokens) — and re-points `Outcome.refs.preview` at it, plus `ResultPreviewMeta` carrying the truncated-preview continuation metadata (referenced ref, total bytes, next offset, item count) so `result_read` pagination survives. The executor reconstructs the `ResultReference` observation from real content, preserving the inline first-look optimization (no extra `result_read` round-trip). Resume-core completions surfaced driving the production integration tiers green (the flip drops the loop-facing `correlation_id`, so every resume consumer must reconstitute it host-side, not read it off the wire): - Loop-host runtime resume reconstitutes `correlation_id` from the persisted `ReplayPayload` (keyed by invocation id) and applies it to the invocation context before the fingerprinted approval/auth lease is matched — the loop's post-flip value is advisory. Without this the lease's correlation match fails ("approval request does not match invocation: correlation_id") and the resume terminalizes (caught by `reborn_group_approvals`, `reborn_group_journeys`). - The synthetic outbound-delivery handler (raises its OWN approval gate outside the loop-host persist seam) reconstitutes `correlation_id` from its own replay payload for the approval cross-check instead of trusting the executor's now- fresh `resume.correlation_id`, completing the same §5.3 Stage 2a-i render-from- record pattern it already applied to `{input, estimate}` (caught by `reborn_integration_outbound_target`). - Deterministic gate-record keys (`for_auth_gate`, and `for_approval_request` on the authorize path) make a re-raised gate derive the SAME content-addressed key; the write-once `GateRecordStore` reports `GateRecordAlreadyExists`, which the host now treats as benign (first-write-wins, record byte-identical), mirroring the existing `ReplayPayloadAlreadyExists` tolerance — it strengthens, not weakens, the write-once contract (caught by the auth-convergence journey). - The completed-result `ResultReference` observation's own producer-authored `summary` (the truncation/continuation hint, distinct from the generic result- message caption) is carried through the collapse on `ResultPreviewMeta.summary` so the executor rebuilds the observation with the producer's exact text rather than the caption (caught by `reborn_integration_tool_call`'s truncated-preview and array-item-count transcript assertions). Full gate green: workspace `cargo clippy --all-targets --all-features -D warnings`; `ironclaw_architecture`; the changed-crate unit suites (the only red is the 3 pre-existing `llm_admin::nearai_mcp` env-var tests, unrelated); and all 51 Reborn integration bins (706 scenarios) including every §11.9 per-channel / gate / resume / auth tier. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): gate record keyed to returned Resolution + fail unsubmittable auth block (#6287 IronLoop) Two blocking gate-record correctness fixes on the §5.3 Stage 2 flip: 1. Map once (capability_port.rs). `invoke_capability` mapped the outcome twice — once inside `persist_gate_record_for_outcome` to derive the key it saved under, and once via `capability_outcome_to_resolution` to build the return value. The approval/resource/dependent/external channels mint a fresh random `GateRef` per mapping (only auth is deterministic), so the persisted record and the returned `Resolution` pointed at different refs and a resume could never load the record. Now maps once and persists/returns the same `MappedResolution` (`persist_gate_record_for_outcome` -> `persist_gate_record_for_mapped`, no internal re-map). 2. Fail the exit on unsourceable auth requirements (turn_run_executor.rs). The flip made the durable `GateRecord::Auth` the ONLY source of `credential_requirements`, so a lookup that does not yield the record is a real regression: applying the block leaves an unsubmittable, provider-null auth gate. `enrich_auth_block_credential_requirements` now returns `Result`; the three lookup-result miss arms (store fault, `Ok(None)`, wrong kind) return `Err`, and `apply_exit` records a terminal failure (shared `record_exit_failure` helper) instead of applying the incomplete block. The two tolerant pre-conditions (no store, non-auth ref) stay warn+empty. Regression tests: extend `approval_gate_outcome_persists_gate_record_at_the_seam` to assert the returned Resolution's gate ref equals the persisted key; new `auth_block_with_unsourceable_requirements_fails_the_exit` drives `apply_exit` with a missing-record store and asserts a terminal failure is recorded (this test also caught a `&'static str` lifetime bug in the helper). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): forward dependent-run observation caption to the resumed parent (#6287 IronLoop) `dependent_run_result_message` hardcoded `model_observation: None`, so `append_capability_result_ref`'s synthesize-fallback re-dropped the child's staged observation and the resumed parent saw only a bare success sentinel — not the caption/result reference the `AwaitDependentRun` channel forwarded before the flip. The mapping already preserves the caption on `DependentRunResult.observation` ("model_observation now rides the inline observation preview (was dropped entirely)"); the consumer just dropped it. Forward it as a Success `ResultReference` observation carrying the caption as its summary and pointing at the staged child result (`result_ref` + `byte_len`), so the parent can `result_read` the child output. The inline first-look preview content stays host-owned (the completed-`Outcome` path), per the mapping's "bounded SafeSummary caption" design — so `preview` stays `None` here. Test: `await_dependent_run_preserves_model_observation_for_replay` now asserts the forwarded `ResultReference` observation instead of the previously-pinned synthesized `GenericFailure` sentinel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): replay returns the record-backed gate ref, not a fresh mint (#6287 IronLoop) The map-once fix made the first invocation's persisted key and returned gate ref agree, but the replay path still diverged: on an idempotent replay the dispatch cache returns the same CapabilityOutcome, invoke_capability re-maps it (a fresh random GateRef for the approval/resource/dependent/external channels), and the set-based guard then skipped the persist — so the replayed Resolution carried a gate ref no record was ever saved under. Turn the replay guard into a resolution cache: `persisted_gate_resolutions: HashMap<IdempotencyKey, Resolution>`. The first invocation reserves the key with its resolution (atomic check-and-insert under one lock) and persists exactly one record under that resolution's gate ref; a repeat (or concurrent duplicate) finds the reservation and returns the SAME resolution — the one whose gate ref the record is under — instead of re-minting. Rollback-on-failed-save is preserved so the next replay retries. Regression: extend `replayed_gate_invocation_does_not_persist_a_duplicate_record` to assert the replayed Resolution's gate ref equals the first invocation's and the single persisted record's key, and that the record loads by the replayed ref. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): gate resolution reservation waits for durable persist (#6287 IronLoop) The reserve-then-save replay cache had a concurrency window: a concurrent duplicate could find the reservation and return the resolution before the owner's save durably completed, so a transient store fault (owner rolls back and errors) left the waiter holding a blocked resolution whose gate record was never persisted — an unresumable gate. Make the reservation a proper in-flight/persisted state machine mirroring the crate's own DispatchRecordStore + wait_for_dispatch_completion: - persisted_gate_resolutions: HashMap<IdempotencyKey, GateResolutionState>, where GateResolutionState = InFlight(Arc<Notify>) | Persisted(Box<Resolution>). - The first caller reserves InFlight and is the sole persister. A concurrent duplicate / later replay finds InFlight and WAITS on the notify, using the lost-wakeup-safe pattern (create notified(), re-check the same reservation under the lock via gate_resolution_in_flight_matches, await only if still in-flight). - The owner publishes only AFTER the durable save resolves: Persisted on Ok/benign GateRecordAlreadyExists, cleared on a transient fault; it wakes the waiters either way, so a woken waiter receives the now-durable resolution or re-owns and retries. No waiter receives a resolution before durable persist. Covered by the existing replay/rollback regression tests (replayed_gate_invocation_does_not_persist_a_duplicate_record, failed_gate_record_persist_is_retried_on_replay), which still hold. [skip-regression-check] deterministic single-threaded coverage is unchanged; the concurrency window is a narrow race the existing tests already pin the sequential behavior of, and a hermetic multi-waiter race harness is infeasible to make reliably deterministic here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): wire durable gate/replay stores into the ProductLive capability port (#6287 IronLoop) The ProductLive planned-runtime path built its capability port with only runtime/input/result/milestone/mounts — never `.with_gate_record_store(..)` / `.with_replay_payload_store(..)` — so on that path the gate record was never persisted and the replay payload never reconstituted, breaking approval/auth resumes (the local-dev path already wires both). - `ProductLivePlannedRuntimeAdapterConfig` + `ProductLiveLoopCapabilityPortFactory` gain `gate_record_store` + `replay_payload_store`; `create_capability_port` now calls `.with_gate_record_store(..).with_replay_payload_store(..)` on the host-runtime factory, exactly as the local-dev path does. - The product-workflow planned-loop harness builds both stores over ONE in-memory filesystem (production mount view via `wrap_scoped`) so a raise and its resume round-trip through the same records; the composition adapter tests supply in-memory-backed stores through the config. - Adds `ironclaw_capabilities` to product-workflow dev-deps for the harness's `FilesystemReplayPayloadStore`. Verified: `cargo test -p ironclaw_reborn_composition --test product_live_adapters` passes; product-workflow planned-loop harness tests build; clippy --all-features clean on both crates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): RAII guard clears orphaned gate-resolution reservation on cancel/fault (#6287 IronLoop) The in-flight gate-resolution reservation cleaned up only on the paths that ran to completion. If the owning persist future was cancelled (dropped mid-`save`), or returned early before the publish block, the `InFlight` entry was orphaned and same-key replays waited on it forever; the transient-failure cleanup relied on code after the `await` running. Protect the reservation with an RAII drop guard mirroring the crate's own `DispatchReservationGuard`: - On reserving `InFlight`, the owner holds `GateResolutionReservationGuard`. Any drop without `commit()` — cancellation, transient store fault, or an early error — calls `clear_gate_resolution_reservation`, which removes the entry (only if still `InFlight`, never a published resolution) and wakes its waiters so one re-owns and retries. - The success / benign `GateRecordAlreadyExists` path publishes the durable `Persisted` resolution (`publish_gate_resolution`), wakes waiters, then `commit()`s the guard so its drop is a no-op. No waiter can be left blocked on an orphaned reservation regardless of how the owner exits. Regression: `cancelled_gate_persist_clears_reservation_so_replay_can_re_own` blocks the first `save` on a barrier, cancels the invocation while parked in `save`, and asserts a replay re-owns, completes within a timeout (no hang), and persists exactly one record. `failed_gate_record_persist_is_retried_on_replay` covers the transient-fault cleanup path through the guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): share the ProductLive gate store with the turn executor too (#6287 IronLoop) The prior ProductLive wiring fix only wired the gate-record store into the capability port (the persist side). The turn executor (`RebornTurnRunExecutor`, via `DefaultPlannedRuntimeParts.gate_record_store`) still got `None`, so its render-from-record path (`enrich_auth_block_credential_requirements`, §5.2.9) could not read the record — an auth block was applied with empty requirements / failed the exit on the ProductLive path. The harness now shares ONE gate-record store between both sides: the store built for the capability port is captured into `turn_executor_gate_store` and passed to `DefaultPlannedRuntimeParts.gate_record_store`, so a persisted auth gate record round-trips to the executor read. Stays `None` for the non-ProductLive capability fakes (which do not persist gate records), preserving the executor's tolerant "no store wired" path; `inbound_turn_contract.rs` uses `EmptyCapabilityFactory`, so its `None` is correct and unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): commit the dispatch reservation only after the replay-payload write (#6287 IronLoop) `guard.commit()` ran before the fallible `persist_replay_payload_for_fresh_gate` store write. On a transient store error the `?` returned with the dispatch reservation still `InFlight`, and the already-committed guard skipped its cleanup — stranding retries/duplicates of the same idempotency key waiting on the reservation forever. Move `guard.commit()` to AFTER the replay-payload write succeeds, so a store error leaves the guard uncommitted; its drop then clears the reservation and wakes waiters so one re-dispatches. The finish-runtime-outcome ordering is unchanged. Covered by `failed_gate_record_persist_is_retried_on_replay` and the `replayed_gate` seam tests (the reservation-clears-then-retry path). [skip-regression-check] the store-write-failure reservation-clear path is exercised by the existing retry/replay seam tests; a dedicated dispatch-cancel harness for this specific reordering is redundant with them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): producers emit Resolution directly; delete CapabilityOutcome (§5.3 Stage 2b — collapse complete) (#6293) * chore(reborn): anchor s2b worktree at flip base Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): add producer-facing Resolution constructors in ironclaw_turns (§5.3 Stage 2b) New run_profile::resolution module with completed/failed/spawned_process/ spawned_child_run (return Resolution), approval_required/auth_required/ resource_blocked/await_dependent_run/external_tool_pending (return GatedResolution), and denied (returns DeniedResolution). Moves the non-lossy redaction verbatim from resolution_mapping (ModelResultPreview 24 KiB word-boundary redaction, ModelFailureDiagnostic, deterministic GateRef::for_auth_gate auth key, resume-token carry, DependentRunResult inline staged result, DenyReason mapping). resolution_mapping:: capability_outcome_to_resolution is retained transitionally as a thin delegator so unmigrated producers keep compiling; RefBindings is now empty (loop refs ride the channel origin). 17 retargeted constructor tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): loop_host producers emit Resolution directly (§5.3 Stage 2b) capability_port.rs: invoke_capability_dispatch now returns GatedResolution; the seam persists the durable GateRecord from .gate_record (GateRef::for_auth_gate key + replay-payload persistence intact) and returns .resolution — deleting the capability_outcome_to_resolution re-map. runtime_outcome_to_loop and the synthetic/inline dispatch arms emit Resolution/GatedResolution via the new constructors; the runtime failure classifier returns a private LoopFailureClass (Failed | Denied) so the per-tool display preview still stages from the raw fields. lib.rs empty-surface denial, subagent_spawn_port (spawn gates + batch coalescing keyed on the DependentRun channel origin), and capability_surface_filter denials all emit Resolution directly. 378 loop_host tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): hooks middleware emits Resolution directly (§5.3 Stage 2b) HookedLoopCapabilityPort::decision_to_outcome and fail_closed_gate_ref_unavailable now return host_api Resolution (deny/approval/auth) via the producer constructors instead of building CapabilityOutcome and re-mapping; single and batch invoke paths consume the Resolution directly. Test double emits resolution::completed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): local_dev synthetic capabilities emit Resolution directly (§5.3 Stage 2b) SyntheticCapabilityHandler::invoke now returns host_api Resolution; the synthetic port and external-tool port delegate it straight through (no capability_outcome_to_resolution re-map). outbound_delivery (approval gate + denial/failure paths, internal ApprovedResumeDecision + approval_lease_outcome now carry Resolution), skill_activation, project_create, result_read (parse error boxed for result_large_err), and external_tool_capability all build Resolution via the producer constructors. Tests assert Resolution::Done recoverable-failure verdicts and Resolution::Denied. 230 local_dev tests green; crate clippy -D warnings clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): runner tool-disclosure + subagent flavors emit Resolution directly (§5.3 Stage 2b) tool_disclosure_port bridge helpers (invoke_bridge/tool_search/describe/ describe_first/completed_bridge_result, failed_invalid_input) and the test double now build host_api Resolution via the producer constructors; subagent flavors test double likewise. No capability_outcome_to_resolution re-map remains in runner producers. Runner tests + clippy -D warnings green (all-features). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): agent_loop executor test fixtures build Resolution via constructors (§5.3 Stage 2b) shared_await_dependent_gate fixtures (await_dependent/completed/approval) now use the producer constructors directly instead of mapping a CapabilityOutcome. 401 agent_loop tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reborn): migrate agent_loop capability fixtures off CapabilityOutcome (§5.3 Stage 2b) MockHost stores Resolution/ResolutionBatch directly; scripted_capability_outcome maps ScriptedCapabilityOutcome -> Resolution via the producer constructors; ~90 executor test fixtures rebuilt as resolution::* + ironclaw_host_api::ResolutionBatch (transformed by a comment/brace-aware script). resolution_from_scripted_outcome deleted; resolution_batch_from_scripted takes Resolutions. 401 agent_loop tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): delete CapabilityOutcome and the transitional mapping (§5.3 Stage 2b — collapse complete) Deletes CapabilityOutcome, CapabilityBatchOutcome, CapabilityResultMessage, CapabilityFailure, CapabilityDenied, ProcessHandleSummary from run_profile::host, the resolution_mapping delegator (capability_outcome_to_resolution / MappedResolution / RefBindings), and their re-exports. Retargets the last serde-fixture tests (turn_coordinator, agent_loop_host_contract, content_digest) onto the surviving vocabulary (CapabilityDeniedReasonKind / CapabilityProgress / Resolution). CapabilityApprovalResume/AuthResume/ResumeToken kept (resume requests). Architecture ratchet trims CapabilityOutcome from FROZEN_COLLAPSE_DTOS per its own shrink-only instructions. No production code references CapabilityOutcome; turns + architecture ratchet green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): retain CapabilityResultMessage/CapabilityFailure as executor-internal vocabulary Scope correction: these two are NOT dead payloads — the agent_loop executor reconstructs them from host_api::Outcome (capability_result_from_outcome / capability_failure_from_recoverable, added by the flip) and consumes them across gates/capability_helpers/strategies. They are loop-internal working types now (no producer emits them; documented as such). The genuinely-dead CapabilityOutcome/CapabilityBatchOutcome/CapabilityDenied/ProcessHandleSummary stay deleted. Workspace builds clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reborn): migrate integration harness capability double off CapabilityOutcome (§5.3 Stage 2b) RecordingTestCapabilityPort (the shared tests/integration double) builds host_api::Resolution via the producer constructors; completed_result returns Resolution, approval/echo paths emit resolution::approval_required/completed directly. Unblocks every reborn_integration_* binary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(reborn): refresh loop_host seam comments post-collapse (§5.3 Stage 2b) The seam TODO to delete CapabilityOutcome is done; dispatch emits GatedResolution directly. Comment-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(reborn): cargo fmt the collapse tree (import ordering + line wrapping) Formatting-only. The capability-result collapse and the -s ours main merge left import ordering and long-line wrapping unformatted across the loop-host, agent-loop executor tests, composition local_dev, and turns run_profile files; `cargo fmt --all` resolves them. No logic change. Fixes the red Formatting lane on PR #6299. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): retain gate-record + replay-payload stores on ProductLivePlannedRuntimeAdapters (#6299 IronLoop) `ProductLivePlannedRuntimeAdapters::from_services` moved the host-private `gate_record_store` / `replay_payload_store` into the capability factory but did not retain them on the bundle. The bundle is the production API for composing a product-live planned runtime, so a consumer building the runner (`DefaultPlannedRuntimeParts`) from it had no way to wire the SAME store the capability port persists `GateRecord::Auth` into — it would pass `gate_record_store: None`, and on an `AuthRequired` resume the turn executor reloads an empty requirement set and applies an unactionable auth block (the §5.2.9 render-from-record seam, same class as #6287 IronLoop f6/f8). Expose both stores as `pub` fields, populated from cheap `Arc` clones taken before the originals move into the factory, so both sides share one durable store. Additive, no behavior change to existing consumers. Note: the only current bundle consumer (the product-workflow planned-loop test harness) extracts just `.capability_factory`; its runner already wires the same store via `turn_executor_gate_store`, so the end-to-end product-live auth-gate tests through that path pass unchanged. This closes the bundle's API gap so a future/production runner consumer wires the store, not None. cargo check + clippy (test-support,libsql) clean on ironclaw_reborn_composition. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…model + implementation status (r8) (#6291) * docs(reborn): fold #6284 recoverability contract into the resolution model + add implementation status (r8) Two additions to the architecture-simplification note, no design changes to §1–§13: - §5.3.4 — issue #6284 (error-recoverability endgame) as a binding contract on the five resolution channels: every mid-run error must let the run survive, be seen by the model, carry cause + remediation, and give the model a turn to act; terminal is reserved for cancellation/budget/DriverBug. Narrows `HostFailure` to genuine terminal infra (model-fixable kinds re-bucket to a model-visible channel), requires per-kind remediation on `Outcome`/`Denied`, and adds the model-error observation channel + addressable `diag_ref`. Extends §11.7 with the recoverability conformance matrix and ties it to §11.9's compile-time exhaustiveness. - §14 — implementation status as of 2026-07-19, mapped to the slices/ratchets: Slice C.1 `Invocation` vocab, Slice A store deletions, and Slice B renames + the three §10 ratchets are merged; the §5.3 five-channel `Resolution` flip is in flight on integration/reborn-flip-base (#6271→#6287). Marked as the mutable status log beneath the frozen contract. Revision-log r8; references updated with #6284 and the flip stack. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(reborn): §14 — facade-method freeze landed; correct the merged-ratchet inventory Reflects the RebornServicesApi facade-method freeze ratchet (§5.2.5 step 1, PR adding reborn_facade_method_freeze_ratchet.rs) and corrects the §10 merged-ratchet count: seven ratchets are frozen on main (InMemory-store, LocalDev-typename, deployment-mode-typename, deployment-mode-branching, capability-DTO-collapse, Authorized-seal, facade-method freeze), not three. The §5.2 "Not started" bullet now scopes to the actual migration (mutations → capability descriptors, reads → view descriptors; Slice 1 synthetic-capability promotion), which the freeze unblocks but does not itself perform. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(reborn): fix RuntimeDispatchErrorKind name + scope the no-design-changes claim (#6291 review) - Gemini: the host_api dispatch error enum is `RuntimeDispatchErrorKind`, not `RuntimeFailureKind` (which does not exist) — corrected both references. - CodeRabbit: the r8 entry adds §5.3.4 and extends §11.7 (both within §1–§13), so "No design changes to §1–§13" was inaccurate — reworded to except those two additions, which are the only §1–§13 changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
§5.3 Stage 2 — the atomic flip
Collapses the overloaded ten-variant
CapabilityOutcome/CapabilityBatchOutcomeinto the five-channel
host_api::Resolution/ResolutionBatchat theLoopCapabilityPorttrait boundary. Producers convert at their seam viacapability_outcome_to_resolution(..).resolution;CapabilityOutcomeis retainedfor Stage 2b to delete.
The flip
invoke_capability -> Result<Resolution, _>,invoke_capability_batch -> Result<ResolutionBatch, _>; batch loops gate onResolution::parks()(H1: a re-entrantBlockedgate stops the batch, replacingis_suspension()).tests/integrationdoubles.Resolution— no wildcard (§11.9): loop refs from the preservedorigin,Donere-split onToolVerdict, the DependentRun staged result fromSuspension::dependent_result().Resume identity (byte-stable)
approval_request_idreconstructed deterministically from thegate:approval-{id}routing ref;prior_approvalkept on the wire (host-move deferred to Stage 2a-ii).credential_requirementsrender-from-record: moved off the loop channel ontoGateRecord::Auth, keyed by the new name-basedGateRef::for_auth_gate(auth gate id isauth-{sha256hex}, so a v5 uuid); the runner re-reads them from the durable record at blocked-exit.correlation_id, so every resume consumer reconstitutes it host-side): loop-host runtime resume reconstitutes it from theReplayPayloadbefore the fingerprinted lease match; the synthetic outbound-delivery handler does the same from its own replay payload for the approval cross-check; deterministic gate-record keys make a re-raise idempotent (GateRecordAlreadyExiststreated as benign first-write-wins, mirroringReplayPayloadAlreadyExists).Model-visible preview (#5838) — fixed, not dropped
The collapse mis-routed tool-result CONTENT through
SafeSummary, a caption typethat rejects
{ } [ ] /delimiters (all structured output) and substring-scrubbedthe ordinary word "Secretary" (the #6129 amnesia re-read loop). This ports the
canonical word-boundary credential matcher into
host_api(credential_redaction),applies it to
SafeSummary, addsModelResultPreview(the 24 KiB content vehiclethat tolerates delimiters/newlines and redacts only genuine credentials), and
carries truncated-preview continuation metadata + the producer's own observation
summary on
ResultPreviewMetasoresult_readpagination and the truncation hintsurvive. The executor reconstructs the
ResultReferenceobservation from realcontent — the inline first-look optimization is preserved.
Verification
cargo clippy --workspace --all-targets --all-features -- -D warnings— clean.cargo test -p ironclaw_architecture— green.llm_admin::nearai_mcpenv-var tests, unrelated to this change).reborn_group_approvals,reborn_group_journeys,reborn_integration_auth_gate,reborn_integration_auth_failure,reborn_integration_outbound_target,reborn_integration_tool_call, …).scripts/pre-commit-safety.sh— OK.What remains on
CapabilityOutcome(Stage 2b)Producers still emit
CapabilityOutcomeand convert at the loop_host/mapping seam;Stage 2b migrates producers to emit
Resolutiondirectly and deletesCapabilityOutcome.prior_approvalon the auth wire moves host-side in Stage 2a-ii.🤖 Generated with Claude Code