Skip to content

fix: support a host-owned idle steering fallback - #593

Merged
EugeneTheDev merged 6 commits into
agentclientprotocol:mainfrom
leehom0123:fix/host-owned-steering-fallback
Oct 28, 2026
Merged

EugeneTheDev merged 6 commits into
agentclientprotocol:mainfrom
leehom0123:fix/host-owned-steering-fallback

Conversation

@leehom0123

Copy link
Copy Markdown
Contributor

If a Codex turn finishes just before _session/steering reaches it, the adapter starts another turn and returns startedNewTurn. The client never receives that new prompt's final response. If the original prompt is still waiting for plan approval, steering waits too, which can block clients that process steering and permission replies serially.

This adds the opt-in requested in #440:

{"_meta":{"steering":{"idleBehavior":"promptRequired"}}}

Live turns still receive the input through native steering. When no turn can accept it, the adapter returns promptRequired with reason: "noRunningTurn", leaving the input unconsumed and starting no new turn. The client can answer any pending approval, wait for the original prompt to finish, then send the input through a normal session/prompt. Calls without the option behave as before.

This builds on @fontanierh's patch in #441, cherry-picked with the original author and source reference preserved. I added regression tests for pending plan approval, malformed metadata tests, and usage documentation.

Fixes #440.

Testing

  • 33 targeted tests passed. The pending-plan-approval steering tests timed out before the fix.
  • Typecheck, build, and all six standalone binary targets passed.
  • Tested idle, live-turn steering, and pending plan approval with real Codex. Steering returned in 1 ms while approval was pending; rejecting implementation completed the original prompt with its stop reason, usage, and quota.
  • Full suite on Windows: 1,205 passed, 19 failed, 36 skipped. All 19 failures reproduced on unmodified main at 4583cb3, in history, attachment, and path assertions.

The real Codex tests used temporary local runners, adapted for the removed authRequired() call in the repository's test script. Cancellation dispatch has unit coverage; the real test used the reject-plan path.

For clients: promptRequired is a steering result, not completion of the original prompt or its approval request. The existing _meta.steering.supported flag also does not distinguish support for this option, so clients need to check the adapter version.

jim and others added 4 commits October 8, 2026 10:55
Resolve the conflict in performSteeringRequest by keeping the host-owned
idle fallback branch and adopting the new startNewTurnFromSteering
signature from main (ACP v2 rework): the idle steer now forwards its
clientUserMessageId like the injection path does.
@leehom0123

Copy link
Copy Markdown
Contributor Author

Merged latest main (a636804) and resolved the one conflict in performSteeringRequest: the host-owned promptRequired branch is kept, and the legacy fallback now forwards its clientUserMessageId through the new startNewTurnFromSteering(params, clientUserMessageId) signature from the ACP v2 rework.

Verification on Windows: typecheck passes; the 29 targeted steering/plan-approval tests pass. The full suite fails 70 tests in 10 files, and a run of the same 10 files on the unmodified main baseline fails the identical 70 — pre-existing Windows failures, none introduced by this merge.

@EugeneTheDev could you take a look at the conflict resolution, since it touches the steering path your v2 work changed? @fontanierh heads-up that #441's patch in this PR has been rebased over the v2 rework.

@EugeneTheDev EugeneTheDev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A couple of small notes

Comment thread src/CodexAcpServer.ts Outdated
throw RequestError.invalidParams();
}
const meta = params["_meta"];
if (meta !== undefined && !isRecord(meta)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Problem: _meta: null is now rejected with invalidParams, so a client that does not use the new option can no longer steer.

How it happens:

  1. ACP allows _meta to be null (_meta?: {[key: string]: unknown} | null in the SDK types).
  2. A client sends _session/steering with {"sessionId": "...", "prompt": [...], "_meta": null}.
  3. On main the parser ignores _meta, so this works. Here isRecord(null) is false and the call throws "steering _meta must be an object". The new test rejects malformed steering metadata asserts this for null.

Impact: this breaks "Calls without the option behave as before" for clients that serialize an empty _meta as null. The steer fails instead of injecting. It only affects clients that send null, but the PR introduces it.

Fix: treat null like a missing _meta: if (meta !== undefined && meta !== null && !isRecord(meta)). The meta?.["steering"] line already handles null. Move null from the rejected list to the accepted one in steer-events.test.ts, e.g. add it to the reports injected ... (metadata: %j) list. I applied this temporarily and steer-events.test.ts passed (19 tests), with typecheck clean.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: _meta: null is now treated like an absent _meta, and the accepted path is covered by adding null to the reports injected metadata list.

})).rejects.toThrow(RequestError);
});

