@lll9p/pi-better-compaction 0.2.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lll9p
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,183 @@
1
+ # pi-better-compaction
2
+
3
+ A Pi extension that upgrades compaction with two coordinated strategies:
4
+
5
+ 1. **OpenAI Responses APIs** (`openai-responses`, `openai-codex-responses`) use the provider's
6
+ native `/responses/compact` endpoint, then replay the opaque compacted window on later
7
+ requests without patching Pi core.
8
+ 2. **Every other API** (Anthropic, Gemini, etc.) runs Pi's own native compaction method,
9
+ optionally driven by a **dedicated compaction model** so you can summarize with a cheaper/faster
10
+ model than the one you are chatting with.
11
+
12
+ Everything fails open: if any step cannot proceed, the extension returns control to Pi's default
13
+ compaction so a compaction never breaks because of this extension.
14
+
15
+ ## Requirements
16
+
17
+ - **Minimum Pi version:** `@earendil-works/pi-coding-agent >= 0.80.0`
18
+
19
+ This extension relies on `modelRegistry.getApiKeyAndHeaders(model)` and the exported native
20
+ `compact()` function.
21
+
22
+ ## Behavior
23
+
24
+ The `session_before_compact` decision tree:
25
+
26
+ ```
27
+ session_before_compact
28
+
29
+ ├─ config.enabled == false ───────────────────────► Pi default compaction
30
+
31
+ ├─ current model API is a Responses API
32
+ │ │ (openai-responses / openai-codex-responses, narrowable via config)
33
+ │ ├─ POST /responses/compact
34
+ │ │ ├─ success ─────────────────────────────────► store opaque window + real summary
35
+ │ │ ├─ user aborted ─────────────────────────────► cancel
36
+ │ │ └─ failure (404 / network / malformed) ──────► fall through ▼
37
+ │ └─ (missing base URL / API key) ─────────────────► fall through ▼
38
+
39
+ ├─ config.compactionModel is set and resolvable and ≠ current model
40
+ │ └─ run Pi's native compact() with that model ────► use its result
41
+
42
+ └─ otherwise ─────────────────────────────────────► Pi default compaction
43
+ (no model configured, or it equals the current model — Pi runs the
44
+ same native method itself, keeping its streaming progress UI)
45
+ ```
46
+
47
+ On the next supported Responses request after a native `/responses/compact`, the
48
+ `before_provider_request` hook rewrites Pi's summary-oriented replay into:
49
+
50
+ - fresh current prompt envelope
51
+ - stored opaque compacted window
52
+ - live post-compaction tail
53
+
54
+ Requests produced by the native-method fallback carry Pi's standard `{readFiles, modifiedFiles}`
55
+ details, so they replay through Pi's default path — no rewrite, no special handling.
56
+
57
+ ### Selection is by API, not provider
58
+
59
+ Any provider speaking a Responses API gets a native compact attempt, including OpenAI-compatible
60
+ proxies with a custom `baseUrl`. If such an endpoint does not implement `/responses/compact`, the
61
+ request 404s and the extension fails through to the configured fallback model (or Pi default). To
62
+ avoid the probe entirely for one API, narrow `responsesCompactApis`.
63
+
64
+ ## Install
65
+
66
+ From npm (recommended):
67
+
68
+ ```bash
69
+ pi install npm:@lll9p/pi-better-compaction
70
+ ```
71
+
72
+ Try it for a single run without installing:
73
+
74
+ ```bash
75
+ pi -e npm:@lll9p/pi-better-compaction
76
+ ```
77
+
78
+ From a checkout (development):
79
+
80
+ ```bash
81
+ git clone https://github.com/lll9p/pi-better-compaction.git
82
+ cd pi-better-compaction
83
+ pi install .
84
+ ```
85
+
86
+ After installation, run `/reload`.
87
+
88
+ ## Configuration
89
+
90
+ Single source, merged over built-in defaults:
91
+
92
+ ```
93
+ ~/.pi/agent/extensions/pi-better-compaction/config.json
94
+ ```
95
+
96
+ A missing file silently uses the defaults below. The extension never writes this file for you.
97
+
98
+ ```json
99
+ {
100
+ "enabled": true,
101
+
102
+ "compactionModel": "openai/gpt-5.1-mini",
103
+ "compactionThinkingLevel": "off",
104
+
105
+ "responsesCompactApis": ["openai-responses", "openai-codex-responses"],
106
+
107
+ "notifyOnLoad": false,
108
+ "debug": false,
109
+ "logProviderPayloads": false,
110
+ "logCompactResponses": false,
111
+ "redactSensitiveData": true,
112
+ "artifactRoot": "~/.pi/agent/artifacts/pi-better-compaction"
113
+ }
114
+ ```
115
+
116
+ | Key | Default | Description |
117
+ |-----|---------|-------------|
118
+ | `enabled` | `true` | Master switch. `false` → Pi default compaction everywhere. |
119
+ | `compactionModel` | *(unset)* | `"provider/model-id"` used for native-method fallback (non-Responses APIs, or when the compact endpoint fails). `null`/unset → use the current model via Pi's default path. The provider is split on the first `/`, so model ids may contain slashes (e.g. `"openrouter/deepseek/deepseek-chat"`). |
120
+ | `compactionThinkingLevel` | `"off"` | Thinking level passed to the native `compact()` fallback. One of `off, minimal, low, medium, high, xhigh, max`. |
121
+ | `responsesCompactApis` | both Responses APIs | Which Responses APIs use the compact endpoint. May only narrow the built-in set; unknown entries are ignored with a warning. |
122
+ | `notifyOnLoad` | `false` | Show a load notification in the TUI. |
123
+ | `debug` | `false` | Write lifecycle + compaction-event artifacts. |
124
+ | `logProviderPayloads` | `false` | Write `before_provider_request` payload artifacts. |
125
+ | `logCompactResponses` | `false` | Write compact endpoint request/response artifacts. |
126
+ | `redactSensitiveData` | `true` | Redact secrets in artifacts. Keep on. |
127
+ | `artifactRoot` | `~/.pi/agent/artifacts/pi-better-compaction` | Debug artifact root. `~/` and relative paths (resolved against the config dir) are supported. |
128
+
129
+ ### Codex-aligned compact request
130
+
131
+ For Responses compaction, the extension mirrors the latest codex_rs `CompactionInput` fields
132
+ (`tools`, `parallel_tool_calls`, `reasoning`, `service_tier`, `prompt_cache_key`, `text`) by
133
+ capturing them from the most recent live provider request for the same model/session and attaching
134
+ them to the compact request body. When no such request has been seen yet, the compact request falls
135
+ back to the minimal `model` / `input` / `instructions` body.
136
+
137
+ ## Debug artifacts
138
+
139
+ Written per session under:
140
+
141
+ ```
142
+ <artifactRoot>/sessions/<session-id>/
143
+ ├── provider-requests/
144
+ ├── compact-responses/
145
+ ├── compaction-events/
146
+ └── lifecycle/
147
+ ```
148
+
149
+ Troubleshooting flow:
150
+
151
+ 1. set `debug: true` and `logCompactResponses: true` (keep `redactSensitiveData: true`)
152
+ 2. `/reload`
153
+ 3. run `/compact`, then send a follow-up message
154
+ 4. inspect the newest artifact in the session directory
155
+
156
+ ## Package structure
157
+
158
+ ```
159
+ package-root/
160
+ ├── index.ts # entrypoint declared in package.json
161
+ ├── src/
162
+ │ ├── extension-runtime.ts # hook registration + compaction decision tree
163
+ │ ├── config.ts # config.json loader (single source + defaults)
164
+ │ ├── runtime.ts # API-based environment resolution + auth
165
+ │ ├── compact-client.ts # /responses/compact client + summary extraction
166
+ │ ├── native-fallback.ts # configured-model native compact() driver
167
+ │ ├── request-context-cache.ts # captures codex-aligned fields from live requests
168
+ │ ├── serializer.ts # Responses input serialization
169
+ │ ├── payload-rewrite.ts # native opaque-window replay rewrite
170
+ │ ├── details-store.ts # latest-valid native compaction lookup
171
+ │ ├── debug.ts # artifact writing + redaction
172
+ │ ├── supported-environment.ts # re-exports
173
+ │ └── types.ts # config + persisted native-compaction types
174
+ └── test/
175
+ ```
176
+
177
+ ## Tests
178
+
179
+ ```bash
180
+ bun test
181
+ bun test --coverage --coverage-reporter=text --coverage-reporter=lcov
182
+ bun test ./test/pi-smoke.test.ts
183
+ ```
package/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./src/extension-runtime.ts";
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@lll9p/pi-better-compaction",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Better compaction for pi: native /responses/compact replay for OpenAI Responses APIs, plus a configurable compaction model driving pi's native summarization everywhere else.",
6
+ "author": "Lilin Lao",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/lll9p/pi-better-compaction.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/lll9p/pi-better-compaction/issues"
14
+ },
15
+ "homepage": "https://github.com/lll9p/pi-better-compaction#readme",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "keywords": [
20
+ "pi-package",
21
+ "pi-extension",
22
+ "openai",
23
+ "compaction",
24
+ "codex",
25
+ "responses"
26
+ ],
27
+ "pi": {
28
+ "extensions": [
29
+ "./index.ts"
30
+ ]
31
+ },
32
+ "files": [
33
+ "index.ts",
34
+ "src/compact-client.ts",
35
+ "src/config.ts",
36
+ "src/debug.ts",
37
+ "src/details-store.ts",
38
+ "src/extension-runtime.ts",
39
+ "src/native-fallback.ts",
40
+ "src/payload-rewrite.ts",
41
+ "src/request-context-cache.ts",
42
+ "src/runtime.ts",
43
+ "src/serializer.ts",
44
+ "src/supported-environment.ts",
45
+ "src/types.ts",
46
+ "README.md",
47
+ "LICENSE"
48
+ ],
49
+ "scripts": {
50
+ "test": "bun test",
51
+ "test:coverage": "bun test --coverage --coverage-reporter=text --coverage-reporter=lcov && node ./scripts/check-lcov.mjs ./coverage/lcov.info",
52
+ "test:pi": "bun test ./test/pi-smoke.test.ts"
53
+ },
54
+ "peerDependencies": {
55
+ "@earendil-works/pi-agent-core": "*",
56
+ "@earendil-works/pi-ai": "*",
57
+ "@earendil-works/pi-coding-agent": ">=0.80.0"
58
+ }
59
+ }
@@ -0,0 +1,428 @@
1
+ import { writeDebugArtifact } from "./debug";
2
+ import type { NativeCompactionRuntime } from "./runtime";
3
+ import type { NativeCompactionRequestBody } from "./serializer";
4
+ import type { ArtifactContext, ExtensionConfig } from "./types";
5
+
6
+ const JSON_CONTENT_TYPE = "application/json";
7
+
8
+ type CompactResponseEnvelope = {
9
+ id?: string;
10
+ created_at?: number | string;
11
+ output: unknown[];
12
+ [key: string]: unknown;
13
+ };
14
+
15
+ export type NativeCompactionClientFailureReason =
16
+ | "aborted"
17
+ | "network-error"
18
+ | "non-2xx"
19
+ | "empty-body"
20
+ | "invalid-json"
21
+ | "malformed-response"
22
+ | "empty-output";
23
+
24
+ export type NativeCompactionClientSuccess = {
25
+ ok: true;
26
+ status: number;
27
+ compactedWindow: unknown[];
28
+ compactResponseId?: string;
29
+ createdAt?: string;
30
+ /** Assistant summary text extracted from the compact output, for CompactionEntry.summary. */
31
+ summaryText?: string;
32
+ response: CompactResponseEnvelope;
33
+ };
34
+
35
+ export type NativeCompactionClientFailure = {
36
+ ok: false;
37
+ reason: NativeCompactionClientFailureReason;
38
+ status?: number;
39
+ errorMessage?: string;
40
+ responseText?: string;
41
+ responseJson?: unknown;
42
+ };
43
+
44
+ export type NativeCompactionClientResult = NativeCompactionClientSuccess | NativeCompactionClientFailure;
45
+
46
+ export type ExecuteNativeCompactionOptions = {
47
+ runtime: NativeCompactionRuntime;
48
+ request: NativeCompactionRequestBody;
49
+ signal?: AbortSignal;
50
+ settings?: ExtensionConfig;
51
+ context?: ArtifactContext;
52
+ };
53
+
54
+ function isRecord(value: unknown): value is Record<string, unknown> {
55
+ return !!value && typeof value === "object" && !Array.isArray(value);
56
+ }
57
+
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
+ function normalizeResponseTimestamp(value: unknown): string | undefined {
66
+ if (typeof value === "number" && Number.isFinite(value)) {
67
+ const milliseconds = value > 1_000_000_000_000 ? value : value * 1000;
68
+ return new Date(milliseconds).toISOString();
69
+ }
70
+
71
+ if (typeof value !== "string") {
72
+ return undefined;
73
+ }
74
+
75
+ const trimmed = value.trim();
76
+ if (!trimmed) {
77
+ return undefined;
78
+ }
79
+
80
+ const parsed = Date.parse(trimmed);
81
+ return Number.isNaN(parsed) ? trimmed : new Date(parsed).toISOString();
82
+ }
83
+
84
+ function isCompactOutputItem(value: unknown): value is Record<string, unknown> {
85
+ return isRecord(value);
86
+ }
87
+
88
+ function isCompactResponseEnvelope(value: unknown): value is CompactResponseEnvelope {
89
+ return isRecord(value) && Array.isArray(value.output) && value.output.every(isCompactOutputItem);
90
+ }
91
+
92
+ /**
93
+ * Extract the assistant-authored summary text from the compacted window so the
94
+ * persisted CompactionEntry.summary carries real context. Without this, switching
95
+ * to a non-Responses model later would replay a meaningless placeholder.
96
+ */
97
+ export function extractCompactedSummaryText(output: readonly unknown[]): string | undefined {
98
+ const texts: string[] = [];
99
+ for (const item of output) {
100
+ if (!isRecord(item) || item.type !== "message" || item.role !== "assistant" || !Array.isArray(item.content)) {
101
+ continue;
102
+ }
103
+ for (const block of item.content) {
104
+ if (isRecord(block) && block.type === "output_text" && typeof block.text === "string" && block.text.trim()) {
105
+ texts.push(block.text.trim());
106
+ }
107
+ }
108
+ }
109
+
110
+ const joined = texts.join("\n\n").trim();
111
+ return joined.length > 0 ? joined : undefined;
112
+ }
113
+
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
+ function writeCompactArtifact(
171
+ data: unknown,
172
+ settings: ExtensionConfig | undefined,
173
+ context: ArtifactContext | undefined,
174
+ ): void {
175
+ if (!settings || !context) {
176
+ return;
177
+ }
178
+
179
+ writeDebugArtifact("compact-response", data, settings, context);
180
+ }
181
+
182
+ export async function executeNativeCompaction(
183
+ options: ExecuteNativeCompactionOptions,
184
+ ): Promise<NativeCompactionClientResult> {
185
+ const { runtime, request, signal, settings, context } = options;
186
+ const headers = toHeaders(runtime);
187
+
188
+ if (signal?.aborted) {
189
+ const aborted: NativeCompactionClientFailure = {
190
+ ok: false,
191
+ reason: "aborted",
192
+ };
193
+ writeCompactArtifact(
194
+ {
195
+ request: {
196
+ url: runtime.compactUrl,
197
+ headers,
198
+ body: request,
199
+ },
200
+ outcome: aborted,
201
+ },
202
+ settings,
203
+ context,
204
+ );
205
+ return aborted;
206
+ }
207
+
208
+ try {
209
+ const response = await fetch(runtime.compactUrl, {
210
+ method: "POST",
211
+ headers,
212
+ body: JSON.stringify(request),
213
+ signal,
214
+ });
215
+ const responseText = await response.text();
216
+ const responseHeaders: Record<string, string> = {};
217
+ response.headers.forEach((value, key) => {
218
+ responseHeaders[key] = value;
219
+ });
220
+
221
+ if (!response.ok) {
222
+ let responseJson: unknown;
223
+ if (responseText.trim().length > 0) {
224
+ try {
225
+ responseJson = JSON.parse(responseText);
226
+ } catch {
227
+ responseJson = undefined;
228
+ }
229
+ }
230
+
231
+ const failure: NativeCompactionClientFailure = {
232
+ ok: false,
233
+ reason: "non-2xx",
234
+ status: response.status,
235
+ responseText: responseText || undefined,
236
+ responseJson,
237
+ };
238
+ writeCompactArtifact(
239
+ {
240
+ request: {
241
+ url: runtime.compactUrl,
242
+ headers,
243
+ body: request,
244
+ },
245
+ response: {
246
+ status: response.status,
247
+ headers: responseHeaders,
248
+ body: responseJson ?? responseText,
249
+ },
250
+ outcome: failure,
251
+ },
252
+ settings,
253
+ context,
254
+ );
255
+ return failure;
256
+ }
257
+
258
+ if (!responseText.trim()) {
259
+ const failure: NativeCompactionClientFailure = {
260
+ ok: false,
261
+ reason: "empty-body",
262
+ status: response.status,
263
+ };
264
+ writeCompactArtifact(
265
+ {
266
+ request: {
267
+ url: runtime.compactUrl,
268
+ headers,
269
+ body: request,
270
+ },
271
+ response: {
272
+ status: response.status,
273
+ headers: responseHeaders,
274
+ body: responseText,
275
+ },
276
+ outcome: failure,
277
+ },
278
+ settings,
279
+ context,
280
+ );
281
+ return failure;
282
+ }
283
+
284
+ let parsed: unknown;
285
+ try {
286
+ parsed = JSON.parse(responseText);
287
+ } catch (error) {
288
+ const failure: NativeCompactionClientFailure = {
289
+ ok: false,
290
+ reason: "invalid-json",
291
+ status: response.status,
292
+ errorMessage: error instanceof Error ? error.message : String(error),
293
+ responseText,
294
+ };
295
+ writeCompactArtifact(
296
+ {
297
+ request: {
298
+ url: runtime.compactUrl,
299
+ headers,
300
+ body: request,
301
+ },
302
+ response: {
303
+ status: response.status,
304
+ headers: responseHeaders,
305
+ body: responseText,
306
+ },
307
+ outcome: failure,
308
+ },
309
+ settings,
310
+ context,
311
+ );
312
+ return failure;
313
+ }
314
+
315
+ if (!isCompactResponseEnvelope(parsed)) {
316
+ const failure: NativeCompactionClientFailure = {
317
+ ok: false,
318
+ reason: "malformed-response",
319
+ status: response.status,
320
+ responseJson: parsed,
321
+ };
322
+ writeCompactArtifact(
323
+ {
324
+ request: {
325
+ url: runtime.compactUrl,
326
+ headers,
327
+ body: request,
328
+ },
329
+ response: {
330
+ status: response.status,
331
+ headers: responseHeaders,
332
+ body: parsed,
333
+ },
334
+ outcome: failure,
335
+ },
336
+ settings,
337
+ context,
338
+ );
339
+ return failure;
340
+ }
341
+
342
+ if (parsed.output.length === 0) {
343
+ const failure: NativeCompactionClientFailure = {
344
+ ok: false,
345
+ reason: "empty-output",
346
+ status: response.status,
347
+ responseJson: parsed,
348
+ };
349
+ writeCompactArtifact(
350
+ {
351
+ request: {
352
+ url: runtime.compactUrl,
353
+ headers,
354
+ body: request,
355
+ },
356
+ response: {
357
+ status: response.status,
358
+ headers: responseHeaders,
359
+ body: parsed,
360
+ },
361
+ outcome: failure,
362
+ },
363
+ settings,
364
+ context,
365
+ );
366
+ return failure;
367
+ }
368
+
369
+ const success: NativeCompactionClientSuccess = {
370
+ ok: true,
371
+ status: response.status,
372
+ compactedWindow: [...parsed.output],
373
+ compactResponseId: typeof parsed.id === "string" && parsed.id.trim() ? parsed.id.trim() : undefined,
374
+ createdAt: normalizeResponseTimestamp(parsed.created_at),
375
+ summaryText: extractCompactedSummaryText(parsed.output),
376
+ response: parsed,
377
+ };
378
+ writeCompactArtifact(
379
+ {
380
+ request: {
381
+ url: runtime.compactUrl,
382
+ headers,
383
+ body: request,
384
+ },
385
+ response: {
386
+ status: response.status,
387
+ headers: responseHeaders,
388
+ body: parsed,
389
+ },
390
+ outcome: {
391
+ ok: true,
392
+ status: success.status,
393
+ compactResponseId: success.compactResponseId,
394
+ createdAt: success.createdAt,
395
+ compactedItems: success.compactedWindow.length,
396
+ },
397
+ },
398
+ settings,
399
+ context,
400
+ );
401
+ return success;
402
+ } catch (error) {
403
+ const failure: NativeCompactionClientFailure = isAbortError(error)
404
+ ? {
405
+ ok: false,
406
+ reason: "aborted",
407
+ }
408
+ : {
409
+ ok: false,
410
+ reason: "network-error",
411
+ errorMessage: error instanceof Error ? error.message : String(error),
412
+ };
413
+
414
+ writeCompactArtifact(
415
+ {
416
+ request: {
417
+ url: runtime.compactUrl,
418
+ headers,
419
+ body: request,
420
+ },
421
+ outcome: failure,
422
+ },
423
+ settings,
424
+ context,
425
+ );
426
+ return failure;
427
+ }
428
+ }