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.
@@ -12,14 +12,14 @@ covers_sources: []
12
12
  This authored section defines the boundary: documentation facts are extracted from package metadata and TypeScript ASTs, then generated into docs and the manifest. Unsupported syntax fails the gate rather than falling back to regex or stale hand-maintained inventories. Public registrations must remain unconditional top-level direct calls or use the one validated local tool-wrapper shape; host/method aliases, computed access, nested or conditional registration, wrapper chaining/passing, constructor helpers, ambiguous public metadata, destructured Pi parameters, and repeated imported registrars are rejected.
13
13
 
14
14
  <!-- pi-docs:begin name="docs-freshness-gate" generator="scripts/docs/generate.mjs" -->
15
- - Canonical package version: `2.3.0`
15
+ - Canonical package version: `2.4.2`
16
16
  - Governed markdown docs: 42
17
17
  - Public surfaces extracted: 31
18
18
  - Governed production sources: 50
19
19
  - Tool contracts extracted: 11
20
- - Schema IDs extracted: 44
20
+ - Schema IDs extracted: 46
21
21
  - Environment variable references extracted: 49
22
- - Behavioral attestation receipts not passing: 7
22
+ - Behavioral attestation receipts not passing: 8
23
23
  - Receipt store: `docs/attestations.json`
24
24
 
25
25
  `npm run docs:verify` is read-only: it renders generated files twice in memory and compares them with committed bytes. `npm run docs:generate` is the only docs writer.
@@ -80,11 +80,11 @@ Evaluator, evaluator-repair, and merger always use capability `reason` and empty
80
80
 
81
81
  Research is targeted fetch, not search. The public caller declares exact non-duplicate public `http(s)` URLs and purposes. There is no browser, PDF reader, cache, search provider, page-recrawl loop, or domain allowlist.
82
82
 
83
- `fusion_web_fetch` is private to research children and has a closed `{url, extract?}` schema. It rejects credentials, non-http schemes, localhost/known-metadata names, and enumerated private/reserved address classes; vets all DNS answers against that classifier; pins the request to a vetted address; checks the response socket address; follows at most five re-vetted redirects; accepts only HTML/XHTML/plain text/Markdown; caps response bytes at 2 MiB and extracted output at 32 KiB; uses a 60 second deadline; strips script/style/noscript; and extracts text or Markdown. Source-policy admission also rejects literal Azure service address `168.63.129.16`, but the transport classifier does not currently special-case a public DNS/redirect target resolving to that address.
83
+ `fusion_web_fetch` is private to research children and has a closed `{url, extract?}` schema. It rejects credentials, non-http schemes, localhost/known-metadata names, and enumerated private/reserved address classes; vets all DNS answers against that classifier; pins the request to a vetted address; checks the response socket address; follows at most five re-vetted redirects; accepts only HTML/XHTML/plain text/Markdown; caps response bytes at 4 MiB and extracted output at 32 KiB; uses one 90 second full-operation deadline across DNS, redirects, response transfer, and extraction; strips script/style/noscript; and extracts text or Markdown. Source-policy admission also rejects literal Azure service address `168.63.129.16`, but the transport classifier does not currently special-case a public DNS/redirect target resolving to that address.
84
84
 
85
85
  Research intentionally combines read-only file tools and network fetch in one child. This supports source-backed synthesis but is security-sensitive: operators must not supply secret-bearing URLs or ask children to put private data in URL strings. The package blocks common SSRF targets and credential URLs, but its deny rules are not an exhaustive network sandbox; fetched content remains untrusted and caller-declared public URLs can still disclose access through remote logs/timing.
86
86
 
87
- Inspect/research candidates write sealed tool-call audit logs. The log contains schema version, ordinal, tool name, argument/result byte counts and SHA-256 digests, status, duration, and fetch provenance. Raw arguments, raw results, page content, and rejected raw URLs are not persisted. The parent requires the log and seal, verifies hashes/counts/ordinals/status, enforces the 8 MiB aggregate result-byte cap, and rejects non-allowlisted tools. A child may attempt at most 192 tool calls; crossing that limit aborts the run, emits structured refusal evidence, and prevents a complete audit seal.
87
+ Inspect/research candidates write sealed tool-call audit logs. The log contains schema version, ordinal, tool name, argument/result byte counts and SHA-256 digests, status, duration, and fetch provenance. Raw arguments, raw results, page content, and rejected raw URLs are not persisted. The parent requires the log and seal, verifies hashes/counts/ordinals/status, independently enforces both the 600-call and 32 MiB aggregate result-byte caps, and rejects non-allowlisted tools. A child may attempt at most 600 tool calls; crossing that limit aborts the run, emits structured refusal evidence, and prevents a complete audit seal.
88
88
 
89
89
  ## Child process isolation
90
90
 
