Skip to content

fix(proxy): recover tool-complete goal followups from unavailable owners - #2121

Open
Irvinwop wants to merge 1 commit into
Soju06:mainfrom
Irvinwop:fix/recover-exhausted-owner
Open

Irvinwop wants to merge 1 commit into
Soju06:mainfrom
Irvinwop:fix/recover-exhausted-owner

Conversation

@Irvinwop

@Irvinwop Irvinwop commented Sep 6, 2026 •

Copy link
Copy Markdown

Summary

Allow a complete, durable-manifest-verified tool batch to be followed by fresh user input when proving that a full resend can recover from an unavailable response owner.

Refs #1707 — partial fix for replayable tool-complete goal restarts. This does not make opaque encrypted compaction checkpoints portable between accounts.

Type of change

  • fix: — bug fix

OpenSpec

  • This PR includes / updates an OpenSpec change.
  • This PR touches a codex-faithful path and preserves request/response payloads.

Archived change: openspec/changes/archive/2026-09-06-recover-tool-complete-goal-followup/

Changes

  • Prove the exact prior-response call/result batch independently of a trailing self-contained user-input suffix. The helper previously rejected that suffix even when the complete batch matched its durable pending-call manifest.
  • Keep the existing complete-body projection, durable-prefix fingerprint, account-neutrality, account scope, file ownership, and pre-dispatch checks. No input is dropped by this proof helper.
  • Preserve rejection of missing parallel results, duplicate or mismatched calls, interleaved user input, and later tool/developer/assistant items. The existing three-item developer-interleave exception is not broadened.
  • Exercise both HTTP endpoints with the real database and account selector: paused or quota-exhausted owner, fresh replay to a replacement, then anchored continuation on that replacement. Account-owned files remain pinned and cannot use this recovery.

No settings, migrations, defaults, or dashboard changes.

Test plan

Base: current upstream main, 5ad638b6a4c9c094bcc8866b1d7487173fe3b54e.

With the production helper patch removed, the new goal-recovery regression failed on both HTTP endpoints with 502. With the patch, it recovers without the unavailable owner's anchor and retains the complete input.

uv run pytest tests/unit/test_replay_safety.py \
  tests/unit/test_durable_bridge_sessions.py \
  tests/unit/test_durable_bridge_transcript_codec.py \
  tests/integration/test_http_responses_bridge.py \
  tests/integration/test_proxy_websocket_responses.py \
  -q --tb=short --show-capture=no --timeout=120
585 passed

# Final focused run, including added file-owner rejection cases:
uv run pytest tests/unit/test_replay_safety.py \
  tests/integration/test_http_responses_bridge.py \
  -q -k goal_followup --tb=short --show-capture=no --timeout=90
22 passed, 360 deselected

make lint
uv run ty check
openspec validate responses-api-compat --type spec --strict
openspec validate --specs
git diff --check
All passed

An earlier broad run with a 60-second per-test timeout timed out in the existing idle-reader replacement test; the 120-second run above passed. These are local test results, not a claim about hosted CI or production deployment.

Example behavior

The replay input contains a verified stored prefix, the prior response's complete tool call and result, and a new goal instruction. There is no client-supplied previous_response_id.

  • Before: context proof fails; bridge restores the old anchor and returns previous_response_owner_unavailable when its owner cannot serve.
  • After: existing account-neutral recovery removes that anchor, forwards the full projected history to an eligible replacement, and completes. The next anchored request stays on that replacement.
  • If an account-owned file or opaque encrypted checkpoint is involved, this patch does not authorize cross-account recovery.

Checklist

  • Conventional Commits title and related issue linked.
  • Regression, integration, rejection, and continuation tests added.
  • Relevant lint/type/test targets run locally.
  • OpenSpec delta verified, synchronized, archived; specs validated.
  • Simplicity gates reviewed; no additional setup or configuration.
  • CHANGELOG not edited.

Summary by CodeRabbit

  • New Features

    • Added support for continuing a conversation with a fresh user message after a completed tool-call batch.
    • Full tool-call and result history is preserved when resuming interrupted goals.
  • Bug Fixes

    • Improved recovery when the previous account is unavailable, allowing eligible conversations to continue on a replacement account.
    • Added safeguards to reject incomplete, reordered, duplicated, or interleaved tool-call sequences.

Copilot AI lite review requested due to automatic review settings September 6, 2026 19:13
@coderabbitai

