fix(proxy): recover goal restarts from unavailable owners - #1679
fix(proxy): recover goal restarts from unavailable owners#1679leventov wants to merge 3 commits into
Conversation
Permit an explicit, account-neutral Codex goal restart to retire an unchanged legacy owner only while its persisted account status is unavailable. Preserve fail-closed routing for all other continuity evidence and guard retirement with one compare-and-set tombstone.
2412c6d to
a8e8199
Compare
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cd554303b9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| policy.codex_session_source == "session_header" | ||
| and _request_allows_unavailable_legacy_owner_abandonment(payload) | ||
| ): | ||
| policy = replace(policy, abandon_unavailable_legacy_owner=True) |
There was a problem hiding this comment.
Preserve the provenance of legacy rows before tombstoning
When a client supplies a session_id equal to a raw row originally written from an explicit x-codex-turn-state, this condition treats the request as process-session provenance and later permits that row to be tombstoned if its owner is unavailable. The legacy namespace deliberately mixes both sources, and these client-controlled headers can collide within the same effective account-policy scope, so current-request provenance does not prove the persisted row's provenance; one session can consequently retire another session's hard turn-state ownership and break its later continuation. Store or migrate row provenance, or keep ambiguous raw rows fail-closed.
AGENTS.md reference: AGENTS.md:L105-L110
Useful? React with 👍 / 👎.
| if ( | ||
| request_state is not None | ||
| and upstream is not None | ||
| and account is not None | ||
| and request_state.affinity_policy.abandon_unavailable_legacy_owner |
There was a problem hiding this comment.
Force goal restarts through selection on the HTTP bridge
This forced reselection covers only direct downstream WebSockets. With the default http_responses_session_bridge_enabled=True, a warm HTTP bridge session can be returned by _get_or_create_http_bridge_session before account selection runs; _http_bridge_session_account_active deliberately excludes RATE_LIMITED and QUOTA_EXCEEDED from its routing-unavailable snapshot, so when another request or replica persists either status after the socket opened, the cached session can still appear active. A marked restart is then submitted on the old account without invoking the new tombstone path or selecting a replacement—the added routed regression avoids this by disabling the bridge. Apply the same pre-dispatch retirement and reselection behavior to HTTP bridge reuse.
AGENTS.md reference: AGENTS.md:L105-L110
Useful? React with 👍 / 👎.
| legacy_owner_in_effective_policy_scope = isinstance(sticky_existing_account_id, str) and any( | ||
| account.id == sticky_existing_account_id | ||
| for account in selection_inputs.effective_continuity_owner_candidates | ||
| ) |
There was a problem hiding this comment.
Check mutation authority before model eligibility
When unavailable owner A is authorized by the caller but does not support the restart's requested model while replacement B does, _load_selection_inputs builds effective_continuity_owner_candidates by model-filtering scoped_accounts, so this test incorrectly treats A as outside the authenticated policy scope. The raw row therefore remains hard, A is absent from the selectable model pool, and the marked restart returns hard_affinity_saturated instead of moving to B. Derive retirement authority from the authenticated account-assignment/security scope before model and service-tier eligibility are applied; keep those latter filters only for replacement selection.
AGENTS.md reference: AGENTS.md:L105-L110
Useful? React with 👍 / 👎.
Summary
A Codex conversation restart can resend a self-contained thread under the same
process-session identifier after its legacy owner exhausts quota. Raw legacy
codex_sessionrows are intentionally hard, so ordinary requests must failclosed; before this change, that same row also trapped an explicit,
self-contained goal restart on an unavailable account.
This PR adds one proof-gated direct-routing exception. A request may retire an
unavailable raw legacy owner only when it carries Codex's recognized
goal-continuation marker and its canonical upstream Responses payload is
account-neutral and self-contained. Retirement is compare-and-set,
policy-scoped, and request-owned. Ordinary, incremental, file-pinned,
conversation-bound, and unresolved-tool requests remain fail-closed.
HTTP bridge reuse/replacement is deliberately split into the dependent #1680.
No public wire format, setting, schema, or default timeout changes.
Linked issue: none exists for this incident-derived defect; routed regression
coverage exercises the public Codex Responses and direct WebSocket paths.
Behavior and safety
request to the upstream Responses body.
owner is durably
PAUSED,RATE_LIMITED, orQUOTA_EXCEEDED.account-policy scope.
change or recovery wins.
even when selection loaded a stale ACTIVE account snapshot.
only proxy-generated turn state when the restart changes accounts.
OpenSpec
openspec/changes/archive/2026-08-10-recover-restarted-conversation-affinity/.openspec/specs/sticky-session-operations/.stale-selection exclusion, and direct WebSocket state provenance.
Origin and concurrent work
Landed lineage:
b1d27bc6) introduced bounded stale hard-owner cleanup while keepinghot-path requests hard. This PR preserves that default and adds only a
proof-gated explicit-restart path.
201281b5) established the canonical account-neutral fresh-resendclassifier. This PR reuses that boundary rather than defining another movable
payload heuristic.
68397054) enforced ownership ofprevious_response_id; requests withthat anchor remain immovable here.
f2f8f916) and fix(proxy): recover dead durable bridge anchors fast #1625 (85f1ee4b) expanded bridge recovery anddead-owner handling. Their bridge lifecycle concerns are handled separately by
fix(http-bridge): preserve goal-restart recovery across reconnects #1680, not hidden in this object.
Concurrent work reviewed for overlap:
capability and must merge after this PR.
and direct-selection boundary; its overlap is documented in fix(http-bridge): preserve goal-restart recovery across reconnects #1680.
PR.
sticky-row retirement.
neither may reinterpret an ordinary hard session request as a restart.
Review findings addressed
Concrete review findings incorporated in this object:
the canonical upstream request body;
change, while client-supplied state remains hard.
Validation
Fresh current-head cloud CI is in progress after the object-level history split.
Screenshots / output
No dashboard-visible change.
Before:
After:
Simplicity
Checklist
regressions are included.
CHANGELOG.mdwas not edited.