@narumitw/pi-codex-compact 0.47.0
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/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +52 -0
- package/src/checkpoint.ts +257 -0
- package/src/codex-compact.ts +283 -0
- package/src/index.ts +1 -0
- package/src/protocol.ts +224 -0
- package/src/remote.ts +117 -0
- package/src/settings-menu.ts +232 -0
- package/src/settings.ts +227 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Narumi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# @narumitw/pi-codex-compact
|
|
2
|
+
|
|
3
|
+
> [!WARNING]
|
|
4
|
+
> This extension is experimental. It depends on an undocumented OpenAI Codex Responses wire
|
|
5
|
+
> contract and stores provider-specific opaque checkpoints. Keep backups of important sessions.
|
|
6
|
+
|
|
7
|
+
Codex Remote Compaction V2 for Pi's built-in `openai-codex` OAuth provider. It replaces Pi's
|
|
8
|
+
plaintext summary-generation call with a server-generated opaque compaction item, then replays
|
|
9
|
+
that item in later OpenAI Codex Responses requests.
|
|
10
|
+
|
|
11
|
+
Pi still owns compaction thresholds, `/compact`, overflow retries, retained-message selection, and
|
|
12
|
+
the append-only session tree. This package does not reproduce Codex core's context-window lineage
|
|
13
|
+
or exact pre-turn and mid-turn lifecycle.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pi install npm:@narumitw/pi-codex-compact
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
From this repository:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
just try codex-compact
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Loading the package enables it. Use Pi normally, including its built-in `/compact` command. Open
|
|
28
|
+
the extension's TUI control menu with:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
/codex-compact
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The menu shows the active model and whether compaction will use Codex Remote V2 or Pi's native
|
|
35
|
+
fallback. Choose **Compact now** to close the menu and compact immediately, or **Settings** to edit
|
|
36
|
+
user settings in `~/.pi/agent/pi-codex-compact.json` (or the active Pi agent directory). Invalid
|
|
37
|
+
files are never overwritten; manual compaction remains available, while Settings opens read-only
|
|
38
|
+
repair guidance until the file is fixed and `/reload` is run.
|
|
39
|
+
|
|
40
|
+
| Setting | Default | Accepted values |
|
|
41
|
+
| --- | ---: | --- |
|
|
42
|
+
| `enabled` | `true` | Boolean |
|
|
43
|
+
| `requestTimeoutMs` | `300000` | Integer from 30,000 to 600,000 ms |
|
|
44
|
+
| `maxRetries` | `2` | Integer from 0 to 2 |
|
|
45
|
+
| `replacementTokenBudget` | `64000` | Integer from 8,000 to 128,000 tokens |
|
|
46
|
+
| `notifyOnFallback` | `true` | Boolean |
|
|
47
|
+
|
|
48
|
+
Missing fields use built-in defaults; the file has no environment-variable or project-level
|
|
49
|
+
override. Settings reload on every `session_start`, including `/reload`, resume, and fork. Menu
|
|
50
|
+
writes apply immediately, preserve unknown JSON fields, serialize within the current Pi process, and
|
|
51
|
+
use a conflict check plus atomic rename. Separate Pi processes are not coordinated by a shared lock;
|
|
52
|
+
a detected concurrent edit is rejected for the user to reopen and retry.
|
|
53
|
+
|
|
54
|
+
## Requirements
|
|
55
|
+
|
|
56
|
+
- Pi `0.83.0`-compatible extension APIs.
|
|
57
|
+
- A model using provider `openai-codex` and API `openai-codex-responses`.
|
|
58
|
+
- A working OpenAI Codex OAuth login in Pi.
|
|
59
|
+
|
|
60
|
+
OpenAI API-key, Azure, Copilot, proxies, and generic Responses-compatible providers are not
|
|
61
|
+
supported. Unsupported models continue to use Pi's native compaction.
|
|
62
|
+
|
|
63
|
+
## How it works
|
|
64
|
+
|
|
65
|
+
1. Pi decides when compaction is needed.
|
|
66
|
+
2. The extension sends the current Codex Responses input with a final `compaction_trigger`.
|
|
67
|
+
3. It validates and persists the server's encrypted `compaction` item in the Pi
|
|
68
|
+
`CompactionEntry.details` field.
|
|
69
|
+
4. Later Codex requests replace Pi's fallback summary and kept suffix with the opaque replacement
|
|
70
|
+
history before dispatch.
|
|
71
|
+
|
|
72
|
+
The SSE response is limited to 8 MiB, the opaque item to 2 MiB, and the persisted replacement
|
|
73
|
+
history to the configured text budget (64,000 tokens by default) and 8 MiB. Oversized media
|
|
74
|
+
retention is dropped rather than creating an unbounded session entry.
|
|
75
|
+
|
|
76
|
+
## Failure and portability
|
|
77
|
+
|
|
78
|
+
Remote auth, transport, protocol, or validation failures fall back to Pi's native plaintext
|
|
79
|
+
compaction. User cancellation does not start that fallback.
|
|
80
|
+
|
|
81
|
+
After a successful remote compaction, full older history can be replayed only when this extension
|
|
82
|
+
is loaded with the same `openai-codex` model. If the extension is removed or the model/provider is
|
|
83
|
+
changed, Pi exposes a warning summary plus its retained recent messages. Switching back restores
|
|
84
|
+
opaque replay. Repeated compaction after an opaque checkpoint also requires the checkpoint to be
|
|
85
|
+
projected safely; otherwise Pi falls back rather than guessing.
|
|
86
|
+
|
|
87
|
+
## Privacy and storage
|
|
88
|
+
|
|
89
|
+
Remote compaction sends the active conversation context, system prompt, and active tool schemas to
|
|
90
|
+
the same OpenAI Codex backend used by the selected model. The extension stores the encrypted
|
|
91
|
+
compaction item and bounded recent user-role Responses items in the Pi session. It never persists
|
|
92
|
+
the OAuth token or request headers.
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
npm --workspace @narumitw/pi-codex-compact run check
|
|
98
|
+
npm test
|
|
99
|
+
just pack codex-compact
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
See
|
|
103
|
+
[`docs/implementation-notes/codex-compaction-mechanism.md`](../../docs/implementation-notes/codex-compaction-mechanism.md)
|
|
104
|
+
for the underlying Codex mechanism research and the extension boundary.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@narumitw/pi-codex-compact",
|
|
3
|
+
"version": "0.47.0",
|
|
4
|
+
"description": "Experimental Codex Remote Compaction V2 for Pi's OpenAI Codex provider.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"private": false,
|
|
8
|
+
"keywords": [
|
|
9
|
+
"pi-package",
|
|
10
|
+
"pi-extension",
|
|
11
|
+
"pi",
|
|
12
|
+
"codex",
|
|
13
|
+
"compaction",
|
|
14
|
+
"openai"
|
|
15
|
+
],
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"pi": {
|
|
22
|
+
"extensions": [
|
|
23
|
+
"./src/index.ts"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"check": "biome check --vcs-use-ignore-file=false src test package.json tsconfig.json README.md && npm run typecheck",
|
|
28
|
+
"format": "biome check --write --vcs-use-ignore-file=false src test package.json tsconfig.json README.md",
|
|
29
|
+
"typecheck": "tsc --noEmit"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@narumitw/pi-tui-kit": "^0.45.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@earendil-works/pi-agent-core": "*",
|
|
36
|
+
"@earendil-works/pi-ai": "*",
|
|
37
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@biomejs/biome": "2.5.6",
|
|
41
|
+
"@earendil-works/pi-agent-core": "0.83.0",
|
|
42
|
+
"@earendil-works/pi-ai": "0.83.0",
|
|
43
|
+
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
44
|
+
"@types/node": "26.1.2",
|
|
45
|
+
"typescript": "7.0.2"
|
|
46
|
+
},
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/narumiruna/pi-extensions",
|
|
50
|
+
"directory": "experimental/pi-codex-compact"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import type { CompactionEntry, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { type JsonObject, validateCompactionItem } from "./protocol.js";
|
|
5
|
+
|
|
6
|
+
export const CHECKPOINT_KIND = "pi-codex-remote-compaction";
|
|
7
|
+
export const CHECKPOINT_VERSION = 1;
|
|
8
|
+
export const REPLACEMENT_TOKEN_BUDGET = 64_000;
|
|
9
|
+
export const REPLACEMENT_BYTE_BUDGET = 8 * 1024 * 1024;
|
|
10
|
+
const MAX_MEDIA_ITEM_BYTES = 2 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
export interface CodexCheckpointDetails {
|
|
13
|
+
kind: typeof CHECKPOINT_KIND;
|
|
14
|
+
version: typeof CHECKPOINT_VERSION;
|
|
15
|
+
checkpointId: string;
|
|
16
|
+
provider: "openai-codex";
|
|
17
|
+
api: "openai-codex-responses";
|
|
18
|
+
modelId: string;
|
|
19
|
+
protocol: "remote-compaction-v2";
|
|
20
|
+
replacementHistory: JsonObject[];
|
|
21
|
+
keptMessageFingerprints: string[];
|
|
22
|
+
createdAt: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isObject(value: unknown): value is JsonObject {
|
|
26
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function stableValue(value: unknown): unknown {
|
|
30
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
31
|
+
if (!isObject(value)) return value;
|
|
32
|
+
return Object.fromEntries(
|
|
33
|
+
Object.entries(value)
|
|
34
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
35
|
+
.map(([key, child]) => [key, stableValue(child)]),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function serializedBytes(value: unknown): number {
|
|
40
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function fingerprintMessage(message: AgentMessage): string {
|
|
44
|
+
return createHash("sha256")
|
|
45
|
+
.update(JSON.stringify(stableValue(message)))
|
|
46
|
+
.digest("hex");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function checkpointMarker(checkpointId: string): string {
|
|
50
|
+
return [
|
|
51
|
+
`[PI_CODEX_REMOTE_CHECKPOINT:${checkpointId}]`,
|
|
52
|
+
"Opaque checkpoint injection failed. Do not infer missing history; tell the user to re-enable",
|
|
53
|
+
"@narumitw/pi-codex-compact with an openai-codex model.",
|
|
54
|
+
].join(" ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function fallbackSummary(checkpointId: string): string {
|
|
58
|
+
return [
|
|
59
|
+
`OpenAI Codex Remote Compaction V2 checkpoint ${checkpointId} stores the older history opaquely.`,
|
|
60
|
+
"Full replay requires @narumitw/pi-codex-compact and an openai-codex model.",
|
|
61
|
+
"Without them, only Pi's retained recent messages remain available.",
|
|
62
|
+
].join(" ");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function markerMessage(checkpointId: string, timestamp: number): AgentMessage {
|
|
66
|
+
return {
|
|
67
|
+
role: "user",
|
|
68
|
+
content: [{ type: "text", text: checkpointMarker(checkpointId) }],
|
|
69
|
+
timestamp,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function parseCheckpointDetails(value: unknown): CodexCheckpointDetails | undefined {
|
|
74
|
+
if (!isObject(value)) return undefined;
|
|
75
|
+
if (
|
|
76
|
+
value.kind !== CHECKPOINT_KIND ||
|
|
77
|
+
value.version !== CHECKPOINT_VERSION ||
|
|
78
|
+
typeof value.checkpointId !== "string" ||
|
|
79
|
+
value.checkpointId.length < 8 ||
|
|
80
|
+
value.provider !== "openai-codex" ||
|
|
81
|
+
value.api !== "openai-codex-responses" ||
|
|
82
|
+
typeof value.modelId !== "string" ||
|
|
83
|
+
value.protocol !== "remote-compaction-v2" ||
|
|
84
|
+
!Array.isArray(value.replacementHistory) ||
|
|
85
|
+
!Array.isArray(value.keptMessageFingerprints) ||
|
|
86
|
+
typeof value.createdAt !== "string"
|
|
87
|
+
) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
if (
|
|
91
|
+
value.replacementHistory.length === 0 ||
|
|
92
|
+
!value.replacementHistory.every(isObject) ||
|
|
93
|
+
!value.keptMessageFingerprints.every(
|
|
94
|
+
(fingerprint) => typeof fingerprint === "string" && /^[a-f0-9]{64}$/.test(fingerprint),
|
|
95
|
+
) ||
|
|
96
|
+
serializedBytes(value.replacementHistory) > REPLACEMENT_BYTE_BUDGET
|
|
97
|
+
) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const last = value.replacementHistory.at(-1);
|
|
101
|
+
try {
|
|
102
|
+
validateCompactionItem(last);
|
|
103
|
+
} catch {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return structuredClone(value) as unknown as CodexCheckpointDetails;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function latestCheckpoint(entries: readonly SessionEntry[]):
|
|
110
|
+
| {
|
|
111
|
+
entry: CompactionEntry<CodexCheckpointDetails>;
|
|
112
|
+
details: CodexCheckpointDetails;
|
|
113
|
+
}
|
|
114
|
+
| undefined {
|
|
115
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
116
|
+
const entry = entries[index];
|
|
117
|
+
if (entry.type !== "compaction") continue;
|
|
118
|
+
const details = parseCheckpointDetails(entry.details);
|
|
119
|
+
return details
|
|
120
|
+
? { entry: entry as CompactionEntry<CodexCheckpointDetails>, details }
|
|
121
|
+
: undefined;
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function projectCheckpointContext(
|
|
127
|
+
messages: readonly AgentMessage[],
|
|
128
|
+
details: CodexCheckpointDetails,
|
|
129
|
+
): AgentMessage[] | undefined {
|
|
130
|
+
const summary = fallbackSummary(details.checkpointId);
|
|
131
|
+
const summaryIndex = messages.findIndex(
|
|
132
|
+
(message) => message.role === "compactionSummary" && message.summary === summary,
|
|
133
|
+
);
|
|
134
|
+
if (summaryIndex < 0) return undefined;
|
|
135
|
+
const keptStart = summaryIndex + 1;
|
|
136
|
+
const keptEnd = keptStart + details.keptMessageFingerprints.length;
|
|
137
|
+
if (keptEnd > messages.length) return undefined;
|
|
138
|
+
for (let index = keptStart; index < keptEnd; index++) {
|
|
139
|
+
if (
|
|
140
|
+
fingerprintMessage(messages[index]) !== details.keptMessageFingerprints[index - keptStart]
|
|
141
|
+
) {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const timestamp = messages[summaryIndex].timestamp;
|
|
146
|
+
return [
|
|
147
|
+
...messages.slice(0, summaryIndex),
|
|
148
|
+
markerMessage(details.checkpointId, timestamp),
|
|
149
|
+
...messages.slice(keptEnd),
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function rawText(item: JsonObject): string {
|
|
154
|
+
if (!Array.isArray(item.content)) return "";
|
|
155
|
+
return item.content
|
|
156
|
+
.flatMap((part) =>
|
|
157
|
+
isObject(part) && typeof part.text === "string" && part.type === "input_text"
|
|
158
|
+
? [part.text]
|
|
159
|
+
: [],
|
|
160
|
+
)
|
|
161
|
+
.join("\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function hasMedia(item: JsonObject): boolean {
|
|
165
|
+
return (
|
|
166
|
+
Array.isArray(item.content) &&
|
|
167
|
+
item.content.some((part) => isObject(part) && part.type === "input_image")
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function truncateTextItem(item: JsonObject, maxChars: number): JsonObject | undefined {
|
|
172
|
+
if (!Array.isArray(item.content) || maxChars <= 32) return undefined;
|
|
173
|
+
let remaining = maxChars - 16;
|
|
174
|
+
const content = [...item.content].reverse().flatMap((part) => {
|
|
175
|
+
if (
|
|
176
|
+
!isObject(part) ||
|
|
177
|
+
part.type !== "input_text" ||
|
|
178
|
+
typeof part.text !== "string" ||
|
|
179
|
+
remaining <= 0
|
|
180
|
+
) {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
const text = part.text.slice(-remaining);
|
|
184
|
+
remaining -= text.length;
|
|
185
|
+
return [{ ...part, text: `[truncated]\n${text}` }];
|
|
186
|
+
});
|
|
187
|
+
if (content.length === 0) return undefined;
|
|
188
|
+
return { ...item, content: content.reverse() };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function buildReplacementHistory(
|
|
192
|
+
input: readonly unknown[],
|
|
193
|
+
compactionItem: JsonObject,
|
|
194
|
+
options: { tokenBudget?: number; byteBudget?: number } = {},
|
|
195
|
+
): JsonObject[] {
|
|
196
|
+
const tokenBudget = options.tokenBudget ?? REPLACEMENT_TOKEN_BUDGET;
|
|
197
|
+
const byteBudget = options.byteBudget ?? REPLACEMENT_BYTE_BUDGET;
|
|
198
|
+
const opaque = validateCompactionItem(compactionItem);
|
|
199
|
+
let remainingBytes = byteBudget - serializedBytes(opaque);
|
|
200
|
+
let remainingChars = tokenBudget * 4;
|
|
201
|
+
if (remainingBytes <= 0)
|
|
202
|
+
throw new Error("Opaque compaction item exceeds replacement history budget");
|
|
203
|
+
const retainedNewestFirst: JsonObject[] = [];
|
|
204
|
+
const candidates = input.filter(
|
|
205
|
+
(item): item is JsonObject =>
|
|
206
|
+
isObject(item) && item.role === "user" && item.type !== "compaction_trigger",
|
|
207
|
+
);
|
|
208
|
+
for (let index = candidates.length - 1; index >= 0; index--) {
|
|
209
|
+
const candidate = candidates[index];
|
|
210
|
+
const bytes = serializedBytes(candidate);
|
|
211
|
+
if (hasMedia(candidate) && bytes > MAX_MEDIA_ITEM_BYTES) continue;
|
|
212
|
+
const text = rawText(candidate);
|
|
213
|
+
let retained = candidate;
|
|
214
|
+
if (text.length > remainingChars) {
|
|
215
|
+
if (hasMedia(candidate)) continue;
|
|
216
|
+
const truncated = truncateTextItem(candidate, remainingChars);
|
|
217
|
+
if (!truncated) continue;
|
|
218
|
+
retained = truncated;
|
|
219
|
+
}
|
|
220
|
+
if (serializedBytes(retained) > remainingBytes) {
|
|
221
|
+
if (hasMedia(retained)) continue;
|
|
222
|
+
const maxCharsByBytes = Math.max(0, remainingBytes - 128);
|
|
223
|
+
const truncated = truncateTextItem(retained, Math.min(remainingChars, maxCharsByBytes));
|
|
224
|
+
if (!truncated || serializedBytes(truncated) > remainingBytes) continue;
|
|
225
|
+
retained = truncated;
|
|
226
|
+
}
|
|
227
|
+
retainedNewestFirst.push(structuredClone(retained));
|
|
228
|
+
remainingBytes -= serializedBytes(retained);
|
|
229
|
+
remainingChars -= Math.min(remainingChars, rawText(retained).length);
|
|
230
|
+
if (remainingBytes <= 128 || remainingChars <= 32) break;
|
|
231
|
+
}
|
|
232
|
+
return [...retainedNewestFirst.reverse(), opaque];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function createCheckpointDetails(input: {
|
|
236
|
+
modelId: string;
|
|
237
|
+
replacementHistory: JsonObject[];
|
|
238
|
+
keptMessages: readonly AgentMessage[];
|
|
239
|
+
checkpointId?: string;
|
|
240
|
+
createdAt?: string;
|
|
241
|
+
}): CodexCheckpointDetails {
|
|
242
|
+
const details: CodexCheckpointDetails = {
|
|
243
|
+
kind: CHECKPOINT_KIND,
|
|
244
|
+
version: CHECKPOINT_VERSION,
|
|
245
|
+
checkpointId: input.checkpointId ?? randomUUID(),
|
|
246
|
+
provider: "openai-codex",
|
|
247
|
+
api: "openai-codex-responses",
|
|
248
|
+
modelId: input.modelId,
|
|
249
|
+
protocol: "remote-compaction-v2",
|
|
250
|
+
replacementHistory: structuredClone(input.replacementHistory),
|
|
251
|
+
keptMessageFingerprints: input.keptMessages.map(fingerprintMessage),
|
|
252
|
+
createdAt: input.createdAt ?? new Date().toISOString(),
|
|
253
|
+
};
|
|
254
|
+
const parsed = parseCheckpointDetails(details);
|
|
255
|
+
if (!parsed) throw new Error("Created an invalid Codex checkpoint");
|
|
256
|
+
return parsed;
|
|
257
|
+
}
|