pi-openai-toolkit 0.20.8 → 0.21.1

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/src/config.ts CHANGED
@@ -7,6 +7,7 @@ import { CONFIG_PATH, normalizeLegacyConfig } from "./config/legacy";
7
7
  import { resolveV2Config } from "./config/v2";
8
8
  import {
9
9
  CONFIG_FEATURES,
10
+ RETIRED_AUTO_MODE_ISSUE,
10
11
  createPolicyDefaults,
11
12
  type ConfigDocumentSnapshot,
12
13
  type ConfigFeature,
@@ -116,7 +117,6 @@ const LEGACY_PATHS: Record<string, string> = {
116
117
  "imageGeneration.enabled": "imageGeneration.enabled",
117
118
  "imageGeneration.defaultModel": "imageGeneration.models",
118
119
  "imageGeneration.allowedModels": "imageGeneration.models",
119
- "autoMode.available": "autoMode.models",
120
120
  "compatibility.transport": "compaction.gatewayContextModels",
121
121
  "diagnostics.level": "compaction.debug",
122
122
  "diagnostics.notifyOnLoad": "compaction.notifyOnLoad",
@@ -155,13 +155,17 @@ function legacyIssues(loaded: LoadedToolkitConfig, modelKey: string | undefined)
155
155
  }
156
156
  }
