Skip to content

refactor(reborn): loop-facing result becomes host_api::Resolution (§5.3 Stage 2 — the flip) - #6287

Merged
ilblackdragon merged 9 commits into
integration/reborn-flip-basefrom
refactor/reborn-loop-resolution-flip-s2b
Jul 20, 2026
Merged

ilblackdragon merged 9 commits into
integration/reborn-flip-basefrom
refactor/reborn-loop-resolution-flip-s2b

Conversation

@ilblackdragon

Copy link
Copy Markdown
Member

§5.3 Stage 2 — the atomic flip

Collapses the overloaded ten-variant CapabilityOutcome/CapabilityBatchOutcome
into the five-channel host_api::Resolution/ResolutionBatch at the
LoopCapabilityPort trait boundary. Producers convert at their seam via
capability_outcome_to_resolution(..).resolution; CapabilityOutcome is retained
for Stage 2b to delete.

The flip

  • invoke_capability -> Result<Resolution, _>, invoke_capability_batch -> Result<ResolutionBatch, _>; batch loops gate on Resolution::parks() (H1: a re-entrant Blocked gate stops the batch, replacing is_suspension()).
  • ~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 matches exhaustively over Resolution — no wildcard (§11.9): loop refs from the preserved origin, Done re-split on ToolVerdict, the DependentRun staged result from Suspension::dependent_result().

Resume identity (byte-stable)

  • approval_request_id reconstructed deterministically from the gate:approval-{id} routing ref; prior_approval kept on the wire (host-move deferred to Stage 2a-ii).
  • Auth credential_requirements render-from-record: moved off the loop channel onto GateRecord::Auth, keyed by the new name-based GateRef::for_auth_gate (auth gate id is auth-{sha256hex}, so a v5 uuid); the runner re-reads them from the durable record at blocked-exit.
  • Drive-to-green resume completions (the flip drops the loop-facing correlation_id, so every resume consumer reconstitutes it host-side): loop-host runtime resume reconstitutes it from the ReplayPayload before 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 (GateRecordAlreadyExists treated as benign first-write-wins, mirroring ReplayPayloadAlreadyExists).

Model-visible preview (#5838) — fixed, not dropped

The collapse mis-routed tool-result CONTENT through SafeSummary, a caption type
that rejects { } [ ] / delimiters (all structured output) and substring-scrubbed
the 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, adds ModelResultPreview (the 24 KiB content vehicle
that tolerates delimiters/newlines and redacts only genuine credentials), and
carries truncated-preview continuation metadata + the producer's own observation
summary on ResultPreviewMeta so result_read pagination and the truncation hint
survive. The executor reconstructs the ResultReference observation from real
content — the inline first-look optimization is preserved.

Verification

  • cargo clippy --workspace --all-targets --all-features -- -D warnings — clean.
  • cargo test -p ironclaw_architecture — green.
  • Changed-crate unit suites — green (only red is 3 pre-existing llm_admin::nearai_mcp env-var tests, unrelated to this change).
  • All 51 Reborn integration bins (706 scenarios) — green, including every §11.9 per-channel / gate / resume / auth tier (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 CapabilityOutcome and convert at the loop_host/mapping seam;
Stage 2b migrates producers to emit Resolution directly and deletes
CapabilityOutcome. prior_approval on the auth wire moves host-side in Stage 2a-ii.

🤖 Generated with Claude Code

….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>
@ironloopai

ironloopai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown
Contributor

🔎 IronLoop Review Status

Head: f38e56122786604d7c6498736f115c3d5b001fe9
Result: Reviewer output needs human attention or validation.
Next: Review the flagged rows before merging.
Updated: 2026-07-20T00:02:20.926Z

Current reviewers:

Reviewer State Verdict Findings Last update
ironloop/common-reviewer (reviewer) Completed Needs validation 0 blocking findings / 0 notes; needs validation 2026-07-20T00:02:20.917Z
Reviewer summaries
Reviewer Detail
ironloop/common-reviewer (reviewer) Needs validation; 0 blocking findings; No concrete correctness, security, or concurrency defect found in source review. Runtime validation remains required because Cargo is unavailable in this environment.
Recent activity
Time Reviewer State Detail
2026-07-19T23:48:24.676Z ironloop/common-reviewer (reviewer) Completed Review completed and terminal status was persisted.
2026-07-19T23:52:58.496Z ironloop/common-reviewer (reviewer) Superseded A newer PR head replaced this review (f38e561).
2026-07-19T23:53:35.680Z ironloop/common-reviewer (reviewer) Queued Accepted review request for head f38e561.
2026-07-19T23:53:35.680Z ironloop/common-reviewer (reviewer) Queued Waiting for this reviewer lane to become available.
2026-07-19T23:53:35.996Z ironloop/common-reviewer (reviewer) Started Reviewer worker started.
2026-07-19T23:53:44.014Z ironloop/common-reviewer (reviewer) Workspace ready Prepared isolated checkout (merge_ref) at 6ed60c5.
2026-07-20T00:02:20.917Z ironloop/common-reviewer (reviewer) Result captured Needs validation; 0 blocking findings.
2026-07-20T00:02:20.917Z ironloop/common-reviewer (reviewer) Completed Review completed and terminal status was persisted.
Available commands
  • @ironloopai help
  • @ironloopai agents
  • @ironloopai review
  • @ironloopai review --agent <agent>
Run metadata

Admission: webhook accepted the request and IronLoop persisted reviewer state before this projection.

@github-actions github-actions Bot added scope: dependencies Dependency updates size: XL 500+ changed lines risk: low Changes to docs, tests, or low-risk modules labels Jul 19, 2026
@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • staging
  • reborn-integration

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 37e78269-f04d-4758-8f57-ee2506e03323

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

❤️ Share

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

@github-actions github-actions Bot added the contributor: core 20+ merged PRs label Jul 19, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@ironloopai ironloopai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ 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:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when the fix may affect multiple areas.

self.persist_gate_record_for_outcome(&request, &outcome)
.await?;
Ok(outcome)
Ok(capability_outcome_to_resolution(outcome).resolution)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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>
@ilblackdragon

Copy link
Copy Markdown
Member Author

@ironloopai review

@ilblackdragon

Copy link
Copy Markdown
Member Author

✅ 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 (capability_port.rs) — invoke_capability mapped the outcome twice; approval/resource/dependent/external gates 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 it. Now maps once (persist_gate_record_for_mapped). Regression: extended approval_gate_outcome_persists_gate_record_at_the_seam to assert the returned Resolution's gate ref equals the persisted key.

2. Unsubmittable auth block fails the exit (turn_run_executor.rs) — the flip made the durable GateRecord::Auth the only source of credential_requirements, so a lookup miss (store fault / Ok(None) / wrong kind) left them empty and apply_exit persisted a provider-null, unsubmittable gate. enrich_auth_block_credential_requirements now returns Result; those arms fail the exit via a shared record_exit_failure helper. Regression: new caller-level test auth_block_with_unsourceable_requirements_fails_the_exit (which also caught a &'static str lifetime bug in the helper).

One candidate rejected as intentional: a review pass flagged the AwaitDependentRun model observation collapsing to the synthesized safe_summary. That is by design for this stage — await_dependent_run_preserves_model_observation_for_replay explicitly locks it and the PR body defers the structured-preview restoration to Stage 1b. Left as-is.

Verification: clippy --all-features clean across loop_host/runner/agent_loop; all targeted tests pass; cargo fmt --all --check clean. (Note: runner/tests/loop_driver_host.rs has 2 pre-existing --all-features failures — a feature-gated random-ResultRef retry-equality nondeterminism — that reproduce identically on the pre-fix branch head and don't run in the default-feature unit lane; not introduced here.)

IronLoop's automated re-review of the fix commit (8dfa0adf1) is still in flight, so its reviewer badge may briefly lag behind these fixes.

@ironloopai ironloopai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ 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:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when the fix may affect multiple areas.

terminate_hint: false,
byte_len: result.byte_len,
output_digest: None,
model_observation: None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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>
@ilblackdragon

Copy link
Copy Markdown
Member Author

@ironloopai review

@ilblackdragon

Copy link
Copy Markdown
Member Author

Update — IronLoop's re-review cleared the two gate-record findings and raised a third (the AwaitDependentRun model observation). On a closer look I reversed my earlier disposition: the mapping already preserves the child's observation caption on DependentRunResult.observation ("model_observation now rides the inline observation preview (was dropped entirely)"), and append_capability_result_ref keeps a Some observation — so dependent_run_result_message hardcoding None was re-dropping it, not an intentional deferral. Fixed in 4f449a3f7: it now forwards the caption as a ResultReference observation pointing at the staged child result, so the resumed parent can result_read it instead of getting a bare synthesized summary. Only the inline first-look preview content stays host-owned (the mapping's bounded-caption design), so preview is None. await_dependent_run_preserves_model_observation_for_replay updated to assert the forwarded observation; clippy --all-features + the full agent_loop suite are clean.

All three IronLoop findings are now addressed with regression tests. Re-running its review.

@ironloopai ironloopai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ 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:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when 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>
@ilblackdragon

Copy link
Copy Markdown
Member Author

Re: IronLoop's replay finding ("idempotent replays can return a newly minted gate reference that has no persisted gate record") — fixed in 2b7b192e0.

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 CapabilityOutcome, invoke_capability re-maps it (a fresh random GateRef for the approval/resource/dependent/external channels), and the old set-based guard then skipped the persist — so the replayed Resolution carried a gate ref no record was ever saved under.

The replay guard is now 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 a concurrent duplicate) finds the reservation and returns the SAME resolution — the one whose gate ref the record is under — instead of re-minting. The rollback-on-failed-save is preserved (remove the reservation so the next replay retries), so failed_gate_record_persist_is_retried_on_replay still holds.

Regression: extended 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. Before the fix the replayed ref was a fresh mint with no record.

@ilblackdragon

Copy link
Copy Markdown
Member Author

@ironloopai review

@ironloopai ironloopai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ 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:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when the fix may affect multiple areas.

&self.persisted_gate_resolutions,
"gate resolution replay cache",
)?;
if let Some(existing) = reserved.get(&idempotency_key) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
@ilblackdragon

Copy link
Copy Markdown
Member Author

Re: IronLoop's concurrency finding ("a concurrent replay can receive a blocked resolution before its gate record is durably persisted") — fixed in e4f34d788.

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 save durably completed, so a transient store fault (owner rolls back and errors) left the waiter holding a blocked resolution whose record was never persisted — an unresumable gate.

The reservation is now 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(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 same lost-wakeup-safe pattern (notify.notified() created, then re-check gate_resolution_in_flight_matches under the lock, await only if still the same reservation).
  • The owner publishes only AFTER the durable save resolves: on Ok/benign GateRecordAlreadyExists it stores Persisted(resolution); on a transient fault it clears the entry. Either way it wakes the waiters, so a woken waiter either receives the now-durable resolution or re-owns and retries the persist. No waiter ever receives a resolution before its record is durably persisted.

Note this whole class (random GateRef per map → replay/concurrency reconciliation) is the symptom the deterministic canonical-key work (Stage 2a-ii) removes at the root: with a content-addressed gate ref, re-mapping yields the same ref and the write-once store dedups without any in-memory reservation. This change makes the current random-ref design correct in the meantime.

@ilblackdragon

Copy link
Copy Markdown
Member Author

@ironloopai review

@ironloopai ironloopai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ 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:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when 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:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when 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>
@ilblackdragon

Copy link
Copy Markdown
Member Author

Re: IronLoop's ProductLive wiring finding ("ProductLive adapters' capability factory never receives the new durable replay and gate stores") — fixed in a3abf3dd0.

Fixed. The ProductLiveLoopCapabilityPortFactory built its HostRuntimeLoopCapabilityPortFactory 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, and approval/auth resumes broke (the local-dev path already wires both).

Wiring added end-to-end:

  • ProductLivePlannedRuntimeAdapterConfig gains gate_record_store + replay_payload_store; ProductLiveLoopCapabilityPortFactory carries them and 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-live planned-loop harness (planned_agent_loop.rs) 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.

cargo test -p ironclaw_reborn_composition --test product_live_adapters and the product-workflow planned-loop harness tests pass; clippy --all-features clean on both crates.

@ilblackdragon

Copy link
Copy Markdown
Member Author

@ironloopai review

@ironloopai ironloopai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ 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:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when 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>
@ilblackdragon

Copy link
Copy Markdown
Member Author

Re: IronLoop's two in-flight reservation cleanup findings ("a transient write failure or cancelled writer can leave same-key replays waiting") — fixed in a2d9ea6da.

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-save), or returned early before the publish block (e.g. a poisoned-lock ?), the InFlight reservation was orphaned and same-key replays waited on it forever; and the transient-failure cleanup relied on code after the await running.

The reservation is now protected by an RAII drop guard, mirroring the crate's own DispatchReservationGuard:

  • On reserving InFlight, the owner creates GateResolutionReservationGuard { port, key, committed: false }. Any drop without commit() — cancellation, a transient store fault, or any early error — calls clear_gate_resolution_reservation, which removes the InFlight entry (only if it is still in-flight, never a published resolution) and wakes its waiters so one re-owns and retries.
  • The success (and benign GateRecordAlreadyExists) path publishes the durable Persisted resolution, wakes waiters, then commit()s the guard so its drop is a no-op.

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: cancelled_gate_persist_clears_reservation_so_replay_can_re_own blocks the first save on a barrier, spawns the invocation, cancels it while parked in save, and asserts a replay re-owns the reservation, completes within a timeout (no hang), and persists exactly one record. The existing failed_gate_record_persist_is_retried_on_replay covers the transient-fault → cleanup → retry path through the guard.

@ilblackdragon

Copy link
Copy Markdown
Member Author

@ironloopai review

@ironloopai ironloopai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ 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:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when 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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
@ilblackdragon

Copy link
Copy Markdown
Member Author

Re: IronLoop's ProductLive auth-gate executor-wiring finding ("the capability port receives the durable gate store, but the turn executor cannot") — fixed in 4efc26f5b.

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 (RebornTurnRunExecutor, via DefaultPlannedRuntimeParts.gate_record_store) still got None, so its render-from-record path (enrich_auth_block_credential_requirements, §5.2.9) couldn't read the record and an auth block was applied with empty requirements / failed the exit.

The ProductLive 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 (was None), so a persisted auth gate record round-trips to the executor's read. It stays None only for the non-ProductLive capability fakes (RecordingCapabilityFactory/EmptyCapabilityFactory), which don't 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.

(A production ProductLive caller already holds the store — it puts it in ProductLivePlannedRuntimeAdapterConfig.gate_record_store — and wires the same instance into DefaultPlannedRuntimeParts, exactly as the harness now does; no separate bundle accessor is needed.)

product-workflow tests build + pass; clippy --all-features clean.

@ilblackdragon

Copy link
Copy Markdown
Member Author

@ironloopai review

@ironloopai ironloopai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ 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:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when 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>
@ilblackdragon

Copy link
Copy Markdown
Member Author

Re: IronLoop's replay-payload persistence retry/concurrency finding — fixed in f38e56122. guard.commit() ran before the fallible persist_replay_payload_for_fresh_gate store write, so a transient store error ?-returned with the dispatch reservation still InFlight and the committed guard skipping cleanup — stranding retries/duplicates of the same key. Moved the commit to AFTER the write succeeds, so a store error leaves the guard uncommitted and its drop clears the reservation + wakes waiters to re-dispatch. Build + loop_host tests pass.

@ilblackdragon

Copy link
Copy Markdown
Member Author

@ironloopai review

@ironloopai ironloopai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ IronLoop Review: reviewer

Review at a glance

Verdict Blocking Notes Inline Head
⚠️ Needs validation 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:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloopai review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloopai review when the fix may affect multiple areas.

@ilblackdragon

Copy link
Copy Markdown
Member Author

✅ Ready for merge — §5.3 Stage 2 (the flip: loop-facing result → host_api::Resolution). CI green 19/19; IronLoop: 0 blocking findings.

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:

  1. Map-once gate ref — the outcome was mapped twice, minting divergent random GateRefs so the persisted record and returned Resolution disagreed; now maps once.
  2. Unsubmittable auth block — apply_exit failed the exit instead of applying an auth block whose requirements couldn't be re-sourced.
  3. Dependent-run observation — forward the child's staged caption instead of dropping it (reversed after re-examining the mapping's intent).
  4. Replay re-mint — resolution cache returns the record-backed gate ref on replay instead of a fresh mint.
  5. Concurrent replay before durable persist — Notify in-flight state machine; waiters block until the durable save.
  6. Reservation cleanup on cancel/fault — RAII guard clears an orphaned reservation on any early drop (with a deterministic cancellation regression test).
  7. ProductLive capability-port store wiring — wire the gate/replay stores into the ProductLive port.
  8. ProductLive executor store sharing — share the same gate store with the turn executor (Fix B's render-from-record read).
  9. Dispatch reservation commit ordering — commit only after the replay-payload write, so a store error clears the reservation instead of stranding it.

Verification: cargo clippy --all-features clean across the touched crates (loop_host / runner / agent_loop / composition / product_workflow); all regression + gate/replay/runner/product-live suites pass; cargo fmt --all --check clean.

Note for the stack: this sits on integration/reborn-flip-base.

@ilblackdragon
ilblackdragon merged commit 8f78a0f into integration/reborn-flip-base Jul 20, 2026
23 checks passed
@ilblackdragon
ilblackdragon deleted the refactor/reborn-loop-resolution-flip-s2b branch July 20, 2026 00:35
ilblackdragon added a commit that referenced this pull request Jul 20, 2026
…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>
ilblackdragon added a commit that referenced this pull request Jul 20, 2026
… 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>
ilblackdragon added a commit that referenced this pull request Jul 20, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: low Changes to docs, tests, or low-risk modules scope: dependencies Dependency updates size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant