## Why
Plugin analytics overloaded `plugin_id`: most events used the Codex
`<plugin>@<marketplace>` identity, while remote install events used the
backend plugin ID. That makes the same field change meaning across event
types and complicates downstream identity resolution.
This change makes the contract unambiguous:
- `plugin_id`: the local Codex `<plugin>@<marketplace>` identity, when
resolved
- `remote_plugin_id`: the backend plugin identity, when available
For a remote install failure that happens before plugin details resolve,
`plugin_id` is `null` and `remote_plugin_id` remains populated.
## What changed
All six plugin analytics events use the same identity contract:
- `codex_plugin_installed`
- `codex_plugin_install_failed`
- `codex_plugin_uninstalled`
- `codex_plugin_enabled`
- `codex_plugin_disabled`
- `codex_plugin_used`
Remote identity is resolved from the current installed-plugin snapshot
first, with persisted install metadata as fallback. The telemetry
metadata type keeps local identity optional for failures that occur
before remote details are available.
The app-server test client's manual analytics smokes now find remote
mutation events through `remote_plugin_id` and validate that `plugin_id`
remains local.
## Remote uninstall
Resolve and capture telemetry metadata before removing the local plugin
cache, then emit `codex_plugin_uninstalled` after the backend confirms
success. The event is also emitted when backend uninstall succeeds but
local cache cleanup reports `CacheRemove`.
If a concurrent remote-cache refresh removes the local bundle before
telemetry capture, the already-fetched remote plugin detail supplies
fallback capability metadata.
## Validation
- `just test -p codex-analytics` — 82 passed
- `just test -p codex-core-plugins` — 271 passed
- `just test -p codex-app-server-test-client` — 5 passed
- `just test -p codex-plugin` — 3 passed
- `just test -p codex-app-server plugin_install` — 37 passed
- `just test -p codex-app-server plugin_uninstall` — 10 passed
The production app-server install/uninstall flow was also exercised
against `plugins~Plugin_f1b845ac33888191ac156169c58733c2`
(`build-ios-apps@openai-curated-remote`), and the plugin's original
uninstalled state was restored.
## Why
`codex-app-server-test-client` previously treated
`item/tool/requestUserInput` as an unsupported server request and
terminated the connection. That made it impossible to use the client for
end-to-end testing of interactive turns: an operator could observe the
request, but could not answer it and confirm that the same turn resumed.
## What changed
- Handle `ToolRequestUserInput` server requests in the test client's
central request dispatcher.
- Render numbered terminal choices, accept exact option labels, support
free-form `Other` and text-only questions, and collect multiple answers.
- Send a protocol-native `ToolRequestUserInputResponse` and continue
streaming the active turn.
- Fail clearly when interactive input is requested without a terminal.
- Document the interactive behavior and add focused tests for option
selection, free-form answers, multiple questions, and invalid-selection
retries.
## Testing
- `just test -p codex-app-server-test-client`
- `just bazel-lock-check`
- Manually exercised the app-server flow, selected `TUI`, observed
`serverRequest/resolved`, and verified that the same turn completed with
the selected answer.
## This PR
The original [combined remote plugin analytics PR
#26281](https://github.com/openai/codex/pull/26281) mixed reusable
analytics test infrastructure, two manual smoke workflows, a metadata
refactor, and the final identity behavior. This PR adds the
account-mutating validation workflow separately so its cleanup and
recovery guarantees can be reviewed without the final analytics behavior
change.
- Add a manually invoked remote plugin install/uninstall smoke workflow.
- Require explicit account-mutation confirmation and an initially
uninstalled plugin.
- Validate the current `codex_plugin_installed` contract, where
`plugin_id` is the backend ID.
- Restore and verify the original uninstalled state, with a dedicated
recovery command.
This baseline intentionally does not require `codex_plugin_uninstalled`,
because production does not emit that event yet. The final PR will
update this smoke to require local `plugin_id`, `remote_plugin_id`, and
uninstall emission. Review this PR as the net diff against #27099.
## Testing
- `just test -p codex-app-server-test-client` (3 focused
capture/validation tests passed)
- The live workflow was previously exercised on the green combined
reference branch, and the original uninstalled account state was
restored.
- CI is green across the required platform matrix.
## Split Overview
```text
main
├── #27093 Debug analytics capture
│ └── #27099 Non-mutating plugin smoke
│ └── #27100 Remote install/uninstall smoke ← you are here
└── #27102 Plugin telemetry metadata refactor
After #27093, #27099, #27100, and #27102 merge:
└── Final PR: add remote_plugin_id to plugin analytics
```
Review order and dependencies:
1. [#27093 Add debug-only analytics event
capture](https://github.com/openai/codex/pull/27093) (based on `main`)
2. [#27099 Add a plugin analytics smoke
workflow](https://github.com/openai/codex/pull/27099) (stacked on
#27093)
3. [#27100 Add a remote plugin analytics mutation smoke
workflow](https://github.com/openai/codex/pull/27100) **(this PR,
stacked on #27099)**
4. [#27102 Centralize plugin telemetry metadata
construction](https://github.com/openai/codex/pull/27102) (independent,
based on `main`)
5. Final remote-ID behavior PR (created after PRs 1-4 merge)
The original [#26281](https://github.com/openai/codex/pull/26281)
remains open as the green aggregate reference until the final PR is
published.
## This PR
The original [combined remote plugin analytics PR
#26281](https://github.com/openai/codex/pull/26281) mixed reusable
analytics test infrastructure, two manual smoke workflows, a metadata
refactor, and the final identity behavior. This PR establishes a
non-mutating end-to-end plugin smoke workflow before any analytics
identity semantics change.
- Add `plugin-analytics-smoke` to the existing app-server test client.
- Exercise plugin disable, enable, and use through production app-server
RPC paths.
- Isolate config writes in a temporary file and use a loopback Responses
API server.
- Capture analytics without sending them to the production analytics
backend.
- Validate the current local `plugin_id`, names, capability metadata,
thread, turn, and model fields.
This is intentionally a baseline smoke workflow. It does not assert
`remote_plugin_id`; the final PR will update it when that field exists.
Review this PR as the net diff against #27093.
## Testing
- The test-client target compiles successfully.
- The combined reference branch exercised the manual smoke against the
live remote plugin service.
- CI is green across the required platform matrix.
## Split Overview
```text
main
├── #27093 Debug analytics capture
│ └── #27099 Non-mutating plugin smoke ← you are here
│ └── #27100 Remote install/uninstall smoke
└── #27102 Plugin telemetry metadata refactor
After #27093, #27099, #27100, and #27102 merge:
└── Final PR: add remote_plugin_id to plugin analytics
```
Review order and dependencies:
1. [#27093 Add debug-only analytics event
capture](https://github.com/openai/codex/pull/27093) (based on `main`)
2. [#27099 Add a plugin analytics smoke
workflow](https://github.com/openai/codex/pull/27099) **(this PR,
stacked on #27093)**
3. [#27100 Add a remote plugin analytics mutation smoke
workflow](https://github.com/openai/codex/pull/27100) (stacked on this
PR)
4. [#27102 Centralize plugin telemetry metadata
construction](https://github.com/openai/codex/pull/27102) (independent,
based on `main`)
5. Final remote-ID behavior PR (created after PRs 1-4 merge)
The original [#26281](https://github.com/openai/codex/pull/26281)
remains open as the green aggregate reference until the final PR is
published.
## Summary
- always rejoin an in-memory running thread on `thread/resume`, even
when overrides are present
- reject `thread/resume` when `history` is provided for a running thread
- reject `thread/resume` when `path` mismatches the running thread
rollout path
- warn (but do not fail) on override mismatches for running threads
- add more `thread_resume` integration tests and fixes; including
restart-based resume-with-overrides coverage
## Validation
- `just fmt`
- `cargo test -p codex-app-server --test all thread_resume`
- manual test with app-server-test-client
https://github.com/openai/codex/pull/11755
- manual test both stdio and websocket in app
For app-server development it's been helpful to be able to trigger some
test flows end-to-end and print the JSON-RPC messages sent between
client and server.