pi-background-tasks 1.0.3 → 1.0.6
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/BACKGROUND-TASKS-INSTRUCTIONS.md +2 -1
- package/README.md +2 -1
- package/TESTING.md +3 -3
- package/TEST_PLAN.md +10 -9
- package/docs/attestations.json +5 -5
- package/docs/manifest.json +9 -5
- package/docs/operations/configuration.md +10 -0
- package/docs/operations/testing.md +2 -1
- package/docs/operations/troubleshooting.md +3 -1
- package/docs/read-before-edit.md +1 -0
- package/docs/reference/runtime-contracts.md +44 -40
- package/docs/subsystems/docs-freshness-gate.md +6 -6
- package/docs/subsystems/fusion.md +18 -11
- package/package.json +4 -3
- package/src/core/fusion/budget.ts +11 -4
- package/src/core/fusion/child-protocol.ts +137 -12
- package/src/core/fusion/claude-cache.ts +186 -0
- package/src/core/fusion/config.ts +13 -0
- package/src/core/fusion/pi-child.ts +617 -30
- package/src/core/fusion/types.ts +29 -7
- package/src/fusion-child-extension.ts +659 -55
|
@@ -6,17 +6,39 @@ 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_CHILD_MAX_PROVIDER_REQUESTS,
|
|
10
|
+
FUSION_CHILD_MAX_TOOL_CALLS,
|
|
11
|
+
FUSION_CHILD_MIN_OUTPUT_RESERVE_TOKENS,
|
|
9
12
|
FUSION_CHILD_RESULT_PREFIX,
|
|
10
13
|
FUSION_CHILD_RESULT_SCHEMA_VERSION,
|
|
14
|
+
FUSION_CHILD_SAFETY_RESERVE_TOKENS,
|
|
15
|
+
FUSION_CHILD_SETTLEMENT_PREFIX,
|
|
16
|
+
FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION,
|
|
11
17
|
FUSION_RESEARCH_ENABLED_ENV,
|
|
18
|
+
FUSION_RUNTIME_GUARD_PREFIX,
|
|
19
|
+
FUSION_RUNTIME_GUARD_SCHEMA_VERSION,
|
|
12
20
|
FUSION_SOURCE_POLICY_PATH_ENV,
|
|
13
21
|
FUSION_SOURCE_POLICY_SHA256_ENV,
|
|
14
22
|
FUSION_TOOL_CALL_LOG_PATH_ENV,
|
|
15
23
|
FUSION_TOOL_CALL_SEAL_SCHEMA_VERSION,
|
|
16
24
|
FUSION_TOOL_CALL_SEAL_SUFFIX,
|
|
17
25
|
FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES,
|
|
26
|
+
buildFusionChildSettlement,
|
|
27
|
+
isRecoverableFusionChildErrorRecord,
|
|
28
|
+
serializeFusionChildResultRecords,
|
|
18
29
|
type FusionChildResultMetadata,
|
|
30
|
+
type FusionChildSettlementFailureReason,
|
|
31
|
+
type FusionChildSettlementRecord,
|
|
32
|
+
type FusionRuntimeGuardCode,
|
|
33
|
+
type FusionRuntimeGuardRecord,
|
|
19
34
|
} from './child-protocol.js';
|
|
35
|
+
import {
|
|
36
|
+
FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT,
|
|
37
|
+
FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION,
|
|
38
|
+
FUSION_CLAUDE_CACHE_RETENTION_ENV,
|
|
39
|
+
type FusionClaudeCacheObservation,
|
|
40
|
+
type FusionClaudeCacheRetention,
|
|
41
|
+
} from './claude-cache.js';
|
|
20
42
|
import {
|
|
21
43
|
FUSION_FORBIDDEN_TOOLS,
|
|
22
44
|
FUSION_NO_TOOLS_CAPABILITY,
|
|
@@ -64,7 +86,8 @@ export const FUSION_CHILD_TIMEOUT_MS = 30 * 60 * 1000;
|
|
|
64
86
|
export const FUSION_CHILD_IDLE_TIMEOUT_MS = 20 * 60 * 1000;
|
|
65
87
|
export const FUSION_CHILD_KILL_GRACE_MS = 3000;
|
|
66
88
|
export const FUSION_CHILD_SIGKILL_WAIT_MS = 5000;
|
|
67
|
-
const FUSION_PI_CHILD_O_NOFOLLOW =
|
|
89
|
+
const FUSION_PI_CHILD_O_NOFOLLOW =
|
|
90
|
+
typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
68
91
|
|
|
69
92
|
export const FUSION_CHILD_REMOVED_ENV_KEYS = [
|
|
70
93
|
'PI_SESSION_ID',
|
|
@@ -404,8 +427,9 @@ function fusionToolArgv(capability: FusionCapability): string[] {
|
|
|
404
427
|
*
|
|
405
428
|
* `--no-extensions` disables discovery but still honours explicit `--extension`
|
|
406
429
|
* paths, so this list is the complete set a child receives. The metadata
|
|
407
|
-
* extension is always present
|
|
408
|
-
*
|
|
430
|
+
* 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.
|
|
409
433
|
*/
|
|
410
434
|
export function fusionChildExtensionPaths(
|
|
411
435
|
model: ResolvedFusionModel,
|
|
@@ -413,7 +437,7 @@ export function fusionChildExtensionPaths(
|
|
|
413
437
|
resolveSanitizer: () => string = resolveAnthropicSanitizerExtensionPath,
|
|
414
438
|
): readonly string[] {
|
|
415
439
|
if (model.provider !== FUSION_SANITIZED_PROVIDER) return [childExtensionPath];
|
|
416
|
-
return [
|
|
440
|
+
return [resolveSanitizer(), childExtensionPath];
|
|
417
441
|
}
|
|
418
442
|
|
|
419
443
|
export function buildFusionPiChildArgv(
|
|
@@ -452,6 +476,9 @@ export function buildFusionPiChildArgv(
|
|
|
452
476
|
|
|
453
477
|
const SHA256_HEX_PATTERN = /^[0-9a-f]{64}$/;
|
|
454
478
|
const FUSION_CHILD_RESULT_PREFIX_BYTES = Buffer.from(FUSION_CHILD_RESULT_PREFIX, 'utf8');
|
|
479
|
+
const FUSION_CHILD_SETTLEMENT_PREFIX_BYTES = Buffer.from(FUSION_CHILD_SETTLEMENT_PREFIX, 'utf8');
|
|
480
|
+
const FUSION_RUNTIME_GUARD_PREFIX_BYTES = Buffer.from(FUSION_RUNTIME_GUARD_PREFIX, 'utf8');
|
|
481
|
+
const PI_EXTENSION_ERROR_PREFIX_BYTES = Buffer.from('Extension error (', 'utf8');
|
|
455
482
|
|
|
456
483
|
interface ParsedFusionChildStderr {
|
|
457
484
|
records: FusionChildResultMetadata[];
|
|
@@ -525,6 +552,16 @@ function requireUsageInteger(
|
|
|
525
552
|
return value;
|
|
526
553
|
}
|
|
527
554
|
|
|
555
|
+
function requirePositiveSafeInteger(
|
|
556
|
+
record: Record<PropertyKey, unknown>,
|
|
557
|
+
key: string,
|
|
558
|
+
label: string,
|
|
559
|
+
): number {
|
|
560
|
+
const value = requireUsageInteger(record, key, label);
|
|
561
|
+
if (value === 0) throw new Error(`${label}.${key} must be a positive safe integer`);
|
|
562
|
+
return value;
|
|
563
|
+
}
|
|
564
|
+
|
|
528
565
|
function requireCostNumber(
|
|
529
566
|
record: Record<PropertyKey, unknown>,
|
|
530
567
|
key: string,
|
|
@@ -563,10 +600,124 @@ function parseCompactUsage(value: unknown): FusionUsage {
|
|
|
563
600
|
};
|
|
564
601
|
}
|
|
565
602
|
|
|
603
|
+
const FUSION_CLAUDE_CACHE_RETENTIONS = new Set<FusionClaudeCacheRetention>([
|
|
604
|
+
'none',
|
|
605
|
+
'short',
|
|
606
|
+
'long',
|
|
607
|
+
]);
|
|
608
|
+
|
|
609
|
+
function parseFusionClaudeCacheObservation(
|
|
610
|
+
value: unknown,
|
|
611
|
+
provider: string,
|
|
612
|
+
): FusionClaudeCacheObservation {
|
|
613
|
+
const record = assertClosedRecord(
|
|
614
|
+
value,
|
|
615
|
+
[
|
|
616
|
+
'schema_version',
|
|
617
|
+
'applicability',
|
|
618
|
+
'source',
|
|
619
|
+
'requested_retention',
|
|
620
|
+
'effective_retention',
|
|
621
|
+
'breakpoint_count',
|
|
622
|
+
'request_ordinal',
|
|
623
|
+
],
|
|
624
|
+
'fusion child cache observation',
|
|
625
|
+
);
|
|
626
|
+
if (record['schema_version'] !== FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION) {
|
|
627
|
+
throw new Error('fusion child cache observation schema_version mismatch');
|
|
628
|
+
}
|
|
629
|
+
const applicability = record['applicability'];
|
|
630
|
+
const source = record['source'];
|
|
631
|
+
const requested = record['requested_retention'];
|
|
632
|
+
const effective = record['effective_retention'];
|
|
633
|
+
const breakpointCount = requireUsageInteger(
|
|
634
|
+
record,
|
|
635
|
+
'breakpoint_count',
|
|
636
|
+
'fusion child cache observation',
|
|
637
|
+
);
|
|
638
|
+
const requestOrdinal = requirePositiveSafeInteger(
|
|
639
|
+
record,
|
|
640
|
+
'request_ordinal',
|
|
641
|
+
'fusion child cache observation',
|
|
642
|
+
);
|
|
643
|
+
if (requestOrdinal > FUSION_CHILD_MAX_PROVIDER_REQUESTS) {
|
|
644
|
+
throw new Error('fusion child cache observation exceeds the provider request limit');
|
|
645
|
+
}
|
|
646
|
+
if (breakpointCount > FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT) {
|
|
647
|
+
throw new Error('fusion child cache observation exceeds the Anthropic breakpoint limit');
|
|
648
|
+
}
|
|
649
|
+
if (provider !== 'anthropic') {
|
|
650
|
+
if (
|
|
651
|
+
applicability !== 'not_applicable' ||
|
|
652
|
+
source !== 'not_applicable' ||
|
|
653
|
+
requested !== null ||
|
|
654
|
+
effective !== null ||
|
|
655
|
+
breakpointCount !== 0
|
|
656
|
+
) {
|
|
657
|
+
throw new Error('non-Anthropic fusion child has contradictory cache observation');
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
schema_version: FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION,
|
|
661
|
+
applicability: 'not_applicable',
|
|
662
|
+
source: 'not_applicable',
|
|
663
|
+
requested_retention: null,
|
|
664
|
+
effective_retention: null,
|
|
665
|
+
breakpoint_count: 0,
|
|
666
|
+
request_ordinal: requestOrdinal,
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
if (applicability !== 'anthropic') {
|
|
670
|
+
throw new Error('Anthropic fusion child cache observation is not applicable');
|
|
671
|
+
}
|
|
672
|
+
if (source !== 'default' && source !== FUSION_CLAUDE_CACHE_RETENTION_ENV) {
|
|
673
|
+
throw new Error('Anthropic fusion child cache observation source is invalid');
|
|
674
|
+
}
|
|
675
|
+
if (
|
|
676
|
+
typeof requested !== 'string' ||
|
|
677
|
+
!FUSION_CLAUDE_CACHE_RETENTIONS.has(requested as FusionClaudeCacheRetention) ||
|
|
678
|
+
typeof effective !== 'string' ||
|
|
679
|
+
!FUSION_CLAUDE_CACHE_RETENTIONS.has(effective as FusionClaudeCacheRetention)
|
|
680
|
+
) {
|
|
681
|
+
throw new Error('Anthropic fusion child cache retention is invalid');
|
|
682
|
+
}
|
|
683
|
+
const requestedRetention = requested as FusionClaudeCacheRetention;
|
|
684
|
+
const effectiveRetention = effective as FusionClaudeCacheRetention;
|
|
685
|
+
if (source === 'default' && requestedRetention !== 'long') {
|
|
686
|
+
throw new Error('default Anthropic cache policy did not request long retention');
|
|
687
|
+
}
|
|
688
|
+
if ((effectiveRetention === 'none') !== (breakpointCount === 0)) {
|
|
689
|
+
throw new Error('Anthropic fusion child cache retention/breakpoint evidence mismatch');
|
|
690
|
+
}
|
|
691
|
+
if (requestedRetention === 'none' && effectiveRetention !== 'none') {
|
|
692
|
+
throw new Error('disabled Anthropic cache policy reported active breakpoints');
|
|
693
|
+
}
|
|
694
|
+
if (effectiveRetention === 'long' && requestedRetention !== 'long') {
|
|
695
|
+
throw new Error('Anthropic long cache retention was not requested');
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
schema_version: FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION,
|
|
699
|
+
applicability: 'anthropic',
|
|
700
|
+
source,
|
|
701
|
+
requested_retention: requestedRetention,
|
|
702
|
+
effective_retention: effectiveRetention,
|
|
703
|
+
breakpoint_count: breakpointCount,
|
|
704
|
+
request_ordinal: requestOrdinal,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
566
708
|
function parseChildResultMetadata(value: unknown): FusionChildResultMetadata {
|
|
567
709
|
const record = assertClosedRecord(
|
|
568
710
|
value,
|
|
569
|
-
[
|
|
711
|
+
[
|
|
712
|
+
'schema_version',
|
|
713
|
+
'provider',
|
|
714
|
+
'model',
|
|
715
|
+
'stop_reason',
|
|
716
|
+
'text_blocks',
|
|
717
|
+
'text_sha256',
|
|
718
|
+
'usage',
|
|
719
|
+
'cache_observation',
|
|
720
|
+
],
|
|
570
721
|
'fusion child result',
|
|
571
722
|
);
|
|
572
723
|
if (record['schema_version'] !== FUSION_CHILD_RESULT_SCHEMA_VERSION)
|
|
@@ -583,18 +734,346 @@ function parseChildResultMetadata(value: unknown): FusionChildResultMetadata {
|
|
|
583
734
|
};
|
|
584
735
|
});
|
|
585
736
|
const usage = parseCompactUsage(record['usage']);
|
|
737
|
+
const provider = requireNonBlankString(record, 'provider', 'fusion child result');
|
|
586
738
|
return {
|
|
587
739
|
schema_version: FUSION_CHILD_RESULT_SCHEMA_VERSION,
|
|
588
|
-
provider
|
|
740
|
+
provider,
|
|
589
741
|
model: requireNonBlankString(record, 'model', 'fusion child result'),
|
|
590
742
|
stop_reason: requireNonBlankString(record, 'stop_reason', 'fusion child result'),
|
|
591
743
|
text_blocks: textBlocks,
|
|
592
744
|
text_sha256: requireSha256(record, 'text_sha256', 'fusion child result'),
|
|
593
745
|
usage,
|
|
746
|
+
cache_observation: parseFusionClaudeCacheObservation(record['cache_observation'], provider),
|
|
594
747
|
};
|
|
595
748
|
}
|
|
596
749
|
|
|
750
|
+
const FUSION_RUNTIME_GUARD_CODES = new Set<FusionRuntimeGuardCode>([
|
|
751
|
+
'provider_request_limit',
|
|
752
|
+
'provider_request_budget',
|
|
753
|
+
'provider_payload_invalid',
|
|
754
|
+
'claude_cache_policy',
|
|
755
|
+
'tool_call_limit',
|
|
756
|
+
]);
|
|
757
|
+
|
|
758
|
+
export function parseFusionRuntimeGuard(stderr: Buffer): FusionRuntimeGuardRecord | undefined {
|
|
759
|
+
const frames: FusionRuntimeGuardRecord[] = [];
|
|
760
|
+
let cursor = 0;
|
|
761
|
+
for (;;) {
|
|
762
|
+
const frameStart = stderr.indexOf(FUSION_RUNTIME_GUARD_PREFIX_BYTES, cursor);
|
|
763
|
+
if (frameStart < 0) break;
|
|
764
|
+
const payloadStart = frameStart + FUSION_RUNTIME_GUARD_PREFIX_BYTES.length;
|
|
765
|
+
const newline = stderr.indexOf(10, payloadStart);
|
|
766
|
+
if (newline < 0) throw new Error('fusion runtime guard frame is not newline-terminated');
|
|
767
|
+
const bytes = stderr.subarray(payloadStart, newline);
|
|
768
|
+
const text = bytes.toString('utf8');
|
|
769
|
+
if (!Buffer.from(text, 'utf8').equals(bytes)) {
|
|
770
|
+
throw new Error('fusion runtime guard frame is not valid UTF-8');
|
|
771
|
+
}
|
|
772
|
+
const record = assertClosedRecord(
|
|
773
|
+
parseJsonText(text),
|
|
774
|
+
[
|
|
775
|
+
'schema_version',
|
|
776
|
+
'code',
|
|
777
|
+
'provider',
|
|
778
|
+
'model',
|
|
779
|
+
'request_ordinal',
|
|
780
|
+
'tool_call_count',
|
|
781
|
+
'payload_bytes',
|
|
782
|
+
'payload_sha256',
|
|
783
|
+
'estimated_input_tokens',
|
|
784
|
+
'context_window_tokens',
|
|
785
|
+
'reserved_output_tokens',
|
|
786
|
+
'safety_reserve_tokens',
|
|
787
|
+
'allowed_input_tokens',
|
|
788
|
+
'message',
|
|
789
|
+
],
|
|
790
|
+
'fusion runtime guard',
|
|
791
|
+
);
|
|
792
|
+
if (record['schema_version'] !== FUSION_RUNTIME_GUARD_SCHEMA_VERSION) {
|
|
793
|
+
throw new Error('fusion runtime guard schema_version mismatch');
|
|
794
|
+
}
|
|
795
|
+
const code = requireNonBlankString(record, 'code', 'fusion runtime guard');
|
|
796
|
+
if (!FUSION_RUNTIME_GUARD_CODES.has(code as FusionRuntimeGuardCode)) {
|
|
797
|
+
throw new Error(`fusion runtime guard code is unsupported: ${code}`);
|
|
798
|
+
}
|
|
799
|
+
const frame: FusionRuntimeGuardRecord = {
|
|
800
|
+
schema_version: FUSION_RUNTIME_GUARD_SCHEMA_VERSION,
|
|
801
|
+
code: code as FusionRuntimeGuardCode,
|
|
802
|
+
provider: requireNonBlankString(record, 'provider', 'fusion runtime guard'),
|
|
803
|
+
model: requireNonBlankString(record, 'model', 'fusion runtime guard'),
|
|
804
|
+
request_ordinal: requirePositiveSafeInteger(
|
|
805
|
+
record,
|
|
806
|
+
'request_ordinal',
|
|
807
|
+
'fusion runtime guard',
|
|
808
|
+
),
|
|
809
|
+
tool_call_count: requireUsageInteger(record, 'tool_call_count', 'fusion runtime guard'),
|
|
810
|
+
payload_bytes: requireUsageInteger(record, 'payload_bytes', 'fusion runtime guard'),
|
|
811
|
+
payload_sha256: requireSha256(record, 'payload_sha256', 'fusion runtime guard'),
|
|
812
|
+
estimated_input_tokens: requireUsageInteger(
|
|
813
|
+
record,
|
|
814
|
+
'estimated_input_tokens',
|
|
815
|
+
'fusion runtime guard',
|
|
816
|
+
),
|
|
817
|
+
context_window_tokens: requireUsageInteger(
|
|
818
|
+
record,
|
|
819
|
+
'context_window_tokens',
|
|
820
|
+
'fusion runtime guard',
|
|
821
|
+
),
|
|
822
|
+
reserved_output_tokens: requirePositiveSafeInteger(
|
|
823
|
+
record,
|
|
824
|
+
'reserved_output_tokens',
|
|
825
|
+
'fusion runtime guard',
|
|
826
|
+
),
|
|
827
|
+
safety_reserve_tokens: requirePositiveSafeInteger(
|
|
828
|
+
record,
|
|
829
|
+
'safety_reserve_tokens',
|
|
830
|
+
'fusion runtime guard',
|
|
831
|
+
),
|
|
832
|
+
allowed_input_tokens: requireUsageInteger(
|
|
833
|
+
record,
|
|
834
|
+
'allowed_input_tokens',
|
|
835
|
+
'fusion runtime guard',
|
|
836
|
+
),
|
|
837
|
+
message: requireNonBlankString(record, 'message', 'fusion runtime guard'),
|
|
838
|
+
};
|
|
839
|
+
if (frame.code !== 'provider_payload_invalid') {
|
|
840
|
+
if (frame.context_window_tokens === 0) {
|
|
841
|
+
throw new Error('fusion runtime guard.context_window_tokens must be positive');
|
|
842
|
+
}
|
|
843
|
+
const expectedAllowed =
|
|
844
|
+
frame.context_window_tokens - frame.reserved_output_tokens - frame.safety_reserve_tokens;
|
|
845
|
+
if (expectedAllowed < 0 || frame.allowed_input_tokens !== expectedAllowed) {
|
|
846
|
+
throw new Error('fusion runtime guard allowed-input arithmetic mismatch');
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (frame.code === 'claude_cache_policy') {
|
|
850
|
+
if (
|
|
851
|
+
frame.payload_bytes !== 0 ||
|
|
852
|
+
frame.payload_sha256 !== createHash('sha256').update(Buffer.alloc(0)).digest('hex') ||
|
|
853
|
+
frame.estimated_input_tokens !== 0
|
|
854
|
+
) {
|
|
855
|
+
throw new Error('fusion runtime guard Claude cache policy payload evidence mismatch');
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (
|
|
859
|
+
frame.code === 'provider_request_budget' &&
|
|
860
|
+
frame.estimated_input_tokens <= frame.allowed_input_tokens
|
|
861
|
+
) {
|
|
862
|
+
throw new Error('fusion runtime guard provider budget code has no token overage');
|
|
863
|
+
}
|
|
864
|
+
if (
|
|
865
|
+
frame.code === 'provider_request_limit' &&
|
|
866
|
+
frame.request_ordinal <= FUSION_CHILD_MAX_PROVIDER_REQUESTS
|
|
867
|
+
) {
|
|
868
|
+
throw new Error('fusion runtime guard provider request limit was not exceeded');
|
|
869
|
+
}
|
|
870
|
+
if (frame.code === 'tool_call_limit') {
|
|
871
|
+
if (frame.tool_call_count <= FUSION_CHILD_MAX_TOOL_CALLS) {
|
|
872
|
+
throw new Error('fusion runtime guard tool call limit was not exceeded');
|
|
873
|
+
}
|
|
874
|
+
if (
|
|
875
|
+
frame.payload_bytes !== 0 ||
|
|
876
|
+
frame.payload_sha256 !== createHash('sha256').update(Buffer.alloc(0)).digest('hex') ||
|
|
877
|
+
frame.estimated_input_tokens !== 0
|
|
878
|
+
) {
|
|
879
|
+
throw new Error('fusion runtime guard tool call limit payload evidence mismatch');
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
frames.push(frame);
|
|
883
|
+
cursor = newline + 1;
|
|
884
|
+
}
|
|
885
|
+
if (frames.length > 1) throw new Error('fusion child emitted multiple runtime guard frames');
|
|
886
|
+
return frames[0];
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const FUSION_CHILD_SETTLEMENT_FAILURE_REASONS = new Set<FusionChildSettlementFailureReason>([
|
|
890
|
+
'no_records',
|
|
891
|
+
'final_not_stop',
|
|
892
|
+
'invalid_non_final',
|
|
893
|
+
'runtime_guard',
|
|
894
|
+
'cache_observation',
|
|
895
|
+
]);
|
|
896
|
+
|
|
897
|
+
export function parseFusionChildSettlement(
|
|
898
|
+
stderr: Buffer,
|
|
899
|
+
): FusionChildSettlementRecord | undefined {
|
|
900
|
+
const frames: FusionChildSettlementRecord[] = [];
|
|
901
|
+
let cursor = 0;
|
|
902
|
+
for (;;) {
|
|
903
|
+
const frameStart = stderr.indexOf(FUSION_CHILD_SETTLEMENT_PREFIX_BYTES, cursor);
|
|
904
|
+
if (frameStart < 0) break;
|
|
905
|
+
const payloadStart = frameStart + FUSION_CHILD_SETTLEMENT_PREFIX_BYTES.length;
|
|
906
|
+
const newline = stderr.indexOf(10, payloadStart);
|
|
907
|
+
if (newline < 0) throw new Error('fusion child settlement frame is not newline-terminated');
|
|
908
|
+
const bytes = stderr.subarray(payloadStart, newline);
|
|
909
|
+
const text = bytes.toString('utf8');
|
|
910
|
+
if (!Buffer.from(text, 'utf8').equals(bytes)) {
|
|
911
|
+
throw new Error('fusion child settlement frame is not valid UTF-8');
|
|
912
|
+
}
|
|
913
|
+
const record = assertClosedRecord(
|
|
914
|
+
parseJsonText(text),
|
|
915
|
+
[
|
|
916
|
+
'schema_version',
|
|
917
|
+
'status',
|
|
918
|
+
'record_count',
|
|
919
|
+
'records_sha256',
|
|
920
|
+
'final_record_index',
|
|
921
|
+
'final_text_sha256',
|
|
922
|
+
'recovered_error_ordinals',
|
|
923
|
+
'failure_reason',
|
|
924
|
+
],
|
|
925
|
+
'fusion child settlement',
|
|
926
|
+
);
|
|
927
|
+
if (record['schema_version'] !== FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION) {
|
|
928
|
+
throw new Error('fusion child settlement schema_version mismatch');
|
|
929
|
+
}
|
|
930
|
+
const status = record['status'];
|
|
931
|
+
if (status !== 'complete' && status !== 'failed') {
|
|
932
|
+
throw new Error('fusion child settlement.status is invalid');
|
|
933
|
+
}
|
|
934
|
+
const recordCount = requireUsageInteger(record, 'record_count', 'fusion child settlement');
|
|
935
|
+
const finalIndexValue = record['final_record_index'];
|
|
936
|
+
const finalRecordIndex =
|
|
937
|
+
finalIndexValue === null
|
|
938
|
+
? null
|
|
939
|
+
: requireUsageInteger(record, 'final_record_index', 'fusion child settlement');
|
|
940
|
+
const finalHashValue = record['final_text_sha256'];
|
|
941
|
+
const finalTextSha256 =
|
|
942
|
+
finalHashValue === null
|
|
943
|
+
? null
|
|
944
|
+
: requireSha256(record, 'final_text_sha256', 'fusion child settlement');
|
|
945
|
+
const recoveredValue = record['recovered_error_ordinals'];
|
|
946
|
+
if (!Array.isArray(recoveredValue)) {
|
|
947
|
+
throw new Error('fusion child settlement.recovered_error_ordinals must be an array');
|
|
948
|
+
}
|
|
949
|
+
const recoveredErrorOrdinals = recoveredValue.map((value, index) => {
|
|
950
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
951
|
+
throw new Error(
|
|
952
|
+
`fusion child settlement.recovered_error_ordinals[${String(index)}] must be a non-negative safe integer`,
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
return value;
|
|
956
|
+
});
|
|
957
|
+
for (let index = 0; index < recoveredErrorOrdinals.length; index += 1) {
|
|
958
|
+
const ordinal = recoveredErrorOrdinals[index];
|
|
959
|
+
if (
|
|
960
|
+
ordinal === undefined ||
|
|
961
|
+
ordinal >= recordCount - 1 ||
|
|
962
|
+
(index > 0 && ordinal <= (recoveredErrorOrdinals[index - 1] ?? -1))
|
|
963
|
+
) {
|
|
964
|
+
throw new Error('fusion child settlement recovered-error ordinals are not canonical');
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
const failureValue = record['failure_reason'];
|
|
968
|
+
let failureReason: FusionChildSettlementFailureReason | null;
|
|
969
|
+
if (failureValue === null) failureReason = null;
|
|
970
|
+
else if (
|
|
971
|
+
typeof failureValue === 'string' &&
|
|
972
|
+
FUSION_CHILD_SETTLEMENT_FAILURE_REASONS.has(
|
|
973
|
+
failureValue as FusionChildSettlementFailureReason,
|
|
974
|
+
)
|
|
975
|
+
) {
|
|
976
|
+
failureReason = failureValue as FusionChildSettlementFailureReason;
|
|
977
|
+
} else {
|
|
978
|
+
throw new Error('fusion child settlement.failure_reason is invalid');
|
|
979
|
+
}
|
|
980
|
+
if ((status === 'complete') !== (failureReason === null)) {
|
|
981
|
+
throw new Error('fusion child settlement status/failure_reason mismatch');
|
|
982
|
+
}
|
|
983
|
+
if (recordCount === 0) {
|
|
984
|
+
if (finalRecordIndex !== null || finalTextSha256 !== null) {
|
|
985
|
+
throw new Error('fusion child settlement empty stream has final-record evidence');
|
|
986
|
+
}
|
|
987
|
+
} else if (finalRecordIndex !== recordCount - 1 || finalTextSha256 === null) {
|
|
988
|
+
throw new Error('fusion child settlement final-record evidence mismatch');
|
|
989
|
+
}
|
|
990
|
+
frames.push({
|
|
991
|
+
schema_version: FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION,
|
|
992
|
+
status,
|
|
993
|
+
record_count: recordCount,
|
|
994
|
+
records_sha256: requireSha256(record, 'records_sha256', 'fusion child settlement'),
|
|
995
|
+
final_record_index: finalRecordIndex,
|
|
996
|
+
final_text_sha256: finalTextSha256,
|
|
997
|
+
recovered_error_ordinals: recoveredErrorOrdinals,
|
|
998
|
+
failure_reason: failureReason,
|
|
999
|
+
});
|
|
1000
|
+
cursor = newline + 1;
|
|
1001
|
+
}
|
|
1002
|
+
if (frames.length > 1) throw new Error('fusion child emitted multiple settlement frames');
|
|
1003
|
+
return frames[0];
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function assertFusionChildSettlementOrdering(stderr: Buffer): void {
|
|
1007
|
+
const settlementStart = stderr.indexOf(FUSION_CHILD_SETTLEMENT_PREFIX_BYTES);
|
|
1008
|
+
if (settlementStart < 0) return;
|
|
1009
|
+
if (stderr.indexOf(FUSION_CHILD_RESULT_PREFIX_BYTES, settlementStart) >= 0) {
|
|
1010
|
+
throw new Error('fusion child emitted result metadata after terminal settlement');
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function stripChildControlFrames(stderr: Buffer): Buffer {
|
|
1015
|
+
const prefixes = [FUSION_CHILD_SETTLEMENT_PREFIX_BYTES, FUSION_RUNTIME_GUARD_PREFIX_BYTES];
|
|
1016
|
+
const diagnostics: Buffer[] = [];
|
|
1017
|
+
let cursor = 0;
|
|
1018
|
+
for (;;) {
|
|
1019
|
+
let next = -1;
|
|
1020
|
+
let prefix: Buffer | undefined;
|
|
1021
|
+
for (const candidate of prefixes) {
|
|
1022
|
+
const found = stderr.indexOf(candidate, cursor);
|
|
1023
|
+
if (found >= 0 && (next < 0 || found < next)) {
|
|
1024
|
+
next = found;
|
|
1025
|
+
prefix = candidate;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
if (next < 0 || prefix === undefined) {
|
|
1029
|
+
if (cursor < stderr.length) diagnostics.push(stderr.subarray(cursor));
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
1032
|
+
if (next > cursor) diagnostics.push(stderr.subarray(cursor, next));
|
|
1033
|
+
const newline = stderr.indexOf(10, next + prefix.length);
|
|
1034
|
+
if (newline < 0) throw new Error('fusion child control frame is not newline-terminated');
|
|
1035
|
+
cursor = newline + 1;
|
|
1036
|
+
}
|
|
1037
|
+
return Buffer.concat(diagnostics);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
export function assertFusionRuntimeGuardMatchesModel(
|
|
1041
|
+
guard: FusionRuntimeGuardRecord,
|
|
1042
|
+
model: ResolvedFusionModel,
|
|
1043
|
+
): void {
|
|
1044
|
+
const routeUnknown = guard.provider === 'unknown' && guard.model === 'unknown';
|
|
1045
|
+
if (!routeUnknown && (guard.provider !== model.provider || guard.model !== model.model)) {
|
|
1046
|
+
throw new Error(
|
|
1047
|
+
`fusion runtime guard route mismatch: expected ${model.qualifiedId}, observed ${guard.provider}/${guard.model}`,
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
if (routeUnknown && guard.code !== 'provider_payload_invalid') {
|
|
1051
|
+
throw new Error('fusion runtime guard omitted the route for a capacity-backed refusal');
|
|
1052
|
+
}
|
|
1053
|
+
if (guard.code === 'provider_payload_invalid') return;
|
|
1054
|
+
const expectedReservedOutput = Math.max(
|
|
1055
|
+
FUSION_CHILD_MIN_OUTPUT_RESERVE_TOKENS,
|
|
1056
|
+
model.maxOutputTokens,
|
|
1057
|
+
);
|
|
1058
|
+
const expectedAllowedInput =
|
|
1059
|
+
model.contextWindow - expectedReservedOutput - FUSION_CHILD_SAFETY_RESERVE_TOKENS;
|
|
1060
|
+
if (
|
|
1061
|
+
guard.context_window_tokens !== model.contextWindow ||
|
|
1062
|
+
guard.reserved_output_tokens !== expectedReservedOutput ||
|
|
1063
|
+
guard.safety_reserve_tokens !== FUSION_CHILD_SAFETY_RESERVE_TOKENS ||
|
|
1064
|
+
guard.allowed_input_tokens !== expectedAllowedInput
|
|
1065
|
+
) {
|
|
1066
|
+
throw new Error('fusion runtime guard capacity evidence does not match the resolved route');
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
597
1070
|
export function parseFusionChildStderr(stderr: Buffer): ParsedFusionChildStderr {
|
|
1071
|
+
// Validate every package-owned side frame even though result metadata is parsed
|
|
1072
|
+
// independently below. Malformed refusal/settlement evidence must never degrade
|
|
1073
|
+
// into opaque diagnostics.
|
|
1074
|
+
parseFusionRuntimeGuard(stderr);
|
|
1075
|
+
const settlement = parseFusionChildSettlement(stderr);
|
|
1076
|
+
assertFusionChildSettlementOrdering(stderr);
|
|
598
1077
|
const records: FusionChildResultMetadata[] = [];
|
|
599
1078
|
const diagnostics: Buffer[] = [];
|
|
600
1079
|
let cursor = 0;
|
|
@@ -623,11 +1102,16 @@ export function parseFusionChildStderr(stderr: Buffer): ParsedFusionChildStderr
|
|
|
623
1102
|
records.push(parseChildResultMetadata(parsed));
|
|
624
1103
|
cursor = newline + 1;
|
|
625
1104
|
}
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
1105
|
+
const resultEvents = serializeFusionChildResultRecords(records);
|
|
1106
|
+
const events =
|
|
1107
|
+
settlement === undefined
|
|
1108
|
+
? resultEvents
|
|
1109
|
+
: Buffer.concat([resultEvents, Buffer.from(`${JSON.stringify(settlement)}\n`, 'utf8')]);
|
|
1110
|
+
return {
|
|
1111
|
+
records,
|
|
1112
|
+
events,
|
|
1113
|
+
diagnostics: stripChildControlFrames(Buffer.concat(diagnostics)),
|
|
1114
|
+
};
|
|
631
1115
|
}
|
|
632
1116
|
|
|
633
1117
|
function parseToolCallLogRecord(value: unknown, label: string): FusionToolCallLogRecord {
|
|
@@ -728,36 +1212,53 @@ export function parseFusionToolCallLog(bytes: Buffer): FusionToolCallTrace {
|
|
|
728
1212
|
};
|
|
729
1213
|
}
|
|
730
1214
|
|
|
731
|
-
|
|
732
1215
|
async function assertCompletedToolPolicy(
|
|
733
1216
|
trace: FusionToolCallTrace,
|
|
734
1217
|
capability: FusionCapability,
|
|
735
1218
|
sourcePolicy: { path: string; sha256: string } | undefined,
|
|
736
1219
|
): Promise<void> {
|
|
737
|
-
const allowed =
|
|
1220
|
+
const allowed =
|
|
1221
|
+
capability === 'inspect'
|
|
1222
|
+
? FUSION_INSPECT_TOOLS
|
|
1223
|
+
: capability === 'research'
|
|
1224
|
+
? FUSION_RESEARCH_TOOLS
|
|
1225
|
+
: [];
|
|
738
1226
|
const allowedSet = new Set<string>(allowed);
|
|
739
1227
|
const declared =
|
|
740
1228
|
capability === 'research' && sourcePolicy !== undefined
|
|
741
|
-
? new Set(
|
|
1229
|
+
? new Set(
|
|
1230
|
+
(await readFusionSourcePolicyFile(sourcePolicy.path, sourcePolicy.sha256)).sources.map(
|
|
1231
|
+
(source) => source.canonical_url,
|
|
1232
|
+
),
|
|
1233
|
+
)
|
|
742
1234
|
: undefined;
|
|
743
1235
|
for (const record of trace.records) {
|
|
744
1236
|
if (!allowedSet.has(record.tool_name)) {
|
|
745
1237
|
throw new Error(`fusion child used non-allowlisted tool ${record.tool_name}`);
|
|
746
1238
|
}
|
|
747
1239
|
if (capability === 'research' && record.tool_name === FUSION_WEB_FETCH_TOOL_NAME) {
|
|
748
|
-
if (sourcePolicy === undefined || declared === undefined)
|
|
1240
|
+
if (sourcePolicy === undefined || declared === undefined)
|
|
1241
|
+
throw new Error('fusion research source policy missing during audit');
|
|
749
1242
|
if (record.status === 'ok') {
|
|
750
1243
|
if (record.url === undefined) throw new Error('fusion research fetch audit is missing url');
|
|
751
1244
|
const canonicalUrl = canonicalizeFusionPublicUrl(record.url);
|
|
752
|
-
if (record.url !== canonicalUrl)
|
|
753
|
-
|
|
1245
|
+
if (record.url !== canonicalUrl)
|
|
1246
|
+
throw new Error('fusion research fetch audit URL was not canonical');
|
|
1247
|
+
if (!declared.has(canonicalUrl))
|
|
1248
|
+
throw new Error('fusion research fetch audit URL was not declared');
|
|
754
1249
|
if (record.rejected_url_sha256 !== undefined) {
|
|
755
|
-
throw new Error(
|
|
1250
|
+
throw new Error(
|
|
1251
|
+
'fusion research successful fetch audit must not include rejected_url_sha256',
|
|
1252
|
+
);
|
|
756
1253
|
}
|
|
757
|
-
if (record.final_url === undefined)
|
|
758
|
-
|
|
759
|
-
if (record.
|
|
760
|
-
|
|
1254
|
+
if (record.final_url === undefined)
|
|
1255
|
+
throw new Error('fusion research fetch audit is missing final_url');
|
|
1256
|
+
if (record.http_status === undefined)
|
|
1257
|
+
throw new Error('fusion research fetch audit is missing http_status');
|
|
1258
|
+
if (record.response_bytes === undefined)
|
|
1259
|
+
throw new Error('fusion research fetch audit is missing response_bytes');
|
|
1260
|
+
if (record.content_sha256 === undefined)
|
|
1261
|
+
throw new Error('fusion research fetch audit is missing content_sha256');
|
|
761
1262
|
} else {
|
|
762
1263
|
if (record.url !== undefined || record.final_url !== undefined) {
|
|
763
1264
|
throw new Error('fusion research rejected fetch audit must not persist raw URL');
|
|
@@ -828,7 +1329,8 @@ async function assertFusionToolCallLogSeal(
|
|
|
828
1329
|
}
|
|
829
1330
|
try {
|
|
830
1331
|
const stats = await handle.stat();
|
|
831
|
-
if (!stats.isFile())
|
|
1332
|
+
if (!stats.isFile())
|
|
1333
|
+
throw new Error('fusion tool-call audit completion seal is not a regular file');
|
|
832
1334
|
if (stats.size > 4096) throw new Error('fusion tool-call audit completion seal is oversized');
|
|
833
1335
|
const bytes = await handle.readFile();
|
|
834
1336
|
if (bytes.at(-1) !== 10) throw new Error('fusion tool-call audit completion seal is partial');
|
|
@@ -841,7 +1343,13 @@ async function assertFusionToolCallLogSeal(
|
|
|
841
1343
|
throw new Error('fusion tool-call audit completion seal must be an object');
|
|
842
1344
|
}
|
|
843
1345
|
const keys = Object.keys(parsed).sort();
|
|
844
|
-
const expected = [
|
|
1346
|
+
const expected = [
|
|
1347
|
+
'log_sha256',
|
|
1348
|
+
'record_count',
|
|
1349
|
+
'schema_version',
|
|
1350
|
+
'status',
|
|
1351
|
+
'total_result_bytes',
|
|
1352
|
+
];
|
|
845
1353
|
if (keys.join('\0') !== expected.join('\0')) {
|
|
846
1354
|
throw new Error('fusion tool-call audit completion seal keys mismatch');
|
|
847
1355
|
}
|
|
@@ -939,10 +1447,29 @@ export class FusionPiCompactResultParser {
|
|
|
939
1447
|
diagnostics: Buffer;
|
|
940
1448
|
} {
|
|
941
1449
|
const parsed = parseFusionChildStderr(stderr);
|
|
1450
|
+
const runtimeGuard = parseFusionRuntimeGuard(stderr);
|
|
1451
|
+
if (runtimeGuard !== undefined) {
|
|
1452
|
+
throw new Error(`Pi child runtime guard refused the run: ${runtimeGuard.message}`);
|
|
1453
|
+
}
|
|
1454
|
+
const settlement = parseFusionChildSettlement(stderr);
|
|
1455
|
+
if (settlement === undefined) throw new Error('Pi child emitted no terminal result settlement');
|
|
1456
|
+
const expectedSettlement = buildFusionChildSettlement(parsed.records);
|
|
1457
|
+
if (JSON.stringify(settlement) !== JSON.stringify(expectedSettlement)) {
|
|
1458
|
+
throw new Error('Pi child terminal result settlement does not match the metadata stream');
|
|
1459
|
+
}
|
|
1460
|
+
if (parsed.diagnostics.includes(PI_EXTENSION_ERROR_PREFIX_BYTES)) {
|
|
1461
|
+
throw new Error('Pi child reported an extension error diagnostic');
|
|
1462
|
+
}
|
|
942
1463
|
const final = parsed.records.at(-1);
|
|
943
1464
|
if (final === undefined) throw new Error('Pi child emitted no compact result metadata');
|
|
944
1465
|
for (const record of parsed.records) this.assertModel(record);
|
|
1466
|
+
this.assertCacheObservationOrdinals(parsed.records);
|
|
945
1467
|
this.assertTranscriptStopReasons(parsed.records);
|
|
1468
|
+
if (settlement.status !== 'complete') {
|
|
1469
|
+
throw new Error(
|
|
1470
|
+
`Pi child terminal result settlement failed: ${settlement.failure_reason ?? 'unknown'}`,
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
946
1473
|
const observed = this.observedFromRecords(parsed.records);
|
|
947
1474
|
return {
|
|
948
1475
|
text: reconstructFinalText(response, final),
|
|
@@ -963,6 +1490,19 @@ export class FusionPiCompactResultParser {
|
|
|
963
1490
|
}
|
|
964
1491
|
}
|
|
965
1492
|
|
|
1493
|
+
private assertCacheObservationOrdinals(records: readonly FusionChildResultMetadata[]): void {
|
|
1494
|
+
let priorOrdinal = 0;
|
|
1495
|
+
for (const [index, record] of records.entries()) {
|
|
1496
|
+
const ordinal = record.cache_observation.request_ordinal;
|
|
1497
|
+
if (ordinal <= priorOrdinal) {
|
|
1498
|
+
throw new Error(
|
|
1499
|
+
`Pi child cache observation ordinal is not increasing at result ${String(index)}: previous ${String(priorOrdinal)}, observed ${String(ordinal)}`,
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
priorOrdinal = ordinal;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
966
1506
|
private assertTranscriptStopReasons(records: readonly FusionChildResultMetadata[]): void {
|
|
967
1507
|
for (const [index, record] of records.entries()) {
|
|
968
1508
|
const isFinal = index === records.length - 1;
|
|
@@ -970,9 +1510,14 @@ export class FusionPiCompactResultParser {
|
|
|
970
1510
|
if (record.stop_reason !== 'stop') {
|
|
971
1511
|
throw new Error(this.stopReasonError('final', 'stop', record.stop_reason, true));
|
|
972
1512
|
}
|
|
973
|
-
} else if (record.stop_reason !== 'toolUse') {
|
|
1513
|
+
} else if (record.stop_reason !== 'toolUse' && !isRecoverableFusionChildErrorRecord(record)) {
|
|
974
1514
|
throw new Error(
|
|
975
|
-
this.stopReasonError(
|
|
1515
|
+
this.stopReasonError(
|
|
1516
|
+
`non-final record ${index}`,
|
|
1517
|
+
'toolUse or a settled zero-usage retry marker',
|
|
1518
|
+
record.stop_reason,
|
|
1519
|
+
true,
|
|
1520
|
+
),
|
|
976
1521
|
);
|
|
977
1522
|
}
|
|
978
1523
|
}
|
|
@@ -1251,7 +1796,13 @@ export async function runPiChild(options: RunPiChildOptions): Promise<FusionChil
|
|
|
1251
1796
|
env[FUSION_TOOL_CALL_LOG_PATH_ENV] = options.toolCallLogPath;
|
|
1252
1797
|
if (capability === 'research') {
|
|
1253
1798
|
if (options.sourcePolicy === undefined) {
|
|
1254
|
-
throw childError(
|
|
1799
|
+
throw childError(
|
|
1800
|
+
'fusion research child requires a source-policy path and hash',
|
|
1801
|
+
'orchestration_failed',
|
|
1802
|
+
options,
|
|
1803
|
+
false,
|
|
1804
|
+
false,
|
|
1805
|
+
);
|
|
1255
1806
|
}
|
|
1256
1807
|
env[FUSION_RESEARCH_ENABLED_ENV] = '1';
|
|
1257
1808
|
env[FUSION_SOURCE_POLICY_PATH_ENV] = options.sourcePolicy.path;
|
|
@@ -1335,7 +1886,15 @@ export async function runPiChild(options: RunPiChildOptions): Promise<FusionChil
|
|
|
1335
1886
|
options,
|
|
1336
1887
|
);
|
|
1337
1888
|
}
|
|
1338
|
-
terminateChild(
|
|
1889
|
+
terminateChild(
|
|
1890
|
+
child,
|
|
1891
|
+
state,
|
|
1892
|
+
platform,
|
|
1893
|
+
killProcess,
|
|
1894
|
+
killGraceMs,
|
|
1895
|
+
sigkillWaitMs,
|
|
1896
|
+
settleClose,
|
|
1897
|
+
);
|
|
1339
1898
|
}, idleTimeoutMs),
|
|
1340
1899
|
);
|
|
1341
1900
|
};
|
|
@@ -1452,11 +2011,39 @@ export async function runPiChild(options: RunPiChildOptions): Promise<FusionChil
|
|
|
1452
2011
|
observed,
|
|
1453
2012
|
);
|
|
1454
2013
|
if (close.code !== 0 || close.signal !== null) {
|
|
2014
|
+
let runtimeGuard: FusionRuntimeGuardRecord | undefined;
|
|
2015
|
+
try {
|
|
2016
|
+
runtimeGuard = parseFusionRuntimeGuard(rawStderr);
|
|
2017
|
+
if (runtimeGuard !== undefined) {
|
|
2018
|
+
assertFusionRuntimeGuardMatchesModel(runtimeGuard, options.model);
|
|
2019
|
+
}
|
|
2020
|
+
} catch (error) {
|
|
2021
|
+
throw new FusionChildRunError(
|
|
2022
|
+
withCleanupErrors(
|
|
2023
|
+
childError(
|
|
2024
|
+
`Pi child runtime guard evidence invalid: ${error instanceof Error ? error.message : String(error)}`,
|
|
2025
|
+
'child_event_invalid',
|
|
2026
|
+
options,
|
|
2027
|
+
),
|
|
2028
|
+
state.cleanupErrors,
|
|
2029
|
+
),
|
|
2030
|
+
compactEvents,
|
|
2031
|
+
response,
|
|
2032
|
+
diagnostics,
|
|
2033
|
+
close,
|
|
2034
|
+
observed,
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
1455
2037
|
throw new FusionChildRunError(
|
|
1456
2038
|
withCleanupErrors(
|
|
1457
2039
|
childError(
|
|
1458
|
-
|
|
1459
|
-
|
|
2040
|
+
runtimeGuard?.message ??
|
|
2041
|
+
`Pi child exited with code ${close.code === null ? 'null' : String(close.code)}${close.signal === null ? '' : ` (${close.signal})`}`,
|
|
2042
|
+
runtimeGuard === undefined
|
|
2043
|
+
? 'child_exit_failed'
|
|
2044
|
+
: runtimeGuard.code === 'claude_cache_policy'
|
|
2045
|
+
? 'child_cache_policy_invalid'
|
|
2046
|
+
: 'child_runtime_budget_exceeded',
|
|
1460
2047
|
options,
|
|
1461
2048
|
),
|
|
1462
2049
|
state.cleanupErrors,
|