@@ -92,7 +92,7 @@ Fusion never calls direct completion APIs. It launches direct child `pi --mode t
92
92
 
93
93
  Child text mode writes the final full answer to stdout. The private child extension emits compact reasoning-free metadata frames to stderr for finalized assistant messages: provider/model, stop reason, text block byte counts and hashes, aggregate text hash, the complete Pi `Usage` object (including Anthropic `cacheWrite1h` and provider-reported reasoning subsets), and a closed cache-policy observation. It governs every final `before_provider_request` payload after attribution and sanitization. For Anthropic routes, the child environment defaults `PI_CACHE_RETENTION` to `long` before provider serialization, so the attribution/Pi adapter creates system, final-tool, and final-conversation breakpoints with `ttl: "1h"`; inherited `PI_CACHE_RETENTION=short|none|long` remains explicit, and call-level `cacheRetention="none"` still wins for compaction. The final governor validates and normalizes those upstream-selected breakpoints, falls back to short when model compatibility rejects long retention, preserves no-marker compaction payloads, enforces Anthropic's four-breakpoint ceiling, and appends the subscription prompt-caching-scope beta idempotently. Its `effective_retention` field describes the final payload, not provider acceptance. Provider usage is preserved verbatim: `cacheWrite1h > 0` proves a one-hour write, but zero is inconclusive on subscription OAuth. Live normal-spawn and exact Fusion-child controls each observed a unique cache read after 370 idle seconds despite `cacheWrite1h = 0`; therefore payload observations prove request intent and `cacheRead` proves reuse, while neither zero telemetry nor a six-minute hit alone proves the full one-hour lifetime. Malformed controls or policy values abort before transport. Non-Anthropic payloads and child environments remain unchanged apart from the governor's existing JSON normalization.
94
94
 
95
- After cache normalization, the governor validates a stable JSON-object serialization and enforces the 128-provider-request execution limit. It does not estimate live payload tokens, subtract possible model output from the context window, or reject a request by payload size; Pi and the provider own live context handling after the pre-spawn Fusion stage checks. A provider-side context rejection remains a loud child failure and is never route-substituted or hidden. Pi's provider-hook behavior is characterized through the same `openai-codex-responses` transport adapter used by subscription Codex routes in a real local HTTP agent loop: transforms chain in extension load order and `ctx.abort()` prevents transport for the retained cache-policy and execution-limit refusals. Cache observations use `pi-background-tasks.fusion-claude-cache-observation.v1`, state requested/effective retention, source, breakpoint count, and provider-request ordinal, and are hash-bound inside `pi-background-tasks.fusion-child-result.v4` attempt event artifacts. At terminal `agent_settled`, the extension emits exactly one `pi-background-tasks.fusion-child-settlement.v3` frame binding the complete ordered metadata stream by count and SHA-256, the final record/hash, recovered retry-marker ordinals, and any one recovered oversized-original ordinal. The parent validates closed cache/output-contract evidence and increasing request ordinals, reconstructs stdout against the final metadata, requires final stop reason `stop`, verifies model identity, and preserves usage/cost exactly. Non-final `toolUse` records remain normal. A non-final `error` is accepted only when it is a zero-content, empty-hash, zero-usage retry marker, a later final `stop` exists, and the terminal settlement hash/accounts for that exact ordinal. Exactly one non-final `stop` is accepted only as a hash-bound oversized candidate original immediately followed by its same-session replacement. `length`, `aborted`, `pending`, final `error`, error records carrying text or usage, unbound non-final `stop`, missing/duplicate/tampered settlement, and settlement before terminal idleness all fail loudly.
95
+ After cache normalization, the governor validates a stable JSON-object serialization and enforces the 550-provider-request execution limit. It does not estimate live payload tokens, subtract possible model output from the context window, or reject a request by payload size; Pi and the provider own live context handling after the pre-spawn Fusion stage checks. A provider-side context rejection remains a loud child failure and is never route-substituted or hidden. Pi's provider-hook behavior is characterized through the same `openai-codex-responses` transport adapter used by subscription Codex routes in a real local HTTP agent loop: transforms chain in extension load order and `ctx.abort()` prevents transport for the retained cache-policy and execution-limit refusals. Cache observations use `pi-background-tasks.fusion-claude-cache-observation.v1`, state requested/effective retention, source, breakpoint count, and provider-request ordinal, and are hash-bound inside `pi-background-tasks.fusion-child-result.v4` attempt event artifacts. At terminal `agent_settled`, the extension emits exactly one `pi-background-tasks.fusion-child-settlement.v3` frame binding the complete ordered metadata stream by count and SHA-256, the final record/hash, recovered retry-marker ordinals, and any one recovered oversized-original ordinal. The parent validates closed cache/output-contract evidence and increasing request ordinals, reconstructs stdout against the final metadata, requires final stop reason `stop`, verifies model identity, and preserves usage/cost exactly. Non-final `toolUse` records remain normal. A non-final `error` is accepted only when it is a zero-content, empty-hash, zero-usage retry marker, a later final `stop` exists, and the terminal settlement hash/accounts for that exact ordinal. Exactly one non-final `stop` is accepted only as a hash-bound oversized candidate original immediately followed by its same-session replacement. `length`, `aborted`, `pending`, final `error`, error records carrying text or usage, unbound non-final `stop`, missing/duplicate/tampered settlement, and settlement before terminal idleness all fail loudly.
96
96
 
97
97
  Fusion child environments strip session/model/provider variables plus metered credential/base-url variables for OpenRouter, OpenAI, Anthropic, Azure OpenAI, and generic Pi API credentials before launch. Frontier model routes are admitted only when the registry reports subscription OAuth for trusted `anthropic` or `openai-codex` endpoints. There is no fallback, model substitution, endpoint override, or metered API-key route.
98
98
 
@@ -102,7 +102,7 @@ Budget planning is per route and per stage. Every configured candidate, evaluato
102
102
 
103
103
  `budget-plan.json` uses `pi-background-tasks.fusion-budget-plan.v4` and records route capacities, stage forecasts for candidate/evaluation/evaluation-repair/merge, conditional repair reservation, warnings, blockers, empty-request counterfactuals, and remediation. Each route reserves the larger of Fusion's 32,768-token output contract reserve and the resolved model's declared maximum output; a model advertising a 128,000-token maximum therefore receives the full 128,000-token reserve. Fatal preflight blockers launch zero children. High utilization or worst-case reservation pressure is a warning when input still fits. Exact rendered prompt checks happen again immediately before candidate, evaluation, repair, and merge launches.
104
104
 
105
- Every candidate system prompt discloses the exact 49,152 JSON-rendered UTF-8 byte hard maximum and requires explicit limitations when the requested scope cannot fit. Output contracts are checked after durable attempt recording: candidate responses up to 48 KiB JSON-rendered bytes, evaluator up to 64 KiB, merger/final report up to 64 KiB, diagnostics contract 8 KiB, child stdout cap 32 MiB, child stderr cap 4 MiB.
105
+ Every candidate system prompt discloses the exact 49,152 JSON-rendered UTF-8 byte hard maximum and requires explicit limitations when the requested scope cannot fit. Output contracts are checked after durable attempt recording: candidate responses up to 48 KiB JSON-rendered bytes, evaluator up to 64 KiB, merger/final report up to 64 KiB, diagnostics contract 8 KiB, child stdout cap 32 MiB, child stderr cap 16 MiB.
106
106
 
107
107
  When a candidate's first complete `stop` response exceeds 48 KiB, the private child extension durably preserves that full original, removes all active tools, and queues one `followUp` user message before `agent_settled`. The same live Pi process, route, model, and in-memory conversation may only compress/restructure its immediately previous answer; it may not investigate again. Pi text mode then emits only the replacement to stdout. A conforming replacement proceeds normally. A second oversized response hard-fails as `child_output_cap`, preserves both the original artifact and replacement partial response, and never queues another continuation. Cancellation during compression preserves any hash-verified original already written. Fusion never clips, silently forwards, or repairs output in a new child session.
108
108
 
@@ -120,7 +120,7 @@ For tool-enabled children, the private audit journal remains open across every l
120
120
 
121
121
  The four public Fusion tools return a background launch receipt after the readiness barrier. Tool-launched runs default to terminal notification plus follow-up wake; `/fusion` uses notification-only. The first successful `bg_result` retrieval durably claims and attaches complete Fusion usage exactly once; repeated retrieval returns the answer without duplicating session accounting. Running retrieval never waits.
122
122
 
123
- Cancellation and shutdown are loud and durable when a run store exists. The extension tracks active runs, managed tasks own their abort controllers, `bg_kill` and session shutdown abort them, and terminal task publication waits for workflow settlement. Child processes have a 30 minute wall timeout, 20 minute idle watchdog, SIGTERM grace, SIGKILL wait, process-group kill on POSIX, bounded stdout/stderr, and cleanup-error propagation.
123
+ Cancellation and shutdown are loud and durable when a run store exists. The extension tracks active runs, managed tasks own their abort controllers, `bg_kill` and session shutdown abort them, and terminal task publication waits for workflow settlement. Child processes have a 50 minute wall timeout, 35 minute idle watchdog, SIGTERM grace, SIGKILL wait, process-group kill on POSIX, bounded stdout/stderr, and cleanup-error propagation.
124
124
 
125
125
  ## Troubleshooting
126
126
 
@@ -129,7 +129,7 @@ Cancellation and shutdown are loud and durable when a run store exists. The exte
129
129
  - Frontier/API route rejected: use Pi Anthropic or Codex subscription OAuth, not OpenAI/OpenRouter/Azure/API-key routes.
130
130
  - `prompt_budget_exceeded_forecast`: inspect `budget-plan.json`; the error says whether shortening the request can help or whether session history/scope/model context window is the blocker.
131
131
  - `prompt_budget_exceeded_measured`: an exact rendered prompt exceeded capacity after upstream output was known; split the workflow or choose a larger-context subscription route.
132
- - `child_runtime_limit_exceeded`: a child crossed the 128-provider-request or 192-tool-call execution limit. Inspect the failed tool seal and split an unbounded task rather than raising limits blindly.
132
+ - `child_runtime_limit_exceeded`: a child crossed the 550-provider-request or 600-tool-call execution limit. Inspect the failed tool seal and split an unbounded task rather than raising limits blindly.
133
133
  - `child_runtime_payload_invalid`: the final provider payload could not be normalized as one stable JSON object. Fix the payload/provider integration; no fallback serialization is attempted.
134
134
  - `child_cache_policy_invalid`: `PI_CACHE_RETENTION` or Claude cache-control evidence was malformed. Use exactly `none`, `short`, or `long`; do not bypass cache-policy validation.
135
135
  - `child_output_cap` after candidate compression: inspect the final candidate response, its `response.oversized.*` original, and v4/v3 child protocol evidence. The single same-session no-tool compression attempt also exceeded 49,152 JSON-rendered bytes; split the task rather than truncating or starting a repair child.
@@ -186,19 +186,24 @@ Budgets and limits:
186
186
  - route capacity is the declared context window minus reserves: `16,384` output, `8,192` framing, `4,096` safety tokens;
187
187
  - minimum usable input is `8,192` tokens;
188
188
  - launch admission measures the child system prompt plus the exact child prompt bytes that carry the seed;
189
- - runtime context is measured before each model call;
190
- - per-tool-result transcript cap: `64 KiB`;
191
- - aggregate tool-output cap: `64 MiB`;
192
- - answer capture cap value in the seed: `4 MiB`; current child code carries this limit but does not separately enforce it before packaging;
189
+ - backed large prompts use route-family calibration; the plan also records a provable `1.00 B/token` counter-forecast for every byte class, including multibyte input;
190
+ - `32,768` input tokens are protected for finalization, with an `8,192`-token low-runway trigger;
191
+ - runtime context estimates are advisory and never masquerade as provider context truth;
192
+ - tool results spill above `64 KiB` or earlier when retaining them would consume protected runway;
193
+ - artifact range reads are bounded by remaining inline runway;
194
+ - aggregate raw tool-output cap: `64 MiB`;
195
+ - answer capture cap: `4 MiB`, enforced without committing a prefix;
193
196
  - timeout defaults to `1200s`.
194
197
 
195
- Artifacts are under `.pi/delegate/<session-id>-<pid>/<task-id>/` and include `seed.json`, `child-prompt.txt`, `context-omission-ledger.json`, `budget-plan.json`, `manifest.json`, `child-session/`, `spill/`, and later `result.json` / `outcome.json` when produced. Child stdout/stderr are captured through the background task output path; the delegate artifact constants include child stream filenames, but current launch/finalize code does not mirror streams into those delegate files.
198
+ When protected runway becomes low, the child disables tools and is instructed to answer immediately from evidence already gathered. Pi and the provider—not the package estimator—own final live context admission. A genuine provider context rejection remains a loud failure and is never retried on another route.
199
+
200
+ Artifacts are under `.pi/delegate/<session-id>-<pid>/<task-id>/` and include `seed.json`, `child-prompt.txt`, `context-omission-ledger.json`, `budget-plan.json`, `manifest.json`, `child-session/`, `spill/`, `runtime-budget.json`, and later `result.json` / `outcome.json` when produced. Child stdout/stderr are captured through the background task output path; terminal failures report that real merged output path when it exists and do not claim absent delegate-local stream files.
196
201
 
197
202
  ## Spilled tool output
198
203
 
199
- Oversized child tool results are written in full to `spill/...` artifacts and replaced in the transcript by receipts carrying path, byte length, SHA-256, tool name, call id, turn sequence, and source call index. The raw oversized payload is not forwarded as a fallback and is not truncated.
204
+ Oversized child tool results are written in full to `spill/...` artifacts and replaced in the transcript by receipts carrying path, byte length, SHA-256, content format, tool name, call id, turn sequence, and source call index. Single text blocks retain their exact UTF-8 bytes; malformed lone-surrogate text fails loudly instead of being substituted. Multi-block and image-bearing results use a structured JSON envelope preserving text, block boundaries, MIME type, and complete base64 image data. The raw oversized payload is not forwarded as a fallback and is not truncated.
200
205
 
201
- Inside the child, `delegate_read_artifact({artifact, offset, length})` reads an exact byte range. It refuses path escape, negative/non-integer offsets, non-positive lengths, and reads past EOF rather than returning a short/clamped range.
206
+ Inside the child, `delegate_read_artifact({artifact, offset, length})` reads an exact byte range and returns those bytes as base64. It refuses path escape, negative/non-integer offsets, non-positive lengths, and reads past EOF rather than returning a short/clamped range. Base64 prevents a range that splits a UTF-8 sequence from being silently changed to replacement characters.
202
207
 
203
208
  ## Completion
204
209
 
@@ -11,7 +11,7 @@ covers_sources: []
11
11
 
12
12
  <!-- pi-docs:begin name="tool-contract-bg_result" generator="scripts/docs/generate.mjs" -->
13
13
  - Label: **Background Result**
14
- - Source: `src/delegate-extension.ts:515`
14
+ - Source: `src/delegate-extension.ts:516`
15
15
  - Description: Retrieve a hash-verified result from a bg_delegate or background Fusion task. Never blocks: a running task returns a typed not-ready result. Oversized answers are never truncated.
16
16
  - Root schema: `object`; additionalProperties: `false`
17
17
 
@@ -124,7 +124,7 @@ Common delegate retrieval outcomes:
124
124
  - `result_too_large_for_inline` — explicit inline request exceeded the inline cap.
125
125
  - Fusion `summary_status:"integrity_failed"` — a terminal summary or its manifest binding failed verification; no summary metadata is trusted.
126
126
 
127
- Delegate errors include whether a child process was created, preserved artifact hints when known, and remediation text. Usage missing from the provider is reported as `unavailable`, not synthesized as zero. Fusion retrieval additionally fails on non-completed manifests, identity/schema drift, malformed usage/details, invalid UTF-8, or any manifest/result/merged hash or byte-length mismatch; failed/cancelled runs return their preserved terminal error rather than partial output.
127
+ Delegate errors include whether a child process was created, preserved artifact hints that are checked for existence, the real merged task output path when available, and remediation text. Usage missing from the provider is reported as `unavailable`, not synthesized as zero. Fusion retrieval additionally fails on non-completed manifests, identity/schema drift, malformed usage/details, invalid UTF-8, or any manifest/result/merged hash or byte-length mismatch; failed/cancelled runs return their preserved terminal error rather than partial output.
128
128
 
129
129
  ## Parent outcome separation
130
130
 
@@ -147,8 +147,8 @@ The private `fusion_web_fetch` schema is closed: `{url, extract?: 'text'|'markdo
147
147
  - vets every DNS answer against the transport classifier, pins the request to a vetted address, and verifies the response socket address;
148
148
  - follows at most five redirects, revalidating each hop;
149
149
  - accepts only HTML/XHTML, plain text, and Markdown content;
150
- - caps response bytes at 2 MiB and extracted output at 32 KiB;
151
- - uses a 60 second deadline;
150
+ - caps response bytes at 4 MiB and extracted output at 32 KiB;
151
+ - uses one 90 second deadline across DNS, redirects, response transfer, and extraction;
152
152
  - strips script/style/noscript blocks and extracts text or Markdown with table preservation.
153
153
 
154
154
  Failures use typed error codes such as `invalid_url`, `unsupported_scheme`, `blocked_address`, `dns_failure`, `redirect_limit`, `redirect_blocked`, `response_too_large`, `unsupported_content_type`, `request_timeout`, `network_error`, `extraction_failed`, and `http_error`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-background-tasks",
3
- "version": "2.3.0",
3
+ "version": "2.4.2",
4
4
  "description": "Pi extension for durable background shell tasks, read-only delegated agents, local attested Pi runs, and fixed-purpose Fusion workflows through child Pi processes.",
5
5
  "type": "module",
6
6
  "license": "ISC",
@@ -45,7 +45,11 @@ export const TOKEN_BUDGET_SEGMENT_KINDS = [
45
45
  ] as const;
46
46
  export type TokenBudgetSegmentKind = (typeof TOKEN_BUDGET_SEGMENT_KINDS)[number];
47
47
 
48
- export type TokenBudgetEstimatorScope = 'fusion' | 'delegate' | 'conservative';
48
+ export type TokenBudgetEstimatorScope =
49
+ | 'fusion'
50
+ | 'delegate_launch'
51
+ | 'delegate'
52
+ | 'conservative';
49
53
  export type TokenBudgetDominantByteClass =
50
54
  | 'normal'
51
55
  | 'dense_ascii'
@@ -526,7 +530,7 @@ function rateSourceWarning(input: {
526
530
  return `model is not in the exact calibration backing set for family ${input.family}; using the provable 1.00 B/tok floor`;
527
531
  }
528
532
  if (input.source === 'delegate_conservative') {
529
- return 'delegate scope withholds calibrated rates and uses the provable 1.00 B/tok profile for launch/runtime headroom';
533
+ return 'delegate launch/runtime uses the provable 1.00 B/tok profile when the prompt or route is below the backed large-prompt calibration domain';
530
534
  }
531
535
  if (input.source === 'explicit_conservative') {
532
536
  return 'explicit conservative scope uses the provable 1.00 B/tok profile';
@@ -569,7 +573,16 @@ function effectiveRateSource(input: {
569
573
  ? 'unbacked_model_floor'
570
574
  : 'unknown_provider_floor';
571
575
  effective = TOKEN_BUDGET_PROVABLE_RATE_X100;
572
- } else if (input.scope === 'delegate') {
576
+ } else if (
577
+ input.scope === 'delegate' ||
578
+ (input.scope === 'delegate_launch' &&
579
+ (input.profile.concrete_known_bytes < TOKEN_BUDGET_LARGE_PROMPT_MIN_BYTES ||
580
+ (input.allowedInputTokens !== undefined &&
581
+ Math.floor(
582
+ (input.allowedInputTokens * TOKEN_BUDGET_DELEGATE_CONSERVATIVE_RATE_X100) /
583
+ TOKEN_BUDGET_RATE_SCALE,
584
+ ) < TOKEN_BUDGET_LARGE_PROMPT_MIN_BYTES)))
585
+ ) {
573
586
  source = 'delegate_conservative';
574
587
  effective = Math.min(configured, TOKEN_BUDGET_DELEGATE_CONSERVATIVE_RATE_X100);
575
588
  } else if (input.scope === 'conservative') {
@@ -1,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
2
3
  import { chmod, mkdir, open, readFile, rename, rm } from 'node:fs/promises';
3
4
  import { basename, isAbsolute, join, relative, sep } from 'node:path';
4
5
  import { canonicalJson } from '../attested-pi-run.js';
@@ -45,8 +46,7 @@ export const DELEGATE_ARTIFACT_NAMES = {
45
46
  outcome: 'outcome.json',
46
47
  result: DELEGATE_RESULT_PACKAGE_FILENAME,
47
48
  childPrompt: 'child-prompt.txt',
48
- childStderr: 'child.stderr.txt',
49
- childStdout: 'child.stdout.txt',
49
+ runtimeBudget: 'runtime-budget.json',
50
50
  error: 'error.json',
51
51
  } as const;
52
52
 
@@ -232,11 +232,6 @@ export class DelegateArtifactStore {
232
232
  return this.write(DELEGATE_ARTIFACT_NAMES.budgetPlan, `${canonicalJson(plan)}\n`);
233
233
  }
234
234
 
235
- async writeChildStreams(stdout: Buffer, stderr: Buffer): Promise<void> {
236
- await this.write(DELEGATE_ARTIFACT_NAMES.childStdout, stdout);
237
- await this.write(DELEGATE_ARTIFACT_NAMES.childStderr, stderr);
238
- }
239
-
240
235
  /** Commit the run. The rename performed here is the single success point. */
241
236
  async commitResult(pkg: DelegateResultPackageV1): Promise<DelegateArtifactRef> {
242
237
  const ref = await this.write(
@@ -251,17 +246,23 @@ export class DelegateArtifactStore {
251
246
  try {
252
247
  return await readFile(this.resultPathAbs, 'utf8');
253
248
  } catch (error) {
249
+ const diagnosticNames = [
250
+ DELEGATE_ARTIFACT_NAMES.error,
251
+ DELEGATE_ARTIFACT_NAMES.outcome,
252
+ DELEGATE_ARTIFACT_NAMES.runtimeBudget,
253
+ DELEGATE_ARTIFACT_NAMES.manifest,
254
+ ].filter((name) => existsSync(join(this.rootAbs, name)));
254
255
  throw new DelegateError(
255
- `delegate result package is not present at ${join(this.rootDisplay, DELEGATE_ARTIFACT_NAMES.result)}; the child produced no committed answer`,
256
+ `delegate result package could not be read at ${join(this.rootDisplay, DELEGATE_ARTIFACT_NAMES.result)}; no committed answer is available (${error instanceof Error ? error.message : String(error)})`,
256
257
  {
257
258
  code: 'result_unavailable',
258
259
  childCreated: true,
259
260
  taskId: this.manifest.task_id,
260
261
  artifactDir: this.rootDisplay,
261
- preserved: [this.rootDisplay],
262
- remediation: [
263
- 'Inspect child.stderr.txt and error.json in the artifact directory for the recorded failure.',
264
- ],
262
+ preserved: diagnosticNames,
263
+ remediation: diagnosticNames.length === 0
264
+ ? ['No diagnostic control artifact exists; inspect the background task merged output if one was created.']
265
+ : [`Inspect the existing delegate control artifacts: ${diagnosticNames.join(', ')}.`],
265
266
  },
266
267
  );
267
268
  }
@@ -353,6 +354,7 @@ export class DelegateArtifactStore {
353
354
  source_call_index: input.sourceCallIndex,
354
355
  byte_length: input.payload.length,
355
356
  sha256: sha256Bytes(input.payload),
357
+ content_format: 'opaque_bytes',
356
358
  };
357
359
  }
358
360
 
@@ -4,7 +4,6 @@ import {
4
4
  TOKEN_BUDGET_RATE_SCALE,
5
5
  estimateInputTokens,
6
6
  knownTextSegment,
7
- maxKnownTextBytesForTokens,
8
7
  resolveTokenBudgetFamily,
9
8
  utf8ByteClassBreakdown,
10
9
  allowedInputTokens,
@@ -28,11 +27,15 @@ import {
28
27
  * A delegate child is a multi-turn, tool-using agent, so its budget has two
29
28
  * distinct phases rather than Fusion's single-shot stage forecast:
30
29
  *
31
- * 1. Launch admission checks the frozen seed, framing, and child system prompt.
32
- * 2. The runtime governor checks the complete retained input before each call.
30
+ * 1. Launch admission checks the frozen seed, framing, and child system prompt
31
+ * with the same backed family calibration used by Fusion for large prompts.
32
+ * 2. A separate provable 1 B/token forecast sizes the transcript-growth runway
33
+ * used for explicit tool-result spilling.
34
+ * 3. Runtime measurements are advisory. Package-owned growth is controlled
35
+ * before transcript entry; Pi and the provider own live context handling.
33
36
  *
34
- * Nothing here clamps, downgrades, or silently reduces. An input that does not
35
- * fit is a typed refusal.
37
+ * Nothing here clips, substitutes, or silently reduces content. Tool bytes that
38
+ * do not fit the retained-growth runway are preserved as hashed spill artifacts.
36
39
  */
37
40
 
38
41
  /** Output tokens reserved so the child can always finish an answer. */
@@ -54,10 +57,14 @@ export const DELEGATE_DEFAULT_TIMEOUT_SECONDS = 1200;
54
57
  export const DELEGATE_MAX_TOOL_RESULT_BYTES = 64 * 1024;
55
58
  export const DELEGATE_MAX_TOTAL_TOOL_OUTPUT_BYTES = 64 * 1024 * 1024;
56
59
  export const DELEGATE_MAX_ANSWER_BYTES = 4 * 1024 * 1024;
60
+ /** Input runway held back for a final no-tool answer after investigation. */
61
+ export const DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS = 32 * 1024;
62
+ /** Remaining retained-growth runway at which the child disables tools. */
63
+ export const DELEGATE_FINALIZATION_TRIGGER_TOKENS = 8 * 1024;
57
64
  /** Answers at or under this serialize inline; larger ones degrade explicitly. */
58
65
  export const DELEGATE_INLINE_ANSWER_BYTES = 48 * 1024;
59
66
 
60
- export const DELEGATE_BUDGET_POLICY_ID = 'delegate-budget-policy-v2';
67
+ export const DELEGATE_BUDGET_POLICY_ID = 'delegate-budget-policy-v3';
61
68
 
62
69
  export interface DelegateBudgetPolicyDescriptor {
63
70
  id: typeof DELEGATE_BUDGET_POLICY_ID;
@@ -68,7 +75,11 @@ export interface DelegateBudgetPolicyDescriptor {
68
75
  safety_reserve_tokens: number;
69
76
  min_usable_input_tokens: number;
70
77
  inline_answer_bytes: number;
71
- estimator_scope: 'delegate';
78
+ finalization_input_reserve_tokens: number;
79
+ finalization_trigger_tokens: number;
80
+ launch_estimator_scope: 'calibrated_large_prompt';
81
+ retained_growth_estimator_scope: 'provable_1_byte_per_token';
82
+ live_provider_context_owner: 'pi_and_provider';
72
83
  }
73
84
 
74
85
  export const DELEGATE_BUDGET_POLICY: DelegateBudgetPolicyDescriptor = {
@@ -80,7 +91,11 @@ export const DELEGATE_BUDGET_POLICY: DelegateBudgetPolicyDescriptor = {
80
91
  safety_reserve_tokens: DELEGATE_SAFETY_RESERVE_TOKENS,
81
92
  min_usable_input_tokens: DELEGATE_MIN_USABLE_INPUT_TOKENS,
82
93
  inline_answer_bytes: DELEGATE_INLINE_ANSWER_BYTES,
83
- estimator_scope: 'delegate',
94
+ finalization_input_reserve_tokens: DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS,
95
+ finalization_trigger_tokens: DELEGATE_FINALIZATION_TRIGGER_TOKENS,
96
+ launch_estimator_scope: 'calibrated_large_prompt',
97
+ retained_growth_estimator_scope: 'provable_1_byte_per_token',
98
+ live_provider_context_owner: 'pi_and_provider',
84
99
  };
85
100
 
86
101
  export interface DelegateAdmissionPlanV1 {
@@ -97,17 +112,22 @@ export interface DelegateAdmissionPlanV1 {
97
112
  rate_source: TokenBudgetRateSource;
98
113
  byte_capacity_utf8_bytes: number;
99
114
  };
100
- seed_utf8_bytes: number;
101
- seed_multibyte_utf8_bytes: number;
115
+ child_prompt_utf8_bytes: number;
116
+ child_prompt_multibyte_utf8_bytes: number;
102
117
  system_prompt_utf8_bytes: number;
103
118
  system_prompt_multibyte_utf8_bytes: number;
104
119
  launch_utf8_bytes: number;
105
120
  launch_input_tokens_upper_bound: number;
121
+ conservative_launch_input_tokens: number;
122
+ conservative_launch_fits: boolean;
106
123
  signed_headroom_tokens: number;
107
124
  utilization_basis_points: number;
125
+ retained_growth_budget_tokens: number;
126
+ finalization_input_reserve_tokens: number;
108
127
  byte_class_breakdown: TokenBudgetByteClassBreakdown;
109
128
  dominant_byte_class: EstimateInputTokensResult['rateSource']['dominant_byte_class'];
110
129
  estimate: EstimateInputTokensResult;
130
+ conservative_estimate: EstimateInputTokensResult;
111
131
  fits: boolean;
112
132
  limits: DelegateLimits;
113
133
  }
@@ -167,7 +187,7 @@ export function delegateAllowedInputTokens(route: DelegatePinnedRoute): number {
167
187
 
168
188
  export interface DelegateAdmissionInput {
169
189
  route: DelegatePinnedRoute;
170
- seedSerialized: string;
190
+ childPrompt: string;
171
191
  childSystemPrompt: string;
172
192
  limits: DelegateLimits;
173
193
  }
@@ -176,20 +196,39 @@ export interface DelegateAdmissionInput {
176
196
  export function planDelegateAdmission(input: DelegateAdmissionInput): DelegateAdmissionPlanV1 {
177
197
  const allowed = delegateAllowedInputTokens(input.route);
178
198
  const family = routeFamily(input.route);
179
- const seed = utf8ByteClassBreakdown(input.seedSerialized);
199
+ const childPrompt = utf8ByteClassBreakdown(input.childPrompt);
180
200
  const system = utf8ByteClassBreakdown(input.childSystemPrompt);
201
+ const segments = [knownTextSegment(input.childPrompt), knownTextSegment(input.childSystemPrompt)];
181
202
  const estimate = estimateInputTokens({
182
203
  family: family.family,
183
204
  calibrationBacked: family.backed,
184
205
  familyResolution: family.resolution,
185
206
  allowedInputTokens: allowed,
186
- scope: 'delegate',
187
- segments: [knownTextSegment(input.seedSerialized), knownTextSegment(input.childSystemPrompt)],
207
+ scope: 'delegate_launch',
208
+ segments,
188
209
  });
210
+ const conservativeEstimate = estimateInputTokens({
211
+ family: family.family,
212
+ calibrationBacked: family.backed,
213
+ familyResolution: family.resolution,
214
+ allowedInputTokens: allowed,
215
+ scope: 'conservative',
216
+ segments,
217
+ });
218
+ // `conservativeEstimate.tokens` intentionally uses the shared estimator's
219
+ // calibrated multibyte diagnostic rate. The counter-forecast published as
220
+ // "provable" must instead use that estimator's explicit 1 B/token ceiling
221
+ // for multibyte bytes as well as normal/dense bytes.
222
+ const provableConservativeLaunchTokens =
223
+ conservativeEstimate.advisory.input_tokens_if_multibyte_used_provable_ceiling;
189
224
  const byteCapacity = Math.floor(
190
225
  (allowed * estimate.rateSource.effective_rate_bytes_per_token_x100) / TOKEN_BUDGET_RATE_SCALE,
191
226
  );
192
- const launchBytes = seed.bytes + system.bytes;
227
+ const launchBytes = childPrompt.bytes + system.bytes;
228
+ const retainedGrowthBudget = Math.max(
229
+ 0,
230
+ allowed - estimate.tokens - DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS,
231
+ );
193
232
  return {
194
233
  schema_version: DELEGATE_BUDGET_PLAN_SCHEMA_VERSION,
195
234
  policy: DELEGATE_BUDGET_POLICY,
@@ -204,17 +243,22 @@ export function planDelegateAdmission(input: DelegateAdmissionInput): DelegateAd
204
243
  rate_source: estimate.rateSource,
205
244
  byte_capacity_utf8_bytes: byteCapacity,
206
245
  },
207
- seed_utf8_bytes: seed.bytes,
208
- seed_multibyte_utf8_bytes: seed.multibyteBytes,
246
+ child_prompt_utf8_bytes: childPrompt.bytes,
247
+ child_prompt_multibyte_utf8_bytes: childPrompt.multibyteBytes,
209
248
  system_prompt_utf8_bytes: system.bytes,
210
249
  system_prompt_multibyte_utf8_bytes: system.multibyteBytes,
211
250
  launch_utf8_bytes: launchBytes,
212
251
  launch_input_tokens_upper_bound: estimate.tokens,
252
+ conservative_launch_input_tokens: provableConservativeLaunchTokens,
253
+ conservative_launch_fits: provableConservativeLaunchTokens <= allowed,
213
254
  signed_headroom_tokens: allowed - estimate.tokens,
214
255
  utilization_basis_points: utilizationBasisPoints(estimate.tokens, allowed),
256
+ retained_growth_budget_tokens: retainedGrowthBudget,
257
+ finalization_input_reserve_tokens: DELEGATE_FINALIZATION_INPUT_RESERVE_TOKENS,
215
258
  byte_class_breakdown: estimate.byte_class_breakdown,
216
259
  dominant_byte_class: estimate.rateSource.dominant_byte_class,
217
260
  estimate,
261
+ conservative_estimate: conservativeEstimate,
218
262
  fits: estimate.tokens <= allowed,
219
263
  limits: input.limits,
220
264
  };
@@ -226,17 +270,15 @@ function rateWarningText(rateSource: TokenBudgetRateSource, qualifiedId: string)
226
270
  }
227
271
 
228
272
  function requiredByteReduction(plan: DelegateAdmissionPlanV1): number {
229
- return Math.max(
230
- 0,
231
- plan.launch_utf8_bytes -
232
- maxKnownTextBytesForTokens({
233
- family: plan.route.family,
234
- calibrationBacked: plan.route.rate_source.backed,
235
- familyResolution: plan.route.rate_source.model_resolution,
236
- allowedInputTokens: plan.route.allowed_input_tokens,
237
- scope: 'delegate',
238
- }),
239
- );
273
+ const variableTokens =
274
+ plan.route.allowed_input_tokens - plan.route.rate_source.affine_f_tokens;
275
+ const maximumBytes = variableTokens <= 0
276
+ ? 0
277
+ : Math.floor(
278
+ (variableTokens * plan.route.rate_source.effective_rate_bytes_per_token_x100) /
279
+ TOKEN_BUDGET_RATE_SCALE,
280
+ );
281
+ return Math.max(0, plan.launch_utf8_bytes - maximumBytes);
240
282
  }
241
283
 
242
284
  /**
@@ -249,7 +291,7 @@ export function assertDelegateAdmission(plan: DelegateAdmissionPlanV1): void {
249
291
  if (plan.fits) return;
250
292
  const overage = plan.launch_input_tokens_upper_bound - plan.route.allowed_input_tokens;
251
293
  throw new DelegateError(
252
- `bg_delegate seed does not fit the pinned route before launch. Route ${plan.route.qualified_id} allows ${String(plan.route.allowed_input_tokens)} input tokens; the frozen seed plus the child system prompt measure ${String(plan.launch_utf8_bytes)} UTF-8 bytes (<= ${String(plan.launch_input_tokens_upper_bound)} input tokens), over by ${String(overage)} tokens. Estimator family ${plan.route.family}, source ${plan.route.rate_source.source}, backed=${String(plan.route.rate_source.backed)}, dominant_byte_class=${plan.dominant_byte_class}, rate ${String(plan.route.rate_source.effective_rate_bytes_per_token_x100)}/100 B/tok + ${String(plan.route.rate_source.affine_f_tokens)} tokens.${rateWarningText(plan.route.rate_source, plan.route.qualified_id)} Required reduction is at least ${String(requiredByteReduction(plan))} UTF-8 bytes. No child process, child session, or artifact was created. Nothing was clipped, dropped, or substituted.`,
294
+ `bg_delegate child prompt does not fit the pinned route before launch. Route ${plan.route.qualified_id} allows ${String(plan.route.allowed_input_tokens)} input tokens; the exact child prompt plus child system prompt measure ${String(plan.launch_utf8_bytes)} UTF-8 bytes (<= ${String(plan.launch_input_tokens_upper_bound)} input tokens), over by ${String(overage)} tokens. Estimator family ${plan.route.family}, source ${plan.route.rate_source.source}, backed=${String(plan.route.rate_source.backed)}, dominant_byte_class=${plan.dominant_byte_class}, rate ${String(plan.route.rate_source.effective_rate_bytes_per_token_x100)}/100 B/tok + ${String(plan.route.rate_source.affine_f_tokens)} tokens.${rateWarningText(plan.route.rate_source, plan.route.qualified_id)} Required reduction is at least ${String(requiredByteReduction(plan))} UTF-8 bytes. No child process, child session, or artifact was created. Nothing was clipped, dropped, or substituted.`,
253
295
  {
254
296
  code: 'seed_budget_exceeded',
255
297
  childCreated: false,
@@ -291,10 +333,13 @@ export interface DelegateGovernorVerdict {
291
333
  }
292
334
 
293
335
  /**
294
- * Runtime governor decision for one prospective model call.
336
+ * Advisory runtime measurement for one prospective model call.
295
337
  *
296
- * Pure and total, so the child guard can call it from inside a hook with no
297
- * possibility of throwing where a throw would be swallowed.
338
+ * This deliberately uses the calibrated large-prompt policy and never decides
339
+ * whether transport may occur. Fusion's BUG-185 proved that a package-local
340
+ * estimator must not reject a live provider payload by subtracting hypothetical
341
+ * output. The delegate child uses this result for evidence and graceful
342
+ * finalization while proactive spilling controls package-owned growth.
298
343
  */
299
344
  export function evaluateDelegateRuntimeBudget(
300
345
  measurement: DelegateRuntimeMeasurement,
@@ -307,7 +352,7 @@ export function evaluateDelegateRuntimeBudget(
307
352
  calibrationBacked: family.backed,
308
353
  familyResolution: family.resolution,
309
354
  allowedInputTokens: allowedTokens,
310
- scope: 'delegate',
355
+ scope: 'delegate_launch',
311
356
  segments: [
312
357
  {
313
358
  kind: 'known_text',
@@ -380,7 +380,9 @@ export function buildDelegateChildSystemPrompt(seedPathHint: string): string {
380
380
  '',
381
381
  'You are inspect-only. You can read, search, and list files. You cannot run shell commands, edit or write files, reach the network, or start further delegates. Do not claim to have done so.',
382
382
  '',
383
- 'If a tool result is replaced by a spill receipt, the complete bytes are on disk and nothing was truncated. Use delegate_read_artifact with an exact offset and length when you genuinely need them.',
383
+ 'If a tool result is replaced by a spill receipt, the complete encoded content is on disk and nothing was truncated. Use delegate_read_artifact with an exact offset and length when you genuinely need lossless base64 bytes, then interpret them using the receipt content_format.',
384
+ '',
385
+ 'The child controls retained context by spilling tool results before they consume protected final-answer runway. A spill is not a failure. If a finalization-runway notice appears, all investigation tools are finished: stop investigating and answer immediately from the evidence already gathered.',
384
386
  '',
385
387
  'Finish with a single, direct, self-contained answer to the directive. Your final assistant message is the answer that will be returned to the parent.',
386
388
  ].join('\n');
@@ -468,7 +470,7 @@ export function preflightDelegateLaunch(input: DelegatePreflightInput): Delegate
468
470
  route: input.route,
469
471
  // The seed reaches the child inside its prompt, so the admission forecast
470
472
  // must measure the prompt that is actually sent, not the seed alone.
471
- seedSerialized: childPrompt,
473
+ childPrompt,
472
474
  childSystemPrompt,
473
475
  limits,
474
476
  });
@@ -247,6 +247,21 @@ function parseAttestations(
247
247
  });
248
248
  }
249
249
 
250
+ function parseSpillContentFormat(
251
+ value: unknown,
252
+ taskId: string,
253
+ ): DelegateSpillReceipt['content_format'] {
254
+ if (value === undefined) return undefined;
255
+ if (
256
+ value !== 'single_text_utf8' &&
257
+ value !== 'tool_result_content_json_v1' &&
258
+ value !== 'opaque_bytes'
259
+ ) {
260
+ fail('delegate spill receipt content_format is invalid', 'child_result_invalid', taskId);
261
+ }
262
+ return value;
263
+ }
264
+
250
265
  function parseSpillReceipts(value: unknown, taskId: string): readonly DelegateSpillReceipt[] {
251
266
  if (!Array.isArray(value))
252
267
  fail('delegate result package spilled_artifacts must be an array', 'child_result_invalid', taskId);
@@ -264,6 +279,7 @@ function parseSpillReceipts(value: unknown, taskId: string): readonly DelegateSp
264
279
  source_call_index: requireInteger(entry, 'source_call_index', taskId),
265
280
  byte_length: requireInteger(entry, 'byte_length', taskId),
266
281
  sha256: requireSha256(entry, 'sha256', taskId),
282
+ content_format: parseSpillContentFormat(entry['content_format'], taskId),
267
283
  };
268
284
  });
269
285
  }
@@ -160,6 +160,7 @@ export async function prepareDelegateLaunch(
160
160
  budget: {
161
161
  family: preflight.plan.route.family,
162
162
  rate_source: preflight.plan.route.rate_source,
163
+ conservative_rate_source: preflight.plan.conservative_estimate.rateSource,
163
164
  },
164
165
  extensionMode: input.extensionMode,
165
166
  autoDeliver: input.autoDeliver,
@@ -200,6 +201,9 @@ export interface EvaluateDelegateTerminalInput {
200
201
  /** Terminal status observed by the background task registry. */
201
202
  taskStatus: 'completed' | 'failed' | 'killed';
202
203
  taskError: string | undefined;
204
+ /** Real merged child output owned by the background-task registry. */
205
+ taskOutputPath?: string | undefined;
206
+ taskOutputAbsPath?: string | undefined;
203
207
  }
204
208
 
205
209
  /**
@@ -251,6 +255,21 @@ async function adjudicateDelegateTerminal(
251
255
  recorded?.message ??
252
256
  input.taskError ??
253
257
  'the delegate child exited without committing a result package';
258
+ const preserved = ['seed.json', 'budget-plan.json', 'child-terminal.json', 'runtime-budget.json']
259
+ .filter((name) => existsSync(join(input.artifactDirAbs, name)));
260
+ if (
261
+ input.taskOutputPath !== undefined &&
262
+ input.taskOutputAbsPath !== undefined &&
263
+ existsSync(input.taskOutputAbsPath)
264
+ ) {
265
+ preserved.push(input.taskOutputPath);
266
+ }
267
+ const diagnosticTargets = preserved.filter(
268
+ (name) => name === 'child-terminal.json' || name === 'runtime-budget.json' || name === input.taskOutputPath,
269
+ );
270
+ const diagnostic = diagnosticTargets.length === 0
271
+ ? 'No child terminal record or merged task output exists; inspect the preserved launch artifacts listed above.'
272
+ : `Inspect the preserved diagnostic evidence: ${diagnosticTargets.join(', ')}.`;
254
273
  const error = new DelegateError(
255
274
  `bg_delegate produced no committed answer: ${detail}`,
256
275
  {
@@ -258,9 +277,9 @@ async function adjudicateDelegateTerminal(
258
277
  childCreated: true,
259
278
  taskId: input.taskId,
260
279
  artifactDir: input.artifactDirAbs,
261
- preserved: ['seed.json', 'budget-plan.json', 'child.stdout.txt', 'child.stderr.txt'],
280
+ preserved,
262
281
  remediation: [
263
- 'Inspect child.stderr.txt and child-terminal.json in the artifact directory.',
282
+ diagnostic,
264
283
  'No partial answer is returned; nothing was truncated to look like success.',
265
284
  ],
266
285
  },