157
157
  const issues = loaded.warnings.filter((warning) => !warning.startsWith("Ignoring webSearch.routes.")).map((warning): ConfigIssue => {
158
+ // A removed feature is retired, not invalid: it never blocks another feature.
159
+ if (warning.startsWith("Ignoring autoMode:")) {
160
+ return { severity: "warning", code: RETIRED_AUTO_MODE_ISSUE, path: "autoMode", feature: "document" };
161
+ }
158
162
  const match = /^Ignoring ([^:]+):/.exec(warning);
159
163
  const field = warning.startsWith("Ignoring compaction.responsesApis entry ") ? "compaction.responsesApis" : match?.[1] ?? "$";
160
164
  const root = field.split(".")[0];
161
165
  const diagnostic = Object.entries(LEGACY_PATHS).some(([leaf, input]) => leaf.startsWith("diagnostics.") && input === field);
162
166
  const feature: ConfigFeature | "document" = diagnostic ? "diagnostics"
163
167
  : field === "compaction.gatewayContextModels" ? "compatibility" : root === "compaction" ? "context"
164
- : root === "webSearch" || root === "imageGeneration" || root === "autoMode" ? root : "document";
168
+ : root === "webSearch" || root === "imageGeneration" ? root : "document";
165
169
  const supersededDefault = field === "webSearch.defaultRoute" && legacyRouteKey(raw, modelKey) !== undefined;
166
170
  const warningOnly = warning.endsWith("unknown field.") || !match
167
171
  || warning.includes("expected at least one model id") || supersededDefault;
@@ -209,13 +213,6 @@ function fromLegacy(loaded: LoadedToolkitConfig, model: WebSearchModel | undefin
209
213
  allowedModels: [...config.imageGeneration.models],
210
214
  };
211
215
  policy.responsesWebSocket = { enabled: false };
212
- const { enabled, models, reviewerModel, classifier, ...auto } = config.autoMode;
213
- policy.autoMode = {
214
- ...structuredClone(auto),
215
- available: enabled && modelKey !== undefined && models.includes(modelKey),
216
- reviewerModel: reviewerModel ?? null,
217
- classifier: { ...classifier, model: classifier.model ?? null },
218
- };
219
216
  policy.compatibility.transport = modelKey && compaction.gatewayContextModels.includes(modelKey) ? "codex-gateway" : "standard";
220
217
  policy.diagnostics = {
221
218
  level: compaction.debug ? "debug" : "info",
@@ -230,7 +227,6 @@ function fromLegacy(loaded: LoadedToolkitConfig, model: WebSearchModel | undefin
230
227
  for (const leaf of leafPaths(policy)) {
231
228
  let input = LEGACY_PATHS[leaf] ?? leaf;
232
229
  if (leaf === "context.mode" && !compaction.enabled) input = "compaction.enabled";
233
- if (leaf === "autoMode.available" && !enabled) input = "autoMode.enabled";
234
230
  if (leaf === "webSearch.route") {
235
231
  if (!config.webSearch.enabled) input = "webSearch.enabled";
236
232
  else if (route.source === "exact") {
@@ -251,9 +247,8 @@ function fromLegacy(loaded: LoadedToolkitConfig, model: WebSearchModel | undefin
251
247
  }
252
248
 
253
249
  /** Internal engine adapter. All interpretation of document names stays in this module. */
254
- function engineConfig(policy: EffectiveToolkitPolicy, modelKey: string | undefined, gatewayKeys: string[]): ToolkitConfig {
255
- const { context, diagnostics, imageGeneration, autoMode } = policy;
256
- const { available, reviewerModel, classifier, ...auto } = autoMode;
250
+ function engineConfig(policy: EffectiveToolkitPolicy, gatewayKeys: string[]): ToolkitConfig {
251
+ const { context, diagnostics, imageGeneration } = policy;
257
252
  return {
258
253
  compaction: {
259
254
  enabled: context.mode !== "pi",
@@ -276,10 +271,6 @@ function engineConfig(policy: EffectiveToolkitPolicy, modelKey: string | undefin
276
271
  webSearch: policy.webSearch.route === "unmanaged" ? { enabled: false, models: [] }
277
272
  : { enabled: true, models: [], defaultRoute: policy.webSearch.route },
278
273
  imageGeneration: { enabled: imageGeneration.enabled, models: [...imageGeneration.allowedModels] },
279
- autoMode: {
280
- ...structuredClone(auto), enabled: available, models: available && modelKey ? [modelKey] : [],
281
- reviewerModel: reviewerModel ?? undefined, classifier: { ...classifier, model: classifier.model ?? undefined },
282
- },
283
274
  };
284
275
  }
285
276
 
@@ -302,7 +293,7 @@ export function resolveToolkitConfig(loaded: LoadedToolkitConfig, model?: ExactM
302
293
  for (const origin of Object.values(resolution.origins)) if (origin.kind !== "builtin") origin.source = source;
303
294
  }
304
295
  const gatewayModelKeys = v2 ? v2.gatewayModelKeys : [...loaded.config.compaction.gatewayContextModels];
305
- const config = format === "v2" ? engineConfig(resolution.policy, modelKey, gatewayModelKeys) : structuredClone(loaded.config);
296
+ const config = format === "v2" ? engineConfig(resolution.policy, gatewayModelKeys) : structuredClone(loaded.config);
306
297
  return freeze({ ...resolution, config, format, source, modelKey, gatewayModelKeys, snapshot: structuredClone(loaded) });
307
298
  }
308
299
 
@@ -2,8 +2,6 @@ import { randomUUID } from "node:crypto";
2
2
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
3
3
  import { buildSessionContext, estimateTokens, type ExtensionAPI, type ExtensionContext, type SessionBeforeCompactEvent, type SessionEntry } from "@earendil-works/pi-coding-agent";
4
4
  import { resolveToolkitConfig, type ResolvedToolkitConfig } from "../config";
5
- import { isAutoModeEligible } from "../auto-mode/policy";
6
- import { protectAutoModeSelection } from "../auto-mode/model-selection-guard";
7
5
  import { parseModelSpec } from "../runtime";
8
6
  import { THINKING_LEVELS } from "../types";
9
7
  import { resolveCodexContextProvider } from "./codex-provider";
@@ -88,7 +86,6 @@ export class ManagedManualCompact implements ContextRolloverHandoff {
88
86
  private sourcePolicy: ResolvedToolkitConfig | undefined;
89
87
  private instructions = "";
90
88
  private attemptSignal: AbortSignal | undefined;
91
- private releaseApproval: (() => void) | undefined;
92
89
  private selecting: string | undefined;
93
90
  private selectionPromise: Promise<boolean> | undefined;
94
91
  private expectedThinking: ThinkingLevel | undefined;
@@ -123,8 +120,6 @@ export class ManagedManualCompact implements ContextRolloverHandoff {
123
120
  private clearActivity(): void {
124
121
  clearTimeout(this.deliveryTimer);
125
122
  this.deliveryTimer = undefined;
126
- this.releaseApproval?.();
127
- this.releaseApproval = undefined;
128
123
  this.sourcePolicy = undefined;
129
124
  }
130
125
  private belongs(ctx: ExtensionContext, record = this.record): boolean {
@@ -193,12 +188,6 @@ export class ManagedManualCompact implements ContextRolloverHandoff {
193
188
  if (!originalProvider.ok || !targetProvider.ok || !sameContextScope(originalProvider.provider, targetProvider.provider)) {
194
189
  throw new Error("checkpoint model must authenticate to the same context backend/account");
195
190
  }
196
- const protection = protectAutoModeSelection(this.pi, {
197
- sessionId: this.record!.sessionId, operationId: this.record!.operationId, target,
198
- eligible: !targetPolicy.invalidFeatures.includes("autoMode") && isAutoModeEligible(target, targetPolicy.config.autoMode),
199
- });
200
- this.releaseApproval = protection.release;
201
- if (!protection.allowed) throw new Error("checkpoint model would disable the engaged approval gate; allowlist it or choose another model");
202
191
  this.notify(ctx, `checkpoint handoff requested (${this.record!.originalModel} → ${targetKey}); native compaction is intentionally cancelled.`);
203
192
  } catch (error) {
204
193
  if (!this.current(ctx, generation)) return;
package/src/debug.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import * as fs from "node:fs";
2
3
  import * as os from "node:os";
3
4
  import * as path from "node:path";
@@ -157,7 +158,7 @@ export function writeDebugArtifact(
157
158
  ensureDir(targetDir);
158
159
 
159
160
  const timestamp = new Date().toISOString();
160
- const fileName = `${timestamp.replace(/[.:]/g, "-")}-${kind}.json`;
161
+ const fileName = `${timestamp.replace(/[.:]/g, "-")}-${kind}-${randomUUID()}.json`;
161
162
  const filePath = path.join(targetDir, fileName);
162
163
  const criticallyRedactedData = redactCriticalValue(data);
163
164
  const envelope: DebugArtifactEnvelope = {
@@ -174,7 +175,7 @@ export function writeDebugArtifact(
174
175
  data: settings.redactSensitiveData ? redactValue(criticallyRedactedData) : criticallyRedactedData,
175
176
  };
176
177
 
177
- fs.writeFileSync(filePath, `${JSON.stringify(envelope, null, 2)}\n`, "utf8");
178
+ fs.writeFileSync(filePath, `${JSON.stringify(envelope, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
178
179
  return filePath;
179
180
  }
180
181
 
@@ -206,7 +207,7 @@ export function writeReplayFailureArtifact(
206
207
  ensureDir(targetDir);
207
208
 
208
209
  const timestamp = new Date().toISOString();
209
- const filePath = path.join(targetDir, `${timestamp.replace(/[.:]/g, "-")}-replay-failure.json`);
210
+ const filePath = path.join(targetDir, `${timestamp.replace(/[.:]/g, "-")}-replay-failure-${randomUUID()}.json`);
210
211
  const data = redactCriticalValue({
211
212
  event: "before_provider_request.rewrite-failed",
212
213
  reason: details.reason,
@@ -227,7 +228,7 @@ export function writeReplayFailureArtifact(
227
228
  redaction: { enabled: true },
228
229
  data,
229
230
  };
230
- fs.writeFileSync(filePath, `${JSON.stringify(envelope, null, 2)}\n`, "utf8");
231
+ fs.writeFileSync(filePath, `${JSON.stringify(envelope, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
231
232
  return filePath;
232
233
  } catch {
233
234
  // The replay failure path itself must never be masked by an artifact write
@@ -35,6 +35,8 @@ import {
35
35
  type UnprojectedCompactionReason,
36
36
  } from "./compaction-projection";
37
37
  import { CODEX_GATEWAY_FORWARD_HEADERS } from "./responses-headers";
38
+ import { executeNativeCompaction, type NativeCompactionClientResult } from "./compact-client";
39
+ import { qualifiesForStandaloneCompaction, type CompactionErrorInfo } from "./compaction-errors";
38
40
  import { resolveLatestNativeCompactionEntry } from "./details-store";
39
41
  import {
40
42
  getPiContextHookProjector,
@@ -57,6 +59,7 @@ import {
57
59
  resolveEffectiveModel,
58
60
  resolveNativeCompactionEnvironment,
59
61
  resolveRemoteCompactionExecution,
62
+ isStandaloneCompactionEligible,
60
63
  parseModelSpec,
61
64
  type RemoteCompactionExecution,
62
65
  } from "./runtime";
@@ -70,6 +73,8 @@ import {
70
73
  createNativeCompactionDetails,
71
74
  createNativeCompactionResult,
72
75
  COMPACTION_EXTENSION_ID,
76
+ LEGACY_NATIVE_COMPACTION_STRATEGY,
77
+ REMOTE_V2_COMPACTION_STRATEGY,
73
78
  getLatestDeferredToolCarryover,
74
79
  getRemoteV2InputProvenance,
75
80
  isNativeCompactionDetails,
@@ -107,6 +112,7 @@ function compactGatewayModels(resolved: ResolvedToolkitConfig): readonly string[
107
112
  type CompactionDependencies = {
108
113
  loadConfig: typeof loadToolkitConfig;
109
114
  remoteCompact: typeof executeRemoteV2Compaction;
115
+ standaloneCompact: typeof executeNativeCompaction;
110
116
  nativeFallback: typeof runNativeFallbackCompaction;
111
117
  contextWindows: CodexContextWindowManager;
112
118
  manualCompact?: ManagedManualCompact;
@@ -132,7 +138,7 @@ type ContextToolSyncOutcome = {
132
138
  type ResponsesCompactOutcome =
133
139
  | { outcome: "success"; compaction: CompactionResult<NativeCompactionDetails> }
134
140
  | { outcome: "aborted" }
135
- | { outcome: "failed" }
141
+ | { outcome: "failed"; remoteFailure?: CompactionErrorInfo & { reason: string; status?: number }; protocol?: string }
136
142
  | {
137
143
  outcome: "unprojected-input";
138
144
  reason: UnprojectedCompactionReason;
@@ -343,6 +349,8 @@ async function runResponsesNativeCompact(
343
349
  execution: RemoteCompactionExecution,
344
350
  remoteCompact: typeof executeRemoteV2Compaction,
345
351
  projectCompactionContext: CompactionContextProjection | undefined,
352
+ /** Only ordinary compaction supplies this; managed recovery remains V2-only. */
353
+ standaloneCompact?: typeof executeNativeCompaction,
346
354
  ): Promise<ResponsesCompactOutcome> {
347
355
  const { consumer, compactor } = execution;
348
356
  const instructions = buildCompactionInstructions(ctx.getSystemPrompt(), event.customInstructions);
@@ -477,7 +485,9 @@ async function runResponsesNativeCompact(
477
485
  request = { ...request, ...extras };
478
486
  }
479
487
 
480
- const compactResult = await remoteCompact({
488
+ if (event.signal?.aborted) return { outcome: "aborted" };
489
+ let protocol: "remote-v2" | "standalone-compact" = "remote-v2";
490
+ let compactResult: Awaited<ReturnType<typeof executeRemoteV2Compaction>> | NativeCompactionClientResult = await remoteCompact({
481
491
  runtime: compactor,
482
492
  request,
483
493
  signal: event.signal,
@@ -485,24 +495,46 @@ async function runResponsesNativeCompact(
485
495
  context: ctx,
486
496
  });
487
497
 
488
- if (compactResult.ok === false) {
489
- writeDebugArtifact(
490
- "compaction-event",
491
- {
492
- event: "session_before_compact.remote-v2-failure",
498
+ if (!compactResult.ok) {
499
+ if (compactResult.reason === "aborted" || event.signal?.aborted) return { outcome: "aborted" };
500
+ const tryStandalone = !!standaloneCompact && isStandaloneCompactionEligible(compactor) && qualifiesForStandaloneCompaction(compactResult);
501
+ writeDebugArtifact("compaction-event", {
502
+ event: "session_before_compact.remote-v2-failure",
503
+ protocol,
504
+ reason: compactResult.reason,
505
+ status: compactResult.status,
506
+ errorCode: compactResult.errorCode,
507
+ errorType: compactResult.errorType,
508
+ errorParam: compactResult.errorParam,
509
+ errorMessage: compactResult.errorMessage,
510
+ nextStep: tryStandalone ? "standalone-compact" : "native-summary",
511
+ }, config, ctx);
512
+ if (tryStandalone && standaloneCompact) {
513
+ if (event.signal?.aborted) return { outcome: "aborted" };
514
+ protocol = "standalone-compact";
515
+ compactResult = await standaloneCompact({ runtime: compactor, request, signal: event.signal, settings: config, context: ctx });
516
+ }
517
+ if (!compactResult.ok) {
518
+ if (compactResult.reason === "aborted" || event.signal?.aborted) return { outcome: "aborted" };
519
+ const remoteFailure = {
493
520
  reason: compactResult.reason,
494
521
  status: compactResult.status,
522
+ errorCode: compactResult.errorCode,
523
+ errorType: compactResult.errorType,
524
+ errorParam: compactResult.errorParam,
495
525
  errorMessage: compactResult.errorMessage,
496
- },
497
- config,
498
- ctx,
499
- );
500
- return compactResult.reason === "aborted" ? { outcome: "aborted" } : { outcome: "failed" };
526
+ };
527
+ if (protocol === "standalone-compact") writeDebugArtifact("compaction-event", {
528
+ event: "session_before_compact.standalone-compact-failure", protocol, ...remoteFailure, nextStep: "native-summary",
529
+ }, config, ctx);
530
+ return { outcome: "failed", remoteFailure, protocol };
531
+ }
501
532
  }
502
533
 
503
534
  let details: NativeCompactionDetails;
504
535
  try {
505
536
  details = createNativeCompactionDetails({
537
+ strategy: protocol === "standalone-compact" ? LEGACY_NATIVE_COMPACTION_STRATEGY : REMOTE_V2_COMPACTION_STRATEGY,
506
538
  provider: consumer.provider,
507
539
  api: consumer.api,
508
540
  model: consumer.model,
@@ -542,7 +574,7 @@ async function runResponsesNativeCompact(
542
574
  config,
543
575
  ctx,
544
576
  );
545
- return { outcome: "failed" };
577
+ return { outcome: "failed", remoteFailure: { reason: "invalid-native-details" }, protocol };
546
578
  }
547
579
 
548
580
  const compaction = createNativeCompactionResult({
@@ -554,7 +586,8 @@ async function runResponsesNativeCompact(
554
586
  writeDebugArtifact(
555
587
  "compaction-event",
556
588
  {
557
- event: "session_before_compact.remote-v2-success",
589
+ event: `session_before_compact.${protocol}-success`,
590
+ protocol,
558
591
  consumer: {
559
592
  provider: consumer.provider,
560
593
  api: consumer.api,
@@ -829,6 +862,7 @@ async function handleSessionBeforeCompact(
829
862
  resolution.execution,
830
863
  dependencies.remoteCompact,
831
864
  dependencies.projectCompactionContext,
865
+ dependencies.standaloneCompact,
832
866
  );
833
867
  if (responsesOutcome.outcome === "success") {
834
868
  return { compaction: responsesOutcome.compaction };
@@ -865,7 +899,18 @@ async function handleSessionBeforeCompact(
865
899
  );
866
900
  return { cancel: true };
867
901
  }
868
- // failed: fall through to the configured-model fallback below.
902
+ // Only an actual exhausted remote attempt warns, never an input/routing skip.
903
+ if (event.signal?.aborted) return { cancel: true };
904
+ if (responsesOutcome.remoteFailure) {
905
+ const failure = responsesOutcome.remoteFailure;
906
+ writeDebugArtifact("compaction-event", {
907
+ event: "session_before_compact.remote-to-native-summary",
908
+ protocol: responsesOutcome.protocol,
909
+ ...failure,
910
+ nextStep: "native-summary",
911
+ }, config, ctx);
912
+ notifyWarning(ctx, `Remote compaction failed (${failure.reason}${failure.status ? `, HTTP ${failure.status}` : ""}${failure.errorCode ? `, ${failure.errorCode}` : ""}); continuing with a native text summary. No encrypted remote checkpoint was produced.${failure.errorMessage ? ` ${failure.errorMessage}` : ""}`);
913
+ }
869
914
  } else {
870
915
  writeDebugArtifact(
871
916
  "compaction-event",
@@ -1380,6 +1425,7 @@ export default function registerCompactionExtension(
1380
1425
  const manualCompact = new ManagedManualCompact(pi, contextWindows);
1381
1426
  const dependencies: CompactionDependencies = {
1382
1427
  remoteCompact: executeRemoteV2Compaction,
1428
+ standaloneCompact: executeNativeCompaction,
1383
1429
  nativeFallback: runNativeFallbackCompaction,
1384
1430
  contextWindows,
1385
1431
  ...overrides,
@@ -118,11 +118,8 @@ function cloneStructuredValue(value: unknown): unknown {
118
118
  }
119
119
 
120
120
  if (isRecord(value)) {
121
- const clone: Record<string, unknown> = {};
122
- for (const [key, nested] of Object.entries(value)) {
123
- clone[key] = cloneStructuredValue(nested);
124
- }
125
- return clone;
121
+ // Define own data properties, including provider JSON keys such as __proto__.
122
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, cloneStructuredValue(nested)]));
126
123
  }
127
124
 
128
125
  throw new Error(`Unsupported structured value: ${typeof value}`);
@@ -1,5 +1,6 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
2
  import { writeDebugArtifact } from "./debug";
3
+ import { decodeCompactionError, readCompactionResponseText, type CompactionErrorInfo } from "./compaction-errors";
3
4
  import { buildResponsesRequestHeaders } from "./responses-headers";
4
5
  import type { NativeCompactionRuntime } from "./runtime";
5
6
  import type { NativeCompactionRequestBody, ResponsesInputItem } from "./serializer";
@@ -55,7 +56,7 @@ export type RemoteV2CompactionClientSuccess = {
55
56
  response: RemoteV2ResponseEnvelope;
56
57
  };
57
58
 
58
- export type RemoteV2CompactionClientFailure = {
59
+ export type RemoteV2CompactionClientFailure = CompactionErrorInfo & {
59
60
  ok: false;
60
61
  reason: RemoteV2CompactionClientFailureReason;
61
62
  status?: number;
@@ -213,14 +214,6 @@ function getEventType(event: ParsedSseEvent): string | undefined {
213
214
  return isRecord(event.data) && isNonEmptyString(event.data.type) ? event.data.type : event.event;
214
215
  }
215
216
 
216
- function getErrorMessage(value: unknown): string | undefined {
217
- if (!isRecord(value)) return undefined;
218
- if (isNonEmptyString(value.message)) return value.message;
219
- if (isRecord(value.error) && isNonEmptyString(value.error.message)) return value.error.message;
220
- if (isNonEmptyString(value.error)) return value.error;
221
- return undefined;
222
- }
223
-
224
217
  function writeCompactArtifact(
225
218
  data: unknown,
226
219
  settings: CompactionConfig | undefined,
@@ -397,7 +390,7 @@ function reconcileSseCheckpoint(events: readonly ParsedSseEvent[]): CheckpointRe
397
390
  ok: false,
398
391
  reason: "error-event",
399
392
  responseJson: errorEvent.data,
400
- errorMessage: getErrorMessage(errorEvent.data),
393
+ errorMessage: decodeCompactionError(errorEvent.data).errorMessage,
401
394
  };
402
395
  }
403
396
 
@@ -668,7 +661,7 @@ export async function executeRemoteV2Compaction(
668
661
  });
669
662
 
670
663
  if (!response.ok) {
671
- const responseText = await response.text();
664
+ const responseText = await readCompactionResponseText(response, signal);
672
665
  let responseJson: unknown;
673
666
  if (responseText.trim()) {
674
667
  try {
@@ -681,6 +674,7 @@ export async function executeRemoteV2Compaction(
681
674
  ok: false,
682
675
  reason: "non-2xx",
683
676
  status: response.status,
677
+ ...decodeCompactionError(responseJson),
684
678
  responseText: responseText || undefined,
685
679
  responseJson,
686
680
  };
@@ -719,10 +713,13 @@ export async function executeRemoteV2Compaction(
719
713
  }
720
714
 
721
715
  if (!events) {
716
+ let bodyError: CompactionErrorInfo = {};
717
+ try { bodyError = decodeCompactionError(JSON.parse(responseText)); } catch { /* Not a JSON error. */ }
722
718
  const failure: RemoteV2CompactionClientFailure = {
723
719
  ok: false,
724
720
  reason: "invalid-sse",
725
721
  status: response.status,
722
+ ...bodyError,
726
723
  responseText,
727
724
  };
728
725
  writeCompactArtifact(
@@ -744,7 +741,7 @@ export async function executeRemoteV2Compaction(
744
741
  ok: false,
745
742
  reason: reconciliation.reason,
746
743
  status: response.status,
747
- errorMessage: reconciliation.errorMessage,
744
+ ...decodeCompactionError(reconciliation.responseJson),
748
745
  responseJson: reconciliation.responseJson,
749
746
  };
750
747
  writeCompactArtifact(
@@ -793,12 +790,12 @@ export async function executeRemoteV2Compaction(
793
790
  );
794
791
  return success;
795
792
  } catch (error) {
796
- const failure: RemoteV2CompactionClientFailure = isAbortError(error)
793
+ const failure: RemoteV2CompactionClientFailure = signal?.aborted || isAbortError(error)
797
794
  ? { ok: false, reason: "aborted" }
798
795
  : {
799
796
  ok: false,
800
797
  reason: "network-error",
801
- errorMessage: error instanceof Error ? error.message : String(error),
798
+ errorMessage: "Compaction transport failed; no remote checkpoint was produced.",
802
799
  };
803
800
  writeCompactArtifact(
804
801
  {