@lll9p/pi-better-compaction 0.2.1 → 0.5.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/README.md +103 -132
- package/README.zh-CN.md +162 -0
- package/package.json +7 -2
- package/src/compact-client-v2.ts +428 -0
- package/src/compact-client.ts +1 -63
- package/src/config.ts +38 -0
- package/src/extension-runtime.ts +247 -17
- package/src/midrun.ts +229 -0
- package/src/native-fallback.ts +21 -3
- package/src/retained-messages.ts +98 -0
- package/src/runtime.ts +41 -2
- package/src/shared-headers.ts +103 -0
- package/src/types.ts +30 -4
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V2 compaction streaming client.
|
|
3
|
+
*
|
|
4
|
+
* Sends a Responses API request with a `compaction_trigger` input item appended,
|
|
5
|
+
* streams the SSE response, and collects the encrypted `compaction` output blob.
|
|
6
|
+
*
|
|
7
|
+
* This is the pi extension equivalent of codex-rs `compact_remote_v2.rs`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { writeDebugArtifact } from "./debug";
|
|
11
|
+
import type { NativeCompactionRuntime } from "./runtime";
|
|
12
|
+
import type { NativeCompactionRequestBody } from "./serializer";
|
|
13
|
+
import { isAbortError, toHeaders } from "./shared-headers";
|
|
14
|
+
import type { ArtifactContext, ExtensionConfig } from "./types";
|
|
15
|
+
|
|
16
|
+
// ── Types ──────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export type CompactionItem = {
|
|
19
|
+
type: "compaction";
|
|
20
|
+
id?: string;
|
|
21
|
+
encrypted_content: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type V2CompactionUsage = {
|
|
25
|
+
input_tokens?: number;
|
|
26
|
+
output_tokens?: number;
|
|
27
|
+
total_tokens?: number;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type V2CompactionSuccess = {
|
|
32
|
+
ok: true;
|
|
33
|
+
compactionItem: CompactionItem;
|
|
34
|
+
responseId?: string;
|
|
35
|
+
createdAt?: string;
|
|
36
|
+
usage?: V2CompactionUsage;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type V2CompactionFailureReason =
|
|
40
|
+
| "aborted"
|
|
41
|
+
| "network-error"
|
|
42
|
+
| "non-2xx"
|
|
43
|
+
| "no-compaction-output"
|
|
44
|
+
| "multiple-compaction-outputs"
|
|
45
|
+
| "stream-parse-error"
|
|
46
|
+
| "retries-exhausted";
|
|
47
|
+
|
|
48
|
+
export type V2CompactionFailure = {
|
|
49
|
+
ok: false;
|
|
50
|
+
reason: V2CompactionFailureReason;
|
|
51
|
+
status?: number;
|
|
52
|
+
errorMessage?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type V2CompactionResult = V2CompactionSuccess | V2CompactionFailure;
|
|
56
|
+
|
|
57
|
+
export type ExecuteV2CompactionOptions = {
|
|
58
|
+
runtime: NativeCompactionRuntime;
|
|
59
|
+
request: NativeCompactionRequestBody;
|
|
60
|
+
signal?: AbortSignal;
|
|
61
|
+
maxRetries?: number;
|
|
62
|
+
settings?: ExtensionConfig;
|
|
63
|
+
context?: ArtifactContext;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ── Constants ──────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
const DEFAULT_MAX_RETRIES = 2;
|
|
69
|
+
const SSE_ACCEPT = "text/event-stream";
|
|
70
|
+
|
|
71
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
74
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isCompactionItem(item: unknown): item is CompactionItem {
|
|
78
|
+
return (
|
|
79
|
+
isRecord(item) &&
|
|
80
|
+
(item.type === "compaction" || item.type === "compaction_summary") &&
|
|
81
|
+
typeof item.encrypted_content === "string" &&
|
|
82
|
+
item.encrypted_content.length > 0
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function writeV2Artifact(
|
|
87
|
+
data: unknown,
|
|
88
|
+
settings: ExtensionConfig | undefined,
|
|
89
|
+
context: ArtifactContext | undefined,
|
|
90
|
+
): void {
|
|
91
|
+
if (!settings || !context) return;
|
|
92
|
+
writeDebugArtifact("compact-response", data, settings, context);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── SSE stream processing ──────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
type StreamCollectionResult =
|
|
98
|
+
| { ok: true; compactionItems: CompactionItem[]; responseId?: string; createdAt?: string; usage?: V2CompactionUsage }
|
|
99
|
+
| { ok: false; reason: V2CompactionFailureReason; errorMessage?: string };
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Read an SSE stream from a fetch Response and collect compaction output items.
|
|
103
|
+
*
|
|
104
|
+
* Expected SSE events:
|
|
105
|
+
* - `response.output_item.done` with a compaction item in `item`
|
|
106
|
+
* - `response.completed` with `response.id`, `response.usage`, `response.created_at`
|
|
107
|
+
* - `response.failed` / `error` for server-side errors
|
|
108
|
+
*/
|
|
109
|
+
async function collectStreamOutput(response: Response, signal?: AbortSignal): Promise<StreamCollectionResult> {
|
|
110
|
+
const body = response.body;
|
|
111
|
+
if (!body) {
|
|
112
|
+
return { ok: false, reason: "stream-parse-error", errorMessage: "Response body is null" };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const compactionItems: CompactionItem[] = [];
|
|
116
|
+
let responseId: string | undefined;
|
|
117
|
+
let createdAt: string | undefined;
|
|
118
|
+
let usage: V2CompactionUsage | undefined;
|
|
119
|
+
let serverError: string | undefined;
|
|
120
|
+
|
|
121
|
+
const reader = body.getReader();
|
|
122
|
+
const decoder = new TextDecoder();
|
|
123
|
+
let buffer = "";
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
while (true) {
|
|
127
|
+
if (signal?.aborted) {
|
|
128
|
+
reader.cancel();
|
|
129
|
+
return { ok: false, reason: "aborted" as const };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const { done, value } = await reader.read();
|
|
133
|
+
if (done) break;
|
|
134
|
+
|
|
135
|
+
buffer += decoder.decode(value, { stream: true });
|
|
136
|
+
const lines = buffer.split("\n");
|
|
137
|
+
// Keep the last incomplete line in the buffer.
|
|
138
|
+
buffer = lines.pop() ?? "";
|
|
139
|
+
|
|
140
|
+
for (const line of lines) {
|
|
141
|
+
const trimmed = line.trim();
|
|
142
|
+
if (!trimmed || trimmed.startsWith(":")) continue;
|
|
143
|
+
if (!trimmed.startsWith("data: ")) continue;
|
|
144
|
+
|
|
145
|
+
const jsonStr = trimmed.slice(6);
|
|
146
|
+
if (jsonStr === "[DONE]") continue;
|
|
147
|
+
|
|
148
|
+
let event: Record<string, unknown>;
|
|
149
|
+
try {
|
|
150
|
+
event = JSON.parse(jsonStr);
|
|
151
|
+
if (!isRecord(event)) continue;
|
|
152
|
+
} catch {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const eventType = event.type;
|
|
157
|
+
|
|
158
|
+
if (eventType === "response.output_item.done") {
|
|
159
|
+
const item = event.item;
|
|
160
|
+
if (isCompactionItem(item)) {
|
|
161
|
+
compactionItems.push({
|
|
162
|
+
type: "compaction",
|
|
163
|
+
id: typeof item.id === "string" ? item.id : undefined,
|
|
164
|
+
encrypted_content: item.encrypted_content,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (eventType === "response.completed") {
|
|
171
|
+
const resp = event.response;
|
|
172
|
+
if (isRecord(resp)) {
|
|
173
|
+
responseId = typeof resp.id === "string" ? resp.id : undefined;
|
|
174
|
+
createdAt = normalizeTimestamp(resp.created_at);
|
|
175
|
+
if (isRecord(resp.usage)) {
|
|
176
|
+
usage = resp.usage as V2CompactionUsage;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (eventType === "response.failed" || eventType === "error") {
|
|
183
|
+
const errorObj = event.error ?? event;
|
|
184
|
+
serverError = isRecord(errorObj)
|
|
185
|
+
? (typeof errorObj.message === "string" ? errorObj.message : JSON.stringify(errorObj))
|
|
186
|
+
: String(errorObj);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (isAbortError(error)) {
|
|
193
|
+
return { ok: false, reason: "aborted" as const };
|
|
194
|
+
}
|
|
195
|
+
return { ok: false, reason: "stream-parse-error", errorMessage: error instanceof Error ? error.message : String(error) };
|
|
196
|
+
} finally {
|
|
197
|
+
try { reader.releaseLock(); } catch { /* noop */ }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (serverError) {
|
|
201
|
+
return { ok: false, reason: "stream-parse-error", errorMessage: serverError };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { ok: true, compactionItems, responseId, createdAt, usage };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeTimestamp(value: unknown): string | undefined {
|
|
208
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
209
|
+
const ms = value > 1_000_000_000_000 ? value : value * 1000;
|
|
210
|
+
return new Date(ms).toISOString();
|
|
211
|
+
}
|
|
212
|
+
if (typeof value === "string" && value.trim()) {
|
|
213
|
+
const parsed = Date.parse(value.trim());
|
|
214
|
+
return Number.isNaN(parsed) ? value.trim() : new Date(parsed).toISOString();
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Single attempt ─────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
async function executeV2Attempt(
|
|
222
|
+
url: string,
|
|
223
|
+
requestBody: unknown,
|
|
224
|
+
headers: Record<string, string>,
|
|
225
|
+
signal?: AbortSignal,
|
|
226
|
+
): Promise<{ response?: Response; result?: StreamCollectionResult; failure?: V2CompactionFailure }> {
|
|
227
|
+
if (signal?.aborted) {
|
|
228
|
+
return { failure: { ok: false, reason: "aborted" } };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let response: Response;
|
|
232
|
+
try {
|
|
233
|
+
response = await fetch(url, {
|
|
234
|
+
method: "POST",
|
|
235
|
+
headers,
|
|
236
|
+
body: JSON.stringify(requestBody),
|
|
237
|
+
signal,
|
|
238
|
+
});
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (isAbortError(error)) {
|
|
241
|
+
return { failure: { ok: false, reason: "aborted" } };
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
failure: {
|
|
245
|
+
ok: false,
|
|
246
|
+
reason: "network-error",
|
|
247
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!response.ok) {
|
|
253
|
+
let errorMessage: string | undefined;
|
|
254
|
+
try {
|
|
255
|
+
const text = await response.text();
|
|
256
|
+
if (text.trim()) {
|
|
257
|
+
try {
|
|
258
|
+
const json = JSON.parse(text);
|
|
259
|
+
errorMessage = isRecord(json) && isRecord(json.error) && typeof json.error.message === "string"
|
|
260
|
+
? json.error.message
|
|
261
|
+
: text;
|
|
262
|
+
} catch {
|
|
263
|
+
errorMessage = text;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} catch { /* swallow */ }
|
|
267
|
+
return {
|
|
268
|
+
response,
|
|
269
|
+
failure: {
|
|
270
|
+
ok: false,
|
|
271
|
+
reason: "non-2xx",
|
|
272
|
+
status: response.status,
|
|
273
|
+
errorMessage,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const result = await collectStreamOutput(response, signal);
|
|
279
|
+
return { response, result };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── Retryable errors ───────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
function isRetryable(result: V2CompactionFailure): boolean {
|
|
285
|
+
return result.reason === "network-error" || result.reason === "stream-parse-error";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Public API ─────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Execute a V2 compaction request.
|
|
292
|
+
*
|
|
293
|
+
* Builds a Responses API streaming request with a `compaction_trigger` appended
|
|
294
|
+
* to the input, streams the SSE response, and collects the compaction blob.
|
|
295
|
+
*
|
|
296
|
+
* Retries recoverable failures up to `maxRetries` times (default 2).
|
|
297
|
+
*/
|
|
298
|
+
export async function executeV2Compaction(
|
|
299
|
+
options: ExecuteV2CompactionOptions,
|
|
300
|
+
): Promise<V2CompactionResult> {
|
|
301
|
+
const { runtime, request, signal, settings, context } = options;
|
|
302
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
303
|
+
|
|
304
|
+
const headers = toHeaders(runtime, SSE_ACCEPT);
|
|
305
|
+
const url = runtime.responsesUrl;
|
|
306
|
+
|
|
307
|
+
// Build request body: input + compaction_trigger, stream=true.
|
|
308
|
+
const requestBody = {
|
|
309
|
+
...request,
|
|
310
|
+
input: [...request.input, { type: "compaction_trigger" }],
|
|
311
|
+
stream: true,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
let lastFailure: V2CompactionFailure | undefined;
|
|
315
|
+
|
|
316
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
317
|
+
if (signal?.aborted) {
|
|
318
|
+
const aborted: V2CompactionFailure = { ok: false, reason: "aborted" };
|
|
319
|
+
writeV2Artifact(
|
|
320
|
+
{ request: { url, headers, body: requestBody }, attempt, outcome: aborted },
|
|
321
|
+
settings,
|
|
322
|
+
context,
|
|
323
|
+
);
|
|
324
|
+
return aborted;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const { result, failure } = await executeV2Attempt(url, requestBody, headers, signal);
|
|
328
|
+
|
|
329
|
+
if (failure) {
|
|
330
|
+
lastFailure = failure;
|
|
331
|
+
if (!isRetryable(failure) || attempt >= maxRetries) {
|
|
332
|
+
writeV2Artifact(
|
|
333
|
+
{ request: { url, headers, body: requestBody }, attempt, outcome: failure },
|
|
334
|
+
settings,
|
|
335
|
+
context,
|
|
336
|
+
);
|
|
337
|
+
return failure;
|
|
338
|
+
}
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!result) {
|
|
343
|
+
// Should not happen, but guard.
|
|
344
|
+
lastFailure = { ok: false, reason: "stream-parse-error", errorMessage: "No result from attempt" };
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!result.ok) {
|
|
349
|
+
lastFailure = { ok: false, reason: result.reason, errorMessage: result.errorMessage };
|
|
350
|
+
if (result.reason === "aborted" || !isRetryable(lastFailure) || attempt >= maxRetries) {
|
|
351
|
+
writeV2Artifact(
|
|
352
|
+
{ request: { url, headers, body: requestBody }, attempt, outcome: lastFailure },
|
|
353
|
+
settings,
|
|
354
|
+
context,
|
|
355
|
+
);
|
|
356
|
+
return lastFailure;
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Stream collected successfully. Validate compaction output.
|
|
362
|
+
if (result.compactionItems.length === 0) {
|
|
363
|
+
const noOutput: V2CompactionFailure = {
|
|
364
|
+
ok: false,
|
|
365
|
+
reason: "no-compaction-output",
|
|
366
|
+
};
|
|
367
|
+
writeV2Artifact(
|
|
368
|
+
{ request: { url, headers, body: requestBody }, attempt, outcome: noOutput },
|
|
369
|
+
settings,
|
|
370
|
+
context,
|
|
371
|
+
);
|
|
372
|
+
return noOutput;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (result.compactionItems.length > 1) {
|
|
376
|
+
const multiOutput: V2CompactionFailure = {
|
|
377
|
+
ok: false,
|
|
378
|
+
reason: "multiple-compaction-outputs",
|
|
379
|
+
errorMessage: `Expected 1 compaction item, got ${result.compactionItems.length}`,
|
|
380
|
+
};
|
|
381
|
+
writeV2Artifact(
|
|
382
|
+
{ request: { url, headers, body: requestBody }, attempt, outcome: multiOutput },
|
|
383
|
+
settings,
|
|
384
|
+
context,
|
|
385
|
+
);
|
|
386
|
+
return multiOutput;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const success: V2CompactionSuccess = {
|
|
390
|
+
ok: true,
|
|
391
|
+
compactionItem: result.compactionItems[0]!,
|
|
392
|
+
responseId: result.responseId,
|
|
393
|
+
createdAt: result.createdAt,
|
|
394
|
+
usage: result.usage,
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
writeV2Artifact(
|
|
398
|
+
{
|
|
399
|
+
request: { url, headers, body: requestBody },
|
|
400
|
+
attempt,
|
|
401
|
+
outcome: {
|
|
402
|
+
ok: true,
|
|
403
|
+
responseId: success.responseId,
|
|
404
|
+
createdAt: success.createdAt,
|
|
405
|
+
compactionItemId: success.compactionItem.id,
|
|
406
|
+
usage: success.usage,
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
settings,
|
|
410
|
+
context,
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
return success;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// All retries exhausted.
|
|
417
|
+
const exhausted: V2CompactionFailure = {
|
|
418
|
+
ok: false,
|
|
419
|
+
reason: "retries-exhausted",
|
|
420
|
+
errorMessage: lastFailure?.errorMessage ?? "All retry attempts failed",
|
|
421
|
+
};
|
|
422
|
+
writeV2Artifact(
|
|
423
|
+
{ request: { url, headers, body: requestBody }, maxRetries, outcome: exhausted },
|
|
424
|
+
settings,
|
|
425
|
+
context,
|
|
426
|
+
);
|
|
427
|
+
return exhausted;
|
|
428
|
+
}
|
package/src/compact-client.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeDebugArtifact } from "./debug";
|
|
2
2
|
import type { NativeCompactionRuntime } from "./runtime";
|
|
3
3
|
import type { NativeCompactionRequestBody } from "./serializer";
|
|
4
|
+
import { isAbortError, toHeaders } from "./shared-headers";
|
|
4
5
|
import type { ArtifactContext, ExtensionConfig } from "./types";
|
|
5
6
|
|
|
6
7
|
const JSON_CONTENT_TYPE = "application/json";
|
|
@@ -55,13 +56,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
55
56
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
function isAbortError(error: unknown): boolean {
|
|
59
|
-
return (
|
|
60
|
-
(error instanceof DOMException && error.name === "AbortError") ||
|
|
61
|
-
(error instanceof Error && (error.name === "AbortError" || error.name === "ABORT_ERR"))
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
59
|
function normalizeResponseTimestamp(value: unknown): string | undefined {
|
|
66
60
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
67
61
|
const milliseconds = value > 1_000_000_000_000 ? value : value * 1000;
|
|
@@ -111,62 +105,6 @@ export function extractCompactedSummaryText(output: readonly unknown[]): string
|
|
|
111
105
|
return joined.length > 0 ? joined : undefined;
|
|
112
106
|
}
|
|
113
107
|
|
|
114
|
-
function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
|
|
115
|
-
const parts = token.split(".");
|
|
116
|
-
if (parts.length !== 3) {
|
|
117
|
-
return undefined;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
const payloadText = Buffer.from(parts[1]!, "base64url").toString("utf8");
|
|
122
|
-
const payload = JSON.parse(payloadText);
|
|
123
|
-
return isRecord(payload) ? payload : undefined;
|
|
124
|
-
} catch {
|
|
125
|
-
return undefined;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function extractCodexAccountId(token: string): string | undefined {
|
|
130
|
-
const payload = decodeJwtPayload(token);
|
|
131
|
-
const authClaims = payload?.["https://api.openai.com/auth"];
|
|
132
|
-
if (!isRecord(authClaims)) {
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const accountId = authClaims.chatgpt_account_id;
|
|
137
|
-
return typeof accountId === "string" && accountId.trim().length > 0 ? accountId.trim() : undefined;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function buildCodexUserAgent(): string {
|
|
141
|
-
const platform = typeof process !== "undefined" ? process.platform : "browser";
|
|
142
|
-
const arch = typeof process !== "undefined" ? process.arch : "unknown";
|
|
143
|
-
return `pi (${platform}; ${arch})`;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
function toHeaders(runtime: NativeCompactionRuntime): Record<string, string> {
|
|
147
|
-
const headers = new Headers(runtime.currentModel.headers ?? {});
|
|
148
|
-
for (const [key, value] of Object.entries(runtime.headers ?? {})) {
|
|
149
|
-
headers.set(key, value);
|
|
150
|
-
}
|
|
151
|
-
headers.set("accept", JSON_CONTENT_TYPE);
|
|
152
|
-
headers.set("content-type", JSON_CONTENT_TYPE);
|
|
153
|
-
if (!headers.has("authorization")) {
|
|
154
|
-
headers.set("authorization", `Bearer ${runtime.apiKey}`);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (runtime.api === "openai-codex-responses") {
|
|
158
|
-
const accountId = extractCodexAccountId(runtime.apiKey);
|
|
159
|
-
if (accountId) {
|
|
160
|
-
headers.set("chatgpt-account-id", accountId);
|
|
161
|
-
}
|
|
162
|
-
headers.set("originator", "pi");
|
|
163
|
-
headers.set("user-agent", buildCodexUserAgent());
|
|
164
|
-
headers.set("openai-beta", "responses=experimental");
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return Object.fromEntries(headers.entries());
|
|
168
|
-
}
|
|
169
|
-
|
|
170
108
|
function writeCompactArtifact(
|
|
171
109
|
data: unknown,
|
|
172
110
|
settings: ExtensionConfig | undefined,
|
package/src/config.ts
CHANGED
|
@@ -3,10 +3,12 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
5
5
|
import {
|
|
6
|
+
COMPACTION_VERSIONS,
|
|
6
7
|
DEFAULT_EXTENSION_CONFIG,
|
|
7
8
|
EXTENSION_ID,
|
|
8
9
|
RESPONSES_COMPACT_CAPABLE_APIS,
|
|
9
10
|
THINKING_LEVELS,
|
|
11
|
+
type CompactionVersion,
|
|
10
12
|
type ExtensionConfig,
|
|
11
13
|
type LoadedExtensionConfig,
|
|
12
14
|
} from "./types";
|
|
@@ -58,6 +60,15 @@ function toBoolean(value: unknown, fieldPath: string, warnings: string[]): boole
|
|
|
58
60
|
return undefined;
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
function toThresholdPercent(value: unknown, fieldPath: string, warnings: string[]): number | undefined {
|
|
64
|
+
if (value === undefined) return undefined;
|
|
65
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0 && value <= 100) {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
warnings.push(`Ignoring ${fieldPath}: expected a number greater than 0 and at most 100.`);
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
61
72
|
function toModelSpec(value: unknown, fieldPath: string, warnings: string[]): string | null | undefined {
|
|
62
73
|
if (value === undefined) return undefined;
|
|
63
74
|
// Explicit null clears a spec, matching the documented "unset = current model" behavior.
|
|
@@ -78,6 +89,15 @@ function toThinkingLevel(value: unknown, fieldPath: string, warnings: string[]):
|
|
|
78
89
|
return undefined;
|
|
79
90
|
}
|
|
80
91
|
|
|
92
|
+
function toCompactionVersion(value: unknown, fieldPath: string, warnings: string[]): CompactionVersion | undefined {
|
|
93
|
+
if (value === undefined) return undefined;
|
|
94
|
+
if (typeof value === "string" && (COMPACTION_VERSIONS as readonly string[]).includes(value)) {
|
|
95
|
+
return value as CompactionVersion;
|
|
96
|
+
}
|
|
97
|
+
warnings.push(`Ignoring ${fieldPath}: expected one of ${COMPACTION_VERSIONS.join(", ")}.`);
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
81
101
|
function toResponsesCompactApis(value: unknown, fieldPath: string, warnings: string[]): string[] | undefined {
|
|
82
102
|
if (value === undefined) return undefined;
|
|
83
103
|
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
@@ -108,6 +128,7 @@ export function loadExtensionConfig(configPath: string = CONFIG_PATH): LoadedExt
|
|
|
108
128
|
const warnings: string[] = [];
|
|
109
129
|
const resolved: ExtensionConfig = {
|
|
110
130
|
...DEFAULT_EXTENSION_CONFIG,
|
|
131
|
+
midRun: { ...DEFAULT_EXTENSION_CONFIG.midRun },
|
|
111
132
|
responsesCompactApis: [...DEFAULT_EXTENSION_CONFIG.responsesCompactApis],
|
|
112
133
|
};
|
|
113
134
|
let source: string | undefined;
|
|
@@ -117,6 +138,19 @@ export function loadExtensionConfig(configPath: string = CONFIG_PATH): LoadedExt
|
|
|
117
138
|
source = configPath;
|
|
118
139
|
|
|
119
140
|
resolved.enabled = toBoolean(raw.enabled, "enabled", warnings) ?? resolved.enabled;
|
|
141
|
+
|
|
142
|
+
if (raw.midRun === undefined) {
|
|
143
|
+
// Keep defaults.
|
|
144
|
+
} else if (isRecord(raw.midRun)) {
|
|
145
|
+
resolved.midRun.enabled =
|
|
146
|
+
toBoolean(raw.midRun.enabled, "midRun.enabled", warnings) ?? resolved.midRun.enabled;
|
|
147
|
+
resolved.midRun.thresholdPercent =
|
|
148
|
+
toThresholdPercent(raw.midRun.thresholdPercent, "midRun.thresholdPercent", warnings) ??
|
|
149
|
+
resolved.midRun.thresholdPercent;
|
|
150
|
+
} else {
|
|
151
|
+
warnings.push("Ignoring midRun: expected a JSON object.");
|
|
152
|
+
}
|
|
153
|
+
|
|
120
154
|
resolved.allowCompactionContinuityBreak =
|
|
121
155
|
toBoolean(raw.allowCompactionContinuityBreak, "allowCompactionContinuityBreak", warnings) ??
|
|
122
156
|
resolved.allowCompactionContinuityBreak;
|
|
@@ -143,6 +177,10 @@ export function loadExtensionConfig(configPath: string = CONFIG_PATH): LoadedExt
|
|
|
143
177
|
resolved.responsesCompactApis = apis;
|
|
144
178
|
}
|
|
145
179
|
|
|
180
|
+
resolved.compactionVersion =
|
|
181
|
+
toCompactionVersion(raw.compactionVersion, "compactionVersion", warnings) ??
|
|
182
|
+
resolved.compactionVersion;
|
|
183
|
+
|
|
146
184
|
if (typeof raw.artifactRoot === "string" && raw.artifactRoot.trim().length > 0) {
|
|
147
185
|
resolved.artifactRoot = raw.artifactRoot.trim();
|
|
148
186
|
} else if (raw.artifactRoot !== undefined) {
|