pi-openai-codex-compat 0.0.14 → 0.0.16
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 +18 -0
- package/README.md +114 -10
- package/extensions/openai-codex-compat/codex-provider/codex-provider-contracts.ts +2 -0
- package/extensions/openai-codex-compat/codex-provider/codex-provider-runtime.ts +2 -0
- package/extensions/openai-codex-compat/compaction-checkpoint.ts +124 -16
- package/extensions/openai-codex-compat/remote-compaction.ts +6 -3
- package/extensions/openai-codex-compat/responses-lite.ts +2 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.0.16 - 2026-09-23
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Opt-in Responses Lite for GPT-6 Sol and Luna on Pi 0.87.1.
|
|
10
|
+
|
|
11
|
+
## 0.0.15 - 2026-09-23
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Preserve earlier message omissions and replacements during overflow-recovery
|
|
16
|
+
compaction. Recover only the latest attempt Pi omitted for recovery, keeping
|
|
17
|
+
its committed progress without reviving unrelated content. Stored native
|
|
18
|
+
responses no longer override later assistant-message replacements.
|
|
19
|
+
- Rebuild the compaction request's tool declarations when a tool's parameter
|
|
20
|
+
schema changes under an unchanged name and description, instead of reusing a
|
|
21
|
+
stale schema from the previous turn.
|
|
22
|
+
|
|
5
23
|
## 0.0.14 - 2026-09-22
|
|
6
24
|
|
|
7
25
|
### Changed
|
package/README.md
CHANGED
|
@@ -220,9 +220,10 @@ Defaults:
|
|
|
220
220
|
| `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"`. |
|
|
221
221
|
|
|
222
222
|
Responses Lite supports exactly `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`,
|
|
223
|
-
|
|
224
|
-
exactly `gpt-6-astra`.
|
|
225
|
-
|
|
223
|
+
`gpt-6-astra`, `gpt-6-sol`, and `gpt-6-luna`. Pro reasoning mode supports
|
|
224
|
+
`gpt-5.6`, `gpt-5.6-*`, and exactly `gpt-6-astra`. GPT-6 Sol and Luna use
|
|
225
|
+
standard reasoning mode even when `pro` is configured. Both controls remain
|
|
226
|
+
opt-in. GPT-6 Sol and Luna require Pi 0.87.1's model catalog.
|
|
226
227
|
|
|
227
228
|
Invalid JSON setting values are ignored and invalid JSON does not prevent Pi from starting. The settings pane never writes on ordinary changes, refuses to overwrite invalid JSON when `Enter` or `Ctrl+S` attempts to save, and retains unknown keys when saving. Project configuration is read only when the project is trusted.
|
|
228
229
|
|
|
@@ -328,9 +329,21 @@ The extension handles native compaction for `openai-codex`. It follows the Codex
|
|
|
328
329
|
1. Send normal Responses history followed by `{ "type": "compaction_trigger" }`.
|
|
329
330
|
2. Validate the returned opaque `compaction` item.
|
|
330
331
|
3. Retain approximately 64,000 tokens of recent user messages, plus every developer and
|
|
331
|
-
system message outside that budget.
|
|
332
|
+
system message outside that budget. Repeated prompt updates (for example several
|
|
333
|
+
`/reload` runs or prompt-section changes) each remain in place as their own inline
|
|
334
|
+
developer item; they accumulate across checkpoints and are never dropped, merged, or
|
|
335
|
+
deduplicated, because each one is a distinct instruction the model already saw.
|
|
332
336
|
4. Persist the opaque checkpoint in the Pi session and replay it on later requests.
|
|
333
337
|
|
|
338
|
+
Overflow and length recovery follow Pi 0.87's `context_edit` semantics. Every request
|
|
339
|
+
reads Pi's session projection, so earlier omissions and replacements (an elided tool
|
|
340
|
+
output, an omitted reply) apply exactly as they do for Pi's own adapters. Before an
|
|
341
|
+
overflow-recovery compaction, Pi omits the latest failed or truncated attempt and its tool
|
|
342
|
+
results; the checkpoint request projects only those entries as they were before that
|
|
343
|
+
omission, so a truncated response and its tool results survive as committed progress while
|
|
344
|
+
every unrelated earlier edit stays in force. A failed attempt contributes only the native
|
|
345
|
+
output committed before the overflowing subrequest.
|
|
346
|
+
|
|
334
347
|
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.
|
|
335
348
|
|
|
336
349
|
Healthy session WebSockets remain available until the server closes them or Pi tears down the session. Retryable WebSocket failures before model-visible output receive up to five fresh-connection retries before the session switches to sticky SSE. Retryable SSE HTTP failures and dropped streams receive up to five same-request resampling attempts before model-visible output. Both transports use Codex-style exponential backoff with ±10% jitter and preserve the same prompt-cache, session, account, installation, and window identities. Server metadata is not considered model-visible output, so a routing-state-only response can still be retried safely. Transport failures after model-visible output fail closed rather than risk duplicate text or tool calls. Explicit retryable `response.failed`/`response.incomplete` protocol terminals instead return to the provider-owned sampling loop, which preserves completed output items as the next request's history.
|
|
@@ -576,6 +589,15 @@ mise run check
|
|
|
576
589
|
npm run pack:dry
|
|
577
590
|
```
|
|
578
591
|
|
|
592
|
+
`mise run check` runs the linters, formatters, type checks, and the offline
|
|
593
|
+
test suite; `mise run test` runs only the offline suite. Both tasks bind
|
|
594
|
+
`PI_PACKAGE_DIR` to `node_modules/@earendil-works/pi-coding-agent`, so tests
|
|
595
|
+
that load Pi in-process read this repository's Pi 0.87.1 resources even when a
|
|
596
|
+
global `PI_PACKAGE_DIR` points at another installation. Ordinary `pi` launches
|
|
597
|
+
outside these tasks are unaffected. Offline tests also exercise
|
|
598
|
+
`scripts/release.ts` with every child process mocked: they never run Git, npm,
|
|
599
|
+
Mise, or provider calls.
|
|
600
|
+
|
|
579
601
|
Run the credentialed Pi/Codex integration tests separately. They load the real
|
|
580
602
|
extension into headless Pi sessions, use the real WebSocket service, and ask
|
|
581
603
|
the model to report all prior history markers after each text and tool
|
|
@@ -586,13 +608,31 @@ mise run test:live:codex
|
|
|
586
608
|
```
|
|
587
609
|
|
|
588
610
|
The task obtains the local Codex bearer token and runs the tests with
|
|
589
|
-
`gpt-5.6-luna` at medium reasoning effort.
|
|
611
|
+
`gpt-5.6-luna`, `gpt-6-sol`, and `gpt-6-luna` at medium reasoning effort.
|
|
590
612
|
It also packs the extension and loads that archive through the shipped Pi
|
|
591
|
-
0.87.
|
|
613
|
+
0.87.1 CLI. Ordinary Responses and Responses Lite tests exercise tool calls,
|
|
592
614
|
prompt reload, native compaction, and persisted-session resume against Codex.
|
|
615
|
+
All three models use standard reasoning mode. SDK tests verify Responses Lite
|
|
616
|
+
WebSocket history, prewarming, continuation, and the built-in read tool.
|
|
617
|
+
CLI tests also execute the built-in read tool after resume.
|
|
593
618
|
Test credentials stay in memory and child-process environments. Test sessions
|
|
594
619
|
and configuration are isolated from the user's agent directory.
|
|
595
620
|
|
|
621
|
+
The packaged-CLI test selects its archive and executable as follows:
|
|
622
|
+
|
|
623
|
+
- Without `PI_CODEX_PACKAGE_ARCHIVE`, it packs the current worktree into a
|
|
624
|
+
temporary directory. With `PI_CODEX_PACKAGE_ARCHIVE`, it loads exactly that
|
|
625
|
+
archive: the value resolves against the working directory, must be an
|
|
626
|
+
existing regular file that `tar` can list, and an empty, missing, directory,
|
|
627
|
+
or invalid value fails the test instead of falling back to a fresh pack.
|
|
628
|
+
- `PI_CODEX_CLI_PATH` selects another Pi `cli.js`; the default is this
|
|
629
|
+
repository's Pi 0.87.1 dependency. The test asserts that the selected
|
|
630
|
+
executable reports version 0.87.1, the only Pi version the packaged CLI test
|
|
631
|
+
is run against. The supported range is `>=0.87.0 <0.88.0`.
|
|
632
|
+
- Each Pi child process receives `PI_PACKAGE_DIR` bound to the selected
|
|
633
|
+
executable's package directory, so the runtime under test reads its own
|
|
634
|
+
metadata.
|
|
635
|
+
|
|
596
636
|
The public package entrypoint is `extensions/index.ts`; implementation modules remain under
|
|
597
637
|
`extensions/openai-codex-compat/`. The focused Pi AI serializer copy lives under
|
|
598
638
|
`extensions/openai-codex-compat/vendor/pi-ai/`. The custom Codex provider transport and stream
|
|
@@ -606,10 +646,74 @@ search, and compaction continuation.
|
|
|
606
646
|
1. Run `npm run release -- X.Y.Z` from a clean, synchronized `main`.
|
|
607
647
|
2. The command builds the exact package locally and runs live CLI tests against that archive. It also runs the existing live SDK tests against checkout source. Both suites must pass before it records the archive's SHA-256 in an SSH-signed release commit, proves a clean rebuild is reproducible, and creates a lightweight tag. Missing credentials or failing live tests stop the release.
|
|
608
648
|
3. Inspect the result, then push atomically with `git push --atomic origin main vX.Y.Z`.
|
|
609
|
-
4. A read-only GitHub Actions job validates and packs the package. After approval
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
649
|
+
4. A read-only GitHub Actions job validates and packs the package. After approval
|
|
650
|
+
in the tag-restricted `npm-publish` environment, a separate GitHub-owned job
|
|
651
|
+
verifies the signature and signed digest before attesting and staging that
|
|
652
|
+
exact archive through npm trusted publishing.
|
|
653
|
+
5. A final job creates the immutable GitHub release from the verified archive,
|
|
654
|
+
its checksum, and the version's changelog section (`Unreleased` for prereleases).
|
|
655
|
+
6. Approve the staged package on npmjs.com or with `npm stage approve <stage-id>`.
|
|
656
|
+
|
|
657
|
+
The release command runs these steps in order:
|
|
658
|
+
|
|
659
|
+
1. Refuse to start unless the branch is `main`, `git status --porcelain` is
|
|
660
|
+
empty, `CHANGELOG.md` has a section for the version, `HEAD` equals
|
|
661
|
+
`origin/main` after a fetch, and tag `vX.Y.Z` does not exist. Nothing has
|
|
662
|
+
changed when one of these checks fails.
|
|
663
|
+
2. Write the version into `package.json` and `package-lock.json` and stage
|
|
664
|
+
only those two files.
|
|
665
|
+
3. Check out the staged index into a temporary directory, run
|
|
666
|
+
`npm ci --ignore-scripts` and `npm pack` there, validate the package
|
|
667
|
+
identity, file list, and manifest, then run `mise run test:live:codex` in
|
|
668
|
+
the repository with `PI_CODEX_PACKAGE_ARCHIVE` set to that archive. This is
|
|
669
|
+
the exact archive release gate: the live CLI tests load that archive and
|
|
670
|
+
nothing else. A live process that cannot start, a nonzero exit, or a failed
|
|
671
|
+
package validation stops the release here.
|
|
672
|
+
4. Create the SSH-signed `release: vX.Y.Z` commit with the archive's SHA-256
|
|
673
|
+
as its `Npm-Artifact-SHA256` trailer and verify the signature and trailer.
|
|
674
|
+
5. Rebuild the package from the committed tree without repeating the live
|
|
675
|
+
tests and require the same digest.
|
|
676
|
+
6. Create the lightweight tag and verify that it points at the release commit.
|
|
677
|
+
|
|
678
|
+
### Recovering from a failed release command
|
|
679
|
+
|
|
680
|
+
Do not run any blanket `git restore`, `git reset`, `git checkout --`, or
|
|
681
|
+
`git clean`. Inspect first, then undo only what the failed command produced.
|
|
682
|
+
|
|
683
|
+
If the command fails during the version update, version changes can remain
|
|
684
|
+
in the worktree. Once the update and `git add` succeed, package, live-test,
|
|
685
|
+
and signing failures leave those changes staged. No release commit or tag
|
|
686
|
+
was created by that attempt. Inspect the state:
|
|
687
|
+
|
|
688
|
+
```sh
|
|
689
|
+
git status --short
|
|
690
|
+
git diff -- package.json package-lock.json
|
|
691
|
+
git diff --cached -- package.json package-lock.json
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
Undo only this attempt's version edits in the worktree and index. Preserve
|
|
695
|
+
concurrent changes, including edits in those same files. Do not stage whole
|
|
696
|
+
files containing unrelated edits. Fix the cause and confirm that `main` is
|
|
697
|
+
clean and synchronized before rerunning the release command.
|
|
698
|
+
|
|
699
|
+
If the command fails after the release commit (steps 4 to 6), a local signed
|
|
700
|
+
`release: vX.Y.Z` commit exists on `main`, and a local tag may exist. Do not rerun
|
|
701
|
+
the release command: it would refuse the unsynchronized `HEAD`, and a second
|
|
702
|
+
run on top of the existing commit would produce a duplicate release commit.
|
|
703
|
+
Do not push the commit or tag until signature verification, the reproducibility
|
|
704
|
+
check, and tag verification are complete. Inspect the commit and the digest:
|
|
705
|
+
|
|
706
|
+
```sh
|
|
707
|
+
git log -1 --format='%H%n%s%n%(trailers:key=Npm-Artifact-SHA256,valueonly)' HEAD
|
|
708
|
+
git diff origin/main..HEAD -- package.json package-lock.json
|
|
709
|
+
git tag --list 'vX.Y.Z'
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
Removing the local release commit or tag changes local refs. Confirm that
|
|
713
|
+
they were never pushed, record their hashes and a recovery path, and obtain
|
|
714
|
+
explicit approval before changing them. Never replace a published tag.
|
|
715
|
+
Fix the underlying cause before starting a new release from clean,
|
|
716
|
+
synchronized `main`.
|
|
613
717
|
|
|
614
718
|
Stable releases use `latest`; prereleases derive their npm dist-tag from the first prerelease identifier.
|
|
615
719
|
|
|
@@ -47,6 +47,8 @@ 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;
|
|
50
52
|
grammarToolInputProperties: GrammarToolInputProperties;
|
|
51
53
|
requestOptions: OpenAICodexResponsesOptions;
|
|
52
54
|
};
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
resolveRequestTranscript,
|
|
33
33
|
responsesCompatibility,
|
|
34
34
|
searchCheckpoint,
|
|
35
|
+
toolDefinitionFingerprint,
|
|
35
36
|
type CheckpointData,
|
|
36
37
|
type CompactionDecision,
|
|
37
38
|
type GrammarToolInputProperties,
|
|
@@ -871,6 +872,7 @@ export class CodexProviderRuntime {
|
|
|
871
872
|
this.templates.set(runtimeSessionId, {
|
|
872
873
|
modelId: model.id,
|
|
873
874
|
payload: replacementValues ? {} : withoutConversationInput(body),
|
|
875
|
+
toolFingerprint: toolDefinitionFingerprint(getCurrentTools(context.messages)),
|
|
874
876
|
grammarToolInputProperties,
|
|
875
877
|
requestOptions: { ...requestOptions },
|
|
876
878
|
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { isBoolean, isString } from "./value-contracts.ts";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import {
|
|
4
4
|
buildSessionContext,
|
|
5
5
|
buildSessionProjection,
|
|
6
6
|
convertToLlm,
|
|
7
7
|
sessionEntryToContextMessages,
|
|
8
|
+
type ContextEditEntry,
|
|
8
9
|
type SessionEntry,
|
|
9
10
|
type ToolInfo,
|
|
10
11
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
type JsonRecord,
|
|
32
33
|
} from "./codex-protocol.ts";
|
|
33
34
|
import { nativeCommittedPrefixBeforeOverflow, nativeResponseOverrides } from "./native-history.ts";
|
|
35
|
+
import { stableResponsesJson } from "./responses-replay.ts";
|
|
34
36
|
import {
|
|
35
37
|
CODEX_NAMESPACED_TOOL_NAMES,
|
|
36
38
|
CODEX_TEXT_CONTENT_ITEM_TOOL_RESULT_NAMES,
|
|
@@ -181,9 +183,45 @@ export function activeResponsesTools(
|
|
|
181
183
|
: undefined;
|
|
182
184
|
}
|
|
183
185
|
|
|
186
|
+
/** The raw tool definition fields that both Pi AI's `Tool` and Pi's `ToolInfo` carry. */
|
|
187
|
+
export type ToolDeclarationSource = Pick<ToolInfo, "name" | "description" | "parameters">;
|
|
188
|
+
|
|
184
189
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
190
|
+
* Fingerprint raw tool definitions by name, description, and parameter schema,
|
|
191
|
+
* independent of declaration order. Both the transcript tools a turn request
|
|
192
|
+
* declared and the registry's active `ToolInfo` list produce the same value for
|
|
193
|
+
* the same definitions, so a cached turn template can be compared with the
|
|
194
|
+
* current tools without reversing the strict schema conversion applied on the
|
|
195
|
+
* wire.
|
|
196
|
+
*/
|
|
197
|
+
export function toolDefinitionFingerprint(tools: readonly ToolDeclarationSource[]): string {
|
|
198
|
+
const declarations = tools
|
|
199
|
+
.map((tool) => {
|
|
200
|
+
// The JSON round-trip drops typebox symbol keys and undefined fields, as
|
|
201
|
+
// Pi AI's own declaration comparison does.
|
|
202
|
+
const parameters: unknown = JSON.parse(JSON.stringify(tool.parameters));
|
|
203
|
+
return { name: tool.name, description: tool.description, parameters };
|
|
204
|
+
})
|
|
205
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
206
|
+
return createHash("sha256").update(stableResponsesJson(declarations)).digest("hex");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Fingerprint of the registry tools that are currently active. */
|
|
210
|
+
export function activeToolFingerprint(
|
|
211
|
+
allTools: readonly ToolInfo[],
|
|
212
|
+
activeNames: readonly string[],
|
|
213
|
+
): string {
|
|
214
|
+
const enabled = new Set(activeNames);
|
|
215
|
+
return toolDefinitionFingerprint(allTools.filter((tool) => enabled.has(tool.name)));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Whether a cached turn template still declares exactly the active tools.
|
|
220
|
+
*
|
|
221
|
+
* The serialized `tools` list must name the active set (namespace members
|
|
222
|
+
* count as `namespace.member`), and the raw definitions that produced it must
|
|
223
|
+
* fingerprint identically to the active registry tools. Comparing names and
|
|
224
|
+
* descriptions alone missed parameter-schema changes under an unchanged name.
|
|
187
225
|
*
|
|
188
226
|
* A cached turn template carries the exact declarations the turn requests sent,
|
|
189
227
|
* including strict schema conversion that `ToolInfo` cannot reproduce. Reusing
|
|
@@ -191,9 +229,11 @@ export function activeResponsesTools(
|
|
|
191
229
|
*/
|
|
192
230
|
export function declaresActiveTools(
|
|
193
231
|
serialized: readonly JsonRecord[],
|
|
232
|
+
cachedToolFingerprint: string,
|
|
194
233
|
allTools: readonly ToolInfo[],
|
|
195
234
|
activeNames: readonly string[],
|
|
196
235
|
): boolean {
|
|
236
|
+
if (cachedToolFingerprint !== activeToolFingerprint(allTools, activeNames)) return false;
|
|
197
237
|
const declared = new Map<string, string | undefined>();
|
|
198
238
|
for (const item of serialized) {
|
|
199
239
|
if (item.type === "namespace" && isString(item.name) && Array.isArray(item.tools)) {
|
|
@@ -415,18 +455,69 @@ export function checkpointData(
|
|
|
415
455
|
return checkpoint;
|
|
416
456
|
}
|
|
417
457
|
|
|
458
|
+
/**
|
|
459
|
+
* The entries Pi 0.87 omits for overflow or length recovery: the latest
|
|
460
|
+
* assistant attempt and the tool results of that turn, each targeted by a
|
|
461
|
+
* `context_edit` omission appended after the attempt. `leafId` is the parent of
|
|
462
|
+
* the first such omission, so a projection built up to it reflects every edit
|
|
463
|
+
* that existed before Pi omitted the attempt.
|
|
464
|
+
*
|
|
465
|
+
* Pi only selects a recovery attempt while it is still projected, so an
|
|
466
|
+
* omission of the latest assistant found here is Pi's recovery omission and
|
|
467
|
+
* not an unrelated edit.
|
|
468
|
+
*/
|
|
469
|
+
function recoveryOmissions(
|
|
470
|
+
branch: readonly SessionEntry[],
|
|
471
|
+
): { entryIds: ReadonlySet<string>; leafId: string } | undefined {
|
|
472
|
+
const attemptIndex = branch.findLastIndex(
|
|
473
|
+
(entry) => entry.type === "message" && entry.message.role === "assistant",
|
|
474
|
+
);
|
|
475
|
+
const attempt = branch[attemptIndex];
|
|
476
|
+
if (attempt?.type !== "message" || attempt.message.role !== "assistant") return undefined;
|
|
477
|
+
|
|
478
|
+
const toolCallIds = new Set(
|
|
479
|
+
attempt.message.content.flatMap((block) => (block.type === "toolCall" ? [block.id] : [])),
|
|
480
|
+
);
|
|
481
|
+
const candidates = new Set([attempt.id]);
|
|
482
|
+
for (const entry of branch.slice(attemptIndex + 1)) {
|
|
483
|
+
if (entry.type !== "message") continue;
|
|
484
|
+
if (entry.message.role !== "toolResult" || !toolCallIds.has(entry.message.toolCallId)) break;
|
|
485
|
+
candidates.add(entry.id);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const latestEdits = new Map<string, ContextEditEntry>();
|
|
489
|
+
for (const entry of branch.slice(attemptIndex + 1)) {
|
|
490
|
+
if (entry.type === "context_edit" && candidates.has(entry.targetId)) {
|
|
491
|
+
latestEdits.set(entry.targetId, entry);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (latestEdits.get(attempt.id)?.replacement !== null) return undefined;
|
|
495
|
+
const omissions = new Set(
|
|
496
|
+
[...latestEdits.values()].filter((entry) => entry.replacement === null),
|
|
497
|
+
);
|
|
498
|
+
const first = branch.find((entry) => entry.type === "context_edit" && omissions.has(entry));
|
|
499
|
+
if (first?.type !== "context_edit" || first.parentId === null) return undefined;
|
|
500
|
+
return {
|
|
501
|
+
entryIds: new Set([...omissions].map((entry) => entry.targetId)),
|
|
502
|
+
leafId: first.parentId,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
418
506
|
/**
|
|
419
507
|
* Materialize provider history for the active branch. Checkpoint history
|
|
420
508
|
* replaces everything before its entry; later branch entries form the tail.
|
|
421
509
|
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
* compaction
|
|
425
|
-
* truncated
|
|
426
|
-
* runs `session_before_compact`.
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
510
|
+
* Every request reads the session projection, so `context_edit` omissions and
|
|
511
|
+
* replacements apply exactly as they do for Pi's own adapters. Recovery
|
|
512
|
+
* compaction differs only for the latest attempt: Pi 0.87 omits the failed or
|
|
513
|
+
* truncated assistant and its tool results through `context_edit` entries
|
|
514
|
+
* before it runs `session_before_compact`. Those entries are projected as they
|
|
515
|
+
* were before Pi's omission, so a truncated boundary stays as committed
|
|
516
|
+
* progress while every earlier omission or replacement remains in force. The
|
|
517
|
+
* serializer skips error and aborted assistants; a failed attempt contributes
|
|
518
|
+
* only the native prefix committed before the overflowing subrequest. Entries
|
|
519
|
+
* are never removed from the branch copy because the omission entry's parent
|
|
520
|
+
* chain would break and the history would vanish.
|
|
430
521
|
*/
|
|
431
522
|
export function providerHistory(options: {
|
|
432
523
|
branch: readonly SessionEntry[];
|
|
@@ -459,13 +550,30 @@ export function providerHistory(options: {
|
|
|
459
550
|
const projection = buildSessionProjection(branch);
|
|
460
551
|
const projected = new Map<string, AgentMessages>();
|
|
461
552
|
for (const entry of projection.entries) projected.set(entry.sourceEntry.id, entry.messages);
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
553
|
+
const recovery = options.recoverLatestOverflowPrefix ? recoveryOmissions(branch) : undefined;
|
|
554
|
+
if (recovery) {
|
|
555
|
+
for (const entry of buildSessionProjection(branch, recovery.leafId).entries) {
|
|
556
|
+
if (recovery.entryIds.has(entry.sourceEntry.id)) {
|
|
557
|
+
projected.set(entry.sourceEntry.id, entry.messages);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const entryMessages = (entry: SessionEntry): AgentMessages => projected.get(entry.id) ?? [];
|
|
466
562
|
|
|
467
563
|
const checkpoint = searchCheckpoint(branch);
|
|
468
|
-
const nativeAssistantItems = nativeResponseOverrides(branch, options.wireModel.id);
|
|
564
|
+
const nativeAssistantItems = new Map(nativeResponseOverrides(branch, options.wireModel.id));
|
|
565
|
+
for (const entry of branch) {
|
|
566
|
+
if (
|
|
567
|
+
entry.type === "message" &&
|
|
568
|
+
entry.message.role === "assistant" &&
|
|
569
|
+
entry.message.responseId &&
|
|
570
|
+
entryMessages(entry).some((message) => message !== entry.message)
|
|
571
|
+
) {
|
|
572
|
+
// A replacement keeps the response ID but changes the model-visible content.
|
|
573
|
+
// Its original native output must not override the canonical replacement.
|
|
574
|
+
nativeAssistantItems.delete(entry.message.responseId);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
469
577
|
if (checkpoint.kind === "corrupt") {
|
|
470
578
|
throw new Error("The latest Codex compaction checkpoint is corrupt.");
|
|
471
579
|
}
|
|
@@ -258,15 +258,18 @@ export default function registerRemoteCompaction(
|
|
|
258
258
|
});
|
|
259
259
|
// Reuse the last turn's exact declarations when they still describe the
|
|
260
260
|
// active tools, so the compaction request shares the turn requests'
|
|
261
|
-
// prompt-cache prefix. Rebuild from the registry
|
|
262
|
-
//
|
|
261
|
+
// prompt-cache prefix. Rebuild from the registry when the active set or
|
|
262
|
+
// any definition (including a parameter schema under an unchanged name)
|
|
263
|
+
// changed since that turn, or when no turn template exists.
|
|
263
264
|
const template: JsonRecord = { ...matching?.payload };
|
|
264
265
|
const activeNames = pi.getActiveTools();
|
|
265
266
|
const cachedTools = Array.isArray(matching?.payload.tools)
|
|
266
267
|
? matching.payload.tools.filter(isObject)
|
|
267
268
|
: undefined;
|
|
268
269
|
template.tools =
|
|
269
|
-
cachedTools &&
|
|
270
|
+
cachedTools &&
|
|
271
|
+
matching &&
|
|
272
|
+
declaresActiveTools(cachedTools, matching.toolFingerprint, allTools, activeNames)
|
|
270
273
|
? cachedTools
|
|
271
274
|
: (activeResponsesTools(allTools, activeNames, grammarToolInputProperties) ?? []);
|
|
272
275
|
const requestOptions: OpenAICodexResponsesOptions = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-openai-codex-compat",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
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.87.
|
|
55
|
-
"@earendil-works/pi-coding-agent": "0.87.
|
|
56
|
-
"@earendil-works/pi-tui": "0.87.
|
|
54
|
+
"@earendil-works/pi-ai": "0.87.1",
|
|
55
|
+
"@earendil-works/pi-coding-agent": "0.87.1",
|
|
56
|
+
"@earendil-works/pi-tui": "0.87.1",
|
|
57
57
|
"@types/node": "22.20.1",
|
|
58
58
|
"oxfmt": "0.64.0",
|
|
59
59
|
"oxlint": "1.79.0",
|