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.
@@ -1,7 +1,22 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { closeSync, constants, fstatSync, fsyncSync, openSync, readFileSync, writeSync } from 'node:fs';
2
+ import {
3
+ closeSync,
4
+ constants,
5
+ fstatSync,
6
+ fsyncSync,
7
+ openSync,
8
+ readFileSync,
9
+ writeSync,
10
+ } from 'node:fs';
11
+ import { dirname } from 'node:path';
12
+ import { parseJsonText } from './core/common.js';
3
13
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
4
14
  import { Type, type Static } from 'typebox';
15
+ import {
16
+ estimateInputTokens,
17
+ knownJsonSegment,
18
+ resolveTokenBudgetFamily,
19
+ } from './core/context/token-budget.js';
5
20
  import {
6
21
  FUSION_TOOL_CALL_LOG_SCHEMA_VERSION,
7
22
  FUSION_WEB_FETCH_TOOL_NAME,
@@ -17,32 +32,75 @@ import {
17
32
  parseFusionSourcePolicy,
18
33
  } from './core/fusion/source-policy.js';
19
34
  import {
35
+ nonAnthropicFusionCacheObservation,
36
+ normalizeFusionClaudeCachePayload,
37
+ type FusionClaudeCacheObservation,
38
+ } from './core/fusion/claude-cache.js';
39
+ import {
40
+ FUSION_CHILD_MAX_PROVIDER_REQUESTS,
41
+ FUSION_CHILD_MAX_TOOL_CALLS,
20
42
  FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES,
43
+ FUSION_CHILD_MIN_OUTPUT_RESERVE_TOKENS,
21
44
  FUSION_CHILD_RESULT_PREFIX,
45
+ FUSION_CHILD_SAFETY_RESERVE_TOKENS,
46
+ FUSION_CHILD_SETTLEMENT_PREFIX,
22
47
  FUSION_RESEARCH_ENABLED_ENV,
48
+ FUSION_RUNTIME_GUARD_PREFIX,
49
+ FUSION_RUNTIME_GUARD_SCHEMA_VERSION,
23
50
  FUSION_SOURCE_POLICY_PATH_ENV,
24
51
  FUSION_SOURCE_POLICY_SHA256_ENV,
25
52
  FUSION_TOOL_CALL_LOG_PATH_ENV,
26
53
  FUSION_TOOL_CALL_SEAL_SCHEMA_VERSION,
27
54
  FUSION_TOOL_CALL_SEAL_SUFFIX,
28
55
  buildFusionChildResultMetadata,
56
+ buildFusionChildSettlement,
29
57
  type FusionChildResultMetadata,
58
+ type FusionChildSettlementRecord,
59
+ type FusionRuntimeGuardCode,
60
+ type FusionRuntimeGuardRecord,
30
61
  } from './core/fusion/child-protocol.js';
31
62
 
32
63
  export {
64
+ FUSION_CLAUDE_CACHE_BREAKPOINT_LIMIT,
65
+ FUSION_CLAUDE_CACHE_DEFAULT_RETENTION,
66
+ FUSION_CLAUDE_CACHE_OBSERVATION_SCHEMA_VERSION,
67
+ FUSION_CLAUDE_CACHE_RETENTION_ENV,
68
+ nonAnthropicFusionCacheObservation,
69
+ normalizeFusionClaudeCachePayload,
70
+ resolveFusionClaudeCachePolicy,
71
+ type FusionClaudeCacheNormalization,
72
+ type FusionClaudeCacheObservation,
73
+ type FusionClaudeCachePolicySource,
74
+ type FusionClaudeCacheRetention,
75
+ } from './core/fusion/claude-cache.js';
76
+
77
+ export {
78
+ FUSION_CHILD_MAX_PROVIDER_REQUESTS,
79
+ FUSION_CHILD_MAX_TOOL_CALLS,
33
80
  FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES,
81
+ FUSION_CHILD_MIN_OUTPUT_RESERVE_TOKENS,
34
82
  FUSION_CHILD_RESULT_PREFIX,
35
83
  FUSION_CHILD_RESULT_SCHEMA_VERSION,
84
+ FUSION_CHILD_SAFETY_RESERVE_TOKENS,
85
+ FUSION_CHILD_SETTLEMENT_PREFIX,
86
+ FUSION_CHILD_SETTLEMENT_SCHEMA_VERSION,
36
87
  FUSION_RESEARCH_ENABLED_ENV,
88
+ FUSION_RUNTIME_GUARD_PREFIX,
89
+ FUSION_RUNTIME_GUARD_SCHEMA_VERSION,
37
90
  FUSION_SOURCE_POLICY_PATH_ENV,
38
91
  FUSION_SOURCE_POLICY_SHA256_ENV,
39
92
  FUSION_TOOL_CALL_LOG_PATH_ENV,
40
93
  FUSION_TOOL_CALL_SEAL_SCHEMA_VERSION,
41
94
  FUSION_TOOL_CALL_SEAL_SUFFIX,
42
95
  buildFusionChildResultMetadata,
96
+ buildFusionChildSettlement,
43
97
  type FusionChildResultMetadata,
44
98
  type FusionChildResultUsageMetadata,
99
+ type FusionChildSettlementFailureReason,
100
+ type FusionChildSettlementRecord,
45
101
  type FusionChildTextBlockMetadata,
102
+ type FusionRuntimeGuardCode,
103
+ type FusionRuntimeGuardRecord,
46
104
  } from './core/fusion/child-protocol.js';
47
105
 
48
106
  const FUSION_CHILD_O_NOFOLLOW = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
@@ -100,24 +158,115 @@ function utf8JsonBytes(value: unknown, label: string): Buffer {
100
158
  return Buffer.from(text, 'utf8');
101
159
  }
102
160
 
103
- function appendToolCallLogLine(path: string, record: FusionToolCallLogRecord): void {
104
- // The log is an audit trail, not a payload copy: raw tool arguments/results may
105
- // contain secrets, so only byte counts and SHA-256 digests are persisted.
106
- const line = `${JSON.stringify(record)}\n`;
107
- const expectedBytes = Buffer.byteLength(line, 'utf8');
161
+ function throwableError(value: unknown): Error {
162
+ return value instanceof Error ? value : new Error(String(value));
163
+ }
164
+
165
+ function writeAllSync(fd: number, bytes: Buffer, label: string): void {
166
+ let offset = 0;
167
+ while (offset < bytes.length) {
168
+ const written = writeSync(fd, bytes, offset, bytes.length - offset, null);
169
+ if (written <= 0) {
170
+ throw new Error(`${label} made no write progress at byte ${String(offset)}`);
171
+ }
172
+ offset += written;
173
+ }
174
+ }
175
+
176
+ function withRegularFileDescriptorSync(
177
+ path: string,
178
+ flags: number,
179
+ mode: number | undefined,
180
+ label: string,
181
+ operation: (fd: number) => void,
182
+ ): void {
108
183
  let fd: number | undefined;
184
+ let primaryFailure: unknown;
185
+ let closeFailure: unknown;
109
186
  try {
110
- fd = openSync(path, 'a', 0o600);
111
- const written = writeSync(fd, line, undefined, 'utf8');
112
- if (written !== expectedBytes) {
113
- throw new Error(
114
- `short write: wrote ${String(written)} of ${String(expectedBytes)} bytes`,
115
- );
187
+ fd = mode === undefined ? openSync(path, flags) : openSync(path, flags, mode);
188
+ const stats = fstatSync(fd);
189
+ if (!stats.isFile()) throw new Error(`${label} at ${path} is not a regular file`);
190
+ operation(fd);
191
+ } catch (error) {
192
+ primaryFailure = error;
193
+ }
194
+ if (fd !== undefined) {
195
+ try {
196
+ closeSync(fd);
197
+ } catch (error) {
198
+ closeFailure = error;
116
199
  }
200
+ }
201
+ if (primaryFailure !== undefined && closeFailure !== undefined) {
202
+ throw new AggregateError(
203
+ [primaryFailure, closeFailure],
204
+ `${label} operation and descriptor close both failed`,
205
+ );
206
+ }
207
+ if (primaryFailure !== undefined) throw throwableError(primaryFailure);
208
+ if (closeFailure !== undefined) throw throwableError(closeFailure);
209
+ }
210
+
211
+ function fsyncParentDirectorySync(path: string): void {
212
+ if (process.platform === 'win32') return;
213
+ const parent = dirname(path);
214
+ let fd: number | undefined;
215
+ let primaryFailure: unknown;
216
+ let closeFailure: unknown;
217
+ try {
218
+ fd = openSync(parent, constants.O_RDONLY | FUSION_CHILD_O_NOFOLLOW);
219
+ const stats = fstatSync(fd);
220
+ if (!stats.isDirectory())
221
+ throw new Error(`fusion audit parent at ${parent} is not a directory`);
117
222
  fsyncSync(fd);
118
- } finally {
119
- if (fd !== undefined) closeSync(fd);
223
+ } catch (error) {
224
+ primaryFailure = error;
120
225
  }
226
+ if (fd !== undefined) {
227
+ try {
228
+ closeSync(fd);
229
+ } catch (error) {
230
+ closeFailure = error;
231
+ }
232
+ }
233
+ if (primaryFailure !== undefined && closeFailure !== undefined) {
234
+ throw new AggregateError(
235
+ [primaryFailure, closeFailure],
236
+ 'fusion audit directory sync and descriptor close both failed',
237
+ );
238
+ }
239
+ if (primaryFailure !== undefined) throw throwableError(primaryFailure);
240
+ if (closeFailure !== undefined) throw throwableError(closeFailure);
241
+ }
242
+
243
+ function createToolCallLog(path: string): void {
244
+ withRegularFileDescriptorSync(
245
+ path,
246
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | FUSION_CHILD_O_NOFOLLOW,
247
+ 0o600,
248
+ 'fusion tool-call log',
249
+ (fd) => {
250
+ fsyncSync(fd);
251
+ },
252
+ );
253
+ fsyncParentDirectorySync(path);
254
+ }
255
+
256
+ function appendToolCallLogLine(path: string, record: FusionToolCallLogRecord): void {
257
+ // The log is an audit trail, not a payload copy: raw tool arguments/results may
258
+ // contain secrets, so only byte counts and SHA-256 digests are persisted.
259
+ const bytes = Buffer.from(`${JSON.stringify(record)}\n`, 'utf8');
260
+ withRegularFileDescriptorSync(
261
+ path,
262
+ constants.O_WRONLY | constants.O_APPEND | FUSION_CHILD_O_NOFOLLOW,
263
+ undefined,
264
+ 'fusion tool-call log',
265
+ (fd) => {
266
+ writeAllSync(fd, bytes, 'fusion tool-call log append');
267
+ fsyncSync(fd);
268
+ },
269
+ );
121
270
  }
122
271
 
123
272
  function writeToolCallLogSeal(
@@ -126,7 +275,7 @@ function writeToolCallLogSeal(
126
275
  totalResultBytes: number,
127
276
  complete: boolean,
128
277
  ): void {
129
- const logBytes = readFileSync(path);
278
+ const logBytes = readRegularFileNoSymlinkSync(path, 'fusion tool-call log');
130
279
  const seal = {
131
280
  schema_version: FUSION_TOOL_CALL_SEAL_SCHEMA_VERSION,
132
281
  status: complete ? 'complete' : 'failed',
@@ -135,17 +284,22 @@ function writeToolCallLogSeal(
135
284
  log_sha256: sha256(logBytes),
136
285
  } as const;
137
286
  const bytes = Buffer.from(`${JSON.stringify(seal)}\n`, 'utf8');
138
- let fd: number | undefined;
139
- try {
140
- fd = openSync(`${path}${FUSION_TOOL_CALL_SEAL_SUFFIX}`, 'wx', 0o600);
141
- const written = writeSync(fd, bytes);
142
- if (written !== bytes.length) {
143
- throw new Error(`fusion tool-call seal short write: ${String(written)} of ${String(bytes.length)} bytes`);
144
- }
145
- fsyncSync(fd);
146
- } finally {
147
- if (fd !== undefined) closeSync(fd);
148
- }
287
+ const sealPath = `${path}${FUSION_TOOL_CALL_SEAL_SUFFIX}`;
288
+ withRegularFileDescriptorSync(
289
+ sealPath,
290
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | FUSION_CHILD_O_NOFOLLOW,
291
+ 0o600,
292
+ 'fusion tool-call audit completion seal',
293
+ (fd) => {
294
+ writeAllSync(fd, bytes, 'fusion tool-call audit completion seal');
295
+ fsyncSync(fd);
296
+ },
297
+ );
298
+ fsyncParentDirectorySync(sealPath);
299
+ }
300
+
301
+ function latchAuditProcessFailure(): void {
302
+ if (process.exitCode === undefined || process.exitCode === 0) process.exitCode = 1;
149
303
  }
150
304
 
151
305
  async function writeMetadata(record: FusionChildResultMetadata): Promise<void> {
@@ -158,6 +312,237 @@ async function writeMetadata(record: FusionChildResultMetadata): Promise<void> {
158
312
  });
159
313
  }
160
314
 
315
+ async function writeSettlement(record: FusionChildSettlementRecord): Promise<void> {
316
+ const line = `${FUSION_CHILD_SETTLEMENT_PREFIX}${JSON.stringify(record)}\n`;
317
+ await new Promise<void>((resolve, reject) => {
318
+ process.stderr.write(line, (error) => {
319
+ if (error) reject(error);
320
+ else resolve();
321
+ });
322
+ });
323
+ }
324
+
325
+ async function writeRuntimeGuard(record: FusionRuntimeGuardRecord): Promise<void> {
326
+ const line = `${FUSION_RUNTIME_GUARD_PREFIX}${JSON.stringify(record)}\n`;
327
+ await new Promise<void>((resolve, reject) => {
328
+ process.stderr.write(line, (error) => {
329
+ if (error) reject(error);
330
+ else resolve();
331
+ });
332
+ });
333
+ }
334
+
335
+ export interface FusionRuntimeRequestEvaluationInput {
336
+ payload: unknown;
337
+ provider: string | undefined;
338
+ model: string | undefined;
339
+ contextWindowTokens: number | undefined;
340
+ maxOutputTokens: number | undefined;
341
+ requestOrdinal: number;
342
+ toolCallCount: number;
343
+ }
344
+
345
+ function invalidFusionRuntimeRequest(
346
+ input: FusionRuntimeRequestEvaluationInput,
347
+ detail: string,
348
+ code: Extract<
349
+ FusionRuntimeGuardCode,
350
+ 'provider_payload_invalid' | 'claude_cache_policy'
351
+ > = 'provider_payload_invalid',
352
+ ): FusionRuntimeGuardRecord {
353
+ const contextWindowTokens =
354
+ input.contextWindowTokens !== undefined &&
355
+ Number.isSafeInteger(input.contextWindowTokens) &&
356
+ input.contextWindowTokens > 0
357
+ ? input.contextWindowTokens
358
+ : 0;
359
+ const modelOutputTokens =
360
+ input.maxOutputTokens !== undefined &&
361
+ Number.isSafeInteger(input.maxOutputTokens) &&
362
+ input.maxOutputTokens > 0
363
+ ? input.maxOutputTokens
364
+ : 0;
365
+ const reservedOutputTokens = Math.max(FUSION_CHILD_MIN_OUTPUT_RESERVE_TOKENS, modelOutputTokens);
366
+ const allowedInputTokens = Math.max(
367
+ 0,
368
+ contextWindowTokens - reservedOutputTokens - FUSION_CHILD_SAFETY_RESERVE_TOKENS,
369
+ );
370
+ const emptyPayload = Buffer.alloc(0);
371
+ return {
372
+ schema_version: FUSION_RUNTIME_GUARD_SCHEMA_VERSION,
373
+ code,
374
+ provider: input.provider ?? 'unknown',
375
+ model: input.model ?? 'unknown',
376
+ request_ordinal: input.requestOrdinal,
377
+ tool_call_count: input.toolCallCount,
378
+ payload_bytes: 0,
379
+ payload_sha256: sha256(emptyPayload),
380
+ estimated_input_tokens: 0,
381
+ context_window_tokens: contextWindowTokens,
382
+ reserved_output_tokens: reservedOutputTokens,
383
+ safety_reserve_tokens: FUSION_CHILD_SAFETY_RESERVE_TOKENS,
384
+ allowed_input_tokens: allowedInputTokens,
385
+ message: `fusion child could not validate provider request ${String(input.requestOrdinal)}: ${detail}`,
386
+ };
387
+ }
388
+
389
+ export interface PreparedFusionRuntimeRequest {
390
+ payload: unknown;
391
+ guard: FusionRuntimeGuardRecord | undefined;
392
+ }
393
+
394
+ export function prepareFusionRuntimeRequest(
395
+ input: FusionRuntimeRequestEvaluationInput,
396
+ ): PreparedFusionRuntimeRequest {
397
+ try {
398
+ const serialized: unknown = JSON.stringify(input.payload);
399
+ if (typeof serialized !== 'string') {
400
+ throw new Error('provider payload serialized to a non-string value');
401
+ }
402
+ const payload = parseJsonText(serialized);
403
+ if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
404
+ throw new Error('provider payload must serialize to a JSON object');
405
+ }
406
+ if (JSON.stringify(payload) !== serialized) {
407
+ throw new Error('provider payload does not have a stable JSON serialization');
408
+ }
409
+ return { payload, guard: evaluateFusionRuntimeRequest({ ...input, payload }) };
410
+ } catch (error) {
411
+ return {
412
+ payload: input.payload,
413
+ guard: invalidFusionRuntimeRequest(
414
+ input,
415
+ error instanceof Error ? error.message : String(error),
416
+ ),
417
+ };
418
+ }
419
+ }
420
+
421
+ export function evaluateFusionRuntimeRequest(
422
+ input: FusionRuntimeRequestEvaluationInput,
423
+ ): FusionRuntimeGuardRecord | undefined {
424
+ let code: FusionRuntimeGuardCode | undefined;
425
+ let message = '';
426
+ let payloadBytes = Buffer.alloc(0);
427
+ let estimatedInputTokens = 0;
428
+ let allowedInputTokens = 0;
429
+ const provider = input.provider ?? 'unknown';
430
+ const model = input.model ?? 'unknown';
431
+ const contextWindowTokens = input.contextWindowTokens ?? 0;
432
+ const reservedOutputTokens = Math.max(
433
+ FUSION_CHILD_MIN_OUTPUT_RESERVE_TOKENS,
434
+ input.maxOutputTokens ?? 0,
435
+ );
436
+ try {
437
+ if (input.provider === undefined || input.model === undefined) {
438
+ throw new Error('active model is unavailable');
439
+ }
440
+ if (!Number.isSafeInteger(contextWindowTokens) || contextWindowTokens <= 0) {
441
+ throw new Error('active model context window is unavailable');
442
+ }
443
+ if (
444
+ input.maxOutputTokens === undefined ||
445
+ !Number.isSafeInteger(input.maxOutputTokens) ||
446
+ input.maxOutputTokens <= 0
447
+ ) {
448
+ throw new Error('active model maximum output tokens are unavailable');
449
+ }
450
+ allowedInputTokens =
451
+ contextWindowTokens - reservedOutputTokens - FUSION_CHILD_SAFETY_RESERVE_TOKENS;
452
+ if (allowedInputTokens <= 0) {
453
+ throw new Error('active model has no safe provider input capacity');
454
+ }
455
+ const payloadText = JSON.stringify(input.payload);
456
+ if (payloadText === undefined) throw new Error('provider payload serialized to undefined');
457
+ payloadBytes = Buffer.from(payloadText, 'utf8');
458
+ const family = resolveTokenBudgetFamily({ provider, model });
459
+ estimatedInputTokens = estimateInputTokens({
460
+ family: family.family,
461
+ calibrationBacked: family.backed,
462
+ familyResolution: family.resolution,
463
+ allowedInputTokens,
464
+ scope: 'fusion',
465
+ segments: [knownJsonSegment(payloadText)],
466
+ }).tokens;
467
+ if (input.requestOrdinal > FUSION_CHILD_MAX_PROVIDER_REQUESTS) {
468
+ code = 'provider_request_limit';
469
+ message = `fusion child reached provider request ${String(input.requestOrdinal)}, exceeding the ${String(FUSION_CHILD_MAX_PROVIDER_REQUESTS)}-request execution limit`;
470
+ } else if (estimatedInputTokens > allowedInputTokens) {
471
+ code = 'provider_request_budget';
472
+ message = `fusion child blocked provider request ${String(input.requestOrdinal)} before transport: exact final payload is ${String(payloadBytes.length)} UTF-8 bytes (estimated <= ${String(estimatedInputTokens)} input tokens), exceeding ${String(allowedInputTokens)} allowed input tokens after reserving ${String(reservedOutputTokens)} model output + ${String(FUSION_CHILD_SAFETY_RESERVE_TOKENS)} safety from the ${String(contextWindowTokens)}-token context window`;
473
+ }
474
+ } catch (error) {
475
+ code = 'provider_payload_invalid';
476
+ message = `fusion child could not validate provider request ${String(input.requestOrdinal)}: ${error instanceof Error ? error.message : String(error)}`;
477
+ }
478
+ if (code === undefined) return undefined;
479
+ return {
480
+ schema_version: FUSION_RUNTIME_GUARD_SCHEMA_VERSION,
481
+ code,
482
+ provider,
483
+ model,
484
+ request_ordinal: input.requestOrdinal,
485
+ tool_call_count: input.toolCallCount,
486
+ payload_bytes: payloadBytes.length,
487
+ payload_sha256: sha256(payloadBytes),
488
+ estimated_input_tokens: estimatedInputTokens,
489
+ context_window_tokens: contextWindowTokens,
490
+ reserved_output_tokens: reservedOutputTokens,
491
+ safety_reserve_tokens: FUSION_CHILD_SAFETY_RESERVE_TOKENS,
492
+ allowed_input_tokens: allowedInputTokens,
493
+ message,
494
+ };
495
+ }
496
+
497
+ export interface FusionRuntimeToolLimitEvaluationInput {
498
+ provider: string | undefined;
499
+ model: string | undefined;
500
+ contextWindowTokens: number | undefined;
501
+ maxOutputTokens: number | undefined;
502
+ requestOrdinal: number;
503
+ toolCallCount: number;
504
+ }
505
+
506
+ export function evaluateFusionRuntimeToolLimit(
507
+ input: FusionRuntimeToolLimitEvaluationInput,
508
+ ): FusionRuntimeGuardRecord | undefined {
509
+ if (input.toolCallCount <= FUSION_CHILD_MAX_TOOL_CALLS) return undefined;
510
+ const contextWindowTokens =
511
+ input.contextWindowTokens !== undefined &&
512
+ Number.isSafeInteger(input.contextWindowTokens) &&
513
+ input.contextWindowTokens > 0
514
+ ? input.contextWindowTokens
515
+ : 0;
516
+ const modelOutputTokens =
517
+ input.maxOutputTokens !== undefined &&
518
+ Number.isSafeInteger(input.maxOutputTokens) &&
519
+ input.maxOutputTokens > 0
520
+ ? input.maxOutputTokens
521
+ : 0;
522
+ const reservedOutputTokens = Math.max(FUSION_CHILD_MIN_OUTPUT_RESERVE_TOKENS, modelOutputTokens);
523
+ const allowedInputTokens = Math.max(
524
+ 0,
525
+ contextWindowTokens - reservedOutputTokens - FUSION_CHILD_SAFETY_RESERVE_TOKENS,
526
+ );
527
+ const emptyPayload = Buffer.alloc(0);
528
+ return {
529
+ schema_version: FUSION_RUNTIME_GUARD_SCHEMA_VERSION,
530
+ code: 'tool_call_limit',
531
+ provider: input.provider ?? 'unknown',
532
+ model: input.model ?? 'unknown',
533
+ request_ordinal: input.requestOrdinal,
534
+ tool_call_count: input.toolCallCount,
535
+ payload_bytes: 0,
536
+ payload_sha256: sha256(emptyPayload),
537
+ estimated_input_tokens: 0,
538
+ context_window_tokens: contextWindowTokens,
539
+ reserved_output_tokens: reservedOutputTokens,
540
+ safety_reserve_tokens: FUSION_CHILD_SAFETY_RESERVE_TOKENS,
541
+ allowed_input_tokens: allowedInputTokens,
542
+ message: `fusion child reached tool call ${String(input.toolCallCount)}, exceeding the ${String(FUSION_CHILD_MAX_TOOL_CALLS)}-call execution limit`,
543
+ };
544
+ }
545
+
161
546
  function strictFusionWebFetchArgs(args: unknown): FusionWebFetchParamsValue {
162
547
  if (typeof args !== 'object' || args === null || Array.isArray(args)) {
163
548
  throw new Error(`${FUSION_WEB_FETCH_TOOL_NAME} arguments must be an object`);
@@ -165,7 +550,9 @@ function strictFusionWebFetchArgs(args: unknown): FusionWebFetchParamsValue {
165
550
  const keys = Object.keys(args);
166
551
  const unknownKeys = keys.filter((key) => key !== 'url' && key !== 'extract');
167
552
  if (unknownKeys.length > 0 || !keys.includes('url')) {
168
- throw new Error(`${FUSION_WEB_FETCH_TOOL_NAME} arguments must contain url and optional extract only`);
553
+ throw new Error(
554
+ `${FUSION_WEB_FETCH_TOOL_NAME} arguments must contain url and optional extract only`,
555
+ );
169
556
  }
170
557
  const url = Reflect.get(args, 'url');
171
558
  if (typeof url !== 'string' || url.trim().length === 0) {
@@ -189,7 +576,10 @@ function stringField(value: object, key: string): string | undefined {
189
576
  return typeof field === 'string' && field.length > 0 ? field : undefined;
190
577
  }
191
578
 
192
- function fetchAuditMetadataFromObject(value: object, fallbackUrl: string): FusionWebFetchAuditMetadata {
579
+ function fetchAuditMetadataFromObject(
580
+ value: object,
581
+ fallbackUrl: string,
582
+ ): FusionWebFetchAuditMetadata {
193
583
  const metadata: FusionWebFetchAuditMetadata = {
194
584
  url: stringField(value, 'url') ?? canonicalizeFusionPublicUrl(fallbackUrl),
195
585
  };
@@ -204,7 +594,10 @@ function fetchAuditMetadataFromObject(value: object, fallbackUrl: string): Fusio
204
594
  return metadata;
205
595
  }
206
596
 
207
- function fetchAuditMetadataFromError(error: unknown, attemptedUrl: string): FusionWebFetchAuditMetadata {
597
+ function fetchAuditMetadataFromError(
598
+ error: unknown,
599
+ attemptedUrl: string,
600
+ ): FusionWebFetchAuditMetadata {
208
601
  const metadata: FusionWebFetchAuditMetadata = { rejected_url_sha256: sha256(attemptedUrl) };
209
602
  if (error instanceof FusionWebFetchError && typeof error === 'object' && error !== null) {
210
603
  const status = numberField(error, 'status');
@@ -213,7 +606,6 @@ function fetchAuditMetadataFromError(error: unknown, attemptedUrl: string): Fusi
213
606
  return metadata;
214
607
  }
215
608
 
216
-
217
609
  function readRegularFileNoSymlinkSync(path: string, label: string): Buffer {
218
610
  let fd: number | undefined;
219
611
  try {
@@ -237,13 +629,17 @@ function loadDeclaredResearchUrls(): ReadonlySet<string> {
237
629
  const policyPath = process.env[FUSION_SOURCE_POLICY_PATH_ENV];
238
630
  const expectedHash = process.env[FUSION_SOURCE_POLICY_SHA256_ENV];
239
631
  if (policyPath === undefined || expectedHash === undefined) {
240
- throw new Error(`${FUSION_WEB_FETCH_TOOL_NAME} research mode requires source policy path and sha256`);
632
+ throw new Error(
633
+ `${FUSION_WEB_FETCH_TOOL_NAME} research mode requires source policy path and sha256`,
634
+ );
241
635
  }
242
- if (!/^[0-9a-f]{64}$/.test(expectedHash)) throw new Error('fusion source policy hash is malformed');
636
+ if (!/^[0-9a-f]{64}$/.test(expectedHash))
637
+ throw new Error('fusion source policy hash is malformed');
243
638
  const bytes = readRegularFileNoSymlinkSync(policyPath, 'fusion source policy');
244
639
  if (sha256(bytes) !== expectedHash) throw new Error('fusion source policy hash mismatch');
245
640
  const text = bytes.toString('utf8');
246
- if (!Buffer.from(text, 'utf8').equals(bytes)) throw new Error('fusion source policy is not UTF-8');
641
+ if (!Buffer.from(text, 'utf8').equals(bytes))
642
+ throw new Error('fusion source policy is not UTF-8');
247
643
  const parsed = parseFusionSourcePolicy(JSON.parse(text));
248
644
  return new Set(parsed.sources.map((source) => source.canonical_url));
249
645
  }
@@ -279,30 +675,196 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
279
675
  throw new Error(`${FUSION_RESEARCH_ENABLED_ENV} must be unset or exactly 1`);
280
676
  }
281
677
  if (researchEnabled === '1' && toolCallLogPath === undefined) {
282
- throw new Error(`${FUSION_WEB_FETCH_TOOL_NAME} research mode requires ${FUSION_TOOL_CALL_LOG_PATH_ENV}`);
678
+ throw new Error(
679
+ `${FUSION_WEB_FETCH_TOOL_NAME} research mode requires ${FUSION_TOOL_CALL_LOG_PATH_ENV}`,
680
+ );
283
681
  }
284
682
  const declaredResearchUrls = researchEnabled === '1' ? loadDeclaredResearchUrls() : undefined;
285
683
  const fetchAuditMetadata = new Map<string, FusionWebFetchAuditMetadata>();
684
+ let providerRequestCount = 0;
685
+ let toolCallCount = 0;
686
+ let runtimeGuardFailed = false;
687
+ let settlementPublished = false;
688
+ const childResultRecords: FusionChildResultMetadata[] = [];
689
+ let pendingCacheObservation: FusionClaudeCacheObservation | undefined;
690
+
691
+ pi.on('before_provider_request', async (event, ctx) => {
692
+ providerRequestCount += 1;
693
+ if (runtimeGuardFailed) {
694
+ ctx.abort();
695
+ return event.payload;
696
+ }
697
+
698
+ const model = ctx.model;
699
+ let cacheNormalizedPayload = event.payload;
700
+ let cacheObservation: FusionClaudeCacheObservation;
701
+ try {
702
+ if (model?.provider === 'anthropic') {
703
+ const supportsLongCacheRetention =
704
+ model.compat !== undefined && 'supportsLongCacheRetention' in model.compat
705
+ ? model.compat.supportsLongCacheRetention
706
+ : undefined;
707
+ const normalized = normalizeFusionClaudeCachePayload({
708
+ payload: event.payload,
709
+ requestOrdinal: providerRequestCount,
710
+ supportsLongCacheRetention:
711
+ typeof supportsLongCacheRetention === 'boolean'
712
+ ? supportsLongCacheRetention
713
+ : undefined,
714
+ });
715
+ cacheNormalizedPayload = normalized.payload;
716
+ cacheObservation = normalized.observation;
717
+ } else {
718
+ cacheObservation = nonAnthropicFusionCacheObservation(providerRequestCount);
719
+ }
720
+ } catch (error) {
721
+ const guard = invalidFusionRuntimeRequest(
722
+ {
723
+ payload: event.payload,
724
+ provider: model?.provider,
725
+ model: model?.id,
726
+ contextWindowTokens: model?.contextWindow,
727
+ maxOutputTokens: model?.maxTokens,
728
+ requestOrdinal: providerRequestCount,
729
+ toolCallCount,
730
+ },
731
+ `Claude cache policy rejected the final payload: ${error instanceof Error ? error.message : String(error)}`,
732
+ 'claude_cache_policy',
733
+ );
734
+ runtimeGuardFailed = true;
735
+ latchAuditProcessFailure();
736
+ ctx.abort();
737
+ await writeRuntimeGuard(guard);
738
+ return event.payload;
739
+ }
740
+ pendingCacheObservation = cacheObservation;
741
+
742
+ const prepared = prepareFusionRuntimeRequest({
743
+ payload: cacheNormalizedPayload,
744
+ provider: model?.provider,
745
+ model: model?.id,
746
+ contextWindowTokens: model?.contextWindow,
747
+ maxOutputTokens: model?.maxTokens,
748
+ requestOrdinal: providerRequestCount,
749
+ toolCallCount,
750
+ });
751
+ const guard = prepared.guard;
752
+ if (guard === undefined) return prepared.payload;
753
+ runtimeGuardFailed = true;
754
+ latchAuditProcessFailure();
755
+ ctx.abort();
756
+ await writeRuntimeGuard(guard);
757
+ return prepared.payload;
758
+ });
759
+
286
760
  if (toolCallLogPath !== undefined) {
287
- // Create the log immediately, before tools can run. Without this, an absent file
288
- // is ambiguous: it could mean "this child made zero tool calls" or "the audit trail
289
- // was never written". The parent must be able to tell those apart, so existence is
290
- // established up front and a missing file is a hard failure rather than an empty trace.
291
- closeSync(openSync(toolCallLogPath, 'a', 0o600));
761
+ // Establish the audit file before tools can run. Exclusive creation makes a reused
762
+ // attempt path or redirected file loud instead of appending to untrusted history.
763
+ try {
764
+ createToolCallLog(toolCallLogPath);
765
+ } catch (error) {
766
+ latchAuditProcessFailure();
767
+ throw error;
768
+ }
769
+
770
+ type AuditPhase = 'open' | 'finalizing' | 'sealed-complete' | 'sealed-failed';
771
+ interface ToolStart {
772
+ startedAt: number;
773
+ toolName: string;
774
+ }
775
+
776
+ let phase: AuditPhase = 'open';
292
777
  let ordinal = 0;
293
778
  let totalToolResultBytes = 0;
294
779
  let auditFailed = false;
295
- const starts = new Map<string, number>();
296
- pi.on('tool_call', (event) => {
297
- starts.set(event.toolCallId, Date.now());
780
+ const starts = new Map<string, ToolStart>();
781
+
782
+ const failAudit = (error: unknown): never => {
783
+ auditFailed = true;
784
+ latchAuditProcessFailure();
785
+ throw error instanceof Error ? error : new Error(String(error));
786
+ };
787
+ const requireOpen = (eventName: string): void => {
788
+ if (phase !== 'open') {
789
+ failAudit(`fusion tool-call audit received ${eventName} while ${phase}`);
790
+ }
791
+ };
792
+ const finalizeAudit = (normalSettlement: boolean, trigger: string): void => {
793
+ if (phase !== 'open') {
794
+ failAudit(
795
+ `fusion tool-call audit received duplicate finalization from ${trigger} while ${phase}`,
796
+ );
797
+ }
798
+ phase = 'finalizing';
799
+ const unmatchedStarts = starts.size;
800
+ const complete =
801
+ normalSettlement && !auditFailed && !runtimeGuardFailed && unmatchedStarts === 0;
802
+ if (!complete) {
803
+ auditFailed = true;
804
+ latchAuditProcessFailure();
805
+ }
806
+ try {
807
+ writeToolCallLogSeal(toolCallLogPath, ordinal, totalToolResultBytes, complete);
808
+ } catch (error) {
809
+ phase = 'sealed-failed';
810
+ failAudit(error);
811
+ }
812
+ phase = complete ? 'sealed-complete' : 'sealed-failed';
813
+ if (!complete) {
814
+ failAudit(
815
+ `fusion tool-call audit finalized as failed from ${trigger}: ${String(unmatchedStarts)} unmatched tool start(s)`,
816
+ );
817
+ }
818
+ };
819
+
820
+ pi.on('tool_call', async (event, ctx) => {
821
+ try {
822
+ requireOpen('tool_call');
823
+ if (runtimeGuardFailed) {
824
+ ctx.abort();
825
+ return { block: true, reason: 'fusion child runtime guard already refused the run' };
826
+ }
827
+ toolCallCount += 1;
828
+ const model = ctx.model;
829
+ const guard = evaluateFusionRuntimeToolLimit({
830
+ provider: model?.provider,
831
+ model: model?.id,
832
+ contextWindowTokens: model?.contextWindow,
833
+ maxOutputTokens: model?.maxTokens,
834
+ requestOrdinal: providerRequestCount,
835
+ toolCallCount,
836
+ });
837
+ if (guard !== undefined) {
838
+ runtimeGuardFailed = true;
839
+ latchAuditProcessFailure();
840
+ ctx.abort();
841
+ await writeRuntimeGuard(guard);
842
+ return { block: true, reason: guard.message };
843
+ }
844
+ if (starts.has(event.toolCallId)) {
845
+ throw new Error(`fusion tool-call log duplicate start for ${event.toolCallId}`);
846
+ }
847
+ starts.set(event.toolCallId, {
848
+ startedAt: Date.now(),
849
+ toolName: event.toolName,
850
+ });
851
+ return undefined;
852
+ } catch (error) {
853
+ return failAudit(error);
854
+ }
298
855
  });
299
856
  pi.on('tool_result', (event) => {
300
857
  try {
858
+ requireOpen('tool_result');
301
859
  const start = starts.get(event.toolCallId);
302
860
  if (start === undefined) {
303
861
  throw new Error(`fusion tool-call log missing start for ${event.toolCallId}`);
304
862
  }
305
- starts.delete(event.toolCallId);
863
+ if (start.toolName !== event.toolName) {
864
+ throw new Error(
865
+ `fusion tool-call log tool mismatch for ${event.toolCallId}: started ${start.toolName}, completed ${event.toolName}`,
866
+ );
867
+ }
306
868
  const argumentsBytes = utf8JsonBytes(event.input, 'arguments');
307
869
  const resultBytes = utf8JsonBytes(
308
870
  {
@@ -314,7 +876,7 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
314
876
  'result',
315
877
  );
316
878
  const fetchMetadata = fetchAuditMetadata.get(event.toolCallId);
317
- fetchAuditMetadata.delete(event.toolCallId);
879
+ const nextTotalToolResultBytes = totalToolResultBytes + resultBytes.length;
318
880
  const record: FusionToolCallLogRecord = {
319
881
  schema_version: FUSION_TOOL_CALL_LOG_SCHEMA_VERSION,
320
882
  ordinal,
@@ -324,25 +886,33 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
324
886
  result_bytes: resultBytes.length,
325
887
  result_sha256: sha256(resultBytes),
326
888
  status: event.isError === true ? 'error' : 'ok',
327
- duration_ms: Math.max(0, Date.now() - start),
889
+ duration_ms: Math.max(0, Date.now() - start.startedAt),
328
890
  ...(fetchMetadata === undefined ? {} : fetchMetadata),
329
891
  };
330
- ordinal += 1;
331
892
  appendToolCallLogLine(toolCallLogPath, record);
332
- totalToolResultBytes += resultBytes.length;
893
+ starts.delete(event.toolCallId);
894
+ fetchAuditMetadata.delete(event.toolCallId);
895
+ ordinal += 1;
896
+ totalToolResultBytes = nextTotalToolResultBytes;
333
897
  if (totalToolResultBytes > FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES) {
334
898
  throw new Error(
335
899
  `fusion candidate exceeded the aggregate tool-output budget: ${String(totalToolResultBytes)} bytes across ${String(ordinal)} calls exceeds ${String(FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES)}`,
336
900
  );
337
901
  }
338
902
  } catch (error) {
339
- auditFailed = true;
340
- throw error;
903
+ failAudit(error);
341
904
  }
342
905
  });
343
- pi.on('agent_end', () => {
344
- const complete = !auditFailed && starts.size === 0;
345
- writeToolCallLogSeal(toolCallLogPath, ordinal, totalToolResultBytes, complete);
906
+ // agent_end is only the end of one low-level run. Pi may still retry, compact and
907
+ // retry, or consume queued continuations. Sealing there created stale prefix seals.
908
+ pi.on('agent_settled', (_event, ctx) => {
909
+ if (!ctx.isIdle()) {
910
+ failAudit('fusion child emitted agent_settled while the agent was not idle');
911
+ }
912
+ finalizeAudit(true, 'agent_settled');
913
+ });
914
+ pi.on('session_shutdown', () => {
915
+ if (phase === 'open') finalizeAudit(false, 'session_shutdown before agent_settled');
346
916
  });
347
917
  }
348
918
 
@@ -366,10 +936,14 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
366
936
  try {
367
937
  const canonicalUrl = canonicalizeFusionPublicUrl(params.url);
368
938
  if (params.url !== canonicalUrl) {
369
- throw new Error(`${FUSION_WEB_FETCH_TOOL_NAME} URL must exactly match its declared canonical URL`);
939
+ throw new Error(
940
+ `${FUSION_WEB_FETCH_TOOL_NAME} URL must exactly match its declared canonical URL`,
941
+ );
370
942
  }
371
943
  if (declaredResearchUrls === undefined || !declaredResearchUrls.has(canonicalUrl)) {
372
- throw new Error(`${FUSION_WEB_FETCH_TOOL_NAME} URL was not declared in the research source policy`);
944
+ throw new Error(
945
+ `${FUSION_WEB_FETCH_TOOL_NAME} URL was not declared in the research source policy`,
946
+ );
373
947
  }
374
948
  const result = await fusionWebFetch(
375
949
  params.extract === undefined
@@ -402,6 +976,36 @@ export default function fusionChildExtension(pi: ExtensionAPI): void {
402
976
 
403
977
  pi.on('message_end', async (event) => {
404
978
  if (event.message.role !== 'assistant') return;
405
- await writeMetadata(buildFusionChildResultMetadata(event.message));
979
+ const cacheObservation = pendingCacheObservation;
980
+ if (cacheObservation === undefined) {
981
+ latchAuditProcessFailure();
982
+ throw new Error('fusion child assistant result has no matching cache-policy observation');
983
+ }
984
+ pendingCacheObservation = undefined;
985
+ const record = buildFusionChildResultMetadata(event.message, cacheObservation);
986
+ await writeMetadata(record);
987
+ childResultRecords.push(record);
988
+ });
989
+ pi.on('agent_settled', async (_event, ctx) => {
990
+ if (settlementPublished) {
991
+ latchAuditProcessFailure();
992
+ throw new Error('fusion child received duplicate agent_settled for result settlement');
993
+ }
994
+ if (!ctx.isIdle()) {
995
+ latchAuditProcessFailure();
996
+ throw new Error('fusion child result settlement observed agent_settled while not idle');
997
+ }
998
+ settlementPublished = true;
999
+ const cacheObservationFailed = pendingCacheObservation !== undefined;
1000
+ const settlement = buildFusionChildSettlement(
1001
+ childResultRecords,
1002
+ runtimeGuardFailed,
1003
+ cacheObservationFailed,
1004
+ );
1005
+ if (settlement.status !== 'complete') latchAuditProcessFailure();
1006
+ await writeSettlement(settlement);
1007
+ });
1008
+ pi.on('session_shutdown', () => {
1009
+ if (!settlementPublished) latchAuditProcessFailure();
406
1010
  });
407
1011
  }