it('rejects an unsupported idle behavior', async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

rejects an unsupported idle behavior only differs from the rejects malformed steering metadata ... test below by its input. Add {steering: {idleBehavior: "startDetachedTurn"}} to that it.each list and delete this test. That saves about 9 lines, and the unsupported value then also checks that no turn is started or steered.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done: folded {steering: {idleBehavior: "startDetachedTurn"}} into the malformed-metadata it.each list (which also asserts no turn starts) and dropped the standalone test.

The ACP type allows `_meta: null`, so treat it like an absent `_meta`
instead of rejecting the steer. Fold the unsupported-idleBehavior case
into the malformed-metadata it.each list, which also asserts that no
turn is started, and drop its standalone test.

@leehom0123 leehom0123 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Both notes addressed in b2a3c59 — null _meta is now accepted like an absent one, and the standalone idleBehavior test folded into the malformed-metadata list. Typecheck and the steering suites pass (29 tests). Ready for re-review.

@EugeneTheDev EugeneTheDev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good, please check docs comment

Comment thread README.md Outdated
- API key via `CODEX_API_KEY` or `OPENAI_API_KEY`.
- A custom OpenAI-compatible gateway, when the client opts in to the gateway auth capability.

## Host-owned steering fallback

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This section is too detailed for the README. Please move it to a new docs/steering.md and drop it from the README.

  • Narrow features already have their own files in docs/ (subagent-sessions.md, session-compaction.md), so this follows the existing layout.
  • _session/steering has no doc of its own yet. The new file can describe the whole method, not just this option: the _meta.steering.supported capability, the request, every outcome (injected, startedNewTurn, failed, promptRequired), and idleBehavior. The note that supported does not show whether this option is available fits there too.
  • In docs/air-extensions.md, link the steering row of the "JetBrains shared keys" table to steering.md. Don't put the contract itself in that file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the pointer — moving it out makes the README consistent with how the other narrow features are laid out.

Done: the section is out of the README and into a new docs/steering.md. The file covers the whole method, not just this option — the _meta.steering.supported capability, the request shape, every outcome (injected, startedNewTurn, failed, promptRequired), and the idleBehavior opt-in. The note that supported alone doesn’t tell clients whether the option is available went there too. The README now carries a single Features bullet linking to the doc; no other section was left behind. The steering row in the "JetBrains shared keys" table of docs/air-extensions.md links to steering.md, and the contract itself stays out of that file.

Docs-only change, so no test or typecheck impact. Ready for another look when you have a moment.

README held a 28-line section for one opt-in. The whole method now has
its own doc covering the capability, the request, every outcome, and
idleBehavior; the README keeps a single Features bullet and the AIR
shared-keys table links to the doc.
@EugeneTheDev
EugeneTheDev merged commit 356abaf into agentclientprotocol:main Oct 28, 2026
2 checks passed
mrmans0n added a commit to mrmans0n/codex-acp that referenced this pull request Oct 29, 2026
Upstream v2.4.0 (agentclientprotocol#593) adds a host-owned idle steering fallback: parseSessionSteerParams
validates `_meta` and `_meta.steering`, keeps only `{steering: {idleBehavior: "promptRequired"}}`,
and SessionSteerRequest types `_meta` as that shape. No upstream PR absorbs this patch.

The downstream patch forwards the steering request's `_meta`, so a turn the steer starts carries
`_meta.codexHooks.reviewToken` into prompt() and continues the hook review. Forward the whole
`_meta` after upstream's validation instead of upstream's reduced copy, and type it as a record
that also has upstream's `steering` shape. `idleBehavior: "promptRequired"` therefore still
answers `promptRequired` without starting a turn, and any other steer still starts a reviewed
turn. The hook review docs mention the fallback. Every other line of the patch is unchanged.

The existing reviewed-steering test fails with upstream's reduced `_meta`; a new test (idle
fallback next to reviewed metadata, then a reviewed steer that starts a turn) fails with
upstream's reduced `_meta` and when only `codexHooks` is forwarded.
Adapted from c0e41b9 (original 960f779).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Steering should support a host-owned idle fallback

2 participants