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.
- package/README.md +2 -2
- package/TESTING.md +13 -12
- package/TEST_PLAN.md +7 -7
- package/docs/manifest.json +14 -4
- package/docs/operations/configuration.md +5 -3
- package/docs/read-before-edit.md +2 -0
- package/docs/reference/runtime-contracts.md +64 -62
- package/docs/subsystems/delegation.md +2 -2
- package/docs/subsystems/docs-freshness-gate.md +3 -3
- package/docs/subsystems/fusion.md +10 -7
- package/package.json +6 -6
- package/src/core/delegate/hook-contract.ts +14 -13
- package/src/core/fusion/anthropic-attribution.ts +1930 -0
- package/src/core/fusion/artifacts.ts +96 -1
- package/src/core/fusion/budget.ts +23 -23
- package/src/core/fusion/child-protocol.ts +115 -10
- package/src/core/fusion/claude-cache.ts +21 -0
- package/src/core/fusion/config.ts +10 -2
- package/src/core/fusion/orchestrator.ts +128 -10
- package/src/core/fusion/output-contract.ts +34 -0
- package/src/core/fusion/pi-child.ts +420 -12
- package/src/core/fusion/prompts.ts +11 -1
- package/src/core/fusion/result-package.ts +30 -3
- package/src/core/fusion/types.ts +58 -0
- package/src/delegate-child-extension.ts +12 -13
- package/src/fusion-child-extension.ts +117 -2
|
@@ -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(
|
|
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
|
|
432
|
-
*
|
|
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 =
|
|
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
|
|
645
|
+
output,
|
|
590
646
|
cacheRead: requireUsageInteger(record, 'cacheRead', 'fusion child usage'),
|
|
591
|
-
cacheWrite
|
|
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
|
|
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 (
|
|
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
|
|
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 {
|