Skip to content

fix(session): restore codex slash commands + recover dead resume anchors on the direct-CLI path#679

Merged
piorpua merged 4 commits into
mainfrom
fix/codex-session-slash-commands
Jul 24, 2026
Merged

fix(session): restore codex slash commands + recover dead resume anchors on the direct-CLI path#679
piorpua merged 4 commits into
mainfrom
fix/codex-session-slash-commands

Conversation

@BlackKeyZ

Copy link
Copy Markdown
Collaborator

Two session-layer fixes for the direct-CLI (session-port) stack, from Sentry user feedback:

1. codex in-session slash commands were gone (ELECTRON-3PX)

The port dropped the codex-acp bridge's private static command layer, so the in-session / menu advertised nothing and slash text was sent to codex as plain prompt text (codex does not interpret it).

  • Advertise the bridge-parity 6-command table (/review, /review-branch, /review-commit, /init, /compact, /logout) through capabilities + CatalogUpdated, so the REST read, the frontend push, and the agent_metadata writeback all see it.
  • Route slash prompts to the native ops the bridge used: review/start{target}, thread/compact/start, canned init prompt as a normal turn, account/logout (NoTurn + Notice). Unknown names and argument-less /review-branch|commit fall through verbatim (bridge parity).
  • Also fixes a silent drop: a pre-filled agentMessage (review verdicts carry full text on item/started with no deltas) was discarded by the delta-only arm — the verdict never persisted.

Wire lifecycles live-verified on codex 0.144.1 (review/start, thread/compact/start); e2e: /review caught a planted bug and the verdict persisted.

2. A dead resume anchor hung codex forever / error-carded claude (ELECTRON-3Q0)

A conversation whose persisted resume anchor the CLI no longer knows (e.g. pre-port sessions after upgrade) failed every turn — cron manual triggers surfaced UNKNOWN_UPSTREAM_ERROR on claude, and codex left the turn permanently Running with the conversation locked. The existing dead-anchor self-heal fired but was structurally defeated: the auto-replay rebuilt the task from the frozen turn-start snapshot and re-resumed the same dead session.

  • codex: claim the thread/resume response; on error ("no rollout found for thread id", live-captured) clear the pre-seeded binding and fail later sends fast as SessionNotFound instead of writing turn/start at a dead threadId.
  • codex: an error response for a pending send is never silently dropped — turn-flavored requests synthesize an is_error terminal (single-terminal discipline preserved), /logout surfaces a Notice.
  • A dispatch-time SessionNotFound clears the persisted anchor directly and classifies as the retryable UserAgentSessionNotFound.
  • The auto-replay re-reads ONLY the resume anchor (session_id + snapshot) before attempt 2, so the mid-turn clear takes effect and the replay opens Fresh; turn-scoped fields stay frozen.
  • Classifier: codex dead-thread messages are an agent-session error, not a provider-endpoint 404 (the generic "not found" arm was blocking the replay as non-retryable).
  • Replay-safety gate: only Success tips count as visible output; Warning/Info tips are system diagnostics.

Live-verified end to end: with a planted dead anchor, a codex cron trigger now recovers transparently (attempt 2 opens a fresh thread, the reply persists, a real anchor rebinds — previously a permanent hang); claude replays Fresh and rebinds a new session id.

Full pre-push gate: 7442 tests passed.

BlackKeyZ and others added 4 commits July 24, 2026 13:59
The session-port (#609) dropped the codex-acp bridge's slash-command layer:
codex_conn hardcoded an empty slash_commands catalog, so the in-conversation
'/' menu showed no agent commands for codex while the home page still showed
the stale bridge-era catalog persisted in agent_metadata (ELECTRON-3PX).

codex's app-server has no command-discovery wire (all 112 methods audited,
samples/codex-cli/0.137.0/schema-full/ClientRequest.json), and it does not
interpret slash text in a prompt. The bridge instead advertised a static
6-command table and translated each name to a native op at prompt time
(zed-industries/codex-acp v0.14.0 src/thread.rs:2894 builtin_commands, :3252
handle_prompt, :4195 extract_slash_command). Replicate both halves:

