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.
@@ -18,7 +18,7 @@ export const DELEGATE_RESULT_PACKAGE_SCHEMA_VERSION =
18
18
  'pi-background-tasks.delegate-result.v1' as const;
19
19
  export const DELEGATE_RECEIPT_SCHEMA_VERSION = 'pi-background-tasks.delegate-receipt.v1' as const;
20
20
  export const DELEGATE_BUDGET_PLAN_SCHEMA_VERSION =
21
- 'pi-background-tasks.delegate-budget-plan.v2' as const;
21
+ 'pi-background-tasks.delegate-budget-plan.v3' as const;
22
22
  export const DELEGATE_MANIFEST_SCHEMA_VERSION = 'pi-background-tasks.delegate-manifest.v2' as const;
23
23
 
24
24
  /**
@@ -64,6 +64,7 @@ export interface DelegatePinnedRoute extends DelegateRoute {
64
64
  export interface DelegateBudgetRouteSource {
65
65
  family: TokenBudgetFamily;
66
66
  rate_source: TokenBudgetRateSource;
67
+ conservative_rate_source?: TokenBudgetRateSource | undefined;
67
68
  }
68
69
 
69
70
  export interface DelegateContextPolicyDescriptor {
@@ -192,6 +193,11 @@ export interface DelegateResultPackageV1 {
192
193
  spilled_artifacts: readonly DelegateSpillReceipt[];
193
194
  }
194
195
 
196
+ export type DelegateSpillContentFormat =
197
+ | 'single_text_utf8'
198
+ | 'tool_result_content_json_v1'
199
+ | 'opaque_bytes';
200
+
195
201
  export interface DelegateSpillReceipt {
196
202
  schema_version: typeof DELEGATE_RECEIPT_SCHEMA_VERSION;
197
203
  artifact: string;
@@ -201,6 +207,11 @@ export interface DelegateSpillReceipt {
201
207
  source_call_index: number;
202
208
  byte_length: number;
203
209
  sha256: string;
210
+ /**
211
+ * Encoding of the hashed artifact bytes. Optional only for compatibility
212
+ * with v1 receipts written before content formats were recorded.
213
+ */
214
+ content_format?: DelegateSpillContentFormat | undefined;
204
215
  }
205
216
 
206
217
  export const DELEGATE_ERROR_CODES = [
@@ -23,8 +23,8 @@ export const FUSION_TOOL_CALL_SEAL_SUFFIX = '.seal.json';
23
23
  export const FUSION_RUNTIME_GUARD_SCHEMA_VERSION =
24
24
  'pi-background-tasks.fusion-runtime-guard.v2' as const;
25
25
  export const FUSION_RUNTIME_GUARD_PREFIX = '\u001ePI_FUSION_RUNTIME_GUARD ';
26
- export const FUSION_CHILD_MAX_PROVIDER_REQUESTS = 128;
27
- export const FUSION_CHILD_MAX_TOOL_CALLS = 192;
26
+ export const FUSION_CHILD_MAX_PROVIDER_REQUESTS = 550;
27
+ export const FUSION_CHILD_MAX_TOOL_CALLS = 600;
28
28
 
29
29
  /**
30
30
  * Aggregate ceiling on tool-result bytes a single candidate child may accumulate.
@@ -32,7 +32,7 @@ export const FUSION_CHILD_MAX_TOOL_CALLS = 192;
32
32
  * The byte ceiling complements the tool/request count limits and pre-spawn stage
33
33
  * budgets. It remains an independent bound on total tool material across the child run.
34
34
  */
35
- export const FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES = 8 * 1024 * 1024;
35
+ export const FUSION_CHILD_MAX_TOTAL_TOOL_RESULT_BYTES = 32 * 1024 * 1024;
36
36
 
37
37
  export type FusionRuntimeGuardCode =
38
38
  | 'provider_request_limit'
@@ -76,8 +76,8 @@ import { resolveAnthropicAttributionExtensionPath } from '../anthropic-attributi
76
76
 
77
77
  // The response cap now applies to one final full answer, not cumulative Pi JSON events.
78
78
  export const FUSION_CHILD_STDOUT_LIMIT_BYTES = 32 * 1024 * 1024;
79
- export const FUSION_CHILD_STDERR_LIMIT_BYTES = 4 * 1024 * 1024;
80
- export const FUSION_CHILD_TIMEOUT_MS = 30 * 60 * 1000;
79
+ export const FUSION_CHILD_STDERR_LIMIT_BYTES = 16 * 1024 * 1024;
80
+ export const FUSION_CHILD_TIMEOUT_MS = 50 * 60 * 1000;
81
81
  /**
82
82
  * Stale-action watchdog threshold.
83
83
  *
@@ -86,10 +86,12 @@ export const FUSION_CHILD_TIMEOUT_MS = 30 * 60 * 1000;
86
86
  * the final assistant message, so a single slow model turn is genuinely silent on both
87
87
  * streams. The threshold must therefore exceed the longest plausible single turn, not the
88
88
  * longest plausible tool call: a value tuned to tool latency would kill healthy children
89
- * mid-reasoning. 1200s stays inside the 30-minute absolute cap while leaving a wide
90
- * margin over observed turn latency.
89
+ * mid-reasoning. Thirty-five minutes leaves a wide margin over observed turn latency
90
+ * while remaining materially below the 50-minute absolute cap, so the stale-action
91
+ * watchdog retains an independent purpose instead of becoming an unreachable duplicate
92
+ * timeout.
91
93
  */
92
- export const FUSION_CHILD_IDLE_TIMEOUT_MS = 20 * 60 * 1000;
94
+ export const FUSION_CHILD_IDLE_TIMEOUT_MS = 35 * 60 * 1000;
93
95
  export const FUSION_CHILD_KILL_GRACE_MS = 3000;
94
96
  export const FUSION_CHILD_SIGKILL_WAIT_MS = 5000;
95
97
  const FUSION_PI_CHILD_O_NOFOLLOW =
@@ -1311,6 +1313,11 @@ async function assertFusionToolCallLogSeal(
1311
1313
  if (recordCount !== trace.summary.count) {
1312
1314
  throw new Error('fusion tool-call audit completion seal record count mismatch');
1313
1315
  }
1316
+ if (recordCount > FUSION_CHILD_MAX_TOOL_CALLS) {
1317
+ throw new Error(
1318
+ `fusion tool-call audit exceeds tool-call limit ${String(FUSION_CHILD_MAX_TOOL_CALLS)}`,
1319
+ );
1320
+ }
1314
1321
  if (totalResultBytes !== trace.summary.total_result_bytes) {
1315
1322
  throw new Error('fusion tool-call audit completion seal result-byte total mismatch');
1316
1323
  }
@@ -8,8 +8,8 @@ import { TextDecoder } from 'node:util';
8
8
 
9
9
  import type TurndownService from 'turndown';
10
10
 
11
- export const FUSION_WEB_FETCH_TIMEOUT_MS = 60_000;
12
- export const FUSION_WEB_FETCH_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
11
+ export const FUSION_WEB_FETCH_TIMEOUT_MS = 90_000;
12
+ export const FUSION_WEB_FETCH_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
13
13
  export const FUSION_WEB_FETCH_MAX_OUTPUT_BYTES = 32 * 1024;
14
14
  export const FUSION_WEB_FETCH_MAX_REDIRECTS = 5;
15
15
 
@@ -57,6 +57,12 @@ export type FusionTransportRequest = (
57
57
  options: http.RequestOptions | https.RequestOptions,
58
58
  ) => http.ClientRequest;
59
59
 
60
+ export type FusionContentExtractor = (
61
+ body: Buffer,
62
+ contentType: string,
63
+ requestedFormat: 'text' | 'markdown',
64
+ ) => Promise<{ content: string; format: 'text' | 'markdown' }>;
65
+
60
66
  export interface FusionWebFetchOptions {
61
67
  lookup?: FusionDnsLookup;
62
68
  request?: FusionTransportRequest;
@@ -68,6 +74,8 @@ export interface FusionWebFetchOptions {
68
74
  maxOutputBytes?: number;
69
75
  maxRedirects?: number;
70
76
  allowBlockedAddressesForTests?: boolean;
77
+ /** Test seam for proving extraction remains inside the full-operation deadline. */
78
+ extractContent?: FusionContentExtractor;
71
79
  }
72
80
 
73
81
  interface NormalizedRequestUrl {
@@ -106,6 +114,7 @@ interface EffectiveOptions {
106
114
  maxOutputBytes: number;
107
115
  maxRedirects: number;
108
116
  allowBlockedAddressesForTests: boolean;
117
+ extractContent: FusionContentExtractor;
109
118
  }
110
119
 
111
120
  interface ExtractionResult {
@@ -218,8 +227,21 @@ export async function fusionWebFetch(
218
227
  continue;
219
228
  }
220
229
 
221
- const extracted = await extractContent(result.body, result.contentType, requestedFormat);
230
+ const extracted = await extractWithinDeadline(
231
+ result.body,
232
+ result.contentType,
233
+ requestedFormat,
234
+ effective,
235
+ deadlineMs,
236
+ currentUrl,
237
+ );
222
238
  const capped = capUtf8Bytes(extracted.content, effective.maxOutputBytes);
239
+ const contentSha256 = createHash('sha256').update(result.body).digest('hex');
240
+ assertFetchDeadline(effective, deadlineMs, currentUrl, 'content extraction');
241
+ const durationMs = Math.max(0, effective.now() - start);
242
+ if (durationMs > effective.timeoutMs) {
243
+ throw fetchTimeout(currentUrl, 'content extraction');
244
+ }
223
245
  return {
224
246
  url: requestedUrl.toString(),
225
247
  final_url: currentUrl.toString(),
@@ -229,8 +251,8 @@ export async function fusionWebFetch(
229
251
  truncated: capped.truncated,
230
252
  content: capped.content,
231
253
  response_bytes: result.body.byteLength,
232
- content_sha256: createHash('sha256').update(result.body).digest('hex'),
233
- duration_ms: Math.max(0, effective.now() - start),
254
+ content_sha256: contentSha256,
255
+ duration_ms: durationMs,
234
256
  };
235
257
  }
236
258
  }
@@ -247,9 +269,54 @@ function mergeOptions(options: FusionWebFetchOptions): EffectiveOptions {
247
269
  maxOutputBytes: options.maxOutputBytes ?? FUSION_WEB_FETCH_MAX_OUTPUT_BYTES,
248
270
  maxRedirects: options.maxRedirects ?? FUSION_WEB_FETCH_MAX_REDIRECTS,
249
271
  allowBlockedAddressesForTests: options.allowBlockedAddressesForTests ?? false,
272
+ extractContent: options.extractContent ?? extractContent,
250
273
  };
251
274
  }
252
275
 
276
+ function fetchTimeout(url: URL, phase: string): FusionWebFetchError {
277
+ return new FusionWebFetchError(
278
+ 'request_timeout',
279
+ `fusion_web_fetch request timeout elapsed during ${phase}`,
280
+ { url: url.toString() },
281
+ );
282
+ }
283
+
284
+ function assertFetchDeadline(
285
+ options: EffectiveOptions,
286
+ deadlineMs: number,
287
+ url: URL,
288
+ phase: string,
289
+ ): number {
290
+ const remainingMs = deadlineMs - options.now();
291
+ if (remainingMs <= 0) throw fetchTimeout(url, phase);
292
+ return remainingMs;
293
+ }
294
+
295
+ async function extractWithinDeadline(
296
+ body: Buffer,
297
+ contentType: string,
298
+ requestedFormat: 'text' | 'markdown',
299
+ options: EffectiveOptions,
300
+ deadlineMs: number,
301
+ url: URL,
302
+ ): Promise<ExtractionResult> {
303
+ const remainingMs = assertFetchDeadline(options, deadlineMs, url, 'content extraction');
304
+ let timer: NodeJS.Timeout | undefined;
305
+ try {
306
+ const timeout = new Promise<never>((_resolve, reject) => {
307
+ timer = setTimeout(() => reject(fetchTimeout(url, 'content extraction')), remainingMs);
308
+ });
309
+ const extraction = Promise.resolve().then(() =>
310
+ options.extractContent(body, contentType, requestedFormat),
311
+ );
312
+ const result = await Promise.race([extraction, timeout]);
313
+ assertFetchDeadline(options, deadlineMs, url, 'content extraction');
314
+ return result;
315
+ } finally {
316
+ if (timer !== undefined) clearTimeout(timer);
317
+ }
318
+ }
319
+
253
320
  function normalizeRequestUrl(rawUrl: string): NormalizedRequestUrl {
254
321
  let parsed: URL;
255
322
  try {
@@ -66,6 +66,11 @@ export function parsePackageInfo(payload: unknown): PackageInfo {
66
66
 
67
67
  const realFetch: FetchLike = (url, init) => fetch(url, init);
68
68
 
69
+ /** Duck-type AbortError because Node versions differ on DOMException inheritance. */
70
+ function isAbortError(error: unknown): boolean {
71
+ return typeof error === 'object' && error !== null && Reflect.get(error, 'name') === 'AbortError';
72
+ }
73
+
69
74
  /** Time-boxed, never-throwing lookup of the latest published version of a package. */
70
75
  export async function fetchLatestVersion(
71
76
  options: FetchLatestVersionOptions,
@@ -74,7 +79,11 @@ export async function fetchLatestVersion(
74
79
  const timeoutMs = options.timeoutMs ?? DEFAULT_UPDATE_TIMEOUT_MS;
75
80
  const doFetch = options.fetchImpl ?? realFetch;
76
81
  const controller = new AbortController();
82
+ let timedOut = false;
77
83
  const timer = setTimeout(() => {
84
+ // Ownership matters: a fetch implementation may reject with AbortError for
85
+ // another reason. Only the abort initiated by this timer is an expected skip.
86
+ timedOut = true;
78
87
  controller.abort();
79
88
  }, timeoutMs);
80
89
  try {
@@ -84,7 +93,10 @@ export async function fetchLatestVersion(
84
93
  const payload = await response.json();
85
94
  return parseLatestVersionPayload(payload);
86
95
  } catch (error) {
87
- if (options.onError) options.onError(error instanceof Error ? error : new Error(String(error)));
96
+ const expectedTimeoutAbort = timedOut && controller.signal.aborted && isAbortError(error);
97
+ if (options.onError && !expectedTimeoutAbort) {
98
+ options.onError(error instanceof Error ? error : new Error(String(error)));
99
+ }
88
100
  return undefined;
89
101
  } finally {
90
102
  clearTimeout(timer);