Skip to content

test(trace): bind replay results by exact tool call - #6659

Merged
serrrfirat merged 1 commit into
mainfrom
codex/canonical-trace-replay-contract
Jul 27, 2026
Merged

serrrfirat merged 1 commit into
mainfrom
codex/canonical-trace-replay-contract

Conversation

@serrrfirat

@serrrfirat serrrfirat commented Jul 24, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Replace heuristic dynamic-result lookup with one exact fixture marker: the original tool_call_id plus an RFC 6901 JSON Pointer.
  • Teach the opt-in recorder, Rust TraceLlm, and existing Python mock LLM to produce/resolve that marker, including provider-normalized call IDs and complete host-evidence previews.
  • Migrate the one committed fixture that used the old dynamic templates and exercise the real Docs journey through Reborn and Emulate.

This deliberately does not introduce trace format versioning, migrate the fixture catalog, change importer behavior, move trace types, or add a cross-language conformance framework.

Change Type

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • CI/Infrastructure
  • Security
  • Dependencies

Linked Issue

Related #6524

Validation

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo build (built by the standalone Reborn/Emulate E2E setup)
  • Relevant tests pass (listed below)
  • cargo test --features integration — Not applicable: no database-backed behavior changed.
  • Manual testing: QA 5D replayed through standalone Reborn, first-party capabilities, mediated HTTP, and Emulate using the fresh provider-issued document ID.
  • If a coding agent was used and supports it, review-pr or pr-shepherd --fix was run before requesting review.

Test Strategy

User behavior:

Recorded-model tests can replay a later capability call using the fresh resource ID returned by the current hermetic provider run. They no longer guess by tool name or a list of possible field names.

Risk areas:

  • Model behavior
  • Browser
  • Side effect
  • Persistence
  • Security or permissions
  • External provider
  • Cross-component behavior

Tests added or updated:

  • Unit or contract: exact call-ID/pointer success, missing-call failure, complete-vs-truncated evidence, recorder nested binding, and recorder ambiguity behavior.
  • Reborn integration: TraceLlm caller test proves exact binding through the provider call; the existing trace-format suite remains green.
  • Recorded fixture: one legacy dynamic-template fixture migrated; the 61-file Reborn QA scrub validator and all 45 Python replay tests pass.
  • Browser E2E: Not applicable: no browser behavior changes.
  • Backend or runtime: the complete pinned-Emulate smoke lane passed: inventory, journey coverage, provider contracts, fixture replay, and all full-path Reborn journeys (215 tests). The previously flaky QA 6C/6E sequence also passed five consecutive stress runs.
  • Live canary: Not applicable: no live-model/provider behavior changes.

What the tests prove:

A binding consumes only the named prior tool call and exact result path; missing IDs, missing pointers, ambiguous recorder matches, and truncated evidence fail rather than selecting a plausible value. The caller-level Docs run proves a fixture call ID still resolves when the provider stack normalizes the runtime call ID.

Commands run:

cargo test -p ironclaw_llm --no-fail-fast                     # 942 passed
cargo test --test trace_llm_tests
cargo test --test trace_format                                # 8 passed
cargo test -p ironclaw_architecture
cargo clippy -p ironclaw_llm --all-targets --all-features -- -D warnings
cargo clippy --workspace --all-targets --all-features -- -D warnings
uv run --project tests/e2e --no-sync pytest -q tests/e2e/scenarios/test_reborn_qa_trace_replay.py  # 45 passed
IRONCLAW_EMULATE_CLI=<pinned fork CLI> uv run --project tests/e2e --no-sync pytest -q <five Reborn/Emulate smoke files> --timeout=120  # 215 passed
IRONCLAW_EMULATE_CLI=<pinned fork CLI> uv run --project tests/e2e --no-sync pytest -q tests/e2e/scenarios/test_reborn_qa_trace_full_path.py -k "qa_6c or qa_6e" --timeout=120  # 5 consecutive runs passed
scripts/ci/check-reborn-qa-fixtures.sh                         # 61 files passed
jq empty tests/fixtures/llm_traces/live/routine_btc_create_test_and_refire.json

scripts/pre-commit-safety.sh was also run. Its composition budget passed, then it stopped on an existing ARCH-SPRAWL warning in crates/ironclaw_auth/src/product_auth/api/auth.rs; this PR has no diff in that file.

Security Impact

None. This changes test replay infrastructure and the opt-in trace recorder. Binding failures report IDs and paths, not provider result values.

Reborn Trust-Boundary Checklist

N/A: no Reborn trust, policy, ingress, persistence, authorization, or production runtime boundary changes.

Database Impact

None.

Blast Radius

Eleven files: the trace binding helper, recorder, Rust replay support, existing Python mock LLM, focused tests/docs, and one fixture. Existing trace shape and legacy {{call.field}} replay remain compatible.

Rollback Plan

Revert this PR. No database, fixture-catalog, or product migration is involved.

Review Follow-Through

Reviewer judgment is most useful around exact call-ID aliasing and the rule that host-evidence previews are usable only when complete.


Review track: A (docs/tests/chore)

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@ironloopai

ironloopai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

🔎 IronLoop Review Status

Head: 75278582ad07eee5a53fda0bdc832dbf2fa60952
Result: 1/1 reviewers completed without blocking findings.
Next: Ready for normal human review and CI checks.
Updated: 2026-07-24T23:32:33.482Z

Current reviewers:

Reviewer State Verdict Findings Last update
ironloop/common-reviewer (reviewer) Completed Approved 0 blocking findings / 0 notes 2026-07-24T23:32:33.471Z
Reviewer summaries
Reviewer Detail
ironloop/common-reviewer (reviewer) Approved; 0 blocking findings; Approved. Reviewed the focused 11-file merge-base diff; exact ID/pointer binding, alias handling, recorder behavior, and fixture migration are coherent and covered by targeted tes…
Recent activity
Time Reviewer State Detail
2026-07-24T23:15:52.293Z ironloop/common-reviewer (reviewer) Completed Review completed and terminal status was persisted.
2026-07-24T23:26:15.342Z ironloop/common-reviewer (reviewer) Superseded A newer PR head replaced this review (7527858).
2026-07-24T23:27:08.513Z ironloop/common-reviewer (reviewer) Queued Accepted review request for head 7527858.
2026-07-24T23:27:08.513Z ironloop/common-reviewer (reviewer) Queued Waiting for this reviewer lane to become available.
2026-07-24T23:27:09.242Z ironloop/common-reviewer (reviewer) Started Reviewer worker started.
2026-07-24T23:27:11.409Z ironloop/common-reviewer (reviewer) Workspace ready Prepared isolated checkout (merge_ref) at 603561d.
2026-07-24T23:32:33.471Z ironloop/common-reviewer (reviewer) Result captured Approved; 0 blocking findings.
2026-07-24T23:32:33.471Z 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.

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6659 July 24, 2026 17:54 Destroyed
@github-actions github-actions Bot added scope: docs Documentation size: XL 500+ changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels Jul 24, 2026
@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds exact $trace_result bindings using original tool-call IDs and RFC 6901 JSON pointers. Recording, Rust trace replay, Python mock replay, tool-call ID propagation, Google trace normalization, and focused tests now use the structured binding format.

Changes

Trace-result binding and replay

Layer / File(s) Summary
Binding contract and resolver
crates/ironclaw_llm/src/lib.rs, crates/ironclaw_llm/src/trace_binding.rs
Exports observed tool results and recursively resolves validated markers through canonical payloads and JSON pointers.
Recording exact result references
crates/ironclaw_llm/src/recording.rs, crates/ironclaw_llm/CLAUDE.md
Records unique native tool-result matches as tool-call-ID and pointer markers while retaining legacy substitutions.
Rust trace replay integration
tests/support/trace_llm.rs, tests/trace_llm_tests.rs
Resolves bindings before template substitution and preserves unconsumed steps when binding fails.
Python replay identity propagation
tests/e2e/mock_llm.py
Tracks call-ID aliases, resolves pointer bindings, validates IDs, and preserves explicit IDs.
Replay fixture validation
tests/e2e/scenarios/*
Updates Google normalization and replay scenarios for exact-call selection, deterministic IDs, and missing-call validation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RecordingLlm
  participant TraceLlm
  participant MockLLM
  participant ToolResults
  RecordingLlm->>RecordingLlm: create $trace_result with tool_call_id and pointer
  TraceLlm->>ToolResults: collect observed tool results
  TraceLlm->>TraceLlm: resolve pointer bindings
  MockLLM->>ToolResults: map runtime IDs to recorded IDs
  ToolResults-->>MockLLM: return pointer-selected values
Loading

Possibly related PRs

  • nearai/ironclaw#6422: Touches the prior-tool lookup and tool-call argument parameterization path in recording.rs.
  • nearai/ironclaw#6439: Modifies the trace-replay harness and harvested tool-call replay scenarios.
  • nearai/ironclaw#6466: Updates related $trace_result replay resolution and tool-call binding behavior.

Suggested reviewers: ilblackdragon

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title uses Conventional Commits form and accurately describes exact tool-call binding in replay traces.
Description check ✅ Passed The description matches the template and fills the required summary, validation, test strategy, impact, rollback, and review sections.

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.

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

Head: f7ac4586dc731766c5e2734a6a24a67a1b26b999
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 versioned trace contract does not currently build its conformance tests, and exact bindings are not applied by the Reborn replay gateway.

Findings

Blocking: 2 / Notes: 0

Blocking findings

1. ❌ [HIGH] Add the missing shared trace-contract vectors

Location: crates/ironclaw_llm/src/trace.rs:552-554
This include_str! references tests/fixtures/llm_traces/trace_contract_cases.json, but that file is not committed. The Python conformance test opens the same missing path. Consequently cargo test -p ironclaw_llm cannot compile and the pytest contract test fails; add the fixture or update both references to a committed location.

2. ❌ [MEDIUM] Apply result bindings in the Reborn replay path

Location: tests/fixtures/llm_traces/live/routine_btc_create_test_and_refire.json:383
The new resolver is invoked only by TraceLlm and the Python mock. RebornTraceReplayModelGateway forwards TraceToolCall.arguments directly into provider replay calls, so this marker reaches routine_fire as an object instead of the fresh string value. Resolve bindings from request tool-result metadata/content in that gateway and add a caller-level Reborn 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.

Comment thread crates/ironclaw_llm/src/trace.rs Outdated
}

fn contract_cases() -> ContractCases {
serde_json::from_str(include_str!(

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.

The referenced shared conformance fixture is absent. include_str! will make the crate test build fail, and the Python test references the same path. Please commit the vector file or point both consumers at a committed file.

"name": "routine_fire",
"arguments": {
"name": "{{call_657a9167a2d74cdd88556b34.name}}"
"name": {"$trace_result":{"tool_call_id":"call_657a9167a2d74cdd88556b34","pointer":"/name"}}

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.

RebornTraceReplayModelGateway does not invoke the new binding resolver, so a Reborn replay forwards this marker object as the name argument. Resolve it against the current request's tool results in that gateway and cover the path with a Reborn replay test.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Not applicable to the reduced PR: this fixture lives under tests/fixtures/llm_traces/live and is replayed by TraceLlm, which resolves the marker. RebornTraceReplayModelGateway only loads the separate reborn_qa fixture set; no code path loads this live fixture through that gateway. Adding a second resolver there would expand this intentionally simplified PR into cross-harness consolidation work.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@crates/ironclaw_llm/src/recording.rs`:
- Around line 29-34: Remove the public re-export block from recording.rs and
make the relocated trace symbols private imports instead. Update references to
use crate::trace directly, or expose them through the intended named cross-crate
facade for ironclaw_reborn_traces, rather than preserving the recording module
path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b581618c-922b-46e2-a148-6d37a53018ef

📥 Commits

Reviewing files that changed from the base of the PR and between 5efb0e2 and f7ac458.

⛔ Files ignored due to path filters (131)
  • tests/fixtures/llm_traces/README.md is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/advanced/bootstrap_onboarding.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/advanced/iteration_limit.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/advanced/long_tool_chain.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/advanced/mcp_extension_lifecycle.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/advanced/message_queue_during_tools.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/advanced/multi_turn_memory.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/advanced/prompt_injection_resilience.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/advanced/routine_event_any_channel.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/advanced/routine_event_telegram.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/advanced/routine_news_digest.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/advanced/steering.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/advanced/tool_error_recovery.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/advanced/tool_intent_no_false_positive.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/advanced/tool_intent_nudge_cap.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/advanced/tool_intent_nudge_recovery.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/advanced/workspace_search.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/bug_bash/summarization_uses_tools.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/coverage/approval_always.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/coverage/approval_always_floor.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/coverage/approval_bare_yes_no_pending.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/coverage/approval_no.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/coverage/approval_slash.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/coverage/approval_yes.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/coverage/injection_in_echo.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/coverage/status_events_tool_chain.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/engine_v2/execution_obligation_exhaustion.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/engine_v2/execution_obligation_followup.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/engine_v2/execution_obligation_no_nudge.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/engine_v2/execution_obligation_nudge.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/engine_v2/external_tool_round_trip.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/engine_v2/multi_tool_chain.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/engine_v2/multi_turn.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/engine_v2/single_tool_echo.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/engine_v2/smoke_text.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/engine_v2/tool_error_recovery.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/error_path.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/file_write_read.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/live/ceo_full_workflow.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/live/code_review_pr_2483.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/live/content_creator_full_workflow.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/live/developer_full_workflow.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/live/mission_daily_news_digest.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/live/routine_btc_create_test_and_refire.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/live/sandbox_clones_ironclaw_to_megaclaw.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/live/trader_full_workflow.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/live/zizmor_scan.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/live/zizmor_scan_v2.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/memory/write_then_read_same_scope.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/memory/write_to_protected_path_rejected.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/memory_write_read.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/connect_gmail.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/github_notifications.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/investigate_ci_job.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_10a_slack_self_attribution.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_10b_slack_ooo_status.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_10c_slack_thread_replies.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_10d_slack_channel_membership.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_10e_slack_error_honesty.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_10f_slack_mention_encoding.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_10g_slack_last_message_sent.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_10g_slack_last_message_sent_global.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_10h_slack_email_hallucination_guard.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_10i_slack_raw_entity_hygiene.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_2d_calendar_prep_live_chat.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_2f_calendar_prep_email_delivery.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_3b_endpoint_status_live_chat.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_3c_endpoint_status_slack_routine.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_3d_endpoint_status_slack_delivery.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_4e_github_release_email_delivery.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_5d_slack_strategy_doc_answer.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_6c_gmail_to_sheet_live_chat.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_6e_gmail_to_sheet_delivery.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_7c_slack_bug_logger_routine.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_7d_slack_bug_message_trigger.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_7e_slack_bug_sheet_delivery.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_8b_hn_keyword_live_chat.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_8c_hn_keyword_slack_routine.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_8d_hn_keyword_slack_delivery.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_9b_routine_dm_delivery_exactly_once.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/qa_9d_routine_per_trigger_delivery_target.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/quarantined_retired_activation/qa_2a_gmail_connect.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/quarantined_retired_activation/qa_2b_calendar_connect.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/quarantined_retired_activation/qa_2c_drive_connect.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/quarantined_retired_activation/qa_2e_calendar_prep_email_routine.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/quarantined_retired_activation/qa_4a_gmail_connect.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/quarantined_retired_activation/qa_4b_github_connect.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/quarantined_retired_activation/qa_4c_github_release_live_chat.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/quarantined_retired_activation/qa_4d_github_release_slack_routine.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/quarantined_retired_activation/qa_5b_drive_connect.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/quarantined_retired_activation/qa_5c_strategy_doc_knowledge_base.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/quarantined_retired_activation/qa_6a_gmail_connect.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/quarantined_retired_activation/qa_6b_sheets_connect.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/quarantined_retired_activation/qa_6d_gmail_to_sheet_routine.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/quarantined_retired_activation/qa_7b_sheets_connect.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/live_canary/quarantined_retired_activation/qa_9c_slack_digest_names_not_ids.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/routine_crm_inbox.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/routine_health_ping.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/routine_hn_monitor.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/routine_meeting_prep.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/routine_release_watch.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/slack_channel_membership.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/slack_entity_hygiene.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/slack_mention_encoding.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/slack_ooo_status.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/slack_recent_message.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/slack_self_attribution.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/slack_thread_replies.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/web_hn_search.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/web_release_summary.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/reborn_qa/web_status_check.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/recorded/baseball_stats.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/recorded/telegram_check.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/recorded/weather_sf.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/runtime_policy/hosted_dev_no_shell.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/runtime_policy/local_dev_text_only.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/runtime_policy/no_policy_text_only.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/simple_text.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/spot/attachment_audio_transcript.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/spot/attachment_image.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/spot/chain_write_read.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/spot/memory_save_recall.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/spot/robust_correct_tool.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/spot/robust_no_tool.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/spot/smoke_greeting.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/spot/smoke_math.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/spot/tool_echo.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/spot/tool_json.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/threading/concurrent_dispatch.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/threading/multi_turn_state.json is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/threading/undo_redo.json is excluded by !tests/fixtures/**
📒 Files selected for processing (14)
  • crates/ironclaw_llm/CLAUDE.md
  • crates/ironclaw_llm/src/lib.rs
  • crates/ironclaw_llm/src/recording.rs
  • crates/ironclaw_llm/src/trace.rs
  • crates/ironclaw_reborn_traces/src/contribution.rs
  • scripts/ci/check-reborn-qa-fixtures.sh
  • scripts/import-reborn-run-artifact.py
  • scripts/test-import-reborn-run-artifact.py
  • tests/e2e/mock_llm.py
  • tests/e2e/scenarios/test_reborn_qa_trace_full_path.py
  • tests/e2e/scenarios/test_reborn_qa_trace_replay.py
  • tests/e2e/trace_contract.py
  • tests/support/trace_llm.rs
  • tests/trace_format.rs

Comment thread crates/ironclaw_llm/src/recording.rs Outdated
@railway-app

railway-app Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the ironclaw-pr-6659 environment in ironclaw-ci-preview

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jul 24, 2026 at 11:34 pm

@serrrfirat
serrrfirat force-pushed the codex/canonical-trace-replay-contract branch from f7ac458 to 5a45376 Compare July 24, 2026 21:19
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6659 July 24, 2026 21:19 Destroyed
@github-actions github-actions Bot added size: L 200-499 changed lines and removed size: XL 500+ changed lines labels Jul 24, 2026
@serrrfirat serrrfirat changed the title test(trace): canonicalize versioned replay contract test(trace): bind replay results by exact tool call Jul 24, 2026
@serrrfirat

Copy link
Copy Markdown
Collaborator Author

@ironloopai review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
tests/e2e/scenarios/test_reborn_qa_trace_full_path.py (1)

698-706: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Pointer /file/id is silently coupled to the name rewrite 20 lines above.

The create call is rewritten to google-drive__upload_file at line 684, so this pointer targets the Drive upload result shape rather than google-docs__create_document's. That coupling isn't obvious at this call site and will break quietly-then-loudly if the rewrite target ever changes. One line of comment earns its keep here.

♻️ Suggested note
             elif name.startswith("google-docs__") and "document_id" in arguments:
                 call["name"] = "google-drive__download_file"
+                # `create_document` was rewritten to `google-drive__upload_file`
+                # above, so bind against the Drive upload result shape.
                 call["arguments"] = {
                     "file_id": (
                         _result_binding(created_document_call_id, "/file/id")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/scenarios/test_reborn_qa_trace_full_path.py` around lines 698 -
706, Add a concise inline comment in the google-docs call rewrite branch
explaining that `/file/id` intentionally references the result shape produced by
the earlier `google-drive__upload_file` rewrite. Keep the existing `file_id`
binding and fallback behavior unchanged.
tests/e2e/scenarios/test_reborn_qa_trace_replay.py (1)

370-387: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercises the alias remap through the real HTTP caller — good.

Gap worth closing while you're here: _capture_trace_tool_call_id_aliases has two branches that abandon aliasing (count mismatch at line 486, name divergence at lines 497-503 — see my comment on tests/e2e/mock_llm.py lines 485-505). Neither is covered, so a regression there would surface only as a confusing no tool call with id conflict in an unrelated suite. A third case here — same fixture, but the runtime returns a differently-named or extra tool call — would pin the intended failure mode.

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

In `@tests/e2e/scenarios/test_reborn_qa_trace_replay.py` around lines 370 - 387,
Extend the trace replay coverage around created_message to exercise
_capture_trace_tool_call_id_aliases when the runtime returns an extra tool call
or a differently named tool call. Assert that aliasing is abandoned and the real
HTTP caller produces the intended “no tool call with id” conflict, rather than
silently matching the wrong call.
crates/ironclaw_llm/src/recording.rs (2)

809-848: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Ambiguous exact match still falls through to legacy substitution.

The comment states ambiguous exact matches "remain literal rather than guessing," but when exact_matches.len() > 1 the code doesn't return — it proceeds into the lookup.legacy loop (840-847), which can still rewrite the literal via an unrelated, HashMap-iteration-order-dependent legacy entry that coincidentally shares the same string value. This contradicts the stated invariant for that overlap case and isn't covered by the ambiguous test (which uses an empty legacy map).

🐛 Proposed fix
             if let [(tool_call_id, pointer)] = exact_matches.as_slice() {
                 *value = serde_json::json!({
                     "$trace_result": {
                         "tool_call_id": tool_call_id,
                         "pointer": pointer,
                     }
                 });
                 return;
             }
+            if !exact_matches.is_empty() {
+                // Ambiguous exact match: stay literal rather than falling
+                // back to a possibly-unrelated legacy template.
+                return;
+            }
             for (call_id, fields) in &lookup.legacy {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_llm/src/recording.rs` around lines 809 - 848, Update
parameterize_value so an ambiguous exact match (exact_matches containing more
than one result) returns immediately without entering the lookup.legacy
substitution loop. Preserve literal values for this overlap case while retaining
exact replacement for a single match and legacy substitution when no exact match
exists.

574-906: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Document why this expansion cannot move into a smaller module.

crates/ironclaw_llm/src/recording.rs is 2505 lines and this patch makes it larger (+155/-43). Per crates/**/*.rs file-size guidance, 1,500–3,000 line files should become shorter when touched unless no suitable alternative exists, and additions over ~200 lines need inline justification. Add a one-line // arch-exempt: <reason>, plan #NNNN`` above this expansion, or split this into a companion module.

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

In `@crates/ironclaw_llm/src/recording.rs` around lines 574 - 906, Document why
the new prior-tool lookup and parameterization helpers around
build_prior_tool_lookup and parameterize_value remain in recording.rs despite
its size. Add a one-line arch-exempt comment above this expansion with a concise
reason and the applicable plan issue number, or move the helpers into a suitable
companion module if no exemption is justified.

Source: Coding guidelines

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

Inline comments:
In `@crates/ironclaw_llm/src/trace_binding.rs`:
- Around line 24-54: Update TraceBindingError to derive its Display and Error
implementations with thiserror, using appropriate error-message annotations for
InvalidMarker, MissingToolCall, and MissingPointer. Remove the manual
fmt::Display and std::error::Error implementations while preserving the existing
message text and field formatting.

In `@tests/e2e/mock_llm.py`:
- Around line 338-346: The tool-call schema must require id before bindings use
it: update _parse_llm_trace’s tool_calls validation in tests/e2e/mock_llm.py
(lines 338-346) to validate id alongside name and arguments. In
tests/e2e/scenarios/test_reborn_qa_trace_full_path.py, replace direct call["id"]
access at lines 683 and 708-709 with optional lookup via call.get("id"),
preserving the existing seeded-identifier fallback and optional guards.
- Around line 338-346: Update _parse_llm_trace to validate each tool call’s id
alongside its name and arguments, requiring id to be present and a string.
Reject malformed trace steps during parsing with the existing validation error
path before _resolve_trace_result_bindings or pending_tool_calls processing.
- Around line 517-522: Rename the copied result variable inside the loop over
_find_tool_results in the result-building flow so the loop target is not
rebound, while preserving the existing dict conversion, tool_call_id mapping,
and results append behavior.

In `@tests/trace_llm_tests.rs`:
- Around line 3-4: Update the support import in tests/trace_llm_tests.rs to use
the crate-root path by changing the TraceLlm symbols’ import from
support::trace_llm to crate::support::trace_llm.

---

Outside diff comments:
In `@crates/ironclaw_llm/src/recording.rs`:
- Around line 809-848: Update parameterize_value so an ambiguous exact match
(exact_matches containing more than one result) returns immediately without
entering the lookup.legacy substitution loop. Preserve literal values for this
overlap case while retaining exact replacement for a single match and legacy
substitution when no exact match exists.
- Around line 574-906: Document why the new prior-tool lookup and
parameterization helpers around build_prior_tool_lookup and parameterize_value
remain in recording.rs despite its size. Add a one-line arch-exempt comment
above this expansion with a concise reason and the applicable plan issue number,
or move the helpers into a suitable companion module if no exemption is
justified.

