pi-openai-codex-compat 0.0.22 → 0.0.23

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,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.0.23 - 2026-10-12
6
+
7
+ ### Fixed
8
+
9
+ - Send system messages that other extensions add to a single request, such as
10
+ context-threshold instructions from `pi-compact-assistant`, to Codex as
11
+ developer messages at their position. Codex requests are built from the
12
+ stored session, so these messages were previously dropped.
13
+
5
14
  ## 0.0.22 - 2026-10-08
6
15
 
7
16
  ### Fixed
package/README.md CHANGED
@@ -76,7 +76,7 @@ The compatibility baseline is official Codex CLI `0.149.1`, released August 24,
76
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. |
77
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. |
78
78
  | Model switching | This package rejects model switches while the active Pi branch contains a native Codex checkpoint because the checkpoint is model-specific. |
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. |
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. Request history is rebuilt from the stored session, and system messages that another extension adds to only one request, such as `pi-compact-assistant` instructions, are inserted after the stored message they follow. A forced prompt replaces the leading message. Compaction snapshots and resumed sessions lead with Pi's replayed prompt without rewriting old checkpoints. |
80
80
  | 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. |
81
81
  | 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. |
82
82
  | 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. |
@@ -524,11 +524,14 @@ export class CodexProviderRuntime {
524
524
  convertResponsesMessages(model, context, CODEX_TOOL_CALL_PROVIDERS, serializationOptions),
525
525
  );
526
526
  }
527
+ // History comes from the branch so native items replay exactly. Pi's live
528
+ // transcript adds only the system messages extensions placed in this request.
527
529
  return providerHistory({
528
530
  branch: scope.manager.getBranch(),
529
531
  wireModel: model,
530
532
  grammarToolInputProperties,
531
533
  imageDetail: scope.config.imageDetail,
534
+ requestMessages: context.messages,
532
535
  });
533
536
  }
534
537
 
@@ -169,6 +169,8 @@ export type EncodeSessionEntriesOptions = {
169
169
  initialSystemMessage?: SystemMessage;
170
170
  /** Model-visible messages per entry. Defaults to the entry's raw messages. */
171
171
  entryMessages?: (entry: SessionEntry) => AgentMessages;
172
+ /** Pi's live request transcript, whose request-local system messages are added in place. */
173
+ requestMessages?: readonly Message[];
172
174
  };
173
175
 
