pi-openai-codex-compat 0.0.18 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +64 -41
  3. package/extensions/openai-codex-compat/apply-patch.ts +7 -0
  4. package/extensions/openai-codex-compat/codex-provider/codex-provider-contracts.ts +0 -2
  5. package/extensions/openai-codex-compat/codex-provider/codex-provider-runtime.ts +58 -23
  6. package/extensions/openai-codex-compat/codex-transport/codex-transport-contracts.ts +6 -0
  7. package/extensions/openai-codex-compat/codex-transport/codex-transport-errors.ts +13 -1
  8. package/extensions/openai-codex-compat/codex-transport/codex-transport-sse-stream.ts +1 -0
  9. package/extensions/openai-codex-compat/codex-transport/codex-transport-websocket-stream.ts +1 -0
  10. package/extensions/openai-codex-compat/codex-transport.ts +1 -0
  11. package/extensions/openai-codex-compat/command-tool-contract.ts +9 -0
  12. package/extensions/openai-codex-compat/command-tools.ts +4 -0
  13. package/extensions/openai-codex-compat/compaction-checkpoint.ts +35 -145
  14. package/extensions/openai-codex-compat/config.ts +0 -24
  15. package/extensions/openai-codex-compat/footer.ts +0 -1
  16. package/extensions/openai-codex-compat/image-generation.ts +8 -0
  17. package/extensions/openai-codex-compat/index.ts +2 -2
  18. package/extensions/openai-codex-compat/pi-runtime.ts +62 -9
  19. package/extensions/openai-codex-compat/remote-compaction.ts +46 -71
  20. package/extensions/openai-codex-compat/request-options.ts +2 -9
  21. package/extensions/openai-codex-compat/request-tools-entry.ts +113 -0
  22. package/extensions/openai-codex-compat/settings-pane.ts +2 -11
  23. package/extensions/openai-codex-compat/vendor/pi-ai/README.md +4 -0
  24. package/extensions/openai-codex-compat/vendor/pi-ai/openai-responses-serialization.ts +5 -4
  25. package/extensions/openai-codex-compat/web-run.ts +2 -0
  26. package/extensions/settings-session.ts +11 -7
  27. package/package.json +7 -7
package/CHANGELOG.md CHANGED
@@ -2,6 +2,53 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.0.20 - 2026-10-02
6
+
7
+ ### Fixed
8
+
9
+ - Omit incompatible response-item IDs when replaying grammar-tool calls that came
10
+ from another provider or were recorded as function calls.
11
+ Call/result pairing and valid same-model item IDs remain unchanged.
12
+
13
+ ### Changed
14
+
15
+ - Require Pi `>=1.0.0 <1.1.0`. Restart Pi after upgrading its runtime.
16
+
17
+ ## 0.0.19 - 2026-10-01
18
+
19
+ ### Removed
20
+
21
+ - The `reasoningMode` setting, its `PI_OPENAI_CODEX_COMPAT_REASONING_MODE` override,
22
+ and the `pro` footer label. The Codex endpoint rejects `reasoning.mode`, so the
23
+ extension now always omits it. Stale `reasoningMode` values in configuration
24
+ files are ignored.
25
+
26
+ ### Fixed
27
+
28
+ - Declare the same tools in native compaction requests as in turn requests while
29
+ Pi codemode is active. Compaction rebuilt the declarations from Pi's tool
30
+ registry, which lacks codemode's rewritten descriptions and still lists the
31
+ tools codemode `only` mode hides, so every compaction request missed the
32
+ prompt cache. Turn requests now save the tools they send in the session
33
+ whenever those tools change, and compaction declares the saved tools, also
34
+ after a resume or a branch switch.
35
+ - Resume sessions whose saved `/codex-settings` values include a setting this
36
+ version removed or no longer accepts, such as `reasoningMode`. Those fields are
37
+ skipped instead of failing the whole record at session start.
38
+ - Refuse to load on Pi runtimes older than 0.99.1. The load-time check accepted
39
+ any runtime since Pi 0.87 and reported the requirement as Pi 0.99.x. It now
40
+ requires Pi 0.99 host APIs and a reported Pi version of at least 0.99.1.
41
+
42
+ ### Added
43
+
44
+ - Forward every Codex stream event of a turn to Pi's `provider_stream_event` hook
45
+ as the server sent it, including metadata, error events, and terminal aliases,
46
+ so debugging extensions see Codex traffic. Pi reports a failing handler and
47
+ continues the turn.
48
+ - Annotate the command tools, `apply_patch`, `image_gen.imagegen`, and `web.run`
49
+ with read-only, destructive, and open-world hints for permission extensions.
50
+ `web.run` is the only read-only tool.
51
+
5
52
  ## 0.0.18 - 2026-09-30
6
53
 
7
54
  ### Fixed
package/README.md CHANGED
@@ -13,7 +13,7 @@ OpenAI Codex compatibility for [Pi](https://github.com/earendil-works/pi-mono),
13
13
  - **Standalone web search**: exposes Pi's dotted `web.run` tool as a native Responses namespace and executes search and browsing through Codex `alpha/search`.
14
14
  - **Dedicated Codex tool UI**: renders command tools, `apply_patch`, `image_gen.imagegen`, and `web.run` on a shared configurable surface with compact summaries and `Ctrl+O` expansion.
15
15
  - **Hosted web-search fallback**: injects native `web_search` only when `web.run` is inactive, with cached, indexed, or live modes.
16
- - **Native request controls**: configures Responses API text verbosity, reasoning summaries, and standard/pro reasoning mode on supported models.
16
+ - **Native request controls**: configures Responses API text verbosity and reasoning summaries.
17
17
  - **Draft settings pane**: `/codex-settings` stages edits before applying them. Apply to session stores them with the session without changing configuration files. Ctrl+S saves and applies them. Escape discards only unapplied drafts.
18
18
  - **Session-aware footer**: shows the current Pi session ID on the first line and appends non-default Codex request modes to the model side of Pi's normal second line.
19
19
 
@@ -22,13 +22,20 @@ Pi provides the Codex OAuth flow and model catalog. At session start, this packa
22
22
  ## Requirements
23
23
 
24
24
  - Node.js 22.19 or newer
25
- - Pi `>=0.99.1 <0.100.0`
25
+ - Pi `>=1.0.0 <1.1.0`
26
26
  - An OpenAI Codex login in Pi
27
27
 
28
28
  Restart Pi after upgrading its runtime. `/reload` reloads extensions but cannot
29
- upgrade the running Pi process. The extension checks the host's transcript APIs
30
- at load time. `PI_PACKAGE_DIR` can point an older executable at newer package
31
- metadata, so its displayed version alone does not establish compatibility.
29
+ upgrade the running Pi process. At load time, the extension requires the host's
30
+ transcript APIs, the Pi 0.99 APIs `SessionManager.getEntryCount()` and
31
+ `ExtensionAPI.getSettings()`, and a reported Pi version of at least 1.0.0. It
32
+ refuses to load otherwise. The load check does not reject newer releases: the
33
+ `<1.1.0` bound is the package's peer range, which Pi does not enforce when it
34
+ installs packages. `PI_PACKAGE_DIR` can point an older executable at
35
+ newer package metadata, so the reported version alone does not establish
36
+ compatibility. The API checks detect missing capabilities from before Pi 0.99.
37
+ They do not distinguish a Pi 0.99 executable from Pi 1.0.0 when its metadata
38
+ points at the newer installation.
32
39
 
33
40
  Authenticate through Pi if needed:
34
41
 
@@ -36,27 +43,29 @@ Authenticate through Pi if needed:
36
43
  /login openai-codex
37
44
  ```
38
45
 
46
+ **Pi's virtual models are not supported.** The package's features check the selected model, not the model that answers a request. Pi's experimental virtual models, registered with `pi.registerVirtualModel()`, stay selected while Pi routes each request to a physical model. With a virtual model selected, the package deactivates its tools, omits fast mode and the other request controls, and leaves compaction and output-limit continuation to Pi. This holds even for a virtual model listed under `openai-codex` or one that routes every request to an `openai-codex` model. Routed `openai-codex` requests still use the package's transport. Select an `openai-codex` model directly to use the package's features.
47
+
39
48
  ## Compatibility baseline and differences
40
49
 
41
50
  The compatibility baseline is official Codex CLI `0.149.1`, released August 24, 2026, at commit `ff29a44391deccde0aba0f8390337d7f3c319ea4`. It retains the `0.147.0` Responses Lite contract that groups direct function and custom declarations into one canonical `functions` namespace for namespace-capable providers. The `0.148.0`–`0.149.0` review adopted typed misalignment-policy failures while recording official unbounded connection recovery as an intentional bounded-transport deviation. The `0.149.1` review retained this package's checkpoint shape rather than exposing official Codex's new default-disabled retained-image budget. The [official release compatibility log](OFFICIAL_CODEX_CLI_RELEASES.md) is the canonical release-by-release record of protocol and `apply_patch` alignment, intentional deviations, and excluded runtimes. This section is the package's user-facing compatibility contract. See the [Responses Lite compatibility report](RESPONSES_LITE_COMPATIBILITY.md) and [Codex caching and transport comparison](CODEX_CACHE_RESEARCH.md) for detailed request-path findings and live cache trajectories.
42
51
 
43
52
  ### Configurable defaults that differ from Codex
44
53
 
45
- | Area | This package by default | Official Codex | Configuration |
46
- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
47
- | Generated-image detail sent back to the model | Sends image tool-result content with `input_image.detail: "auto"`. On GPT-5.6, `auto` uses original-size image accounting. | Uses `high`. | `imageDetail`: `auto`, `low`, `high`, or `original`. |
48
- | Image-generation tool | Enabled whenever an `openai-codex` model is selected. Backend capability and account failures surface when the tool executes. | Stable and enabled by default, but additionally gated by plan, model, provider, authentication, image-generation, and namespace capabilities. | `imageGeneration`: boolean. |
49
- | Standalone `web.run` | Disabled by default; when enabled, preferred over hosted `web_search` and sent with the complete reserved schema and description. | Enabled by default for `gpt-5.6-sol` through Responses Lite; otherwise subject to standalone-search feature and runtime gates. | `webRun`: boolean. |
50
- | Hosted web search | Disabled by default; when enabled, injected only for ordinary Responses while `web.run` is inactive. Responses Lite omits hosted tools. | Omitted for `gpt-5.6-sol` while standalone `web.run` is available; otherwise defaults to cached mode when hosted search is supported. | `webRun` and `webSearch`: `disabled`, `cached`, `indexed`, or `live`. |
51
- | Coding mutation tools | Enables `apply_patch` and suppresses Pi's active `edit` and `write` tools. | Chooses its tool surface from model metadata and runtime capabilities; there are no Pi `edit` or `write` tools to suppress. | `applyPatch`: boolean. |
52
- | Command tools | Replaces an active Pi `bash` tool with `exec_command` and `write_stdin`. | Chooses unified exec or legacy shell from model metadata, platform, execution environment, and runtime capabilities. | `shellTool`: `unified_exec` or `shell_command`. |
53
- | `apply_patch` debug output | Disabled; collapsed results show the normal visual summary and instruction rows. | Not applicable to Pi's tool-result renderer. | `applyPatchDebug`: boolean. |
54
- | `apply_patch` diagnostics capture | Disabled; no separate request or filesystem snapshot artifacts are retained. | Codex owns its rollout diagnostics rather than writing this package's artifact format. | `applyPatchDiagnostics`: boolean. |
55
- | Codex tool background | Uses a subtle theme-derived surface for extension-owned Codex tools. | Uses Codex's own TUI activity cells rather than Pi tool rows. | `toolBackground`: `subtle`, `status`, or `none`. |
56
- | Auto-compaction trigger | Relies on Pi's reserve-token threshold unless a percentage is configured. | Tracks Codex's model/token-budget state before and between sampling steps. | `autoCompactAtPercent`: percentage or unset. Mid-response percentage boundaries use Pi's bounded compact-and-continue lifecycle, so Pi auto-compaction must remain enabled. |
57
- | Fast mode | Uses the normal tier. | Uses the configured Codex service tier. | `fastMode`: boolean; `true` requests the priority tier. |
58
- | Responses Lite | Disabled; supported models use ordinary Responses. | Enabled according to Codex model metadata. | `responsesLite`: boolean; `true` enables Responses Lite. |
59
- | Text and reasoning request controls | Sends low text verbosity and automatic reasoning summaries; omits the default standard mode and sends `reasoning.mode` only for pro mode. | Resolves these controls through Codex configuration, model metadata, and turn state. | `textVerbosity`, `reasoningSummary`, and `reasoningMode`. |
54
+ | Area | This package by default | Official Codex | Configuration |
55
+ | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
56
+ | Generated-image detail sent back to the model | Sends image tool-result content with `input_image.detail: "auto"`. On GPT-5.6, `auto` uses original-size image accounting. | Uses `high`. | `imageDetail`: `auto`, `low`, `high`, or `original`. |
57
+ | Image-generation tool | Enabled whenever an `openai-codex` model is selected. Backend capability and account failures surface when the tool executes. | Stable and enabled by default, but additionally gated by plan, model, provider, authentication, image-generation, and namespace capabilities. | `imageGeneration`: boolean. |
58
+ | Standalone `web.run` | Disabled by default; when enabled, preferred over hosted `web_search` and sent with the complete reserved schema and description. | Enabled by default for `gpt-5.6-sol` through Responses Lite; otherwise subject to standalone-search feature and runtime gates. | `webRun`: boolean. |
59
+ | Hosted web search | Disabled by default; when enabled, injected only for ordinary Responses while `web.run` is inactive. Responses Lite omits hosted tools. | Omitted for `gpt-5.6-sol` while standalone `web.run` is available; otherwise defaults to cached mode when hosted search is supported. | `webRun` and `webSearch`: `disabled`, `cached`, `indexed`, or `live`. |
60
+ | Coding mutation tools | Enables `apply_patch` and suppresses Pi's active `edit` and `write` tools. | Chooses its tool surface from model metadata and runtime capabilities; there are no Pi `edit` or `write` tools to suppress. | `applyPatch`: boolean. |
61
+ | Command tools | Replaces an active Pi `bash` tool with `exec_command` and `write_stdin`. | Chooses unified exec or legacy shell from model metadata, platform, execution environment, and runtime capabilities. | `shellTool`: `unified_exec` or `shell_command`. |
62
+ | `apply_patch` debug output | Disabled; collapsed results show the normal visual summary and instruction rows. | Not applicable to Pi's tool-result renderer. | `applyPatchDebug`: boolean. |
63
+ | `apply_patch` diagnostics capture | Disabled; no separate request or filesystem snapshot artifacts are retained. | Codex owns its rollout diagnostics rather than writing this package's artifact format. | `applyPatchDiagnostics`: boolean. |
64
+ | Codex tool background | Uses a subtle theme-derived surface for extension-owned Codex tools. | Uses Codex's own TUI activity cells rather than Pi tool rows. | `toolBackground`: `subtle`, `status`, or `none`. |
65
+ | Auto-compaction trigger | Relies on Pi's reserve-token threshold unless a percentage is configured. | Tracks Codex's model/token-budget state before and between sampling steps. | `autoCompactAtPercent`: percentage or unset. Mid-response percentage boundaries use Pi's bounded compact-and-continue lifecycle, so Pi auto-compaction must remain enabled. |
66
+ | Fast mode | Uses the normal tier. | Uses the configured Codex service tier. | `fastMode`: boolean; `true` requests the priority tier. |
67
+ | Responses Lite | Disabled; supported models use ordinary Responses. | Enabled according to Codex model metadata. | `responsesLite`: boolean; `true` enables Responses Lite. |
68
+ | Text and reasoning request controls | Sends low text verbosity and automatic reasoning summaries, and omits `reasoning.mode`, which the Codex endpoint rejects. | Resolves these controls through Codex configuration, model metadata, and turn state. | `textVerbosity` and `reasoningSummary`. |
60
69
 
61
70
  `web.run` is a reserved GPT-5.6 tool name. Its declaration therefore reproduces the complete current Codex post-normalization `SearchCommands` schema and official tool description instead of using Pi's normal compact tool schema. This intentionally omits generated annotations such as `format` and `minimum` that Codex removes before sending the declaration to Responses.
62
71
 
@@ -64,7 +73,7 @@ The compatibility baseline is official Codex CLI `0.149.1`, released August 24,
64
73
 
65
74
  | Area | Difference |
66
75
  | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
67
- | Session storage | Pi remains the canonical session owner. Opaque Codex compaction checkpoints are stored in Pi `compaction` entries, and otherwise lossy Responses output is stored in sparse custom native-response entries. Official Codex owns a rollout/thread store directly. |
76
+ | Session storage | Pi remains the canonical session owner. Opaque Codex compaction checkpoints are stored in Pi `compaction` entries, otherwise lossy Responses output is stored in sparse custom native-response entries, and each change to the tools a turn request sends is stored in a custom request-tools entry. Official Codex owns a rollout/thread store directly. |
68
77
  | Branching | Checkpoints and native response overrides follow Pi's active session branch. Official Codex uses its own thread, turn, rollback, fork, and context-window lineage. |
69
78
  | Model switching | This package rejects model switches while the active Pi branch contains a native Codex checkpoint because the checkpoint is model-specific. |
70
79
  | System instructions | Send the leading Pi system message as the prompt, as Pi AI's Codex adapter does: Responses Lite models prepend it as developer input after `additional_tools`; other models send it through Responses `instructions`. Later Pi system messages (`/reload`, section changes) travel inline as developer items on models that accept mid-conversation system messages and collapse into the leading prompt otherwise. A forced prompt replaces the leading message. Compaction snapshots and resumed sessions lead with Pi's replayed prompt without rewriting old checkpoints. |
@@ -129,6 +138,19 @@ The following official Codex facilities are not exact equivalents in this packag
129
138
  | Goals, memories, and remote skill-resource tools | Not implemented; Pi's sessions, files, and native skills remain separate systems. |
130
139
  | Remote/deferred execution environments | Not implemented. |
131
140
 
141
+ Each tool declares Pi tool annotations, which permission extensions can use to decide which calls to confirm:
142
+
143
+ | Tool | Read-only | Destructive | Open world |
144
+ | ---------------------------------------------- | --------- | ----------- | ---------- |
145
+ | `exec_command`, `write_stdin`, `shell_command` | No | Yes | Yes |
146
+ | `apply_patch` | No | Yes | No |
147
+ | `image_gen.imagegen` | No | No | Yes |
148
+ | `web.run` | Yes | n/a | Yes |
149
+
150
+ None of the tools is idempotent. Pi does not verify annotations.
151
+
152
+ The provider passes every event of a turn request, over WebSocket and SSE, to Pi's `provider_stream_event` hook as the server sent it. Forwarding happens before the transport drops WebSocket `response.metadata` events, renames `response.done` to `response.completed`, or turns an `error` event into a failed turn. Prewarm and remote compaction requests are not forwarded. Pi reports a failing hook handler and continues the turn. When an SDK caller passes its own `onProviderStreamEvent` callback and it throws, the turn ends with an error without a WebSocket retry or the SSE fallback.
153
+
132
154
  See [`OFFICIAL_CODEX_CLI_TOOL_CATALOG.md`](OFFICIAL_CODEX_CLI_TOOL_CATALOG.md) for the complete researched Codex tool inventory, and [`CUSTOM_CODEX_PROVIDER_WEB_REFERENCES.md`](CUSTOM_CODEX_PROVIDER_WEB_REFERENCES.md) for the unimplemented citation/reference design.
133
155
 
134
156
  ## Install
@@ -224,7 +246,7 @@ deleted. External editors do not participate in that lock.
224
246
  Session overrides retain their original conflict checks across menu reopenings.
225
247
  Conflicting external edits remain protected after reload or restart.
226
248
 
227
- The effective settings are printed once when a TUI session starts. The footer shows the current Pi session ID alongside the working directory and optional session name. It shows `fast` and `pro` only when enabled, and shows text verbosity or reasoning summary only when they differ from their defaults.
249
+ The effective settings are printed once when a TUI session starts. The footer shows the current Pi session ID alongside the working directory and optional session name. It shows `fast` only when enabled, and shows text verbosity or reasoning summary only when they differ from their defaults.
228
250
 
229
251
  Example:
230
252
 
@@ -243,8 +265,7 @@ Example:
243
265
  "autoCompactAtPercent": 90,
244
266
  "webSearch": "disabled",
245
267
  "textVerbosity": "low",
246
- "reasoningSummary": "auto",
247
- "reasoningMode": "standard"
268
+ "reasoningSummary": "auto"
248
269
  }
249
270
  ```
250
271
 
@@ -266,18 +287,9 @@ Defaults:
266
287
  | `webSearch` | `disabled`, `cached`, `indexed`, `live` | `disabled` | Controls hosted search and standalone-search external access. `disabled` removes hosted search but leaves an independently enabled `web.run` in cached-only mode; `indexed` prefers indexed content; `live` permits live external access. |
267
288
  | `textVerbosity` | `low`, `medium`, `high` | `low` | Sets Responses API `text.verbosity`. |
268
289
  | `reasoningSummary` | `auto`, `concise`, `detailed`, `off` | `auto` | Sets `reasoning.summary` when reasoning is enabled; `off` omits the summary parameter. |
269
- | `reasoningMode` | `standard`, `pro` | `standard` | Controls supported models' execution mode independently of Pi's reasoning-effort control. The default omits `reasoning.mode`; `pro` sends `reasoning.mode: "pro"`. |
270
290
 
271
291
  Responses Lite supports exactly `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`,
272
- `gpt-6-astra`, `gpt-6-sol`, `gpt-6-luna`, and `gpt-6.1-sol`. Pro reasoning mode
273
- supports `gpt-5.6`, `gpt-5.6-*`, and exactly `gpt-6-astra`. GPT-6 Sol, GPT-6
274
- Luna, and GPT-6.1 Sol use standard reasoning mode even when `pro` is configured.
275
- Both controls remain opt-in. GPT-6.1 Sol requires Pi 0.99.1's model catalog.
276
-
277
- As of September 30, 2026, the Codex endpoint rejects `reasoning.mode` for every
278
- tested model, including those listed above, so `pro` requests fail there. The
279
- setting remains for providers that accept it. Releases do not test pro mode
280
- against `openai-codex`.
292
+ `gpt-6-astra`, `gpt-6-sol`, `gpt-6-luna`, and `gpt-6.1-sol`. It remains opt-in.
281
293
 
282
294
  Invalid JSON setting values are ignored and invalid JSON does not prevent Pi from
283
295
  starting. The settings pane reports malformed JSON instead of overwriting it.
@@ -301,7 +313,6 @@ Every setting can also be overridden for one Pi process with an environment vari
301
313
  | `webSearch` | `PI_OPENAI_CODEX_COMPAT_WEB_SEARCH_MODE` |
302
314
  | `textVerbosity` | `PI_OPENAI_CODEX_COMPAT_TEXT_VERBOSITY` |
303
315
  | `reasoningSummary` | `PI_OPENAI_CODEX_COMPAT_REASONING_SUMMARY` |
304
- | `reasoningMode` | `PI_OPENAI_CODEX_COMPAT_REASONING_MODE` |
305
316
 
306
317
  Environment variables have the highest precedence: defaults < global JSON < trusted-project JSON < environment. Boolean values accept `true`/`false`, `1`/`0`, `on`/`off`, or `enabled`/`disabled`. Other settings use the values in the defaults table; `PI_OPENAI_CODEX_COMPAT_AUTO_COMPACT_AT_PERCENT=off` and `PI_OPENAI_CODEX_COMPAT_AUTO_COMPACT_AT_PERCENT=default` explicitly select Pi's default compaction lifecycle.
307
318
 
@@ -391,6 +402,15 @@ The extension handles native compaction for `openai-codex`. It follows the Codex
391
402
  deduplicated, because each one is a distinct instruction the model already saw.
392
403
  4. Persist the opaque checkpoint in the Pi session and replay it on later requests.
393
404
 
405
+ A compaction request declares the same tools as the branch's latest turn request to the
406
+ selected model, so both share a prompt-cache prefix. Pi's transcript lists every active
407
+ tool, but Pi leaves some out of requests, for example the tools codemode hides in `only`
408
+ mode, and other extensions can edit a request's tools. Each turn request therefore saves
409
+ the tools it sent in an `openai-codex-compat-request-tools` session entry whenever they
410
+ differ from the branch's latest saved tools for that model. Compaction declares the saved
411
+ tools, also after a resume or a branch switch. A branch without a saved entry for the
412
+ model, such as one an older version wrote, declares the transcript's current tools.
413
+
394
414
  Overflow and length recovery follow Pi 0.87's `context_edit` semantics. Every request
395
415
  reads Pi's session projection, so earlier omissions and replacements (an elided tool
396
416
  output, an omitted reply) apply exactly as they do for Pi's own adapters. Before an
@@ -650,7 +670,7 @@ npm run pack:dry
650
670
  `mise run check` runs the linters, formatters, type checks, and the offline
651
671
  test suite; `mise run test` runs only the offline suite. Both tasks bind
652
672
  `PI_PACKAGE_DIR` to `node_modules/@earendil-works/pi-coding-agent`, so tests
653
- that load Pi in-process read this repository's Pi 0.99.1 resources even when a
673
+ that load Pi in-process read this repository's Pi 1.0.0 resources even when a
654
674
  global `PI_PACKAGE_DIR` points at another installation. Ordinary `pi` launches
655
675
  outside these tasks are unaffected. Offline tests also exercise
656
676
  `scripts/release.ts` with every child process mocked: they never run Git, npm,
@@ -668,11 +688,14 @@ mise run test:live:codex
668
688
  The task obtains the local Codex bearer token and runs the tests with
669
689
  `gpt-5.6-luna`, `gpt-6-sol`, `gpt-6-luna`, and `gpt-6.1-sol` at medium reasoning
670
690
  effort. It also packs the extension and loads that archive through the shipped Pi
671
- 0.99.1 CLI. Ordinary Responses and Responses Lite tests exercise tool calls,
691
+ 1.0.0 CLI. Ordinary Responses and Responses Lite tests exercise tool calls,
672
692
  prompt reload, native compaction, and persisted-session resume against Codex.
673
- All four models use standard reasoning mode. SDK tests verify Responses Lite
693
+ SDK tests verify Responses Lite
674
694
  WebSocket history, prewarming, continuation, and the built-in read tool.
675
695
  CLI tests also execute the built-in read tool after resume.
696
+ Six additional CLI cases replay grammar-tool history with `gpt-5.6-luna` in both
697
+ response formats. They cover foreign-provider history, different-model history,
698
+ and calls previously recorded as function calls.
676
699
  Test credentials stay in memory and child-process environments. Test sessions
677
700
  and configuration are isolated from the user's agent directory.
678
701
 
@@ -684,9 +707,9 @@ The packaged-CLI test selects its archive and executable as follows:
684
707
  existing regular file that `tar` can list, and an empty, missing, directory,
685
708
  or invalid value fails the test instead of falling back to a fresh pack.
686
709
  - `PI_CODEX_CLI_PATH` selects another Pi `cli.js`; the default is this
687
- repository's Pi 0.99.1 dependency. The test asserts that the selected
688
- executable reports version 0.99.1, the only Pi version the packaged CLI test
689
- is run against. The supported range is `>=0.99.1 <0.100.0`.
710
+ repository's Pi 1.0.0 dependency. The test asserts that the selected
711
+ executable reports version 1.0.0, the only Pi version the packaged CLI test
712
+ is run against. The supported range is `>=1.0.0 <1.1.0`.
690
713
  - Each Pi child process receives `PI_PACKAGE_DIR` bound to the selected
691
714
  executable's package directory, so the runtime under test reads its own
692
715
  metadata.
@@ -135,6 +135,13 @@ export default function registerApplyPatch(
135
135
  },
136
136
  executionMode: "sequential",
137
137
  renderShell: "self",
138
+ // Patches can overwrite and delete local files; they never reach the network.
139
+ annotations: {
140
+ readOnlyHint: false,
141
+ destructiveHint: true,
142
+ idempotentHint: false,
143
+ openWorldHint: false,
144
+ },
138
145
  async execute(toolCallId, params, signal, onUpdate, ctx) {
139
146
  const diagnosticsStartedAt = performance.now();
140
147
  let preparedDiagnostics: PreparedApplyPatchDiagnostics | undefined;
@@ -47,8 +47,6 @@ export type RuntimeScope = {
47
47
  export type RequestTemplate = {
48
48
  modelId: string;
49
49
  payload: JsonRecord;
50
- /** Fingerprint of the raw tool definitions that produced `payload.tools`. */
51
- toolFingerprint: string;
52
50
  grammarToolInputProperties: GrammarToolInputProperties;
53
51
  requestOptions: OpenAICodexResponsesOptions;
54
52
  };
@@ -6,7 +6,6 @@ import {
6
6
  clampThinkingLevel,
7
7
  createAssistantMessageEventStream,
8
8
  getCurrentTools,
9
- getDeclaredTools,
10
9
  type Api,
11
10
  type AssistantMessage,
12
11
  type AssistantMessageEventStream,
@@ -28,11 +27,12 @@ import {
28
27
  checkpointData,
29
28
  providerHistory,
30
29
  remoteCompactionMarkerSummary,
30
+ requestGrammarToolInputProperties,
31
31
  requestInstructions,
32
+ requestTools,
32
33
  resolveRequestTranscript,
33
34
  responsesCompatibility,
34
35
  searchCheckpoint,
35
- toolDefinitionFingerprint,
36
36
  type CheckpointData,
37
37
  type CompactionDecision,
38
38
  type GrammarToolInputProperties,
@@ -57,11 +57,18 @@ import {
57
57
  } from "../codex-metadata.ts";
58
58
  import { resolveCodexThreadIdentity, type CodexThreadIdentity } from "../codex-thread-lineage.ts";
59
59
  import { applyResponsesLite } from "../responses-lite.ts";
60
+ import {
61
+ REQUEST_TOOLS_ENTRY_TYPE,
62
+ requestToolsChanged,
63
+ requestToolsData,
64
+ type RequestToolsData,
65
+ } from "../request-tools-entry.ts";
60
66
  import { requiredValue } from "../required-value.ts";
61
67
  import { processCodexStream, type CodexStreamAttemptState } from "../codex-stream.ts";
62
68
  import {
63
69
  CodexTransport,
64
70
  CodexTurnState,
71
+ ProviderStreamEventCallbackError,
65
72
  WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE,
66
73
  isWebSocketConnectionLimitReachedError,
67
74
  validateCodexAuthentication,
@@ -84,11 +91,7 @@ import {
84
91
  type ResponsesInputItem,
85
92
  type ResponsesOutputItem,
86
93
  } from "../responses-item-schema.ts";
87
- import {
88
- convertResponsesTools,
89
- convertResponsesMessages,
90
- createGrammarToolInputProperties,
91
- } from "../vendor/pi-ai/openai-responses-serialization.ts";
94
+ import { convertResponsesMessages } from "../vendor/pi-ai/openai-responses-serialization.ts";
92
95
  import { errorFromThrown } from "../error-from-thrown.ts";
93
96
  import type {
94
97
  ActiveAgentTurn,
@@ -539,13 +542,12 @@ export class CodexProviderRuntime {
539
542
  grammarToolInputProperties,
540
543
  turnId,
541
544
  } = options;
542
- const compat = responsesCompatibility(model.compat);
543
545
  // Every request declares the complete current tool set at the top level,
544
546
  // as the official Codex client does. Replaying additions inside the input
545
547
  // made the Codex backend unreliable about which tools exist, and a native
546
548
  // checkpoint drops such history anyway. A mid-session tool change costs one
547
549
  // cache miss.
548
- const requestTools = getCurrentTools(context.messages);
550
+ const currentTools = getCurrentTools(context.messages);
549
551
  // The leading system message is the prompt; later system messages travel
550
552
  // inline in `input` (or collapsed into the leading message by
551
553
  // resolveRequestTranscript), as Pi AI's Codex adapter does.
@@ -573,14 +575,7 @@ export class CodexProviderRuntime {
573
575
  ),
574
576
  });
575
577
  if (requestOptions.serviceTier !== undefined) body.service_tier = requestOptions.serviceTier;
576
- if (requestTools.length > 0) {
577
- body.tools = convertResponsesTools(requestTools, {
578
- strict: false,
579
- supportsStrictMode: compat.supportsStrictMode ?? true,
580
- supportsOpenAIGrammarTools: compat.supportsOpenAIGrammarTools ?? false,
581
- namespacedToolNames: CODEX_NAMESPACED_TOOL_NAMES,
582
- });
583
- }
578
+ if (currentTools.length > 0) body.tools = requestTools(model, currentTools);
584
579
  if (requestOptions.reasoningEffort !== undefined) {
585
580
  const mapped =
586
581
  requestOptions.reasoningEffort === "none"
@@ -846,10 +841,7 @@ export class CodexProviderRuntime {
846
841
  requestOptions.signal,
847
842
  );
848
843
  }
849
- const grammarToolInputProperties = createGrammarToolInputProperties(
850
- getDeclaredTools(context.messages),
851
- responsesCompatibility(model.compat).supportsOpenAIGrammarTools ?? false,
852
- );
844
+ const grammarToolInputProperties = requestGrammarToolInputProperties(model, context.messages);
853
845
  let body = this.buildRequestBody({
854
846
  model,
855
847
  context,
@@ -868,15 +860,30 @@ export class CodexProviderRuntime {
868
860
  body = requireJsonRecord(transformed, "Codex payload replacement");
869
861
  }
870
862
  }
871
- if (runtimeSessionId) {
863
+ // Only agent turns define the prefix native compaction must match. Other
864
+ // requests with the session id, such as Pi's bug-report summary, run
865
+ // outside an agent run and must not replace the turn template or tools.
866
+ const agentRequest =
867
+ runtimeSessionId !== undefined && this.activeAgentTurns.has(runtimeSessionId);
868
+ if (runtimeSessionId && agentRequest) {
872
869
  this.templates.set(runtimeSessionId, {
873
870
  modelId: model.id,
874
871
  payload: replacementValues ? {} : withoutConversationInput(body),
875
- toolFingerprint: toolDefinitionFingerprint(getCurrentTools(context.messages)),
876
872
  grammarToolInputProperties,
877
873
  requestOptions: { ...requestOptions },
878
874
  });
879
875
  }
876
+ // Saved after the response, because an entry appended now would move the
877
+ // branch leaf that percentage compaction checks. A payload edit that
878
+ // removed `tools` sent no tools.
879
+ const sentTools =
880
+ agentRequest && !replacementValues
881
+ ? requestToolsData(
882
+ model.id,
883
+ Array.isArray(body.tools) ? body.tools.filter(isObject) : [],
884
+ grammarToolInputProperties,
885
+ )
886
+ : undefined;
880
887
  if (!replacementValues) {
881
888
  body = await this.maybeCompactPercentage(
882
889
  model,
@@ -937,6 +944,7 @@ export class CodexProviderRuntime {
937
944
  startEmitted = true;
938
945
  stream.push({ type: "start", partial: output });
939
946
  };
947
+ const onProviderStreamEvent = requestOptions.onProviderStreamEvent;
940
948
  const transportRequestOptions = {
941
949
  ...requestOptions,
942
950
  accountId,
@@ -950,6 +958,19 @@ export class CodexProviderRuntime {
950
958
  webSocketResponseHandle = handle;
951
959
  },
952
960
  onTransportStart: emitStart,
961
+ // Pi's `provider_stream_event` hook sees each event as the server sent
962
+ // it, before the transport filters or normalizes it.
963
+ ...(onProviderStreamEvent
964
+ ? {
965
+ async forwardRawEvent(event: JsonRecord) {
966
+ try {
967
+ await onProviderStreamEvent(event, model);
968
+ } catch (error) {
969
+ throw new ProviderStreamEventCallbackError(error);
970
+ }
971
+ },
972
+ }
973
+ : {}),
953
974
  onTransportDiagnostic(diagnostic: CodexTransportDiagnostic) {
954
975
  output.diagnostics = [
955
976
  ...(output.diagnostics ?? []),
@@ -1204,6 +1225,7 @@ export class CodexProviderRuntime {
1204
1225
  (item) =>
1205
1226
  item["type"] !== "function_call_output" && item["type"] !== "custom_tool_call_output",
1206
1227
  );
1228
+ if (runtimeSessionId && sentTools) this.saveRequestTools(runtimeSessionId, sentTools);
1207
1229
  const persistNativeItems =
1208
1230
  rawItems.length > 0 &&
1209
1231
  (responseRequests > 1 || nativeOverrideRequired(rawItems, canonicalItems));
@@ -1277,6 +1299,19 @@ export class CodexProviderRuntime {
1277
1299
  return this.templates.get(sessionId);
1278
1300
  }
1279
1301
 
1302
+ /**
1303
+ * Save the tools a turn request of the session sends when they differ from the
1304
+ * branch's latest saved tools for the model. Native compaction declares them,
1305
+ * because the transcript also lists tools Pi leaves out of requests.
1306
+ */
1307
+ private saveRequestTools(sessionId: string, data: RequestToolsData): void {
1308
+ const scope = this.scopes.get(sessionId);
1309
+ if (!scope) return;
1310
+ if (requestToolsChanged(scope.manager.getBranch(), data)) {
1311
+ this.pi.appendEntry(REQUEST_TOOLS_ENTRY_TYPE, data);
1312
+ }
1313
+ }
1314
+
1280
1315
  async compact(options: {
1281
1316
  model: Model<Api>;
1282
1317
  requestOptions: OpenAICodexResponsesOptions;
@@ -190,6 +190,12 @@ export type CodexTransportOptions = OpenAICodexResponsesOptions & {
190
190
  onWebSocketResponseHandle?: (handle: CodexWebSocketResponseHandle) => void;
191
191
  onTransportStart?: () => void;
192
192
  onTransportDiagnostic?: (diagnostic: CodexTransportDiagnostic) => void;
193
+ /**
194
+ * Receives each parsed server event before the transport filters or
195
+ * normalizes it. A rejection must be a `ProviderStreamEventCallbackError`,
196
+ * which ends the request without a retry or transport fallback.
197
+ */
198
+ forwardRawEvent?: (event: JsonRecord) => Promise<void>;
193
199
  warmup?: boolean;
194
200
  requestKind?: CodexRequestKind;
195
201
  turnState?: CodexTurnState;
@@ -102,11 +102,23 @@ export function thrownMessage(error: unknown): string {
102
102
  return error instanceof Error ? error.message || error.name : String(error);
103
103
  }
104
104
 
105
+ /** A failed `provider_stream_event` callback. It ends the turn and is never retried. */
106
+ export class ProviderStreamEventCallbackError extends Error {
107
+ constructor(cause: unknown) {
108
+ super(
109
+ `Provider stream event handler failed: ${cause instanceof Error ? cause.message : String(cause)}`,
110
+ { cause },
111
+ );
112
+ this.name = "ProviderStreamEventCallbackError";
113
+ }
114
+ }
115
+
105
116
  export function isCodexNonTransportError(error: unknown): boolean {
106
117
  return (
107
118
  error instanceof CodexApiError ||
108
119
  error instanceof CodexHttpError ||
109
- error instanceof CodexProtocolError
120
+ error instanceof CodexProtocolError ||
121
+ error instanceof ProviderStreamEventCallbackError
110
122
  );
111
123
  }
112
124
 
@@ -206,6 +206,7 @@ export async function* requestSse(
206
206
  options.onTransportStart?.();
207
207
  let terminal = false;
208
208
  for await (const event of parseSse(response, options.signal)) {
209
+ await options.forwardRawEvent?.(event);
209
210
  const normalized = normalizeEvent(event);
210
211
  if (!normalized) continue;
211
212
  yield normalized;
@@ -254,6 +254,7 @@ export async function* requestWebSocket(
254
254
  let responseId: string | undefined;
255
255
  let responseCompleted = false;
256
256
  for await (const event of parseWebSocket(acquired.socket, options.signal, timeoutMs)) {
257
+ await options.forwardRawEvent?.(event);
257
258
  captureTurnStateEvent(event, options.turnState);
258
259
  if (event.type === "response.metadata") continue;
259
260
  if (
@@ -95,6 +95,7 @@ export {
95
95
  resetOpenAICodexWebSocketDebugStats,
96
96
  } from "./codex-transport/codex-transport-websocket-pool.ts";
97
97
  export {
98
+ ProviderStreamEventCallbackError,
98
99
  WEBSOCKET_CONNECTION_LIMIT_REACHED_CODE,
99
100
  isWebSocketConnectionLimitReachedError,
100
101
  } from "./codex-transport/codex-transport-errors.ts";
@@ -1,9 +1,18 @@
1
+ import type { ToolAnnotations } from "@earendil-works/pi-coding-agent";
1
2
  import { Type } from "typebox";
2
3
 
3
4
  export const EXEC_COMMAND_TOOL_NAME = "exec_command";
4
5
  export const WRITE_STDIN_TOOL_NAME = "write_stdin";
5
6
  export const SHELL_COMMAND_TOOL_NAME = "shell_command";
6
7
 
8
+ /** Commands run arbitrary programs: they can overwrite or delete data and reach the network. */
9
+ export const COMMAND_TOOL_ANNOTATIONS = {
10
+ readOnlyHint: false,
11
+ destructiveHint: true,
12
+ idempotentHint: false,
13
+ openWorldHint: true,
14
+ } as const satisfies ToolAnnotations;
15
+
7
16
  export type CommandToolPromptMetadata = {
8
17
  description: string;
9
18
  promptSnippet: string;
@@ -24,6 +24,7 @@ import {
24
24
  type CommandCallRenderContext,
25
25
  } from "./command-render.ts";
26
26
  import {
27
+ COMMAND_TOOL_ANNOTATIONS,
27
28
  EXEC_COMMAND_PARAMETERS,
28
29
  EXEC_COMMAND_TOOL_NAME,
29
30
  execCommandPromptMetadata,
@@ -253,6 +254,7 @@ export default function registerCommandTools(
253
254
  pi.registerTool<typeof EXEC_COMMAND_PARAMETERS, CommandOutputDetails, ExecCommandRenderState>({
254
255
  name: EXEC_COMMAND_TOOL_NAME,
255
256
  label: EXEC_COMMAND_TOOL_NAME,
257
+ annotations: COMMAND_TOOL_ANNOTATIONS,
256
258
  description: execCommandPrompt.description,
257
259
  promptSnippet: execCommandPrompt.promptSnippet,
258
260
  promptGuidelines: execCommandPrompt.promptGuidelines,
@@ -289,6 +291,7 @@ export default function registerCommandTools(
289
291
  pi.registerTool<typeof WRITE_STDIN_PARAMETERS, CommandOutputDetails, WriteStdinRenderState>({
290
292
  name: WRITE_STDIN_TOOL_NAME,
291
293
  label: WRITE_STDIN_TOOL_NAME,
294
+ annotations: COMMAND_TOOL_ANNOTATIONS,
292
295
  description: writeStdinPrompt.description,
293
296
  promptSnippet: writeStdinPrompt.promptSnippet,
294
297
  promptGuidelines: writeStdinPrompt.promptGuidelines,
@@ -318,6 +321,7 @@ export default function registerCommandTools(
318
321
  pi.registerTool({
319
322
  name: SHELL_COMMAND_TOOL_NAME,
320
323
  label: SHELL_COMMAND_TOOL_NAME,
324
+ annotations: COMMAND_TOOL_ANNOTATIONS,
321
325
  description: shellCommandPrompt.description,
322
326
  promptSnippet: shellCommandPrompt.promptSnippet,
323
327
  promptGuidelines: shellCommandPrompt.promptGuidelines,