- capabilities()/CatalogUpdated now carry the bridge's 6-command table, so
  the conversation REST read, the AvailableCommands push, and the
  agent_metadata catalog writeback all see it (home + in-session converge).
- dispatch(Send) routes a leading slash to its native op: /review,
  /review-branch <b>, /review-commit <sha> -> review/start{target};
  /compact -> thread/compact/start; /init -> the bridge's canned AGENTS.md
  prompt as a normal turn; /logout -> account/logout (NoTurn + Notice).
  Unknown names and arg-less review-branch/commit fall through verbatim as
  plain text, matching the bridge.

Both wire ops were live-probed on codex 0.144.1 before wiring: each runs a
real turn (turn/started -> items -> turn/completed), so the existing reader
lifecycle applies unchanged (samples/codex-cli/0.144.1/
review_start_uncommitted.jsonl + thread_compact.jsonl + README).

The probe also exposed a second dropped signal: the review VERDICT is a
pre-filled agentMessage (full text on item/started, no deltas ever), and the
agentMessage arm assumed delta-streaming and discarded item text - the
verdict was silently lost. Emit the started edge's non-empty initial text as
a MessageDelta; started.text + subsequent deltas compose identically for
streamed messages, and the completed frame never re-emits.

Verified live end-to-end: '/review' on a repo with a planted bug runs a real
review turn and persists the verdict; the codex agent_metadata row refreshes
to the 6-command catalog.
… of hanging or error-carding

A conversation whose persisted resume anchor the CLI no longer knows (e.g.
pre-port sessions upgraded onto the direct-CLI stack) failed every turn:
claude surfaced an UNKNOWN_UPSTREAM_ERROR card, and codex hung the turn
forever with the conversation locked (cron manual triggers, Sentry
ELECTRON-3Q0). The existing self-heal cleared the anchor mid-turn, but the
auto-replay rebuilt the task from the turn-start snapshot and re-resumed the
same dead session, defeating it.

