pi-background-tasks 2.0.0 → 2.1.2

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.
@@ -6,6 +6,7 @@ import { createRequire } from 'node:module';
6
6
  import { dirname, resolve } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import {
9
+ FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV,
9
10
  FUSION_CHILD_MAX_PROVIDER_REQUESTS,
10
11
  FUSION_CHILD_MAX_TOOL_CALLS,
11
12
  FUSION_CHILD_MIN_OUTPUT_RESERVE_TOKENS,
@@ -26,6 +27,8 @@ import {
26
27
  buildFusionChildSettlement,
27
28
  isRecoverableFusionChildErrorRecord,
28
29
  serializeFusionChildResultRecords,
30
+ type FusionChildOutputContractMetadata,
31
+ type FusionChildOutputRecoveryRole,
29
32
  type FusionChildResultMetadata,
30
33
  type FusionChildSettlementFailureReason,
31
34
  type FusionChildSettlementRecord,
@@ -51,6 +54,7 @@ import {
51
54
  cloneFusionUsage,
52
55
  createEmptyFusionUsage,
53
56
  type FusionCapability,
57
+ type FusionCandidateOutputRecovery,
54
58
  type FusionChildRunResult,
55
59
  type FusionErrorDetails,
56
60
  type FusionStage,
@@ -60,6 +64,10 @@ import {
60
64
  type ResolvedFusionModel,
61
65
  } from './types.js';
62
66
  import { isJsonObject, parseJsonText } from '../common.js';
67
+ import {
68
+ FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
69
+ fusionJsonRenderedTextBytes,
70
+ } from './output-contract.js';
63
71
  import { canonicalizeFusionPublicUrl, readFusionSourcePolicyFile } from './source-policy.js';
64
72
  import {
65
73
  assertWindowsCommandLineWithinLimit,
@@ -113,6 +121,7 @@ export const FUSION_CHILD_REMOVED_ENV_KEYS = [
113
121
  'PI_API_BASE_URL',
114
122
  'PI_AUTH_FILE',
115
123
  FUSION_TOOL_CALL_LOG_PATH_ENV,
124
+ FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV,
116
125
  FUSION_RESEARCH_ENABLED_ENV,
117
126
  FUSION_SOURCE_POLICY_PATH_ENV,
118
127
  FUSION_SOURCE_POLICY_SHA256_ENV,
@@ -180,6 +189,7 @@ export interface RunPiChildOptions {
180
189
  piLaunchDependencies?: PiLaunchDependencies | undefined;
181
190
  toolCallLogPath?: string | undefined;
182
191
  sourcePolicy?: { path: string; sha256: string } | undefined;
192
+ candidateOutputRecoveryPath?: string | undefined;
183
193
  }
184
194
 
185
195
  interface CloseRecord {
@@ -215,6 +225,7 @@ export class FusionChildRunError extends FusionError {
215
225
  readonly provider: string | undefined;
216
226
  readonly modelName: string | undefined;
217
227
  readonly qualifiedId: string | undefined;
228
+ readonly outputRecovery: FusionCandidateOutputRecovery | undefined;
218
229
 
219
230
  constructor(
220
231
  error: FusionError,
@@ -223,6 +234,7 @@ export class FusionChildRunError extends FusionError {
223
234
  stderr: Buffer,
224
235
  close: CloseRecord,
225
236
  observed: ObservedChildSnapshot,
237
+ outputRecovery?: FusionCandidateOutputRecovery,
226
238
  ) {
227
239
  const details: FusionErrorDetails = {
228
240
  code: error.code,
@@ -244,19 +256,41 @@ export class FusionChildRunError extends FusionError {
244
256
  this.provider = observed.provider;
245
257
  this.modelName = observed.model;
246
258
  this.qualifiedId = observed.qualifiedId;
259
+ this.outputRecovery = outputRecovery;
247
260
  }
248
261
  }
249
262
 
250
- export function fusionPiChildEnv(env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
263
+ export function fusionPiChildEnv(
264
+ env: NodeJS.ProcessEnv = process.env,
265
+ provider?: string | undefined,
266
+ ): NodeJS.ProcessEnv {
251
267
  const out: NodeJS.ProcessEnv = { ...env };
252
268
  const removed = new Set<string>(FUSION_CHILD_REMOVED_ENV_KEYS);
253
269
  for (const inheritedKey of Object.keys(out)) {
254
270
  if (removed.has(inheritedKey.toUpperCase())) Reflect.deleteProperty(out, inheritedKey);
255
271
  }
256
272
  out['PI_SKIP_VERSION_CHECK'] = '1';
273
+ if (provider === 'anthropic' && out[FUSION_CLAUDE_CACHE_RETENTION_ENV] === undefined) {
274
+ out[FUSION_CLAUDE_CACHE_RETENTION_ENV] = 'long';
275
+ }
257
276
  return out;
258
277
  }
259
278
 
279
+ export function resolveFusionAnthropicAttributionExtensionPath(
280
+ moduleUrl = import.meta.url,
281
+ pathExists: (path: string) => boolean = existsSync,
282
+ ): string {
283
+ const modulePath = fileURLToPath(moduleUrl);
284
+ const extension = modulePath.endsWith('.ts')
285
+ ? 'anthropic-attribution.ts'
286
+ : 'anthropic-attribution.js';
287
+ const candidate = resolve(dirname(modulePath), extension);
288
+ if (!pathExists(candidate)) {
289
+ throw new Error(`Fusion Anthropic attribution extension is missing: ${candidate}`);
290
+ }
291
+ return candidate;
292
+ }
293
+
260
294
  export function resolveFusionChildExtensionPath(
261
295
  moduleUrl = import.meta.url,
262
296
  pathExists: (path: string) => boolean = existsSync,
@@ -428,16 +462,19 @@ function fusionToolArgv(capability: FusionCapability): string[] {
428
462
  * `--no-extensions` disables discovery but still honours explicit `--extension`
429
463
  * paths, so this list is the complete set a child receives. The metadata
430
464
  * extension is always present. For Claude routes the sanitizer loads first so
431
- * the private runtime governor observes the final post-sanitizer payload; non-
432
- * Anthropic child argv remains unchanged.
465
+ * the package-owned attribution provider establishes the Claude Code OAuth
466
+ * request shape first, the sanitizer preserves that shape while removing only
467
+ * rejected prompt lines, and the private runtime governor observes the final
468
+ * payload. Non-Anthropic child argv remains unchanged.
433
469
  */
434
470
  export function fusionChildExtensionPaths(
435
471
  model: ResolvedFusionModel,
436
472
  childExtensionPath: string,
437
473
  resolveSanitizer: () => string = resolveAnthropicSanitizerExtensionPath,
474
+ resolveAttribution: () => string = resolveFusionAnthropicAttributionExtensionPath,
438
475
  ): readonly string[] {
439
476
  if (model.provider !== FUSION_SANITIZED_PROVIDER) return [childExtensionPath];
440
- return [resolveSanitizer(), childExtensionPath];
477
+ return [resolveAttribution(), resolveSanitizer(), childExtensionPath];
441
478
  }
442
479
 
443
480
  export function buildFusionPiChildArgv(
@@ -446,11 +483,13 @@ export function buildFusionPiChildArgv(
446
483
  childExtensionPath = resolveFusionChildExtensionPath(),
447
484
  capability: FusionCapability = FUSION_NO_TOOLS_CAPABILITY,
448
485
  resolveSanitizer: () => string = resolveAnthropicSanitizerExtensionPath,
486
+ resolveAttribution: () => string = resolveFusionAnthropicAttributionExtensionPath,
449
487
  ): string[] {
450
488
  const extensionArgs = fusionChildExtensionPaths(
451
489
  model,
452
490
  childExtensionPath,
453
491
  resolveSanitizer,
492
+ resolveAttribution,
454
493
  ).flatMap((path) => ['--extension', path]);
455
494
  return [
456
495
  '--mode',
@@ -574,9 +613,10 @@ function requireCostNumber(
574
613
  }
575
614
 
576
615
  function parseCompactUsage(value: unknown): FusionUsage {
577
- const record = assertClosedRecord(
616
+ const record = assertClosedRecordWithOptional(
578
617
  value,
579
618
  ['input', 'output', 'cacheRead', 'cacheWrite', 'totalTokens', 'cost'],
619
+ ['cacheWrite1h', 'reasoning'],
580
620
  'fusion child usage',
581
621
  );
582
622
  const cost = assertClosedRecord(
@@ -584,11 +624,29 @@ function parseCompactUsage(value: unknown): FusionUsage {
584
624
  ['input', 'output', 'cacheRead', 'cacheWrite', 'total'],
585
625
  'fusion child usage.cost',
586
626
  );
627
+ const output = requireUsageInteger(record, 'output', 'fusion child usage');
628
+ const cacheWrite = requireUsageInteger(record, 'cacheWrite', 'fusion child usage');
629
+ const cacheWrite1h =
630
+ record['cacheWrite1h'] === undefined
631
+ ? undefined
632
+ : requireUsageInteger(record, 'cacheWrite1h', 'fusion child usage');
633
+ const reasoning =
634
+ record['reasoning'] === undefined
635
+ ? undefined
636
+ : requireUsageInteger(record, 'reasoning', 'fusion child usage');
637
+ if (cacheWrite1h !== undefined && cacheWrite1h > cacheWrite) {
638
+ throw new Error('fusion child usage.cacheWrite1h must not exceed cacheWrite');
639
+ }
640
+ if (reasoning !== undefined && reasoning > output) {
641
+ throw new Error('fusion child usage.reasoning must not exceed output');
642
+ }
587
643
  return {
588
644
  input: requireUsageInteger(record, 'input', 'fusion child usage'),
589
- output: requireUsageInteger(record, 'output', 'fusion child usage'),
645
+ output,
590
646
  cacheRead: requireUsageInteger(record, 'cacheRead', 'fusion child usage'),
591
- cacheWrite: requireUsageInteger(record, 'cacheWrite', 'fusion child usage'),
647
+ cacheWrite,
648
+ ...(cacheWrite1h === undefined ? {} : { cacheWrite1h }),
649
+ ...(reasoning === undefined ? {} : { reasoning }),
592
650
  totalTokens: requireUsageInteger(record, 'totalTokens', 'fusion child usage'),
593
651
  cost: {
594
652
  input: requireCostNumber(cost, 'input', 'fusion child usage.cost'),
@@ -705,6 +763,51 @@ function parseFusionClaudeCacheObservation(
705
763
  };
706
764
  }
707
765
 
766
+ const FUSION_OUTPUT_RECOVERY_ROLES = new Set<FusionChildOutputRecoveryRole>([
767
+ 'none',
768
+ 'oversized_original',
769
+ 'replacement',
770
+ ]);
771
+
772
+ function parseChildOutputContract(value: unknown): FusionChildOutputContractMetadata {
773
+ const record = assertClosedRecord(
774
+ value,
775
+ ['json_rendered_bytes', 'candidate_limit_bytes', 'recovery_role'],
776
+ 'fusion child result.output_contract',
777
+ );
778
+ const candidateLimitValue = record['candidate_limit_bytes'];
779
+ const candidateLimit =
780
+ candidateLimitValue === null
781
+ ? null
782
+ : requirePositiveSafeInteger(
783
+ record,
784
+ 'candidate_limit_bytes',
785
+ 'fusion child result.output_contract',
786
+ );
787
+ if (candidateLimit !== null && candidateLimit !== FUSION_CANDIDATE_MAX_OUTPUT_BYTES) {
788
+ throw new Error('fusion child result candidate output limit mismatches the shared contract');
789
+ }
790
+ const recoveryRole = record['recovery_role'];
791
+ if (
792
+ typeof recoveryRole !== 'string' ||
793
+ !FUSION_OUTPUT_RECOVERY_ROLES.has(recoveryRole as FusionChildOutputRecoveryRole)
794
+ ) {
795
+ throw new Error('fusion child result.output_contract.recovery_role is invalid');
796
+ }
797
+ if (recoveryRole !== 'none' && candidateLimit === null) {
798
+ throw new Error('fusion child result output recovery has no candidate contract');
799
+ }
800
+ return {
801
+ json_rendered_bytes: requireUsageInteger(
802
+ record,
803
+ 'json_rendered_bytes',
804
+ 'fusion child result.output_contract',
805
+ ),
806
+ candidate_limit_bytes: candidateLimit,
807
+ recovery_role: recoveryRole as FusionChildOutputRecoveryRole,
808
+ };
809
+ }
810
+
708
811
  function parseChildResultMetadata(value: unknown): FusionChildResultMetadata {
709
812
  const record = assertClosedRecord(
710
813
  value,
@@ -717,6 +820,7 @@ function parseChildResultMetadata(value: unknown): FusionChildResultMetadata {
717
820
  'text_sha256',
718
821
  'usage',
719
822
  'cache_observation',
823
+ 'output_contract',
720
824
  ],
721
825
  'fusion child result',
722
826
  );
@@ -744,6 +848,7 @@ function parseChildResultMetadata(value: unknown): FusionChildResultMetadata {
744
848
  text_sha256: requireSha256(record, 'text_sha256', 'fusion child result'),
745
849
  usage,
746
850
  cache_observation: parseFusionClaudeCacheObservation(record['cache_observation'], provider),
851
+ output_contract: parseChildOutputContract(record['output_contract']),
747
852
  };
748
853
  }
749
854
 
@@ -892,6 +997,7 @@ const FUSION_CHILD_SETTLEMENT_FAILURE_REASONS = new Set<FusionChildSettlementFai
892
997
  'invalid_non_final',
893
998
  'runtime_guard',
894
999
  'cache_observation',
1000
+ 'output_recovery',
895
1001
  ]);
896
1002
 
897
1003
  export function parseFusionChildSettlement(
@@ -920,6 +1026,7 @@ export function parseFusionChildSettlement(
920
1026
  'final_record_index',
921
1027
  'final_text_sha256',
922
1028
  'recovered_error_ordinals',
1029
+ 'recovered_output_cap_ordinals',
923
1030
  'failure_reason',
924
1031
  ],
925
1032
  'fusion child settlement',
@@ -964,6 +1071,28 @@ export function parseFusionChildSettlement(
964
1071
  throw new Error('fusion child settlement recovered-error ordinals are not canonical');
965
1072
  }
966
1073
  }
1074
+ const recoveredOutputValue = record['recovered_output_cap_ordinals'];
1075
+ if (!Array.isArray(recoveredOutputValue)) {
1076
+ throw new Error('fusion child settlement.recovered_output_cap_ordinals must be an array');
1077
+ }
1078
+ const recoveredOutputCapOrdinals = recoveredOutputValue.map((value, index) => {
1079
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
1080
+ throw new Error(
1081
+ `fusion child settlement.recovered_output_cap_ordinals[${String(index)}] must be a non-negative safe integer`,
1082
+ );
1083
+ }
1084
+ return value;
1085
+ });
1086
+ for (let index = 0; index < recoveredOutputCapOrdinals.length; index += 1) {
1087
+ const ordinal = recoveredOutputCapOrdinals[index];
1088
+ if (
1089
+ ordinal === undefined ||
1090
+ ordinal >= recordCount - 1 ||
1091
+ (index > 0 && ordinal <= (recoveredOutputCapOrdinals[index - 1] ?? -1))
1092
+ ) {
1093
+ throw new Error('fusion child settlement recovered-output ordinals are not canonical');
1094
+ }
1095
+ }
967
1096
  const failureValue = record['failure_reason'];
968
1097
  let failureReason: FusionChildSettlementFailureReason | null;
969
1098
  if (failureValue === null) failureReason = null;
@@ -995,6 +1124,7 @@ export function parseFusionChildSettlement(
995
1124
  final_record_index: finalRecordIndex,
996
1125
  final_text_sha256: finalTextSha256,
997
1126
  recovered_error_ordinals: recoveredErrorOrdinals,
1127
+ recovered_output_cap_ordinals: recoveredOutputCapOrdinals,
998
1128
  failure_reason: failureReason,
999
1129
  });
1000
1130
  cursor = newline + 1;
@@ -1412,10 +1542,86 @@ function reconstructFinalText(response: Buffer, record: FusionChildResultMetadat
1412
1542
  const text = joined.toString('utf8');
1413
1543
  if (!Buffer.from(text, 'utf8').equals(joined))
1414
1544
  throw new Error('Pi final text is not valid UTF-8');
1545
+ if (fusionJsonRenderedTextBytes(text) !== record.output_contract.json_rendered_bytes) {
1546
+ throw new Error('Pi final text JSON-rendered byte count mismatch');
1547
+ }
1415
1548
  if (text.trim().length === 0) throw new Error('Pi assistant response is empty');
1416
1549
  return text;
1417
1550
  }
1418
1551
 
1552
+ type ParsedCandidateOutputRecovery = Omit<FusionCandidateOutputRecovery, 'original_text'>;
1553
+
1554
+ async function readCandidateOutputRecovery(
1555
+ path: string,
1556
+ evidence: ParsedCandidateOutputRecovery,
1557
+ ): Promise<FusionCandidateOutputRecovery> {
1558
+ let handle: Awaited<ReturnType<typeof open>>;
1559
+ try {
1560
+ handle = await open(path, constants.O_RDONLY | FUSION_PI_CHILD_O_NOFOLLOW);
1561
+ } catch (error) {
1562
+ if (isNotFound(error))
1563
+ throw new Error('fusion oversized candidate response artifact is missing');
1564
+ if (isJsonObject(error) && error['code'] === 'ELOOP') {
1565
+ throw new Error('fusion oversized candidate response artifact is a symlink');
1566
+ }
1567
+ throw error;
1568
+ }
1569
+ try {
1570
+ const stats = await handle.stat();
1571
+ if (!stats.isFile()) {
1572
+ throw new Error('fusion oversized candidate response artifact is not a regular file');
1573
+ }
1574
+ if (stats.size > FUSION_CHILD_STDOUT_LIMIT_BYTES) {
1575
+ throw new Error(
1576
+ `fusion oversized candidate response artifact exceeds ${String(FUSION_CHILD_STDOUT_LIMIT_BYTES)} bytes`,
1577
+ );
1578
+ }
1579
+ const bytes = await handle.readFile();
1580
+ if (sha256Buffer(bytes) !== evidence.original_text_sha256) {
1581
+ throw new Error('fusion oversized candidate response artifact hash mismatch');
1582
+ }
1583
+ const text = bytes.toString('utf8');
1584
+ if (!Buffer.from(text, 'utf8').equals(bytes)) {
1585
+ throw new Error('fusion oversized candidate response artifact is not valid UTF-8');
1586
+ }
1587
+ if (fusionJsonRenderedTextBytes(text) !== evidence.original_json_rendered_bytes) {
1588
+ throw new Error('fusion oversized candidate response JSON-rendered byte count mismatch');
1589
+ }
1590
+ if (evidence.original_json_rendered_bytes <= evidence.limit_bytes) {
1591
+ throw new Error('fusion output recovery original did not exceed the candidate contract');
1592
+ }
1593
+ return { ...evidence, original_text: text };
1594
+ } finally {
1595
+ await handle.close();
1596
+ }
1597
+ }
1598
+
1599
+ function parsedCandidateOutputRecovery(
1600
+ records: readonly FusionChildResultMetadata[],
1601
+ status: FusionCandidateOutputRecovery['status'],
1602
+ ): ParsedCandidateOutputRecovery | undefined {
1603
+ const originalRecordIndex = records.findIndex(
1604
+ (record) => record.output_contract.recovery_role === 'oversized_original',
1605
+ );
1606
+ if (originalRecordIndex < 0) return undefined;
1607
+ const original = records[originalRecordIndex];
1608
+ if (original === undefined) throw new Error('Pi output recovery original record disappeared');
1609
+ const replacementRecordIndex = records.findIndex(
1610
+ (record) => record.output_contract.recovery_role === 'replacement',
1611
+ );
1612
+ const replacement = replacementRecordIndex < 0 ? undefined : records[replacementRecordIndex];
1613
+ return {
1614
+ kind: 'same_session_compression',
1615
+ limit_bytes: FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
1616
+ original_record_index: originalRecordIndex,
1617
+ replacement_record_index: replacementRecordIndex < 0 ? null : replacementRecordIndex,
1618
+ original_json_rendered_bytes: original.output_contract.json_rendered_bytes,
1619
+ replacement_json_rendered_bytes: replacement?.output_contract.json_rendered_bytes ?? null,
1620
+ original_text_sha256: original.text_sha256,
1621
+ status,
1622
+ };
1623
+ }
1624
+
1419
1625
  export class FusionPiCompactResultParser {
1420
1626
  private readonly expectedProvider: string;
1421
1627
  private readonly expectedModel: string;
@@ -1434,6 +1640,53 @@ export class FusionPiCompactResultParser {
1434
1640
  }
1435
1641
  }
1436
1642
 
1643
+ finishOutputRecoveryFailure(
1644
+ response: Buffer,
1645
+ stderr: Buffer,
1646
+ ): {
1647
+ usage: FusionUsage;
1648
+ provider: string;
1649
+ model: string;
1650
+ qualifiedId: string;
1651
+ events: Buffer;
1652
+ diagnostics: Buffer;
1653
+ outputRecovery: ParsedCandidateOutputRecovery;
1654
+ } {
1655
+ const parsed = parseFusionChildStderr(stderr);
1656
+ const settlement = parseFusionChildSettlement(stderr);
1657
+ if (settlement === undefined) throw new Error('Pi child emitted no terminal result settlement');
1658
+ const expectedSettlement = buildFusionChildSettlement(parsed.records);
1659
+ if (JSON.stringify(settlement) !== JSON.stringify(expectedSettlement)) {
1660
+ throw new Error('Pi child terminal result settlement does not match the metadata stream');
1661
+ }
1662
+ if (settlement.status !== 'failed' || settlement.failure_reason !== 'output_recovery') {
1663
+ throw new Error('Pi child did not report a failed candidate output recovery');
1664
+ }
1665
+ if (parsed.diagnostics.includes(PI_EXTENSION_ERROR_PREFIX_BYTES)) {
1666
+ throw new Error('Pi child reported an extension error diagnostic');
1667
+ }
1668
+ const final = parsed.records.at(-1);
1669
+ if (final === undefined) throw new Error('Pi child emitted no compact result metadata');
1670
+ for (const record of parsed.records) this.assertModel(record);
1671
+ this.assertCacheObservationOrdinals(parsed.records);
1672
+ this.assertTranscriptStopReasons(parsed.records);
1673
+ reconstructFinalText(response, final);
1674
+ const outputRecovery = parsedCandidateOutputRecovery(parsed.records, 'failed');
1675
+ if (outputRecovery === undefined) {
1676
+ throw new Error('Pi child output-recovery failure omitted the oversized original');
1677
+ }
1678
+ const observed = this.observedFromRecords(parsed.records);
1679
+ return {
1680
+ usage: observed.usage,
1681
+ provider: final.provider,
1682
+ model: final.model,
1683
+ qualifiedId: `${final.provider}/${final.model}`,
1684
+ events: parsed.events,
1685
+ diagnostics: parsed.diagnostics,
1686
+ outputRecovery,
1687
+ };
1688
+ }
1689
+
1437
1690
  finish(
1438
1691
  response: Buffer,
1439
1692
  stderr: Buffer,
@@ -1447,6 +1700,7 @@ export class FusionPiCompactResultParser {
1447
1700
  qualifiedId: string;
1448
1701
  events: Buffer;
1449
1702
  diagnostics: Buffer;
1703
+ outputRecovery: ParsedCandidateOutputRecovery | undefined;
1450
1704
  } {
1451
1705
  const parsed = parseFusionChildStderr(stderr);
1452
1706
  const runtimeGuard = parseFusionRuntimeGuard(stderr);
@@ -1473,8 +1727,9 @@ export class FusionPiCompactResultParser {
1473
1727
  );
1474
1728
  }
1475
1729
  const observed = this.observedFromRecords(parsed.records);
1730
+ const text = reconstructFinalText(response, final);
1476
1731
  return {
1477
- text: reconstructFinalText(response, final),
1732
+ text,
1478
1733
  usage: observed.usage,
1479
1734
  firstRequestUsage: cloneFusionUsage(parsed.records[0]?.usage ?? createEmptyFusionUsage()),
1480
1735
  providerRequestCount: parsed.records.length,
@@ -1483,6 +1738,7 @@ export class FusionPiCompactResultParser {
1483
1738
  qualifiedId: `${final.provider}/${final.model}`,
1484
1739
  events: parsed.events,
1485
1740
  diagnostics: parsed.diagnostics,
1741
+ outputRecovery: parsedCandidateOutputRecovery(parsed.records, 'completed'),
1486
1742
  };
1487
1743
  }
1488
1744
 
@@ -1514,11 +1770,15 @@ export class FusionPiCompactResultParser {
1514
1770
  if (record.stop_reason !== 'stop') {
1515
1771
  throw new Error(this.stopReasonError('final', 'stop', record.stop_reason, true));
1516
1772
  }
1517
- } else if (record.stop_reason !== 'toolUse' && !isRecoverableFusionChildErrorRecord(record)) {
1773
+ } else if (
1774
+ record.stop_reason !== 'toolUse' &&
1775
+ !isRecoverableFusionChildErrorRecord(record) &&
1776
+ record.output_contract.recovery_role !== 'oversized_original'
1777
+ ) {
1518
1778
  throw new Error(
1519
1779
  this.stopReasonError(
1520
1780
  `non-final record ${index}`,
1521
- 'toolUse or a settled zero-usage retry marker',
1781
+ 'toolUse, a settled zero-usage retry marker, or one oversized original',
1522
1782
  record.stop_reason,
1523
1783
  true,
1524
1784
  ),
@@ -1786,7 +2046,19 @@ export async function runPiChild(options: RunPiChildOptions): Promise<FusionChil
1786
2046
  const killProcess = options.killProcess ?? process.kill.bind(process);
1787
2047
  const platform = options.platform ?? process.platform;
1788
2048
  const capability = options.capability ?? FUSION_NO_TOOLS_CAPABILITY;
1789
- const env = fusionPiChildEnv(options.env ?? process.env);
2049
+ const env = fusionPiChildEnv(options.env ?? process.env, options.model.provider);
2050
+ if (options.candidateOutputRecoveryPath !== undefined) {
2051
+ if (options.stage !== 'candidate') {
2052
+ throw childError(
2053
+ 'candidate output recovery may be enabled only for candidate children',
2054
+ 'orchestration_failed',
2055
+ options,
2056
+ false,
2057
+ false,
2058
+ );
2059
+ }
2060
+ env[FUSION_CANDIDATE_OUTPUT_RECOVERY_PATH_ENV] = options.candidateOutputRecoveryPath;
2061
+ }
1790
2062
  if (capability !== 'reason') {
1791
2063
  if (options.toolCallLogPath === undefined) {
1792
2064
  throw childError(
@@ -2004,8 +2276,32 @@ export async function runPiChild(options: RunPiChildOptions): Promise<FusionChil
2004
2276
  // A primary process/cap error remains authoritative; malformed metadata is
2005
2277
  // surfaced below when the child otherwise exits successfully.
2006
2278
  }
2279
+ const readPartialOutputRecovery = async (): Promise<
2280
+ FusionCandidateOutputRecovery | undefined
2281
+ > => {
2282
+ let parsedRecords: readonly FusionChildResultMetadata[];
2283
+ try {
2284
+ parsedRecords = parseFusionChildStderr(rawStderr).records;
2285
+ } catch {
2286
+ return undefined;
2287
+ }
2288
+ const evidence = parsedCandidateOutputRecovery(parsedRecords, 'failed');
2289
+ if (evidence === undefined) return undefined;
2290
+ if (options.candidateOutputRecoveryPath === undefined) {
2291
+ throw new Error('fusion child emitted output-recovery evidence without an artifact path');
2292
+ }
2293
+ return readCandidateOutputRecovery(options.candidateOutputRecoveryPath, evidence);
2294
+ };
2007
2295
  const primary = state.primaryError;
2008
- if (primary !== undefined)
2296
+ if (primary !== undefined) {
2297
+ let outputRecovery: FusionCandidateOutputRecovery | undefined;
2298
+ try {
2299
+ outputRecovery = await readPartialOutputRecovery();
2300
+ } catch (error) {
2301
+ state.cleanupErrors.push(
2302
+ `output recovery evidence invalid: ${error instanceof Error ? error.message : String(error)}`,
2303
+ );
2304
+ }
2009
2305
  throw new FusionChildRunError(
2010
2306
  withCleanupErrors(primary, state.cleanupErrors),
2011
2307
  compactEvents,
@@ -2013,7 +2309,81 @@ export async function runPiChild(options: RunPiChildOptions): Promise<FusionChil
2013
2309
  diagnostics,
2014
2310
  close,
2015
2311
  observed,
2312
+ outputRecovery,
2313
+ );
2314
+ }
2315
+ let terminalSettlement: FusionChildSettlementRecord | undefined;
2316
+ try {
2317
+ terminalSettlement = parseFusionChildSettlement(rawStderr);
2318
+ } catch (error) {
2319
+ throw new FusionChildRunError(
2320
+ withCleanupErrors(
2321
+ childError(
2322
+ `Pi child terminal settlement invalid: ${error instanceof Error ? error.message : String(error)}`,
2323
+ 'child_event_invalid',
2324
+ options,
2325
+ ),
2326
+ state.cleanupErrors,
2327
+ ),
2328
+ compactEvents,
2329
+ response,
2330
+ diagnostics,
2331
+ close,
2332
+ observed,
2016
2333
  );
2334
+ }
2335
+ if (terminalSettlement?.failure_reason === 'output_recovery') {
2336
+ try {
2337
+ const failure = parser.finishOutputRecoveryFailure(response, rawStderr);
2338
+ if (options.candidateOutputRecoveryPath === undefined) {
2339
+ throw new Error(
2340
+ 'fusion child output-recovery failure omitted the configured artifact path',
2341
+ );
2342
+ }
2343
+ const outputRecovery = await readCandidateOutputRecovery(
2344
+ options.candidateOutputRecoveryPath,
2345
+ failure.outputRecovery,
2346
+ );
2347
+ const recoveryMessage =
2348
+ outputRecovery.replacement_json_rendered_bytes === null
2349
+ ? `Pi child could not complete its one allowed same-session candidate compression continuation; the ${String(outputRecovery.original_json_rendered_bytes)}-byte original is preserved and nothing was truncated`
2350
+ : `Pi child compressed candidate response is still ${String(outputRecovery.replacement_json_rendered_bytes)} JSON-rendered bytes, exceeding the ${String(outputRecovery.limit_bytes)}-byte output contract; both responses are preserved and nothing was truncated`;
2351
+ throw new FusionChildRunError(
2352
+ withCleanupErrors(
2353
+ childError(recoveryMessage, 'child_output_cap', options),
2354
+ state.cleanupErrors,
2355
+ ),
2356
+ failure.events,
2357
+ response,
2358
+ failure.diagnostics,
2359
+ close,
2360
+ {
2361
+ usage: failure.usage,
2362
+ provider: failure.provider,
2363
+ model: failure.model,
2364
+ qualifiedId: failure.qualifiedId,
2365
+ },
2366
+ outputRecovery,
2367
+ );
2368
+ } catch (error) {
2369
+ if (error instanceof FusionChildRunError) throw error;
2370
+ throw new FusionChildRunError(
2371
+ withCleanupErrors(
2372
+ childError(
2373
+ `Pi child output-recovery evidence invalid: ${error instanceof Error ? error.message : String(error)}`,
2374
+ 'child_event_invalid',
2375
+ options,
2376
+ ),
2377
+ state.cleanupErrors,
2378
+ ),
2379
+ compactEvents,
2380
+ response,
2381
+ diagnostics,
2382
+ close,
2383
+ observed,
2384
+ );
2385
+ }
2386
+ }
2017
2387
  if (close.code !== 0 || close.signal !== null) {
2018
2388
  let runtimeGuard: FusionRuntimeGuardRecord | undefined;
2019
2389
  try {
@@ -2079,6 +2449,42 @@ export async function runPiChild(options: RunPiChildOptions): Promise<FusionChil
2079
2449
  observed,
2080
2450
  );
2081
2451
  }
2452
+ let outputRecovery: FusionCandidateOutputRecovery | undefined;
2453
+ if (parsed.outputRecovery !== undefined) {
2454
+ if (options.candidateOutputRecoveryPath === undefined) {
2455
+ throw new FusionChildRunError(
2456
+ childError(
2457
+ 'Pi child output-recovery evidence has no configured artifact path',
2458
+ 'child_event_invalid',
2459
+ options,
2460
+ ),
2461
+ parsed.events,
2462
+ response,
2463
+ parsed.diagnostics,
2464
+ close,
2465
+ observed,
2466
+ );
2467
+ }
2468
+ try {
2469
+ outputRecovery = await readCandidateOutputRecovery(
2470
+ options.candidateOutputRecoveryPath,
2471
+ parsed.outputRecovery,
2472
+ );
2473
+ } catch (error) {
2474
+ throw new FusionChildRunError(
2475
+ childError(
2476
+ `Pi child output-recovery artifact invalid: ${error instanceof Error ? error.message : String(error)}`,
2477
+ 'child_event_invalid',
2478
+ options,
2479
+ ),
2480
+ parsed.events,
2481
+ response,
2482
+ parsed.diagnostics,
2483
+ close,
2484
+ observed,
2485
+ );
2486
+ }
2487
+ }
2082
2488
  let toolCallTrace: FusionToolCallTrace | undefined;
2083
2489
  if (capability !== 'reason') {
2084
2490
  // The launch path above refuses to spawn a tool-enabled child without a log path, so
@@ -2112,6 +2518,7 @@ export async function runPiChild(options: RunPiChildOptions): Promise<FusionChil
2112
2518
  diagnostics,
2113
2519
  close,
2114
2520
  observed,
2521
+ outputRecovery,
2115
2522
  );
2116
2523
  }
2117
2524
  }
@@ -2131,6 +2538,7 @@ export async function runPiChild(options: RunPiChildOptions): Promise<FusionChil
2131
2538
  signal: close.signal,
2132
2539
  };
2133
2540
  if (options.slot !== undefined) result.slot = options.slot;
2541
+ if (outputRecovery !== undefined) result.outputRecovery = outputRecovery;
2134
2542
  if (toolCallTrace !== undefined) result.toolCallTrace = toolCallTrace;
2135
2543
  return result;
2136
2544
  } finally {