Codex client-tool continuation: Family A suspended turn + worker protocol v3 (D-062, #137) #159

Merged
adrian.tkacz merged 7 commits from gateway/codex-client-tools into develop 2026-09-29 20:18:10 +00:00
Member

Closes #137 (child E of program #132). Security-critical — the mandatory gpt-daybreak-blue-latest security review applies on the exact head before any merge; this PR is open for human review and is NOT merged by agents.

What this implements

The D-062 client-tool continuation for the Codex execution source — Family A, the protocol-native suspended-turn design selected by #137's TOOL_BRIDGE_VIABLE investigation. Numbered D-062 (D-060 reserved by issue #156; D-061 taken by #157 mid-flight).

  • Worker protocol v3 (closed, version-gated): execute_tool_call / execute_tool_result. Rolling upgrade: v1/v2 workers negotiate as before, are never tool-continuation-capable, keep ordinary execution; a v1/v2 session carrying v3 frames is fatal.
  • Family A lifecycle: one App Server process/thread/turn; the backend's item/tool/call is held and relayed to the harness as a normal OpenAI tool_calls response; the harness's ordinary role:"tool" request is correlated to the SAME turn. The round trip deliberately does NOT complete — see the STOP below.
  • The success STOP (review round 2, finding 1): upstream (openai/codex @ 36650394) defines the answer's success as "Whether the tool call succeeded" — a required bool the generic text-only role:"tool" message cannot express. The adapter REFUSES to fabricate it: delivery fails typed (tool_result_success_unresolved), tool_results stays UNSUPPORTED, and an owner decision packet (issue #137, options a/b/c) resolves the mapping. No compatibility rule was invented.
  • Continuation security boundary (gateway_continuation.py): opaque srct- tool_call ids; single-subscriber exactly-once claims; digest-only retention (no raw arguments/results/prompts); bounded pending count (64) + client-scoped replay tombstones; shape-aware detection (only a final role:"tool" message triggers continuation logic — follow-up turns are never poisoned; a dangling gateway-issued token is a typed 404, never a fresh completion).
  • Register-before-exposure: the continuation is registered inside the dispatch BEFORE the tool_call id is observable (stream or body); bound failure cancels the backend, releases the adapter slot, and exposes no token.
  • Terminal cleanup on every exit after a claim (permanent/ambiguous/timeout/lost/cancelled/failed/unexpected/disconnect) plus RESUMING-record expiry that cancels + tombstones.
  • Live v3 eligibility, pre-ranking: tool-bearing requests availability-gate per worker_bridged resource on the owning worker's LIVE v3 negotiation (worker_continuation_unavailable); v2/v3 same-identity routes select v3; no fact ⇒ 503; non-tool requests unaffected; the D-043 matrix key untouched.
  • Authority recheck before delivery: the client's CURRENT grant vs the EXACT original target through the same M02 authorization stage; revocation ⇒ 403 unauthorized_target + turn cancelled; no scarcity/campaign rerun (D-059 non-authoritative for live turns).
  • One absolute lifetime: the original attempt's admission deadline bounds the whole logical turn; worker deadline timer + failure-tolerant reaper cancel at expiry.

Evidence changes (conservative)

tool_calls: UNSUPPORTED → PARTIAL (mechanism evidence 2026-09-28 on codex-cli 0.155.0-alpha.16.3, upstream re-pin rust-v0.157.1; declarations/suspension/relay test-verified; live signed-in acceptance pending). tool_results: STAYS UNSUPPORTED (the answer-point STOP). CODEX_SURFACE_CAPABILITIES.tool_calls → true. Decision: D-062.

Gates (exact head e232e39)

  • Three review rounds on exact heads: round 1 (2 MERGE_BLOCKERs) → remediated → round-2 fresh review (6 findings, incl. the STOP) → remediated → round-3 fresh review (docs-only blocker + 2 defers) → remediated. Implementation verdict after every adversarial pass: held.
  • Full test suite: 2318 tests OK; basedpyright 0/0/0; guardrails OK; git diff --check clean; rebased on origin/develop = 59538e9; 5 commits, no merges.
  • Known flake: one #143-family test_scenario_11 cancellation flake fired once under full-suite load (pre-existing, passes on rerun; not folded in).

Remaining gates (open at the human decision point)

  • Daybreak security review: SECURITY_REVIEW_BLOCKED — unavailable in this environment; no substitute closes the gate.
  • Owner decision packet (issue #137): how to answer upstream success — (a) documented compatibility rule, (b) keep the STOP until a structured surface, (c) hybrid. Option (b) is shipped.
  • Live acceptance: LIVE_TOOL_BRIDGE_NOT_VERIFIED (#141 owns layered acceptance).
  • Minor review DEFERs recorded on #137.
Closes #137 (child E of program #132). **Security-critical** — the mandatory `gpt-daybreak-blue-latest` security review applies on the exact head before any merge; this PR is open for human review and is NOT merged by agents. ## What this implements The **D-062** client-tool continuation for the Codex execution source — Family A, the protocol-native suspended-turn design selected by #137's TOOL_BRIDGE_VIABLE investigation. Numbered D-062 (D-060 reserved by issue #156; D-061 taken by #157 mid-flight). - **Worker protocol v3** (closed, version-gated): `execute_tool_call` / `execute_tool_result`. Rolling upgrade: v1/v2 workers negotiate as before, are never tool-continuation-capable, keep ordinary execution; a v1/v2 session carrying v3 frames is fatal. - **Family A lifecycle:** one App Server process/thread/turn; the backend's `item/tool/call` is held and relayed to the harness as a normal OpenAI `tool_calls` response; the harness's ordinary `role:"tool"` request is correlated to the SAME turn. **The round trip deliberately does NOT complete** — see the STOP below. - **The `success` STOP (review round 2, finding 1):** upstream (openai/codex @ 36650394) defines the answer's `success` as "Whether the tool call succeeded" — a required bool the generic text-only `role:"tool"` message cannot express. The adapter REFUSES to fabricate it: delivery fails typed (`tool_result_success_unresolved`), `tool_results` stays UNSUPPORTED, and an owner decision packet (issue #137, options a/b/c) resolves the mapping. No compatibility rule was invented. - **Continuation security boundary** (`gateway_continuation.py`): opaque `srct-` tool_call ids; single-subscriber exactly-once claims; digest-only retention (no raw arguments/results/prompts); bounded pending count (64) + client-scoped replay tombstones; shape-aware detection (only a final `role:"tool"` message triggers continuation logic — follow-up turns are never poisoned; a dangling gateway-issued token is a typed 404, never a fresh completion). - **Register-before-exposure:** the continuation is registered inside the dispatch BEFORE the tool_call id is observable (stream or body); bound failure cancels the backend, releases the adapter slot, and exposes no token. - **Terminal cleanup on every exit** after a claim (permanent/ambiguous/timeout/lost/cancelled/failed/unexpected/disconnect) plus RESUMING-record expiry that cancels + tombstones. - **Live v3 eligibility, pre-ranking:** tool-bearing requests availability-gate per worker_bridged resource on the owning worker's LIVE v3 negotiation (`worker_continuation_unavailable`); v2/v3 same-identity routes select v3; no fact ⇒ 503; non-tool requests unaffected; the D-043 matrix key untouched. - **Authority recheck before delivery:** the client's CURRENT grant vs the EXACT original target through the same M02 authorization stage; revocation ⇒ 403 `unauthorized_target` + turn cancelled; no scarcity/campaign rerun (D-059 non-authoritative for live turns). - **One absolute lifetime:** the original attempt's admission deadline bounds the whole logical turn; worker deadline timer + failure-tolerant reaper cancel at expiry. ## Evidence changes (conservative) `tool_calls`: UNSUPPORTED → **PARTIAL** (mechanism evidence 2026-09-28 on `codex-cli 0.155.0-alpha.16.3`, upstream re-pin `rust-v0.157.1`; declarations/suspension/relay test-verified; live signed-in acceptance pending). `tool_results`: **STAYS UNSUPPORTED** (the answer-point STOP). `CODEX_SURFACE_CAPABILITIES.tool_calls` → true. Decision: **D-062**. ## Gates (exact head `e232e39`) - Three review rounds on exact heads: round 1 (2 MERGE_BLOCKERs) → remediated → round-2 fresh review (6 findings, incl. the STOP) → remediated → round-3 fresh review (**docs-only blocker + 2 defers**) → remediated. Implementation verdict after every adversarial pass: held. - Full test suite: **2318 tests OK**; `basedpyright` **0/0/0**; guardrails OK; `git diff --check` clean; rebased on `origin/develop` = `59538e9`; 5 commits, no merges. - Known flake: one #143-family `test_scenario_11` cancellation flake fired once under full-suite load (pre-existing, passes on rerun; not folded in). ## Remaining gates (open at the human decision point) - **Daybreak security review: `SECURITY_REVIEW_BLOCKED`** — unavailable in this environment; no substitute closes the gate. - **Owner decision packet** (issue #137): how to answer upstream `success` — (a) documented compatibility rule, (b) keep the STOP until a structured surface, (c) hybrid. Option (b) is shipped. - **Live acceptance: `LIVE_TOOL_BRIDGE_NOT_VERIFIED`** (#141 owns layered acceptance). - Minor review DEFERs recorded on #137.
feat: D-060 client-tool continuation on the Codex source (Family A, protocol v3)
All checks were successful
ci / check (pull_request) Successful in 8m37s
97441a6a65
Implements #137: the harness-owned client-tool round trip on the Codex
execution source as a suspended stateful continuation (Family A), the
protocol-native design selected by the TOOL_BRIDGE_VIABLE investigation.

- worker protocol v3 (closed, version-gated): execute_tool_call /
  execute_tool_result; v1/v2 workers are never tool-continuation-capable
- gateway continuation registry: opaque srct- tool_call ids,
  single-subscriber exactly-once claims, bounded pending count,
  fingerprint validation (model/effort/tools/tool_choice/prefix/echo),
  replay conflict, foreign-client not-found, one absolute lifetime
  (the original attempt's admission deadline; no per-leg reset)
- Codex adapter: narrowly scoped experimentalApi opt-in, verbatim
  thread-scoped dynamicTools, held item/tool/call answered only with
  the harness's verbatim text (success/contentItems mapping recorded),
  sequential rounds bounded, malformed/undeclared calls fail closed
- sticky continuation: no routing/capacity/policy I/O on the resume leg
  (D-059/scarcity changes cannot move a suspended turn); hard
  authority loss fails closed
- honest audit: suspension leg carries an unknown-status usage-free
  observation; the terminal leg carries the turn's single usage-bearing
  observation and repeats the original decision identity
- evidence: tool_calls/tool_results UNSUPPORTED -> PARTIAL (dated
  2026-09-28; live signed-in acceptance still gated); surface
  registration fact tool_calls true; D-060 recorded in decisions.md

Gates: full suite 2303 tests OK; basedpyright 0/0/0; guardrails OK;
git diff --check clean. One #143-family flake observed once
(test_scenario_11, load-sensitive, pre-existing; passes on rerun).
fix: review round 1 (PR #159) — shape-aware continuation detection + lifecycle tightening
All checks were successful
ci / check (pull_request) Successful in 8m38s
8b02bce3ed
Review findings (independent exact-head review, CHANGES_REQUESTED):

- MERGE_BLOCKER 1 (tombstone poisoning): continuation detection is now
  SHAPE-AWARE — only a request whose FINAL message is a role:tool
  result can trigger continuation/replay/conflict logic, so a
  conforming full-history harness's follow-up turns are never mistaken
  for replays of a consumed token. Regression-pinned.
- MERGE_BLOCKER 2 (dangling srct- result silently re-routed): a result
  addressed to a gateway-issued srct- token with no registry state
  (restart, bound) is now the typed 404 continuation_not_found — never
  fed to a fresh model selection. At the pending-registration bound the
  already-suspended worker turn is cancelled immediately instead of
  being orphaned until its deadline. Regression-pinned.
- DEFER 3: a sequential tool round now closes the superseded token as
  terminal (precise 409 on very-late replays; the registry slot is
  freed the moment the round advances).
- DEFER 4: a failed reaper tick is skipped, not fatal — the reaper
  survives (the previous early-return could silently disable worker
  liveness reaping and continuation expiry for the process lifetime).

Remaining review DEFERs (5 admission pre-check on continuation legs,
6 over-broad thread/start error mapping, 7 tombstone precision in a
close race) are recorded on the PR and left for follow-up.

Gates on this head: full suite 2307 tests OK; basedpyright 0/0/0;
git diff --check clean.
fix: renumber the continuation decision D-060 -> D-062 (numbering collision)
Some checks failed
ci / check (pull_request) Failing after 8m11s
71d0c79ab7
D-060 is reserved by open issue #156 (governance follow-up) and D-061 by
open PR #157 while this branch was in flight; canonical develop carried
neither. Whole-branch textual substitution only; the decisions.md entry
records the renumbering. Gates on this head: full suite 2307 tests OK,
basedpyright 0/0/0.
kilo changed title from Codex client-tool continuation: Family A suspended turn + worker protocol v3 (D-060, #137) to Codex client-tool continuation: Family A suspended turn + worker protocol v3 (D-062, #137) 2026-09-28 18:48:33 +00:00
Author
Member

Cross-reference for your human merge gate: PR #160 (reasoning output semantics, #158, head 7390277, opened 19:02 after this PR) independently reserved decision number D-062 from the same reservation logic (D-060 → #156, D-061 → #157) before this PR became visible to that session, and also bumps WORKER_PROTOCOL_VERSION to 3 — with a disjoint member set (optional conversation-message reasoning member vs this PR's execute_tool_call/execute_tool_result types). Both heads are reviewed/frozen artifacts, so neither renumbered unilaterally; the conflict is recorded with the proposed D-057/D-058-style resolution (second-merged PR renumbers and reconciles the protocol v3 section) in PR #160 comment. No action requested from this branch's review; this is the AGENTS.md conflict record so the owner sees both claims at merge time.

Cross-reference for your human merge gate: PR #160 (reasoning output semantics, #158, head `7390277`, opened 19:02 after this PR) independently reserved decision number **D-062** from the same reservation logic (D-060 → #156, D-061 → #157) before this PR became visible to that session, and **also bumps `WORKER_PROTOCOL_VERSION` to 3** — with a disjoint member set (optional conversation-message `reasoning` member vs this PR's `execute_tool_call`/`execute_tool_result` types). Both heads are reviewed/frozen artifacts, so neither renumbered unilaterally; the conflict is recorded with the proposed D-057/D-058-style resolution (second-merged PR renumbers and reconciles the protocol v3 section) in [PR #160 comment](https://forgejo.creatidy.com/BioMedical-IT/scarcity-router/pulls/160#issuecomment-12886). No action requested from this branch's review; this is the AGENTS.md conflict record so the owner sees both claims at merge time.
Six owner-review findings resolved, Family A / protocol v3 / generic
continuation architecture unchanged:

- F1 (MERGE BLOCKER): the unconditional success=true mapping is
  RETRACTED. Upstream (openai/codex @ 36650394, protocol.rs) defines
  `success` as "Whether the tool call succeeded" — a required bool; the
  generic text-only role:tool message and the representative capture
  carry no such fact, so the adapter REFUSES to fabricate the answer
  (typed tool_result_success_unresolved; the held request is never
  answered), tool_results returns to UNSUPPORTED (dated), and D-062 pt 6
  is rewritten. Owner decision packet posted on #137.
- F2 (MERGE BLOCKER): continuation records retain the assistant echo as
  a bounded DIGEST; raw arguments/results/prompts are no longer in
  gateway state (retention test pins the absence).
- F3 (MERGE BLOCKER): tool-bearing requests are availability-gated
  BEFORE ranking on the owning worker's LIVE protocol-v3 negotiation
  (worker_continuation_unavailable); v2 worker vs v3 worker for the same
  identity selects the v3 route; empty/absent fact fails closed;
  non-tool requests unaffected; D-043 matrix key untouched.
- F4 (MERGE BLOCKER): register-before-exposure — the adapter invokes a
  coordinator registration closure inside the dispatch BEFORE the
  tool_call id is observable (stream or body); bound failure cancels the
  backend and exposes no token.
- F5 (SECURITY): every post-claim exit closes the record terminal
  (permanent/ambiguous/timeout/lost/cancelled/failed/unexpected/
  disconnect); expire_due on a RESUMING record cancels + tombstones
  instead of silently dropping; pending_count returns to 0 immediately
  on each path (tested per case).
- F6 (SECURITY): current client authorization is re-evaluated against
  the EXACT original target through the same M02 authorization stage
  before delivery; revocation => 403 unauthorized_target, record closed
  cancelled, backend turn interrupted, never rerouted; D-059/scarcity
  stay non-authoritative for live turns.
- Cleanups: control-server banner prints the real protocol version
  (was hardcoded v1); MAX_FRAME_BYTES comment no longer claims the stale
  1 MiB body default; _compatibility_failure dedupes its reason codes
  (docstring already promised sorted-duplicate-free).

Gates on this head: full suite 2317 tests OK; basedpyright 0/0/0;
guardrails OK; git diff --check clean.
adrian.tkacz force-pushed gateway/codex-client-tools from f54f037916
All checks were successful
ci / check (pull_request) Successful in 8m56s
to babf3f9217
All checks were successful
ci / check (pull_request) Successful in 8m36s
2026-09-28 21:35:52 +00:00
Compare
fix: review round 3 (PR #159) — capability-claim contradiction + suspension-slot release
All checks were successful
ci / check (pull_request) Successful in 8m34s
e232e3983f
- MERGE BLOCKER (docs-only): three prose sites still claimed
  tool_results moved to PARTIAL while the authoritative record says
  UNSUPPORTED (the D-062 answer-point STOP). D-062 point 8 now states
  the split truthfully (tool_calls PARTIAL; tool_results STAYS
  UNSUPPORTED), the evidence-module docstring matches its own cell
  table, and the stage1 addendum heading/intro record the SPLIT. The
  duplicated D-062 numbering rationale is folded into one bullet.
- DEFER (leak): a failed continuation admission now releases the
  adapter's local suspension handle on both legs — the bounded table
  can no longer starve under sustained registry-full conditions.
  Regression: a v3-session bound failure asserts the local table is
  empty and the worker received the cancel.

Gates on this head: full suite 2318 tests OK; basedpyright 0/0/0;
guardrails OK; git diff --check clean.
The owner resolves the D-062 decision packet with option (a): an
explicit, owner-accepted LOSSY protocol translation for the generic
Chat Completions execution surface. A syntactically valid role:tool
message with the expected tool_call_id is represented on the Codex side
as DynamicToolCallResponse {success: true, contentItems:[inputText]}
with the verbatim message content.

- The adapter answers with the owner-approved mapping after ALL
  continuation validation (the tool_result_success_unresolved STOP is
  superseded and removed). success=true is NOT the native upstream
  meaning and NOT proof the external operation succeeded; content is
  never inspected, parsed or reinterpreted; success=false is never
  inferred; no client-specific branch exists.
- Evidence: tool_calls and tool_results both PARTIAL (live acceptance
  still outstanding; no PASS claim). Cells, module docstring, surface
  comment, execution-surface rows/section, and the stage1 addendum
  heading/intro/mapping/cell-state all tell the one story.
- D-062 pt 6 rewritten as the owner decision (upstream richer semantic
  recorded; lossiness explicit; content opaque; generic rule); pt 8 and
  Confidence reconciled; Reconciliation records the amendment provenance.
- Tests: full deterministic round trip restored and pinned for
  non-streaming AND streaming; error-like result text ("ERROR: lookup
  failed") is forwarded verbatim under the same mapping with
  success=false never inferred.

Gates on this head: full suite 2320 tests OK; basedpyright 0/0/0;
guardrails OK; git diff --check clean; base origin/develop = 59538e9.
docs: D-062 Boundary list gains routing_core.py (review round 4 defer)
All checks were successful
ci / check (pull_request) Successful in 8m44s
42a4832ef1
adrian.tkacz deleted branch gateway/codex-client-tools 2026-09-29 20:18:10 +00:00
Sign in to join this conversation.
No description provided.