pi-background-tasks 2.3.0 → 2.4.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.
@@ -14,9 +14,14 @@ import {
14
14
  type DelegateUsageReport,
15
15
  } from './core/delegate/types.js';
16
16
  import { verifyDelegateSeedBytes } from './core/delegate/seed.js';
17
- import { evaluateDelegateRuntimeBudget } from './core/delegate/budget.js';
17
+ import {
18
+ DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS,
19
+ DELEGATE_FINALIZATION_TRIGGER_TOKENS,
20
+ evaluateDelegateRuntimeBudget,
21
+ } from './core/delegate/budget.js';
18
22
  import { utf8ByteClassBreakdown } from './core/context/token-budget.js';
19
23
  import {
24
+ assertWellFormedUtf8,
20
25
  buildDelegateResultPackage,
21
26
  serializeDelegateResultPackage,
22
27
  } from './core/delegate/result-package.js';
@@ -31,10 +36,10 @@ import {
31
36
  * parent:
32
37
  *
33
38
  * - verifying the frozen seed bytes before the first model call;
34
- * - measuring the outgoing message set before every model call and refusing to
35
- * let an oversized one reach the provider;
36
- * - spilling oversized tool results to hashed artifacts and replacing them with
37
- * explicit receipts before they enter the transcript;
39
+ * - measuring the outgoing message set before every model call and requesting
40
+ * no-tool finalization when advisory runway becomes low;
41
+ * - spilling oversized or runway-pressuring tool results to hashed artifacts
42
+ * and replacing them with explicit receipts before transcript entry;
38
43
  * - asserting every assistant message came from the pinned route;
39
44
  * - enforcing turn and tool-call limits;
40
45
  * - committing exactly one result package atomically.
@@ -63,8 +68,19 @@ interface GuardState {
63
68
  spilled: DelegateSpillReceipt[];
64
69
  attestations: DelegateRouteAttestation[];
65
70
  usage: Usage | undefined;
71
+ usageIncomplete: boolean;
66
72
  usageUnavailableReason: string | undefined;
67
73
  answerBlocks: string[];
74
+ answerBytes: number;
75
+ retainedGrowthTokens: number;
76
+ retainedGrowthBudgetTokens: number | undefined;
77
+ retainedToolResultBytes: number;
78
+ contextPressureSpillBytes: number;
79
+ finalizationRequested: boolean;
80
+ finalizationReason: string | undefined;
81
+ contextMeasurements: RuntimeContextMeasurement[];
82
+ firstRequestObservedInputTokens: number | undefined;
83
+ runtimeBudgetWritten: boolean;
68
84
  terminal: TerminalLatch | undefined;
69
85
  committed: boolean;
70
86
  }
@@ -82,6 +98,16 @@ interface TerminalLatch {
82
98
  message: string;
83
99
  }
84
100
 
101
+ interface RuntimeContextMeasurement {
102
+ request_ordinal: number;
103
+ retained_utf8_bytes: number;
104
+ estimated_input_tokens: number;
105
+ allowed_input_tokens: number;
106
+ signed_headroom_tokens: number;
107
+ dominant_byte_class: string;
108
+ finalization_requested: boolean;
109
+ }
110
+
85
111
  /**
86
112
  * Stop reasons that may be committed as a complete answer.
87
113
  *
@@ -115,6 +141,85 @@ function utf8(value: string): Buffer {
115
141
  return Buffer.from(value, 'utf8');
116
142
  }
117
143
 
144
+ interface DelegateToolTextPart {
145
+ readonly type: 'text';
146
+ readonly text: string;
147
+ }
148
+
149
+ interface DelegateToolImagePart {
150
+ readonly type: 'image';
151
+ readonly data: string;
152
+ readonly mimeType: string;
153
+ }
154
+
155
+ type DelegateToolResultPart = DelegateToolTextPart | DelegateToolImagePart;
156
+ type SpillContentFormat = DelegateSpillReceipt['content_format'];
157
+
158
+ function encodeToolResultContent(
159
+ content: ReadonlyArray<DelegateToolResultPart>,
160
+ ): { payload: Buffer; contentFormat: Exclude<SpillContentFormat, undefined> } {
161
+ if (content.length === 1) {
162
+ const only = content[0];
163
+ if (only?.type === 'text') {
164
+ return {
165
+ payload: assertWellFormedUtf8(only.text, 'delegate single-text tool result'),
166
+ contentFormat: 'single_text_utf8',
167
+ };
168
+ }
169
+ }
170
+ const normalized = content.map((part) => {
171
+ if (part.type === 'text' && typeof part.text === 'string') {
172
+ return { type: 'text' as const, text: part.text };
173
+ }
174
+ if (
175
+ part.type === 'image' &&
176
+ typeof part.data === 'string' &&
177
+ typeof part.mimeType === 'string'
178
+ ) {
179
+ return {
180
+ type: 'image' as const,
181
+ data: part.data,
182
+ mimeType: part.mimeType,
183
+ };
184
+ }
185
+ throw new Error('delegate tool result contains an unsupported or malformed content block');
186
+ });
187
+ return {
188
+ payload: utf8(
189
+ JSON.stringify({
190
+ schema_version: 'pi-background-tasks.delegate-tool-result-content.v1',
191
+ content: normalized,
192
+ }),
193
+ ),
194
+ contentFormat: 'tool_result_content_json_v1',
195
+ };
196
+ }
197
+
198
+ function addUsage(current: Usage | undefined, delta: Usage): Usage {
199
+ const prior = current ?? {
200
+ input: 0,
201
+ output: 0,
202
+ cacheRead: 0,
203
+ cacheWrite: 0,
204
+ totalTokens: 0,
205
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
206
+ };
207
+ return {
208
+ input: prior.input + delta.input,
209
+ output: prior.output + delta.output,
210
+ cacheRead: prior.cacheRead + delta.cacheRead,
211
+ cacheWrite: prior.cacheWrite + delta.cacheWrite,
212
+ totalTokens: prior.totalTokens + delta.totalTokens,
213
+ cost: {
214
+ input: prior.cost.input + delta.cost.input,
215
+ output: prior.cost.output + delta.cost.output,
216
+ cacheRead: prior.cost.cacheRead + delta.cost.cacheRead,
217
+ cacheWrite: prior.cost.cacheWrite + delta.cost.cacheWrite,
218
+ total: prior.cost.total + delta.cost.total,
219
+ },
220
+ };
221
+ }
222
+
118
223
  function finiteNonNegative(source: object, key: string): number | undefined {
119
224
  const value: unknown = Reflect.get(source, key);
120
225
  if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return undefined;
@@ -305,8 +410,19 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
305
410
  spilled: [],
306
411
  attestations: [],
307
412
  usage: undefined,
413
+ usageIncomplete: false,
308
414
  usageUnavailableReason: 'the child produced no assistant message carrying usage',
309
415
  answerBlocks: [],
416
+ answerBytes: 0,
417
+ retainedGrowthTokens: 0,
418
+ retainedGrowthBudgetTokens: undefined,
419
+ retainedToolResultBytes: 0,
420
+ contextPressureSpillBytes: 0,
421
+ finalizationRequested: false,
422
+ finalizationReason: undefined,
423
+ contextMeasurements: [],
424
+ firstRequestObservedInputTokens: undefined,
425
+ runtimeBudgetWritten: false,
310
426
  terminal: undefined,
311
427
  committed: false,
312
428
  };
@@ -316,13 +432,92 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
316
432
  }
317
433
 
318
434
  function usageReport(): DelegateUsageReport {
319
- if (state.usage !== undefined) return { status: 'observed', usage: state.usage };
435
+ if (!state.usageIncomplete && state.usage !== undefined) {
436
+ return { status: 'observed', usage: state.usage };
437
+ }
320
438
  return {
321
439
  status: 'unavailable',
322
440
  reason: state.usageUnavailableReason ?? 'usage was not reported by the provider',
323
441
  };
324
442
  }
325
443
 
444
+ function remainingGrowthTokens(): number {
445
+ if (state.retainedGrowthBudgetTokens === undefined) return 0;
446
+ return Math.max(0, state.retainedGrowthBudgetTokens - state.retainedGrowthTokens);
447
+ }
448
+
449
+ function requestFinalization(reason: string): void {
450
+ if (!state.finalizationRequested) {
451
+ state.finalizationRequested = true;
452
+ state.finalizationReason = reason;
453
+ }
454
+ pi.setActiveTools([]);
455
+ }
456
+
457
+ function accountRetainedGrowth(bytes: number, source: string): void {
458
+ state.retainedGrowthTokens += bytes;
459
+ if (
460
+ state.retainedGrowthBudgetTokens !== undefined &&
461
+ remainingGrowthTokens() <= DELEGATE_FINALIZATION_TRIGGER_TOKENS
462
+ ) {
463
+ requestFinalization(
464
+ `${source} left ${String(remainingGrowthTokens())} protected retained-growth tokens`,
465
+ );
466
+ }
467
+ }
468
+
469
+ function writeRuntimeBudgetRecord(): void {
470
+ if (state.runtimeBudgetWritten) return;
471
+ const latest = state.contextMeasurements.at(-1);
472
+ const firstEstimate = state.contextMeasurements[0]?.estimated_input_tokens;
473
+ const calibrationViolation =
474
+ firstEstimate !== undefined &&
475
+ state.firstRequestObservedInputTokens !== undefined &&
476
+ state.firstRequestObservedInputTokens > firstEstimate
477
+ ? {
478
+ forecast_input_tokens: firstEstimate,
479
+ observed_input_tokens: state.firstRequestObservedInputTokens,
480
+ tokens_under_forecast:
481
+ state.firstRequestObservedInputTokens - firstEstimate,
482
+ }
483
+ : null;
484
+ commitFileSync(
485
+ join(artifactDirAbs, 'runtime-budget.json'),
486
+ utf8(
487
+ `${JSON.stringify(
488
+ {
489
+ schema_version: 'pi-background-tasks.delegate-runtime-budget.v1',
490
+ task_id: seed.task_id,
491
+ launch_nonce: seed.launch_nonce,
492
+ policy: {
493
+ live_provider_context_owner: 'pi_and_provider',
494
+ retained_growth_estimator: 'provable_1_byte_per_token',
495
+ finalization_input_reserve_tokens: DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS,
496
+ finalization_trigger_tokens: DELEGATE_FINALIZATION_TRIGGER_TOKENS,
497
+ },
498
+ retained_growth_budget_tokens: state.retainedGrowthBudgetTokens ?? null,
499
+ retained_growth_tokens: state.retainedGrowthTokens,
500
+ retained_tool_result_bytes: state.retainedToolResultBytes,
501
+ spilled_tool_result_bytes: state.spilled.reduce(
502
+ (sum, receipt) => sum + receipt.byte_length,
503
+ 0,
504
+ ),
505
+ context_pressure_spill_bytes: state.contextPressureSpillBytes,
506
+ finalization_requested: state.finalizationRequested,
507
+ finalization_reason: state.finalizationReason ?? null,
508
+ first_request_observed_input_tokens: state.firstRequestObservedInputTokens ?? null,
509
+ calibration_violation: calibrationViolation,
510
+ latest_context_estimate: latest ?? null,
511
+ context_measurements: state.contextMeasurements,
512
+ },
513
+ null,
514
+ 2,
515
+ )}\n`,
516
+ ),
517
+ );
518
+ state.runtimeBudgetWritten = true;
519
+ }
520
+
326
521
  /**
327
522
  * Commit exactly one result package.
328
523
  *
@@ -335,13 +530,6 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
335
530
  writeTerminalRecord(state.terminal);
336
531
  return;
337
532
  }
338
- if (state.answerBlocks.length === 0) {
339
- writeTerminalRecord({
340
- code: 'child_exited_without_commit',
341
- message: 'the delegate child produced no assistant answer text',
342
- });
343
- return;
344
- }
345
533
  // A hash proves the bytes are intact; it cannot prove they are complete.
346
534
  // Only an approved terminal stop reason may be committed as success, so a
347
535
  // response cut short by the output-token limit, a content filter, an
@@ -349,7 +537,14 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
349
537
  if (!ACCEPTED_STOP_REASONS.has(stopReason)) {
350
538
  writeTerminalRecord({
351
539
  code: stopReason === 'length' ? 'child_model_output_limit' : 'child_result_invalid',
352
- message: `the delegate child stopped with reason "${stopReason}", so its answer is incomplete and is not committed as a result; the captured text is preserved in the child transcript`,
540
+ message: `the delegate child stopped with reason "${stopReason}", so its answer is incomplete and is not committed as a result; the complete assistant message remains in the child transcript`,
541
+ });
542
+ return;
543
+ }
544
+ if (state.answerBlocks.length === 0) {
545
+ writeTerminalRecord({
546
+ code: 'child_exited_without_commit',
547
+ message: 'the delegate child produced no final assistant answer text',
353
548
  });
354
549
  return;
355
550
  }
@@ -360,6 +555,7 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
360
555
  });
361
556
  return;
362
557
  }
558
+ writeRuntimeBudgetRecord();
363
559
  const pkg = buildDelegateResultPackage({
364
560
  taskId: seed.task_id,
365
561
  launchNonce: seed.launch_nonce,
@@ -379,6 +575,7 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
379
575
  }
380
576
 
381
577
  function writeTerminalRecord(terminal: TerminalLatch): void {
578
+ writeRuntimeBudgetRecord();
382
579
  commitFileSync(
383
580
  join(artifactDirAbs, 'child-terminal.json'),
384
581
  utf8(
@@ -404,10 +601,11 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
404
601
  name: 'delegate_read_artifact',
405
602
  label: 'Delegate Artifact Read',
406
603
  description:
407
- 'Read an exact byte range from a spilled tool-result artifact. Returns exactly the requested range or fails; it never returns fewer bytes than requested and never clamps the request.',
408
- promptSnippet: 'Read an exact byte range from a spilled tool-result artifact',
604
+ 'Read an exact byte range from a spilled tool-result artifact as lossless base64. Returns the complete requested range or fails; it never decodes arbitrary bytes as UTF-8, returns fewer bytes, or clamps the request.',
605
+ promptSnippet: 'Read an exact artifact byte range as lossless base64',
409
606
  promptGuidelines: [
410
607
  'Use delegate_read_artifact when a tool result was replaced by a spill receipt and the omitted bytes are actually needed.',
608
+ 'The response body is base64 for the exact requested bytes. Decode it according to the receipt content_format.',
411
609
  'Request a bounded range. A request past the end of the artifact fails loudly rather than returning a short read.',
412
610
  ],
413
611
  parameters: ArtifactReadParams,
@@ -445,8 +643,32 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
445
643
  `delegate_read_artifact returned ${String(slice.length)} of ${String(params.length)} requested bytes`,
446
644
  );
447
645
  }
646
+ const encoded = slice.toString('base64');
647
+ const responseText = [
648
+ `[delegate artifact range] artifact=${params.artifact} offset=${String(params.offset)} length=${String(params.length)} encoding=base64`,
649
+ encoded,
650
+ ].join('\n');
651
+ const responseBytes = utf8(responseText).length;
652
+ const availableInlineBytes = Math.max(
653
+ 0,
654
+ Math.min(
655
+ seed.limits.max_tool_result_bytes,
656
+ remainingGrowthTokens() - DELEGATE_FINALIZATION_TRIGGER_TOKENS,
657
+ ),
658
+ );
659
+ if (responseBytes > availableInlineBytes) {
660
+ if (availableInlineBytes === 0) {
661
+ requestFinalization('no retained-growth runway remains for an artifact range read');
662
+ throw new Error(
663
+ `delegate_read_artifact cannot retain another artifact range because protected final-answer runway is active; stop reading and answer from gathered evidence`,
664
+ );
665
+ }
666
+ throw new Error(
667
+ `delegate_read_artifact requested ${String(params.length)} raw bytes whose lossless base64 response is ${String(responseBytes)} bytes, but current protected runway permits at most ${String(availableInlineBytes)} inline bytes; request a smaller exact range`,
668
+ );
669
+ }
448
670
  return Promise.resolve({
449
- content: [{ type: 'text' as const, text: slice.toString('utf8') }],
671
+ content: [{ type: 'text' as const, text: responseText }],
450
672
  details: {
451
673
  artifact: params.artifact,
452
674
  offset: params.offset,
@@ -458,12 +680,15 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
458
680
  });
459
681
 
460
682
  pi.on('context', (event, ctx) => {
461
- // Fail closed. Pi swallows exceptions thrown from a `context` handler and
462
- // dispatches the call regardless, so a throw inside this guard would let the
463
- // ORIGINAL unguarded message set reach the provider. Every path therefore
464
- // runs inside this try, and the catch latches terminal state and suppresses
465
- // the content rather than letting the original through.
683
+ // Measurement failures remain fail-closed. A successful estimate is
684
+ // advisory: Fusion BUG-185 proved that subtracting hypothetical output from
685
+ // a live provider payload can falsely refuse valid work. Package-owned
686
+ // growth is bounded earlier by the tool-result spill governor instead.
466
687
  try {
688
+ if (state.terminal !== undefined) {
689
+ ctx.abort();
690
+ return { messages: suppressedMessages(event.messages) };
691
+ }
467
692
  const measurement = retainedInputMeasurement(event.messages, ctx.getSystemPrompt());
468
693
  const verdict = evaluateDelegateRuntimeBudget(
469
694
  {
@@ -474,32 +699,74 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
474
699
  seed.limits.allowed_input_tokens,
475
700
  seed.route,
476
701
  );
477
- if (verdict.withinBudget) return undefined;
478
- const message = `delegate child context reached ${String(verdict.measuredTokens)} input tokens against a ${String(verdict.allowedTokens)}-token allowance on route ${seed.route.qualified_id}, over by ${String(verdict.overageTokens)}; estimator family ${verdict.rateSource.family}, source ${verdict.rateSource.source}, backed=${String(verdict.backed)}, dominant_byte_class=${verdict.dominantByteClass}, rate ${String(verdict.rateSource.effective_rate_bytes_per_token_x100)}/100 B/tok + ${String(verdict.rateSource.affine_f_tokens)} tokens`;
479
- latch('provider_context_budget_exhausted', message);
480
- // Barrier one: terminate the run. Depending on the supported Pi line,
481
- // this skips provider dispatch or hands it an already-aborted signal.
482
- ctx.abort();
483
- // Barrier two: remove the content itself, so the request could not carry it
484
- // even if a provider ignored the aborted signal. Retaining only the first
485
- // message keeps the shape valid without transmitting the oversized tail.
486
- return { messages: suppressedMessages(event.messages) };
702
+ if (state.retainedGrowthBudgetTokens === undefined) {
703
+ state.retainedGrowthBudgetTokens = Math.max(
704
+ 0,
705
+ verdict.allowedTokens -
706
+ verdict.measuredTokens -
707
+ DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS,
708
+ );
709
+ }
710
+ state.contextMeasurements.push({
711
+ request_ordinal: state.contextMeasurements.length + 1,
712
+ retained_utf8_bytes: measurement.bytes,
713
+ estimated_input_tokens: verdict.measuredTokens,
714
+ allowed_input_tokens: verdict.allowedTokens,
715
+ signed_headroom_tokens: verdict.allowedTokens - verdict.measuredTokens,
716
+ dominant_byte_class: verdict.dominantByteClass,
717
+ finalization_requested: state.finalizationRequested,
718
+ });
719
+ if (
720
+ verdict.measuredTokens >=
721
+ verdict.allowedTokens - DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS
722
+ ) {
723
+ requestFinalization(
724
+ `advisory retained-input estimate reached ${String(verdict.measuredTokens)} of ${String(verdict.allowedTokens)} allowed tokens`,
725
+ );
726
+ }
727
+ if (!state.finalizationRequested) return undefined;
728
+ const finalizationMessage = {
729
+ role: 'user' as const,
730
+ content: `[delegate finalization runway] Stop investigating and do not call tools. Produce the final self-contained answer now from evidence already gathered. Reason: ${state.finalizationReason ?? 'protected final-answer runway is active'}.`,
731
+ timestamp: Date.now(),
732
+ };
733
+ return { messages: [...event.messages, finalizationMessage] };
487
734
  } catch (error) {
488
735
  latch(
489
736
  'child_result_invalid',
490
- `delegate context guard failed and the run was stopped rather than dispatched unguarded: ${error instanceof Error ? error.message : String(error)}`,
737
+ `delegate context measurement failed and the run was stopped rather than dispatched unguarded: ${error instanceof Error ? error.message : String(error)}`,
491
738
  );
492
739
  try {
493
740
  ctx.abort();
494
741
  } catch {
495
- // An abort failure must not resurrect the unguarded message set. The
496
- // latch above already prevents a success commit, and the suppressed
497
- // replacement below still removes the content from this request.
742
+ // The terminal latch prevents success even if abort itself fails.
498
743
  }
499
744
  return { messages: suppressedMessages(event.messages) };
500
745
  }
501
746
  });
502
747
 
748
+ pi.on('tool_call', (_event, ctx) => {
749
+ if (state.finalizationRequested) {
750
+ return {
751
+ block: true,
752
+ reason: 'delegate protected final-answer runway is active; answer now without tools',
753
+ };
754
+ }
755
+ state.toolCalls += 1;
756
+ if (state.toolCalls > seed.limits.max_tool_calls) {
757
+ latch(
758
+ 'child_tool_call_limit',
759
+ `delegate child exceeded its ${String(seed.limits.max_tool_calls)} tool-call limit`,
760
+ );
761
+ ctx.abort();
762
+ return {
763
+ block: true,
764
+ reason: `delegate child exceeded its ${String(seed.limits.max_tool_calls)} tool-call limit`,
765
+ };
766
+ }
767
+ return undefined;
768
+ });
769
+
503
770
  pi.on('tool_result', (event) => {
504
771
  // Fail closed for the same reason as the context guard: a throw here would
505
772
  // let the ORIGINAL oversized payload flow into the transcript.
@@ -525,31 +792,36 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
525
792
  function guardToolResult(event: {
526
793
  toolName: string;
527
794
  toolCallId: string;
528
- content: ReadonlyArray<{ type: string; text?: string }>;
795
+ content: ReadonlyArray<DelegateToolResultPart>;
529
796
  }): { content: Array<{ type: 'text'; text: string }>; isError?: boolean } | undefined {
530
- state.toolCalls += 1;
531
- if (state.toolCalls > seed.limits.max_tool_calls) {
532
- latch(
533
- 'child_tool_call_limit',
534
- `delegate child exceeded its ${String(seed.limits.max_tool_calls)} tool-call limit`,
535
- );
536
- }
537
- const texts = event.content.flatMap((part) =>
538
- part.type === 'text' && typeof part.text === 'string' ? [part.text] : [],
539
- );
540
- const joined = texts.join('');
541
- const payload = utf8(joined);
797
+ // A single text block keeps its exact UTF-8 payload for convenient range
798
+ // reads. Multi-block and image-bearing results use a closed JSON envelope
799
+ // so block boundaries, MIME types, and complete base64 image data survive
800
+ // a spill. Unknown blocks fail closed rather than disappearing from hashes.
801
+ const encodedContent = encodeToolResultContent(event.content);
802
+ const payload = encodedContent.payload;
542
803
  state.totalToolOutputBytes += payload.length;
543
804
  if (state.totalToolOutputBytes > seed.limits.max_total_tool_output_bytes) {
544
805
  latch(
545
806
  'aggregate_tool_output_cap',
546
807
  `delegate child accumulated ${String(state.totalToolOutputBytes)} bytes of tool output, exceeding its ${String(seed.limits.max_total_tool_output_bytes)}-byte cap`,
547
808
  );
809
+ requestFinalization('the aggregate raw tool-output cap was reached');
810
+ const withheld = '[delegate: tool result withheld because the aggregate raw-output cap was reached; answer from evidence already gathered]';
811
+ accountRetainedGrowth(utf8(withheld).length, 'aggregate-cap receipt');
812
+ return { content: [{ type: 'text', text: withheld }], isError: true };
813
+ }
814
+ const contextPressure =
815
+ state.retainedGrowthBudgetTokens === undefined || payload.length > remainingGrowthTokens();
816
+ if (!contextPressure && payload.length <= seed.limits.max_tool_result_bytes) {
817
+ state.retainedToolResultBytes += payload.length;
818
+ accountRetainedGrowth(payload.length, `${event.toolName} inline result`);
819
+ return undefined;
548
820
  }
549
- if (payload.length <= seed.limits.max_tool_result_bytes) return undefined;
550
821
 
551
- // Oversized: spill to a hashed artifact and replace the transcript content
552
- // with an explicit receipt. The raw payload never enters the transcript.
822
+ // Per-result oversized or route-pressure output is spilled in full and
823
+ // replaced by a receipt. Conservative false positives cause an explicit
824
+ // spill, never task failure or silent byte loss.
553
825
  const turnSequence = state.turns;
554
826
  const sourceCallIndex = state.spilled.length;
555
827
  const safeCallId = event.toolCallId.replace(/[^a-zA-Z0-9_.-]+/g, '-').slice(0, 64);
@@ -595,20 +867,22 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
595
867
  source_call_index: sourceCallIndex,
596
868
  byte_length: payload.length,
597
869
  sha256: sha256(payload),
870
+ content_format: encodedContent.contentFormat,
598
871
  };
599
872
  state.spilled.push(receipt);
873
+ if (contextPressure) state.contextPressureSpillBytes += payload.length;
874
+ const spillReason = contextPressure
875
+ ? `retaining it would consume protected final-answer runway (${String(remainingGrowthTokens())} tokens remain)`
876
+ : `it exceeded the ${String(seed.limits.max_tool_result_bytes)}-byte per-result transcript cap`;
877
+ const receiptText = [
878
+ `[delegate spill receipt] The ${event.toolName} result was ${String(payload.length)} bytes; ${spillReason}.`,
879
+ `It was written in full to ${relPath} (sha256 ${receipt.sha256}, content_format ${encodedContent.contentFormat}).`,
880
+ 'Nothing was truncated: the complete encoded content is on disk.',
881
+ `Read an exact range as base64 with delegate_read_artifact({artifact:"${relPath}", offset, length}).`,
882
+ ].join('\n');
883
+ accountRetainedGrowth(utf8(receiptText).length, `${event.toolName} spill receipt`);
600
884
  return {
601
- content: [
602
- {
603
- type: 'text' as const,
604
- text: [
605
- `[delegate spill receipt] The ${event.toolName} result was ${String(payload.length)} bytes, over the ${String(seed.limits.max_tool_result_bytes)}-byte transcript cap.`,
606
- `It was written in full to ${relPath} (sha256 ${receipt.sha256}).`,
607
- 'Nothing was truncated: the complete bytes are on disk.',
608
- `Read an exact range with delegate_read_artifact({artifact:"${relPath}", offset, length}).`,
609
- ].join('\n'),
610
- },
611
- ],
885
+ content: [{ type: 'text' as const, text: receiptText }],
612
886
  };
613
887
  }
614
888
 
@@ -644,21 +918,46 @@ export default function delegateChildExtension(pi: ExtensionAPI): void {
644
918
  }
645
919
  const observedUsage = readUsage(Reflect.get(event.message, 'usage'));
646
920
  if (observedUsage === undefined) {
647
- // Never synthesize zero usage. An absent or incomplete usage record stays
648
- // explicitly unavailable so the parent cannot report a free run.
921
+ // Never synthesize zero usage. Once one turn is incomplete, later usage
922
+ // cannot conceal it by replacing the missing record.
923
+ state.usageIncomplete = true;
649
924
  state.usageUnavailableReason =
650
- 'the provider did not report a complete token/cost usage record';
925
+ 'at least one provider turn did not report a complete token/cost usage record';
651
926
  } else {
652
- state.usage = observedUsage;
653
- state.usageUnavailableReason = undefined;
927
+ if (state.firstRequestObservedInputTokens === undefined) {
928
+ state.firstRequestObservedInputTokens =
929
+ observedUsage.input + observedUsage.cacheRead + observedUsage.cacheWrite;
930
+ }
931
+ state.usage = addUsage(state.usage, observedUsage);
932
+ if (!state.usageIncomplete) state.usageUnavailableReason = undefined;
654
933
  }
655
934
  const content: unknown = Reflect.get(event.message, 'content');
656
935
  if (!Array.isArray(content)) return;
936
+ if (attestation.stop_reason !== 'stop') {
937
+ const retained = messageContentMeasurement(content);
938
+ accountRetainedGrowth(retained.bytes, 'assistant intermediate message');
939
+ // Tool-use narration is retained in the transcript for reasoning but is
940
+ // not part of the delegate's committed answer. Only the final clean-stop
941
+ // assistant message owns the answer data plane.
942
+ return;
943
+ }
944
+ state.answerBlocks = [];
945
+ state.answerBytes = 0;
657
946
  for (const part of content) {
658
947
  if (typeof part !== 'object' || part === null) continue;
659
948
  if (Reflect.get(part, 'type') !== 'text') continue;
660
949
  const text: unknown = Reflect.get(part, 'text');
661
- if (typeof text === 'string' && text.length > 0) state.answerBlocks.push(text);
950
+ if (typeof text !== 'string' || text.length === 0) continue;
951
+ const bytes = utf8(text).length;
952
+ state.answerBytes += bytes;
953
+ if (state.answerBytes > seed.limits.max_answer_bytes) {
954
+ latch(
955
+ 'child_capture_limit',
956
+ `delegate child answer text reached ${String(state.answerBytes)} bytes, exceeding its ${String(seed.limits.max_answer_bytes)}-byte capture contract; the transcript is preserved and no prefix is committed`,
957
+ );
958
+ continue;
959
+ }
960
+ state.answerBlocks.push(text);
662
961
  }
663
962
  });
664
963
 
@@ -466,7 +466,7 @@ export function registerDelegateExtension(
466
466
  child_session_id: prepared.preflight.childSessionId,
467
467
  artifact_dir: prepared.facts.artifactDir,
468
468
  seed_sha256: prepared.facts.seedSha256,
469
- seed_utf8_bytes: prepared.preflight.plan.seed_utf8_bytes,
469
+ seed_utf8_bytes: Buffer.byteLength(prepared.preflight.seed.serialized, 'utf8'),
470
470
  budget: prepared.facts.budget,
471
471
  extension_mode: extensionMode,
472
472
  auto_deliver: autoDeliver,
@@ -480,7 +480,8 @@ export function registerDelegateExtension(
480
480
  `Route pinned: ${route.qualified_id} (${route.origin}); it is never substituted.`,
481
481
  `Child session: ${prepared.preflight.childSessionId} (separate from this session)`,
482
482
  `Artifacts: ${prepared.facts.artifactDir}`,
483
- `Seed: ${String(prepared.preflight.plan.seed_utf8_bytes)} bytes, sha256 ${prepared.facts.seedSha256}`,
483
+ `Seed: ${String(Buffer.byteLength(prepared.preflight.seed.serialized, 'utf8'))} bytes, sha256 ${prepared.facts.seedSha256}`,
484
+ `Child prompt: ${String(prepared.preflight.plan.child_prompt_utf8_bytes)} bytes; launch estimate ${String(prepared.preflight.plan.launch_input_tokens_upper_bound)} / ${String(prepared.preflight.plan.route.allowed_input_tokens)} allowed input tokens; protected retained-growth runway ${String(prepared.preflight.plan.retained_growth_budget_tokens)} tokens.`,
484
485
  `Estimator: family ${prepared.facts.budget.family}, source ${prepared.facts.budget.rate_source.source}, rate ${String(prepared.facts.budget.rate_source.effective_rate_bytes_per_token_x100)}/100 B/tok + ${String(prepared.facts.budget.rate_source.affine_f_tokens)} tokens${prepared.facts.budget.rate_source.warning === null ? '' : `; warning: ${prepared.facts.budget.rate_source.warning}`}`,
485
486
  `Capability: ${capability} (read/search/list only)`,
486
487
  `Extension mode: ${extensionMode}${extensionMode === 'ambient' ? ' — WARNING: arbitrary discovered extension code executes in the child; the tool allowlist does not sandbox it, so inspect-only process isolation is weakened.' : ' (ambient extension discovery disabled)'}`,
@@ -698,6 +699,8 @@ export function registerDelegateExtension(
698
699
  route: { provider: facts.route.provider, model: facts.route.model },
699
700
  taskStatus: task.status === 'completed' ? 'completed' : task.status,
700
701
  taskError: task.error,
702
+ taskOutputPath: task.outputPath,
703
+ taskOutputAbsPath: task.outputAbsPath,
701
704
  });
702
705
 
703
706
  if (terminal.error !== undefined || terminal.result === undefined) {