In `@tests/e2e/scenarios/test_reborn_qa_trace_full_path.py`:
- Around line 698-706: Add a concise inline comment in the google-docs call
rewrite branch explaining that `/file/id` intentionally references the result
shape produced by the earlier `google-drive__upload_file` rewrite. Keep the
existing `file_id` binding and fallback behavior unchanged.

In `@tests/e2e/scenarios/test_reborn_qa_trace_replay.py`:
- Around line 370-387: Extend the trace replay coverage around created_message
to exercise _capture_trace_tool_call_id_aliases when the runtime returns an
extra tool call or a differently named tool call. Assert that aliasing is
abandoned and the real HTTP caller produces the intended “no tool call with id”
conflict, rather than silently matching the wrong call.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5d512560-74ed-4ba5-a6ca-0210a4a64d60

📥 Commits

Reviewing files that changed from the base of the PR and between f7ac458 and 5a45376.

⛔ Files ignored due to path filters (2)
  • tests/fixtures/llm_traces/README.md is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/live/routine_btc_create_test_and_refire.json is excluded by !tests/fixtures/**
📒 Files selected for processing (9)
  • crates/ironclaw_llm/CLAUDE.md
  • crates/ironclaw_llm/src/lib.rs
  • crates/ironclaw_llm/src/recording.rs
  • crates/ironclaw_llm/src/trace_binding.rs
  • tests/e2e/mock_llm.py
  • tests/e2e/scenarios/test_reborn_qa_trace_full_path.py
  • tests/e2e/scenarios/test_reborn_qa_trace_replay.py
  • tests/support/trace_llm.rs
  • tests/trace_llm_tests.rs

Comment thread crates/ironclaw_llm/src/trace_binding.rs Outdated
Comment thread tests/e2e/mock_llm.py
Comment thread tests/e2e/mock_llm.py Outdated
Comment thread tests/trace_llm_tests.rs Outdated

@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 5a45376dd3bd

Head: 5a45376dd3bd817fa1e144d1d2b4d48f1ed34443
Next: Human review or validation is required before merging.

Run details

Status: Current
Needs human: no
Needs validation: yes

Summary

Static review found no actionable defects. Focused runtime validation remains required because this environment lacks Cargo and Python/UV tooling.

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.

@serrrfirat
serrrfirat force-pushed the codex/canonical-trace-replay-contract branch from 5a45376 to 7716877 Compare July 24, 2026 21:47
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6659 July 24, 2026 21:47 Destroyed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/e2e/scenarios/test_reborn_qa_trace_full_path.py (1)

698-722: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Align the Sheets/drive trace markers with the recorded provider result shapes.

These exact markers (/file/id, /spreadsheet_id, /sheets/0/sheet_id) will fail when the replay resolves against native/canonical return shapes such as Drive id or Sheets spreadsheetId/nested sheet objects, violating tests/e2e/CLAUDE.md’s recorded→replay determinism requirement. Include a read-only replay invariant that asserts each binding pointer is present in the corresponding fixture payload.

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

In `@tests/e2e/scenarios/test_reborn_qa_trace_full_path.py` around lines 698 -
722, Update the Google Drive/Sheets call-rewrite logic using _result_binding so
pointers match the recorded provider result shapes: Drive’s canonical id,
Sheets’ spreadsheetId, and the nested sheet identifier. Add a read-only replay
invariant that verifies each generated binding pointer exists in its
corresponding fixture payload, preserving recorded-to-replay determinism.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/ironclaw_llm/src/recording.rs`:
- Around line 618-629: Update index_json_into to call parse_tool_result_content
for its content parsing instead of duplicating the serde_json::from_str and
coerce_python_repr_to_json fallback chain. Preserve the existing indexing
behavior while making parse_tool_result_content the single shared parsing path.
- Around line 863-901: The find_string_pointers function currently excludes
matches at the document root. Remove the pointer non-empty condition so a root
scalar string matching target records an empty JSON Pointer, preserving
resolve_trace_result_bindings support and exact replay of root scalar tool
results.

In `@tests/e2e/mock_llm.py`:
- Around line 476-523: Update _capture_trace_tool_call_id_aliases so a pending
batch is always cleared when actual_calls and pending_tool_calls have different
lengths. Before returning from the mismatch guard, reset
state["pending_tool_calls"] (or record the drift while dropping the batch),
preventing stale calls from being matched against a later assistant turn.
- Around line 368-390: Update _canonical_trace_result_payload to enforce the
Rust resolver’s trace-binding contract: validate that detail.preview.preview
contains valid JSON and that byte_len and total_bytes are integers, rejecting
non-integer numeric values. Preserve the existing $trace_result envelope,
tool_call_id, and pointer validation.

In `@tests/e2e/scenarios/test_reborn_qa_trace_replay.py`:
- Around line 45-87: Extend the trace-result binding tests around
_resolve_trace_result_bindings to use the host-evidence
schema_version/status/trust envelope with detail.preview payloads instead of
only bare content. Add mirrored coverage for complete previews being unwrapped,
truncated previews remaining intact, and JSON Pointer tokens decoding ~0 and ~1
(including the /files/0/a~1b~0c path), while preserving the existing
requested-call and missing-call assertions.

---

Outside diff comments:
In `@tests/e2e/scenarios/test_reborn_qa_trace_full_path.py`:
- Around line 698-722: Update the Google Drive/Sheets call-rewrite logic using
_result_binding so pointers match the recorded provider result shapes: Drive’s
canonical id, Sheets’ spreadsheetId, and the nested sheet identifier. Add a
read-only replay invariant that verifies each generated binding pointer exists
in its corresponding fixture payload, preserving recorded-to-replay determinism.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: efa87d87-db0d-41db-90c6-2624c5069583

📥 Commits

Reviewing files that changed from the base of the PR and between 5a45376 and 7716877.

⛔ Files ignored due to path filters (2)
  • tests/fixtures/llm_traces/README.md is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/live/routine_btc_create_test_and_refire.json is excluded by !tests/fixtures/**
📒 Files selected for processing (9)
  • crates/ironclaw_llm/CLAUDE.md
  • crates/ironclaw_llm/src/lib.rs
  • crates/ironclaw_llm/src/recording.rs
  • crates/ironclaw_llm/src/trace_binding.rs
  • tests/e2e/mock_llm.py
  • tests/e2e/scenarios/test_reborn_qa_trace_full_path.py
  • tests/e2e/scenarios/test_reborn_qa_trace_replay.py
  • tests/support/trace_llm.rs
  • tests/trace_llm_tests.rs

Comment thread crates/ironclaw_llm/src/recording.rs
Comment thread crates/ironclaw_llm/src/recording.rs
Comment thread tests/e2e/mock_llm.py
Comment thread tests/e2e/mock_llm.py
Comment thread tests/e2e/scenarios/test_reborn_qa_trace_replay.py
@serrrfirat
serrrfirat force-pushed the codex/canonical-trace-replay-contract branch from 7716877 to c3162dc Compare July 24, 2026 21:56
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6659 July 24, 2026 21:56 Destroyed
@serrrfirat

Copy link
Copy Markdown
Collaborator 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 c3162dc1ba3c

Head: c3162dc1ba3ca6c0c907273c8fc5289a0eee8afb
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

Two correctness issues block the updated trace-replay coverage: QA 9C’s generated disclosure calls now fail strict trace installation, and truncated host-evidence previews can still be treated as binding sources.

Findings

Blocking: 2 / Notes: 0

Blocking findings

1. ❌ [MEDIUM] Generated QA 9C disclosure calls lack newly-required IDs

Location: tests/e2e/mock_llm.py:183-184
The new validation rejects every tool call without an id, but _load_trace still invokes _inject_deferred_tool_disclosure for QA 9C, and that helper creates capability_info calls with only name and arguments. Installing that transformed trace now returns 400 before the full-path journey runs. Give injected calls deterministic IDs (or otherwise preserve compatibility).

2. ❌ [MEDIUM] Paged evidence previews can be mistaken for complete results

Location: crates/ironclaw_llm/src/trace_binding.rs:119-120
byte_len == total_bytes is not sufficient to establish that a result-reference preview is complete: paged/truncated host observations can carry equal byte counts while detail.next_offset is present. If such a partial preview is valid JSON, this unwraps and binds from it instead of failing as intended. Require an absent/null next_offset before accepting the preview, mirror that condition in mock_llm.py, and cover a valid-JSON truncated preview.

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.

Comment thread tests/e2e/mock_llm.py
for tool_index, tool_call in enumerate(tool_calls):
if (
not isinstance(tool_call, dict)
or not isinstance(tool_call.get("id"), str)

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.

QA 9C still injects capability_info calls without IDs in _inject_deferred_tool_disclosure, which _load_trace posts through this parser. This new requirement therefore makes that full-path trace fail installation with 400. Please assign deterministic IDs to those generated calls.

.get("total_bytes")
.and_then(serde_json::Value::as_u64),
),
(Some(byte_len), Some(total_bytes)) if byte_len == total_bytes

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.

Equal byte counts do not prove a host-evidence preview is complete: paged result references may still have next_offset. A valid-JSON partial preview would be accepted here and bind instead of failing. Require no next_offset as well, and mirror the rule in the Python resolver.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
tests/e2e/mock_llm.py (1)

478-492: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Drift guard still leaks a stale batch into later turns.

pending is only consumed on the success path (Line 525). When the runtime batch length differs, Line 492 returns with state["pending_tool_calls"] intact, so those recorded IDs survive and get name-matched against a different assistant batch once the counts happen to coincide. _trace_tool_results_with_recorded_ids then relabels the wrong runtime results and $trace_result resolves to stale data instead of failing — inverting the exact-binding guarantee this PR is adding. Clear (or record) the batch on drift.

🐛 Drop the batch instead of carrying it forward
     if len(actual_calls) != len(pending):
+        state["pending_tool_calls"] = []
         return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/mock_llm.py` around lines 478 - 492, Update
_capture_trace_tool_call_id_aliases so a pending_tool_calls batch is cleared or
otherwise discarded before returning when the actual assistant tool-call count
differs from pending. Preserve the existing successful alias-mapping behavior,
but ensure drifted IDs cannot be reused by later assistant batches or relabeled
by _trace_tool_results_with_recorded_ids.
tests/e2e/scenarios/test_reborn_qa_trace_replay.py (1)

45-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Python resolver's envelope and pointer-escape paths are still uncovered.

Both cases feed bare dict content, so _canonical_trace_result_payload and _decode_trace_pointer_token — the two functions that must stay byte-compatible with resolve_trace_result_bindings in crates/ironclaw_llm/src/trace_binding.rs — are never exercised, even though real runtime results arrive as the schema_version/status/trust + detail.preview envelope. Add the mirrored cases the Rust suite already has: complete preview unwrapped, truncated preview (byte_len != total_bytes) left intact, and a ~0/~1 escaped token.

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

In `@tests/e2e/scenarios/test_reborn_qa_trace_replay.py` around lines 45 - 85, The
resolver tests currently bypass the envelope normalization and JSON Pointer
decoding paths. Extend the tests around _resolve_trace_result_bindings with
cases using the real schema_version/status/trust and detail.preview envelope:
unwrap a complete preview, preserve a truncated preview when byte_len differs
from total_bytes, and resolve pointers containing ~0/~1 escapes, matching the
Rust trace_binding behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/ironclaw_llm/src/recording.rs`:
- Around line 2138-2183: Add an async integration-style test that exercises
RecordingLlm::complete_with_tools rather than calling build_prior_tool_lookup or
parameterize_value directly. Configure a tool result containing the nested file
ID, invoke completion with matching arguments, then inspect the persisted
TraceFile and assert it contains the exact $trace_result marker with the
expected tool_call_id and pointer.

---

Duplicate comments:
In `@tests/e2e/mock_llm.py`:
- Around line 478-492: Update _capture_trace_tool_call_id_aliases so a
pending_tool_calls batch is cleared or otherwise discarded before returning when
the actual assistant tool-call count differs from pending. Preserve the existing
successful alias-mapping behavior, but ensure drifted IDs cannot be reused by
later assistant batches or relabeled by _trace_tool_results_with_recorded_ids.

In `@tests/e2e/scenarios/test_reborn_qa_trace_replay.py`:
- Around line 45-85: The resolver tests currently bypass the envelope
normalization and JSON Pointer decoding paths. Extend the tests around
_resolve_trace_result_bindings with cases using the real
schema_version/status/trust and detail.preview envelope: unwrap a complete
preview, preserve a truncated preview when byte_len differs from total_bytes,
and resolve pointers containing ~0/~1 escapes, matching the Rust trace_binding
behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c5d80c29-4a04-41b0-814a-da765cade621

📥 Commits

Reviewing files that changed from the base of the PR and between 7716877 and c3162dc.

⛔ Files ignored due to path filters (2)
  • tests/fixtures/llm_traces/README.md is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/live/routine_btc_create_test_and_refire.json is excluded by !tests/fixtures/**
📒 Files selected for processing (9)
  • crates/ironclaw_llm/CLAUDE.md
  • crates/ironclaw_llm/src/lib.rs
  • crates/ironclaw_llm/src/recording.rs
  • crates/ironclaw_llm/src/trace_binding.rs
  • tests/e2e/mock_llm.py
  • tests/e2e/scenarios/test_reborn_qa_trace_full_path.py
  • tests/e2e/scenarios/test_reborn_qa_trace_replay.py
  • tests/support/trace_llm.rs
  • tests/trace_llm_tests.rs

Comment thread crates/ironclaw_llm/src/recording.rs
@serrrfirat

Copy link
Copy Markdown
Collaborator Author

@ironloopai review --agent reviewer

@github-actions github-actions Bot added the size: XL 500+ changed lines label Jul 24, 2026

@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
✅ Approved 0 0 0 1699dadab81d

Head: 1699dadab81dd432a6863adec954e14916c06214
Next: No reviewer action needed.

Run details

Status: Current
Needs human: no
Needs validation: no

Summary

No concrete correctness, security, or maintainability defects found in the exact trace-result binding migration.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
tests/e2e/mock_llm.py (1)

180-190: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

id is now load-bearing but still not required to be unique across the trace.

Aliases are keyed by recorded id in a single trace-scoped dict (Line 527) and inverted back in _trace_tool_results_with_recorded_ids (Lines 577-580). A fixture that reuses one id in two steps overwrites the alias and resolves $trace_result against whichever turn aliased last — a silent wrong-result bind, which is the exact failure mode this PR removes. Parse time is the cheap place to reject it.

🛡️ Reject duplicate ids at install time
     responses = []
     expected_user_inputs = {0: first_response["content"]}
     request_hints = []
     pending_user_input = True
+    seen_tool_call_ids: set[str] = set()
     for index, step in enumerate(steps[1:], start=1):
@@
                     raise ValueError(
                         f"trace.steps[{index}].tool_calls[{tool_index}] is invalid"
                     )
+                if tool_call["id"] in seen_tool_call_ids:
+                    raise ValueError(
+                        f"trace.steps[{index}].tool_calls[{tool_index}] "
+                        f"reuses tool call id {tool_call['id']!r}"
+                    )
+                seen_tool_call_ids.add(tool_call["id"])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/mock_llm.py` around lines 180 - 190, Update the tool-call
validation loop in the trace parsing path to track recorded tool-call ids across
the entire trace and raise ValueError when an id appears more than once. Keep
the existing type and field validation, and perform the uniqueness check before
installing aliases so duplicate ids cannot overwrite entries used by
_trace_tool_results_with_recorded_ids.
crates/ironclaw_llm/src/recording.rs (1)

814-863: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

No distinctiveness guard on exact-match binding for trivial string literals.

parameterize_value binds any argument string that uniquely matches a value inside a prior tool result, with no minimum-length/triviality check on s. A short or common literal (empty string, "true", a locale code, etc.) that happens to appear exactly once in an unrelated tool-result payload gets recorded as a $trace_result reference to that call, even though it isn't genuinely derived from it — producing a semantically wrong but silently-passing fixture.

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

In `@crates/ironclaw_llm/src/recording.rs` around lines 814 - 863, Add a
distinctiveness guard in parameterize_value before exact-match binding so
trivial literals such as empty strings, booleans, and similarly short/common
values are not converted to $trace_result references. Only run the exact-match
lookup when s passes the established non-triviality criteria; otherwise preserve
the existing legacy lookup and recursive behavior.
♻️ Duplicate comments (1)
crates/ironclaw_llm/src/recording.rs (1)

594-629: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

parse_tool_result_content re-introduces the duplicate parse chain a past review flagged.

index_json_into (unchanged, lines 640-642) still hand-rolls the identical serde_json::from_str(...).ok().or_else(coerce_python_repr_to_json → from_str) fallback that this new parse_tool_result_content formalizes. The previous review comment asking index_json_into to call the shared helper instead of duplicating it is marked addressed, but the duplication is present in this code.

♻️ Proposed fix
 fn index_json_into(
     lookup: &mut HashMap<String, HashMap<String, String>>,
     key: &str,
     content: &str,
 ) {
-    let parsed: Option<serde_json::Value> = serde_json::from_str(content).ok().or_else(|| {
-        coerce_python_repr_to_json(content).and_then(|s| serde_json::from_str(&s).ok())
-    });
+    let parsed = parse_tool_result_content(content);
     let Some(json) = parsed else { return };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_llm/src/recording.rs` around lines 594 - 629, Remove the
duplicated JSON parsing chain by updating index_json_into to call the shared
parse_tool_result_content helper for its payload parsing. Preserve the existing
indexing behavior and use parse_tool_result_content as the single implementation
of direct JSON plus Python-representation fallback parsing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@crates/ironclaw_llm/src/recording.rs`:
- Around line 814-863: Add a distinctiveness guard in parameterize_value before
exact-match binding so trivial literals such as empty strings, booleans, and
similarly short/common values are not converted to $trace_result references.
Only run the exact-match lookup when s passes the established non-triviality
criteria; otherwise preserve the existing legacy lookup and recursive behavior.

In `@tests/e2e/mock_llm.py`:
- Around line 180-190: Update the tool-call validation loop in the trace parsing
path to track recorded tool-call ids across the entire trace and raise
ValueError when an id appears more than once. Keep the existing type and field
validation, and perform the uniqueness check before installing aliases so
duplicate ids cannot overwrite entries used by
_trace_tool_results_with_recorded_ids.

---

Duplicate comments:
In `@crates/ironclaw_llm/src/recording.rs`:
- Around line 594-629: Remove the duplicated JSON parsing chain by updating
index_json_into to call the shared parse_tool_result_content helper for its
payload parsing. Preserve the existing indexing behavior and use
parse_tool_result_content as the single implementation of direct JSON plus
Python-representation fallback parsing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 945b2a23-9646-4022-8751-7eec46781d5e

📥 Commits

Reviewing files that changed from the base of the PR and between ec14b26 and 1699dad.

⛔ Files ignored due to path filters (2)
  • tests/fixtures/llm_traces/README.md is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/live/routine_btc_create_test_and_refire.json is excluded by !tests/fixtures/**
📒 Files selected for processing (9)
  • crates/ironclaw_llm/CLAUDE.md
  • crates/ironclaw_llm/src/lib.rs
  • crates/ironclaw_llm/src/recording.rs
  • crates/ironclaw_llm/src/trace_binding.rs
  • tests/e2e/mock_llm.py
  • tests/e2e/scenarios/test_reborn_qa_trace_full_path.py
  • tests/e2e/scenarios/test_reborn_qa_trace_replay.py
  • tests/support/trace_llm.rs
  • tests/trace_llm_tests.rs

@serrrfirat

Copy link
Copy Markdown
Collaborator Author

Addressed the current-head outside-diff review by rejecting duplicate tool-call IDs at trace install time (with regression coverage). I did not add an arbitrary short-string cutoff to recorder binding: the repository has no established threshold, and such a heuristic would silently reject valid short provider outputs. Exact unique matching remains deterministic; true provenance annotations can be a separate contract if needed.

@serrrfirat
serrrfirat force-pushed the codex/canonical-trace-replay-contract branch from 1699dad to 0c39889 Compare July 24, 2026 23:01
@serrrfirat

Copy link
Copy Markdown
Collaborator Author

@ironloopai review --agent reviewer

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6659 July 24, 2026 23:01 Destroyed

@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 0c3988976be7

Head: 0c3988976be75d3a7211f0ffbb4fe06dfe2d551d
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

Exact trace-result replay is not reliable across user turns, and the Rust path still permits ambiguous duplicate call IDs.

Findings

Blocking: 2 / Notes: 0

Blocking findings

1. ❌ [MEDIUM] Preserve prior-turn results for exact bindings

Location: tests/e2e/mock_llm.py:590
_find_tool_results only returns tool messages after the most recent user message. Therefore a valid marker in a later user turn cannot reference a resource created in an earlier turn: despite the original result and its persisted alias being present in messages, this loop supplies no candidate and replay returns 409. Collect historical tool results by call ID (or retain that evidence separately) before resolving, and add a multi-turn regression.

2. ❌ [MEDIUM] Reject ambiguous duplicate IDs in Rust replay

Location: crates/ironclaw_llm/src/trace_binding.rs:77
Selecting the newest matching result makes a duplicated tool-call ID resolve silently to the wrong resource. The Python trace loader now rejects duplicate fixture IDs, but Rust TraceLlm accepts such traces and reaches this resolver. Reject duplicate observed IDs here or validate unique non-empty IDs when loading Rust traces so $trace_result remains exact.

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.

Comment thread tests/e2e/mock_llm.py Outdated
for recorded_id, runtime_id in state["tool_call_id_aliases"].items()
}
results = []
for observed in _find_tool_results(messages):

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 scans only tool results after the latest user message. A later user turn cannot bind to a resource created in an earlier turn even though the exact call ID is still in the conversation. Preserve historical results by ID (and add a multi-turn binding regression).

let result = observed
.iter()
.rev()
.find(|result| result.tool_call_id == binding.tool_call_id)

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.

Duplicate IDs resolve to the most recent result instead of failing. Rust TraceLlm does not enforce the Python loader's unique-ID rule, so a malformed fixture can bind a later call to the wrong resource. Reject duplicate observed IDs or validate IDs when loading the trace.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@crates/ironclaw_llm/CLAUDE.md`:
- Around line 289-291: Update the documentation statement around
tool-result-derived arguments to acknowledge the legacy `{{call_id.field}}`
fallback used by `parameterize_value` in `recording.rs` when matching is
ambiguous or no call ID is available. Soften the absolute guarantee about the
exact `$trace_result` marker while preserving the documented preferred behavior
and RFC 6901 pointer details.

In `@tests/e2e/scenarios/test_reborn_qa_trace_replay.py`:
- Around line 91-118: Extend the Python replay tests around
_canonical_trace_result_payload and _resolve_trace_result_bindings to cover
complete previews: when byte_len equals total_bytes and next_offset is absent,
unwrap the preview and resolve its JSON Pointer successfully. Include a pointer
containing escaped ~0 and ~1 tokens, asserting they decode to the correct object
keys and preserve byte-compatible behavior with resolve_trace_result_bindings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8b635fa9-f4bb-48db-9b1c-4cc724352871

📥 Commits

Reviewing files that changed from the base of the PR and between 1699dad and 0c39889.

⛔ Files ignored due to path filters (2)
  • tests/fixtures/llm_traces/README.md is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/live/routine_btc_create_test_and_refire.json is excluded by !tests/fixtures/**
📒 Files selected for processing (9)
  • crates/ironclaw_llm/CLAUDE.md
  • crates/ironclaw_llm/src/lib.rs
  • crates/ironclaw_llm/src/recording.rs
  • crates/ironclaw_llm/src/trace_binding.rs
  • tests/e2e/mock_llm.py
  • tests/e2e/scenarios/test_reborn_qa_trace_full_path.py
  • tests/e2e/scenarios/test_reborn_qa_trace_replay.py
  • tests/support/trace_llm.rs
  • tests/trace_llm_tests.rs

Comment on lines +289 to +291

Arguments derived from an earlier tool result use an exact `$trace_result`
marker with the original `tool_call_id` and an RFC 6901 JSON Pointer.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Soften the absolute claim to mention the legacy fallback.

This states unqualified that tool-result-derived arguments "use an exact $trace_result marker," but parameterize_value in recording.rs still falls back to the legacy {{call_id.field}} template when a match is ambiguous or the source is a sanitized user message without a call ID. As written, this overstates the guarantee.

As per coding guidelines, **/*.{rs,md}: "Comments promising cross-layer guarantees must be enforced by code or tests, or softened to describe intent."

