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.
- package/README.md +2 -1
- package/TESTING.md +10 -6
- package/TEST_PLAN.md +8 -8
- package/docs/INDEX.md +1 -1
- package/docs/concepts/context-projection-and-budgeting.md +4 -2
- package/docs/manifest.json +6 -7
- package/docs/operations/configuration.md +8 -8
- package/docs/operations/troubleshooting.md +5 -3
- package/docs/reference/runtime-contracts.md +47 -47
- package/docs/subsystems/delegation.md +22 -13
- package/docs/subsystems/docs-freshness-gate.md +3 -3
- package/docs/subsystems/fusion.md +6 -6
- package/docs/tools/bg_delegate.md +12 -7
- package/docs/tools/bg_result.md +2 -2
- package/docs/tools/fusion_research.md +2 -2
- package/package.json +1 -1
- package/src/core/context/token-budget.ts +16 -3
- package/src/core/delegate/artifacts.ts +14 -12
- package/src/core/delegate/budget.ts +78 -33
- package/src/core/delegate/launch.ts +4 -2
- package/src/core/delegate/result-package.ts +16 -0
- package/src/core/delegate/runner.ts +21 -2
- package/src/core/delegate/types.ts +12 -1
- package/src/core/fusion/child-protocol.ts +3 -3
- package/src/core/fusion/pi-child.ts +12 -5
- package/src/core/fusion/web-fetch.ts +72 -5
- package/src/core/update-check.ts +13 -1
- package/src/delegate-child-extension.ts +368 -69
- package/src/delegate-extension.ts +5 -2
|
@@ -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.
|
|
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 =
|
|
27
|
-
export const FUSION_CHILD_MAX_TOOL_CALLS =
|
|
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 =
|
|
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 =
|
|
80
|
-
export const FUSION_CHILD_TIMEOUT_MS =
|
|
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.
|
|
90
|
-
*
|
|
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 =
|
|
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 =
|
|
12
|
-
export const FUSION_WEB_FETCH_MAX_RESPONSE_BYTES =
|
|
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
|
|
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:
|
|
233
|
-
duration_ms:
|
|
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 {
|
package/src/core/update-check.ts
CHANGED
|
@@ -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
|
-
|
|
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);
|