174
176
  /**
@@ -221,6 +223,7 @@ export function encodeSessionEntries(options: EncodeSessionEntriesOptions): Resp
221
223
  entryMessages = sessionEntryToContextMessages,
222
224
  } = options;
223
225
  const messages = entries.flatMap((entry) => entryMessages(entry));
226
+ const converted = convertToLlm(messages);
224
227
  const encodeOptions: EncodeMessagesOptions = {
225
228
  model,
226
229
  // A partial tail can start with a system update. Seed its prior state so
@@ -228,7 +231,9 @@ export function encodeSessionEntries(options: EncodeSessionEntriesOptions): Resp
228
231
  // leading system message that `instructions` already carries.
229
232
  messages: [
230
233
  options.initialSystemMessage ?? { role: "system", content: "", timestamp: 0 },
231
- ...convertToLlm(messages),
234
+ ...(options.requestMessages
235
+ ? withRequestSystemMessages(converted, options.requestMessages)
236
+ : converted),
232
237
  ],
233
238
  grammarToolInputProperties,
234
239
  imageDetail,
@@ -237,6 +242,68 @@ export function encodeSessionEntries(options: EncodeSessionEntriesOptions): Resp
237
242
  return encodeMessages(encodeOptions);
238
243
  }
239
244
 
245
+ function systemKey(message: SystemMessage): string {
246
+ return JSON.stringify([message.timestamp, message.content, message.sections ?? null]);
247
+ }
248
+
249
+ function sameMessage(left: Message, right: Message): boolean {
250
+ if (left.role !== right.role || left.timestamp !== right.timestamp) return false;
251
+ if (left.role === "toolResult" && right.role === "toolResult") {
252
+ return left.toolCallId === right.toolCallId;
253
+ }
254
+ return true;
255
+ }
256
+
257
+ /**
258
+ * Add the system messages an extension placed in Pi's live request transcript
259
+ * without storing them in the session, such as request-local instructions from a
260
+ * `context_with_system` handler. History is built from the session branch, so
261
+ * such messages would otherwise never reach Codex.
262
+ *
263
+ * A request-local system message is one that matches no stored system message by
264
+ * timestamp, content, and sections; the leading prompt is excluded. It follows the
265
+ * nearest earlier request message that is also in `messages`, matched by role,
266
+ * timestamp, and tool call ID. Without such a message it leads the conversation
267
+ * after `start` messages. Messages anchored before an encoded range are skipped.
268
+ */
269
+ export function withRequestSystemMessages(
270
+ messages: readonly Message[],
271
+ requestMessages: readonly Message[],
272
+ start = 0,
273
+ ): Message[] {
274
+ const stored = new Set(
275
+ messages.flatMap((message) => (message.role === "system" ? [systemKey(message)] : [])),
276
+ );
277
+ const after = new Map<number, Message[]>();
278
+ for (let index = 1; index < requestMessages.length; index++) {
279
+ const message = requestMessages[index];
280
+ if (message?.role !== "system" || stored.has(systemKey(message))) continue;
281
+ let position = start - 1;
282
+ for (let previous = index - 1; previous > 0; previous--) {
283
+ const anchor = requestMessages[previous];
284
+ if (!anchor || anchor.role === "system") continue;
285
+ const found = messages.findLastIndex(
286
+ (candidate) => candidate.role !== "system" && sameMessage(candidate, anchor),
287
+ );
288
+ if (found >= 0) {
289
+ position = found;
290
+ break;
291
+ }
292
+ position = Number.NaN;
293
+ }
294
+ if (Number.isNaN(position)) continue;
295
+ after.set(position, [...(after.get(position) ?? []), message]);
296
+ }
297
+ if (after.size === 0) return [...messages];
298
+ return [
299
+ ...messages.slice(0, start),
300
+ ...(after.get(start - 1) ?? []),
301
+ ...messages
302
+ .slice(start)
303
+ .flatMap((message, offset) => [message, ...(after.get(start + offset) ?? [])]),
304
+ ];
305
+ }
306
+
240
307
  export function parseCheckpoint(value: unknown): CheckpointData | undefined {
241
308
  if (!isObject(value)) return undefined;
242
309
  if (value.kind !== CHECKPOINT_ENTRY_TYPE || value.version !== CHECKPOINT_FORMAT_VERSION) {
@@ -415,6 +482,8 @@ export function providerHistory(options: {
415
482
  grammarToolInputProperties?: GrammarToolInputProperties;
416
483
  imageDetail?: ImageDetail;
417
484
  recoverLatestOverflowPrefix?: boolean;
485
+ /** Pi's live request transcript, whose request-local system messages are added in place. */
486
+ requestMessages?: readonly Message[];
418
487
  }): ResponsesInputItem[] {
419
488
  const branch = [...options.branch];
420
489
  let recoveredPrefix: ResponsesInputItem[] = [];
@@ -486,17 +555,25 @@ export function providerHistory(options: {
486
555
  nativeAssistantItems,
487
556
  entryMessages,
488
557
  ...(initialSystemMessage ? { initialSystemMessage } : {}),
558
+ ...(options.requestMessages ? { requestMessages: options.requestMessages } : {}),
489
559
  }),
490
560
  ...recoveredPrefix,
491
561
  ];
492
562
  }
493
563
 
564
+ const messages = convertToLlm(
565
+ projection.entries.flatMap((entry) => entryMessages(entry.sourceEntry)),
566
+ );
494
567
  return [
495
568
  ...encodeMessages({
496
569
  model: options.wireModel,
497
- messages: convertToLlm(
498
- projection.entries.flatMap((entry) => entryMessages(entry.sourceEntry)),
499
- ),
570
+ messages: options.requestMessages
571
+ ? withRequestSystemMessages(
572
+ messages,
573
+ options.requestMessages,
574
+ getInitialSystemMessage(messages) ? 1 : 0,
575
+ )
576
+ : messages,
500
577
  grammarToolInputProperties: options.grammarToolInputProperties ?? new Map(),
501
578
  imageDetail: options.imageDetail ?? "auto",
502
579
  nativeAssistantItems,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-openai-codex-compat",
3
- "version": "0.0.22",
3
+ "version": "0.0.23",
4
4
  "description": "OpenAI Codex compatibility for Pi with native compaction, fast mode, and Codex-optimized capabilities",
5
5
  "keywords": [
6
6
  "pi-package"