-
Notifications
You must be signed in to change notification settings - Fork 411
fix(proxy): abandon unavailable owner on thread-scoped goal restart #1764
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Soju06
merged 5 commits into
Soju06:main
from
mastertyko:fix/goal-restart-thread-abandonment
Aug 16, 2026
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
028476b
fix(proxy): abandon unavailable owner on thread-scoped goal restart
mastertyko 076f0c7
test(proxy): avoid optional sticky-map subscript in thread restart test
mastertyko 13f5201
fix(proxy): keep thread-only raw owners and cover route restart
mastertyko d708225
test(proxy): accept legacy continuity source in control selection mock
mastertyko f0e7a83
fix(proxy): use legacy continuity source on HTTP-bridge rebind
mastertyko File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 2 additions & 0 deletions
2
openspec/changes/goal-restart-thread-header-abandonment/.openspec.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| schema: spec-driven | ||
| created: 2026-08-15 |
24 changes: 24 additions & 0 deletions
24
openspec/changes/goal-restart-thread-header-abandonment/context.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| ## Purpose | ||
|
|
||
| Close the `#1703` × `#1680` composition hole: current Codex always | ||
| sends `thread-id`, so the merged goal-restart recovery never fires. | ||
|
|
||
| ## Decision | ||
|
|
||
| Abandonment stays a `session_header` *interpretation* of the raw | ||
| process-session key. Request locality may be `thread_header`. Explicit | ||
| `turn_state` is unchanged. | ||
|
|
||
| ## Failure modes | ||
|
|
||
| - Incremental or file-pinned restarts must still fail closed on the | ||
| required owner. | ||
| - After retirement, a later thread-id turn must not revive the raw | ||
| row as hard ownership. | ||
|
|
||
| ## Example | ||
|
|
||
| Process session `sid` maps to quota-exceeded account A. Codex resends | ||
| an account-neutral goal body with `session-id: sid` and | ||
| `thread-id: t1`. Selection retires `sid` for `session_header`, routes | ||
| to B, and later `t1` turns stay on B. |
56 changes: 56 additions & 0 deletions
56
openspec/changes/goal-restart-thread-header-abandonment/design.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| ## Context | ||
|
|
||
| `#1679` / `#1680` added a proof-gated exception that retires an | ||
| unavailable raw `codex_session` owner for `session_header` | ||
| interpretation. `#1703` then made `thread-id` the winning locality | ||
| source for current Codex. The two compose incorrectly: the flag and | ||
| CAS both require `sticky_source == "session_header"`, which current | ||
| Codex never is. | ||
|
|
||
| The raw compatibility row is the process-session key. Looking it up | ||
| with `continuity_source=thread_header` treats a `session_header` | ||
| tombstone as a live hard owner, so even a successful session-only | ||
| restart is undone by the next thread-id turn. | ||
|
|
||
| ## Goals / Non-Goals | ||
|
|
||
| **Goals:** | ||
|
|
||
| - Account-neutral goal restart with `session-id` + `thread-id` retires | ||
| the unavailable raw owner for process-session interpretation and | ||
| routes to a replacement. | ||
| - Later same-thread turns without a new hard owner stay on that | ||
| replacement. | ||
| - Explicit `turn_state` of the same text stays hard-bound. | ||
|
|
||
| **Non-Goals:** | ||
|
|
||
| - Changing file-pin, previous-response, conversation, or tool-state | ||
| fail-closed ownership. | ||
| - Making `thread_header` an abandonment scope on the raw row. | ||
| - Dashboard, settings, or schema changes. | ||
|
|
||
| ## Decisions | ||
|
|
||
| - Grant `abandon_unavailable_legacy_owner` for `thread_header` only | ||
| when a process session is also present. Thread-only clients have no | ||
| process-session raw row to retire. | ||
| - Allow retirement CAS when request source is `thread_header`. The | ||
| write remains `abandonment_scope=session_header`. | ||
| - Load the raw `legacy_sticky_key` with `continuity_source=session_header`. | ||
| That lookup is process-session interpretation, not thread identity. | ||
|
|
||
| **Alternative considered:** keep CAS gated on request source and only | ||
| set the flag. Rejected because the CAS would still not run. | ||
|
|
||
| **Alternative considered:** abandon the raw row for every source. | ||
| Rejected because colliding explicit `turn_state` must stay hard. | ||
|
|
||
| ## Risks / Trade-offs | ||
|
|
||
| - [Risk] A thread-header request could retire a raw row that was | ||
| written as turn-state with equal text. → Mitigation: CAS still | ||
| writes `session_header` scope only; turn-state lookup of that text | ||
| keeps the stored owner. | ||
| - [Risk] Existing tests only exercise `session_id` without `thread-id`. | ||
| → Mitigation: add the missing header combination next to those tests. |
43 changes: 43 additions & 0 deletions
43
openspec/changes/goal-restart-thread-header-abandonment/proposal.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| ## Why | ||
|
|
||
| Current Codex sends both a shared process `session-id` and a distinct | ||
| `thread-id` on a self-contained goal restart. Affinity classifies that | ||
| request as `thread_header`, so the one-shot | ||
| `abandon_unavailable_legacy_owner` flag never sets and retirement CAS | ||
| never runs. The restart stays fail-closed on the unavailable legacy | ||
| owner even though the payload is account-neutral. | ||
|
|
||
| ## What Changes | ||
|
|
||
| - Grant goal-restart abandonment when a thread-scoped request still | ||
| carries a process session, not only when locality source is | ||
| `session_header`. | ||
| - Let retirement CAS retire the raw process-session row for | ||
| `session_header` interpretation from that thread-scoped request. | ||
| - Consult the raw process-session row as `session_header` | ||
| interpretation so a scoped tombstone hides it from later thread-id | ||
| turns. Explicit `turn_state` of the same text stays hard. | ||
| - Keep incremental, file-pinned, conversation-bound, and unresolved | ||
| tool-state requests fail-closed. | ||
|
|
||
| ## Capabilities | ||
|
|
||
| ### New Capabilities | ||
|
|
||
| - None. | ||
|
|
||
| ### Modified Capabilities | ||
|
|
||
| - `sticky-session-operations`: Current Codex `thread-id` on a | ||
| self-contained goal restart MUST still abandon the unavailable raw | ||
| process-session owner for `session_header` interpretation and keep | ||
| later same-thread continuity on the replacement. | ||
|
|
||
| ## Impact | ||
|
|
||
| - `app/modules/proxy/affinity.py` restart-capability gate. | ||
| - `app/modules/proxy/_load_balancer/sticky_selection.py` retirement CAS | ||
| source check. | ||
| - `app/modules/proxy/load_balancer.py` raw-row lookup source. | ||
| - Focused affinity and sticky-selection tests. | ||
| - No API, schema, setting, dashboard, or wire-format change. |
43 changes: 43 additions & 0 deletions
43
.../goal-restart-thread-header-abandonment/specs/sticky-session-operations/spec.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| ## ADDED Requirements | ||
|
|
||
| ### Requirement: Thread-scoped current Codex restarts still abandon a raw process-session owner | ||
|
|
||
| A self-contained Codex goal-continuation restart that also carries a distinct `thread-id` MUST still be eligible for the existing process-session abandonment exception. The request's thread-scoped locality source MUST NOT prevent the one-shot abandonment capability or the compare-and-set retirement of the raw process-session row. | ||
|
|
||
| The retirement write MUST remain scoped to `session_header` | ||
| interpretation of that raw key. An explicit `turn_state` lookup of the | ||
| same text MUST stay hard-bound to the stored account. After a | ||
| successful retirement, later same-thread turns that have no new hard | ||
| owner MUST keep continuity on the replacement account and MUST NOT | ||
| treat the `session_header`-abandoned raw row as live hard ownership. | ||
|
|
||
| Ordinary incremental, file-pinned, conversation-bound, and unresolved | ||
| tool-state requests MUST remain fail-closed on their required owner. | ||
|
|
||
| #### Scenario: Goal restart with process session and thread-id abandons the unavailable raw owner | ||
|
|
||
| - **GIVEN** a process-session identifier has a raw legacy `codex_session` mapping to account A | ||
| - **AND** account A is paused, rate-limited, or quota-exceeded | ||
| - **AND** account B is eligible | ||
| - **AND** the request also carries a distinct `thread-id` | ||
| - **WHEN** Codex sends the recognized goal-continuation marker with an account-neutral self-contained full resend and no other continuity dependency | ||
| - **THEN** the proxy marks the still-current raw mapping to account A abandoned only for process-session interpretation | ||
| - **AND** it routes the restarted turn to account B | ||
| - **AND** subsequent same-thread continuity remains on account B | ||
|
|
||
| #### Scenario: Thread-id on a goal restart cannot erase colliding explicit turn-state ownership | ||
|
|
||
| - **GIVEN** a raw legacy `codex_session` row was written as explicit turn-state ownership for account A | ||
| - **AND** a later request carries the same text as a process-session header plus a distinct `thread-id` | ||
| - **WHEN** a marked self-contained goal restart abandons that text for process-session interpretation | ||
| - **THEN** the restart may select account B | ||
| - **AND** an explicit turn-state lookup of the same text remains hard-bound to account A | ||
|
|
||
| #### Scenario: Account-dependent thread-scoped restart stays fail-closed | ||
|
|
||
| - **GIVEN** a process-session identifier has a raw legacy mapping to unavailable account A | ||
| - **AND** the request carries a distinct `thread-id` | ||
| - **AND** the body has a previous response, conversation, file pin, or unresolved tool state | ||
| - **WHEN** the request is selected | ||
| - **THEN** the request fails closed on account A | ||
| - **AND** the raw mapping is neither deleted nor rebound |
23 changes: 23 additions & 0 deletions
23
openspec/changes/goal-restart-thread-header-abandonment/tasks.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| ## 1. Implementation | ||
|
|
||
| - [x] 1.1 Grant `abandon_unavailable_legacy_owner` for `thread_header` | ||
| when a process session is present and the payload is | ||
| account-neutral. | ||
| - [x] 1.2 Allow retirement CAS when request source is `thread_header`. | ||
| Keep the write scoped to `session_header`. | ||
| - [x] 1.3 Load the raw `legacy_sticky_key` as `session_header` | ||
| interpretation so a scoped tombstone hides it from later | ||
| thread-id turns. | ||
|
|
||
| ## 2. Regression coverage | ||
|
|
||
| - [x] 2.1 Assert session-id + thread-id goal restart sets the | ||
| abandonment flag; turn-state and account-dependent payloads do | ||
| not. | ||
| - [x] 2.2 Assert sticky selection retires the raw owner and selects a | ||
| replacement when source is `thread_header`. | ||
|
|
||
| ## 3. Validation | ||
|
|
||
| - [x] 3.1 Run the focused affinity and sticky-selection tests. | ||
| - [x] 3.2 Run strict OpenSpec validation for this change. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.