📝 Proposed doc fix
-Arguments derived from an earlier tool result use an exact `$trace_result`
-marker with the original `tool_call_id` and an RFC 6901 JSON Pointer.
+Arguments that uniquely match an earlier native tool result use an exact
+`$trace_result` marker with the original `tool_call_id` and an RFC 6901 JSON
+Pointer. Ambiguous matches and sanitized (call-ID-less) tool results still
+fall back to the legacy `{{call_id.field}}` template.
📝 Committable suggestion

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

Suggested change
Arguments derived from an earlier tool result use an exact `$trace_result`
marker with the original `tool_call_id` and an RFC 6901 JSON Pointer.
Arguments that uniquely match an earlier native tool result use an exact
`$trace_result` marker with the original `tool_call_id` and an RFC 6901 JSON
Pointer. Ambiguous matches and sanitized (call-ID-less) tool results still
fall back to the legacy `{{call_id.field}}` template.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_llm/CLAUDE.md` around lines 289 - 291, Update the
documentation statement around tool-result-derived arguments to acknowledge the
legacy `{{call_id.field}}` fallback used by `parameterize_value` in
`recording.rs` when matching is ambiguous or no call ID is available. Soften the
absolute guarantee about the exact `$trace_result` marker while preserving the
documented preferred behavior and RFC 6901 pointer details.

Source: Coding guidelines

Comment thread tests/e2e/scenarios/test_reborn_qa_trace_replay.py
@serrrfirat
serrrfirat force-pushed the codex/canonical-trace-replay-contract branch from 0c39889 to 86b9af0 Compare July 24, 2026 23:11
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6659 July 24, 2026 23:11 Destroyed
@serrrfirat

Copy link
Copy Markdown
Collaborator Author

@ironloopai review --agent reviewer

@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 86b9af0182be

Head: 86b9af0182bec7ba647df62e12f55a3b74f04212
Next: Human review or validation is required before merging.

Run details

Status: Current
Needs human: no
Needs validation: yes

Summary

Static review found no concrete actionable defects. The 11-file normal PR consistently implements exact call-ID/JSON-Pointer bindings across recording, Rust replay, Python replay, fixture migration, and focused tests.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
tests/e2e/scenarios/test_reborn_qa_trace_replay.py (1)

152-179: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Still only the rejection half is pinned; the unwrap and escape branches remain untested.

This test covers the paged/truncated envelope. Nothing in this file exercises the complementary paths in mock_llm.py: a complete preview (byte_len == total_bytes, next_offset absent) being unwrapped at Lines 491-492, and _decode_trace_pointer_token (Lines 457-469) — no pointer here contains ~0 or ~1. A change that stops unwrapping complete previews, or that mis-decodes an escaped token, passes this entire suite while the Rust lane (which the PR states covers /files/0/a~1b~0c) keeps passing. That is the exact cross-language drift this binding contract is meant to close.

Raised on a prior commit and still open.

🧪 Mirror the two Rust cases
+def test_exact_trace_result_binding_unwraps_complete_evidence_preview():
+    marker = {
+        "$trace_result": {
+            "tool_call_id": "call_create",
+            "pointer": "/document/documentId",
+        }
+    }
+    preview = json.dumps({"document": {"documentId": "whole-document"}})
+    observed = [
+        {
+            "tool_call_id": "call_create",
+            "content": {
+                "schema_version": 1,
+                "status": "success",
+                "trust": "provider",
+                "detail": {
+                    "byte_len": len(preview),
+                    "total_bytes": len(preview),
+                    "preview": preview,
+                },
+            },
+        }
+    ]
+
+    assert _resolve_trace_result_bindings(marker, observed) == "whole-document"
+
+
+def test_exact_trace_result_binding_decodes_escaped_pointer_tokens():
+    marker = {
+        "$trace_result": {
+            "tool_call_id": "call_list",
+            "pointer": "/files/0/a~1b~0c",
+        }
+    }
+    observed = [
+        {
+            "tool_call_id": "call_list",
+            "content": {"files": [{"a/b~c": "escaped-hit"}]},
+        }
+    ]
+
+    assert _resolve_trace_result_bindings(marker, observed) == "escaped-hit"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/scenarios/test_reborn_qa_trace_replay.py` around lines 152 - 179,
Add tests in this file covering the complementary branches of
`_resolve_trace_result_bindings`: verify a complete preview with `byte_len ==
total_bytes` and no `next_offset` is unwrapped, and verify a JSON Pointer
containing escaped `~1` and `~0` tokens is decoded correctly. Mirror the Rust
cases and assert the resolved values, while preserving the existing rejection
test for paged evidence.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/e2e/scenarios/test_reborn_qa_trace_full_path.py`:
- Line 716: Update the renamed Drive upload handling around
created_document_call_id to use the actual google-drive__upload_file response
shape, without rewriting create_document IDs as Drive file IDs; keep Docs
document binders pointed to the Docs output. Add readback assertions for the
Drive upload and Sheets create_spreadsheet response fields at /spreadsheet_id
and /sheets/0/sheet_id so invalid shapes fail before binding defaults.
- Around line 741-755: Update the argument-rewriting logic around
created_spreadsheet_call_id so spreadsheet_id and sheet_id always use a
consistent source: bind both to the created spreadsheet when available,
otherwise replace both with the corresponding seeded spreadsheet and seeded
sheet values. Do not leave the harvested sheet_id when falling back to
seeded_spreadsheet.

In `@tests/e2e/scenarios/test_reborn_qa_trace_replay.py`:
- Around line 182-221: Add a positive test alongside
test_trace_aliasing_rejects_indistinguishable_parallel_calls that uses two
pending calls with the same normalized tool name but different arguments and
runtime messages in reversed order. Call _capture_trace_tool_call_id_aliases,
assert it succeeds, and verify each recorded call id maps to the runtime id with
matching arguments rather than positional order.

---

Duplicate comments:
In `@tests/e2e/scenarios/test_reborn_qa_trace_replay.py`:
- Around line 152-179: Add tests in this file covering the complementary
branches of `_resolve_trace_result_bindings`: verify a complete preview with
`byte_len == total_bytes` and no `next_offset` is unwrapped, and verify a JSON
Pointer containing escaped `~1` and `~0` tokens is decoded correctly. Mirror the
Rust cases and assert the resolved values, while preserving the existing
rejection test for paged evidence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4bafbd91-df36-4950-b820-caf98643a563

📥 Commits

Reviewing files that changed from the base of the PR and between 0c39889 and 86b9af0.

⛔ Files ignored due to path filters (2)
  • tests/fixtures/llm_traces/README.md is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/live/routine_btc_create_test_and_refire.json is excluded by !tests/fixtures/**
📒 Files selected for processing (9)
  • crates/ironclaw_llm/CLAUDE.md
  • crates/ironclaw_llm/src/lib.rs
  • crates/ironclaw_llm/src/recording.rs
  • crates/ironclaw_llm/src/trace_binding.rs
  • tests/e2e/mock_llm.py
  • tests/e2e/scenarios/test_reborn_qa_trace_full_path.py
  • tests/e2e/scenarios/test_reborn_qa_trace_replay.py
  • tests/support/trace_llm.rs
  • tests/trace_llm_tests.rs

Comment thread tests/e2e/scenarios/test_reborn_qa_trace_full_path.py
Comment thread tests/e2e/scenarios/test_reborn_qa_trace_full_path.py
Comment thread tests/e2e/scenarios/test_reborn_qa_trace_replay.py
@serrrfirat
serrrfirat force-pushed the codex/canonical-trace-replay-contract branch from 86b9af0 to 7527858 Compare July 24, 2026 23:26
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6659 July 24, 2026 23:26 Destroyed
@serrrfirat

Copy link
Copy Markdown
Collaborator Author

@ironloopai review --agent reviewer

@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
✅ Approved 0 0 0 75278582ad07

Head: 75278582ad07eee5a53fda0bdc832dbf2fa60952
Next: No reviewer action needed.

Run details

Status: Current
Needs human: no
Needs validation: no

Summary

Approved. Reviewed the focused 11-file merge-base diff; exact ID/pointer binding, alias handling, recorder behavior, and fixture migration are coherent and covered by targeted tests.

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/ironclaw_llm/src/recording.rs (1)

574-908: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Move the record-side binding logic into trace_binding.rs, not deeper into recording.rs.

PriorToolLookup, build_prior_tool_lookup, parse_tool_result_content, parameterize_value, find_string_pointers, and escape_json_pointer_token (~300 new lines) land inline in recording.rs, which is already deep into the 1,500-3,000 line band. This PR proves a suitable alternative location exists — it created trace_binding.rs for the mirror-image resolve-side logic in the same commit — so the "no suitable alternative location" exemption for growing an oversized file doesn't apply, and there's no // arch-exempt: justification for the >200-line addition.

As per coding guidelines, crates/**/*.rs: "Existing files between 1,500 and 3,000 lines should become shorter when touched unless feature expansion has no suitable alternative location. Files over 3,000 lines require a decomposition tracking issue, and additions over 200 lines require inline justification."

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

In `@crates/ironclaw_llm/src/recording.rs` around lines 574 - 908, Move the
record-side binding implementation out of recording.rs into the existing
trace_binding.rs module, including PriorToolLookup, build_prior_tool_lookup,
parse_tool_result_content, parameterize_value, find_string_pointers, and
escape_json_pointer_token plus their required helpers. Update recording.rs
imports and call sites to use the relocated symbols, preserving behavior while
reducing the oversized file; do not add an architecture exemption.

Source: Coding guidelines

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

Outside diff comments:
In `@crates/ironclaw_llm/src/recording.rs`:
- Around line 574-908: Move the record-side binding implementation out of
recording.rs into the existing trace_binding.rs module, including
PriorToolLookup, build_prior_tool_lookup, parse_tool_result_content,
parameterize_value, find_string_pointers, and escape_json_pointer_token plus
their required helpers. Update recording.rs imports and call sites to use the
relocated symbols, preserving behavior while reducing the oversized file; do not
add an architecture exemption.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cc6bb69d-84f0-471a-84c4-35d47b0100db

📥 Commits

Reviewing files that changed from the base of the PR and between 86b9af0 and 7527858.

⛔ Files ignored due to path filters (2)
  • tests/fixtures/llm_traces/README.md is excluded by !tests/fixtures/**
  • tests/fixtures/llm_traces/live/routine_btc_create_test_and_refire.json is excluded by !tests/fixtures/**
📒 Files selected for processing (9)
  • crates/ironclaw_llm/CLAUDE.md
  • crates/ironclaw_llm/src/lib.rs
  • crates/ironclaw_llm/src/recording.rs
  • crates/ironclaw_llm/src/trace_binding.rs
  • tests/e2e/mock_llm.py
  • tests/e2e/scenarios/test_reborn_qa_trace_full_path.py
  • tests/e2e/scenarios/test_reborn_qa_trace_replay.py
  • tests/support/trace_llm.rs
  • tests/trace_llm_tests.rs

@serrrfirat

Copy link
Copy Markdown
Collaborator Author

Follow-up on CodeRabbit's outside-diff module-move suggestion: keeping the record-side helpers in recording.rs. That file already carries the repository's explicit arch-exempt: large_file annotation with tracking plan #6175, and this block includes recorder-only legacy/sanitized-message parsing in addition to exact marker creation. trace_binding.rs remains the small provider-neutral marker/resolution contract. Moving roughly 300 lines would redistribute code and couple the resolver module to recording internals without reducing behavior or ceremony.

@github-actions

Copy link
Copy Markdown
Contributor

Coverage ratchet

Ratchet mode: ENFORCING

RATCHET PASS: global
  observed: 85.97% (314053 / 365320 lines)
  floor:    86.27% (tolerance 0.5pp -> effective floor 85.77%)
  denominator: 365320 lines now vs 354049 at floor capture (+11271 lines, +3.18%) — not a material change

⚠️ 2 Reborn crate(s) have 0 int-tier coverage (target: 0) — ironclaw_prompt_envelope, ironclaw_scripts

Reborn integration-tier coverage

Line coverage (Reborn crates): 85.97% — 314053 / 365320 lines

Per-crate breakdown (61 crates, lowest-covered first)
Crate Line % Covered / Total
ironclaw_prompt_envelope 0% 0 / 88
ironclaw_scripts 0% 0 / 345
ironclaw_host_ingress 42.5% 17 / 40
ironclaw_event_projections 43.71% 684 / 1565
ironclaw_observability 61.54% 16 / 26
ironclaw_telegram_v2_adapter 62.35% 631 / 1012
ironclaw_authorization 62.98% 609 / 967
ironclaw_dispatcher 64.75% 79 / 122
ironclaw_memory 70.15% 919 / 1310
ironclaw_trust 73.21% 664 / 907
ironclaw_filesystem 73.7% 4587 / 6224
ironclaw_wasm_limiter 74.6% 47 / 63
ironclaw_extractors 74.72% 538 / 720
ironclaw_capabilities 75.33% 2592 / 3441
ironclaw_mcp 76.2% 775 / 1017
ironclaw_projects 76.48% 400 / 523
ironclaw_reborn_cli 78.17% 10668 / 13647
ironclaw_telegram_extension 78.59% 962 / 1224
ironclaw_llm 78.97% 21305 / 26977
ironclaw_wasm 79.72% 735 / 922
ironclaw_process_sandbox 80.46% 671 / 834
ironclaw_memory_native 80.97% 3114 / 3846
ironclaw_auth 82.08% 6679 / 8137
ironclaw_first_party_extensions 82.38% 6682 / 8111
ironclaw_events 82.47% 1604 / 1945
ironclaw_operator 83.2% 5625 / 6761
ironclaw_processes 83.3% 933 / 1120
ironclaw_host_api 83.5% 8942 / 10709
ironclaw_secrets 83.79% 2548 / 3041
ironclaw_reborn_identity 83.8% 450 / 537
ironclaw_reborn_config 85.24% 2102 / 2466
ironclaw_common 85.53% 2252 / 2633
ironclaw_run_state 85.77% 458 / 534
ironclaw_triggers 85.92% 2783 / 3239
ironclaw_network 85.97% 913 / 1062
ironclaw_webui 86% 10791 / 12547
ironclaw_reborn_event_store 86.51% 1251 / 1446
ironclaw_hooks 86.58% 9930 / 11469
ironclaw_product 86.8% 21683 / 24981
ironclaw_extensions 87.2% 3678 / 4218
ironclaw_threads 87.2% 4851 / 5563
ironclaw_skills 87.77% 4480 / 5104
ironclaw_turns 88.06% 14312 / 16253
ironclaw_reborn_traces 88.13% 11986 / 13600
ironclaw_reborn_composition 88.16% 43849 / 49740
ironclaw_slack_extension 88.47% 1934 / 2186
ironclaw_host_runtime 88.57% 18997 / 21448
ironclaw_extension_host 89.12% 3745 / 4202
ironclaw_reborn_openai_compat 89.32% 3780 / 4232
ironclaw_resources 90.84% 4474 / 4925
ironclaw_runner 90.87% 17192 / 18920
ironclaw_conversations 91.1% 3202 / 3515
ironclaw_approvals 91.12% 1682 / 1846
ironclaw_event_streams 91.24% 1063 / 1165
ironclaw_loop_host 91.9% 16503 / 17958
ironclaw_attachments 93.06% 630 / 677
ironclaw_outbound 94.43% 3987 / 4222
ironclaw_agent_loop 94.88% 9837 / 10368
ironclaw_safety 95.15% 3749 / 3940
ironclaw_first_party_extension_ports 95.62% 3672 / 3840
ironclaw_runtime_policy 96.55% 811 / 840

This table itself is informational and never gates the PR on its own — not the percentage, not the per-crate holes, not the 0-coverage callout. A separate coverage ratchet (dry-run until enforce=true; see tests/integration/coverage-floor.toml) can fail the build on specific configured floors.

Exemptions (3 entry/entries excluded from the accounting above)
Module / Crate Reason Issue
crate: ironclaw_embeddings v1-only: consumed only by root ironclaw (src/app.rs, src/tools/builtin/memory.rs, src/workspace/mod.rs, src/config/{mod,embeddings}.rs); no crates/* dependents. Covered by "Tests (Legacy)". #5657
crate: ironclaw_gateway v1-only: consumed only by root ironclaw (src/channels/web/platform/static_files.rs, src/channels/web/handlers/frontend.rs); no crates/* dependents. Covered by "Tests (Legacy)". #5657
crate: ironclaw_tui v1-only: consumed only by root ironclaw (src/main.rs, src/channels/tui.rs); no crates/* dependents. Crate's own doc comment confirms it bridges INTO v1, not Reborn. Covered by "Tests (Legacy)". #5657

@serrrfirat
serrrfirat merged commit f1856f9 into main Jul 27, 2026
62 checks passed
@serrrfirat
serrrfirat deleted the codex/canonical-trace-replay-contract branch July 27, 2026 08:17
l3ocifer pushed a commit to l3ocifer/frick-ironclaw that referenced this pull request Sep 3, 2026

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-6659 — 75278582 Deployed Jul 24, 2026 by railway-app[bot]
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: docs Documentation size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant