pi-openai-codex-compat 0.0.13 → 0.0.14

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,45 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.0.14 - 2026-09-22
6
+
7
+ ### Changed
8
+
9
+ - Require Pi `>=0.87.0 <0.88.0` and use Pi 0.87.0 as the development and
10
+ packaged-CLI integration baseline. The load-time host check now also
11
+ requires Pi 0.87's session projection API.
12
+ - Honour Pi 0.87 `context_edit` entries when building request history from
13
+ the branch, so omitted or replaced messages match what Pi's own adapters
14
+ send.
15
+ - Preserve mid-conversation prompt updates when filtering compaction summaries.
16
+ - Skip percentage compaction when nothing has been sampled yet. Pi 0.87
17
+ estimates the prompt and tool declarations before the first response, so a
18
+ low threshold could trigger an empty compaction request.
19
+ - Send Pi's leading system message as the prompt and later system messages as
20
+ inline developer items, exactly as Pi AI's Codex adapter does. Previously
21
+ every system message was replayed into one `instructions` string, so any
22
+ prompt-section change rewrote the stable prefix. Models without
23
+ mid-conversation system-message support still receive one collapsed prompt.
24
+ - Keep every developer and system message in the native compaction checkpoint,
25
+ in place and outside the 64k retained-context budget, so prompt updates
26
+ survive compaction. Only user messages are charged to the budget.
27
+
28
+ ### Fixed
29
+
30
+ - Keep the conversation in overflow-recovery compaction on Pi 0.87. Pi now
31
+ records the failed attempt's omission as a session entry whose parent is
32
+ that attempt; removing the attempt from the branch copy broke the entry
33
+ chain and the compaction request contained only the recovered prefix.
34
+ - Hold a system message that lands between a tool call and its results until
35
+ after those results in replayed history, matching Pi AI.
36
+ - Send the branch's leading system message as compaction `instructions`
37
+ instead of Pi's fully replayed prompt, so Pi-driven compaction shares the
38
+ turn requests' prompt prefix.
39
+ - Reuse the last turn's exact tool declarations for `/compact` and threshold
40
+ compaction while they still describe the active tools. Rebuilding them from
41
+ the tool registry lost Pi's strict schemas for built-in tools, so every
42
+ Pi-driven compaction request missed the turn requests' prompt-cache prefix.
43
+
5
44
  ## 0.0.13 - 2026-09-21
6
45
 
7
46
  ### Fixed
package/README.md CHANGED
@@ -22,7 +22,7 @@ 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.86.0 <0.87.0`
25
+ - Pi `>=0.87.0 <0.88.0`
26
26
  - An OpenAI Codex login in Pi
27
27
 
28
28
  Restart Pi after upgrading its runtime. `/reload` reloads extensions but cannot
@@ -67,7 +67,7 @@ The compatibility baseline is official Codex CLI `0.149.1`, released August 24,
67
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. |
68
68
  | 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
69
  | Model switching | This package rejects model switches while the active Pi branch contains a native Codex checkpoint because the checkpoint is model-specific. |
70
- | System instructions | Replay Pi's system-message sections into the complete current prompt. Responses Lite models prepend it as developer input after `additional_tools`; other models send it through Responses `instructions`. Do not replay older Pi system-prompt text alongside that current prompt. `/reload`, forced prompts, compaction snapshots, and resumed sessions use the current state without rewriting old checkpoints. |
70
+ | 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. |
71
71
  | Dynamic tool declarations | Send the complete current tool set, replayed from Pi's `toolsAdded` and `toolsRemoved` system messages, in the top-level `tools` field on every request, as the official Codex client does. Tool declarations never travel inside `input`: inline `additional_tools` items and synthetic tool-search pairs made the Codex backend unreliable about which tools exist. A mid-session tool change costs one prompt-cache miss, and native checkpoints need no declaration rebase. |
72
72
  | Turn metadata | Requests send a persisted installation id plus Pi-derived session, thread, context-window, turn, source, sandbox, request-kind, and nested compaction-operation metadata in `client_metadata` and compatible headers. The in-memory context-window number advances after successful compaction. One turn id is reused throughout a Pi agent run, while prewarm has its own id. First-party requests also carry Codex's model-and-tier routing hint. The provider captures the server-issued `x-codex-turn-state` once per agent run, replays it on WebSocket retries, SSE requests, and WebSocket-to-SSE fallback, and records all identity values in transport diagnostics. Pi does not reconstruct prior window number after extension reload/session resume or reproduce workspace Git/parent/subagent/Code Mode metadata. Each marked Pi tree branch receives its own persisted thread UUID. |
73
73
  | Cache preparation | Before the first cache-enabled WebSocket turn, the package prewarms only the stable instruction/tool prefix: ordinary Responses uses empty `input`, while Responses Lite uses `additional_tools` plus the developer instructions. The first generated request then contributes only dynamic conversation input to the continuation. No explicit prompt-cache breakpoints are added. |
@@ -76,7 +76,7 @@ The compatibility baseline is official Codex CLI `0.149.1`, released August 24,
76
76
  | Transport recovery | WebSocket failures before model-visible output receive five fresh-connection retries before sticky SSE fallback; SSE transport failures receive five retries. Official Codex `0.149.1` separately retries sampling connection-establishment failures indefinitely with delays capped at 60 seconds. The package remains bounded because Fetch does not expose Reqwest's narrower connection-error category, and a phase-only approximation could indefinitely repeat requests that reached the server. |
77
77
  | Compaction lifecycle events | Pre-turn percentage compaction writes through Pi's mutable session manager and cannot emit Pi's internal `session_compact` event through the public extension API. Mid-response percentage boundaries and manual, threshold, or overflow compactions initiated by Pi emit the normal lifecycle. |
78
78
  | Header hooks | An internal percentage-compaction request reuses the already transformed provider headers. It cannot independently rerun Pi's `before_provider_headers` hook. |
79
- | Native retained context | Deliberately differs from current Codex. The package retains recent user/developer/system messages under a text-only 64k budget before the opaque compaction item. Current Codex applies a second installed-history filter that drops developer/system wrappers and non-real-user messages, can retain eligible structured agent commentary, and trims oversized function outputs before compaction. Codex `0.149.1` also adds a default-disabled mode that charges retained images to the budget and keeps each image with adjacent harness labels as an atomic boundary unit. Pi keeps its existing checkpoint shape by design. |
79
+ | Native retained context | Deliberately differs from current Codex. The package retains recent user messages under a text-only 64k budget before the opaque compaction item, and keeps every developer and system message in place outside that budget so prompt updates are never compacted away. Current Codex applies a second installed-history filter that drops developer/system wrappers and non-real-user messages, can retain eligible structured agent commentary, and trims oversized function outputs before compaction. Codex `0.149.1` also adds a default-disabled mode that charges retained images to the budget and keeps each image with adjacent harness labels as an atomic boundary unit. Pi keeps its existing checkpoint shape by design. |
80
80
  | Tool namespaces | Responses Lite groups Pi's ordinary function/custom declarations into upstream's canonical `functions` namespace and maps that default namespace back to bare Pi names. Pi registers dotted names such as `web.run` as exact flat identifiers, so the provider converts only the fixed extension-owned allowlist into non-default Responses namespace/member identities and rejects unknown or ambiguously flat namespaced calls. |
81
81
  | Capability gating | Tool activation is based on the selected `openai-codex` provider plus package settings. It does not reproduce every official model-metadata, plan, feature-stage, executor, mode, or account gate. |
82
82
  | Sandbox and approvals | Pi extensions run with full process permissions. Command tools, `apply_patch`, local image reads, generated-image writes, and sibling Codex endpoints do not use Codex's sandbox, permission-profile, environment, or approval lifecycle. |
@@ -327,7 +327,8 @@ The extension handles native compaction for `openai-codex`. It follows the Codex
327
327
 
328
328
  1. Send normal Responses history followed by `{ "type": "compaction_trigger" }`.
329
329
  2. Validate the returned opaque `compaction` item.
330
- 3. Retain approximately 64,000 tokens of recent user, developer, and system context.
330
+ 3. Retain approximately 64,000 tokens of recent user messages, plus every developer and
331
+ system message outside that budget.
331
332
  4. Persist the opaque checkpoint in the Pi session and replay it on later requests.
332
333
 
333
334
  Ordinary responses and compaction use the same extension-managed SSE/WebSocket transport. Before the first WebSocket turn for a session and model, the provider performs a best-effort v2 `generate: false` prewarm of the static instruction/tool prefix, then generates the dynamic conversation input from its continuation. With `responsesLite: false`, supported models use the ordinary Responses envelope and receive their own ordinary-prefix prewarm.
@@ -587,7 +588,7 @@ mise run test:live:codex
587
588
  The task obtains the local Codex bearer token and runs the tests with
588
589
  `gpt-5.6-luna` at medium reasoning effort.
589
590
  It also packs the extension and loads that archive through the shipped Pi
590
- 0.86.1 CLI. Ordinary Responses and Responses Lite tests exercise tool calls,
591
+ 0.87.0 CLI. Ordinary Responses and Responses Lite tests exercise tool calls,
591
592
  prompt reload, native compaction, and persisted-session resume against Codex.
592
593
  Test credentials stay in memory and child-process environments. Test sessions
593
594
  and configuration are isolated from the user's agent directory.
@@ -276,17 +276,23 @@ function shortenMessage(
276
276
  : undefined;
277
277
  }
278
278
 
279
- function retainedRole(item: ResponsesInputItem): boolean {
280
- return (
281
- Value.Check(RESPONSES_MESSAGE_ITEM_SCHEMA, item) &&
282
- (item.role === "user" || item.role === "developer" || item.role === "system")
283
- );
279
+ function messageRole(item: ResponsesInputItem): string | undefined {
280
+ return Value.Check(RESPONSES_MESSAGE_ITEM_SCHEMA, item) ? item.role : undefined;
281
+ }
282
+
283
+ /** Developer and system messages carry prompt state, not conversation history. */
284
+ function isInstructionMessage(item: ResponsesInputItem): boolean {
285
+ const role = messageRole(item);
286
+ return role === "developer" || role === "system";
284
287
  }
285
288
 
286
289
  /**
287
- * Reproduce Codex remote-compaction-v2 history installation: select recent
288
- * user/developer/system messages from newest to oldest, then restore their
289
- * chronological order. The oldest selected message may be truncated.
290
+ * Reproduce Codex remote-compaction-v2 history installation for user messages:
291
+ * select recent user messages from newest to oldest under the budget, then
292
+ * restore chronological order. The oldest selected user message may be
293
+ * truncated. Developer and system messages are prompt updates rather than
294
+ * conversation history: every one of them is retained in place, outside the
295
+ * budget, and never truncated.
290
296
  */
291
297
  export function selectRetainedContext(
292
298
  history: readonly ResponsesInputItem[],
@@ -295,10 +301,14 @@ export function selectRetainedContext(
295
301
  let remaining = budget;
296
302
  const newestFirst: ResponsesInputItem[] = [];
297
303
 
298
- for (let index = history.length - 1; index >= 0 && remaining > 0; index--) {
304
+ for (let index = history.length - 1; index >= 0; index--) {
299
305
  const item = history[index];
300
306
  if (item === undefined) continue;
301
- if (!retainedRole(item)) continue;
307
+ if (isInstructionMessage(item)) {
308
+ newestFirst.push(structuredClone(item));
309
+ continue;
310
+ }
311
+ if (remaining <= 0 || messageRole(item) !== "user") continue;
302
312
 
303
313
  const tokens = Math.max(1, messageTextTokens(item));
304
314
  if (tokens <= remaining) {
@@ -1,4 +1,4 @@
1
- import type { SessionEntry, ToolInfo } from "@earendil-works/pi-coding-agent";
1
+ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
2
2
  import type { Api, Model } from "@earendil-works/pi-ai";
3
3
  import { providerHistory, type GrammarToolInputProperties } from "../compaction-checkpoint.ts";
4
4
  import type { ImageDetail } from "../config.ts";
@@ -37,7 +37,6 @@ export function splitUnsampledUserInput(options: {
37
37
  branch: readonly SessionEntry[];
38
38
  history: readonly ResponsesInputItem[];
39
39
  model: Model<Api>;
40
- allTools: readonly ToolInfo[];
41
40
  grammarToolInputProperties: GrammarToolInputProperties;
42
41
  imageDetail: ImageDetail;
43
42
  }):
@@ -56,7 +55,6 @@ export function splitUnsampledUserInput(options: {
56
55
  const encoded = providerHistory({
57
56
  branch: options.branch.slice(unsampledIndex),
58
57
  wireModel: options.model,
59
- allTools: options.allTools,
60
58
  grammarToolInputProperties: options.grammarToolInputProperties,
61
59
  imageDetail: options.imageDetail,
62
60
  });
@@ -5,10 +5,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
5
  import {
6
6
  clampThinkingLevel,
7
7
  createAssistantMessageEventStream,
8
- getCurrentSystemPrompt,
9
8
  getCurrentTools,
10
9
  getDeclaredTools,
11
- normalizeContext,
12
10
  type Api,
13
11
  type AssistantMessage,
14
12
  type AssistantMessageEventStream,
@@ -30,6 +28,8 @@ import {
30
28
  checkpointData,
31
29
  providerHistory,
32
30
  remoteCompactionMarkerSummary,
31
+ requestInstructions,
32
+ resolveRequestTranscript,
33
33
  responsesCompatibility,
34
34
  searchCheckpoint,
35
35
  type CheckpointData,
@@ -502,8 +502,7 @@ export class CodexProviderRuntime {
502
502
  const nativeItems = new Map<string, ResponsesOutputItem[]>();
503
503
  const serializationOptions: NonNullable<Parameters<typeof convertResponsesMessages>[3]> = {
504
504
  includeSystemPrompt: false,
505
- includeSystemUpdates: false,
506
- supportsMidConvoSystemMessages: true,
505
+ supportsMidConvoSystemMessages: compat.supportsMidConvoSystemMessages ?? false,
507
506
  supportsAdditionalTools: false,
508
507
  supportsToolSearch: false,
509
508
  grammarToolInputProperties,
@@ -524,7 +523,6 @@ export class CodexProviderRuntime {
524
523
  return providerHistory({
525
524
  branch: scope.manager.getBranch(),
526
525
  wireModel: model,
527
- allTools: this.pi.getAllTools(),
528
526
  grammarToolInputProperties,
529
527
  imageDetail: scope.config.imageDetail,
530
528
  });
@@ -547,11 +545,14 @@ export class CodexProviderRuntime {
547
545
  // checkpoint drops such history anyway. A mid-session tool change costs one
548
546
  // cache miss.
549
547
  const requestTools = getCurrentTools(context.messages);
548
+ // The leading system message is the prompt; later system messages travel
549
+ // inline in `input` (or collapsed into the leading message by
550
+ // resolveRequestTranscript), as Pi AI's Codex adapter does.
550
551
  let body: JsonRecord = {
551
552
  model: model.id,
552
553
  store: false,
553
554
  stream: true,
554
- instructions: getCurrentSystemPrompt(context.messages) || "You are a helpful assistant.",
555
+ instructions: requestInstructions(context) || "You are a helpful assistant.",
555
556
  input: this.wireHistory(model, context, grammarToolInputProperties, runtimeSessionId),
556
557
  text: { verbosity: requestOptions.textVerbosity ?? "low" },
557
558
  include: ["reasoning.encrypted_content"],
@@ -735,7 +736,6 @@ export class CodexProviderRuntime {
735
736
  branch,
736
737
  history,
737
738
  model,
738
- allTools: this.pi.getAllTools(),
739
739
  grammarToolInputProperties,
740
740
  imageDetail: scope.config.imageDetail,
741
741
  });
@@ -746,6 +746,10 @@ export class CodexProviderRuntime {
746
746
  );
747
747
  return body;
748
748
  }
749
+ // Pi 0.87 estimates the prompt and tool declarations before any response
750
+ // usage exists, so a low threshold can be reached before anything was
751
+ // sampled. There is nothing to compact until the first response.
752
+ if (split.history.length === 0) return body;
749
753
 
750
754
  const compacted = await this.performCompaction({
751
755
  model,
@@ -755,7 +759,7 @@ export class CodexProviderRuntime {
755
759
  template: withoutConversationInput(body),
756
760
  instructions: isString(body.instructions)
757
761
  ? body.instructions
758
- : getCurrentSystemPrompt(context.messages) || "You are a helpful assistant.",
762
+ : requestInstructions(context) || "You are a helpful assistant.",
759
763
  grammarToolInputProperties,
760
764
  priority: scope.config.fastMode,
761
765
  compactionMetadata: responsesCompactionV2Metadata("auto", "context_limit", "pre_turn"),
@@ -797,7 +801,7 @@ export class CodexProviderRuntime {
797
801
  input: Context,
798
802
  options?: OpenAICodexResponsesOptions,
799
803
  ): AssistantMessageEventStream {
800
- const context = normalizeContext(input);
804
+ const context = resolveRequestTranscript(model, input);
801
805
  const stream = createAssistantMessageEventStream();
802
806
  const requestOptions = transportOptions(options);
803
807
  const output: AssistantMessage = {
@@ -2,12 +2,24 @@ import { isBoolean, isString } from "./value-contracts.ts";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import {
4
4
  buildSessionContext,
5
+ buildSessionProjection,
5
6
  convertToLlm,
6
7
  sessionEntryToContextMessages,
7
8
  type SessionEntry,
8
9
  type ToolInfo,
9
10
  } from "@earendil-works/pi-coding-agent";
10
- import type { Api, Message, Model, SystemMessage } from "@earendil-works/pi-ai";
11
+ import {
12
+ getInitialSystemMessage,
13
+ getSystemMessageText,
14
+ normalizeContext,
15
+ resolveTranscript,
16
+ type Api,
17
+ type Context,
18
+ type Message,
19
+ type Model,
20
+ type SystemMessage,
21
+ type TranscriptContext,
22
+ } from "@earendil-works/pi-ai";
11
23
  import { Value } from "typebox/value";
12
24
  import { APPLY_PATCH_LARK_GRAMMAR, APPLY_PATCH_TOOL_NAME } from "./apply-patch.ts";
13
25
  import { CODEX_TOOL_CALL_PROVIDERS } from "./codex-identifiers.ts";
@@ -34,6 +46,8 @@ import {
34
46
  import type { ResponsesToolDefinition } from "./responses-tool-schema.ts";
35
47
  import { convertResponsesMessages } from "./vendor/pi-ai/openai-responses-serialization.ts";
36
48
 
49
+ type AgentMessages = ReturnType<typeof sessionEntryToContextMessages>;
50
+
37
51
  export const CHECKPOINT_ENTRY_TYPE = "openai-codex-compat-remote-compaction";
38
52
  export const CHECKPOINT_FORMAT_VERSION = 1;
39
53
 
@@ -58,17 +72,16 @@ export type CheckpointSearch =
58
72
  export type GrammarToolInputProperties = ReadonlyMap<string, string>;
59
73
 
60
74
  export interface ResponsesCompatibility {
61
- supportsAdditionalTools?: boolean;
75
+ supportsMidConvoSystemMessages?: boolean;
62
76
  supportsOpenAIGrammarTools?: boolean;
63
77
  supportsStrictMode?: boolean;
64
- supportsToolSearch?: boolean;
65
78
  }
66
79
 
67
80
  export function responsesCompatibility(value: unknown): ResponsesCompatibility {
68
81
  const compatibility: ResponsesCompatibility = {};
69
82
  if (!isObject(value)) return compatibility;
70
- if (isBoolean(value["supportsAdditionalTools"])) {
71
- compatibility.supportsAdditionalTools = value["supportsAdditionalTools"];
83
+ if (isBoolean(value["supportsMidConvoSystemMessages"])) {
84
+ compatibility.supportsMidConvoSystemMessages = value["supportsMidConvoSystemMessages"];
72
85
  }
73
86
  if (isBoolean(value["supportsOpenAIGrammarTools"])) {
74
87
  compatibility.supportsOpenAIGrammarTools = value["supportsOpenAIGrammarTools"];
@@ -76,12 +89,37 @@ export function responsesCompatibility(value: unknown): ResponsesCompatibility {
76
89
  if (isBoolean(value["supportsStrictMode"])) {
77
90
  compatibility.supportsStrictMode = value["supportsStrictMode"];
78
91
  }
79
- if (isBoolean(value["supportsToolSearch"])) {
80
- compatibility.supportsToolSearch = value["supportsToolSearch"];
81
- }
82
92
  return compatibility;
83
93
  }
84
94
 
95
+ /**
96
+ * Resolve Pi's transcript the way Pi AI's Codex adapter does before building a
97
+ * request: models that accept mid-conversation system messages keep later
98
+ * system messages in place, and every other model collapses them into the
99
+ * leading system message.
100
+ */
101
+ export function resolveRequestTranscript(model: Model<Api>, context: Context): TranscriptContext {
102
+ return resolveTranscript(
103
+ normalizeContext(context),
104
+ responsesCompatibility(model.compat).supportsMidConvoSystemMessages ?? false,
105
+ );
106
+ }
107
+
108
+ /** The leading system message of a resolved transcript, rendered as Responses `instructions`. */
109
+ export function requestInstructions(context: TranscriptContext): string {
110
+ const initial = getInitialSystemMessage(context.messages);
111
+ return initial ? getSystemMessageText(initial) : "";
112
+ }
113
+
114
+ /** `instructions` for a request built from a session branch rather than Pi's live transcript. */
115
+ export function branchInstructions(model: Model<Api>, branch: readonly SessionEntry[]): string {
116
+ return requestInstructions(
117
+ resolveRequestTranscript(model, {
118
+ messages: convertToLlm(buildSessionContext([...branch]).messages),
119
+ }),
120
+ );
121
+ }
122
+
85
123
  function responsesToolParameters(tool: ToolInfo): JsonRecord {
86
124
  if (!isObject(tool.parameters)) {
87
125
  throw new Error(`Tool ${tool.name} must have JSON object parameters.`);
@@ -143,6 +181,43 @@ export function activeResponsesTools(
143
181
  : undefined;
144
182
  }
145
183
 
184
+ /**
185
+ * Whether a serialized Responses `tools` list declares exactly the active tools,
186
+ * by name and description. Namespace members count as `namespace.member`.
187
+ *
188
+ * A cached turn template carries the exact declarations the turn requests sent,
189
+ * including strict schema conversion that `ToolInfo` cannot reproduce. Reusing
190
+ * it keeps the compaction request on the turn requests' prompt-cache prefix.
191
+ */
192
+ export function declaresActiveTools(
193
+ serialized: readonly JsonRecord[],
194
+ allTools: readonly ToolInfo[],
195
+ activeNames: readonly string[],
196
+ ): boolean {
197
+ const declared = new Map<string, string | undefined>();
198
+ for (const item of serialized) {
199
+ if (item.type === "namespace" && isString(item.name) && Array.isArray(item.tools)) {
200
+ for (const member of item.tools) {
201
+ if (!isObject(member) || !isString(member.name)) return false;
202
+ declared.set(
203
+ `${item.name}.${member.name}`,
204
+ isString(member["description"]) ? member["description"] : undefined,
205
+ );
206
+ }
207
+ } else if ((item.type === "function" || item.type === "custom") && isString(item.name)) {
208
+ declared.set(item.name, isString(item["description"]) ? item["description"] : undefined);
209
+ } else {
210
+ return false;
211
+ }
212
+ }
213
+ const enabled = new Set(activeNames);
214
+ const active = allTools.filter((tool) => enabled.has(tool.name));
215
+ return (
216
+ declared.size === active.length &&
217
+ active.every((tool) => declared.has(tool.name) && declared.get(tool.name) === tool.description)
218
+ );
219
+ }
220
+
146
221
  export function remoteCompactionMarkerSummary(): string {
147
222
  return `OpenAI Codex remote compaction checkpoint (${randomUUID()}).`;
148
223
  }
@@ -150,7 +225,6 @@ export function remoteCompactionMarkerSummary(): string {
150
225
  type EncodeMessagesOptions = {
151
226
  model: Model<Api>;
152
227
  messages: Message[];
153
- allTools: readonly ToolInfo[];
154
228
  grammarToolInputProperties: GrammarToolInputProperties;
155
229
  imageDetail: ImageDetail;
156
230
  nativeAssistantItems?: ReadonlyMap<string, readonly ResponsesOutputItem[]>;
@@ -159,16 +233,22 @@ type EncodeMessagesOptions = {
159
233
  export type EncodeSessionEntriesOptions = {
160
234
  model: Model<Api>;
161
235
  entries: readonly SessionEntry[];
162
- allTools: readonly ToolInfo[];
163
236
  grammarToolInputProperties?: GrammarToolInputProperties;
164
237
  imageDetail?: ImageDetail;
165
238
  nativeAssistantItems?: ReadonlyMap<string, readonly ResponsesOutputItem[]>;
166
239
  initialSystemMessage?: SystemMessage;
240
+ /** Model-visible messages per entry. Defaults to the entry's raw messages. */
241
+ entryMessages?: (entry: SessionEntry) => AgentMessages;
167
242
  };
168
243
 
169
244
  /**
170
245
  * Encode Pi's canonical messages using Pi AI's OpenAI Responses serializer.
171
246
  *
247
+ * The leading system message travels as `instructions`, so it is excluded here.
248
+ * Later system messages become inline developer items on models that accept
249
+ * mid-conversation system messages and collapse into the leading message
250
+ * otherwise, exactly as Pi AI's Codex adapter does.
251
+ *
172
252
  * Tool declarations never enter the input: every request carries the complete
173
253
  * current tool set at the top level, as the official Codex client does. Inline
174
254
  * `additional_tools` items and synthetic tool-search pairs made the Codex
@@ -180,8 +260,7 @@ function encodeMessages(options: EncodeMessagesOptions): ResponsesInputItem[] {
180
260
  const compat = responsesCompatibility(model.compat);
181
261
  const serializationOptions: NonNullable<Parameters<typeof convertResponsesMessages>[3]> = {
182
262
  includeSystemPrompt: false,
183
- includeSystemUpdates: false,
184
- supportsMidConvoSystemMessages: true,
263
+ supportsMidConvoSystemMessages: compat.supportsMidConvoSystemMessages ?? false,
185
264
  supportsAdditionalTools: false,
186
265
  supportsToolSearch: false,
187
266
  grammarToolInputProperties,
@@ -206,21 +285,21 @@ export function encodeSessionEntries(options: EncodeSessionEntriesOptions): Resp
206
285
  const {
207
286
  model,
208
287
  entries,
209
- allTools,
210
288
  grammarToolInputProperties = new Map(),
211
289
  imageDetail = "auto",
212
290
  nativeAssistantItems,
291
+ entryMessages = sessionEntryToContextMessages,
213
292
  } = options;
214
- const messages = entries.flatMap((entry) => sessionEntryToContextMessages(entry));
293
+ const messages = entries.flatMap((entry) => entryMessages(entry));
215
294
  const encodeOptions: EncodeMessagesOptions = {
216
295
  model,
217
296
  // A partial tail can start with a system update. Seed its prior state so
218
- // the serializer does not mistake that update for the leading declaration.
297
+ // the serializer renders that update inline instead of dropping it as the
298
+ // leading system message that `instructions` already carries.
219
299
  messages: [
220
300
  options.initialSystemMessage ?? { role: "system", content: "", timestamp: 0 },
221
301
  ...convertToLlm(messages),
222
302
  ],
223
- allTools,
224
303
  grammarToolInputProperties,
225
304
  imageDetail,
226
305
  };
@@ -339,11 +418,19 @@ export function checkpointData(
339
418
  /**
340
419
  * Materialize provider history for the active branch. Checkpoint history
341
420
  * replaces everything before its entry; later branch entries form the tail.
421
+ *
422
+ * Ordinary requests read the session projection, so `context_edit` omissions
423
+ * and replacements apply exactly as they do for Pi's own adapters. Recovery
424
+ * compaction reads the raw entries instead: Pi 0.87 omits the failed or
425
+ * truncated attempt through a `context_edit` child of that entry before it
426
+ * runs `session_before_compact`. The serializer already skips error and
427
+ * aborted assistants, and a truncated boundary is committed progress that the
428
+ * checkpoint must keep. Entries are never removed from the branch copy because
429
+ * the omission entry's parent chain would break and the history would vanish.
342
430
  */
343
431
  export function providerHistory(options: {
344
432
  branch: readonly SessionEntry[];
345
433
  wireModel: Model<Api>;
346
- allTools: readonly ToolInfo[];
347
434
  grammarToolInputProperties?: GrammarToolInputProperties;
348
435
  imageDetail?: ImageDetail;
349
436
  recoverLatestOverflowPrefix?: boolean;
@@ -351,27 +438,32 @@ export function providerHistory(options: {
351
438
  const branch = [...options.branch];
352
439
  let recoveredPrefix: ResponsesInputItem[] = [];
353
440
  if (options.recoverLatestOverflowPrefix) {
354
- const index = branch.findLastIndex(
355
- (entry) => entry.type === "message" && entry.message.role === "assistant",
441
+ const entry = branch.findLast(
442
+ (candidate) => candidate.type === "message" && candidate.message.role === "assistant",
356
443
  );
357
- const entry = index >= 0 ? branch[index] : undefined;
358
444
  if (
359
445
  entry?.type === "message" &&
360
446
  entry.message.role === "assistant" &&
361
- (entry.message.stopReason === "error" || entry.message.stopReason === "aborted")
447
+ entry.message.stopReason === "error" &&
448
+ entry.message.responseId
362
449
  ) {
363
- if (entry.message.stopReason === "error" && entry.message.responseId) {
364
- recoveredPrefix =
365
- nativeCommittedPrefixBeforeOverflow(
366
- branch,
367
- options.wireModel.id,
368
- entry.message.responseId,
369
- ) ?? [];
370
- }
371
- branch.splice(index, 1);
450
+ recoveredPrefix =
451
+ nativeCommittedPrefixBeforeOverflow(
452
+ branch,
453
+ options.wireModel.id,
454
+ entry.message.responseId,
455
+ ) ?? [];
372
456
  }
373
457
  }
374
458
 
459
+ const projection = buildSessionProjection(branch);
460
+ const projected = new Map<string, AgentMessages>();
461
+ for (const entry of projection.entries) projected.set(entry.sourceEntry.id, entry.messages);
462
+ const entryMessages = (entry: SessionEntry): AgentMessages =>
463
+ options.recoverLatestOverflowPrefix
464
+ ? sessionEntryToContextMessages(entry)
465
+ : (projected.get(entry.id) ?? []);
466
+
375
467
  const checkpoint = searchCheckpoint(branch);
376
468
  const nativeAssistantItems = nativeResponseOverrides(branch, options.wireModel.id);
377
469
  if (checkpoint.kind === "corrupt") {
@@ -391,22 +483,22 @@ export function providerHistory(options: {
391
483
  ...encodeSessionEntries({
392
484
  model: options.wireModel,
393
485
  entries: branch.slice(checkpoint.entryIndex + 1),
394
- allTools: options.allTools,
395
486
  grammarToolInputProperties: options.grammarToolInputProperties ?? new Map(),
396
487
  imageDetail: options.imageDetail ?? "auto",
397
488
  nativeAssistantItems,
489
+ entryMessages,
398
490
  ...(initialSystemMessage ? { initialSystemMessage } : {}),
399
491
  }),
400
492
  ...recoveredPrefix,
401
493
  ];
402
494
  }
403
495
 
404
- const context = buildSessionContext(branch);
405
496
  return [
406
497
  ...encodeMessages({
407
498
  model: options.wireModel,
408
- messages: convertToLlm(context.messages),
409
- allTools: options.allTools,
499
+ messages: convertToLlm(
500
+ projection.entries.flatMap((entry) => entryMessages(entry.sourceEntry)),
501
+ ),
410
502
  grammarToolInputProperties: options.grammarToolInputProperties ?? new Map(),
411
503
  imageDetail: options.imageDetail ?? "auto",
412
504
  nativeAssistantItems,
@@ -306,7 +306,7 @@ export default function registerImageGeneration(
306
306
  },
307
307
  });
308
308
  const config = resolveConfig(ctx);
309
- const history = codexToolHistory(pi, ctx, model, config.imageDetail);
309
+ const history = codexToolHistory(ctx, model, config.imageDetail);
310
310
  const request = await imageRequest(params, history);
311
311
  const authentication = await codexToolAuthentication(ctx, model);
312
312
  const callId = toolCallId.split("|")[0] || toolCallId;
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import * as piAi from "@earendil-works/pi-ai";
3
+ import * as piCodingAgent from "@earendil-works/pi-coding-agent";
3
4
  import { requirePiTranscriptRuntime } from "./pi-runtime.ts";
4
5
  import { DEFAULT_CONFIG, loadConfig, type CodexCompatConfig } from "./config.ts";
5
6
  import type { ConfigContext } from "./config-context.ts";
@@ -23,7 +24,7 @@ export {
23
24
  } from "./codex-transport.ts";
24
25
 
25
26
  export default function registerOpenAICodexCompat(pi: ExtensionAPI): void {
26
- requirePiTranscriptRuntime(piAi);
27
+ requirePiTranscriptRuntime(piAi, piCodingAgent);
27
28
  let activeConfig: CodexCompatConfig | undefined;
28
29
  const resolveConfig = (ctx: ConfigContext): CodexCompatConfig => {
29
30
  activeConfig ??= loadConfig(ctx.cwd, ctx.isProjectTrusted());
@@ -1,19 +1,45 @@
1
- const TRANSCRIPT_APIS = [
2
- "normalizeContext",
3
- "getCurrentSystemPrompt",
1
+ /**
2
+ * Every Pi 0.86 transcript API this extension imports as a value from
3
+ * `@earendil-works/pi-ai`. Pi's extension loader turns a missing named import
4
+ * into `undefined` rather than a load error, so an older host fails at first use
5
+ * unless the surface is checked up front. Keep this list equal to the value
6
+ * imports in `codex-provider/codex-provider-runtime.ts`, `compaction-checkpoint.ts`,
7
+ * and `vendor/pi-ai/openai-responses-serialization.ts`.
8
+ */
9
+ export const TRANSCRIPT_APIS = [
4
10
  "getCurrentTools",
5
11
  "getDeclaredTools",
6
12
  "getInitialSystemMessage",
13
+ "getSystemMessageText",
14
+ "normalizeContext",
15
+ "renderSystemMessageUpdate",
16
+ "resolveTranscript",
17
+ "resolveTranscriptTools",
7
18
  ] as const;
8
19
 
20
+ /**
21
+ * Every Pi 0.87 session API this extension imports as a value from
22
+ * `@earendil-works/pi-coding-agent`. Keep this list equal to the value imports
23
+ * in `compaction-checkpoint.ts`.
24
+ */
25
+ export const SESSION_APIS = ["buildSessionProjection"] as const;
26
+
27
+ export const REQUIRED_PI = "Pi 0.87.x";
28
+
9
29
  /** Check the loaded host, not package metadata that PI_PACKAGE_DIR can override. */
10
- export function requirePiTranscriptRuntime(api: Record<string, unknown>): void {
11
- const missing = TRANSCRIPT_APIS.filter((name) => typeof api[name] !== "function");
30
+ export function requirePiTranscriptRuntime(
31
+ transcriptApi: Record<string, unknown>,
32
+ sessionApi: Record<string, unknown>,
33
+ ): void {
34
+ const missing = [
35
+ ...TRANSCRIPT_APIS.filter((name) => typeof transcriptApi[name] !== "function"),
36
+ ...SESSION_APIS.filter((name) => typeof sessionApi[name] !== "function"),
37
+ ];
12
38
  if (missing.length > 0) {
13
39
  throw new Error(
14
- "pi-openai-codex-compat requires a running Pi 0.86.x runtime. " +
40
+ `pi-openai-codex-compat requires a running ${REQUIRED_PI} runtime. ` +
15
41
  `Missing host APIs: ${missing.join(", ")}. ` +
16
- "Exit Pi and start Pi 0.86.x after updating. /reload cannot upgrade the running " +
42
+ `Exit Pi and start ${REQUIRED_PI} after updating. /reload cannot upgrade the running ` +
17
43
  "runtime, and PI_PACKAGE_DIR can make an older executable report a newer version.",
18
44
  );
19
45
  }
@@ -2,7 +2,7 @@ import { isString } from "./value-contracts.ts";
2
2
  import type {
3
3
  BeforeProviderHeadersEvent,
4
4
  CompactionResult,
5
- ContextEvent,
5
+ ContextWithSystemEvent,
6
6
  ExtensionAPI,
7
7
  ExtensionContext,
8
8
  SessionBeforeCompactEvent,
@@ -18,6 +18,8 @@ import { addRemoteCompactionFeature, isObject, type JsonRecord } from "./codex-p
18
18
  import { CODEX_API, CODEX_PROVIDER } from "./codex-identifiers.ts";
19
19
  import {
20
20
  activeResponsesTools,
21
+ branchInstructions,
22
+ declaresActiveTools,
21
23
  providerHistory,
22
24
  remoteCompactionMarkerSummary,
23
25
  responsesCompatibility,
@@ -50,9 +52,9 @@ export type RemoteCompactionLifecycleHandler = (
50
52
  ) => void;
51
53
 
52
54
  export type RemoteCompactionContextHandler = (
53
- event: Pick<ContextEvent, "messages">,
55
+ event: Pick<ContextWithSystemEvent, "messages">,
54
56
  ctx: RemoteCompactionContext,
55
- ) => { messages: ContextEvent["messages"] } | undefined;
57
+ ) => { messages: ContextWithSystemEvent["messages"] } | undefined;
56
58
 
57
59
  export type RemoteCompactionHeadersHandler = (
58
60
  event: Pick<BeforeProviderHeadersEvent, "headers">,
@@ -114,7 +116,12 @@ export function remoteCompactionApi(pi: ExtensionAPI): RemoteCompactionApi {
114
116
  getAllTools: () => pi.getAllTools(),
115
117
  onBeforeProviderHeaders: (handler) =>
116
118
  pi.on("before_provider_headers", (event, ctx) => handler(event, context(ctx))),
117
- onContext: (handler) => pi.on("context", (event, ctx) => handler(event, context(ctx))),
119
+ // The full-transcript event returns messages verbatim. A changed `context`
120
+ // result would fold every later system message into the leading one, so
121
+ // `instructions` would no longer be the branch's leading prompt and the
122
+ // inline developer items in `input` would repeat the folded updates.
123
+ onContext: (handler) =>
124
+ pi.on("context_with_system", (event, ctx) => handler(event, context(ctx))),
118
125
  onSessionBeforeCompact: (handler) =>
119
126
  pi.on("session_before_compact", (event, ctx) => handler(event, context(ctx))),
120
127
  onSessionShutdown: (handler) =>
@@ -245,15 +252,23 @@ export default function registerRemoteCompaction(
245
252
  const history = providerHistory({
246
253
  branch: event.branchEntries,
247
254
  wireModel: ctx.model,
248
- allTools,
249
255
  grammarToolInputProperties,
250
256
  imageDetail: config.imageDetail,
251
257
  recoverLatestOverflowPrefix: event.reason === "overflow" && event.willRetry,
252
258
  });
253
- // Compaction rebases history, so use the complete current tool set.
259
+ // Reuse the last turn's exact declarations when they still describe the
260
+ // active tools, so the compaction request shares the turn requests'
261
+ // prompt-cache prefix. Rebuild from the registry only when the active
262
+ // set changed since that turn or no turn template exists.
254
263
  const template: JsonRecord = { ...matching?.payload };
264
+ const activeNames = pi.getActiveTools();
265
+ const cachedTools = Array.isArray(matching?.payload.tools)
266
+ ? matching.payload.tools.filter(isObject)
267
+ : undefined;
255
268
  template.tools =
256
- activeResponsesTools(allTools, pi.getActiveTools(), grammarToolInputProperties) ?? [];
269
+ cachedTools && declaresActiveTools(cachedTools, allTools, activeNames)
270
+ ? cachedTools
271
+ : (activeResponsesTools(allTools, activeNames, grammarToolInputProperties) ?? []);
257
272
  const requestOptions: OpenAICodexResponsesOptions = {
258
273
  ...matching?.requestOptions,
259
274
  apiKey: authentication.apiKey,
@@ -265,7 +280,14 @@ export default function registerRemoteCompaction(
265
280
  model: ctx.model,
266
281
  requestOptions,
267
282
  history,
268
- instructions: instructionsForCompaction(ctx.getSystemPrompt(), event.customInstructions),
283
+ // Turn requests send the branch's leading system message as `instructions`
284
+ // and carry later system messages inline in the history. A branch without
285
+ // a leading system message (a pre-0.86 session) falls back to Pi's
286
+ // current prompt so the compaction still sees instructions.
287
+ instructions: instructionsForCompaction(
288
+ branchInstructions(ctx.model, event.branchEntries) || ctx.getSystemPrompt(),
289
+ event.customInstructions,
290
+ ),
269
291
  grammarToolInputProperties:
270
292
  matching?.grammarToolInputProperties ??
271
293
  requestGrammarToolInputProperties(template, allTools),
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
3
3
  import { providerHistory } from "./compaction-checkpoint.ts";
4
4
  import type { ImageDetail } from "./config.ts";
@@ -12,7 +12,6 @@ export interface CodexToolAuthentication {
12
12
  export type ToolAuthenticationContext = {
13
13
  modelRegistry: Pick<ExtensionContext["modelRegistry"], "getApiKeyAndHeaders">;
14
14
  };
15
- export type ToolHistoryApi = Pick<ExtensionAPI, "getAllTools">;
16
15
  export type ToolHistoryContext = {
17
16
  sessionManager: Pick<ExtensionContext["sessionManager"], "getBranch">;
18
17
  };
@@ -32,7 +31,6 @@ export async function codexToolAuthentication(
32
31
  }
33
32
 
34
33
  export function codexToolHistory(
35
- pi: ToolHistoryApi,
36
34
  ctx: ToolHistoryContext,
37
35
  model: Model<Api>,
38
36
  imageDetail: ImageDetail = "auto",
@@ -40,7 +38,6 @@ export function codexToolHistory(
40
38
  return providerHistory({
41
39
  branch: ctx.sessionManager.getBranch(),
42
40
  wireModel: model,
43
- allTools: pi.getAllTools(),
44
41
  imageDetail,
45
42
  });
46
43
  }
@@ -8,14 +8,15 @@ Pi AI's transcript helpers replay system sections and tool declarations.
8
8
  `supportsAdditionalTools` and `supportsToolSearch` select the dynamic tool
9
9
  history representation for additive `toolsAdded` system messages:
10
10
  message-anchored `additional_tools` or a `tool_search_call`/`tool_search_output`
11
- pair. Tool removal, redefinition, or a model without either capability requires
12
- the complete current top-level tool list.
13
-
14
- Compat uses the extension-owned `includeSystemUpdates: false` option alongside
15
- `includeSystemPrompt: false`. It sends the complete current prompt separately
16
- and preserves only supported tool additions in system-message positions.
17
- This keeps old prompt text from overriding current instructions after
18
- compaction, forced-prompt projection, reload, or resume.
11
+ pair. The copy keeps both options so the wire-equivalence tests hold, but Compat
12
+ always passes `false`: it declares every tool at the top level.
13
+
14
+ System messages follow Pi AI exactly. Compat passes `includeSystemPrompt: false`
15
+ because the leading system message travels as `instructions`, and forwards the
16
+ model's `supportsMidConvoSystemMessages` flag so later system messages render
17
+ inline through `renderSystemMessageUpdate` or collapse into the leading message.
18
+ A system message that lands between a tool call and its results is held until
19
+ after those results, as upstream `transformMessages` does.
19
20
 
20
21
  The optional `namespacedToolNames`, `textContentItemToolResultNames`, and `toolResultImageDetail` paths are extension-owned additions. Without those options, serialization must continue to match Pi AI. `namespacedToolNames` groups only the fixed Codex allowlist into native Responses namespaces and replays namespace/member call identities. `textContentItemToolResultNames` preserves Codex tools such as `web.run` whose successful text output is transported as an `input_text` content-item array instead of Pi's usual plain string. `toolResultImageDetail` overrides the otherwise canonical `auto` detail used for image tool-result content.
21
22
 
@@ -53,8 +53,6 @@ type ToolResultOutput =
53
53
 
54
54
  type ConvertResponsesMessagesOptions = {
55
55
  includeSystemPrompt?: boolean;
56
- /** Compat sends the complete current prompt outside replay history. */
57
- includeSystemUpdates?: boolean;
58
56
  grammarToolInputProperties?: ReadonlyMap<string, string>;
59
57
  supportsMidConvoSystemMessages?: boolean;
60
58
  supportsAdditionalTools?: boolean;
@@ -551,25 +549,33 @@ function transformMessages(
551
549
  const result: Message[] = [];
552
550
  let pendingToolCalls: ToolCall[] = [];
553
551
  let existingToolResultIds = new Set<string>();
554
- const insertSyntheticToolResults = () => {
555
- for (const toolCall of pendingToolCalls) {
556
- if (existingToolResultIds.has(toolCall.id)) continue;
557
- result.push({
558
- role: "toolResult",
559
- toolCallId: toolCall.id,
560
- toolName: toolCall.name,
561
- content: [{ type: "text", text: "No result provided" }],
562
- isError: true,
563
- timestamp: Date.now(),
564
- } satisfies ToolResultMessage);
552
+ // System messages are transparent to tool-call accounting: one that lands between a
553
+ // tool call and its results is held back and emitted after the results (synthetic
554
+ // ones included), so it never causes a duplicate result for a call answered later.
555
+ const heldSystemMessages: Message[] = [];
556
+ const closePendingToolCalls = () => {
557
+ if (pendingToolCalls.length > 0) {
558
+ for (const toolCall of pendingToolCalls) {
559
+ if (existingToolResultIds.has(toolCall.id)) continue;
560
+ result.push({
561
+ role: "toolResult",
562
+ toolCallId: toolCall.id,
563
+ toolName: toolCall.name,
564
+ content: [{ type: "text", text: "No result provided" }],
565
+ isError: true,
566
+ timestamp: Date.now(),
567
+ } satisfies ToolResultMessage);
568
+ }
569
+ pendingToolCalls = [];
570
+ existingToolResultIds = new Set();
565
571
  }
566
- pendingToolCalls = [];
567
- existingToolResultIds = new Set();
572
+ result.push(...heldSystemMessages);
573
+ heldSystemMessages.length = 0;
568
574
  };
569
575
 
570
576
  for (const message of transformed) {
571
577
  if (message.role === "assistant") {
572
- insertSyntheticToolResults();
578
+ closePendingToolCalls();
573
579
  const assistantMessage = message;
574
580
  if (assistantMessage.stopReason === "error" || assistantMessage.stopReason === "aborted") {
575
581
  continue;
@@ -585,14 +591,17 @@ function transformMessages(
585
591
  } else if (message.role === "toolResult") {
586
592
  existingToolResultIds.add(message.toolCallId);
587
593
  result.push(message);
594
+ } else if (message.role === "system") {
595
+ if (pendingToolCalls.length > 0) heldSystemMessages.push(message);
596
+ else result.push(message);
588
597
  } else if (message.role === "user") {
589
- insertSyntheticToolResults();
598
+ closePendingToolCalls();
590
599
  result.push(message);
591
600
  } else {
592
601
  result.push(message);
593
602
  }
594
603
  }
595
- insertSyntheticToolResults();
604
+ closePendingToolCalls();
596
605
  return result;
597
606
  }
598
607
 
@@ -737,7 +746,7 @@ export function convertResponsesMessages(
737
746
  tools: convertResponsesTools(addedTools, { ...toolOptions, deferLoading: true }),
738
747
  });
739
748
  }
740
- if (isLeadingSystemMessage ? includeSystemPrompt : options?.includeSystemUpdates !== false) {
749
+ if (!isLeadingSystemMessage || includeSystemPrompt) {
741
750
  const text = isLeadingSystemMessage
742
751
  ? getSystemMessageText(message)
743
752
  : renderSystemMessageUpdate(message);
@@ -176,7 +176,7 @@ export default function registerWebRun(
176
176
  }
177
177
  const authentication = await codexToolAuthentication(ctx, model);
178
178
  const config = resolveConfig(ctx);
179
- const history = codexToolHistory(pi, ctx, model, config.imageDetail);
179
+ const history = codexToolHistory(ctx, model, config.imageDetail);
180
180
  const input = recentSearchInput(history);
181
181
  const body: JsonRecord = {
182
182
  id: ctx.sessionManager.getSessionId(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-openai-codex-compat",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "OpenAI Codex compatibility for Pi with native compaction, fast mode, and Codex-optimized capabilities",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -51,9 +51,9 @@
51
51
  "devDependencies": {
52
52
  "@2h2d/oxlint-config": "0.1.1",
53
53
  "@2h2d/ts-config": "0.0.1",
54
- "@earendil-works/pi-ai": "0.86.1",
55
- "@earendil-works/pi-coding-agent": "0.86.1",
56
- "@earendil-works/pi-tui": "0.86.1",
54
+ "@earendil-works/pi-ai": "0.87.0",
55
+ "@earendil-works/pi-coding-agent": "0.87.0",
56
+ "@earendil-works/pi-tui": "0.87.0",
57
57
  "@types/node": "22.20.1",
58
58
  "oxfmt": "0.64.0",
59
59
  "oxlint": "1.79.0",
@@ -62,9 +62,9 @@
62
62
  "typescript": "7.0.2"
63
63
  },
64
64
  "peerDependencies": {
65
- "@earendil-works/pi-ai": ">=0.86.0 <0.87.0",
66
- "@earendil-works/pi-coding-agent": ">=0.86.0 <0.87.0",
67
- "@earendil-works/pi-tui": ">=0.86.0 <0.87.0",
65
+ "@earendil-works/pi-ai": ">=0.87.0 <0.88.0",
66
+ "@earendil-works/pi-coding-agent": ">=0.87.0 <0.88.0",
67
+ "@earendil-works/pi-tui": ">=0.87.0 <0.88.0",
68
68
  "typebox": "*"
69
69
  },
70
70
  "engines": {