coderabbitai Bot commented Sep 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: ee763267-9c74-483f-abdc-224cde4a8dff

📥 Commits

Reviewing files that changed from the base of the PR and between 5ad638b and f5e044d.

📒 Files selected for processing (7)
  • app/modules/proxy/replay_safety.py
  • openspec/changes/archive/2026-09-06-recover-tool-complete-goal-followup/proposal.md
  • openspec/changes/archive/2026-09-06-recover-tool-complete-goal-followup/specs/responses-api-compat/spec.md
  • openspec/changes/archive/2026-09-06-recover-tool-complete-goal-followup/tasks.md
  • openspec/specs/responses-api-compat/spec.md
  • tests/integration/test_http_responses_bridge.py
  • tests/unit/test_replay_safety.py

Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

Replay safety now accepts a self-contained goal follow-up after a fully settled tool batch. The specification documents the replay proof, and unit and integration tests cover valid, invalid, account-neutral, and file-pinned recovery paths.

Changes

Tool-manifest replay follow-up

Layer / File(s) Summary
Replay contract and scenarios
openspec/changes/archive/2026-09-06-recover-tool-complete-goal-followup/..., openspec/specs/responses-api-compat/spec.md
The replay contract permits trailing self-contained user input after exact tool-call settlement and preserves durable-prefix, account, ownership, and scope checks.
Suffix validation
app/modules/proxy/replay_safety.py
The validator separates the fresh follow-up from the tool suffix, rejects later items, and validates the preceding suffix against the pending manifest.
Replay regression coverage
tests/unit/test_replay_safety.py, tests/integration/test_http_responses_bridge.py
Tests cover valid and invalid manifests, unavailable owners, replacement replay, anchored continuation, and file-pinned rejection.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to f5e04

The recovery flow accepts only fully settled tool batches with a trailing fresh follow-up, while covered account-owned file cases remain blocked. No actionable merge risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ResponsesBridge
  participant ReplaySafety
  participant OwnerDatabase
  participant ReplacementUpstream
  Client->>ResponsesBridge: resend settled tool batch with goal follow-up
  ResponsesBridge->>ReplaySafety: validate replay suffix
  ReplaySafety-->>ResponsesBridge: accept exact manifest and trailing input
  ResponsesBridge->>OwnerDatabase: check original owner availability
  ResponsesBridge->>ReplacementUpstream: send unanchored full-history replay
  ReplacementUpstream-->>ResponsesBridge: return replacement response
  ResponsesBridge-->>Client: return completed goal follow-up
Loading