- codex_conn: claim the thread/resume response; on error ("no rollout found
  for thread id", live-verified samples/codex-cli/0.144.1/dead_resume.jsonl)
  clear the pre-seeded binding and poison the bound-thread wait so a Send
  fails fast as SessionNotFound instead of writing turn/start at a dead
  threadId or timing out.
- codex_conn: a JSON-RPC error response for a pending Send is no longer a
  silent drop — turn-flavored requests synthesize an is_error terminal
  (shared `terminated` discipline, late real terminals absorbed); NoTurn
  (/logout) surfaces a Notice. A failed dispatch also unmarks turn_in_flight.
- session_agent: a dispatch-time SessionNotFound clears the persisted anchor
  directly (dispatch errors never reach the event pump's is_dead_resume_anchor)
  and classifies as the retryable UserAgentSessionNotFound.
- is_unrecoverable_resume_error: recognize the codex dead-thread messages
  ("no rollout found for thread id", "thread not found:") next to claude's.
- turn_orchestrator/session_context: before an auto-replay attempt, re-read
  ONLY the resume anchor (session_id + snapshot) into the frozen turn-start
  build options, so the mid-turn anchor clear takes effect on attempt 2 and
  the replay opens Fresh. Turn-scoped fields (model/skills/workspace) stay
  frozen.
- send_error: classify the codex dead-thread messages as agent-session-not-
  found BEFORE the generic "not found" arm misreads them as a non-retryable
  provider-endpoint 404 (which blocked the replay).
- stream_relay: only a Success tip marks visible output for the replay-safety
  gate; Warning/Info tips are system diagnostics (the codex mode-seed
  rejection tip was making every such attempt "unsafe to replay").

Live-verified on both scenarios: codex recovers transparently (attempt 2
opens a fresh thread, reply persisted, new anchor rebound); claude replays
Fresh and rebinds a new session id.
…n direct-CLI turn apply

The direct-CLI turn-time mode apply passed the persisted canonical
full-access id (`agent-full-access`) straight to set_config_option, which
REJECTs it against codex's live catalog — codex advertises the legacy bare
token `full-access` (`:danger-full-access`->`full-access`), not the canonical
id. cron forces full-auto (`agent-full-access`) on every scheduled run, so the
turn hard-failed with "mode 'agent-full-access' is not one of the available
modes" before the send, surfaced as UNKNOWN_UPSTREAM_ERROR (ELECTRON-3Q0).

Align the canonical id to the live catalog before apply, mirroring the
AcpAgentManager reconcile path (normalize_requested_mode_for_available_values):
downgrade only the canonical full-access id, only when the live catalog lacks
it but carries the legacy token. A catalog with no full-access tier leaves the
value so set_config_option still REJECTs (never silently pick a weaker mode);
non-codex backends and non-full-access modes pass through untouched.
@piorpua

piorpua commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Added on top of this PR (commit 26f1246f): a second root-cause fix for ELECTRON-3Q0.

The dead-anchor recovery here handles a rejected thread/resume hang. But the cron failure has an earlier, independent trigger this PR doesn't reach:

  • cron forces full-auto on every scheduled run = full_auto_mode_id(codex) = agent-full-access (aionui-common/enums.rs).
  • codex's live catalog only advertises the legacy bare token full-access (:danger-full-accessfull-access, see codex_conn fill_discovery / profile_id_to_legacy_value) — never the canonical agent-full-access.
  • turn_orchestrator::apply_required_runtime_mode passed agent-full-access straight to set_config_option, hitting the "non-empty catalog missing value" arm → bad_request("mode 'agent-full-access' is not one of the available modes").
  • That failure returns Failed in run_user_turn before the send (no replay), so the dead-anchor recovery never engages — surfaced as UNKNOWN_UPSTREAM_ERROR.

Fix: align the canonical id to the live catalog before apply, mirroring the AcpAgentManager reconcile path (normalize_requested_mode_for_available_values) — downgrade the canonical full-access id only when the live catalog lacks it but carries the legacy token. A catalog with no full-access tier leaves the value so set_config_option still REJECTs (never silently pick a weaker mode); non-codex backends and non-full-access modes pass through untouched.

Scope: covers the required_runtime_mode auto-apply path (the always-reproducing cron case). An interactive manual switch to full-access goes through the user's explicit set_config_option and is a related but separate gap — not included here; suggested as a follow-up.

Added 6 unit tests; cargo test -p aionui-conversation + clippy pass.

Note: the "codex advertises full-access via the legacy token" claim is grounded in the AionCore-side parsing (fill_discovery / profile_id_to_legacy_value), not re-confirmed against a codex 0.144.x wire capture (no local samples/codex-cli/).

@piorpua
piorpua merged commit 7b60611 into main Jul 24, 2026
6 checks passed
@piorpua
piorpua deleted the fix/codex-session-slash-commands branch July 24, 2026 08:27
piorpua pushed a commit that referenced this pull request Jul 24, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.1.52](v0.1.51...v0.1.52)
(2026-07-24)


### Features

* **project:** wire project-bind side branch into owner creation
([#676](#676))
([300c833](300c833))


### Bug Fixes

* **agent:** unify CLI probe pipeline with classified failures and
adaptive slow-probe recheck
([#678](#678))
([67945d2](67945d2))
* **channel:** quiet WeChat poll log noise with state-transition logging
and exponential backoff
([#683](#683))
([ea4da55](ea4da55))
* **process:** allow whitespace in workspace cwd segments
([#410](#410) parity)
([#674](#674))
([c344b02](c344b02))
* **session:** restore codex slash commands + recover dead resume
anchors on the direct-CLI path
([#679](#679))
([7b60611](7b60611))
* **team:** converge system/lifecycle wakes into a team run
([#680](#680))
([38facd7](38facd7))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
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.

2 participants