Suggested reviewers: komzpa, mhughdo, choi138

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 2 files. (5 skipped: 4… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: recovering tool-complete goal follow-ups when the previous owner is unavailable.
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 2 files. (5 skipped: 4 unsupported, 1 too large.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copilot AI 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.

🟡 Changes recommended

The new trailing-followup allowance skips the stricter account-neutral/known-fields validation for those follow-up items, which can weaken replay safety guarantees (comment ID: 001).

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR extends the durable full-resend safety proof used by the proxy HTTP bridge so that, after a fully settled pending-tool-call manifest, a trailing self-contained user “goal follow-up” message can be accepted—enabling safe re-anchoring away from an unavailable prior owner while preserving the full projected input.

Changes:

  • Allow responses_input_suffix_matches_pending_tool_calls(...) to tolerate a trailing self-contained user follow-up after an exact, complete tool-call/result batch.
  • Add unit + integration coverage for the goal-followup resend case across both /backend-api/codex/responses and /v1/responses, including rejection when account-owned files are involved.
  • Update the Responses API compatibility OpenSpec with a normative requirement and archive the corresponding verified change artifacts.
File summaries
File Description
app/modules/proxy/replay_safety.py Updates suffix-proof logic to permit trailing fresh user follow-up after exact tool-manifest settlement.
tests/unit/test_replay_safety.py Adds regression tests for allowing goal follow-up and for rejecting malformed/relaxed tool-manifest fences.
tests/integration/test_http_responses_bridge.py Adds end-to-end bridge tests validating recovery from unavailable owners and rejection for file-pinned replays.
openspec/specs/responses-api-compat/spec.md Adds the “Exact tool-manifest replay preserves fresh user follow-up” requirement and scenarios.
openspec/changes/archive/2026-09-06-recover-tool-complete-goal-followup/tasks.md Archives the change task checklist (verification + publication).
openspec/changes/archive/2026-09-06-recover-tool-complete-goal-followup/specs/responses-api-compat/spec.md Archives the delta-spec excerpt for the added requirement.
openspec/changes/archive/2026-09-06-recover-tool-complete-goal-followup/proposal.md Archives the change proposal and scope/constraints.
Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +425 to +434
# A goal restart or steer can append user input after a fully settled
# prior-response tool batch. Prove that batch independently: new input
# must not stand in for a missing parallel call or interrupt its results.
first_followup = next(
(index for index, item in enumerate(suffix) if isinstance(item, dict) and _is_fresh_followup_input(item)),
len(suffix),
)
if not all(isinstance(item, dict) and _is_fresh_followup_input(item) for item in suffix[first_followup:]):
return False
suffix = suffix[:first_followup]
@Soju06

Soju06 commented Sep 8, 2026

Copy link
Copy Markdown
Owner

Verified on main a0d94a46a (round baseline): with your tests applied against main's app/modules/proxy/replay_safety.py, test_full_resend_tool_manifest_accepts_goal_followup_after_all_results fails and all four account-neutral test_http_bridge_goal_followup_after_complete_tool_batch_leaves_unavailable_owner cases return 502; with the patch, 22/22 pass, and the wider replay/durable-full-resend suites (344 tests), the proxy architecture/timing-seam/cancellation guards and openspec validate --specs are green on the merged tree. The change is scoped correctly: the follow-up slice at replay_safety.py:425-434 runs after the three-item developer exception and before the exact-manifest equality, so a trailing user message cannot stand in for a missing parallel call.

Two things stand between this and merge:

  1. Rebase onto current main (e987c56c8). The only conflict is openspec/specs/responses-api-compat/spec.md: since your branch point, refactor(egress): move compact Responses SSE framing into Rust #2168 (8c6467d97, Rust SSE framing), refactor(responses): collect compact SSE results in Rust #2171, fix(proxy): filter vendor events from public Responses streams #2114 and refactor(responses): interpret HTTP stream events in Rust #2184 all appended Requirements at the end of that file, and your "Exact tool-manifest replay preserves fresh user follow-up" requirement lands at the same spot. Keep all blocks (union); there is no code conflict (replay_safety.py and streaming.py are untouched on main since then).
  2. Add yourself to .all-contributorsrc (the Contributors attribution job is the only red on f5e044d8c and it is what fails the "CI Required" aggregate).

On the Copilot thread at replay_safety.py:434: I traced it and do not think it is a hole. The proof only sets safe_fresh_context; the actual cross-account dispatch is still gated by _http_bridge_payload_is_account_neutral_fresh_replay on the projected payload (app/modules/proxy/_service/http_bridge/streaming.py:1611 and :2078 on current main), which runs responses_input_items_are_self_contained_fresh_replay over the whole input, so a trailing user message with foreign internal_chat_message_metadata_passthrough, unknown fields or a non-completed status is rejected before replay (I probed each case: proof=True, payload-neutral=False). This is the same contract responses_input_suffix_retains_prior_output has always used for _is_fresh_followup_input. A one-line reply on the thread noting that gate would let it be resolved.

Heads-up: #2086 rewrites the body of the same function (prefix-settled outputs); whichever lands second will need a small textual rebase.

@Komzpa Komzpa added the needs rebase Needs rebase or conflict repair against current main label Sep 8, 2026
maisi added a commit to maisi/codex-lb that referenced this pull request Sep 9, 2026
…context (#30)

* fix(proxy): recover tool-complete continuations from unavailable owners

Preserve canonical replay ownership checks and make unreplayable continuity errors actionable.

Regression scenarios adapted from Soju06#2121 and extended for temporary rate limits, explicit anchors, unknown fields and repeated rejection.

* docs(proxy): verify and archive continuity-owner recovery

* docs(proxy): record recovery verification on beta6
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has had no activity for 7 days.

It will be closed in 23 more days unless there is new activity.

If this is still relevant, please:

  • Rebase or push an update if the branch drifted
  • Address pending review feedback if there is any
  • Leave a short comment confirming it is still being worked on

Thanks for the contribution 🙏

@github-actions github-actions Bot added the stale No response from reporter; scheduled for close label Sep 16, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs rebase Needs rebase or conflict repair against current main stale No response from reporter; scheduled for close

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants