pi-background-tasks 2.3.0 → 2.4.0

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 CHANGED
@@ -27,7 +27,7 @@
27
27
  | Fact | Value |
28
28
  | --- | --- |
29
29
  | Package | `pi-background-tasks` |
30
- | Version | `2.3.0` |
30
+ | Version | `2.4.0` |
31
31
  | Node engine | `>=22.19.0` |
32
32
  | Pi entrypoints | `./extensions/anthropic-attribution.ts`, `./extensions/background-tasks.ts` |
33
33
  | Package image | [logo.png](https://raw.githubusercontent.com/ismailsaleekh/pi-background-tasks/main/logo.png) |
@@ -295,6 +295,7 @@ Agent tasks launched through `pi -p ...` or `pi --mode json ...` and marked `isA
295
295
  ## Architecture, trust, and safety
296
296
 
297
297
  - Runtime task files live under `.pi/tasks/<session-id>-<pid>/`; Fusion artifacts under `.pi/fusion/...`; delegate artifacts under `.pi/delegate/...`.
298
+ - Delegate launch budgeting uses backed route-family calibration for eligible large prompts and records a provable conservative counter-forecast across every byte class. During investigation, text and image-bearing tool output spill losslessly when retaining them would consume protected final-answer runway—even below the normal 64 KiB per-result threshold. Exact artifact ranges return as base64, final capture excludes intermediate tool-use narration, and near the runway boundary tools are disabled for graceful finalization. Runtime token estimates are advisory; Pi/provider own live context admission, avoiding Fusion BUG-185-style false refusals.
298
299
  - Shell jobs are tracked by the package, but they are not sandboxed. Treat commands as local processes with your permissions and credentials.
299
300
  - Delegate and Fusion child Pi processes are route-pinned where applicable; delegate/Fusion paths do not silently substitute routes.
300
301
  - Fusion uses direct child `pi --mode text` processes, not direct completion APIs. Frontier Fusion routes are admitted only through Pi Anthropic or Codex subscription OAuth; metered frontier API credentials are rejected before child creation.
package/TESTING.md CHANGED
@@ -114,8 +114,7 @@ Two unit gates protect Fusion's persisted artifact bytes, which are a frozen for
114
114
  - `tests/unit/delegate-seed.test.ts` — verbatim visible text, thinking/tool-payload
115
115
  exclusion, marker-only images, sibling-batch exclusion, byte-identical construction
116
116
  across repeated builds and separate processes, and receive-side seed verification.
117
- - `tests/unit/delegate-budget.test.ts` — reserve arithmetic, boundary accept/reject,
118
- and the total runtime governor.
117
+ - `tests/unit/delegate-budget.test.ts` — reserve arithmetic, backed-family versus conservative launch policy, incident byte-class fixture replay, provable multibyte counter-forecast, retained-growth runway, boundary accept/reject, and advisory runtime measurement.
119
118
  - `tests/unit/delegate-result-package.test.ts` — hash verification, strict base64,
120
119
  encoding refusal for lone surrogates, route-mismatch and missing-attestation
121
120
  detection, and explicitly unavailable usage.
@@ -132,8 +131,13 @@ Two unit gates protect Fusion's persisted artifact bytes, which are a frozen for
132
131
  resolves the exact pinned provider, retains the package guard, and commits.
133
132
  - `tests/scripted-provider/delegate-child-guard.test.ts` — the child guard inside a
134
133
  real Pi agent loop: a 2 MB tool result spilled to a hashed artifact with the payload
135
- kept out of the transcript, a blocked over-budget model call, exact bounded range
136
- reads, route-drift refusal, and turn-limit enforcement.
134
+ kept out of the transcript, structured preservation of image-bearing results,
135
+ repeated sub-64-KiB results spilling against protected route runway, BUG-185
136
+ regression coverage proving advisory estimates do not abort a valid live request,
137
+ graceful no-tool finalization, byte-exact base64 range reads including a split UTF-8
138
+ sequence, malformed-Unicode spill refusal, exclusion of intermediate tool-use
139
+ narration from the committed answer, multi-turn usage accumulation, route-drift
140
+ refusal, answer capture, and turn-limit enforcement.
137
141
  - `tests/sdk/delegate-sdk.test.ts` — the full public loop through the shipped
138
142
  entrypoint with a fake child `pi`: launch receipt, projected context actually
139
143
  reaching the child, default isolated argv, explicit ambient argv/warning/metadata,
package/TEST_PLAN.md CHANGED
@@ -91,9 +91,9 @@ SDK/RPC/scripted-provider/package/compatibility coverage asserts exactly four to
91
91
  | Pi hook contract | `npm run test:hook-contract`, `tests/scripted-provider/pi-hook-contract-evidence.json`, `src/core/delegate/hook-contract-evidence.json` | | | | | | evidence parity | yes | Executes real Pi agent loops and records, rather than assumes: Fusion `before_provider_request` transformations chain in extension load order through the Codex transport adapter and abort before local HTTP transport; Delegate `context` fires once before every model call in load order; returned messages reach the provider; **throwing** in `context` does NOT block dispatch (Pi catches and continues); `ctx.abort()` either skips provider invocation (Pi 0.84) or delivers an already-aborted signal (older supported lines), blocks transport, and terminates the run; `tool_result` fires before the transcript entry, chains in load order, replacement reaches the provider while the original does not, and toolCallId/role/`isError` survive. Handler ordering across two separate probe extensions is pinned. The evidence file is compared, never silently regenerated, and a package test asserts the shipped copy is byte-identical. |
92
92
  | Delegate context seeding | `bg_delegate` seed `pi-background-tasks.delegate-seed.v2`, `seed.json`, `context-omission-ledger.json` | yes | yes | | | | mutation guard | yes | Unit covers verbatim visible user/assistant text, complete exclusion of thinking/tool-call arguments/tool-result payloads, marker-only images with no raw bytes anywhere in the seed, exclusion of the in-flight `bg_delegate` call **and every sibling call in the same assistant message** (two delegates launched together get byte-identical projected history), the prompt preserved exactly and marked `explicit_text` authority, hash-bound `extension_mode`, zero payload-preview bytes, byte-identical construction across repeated builds and across separate processes via a fixed-leaf fixture, and refusal of a blank prompt. Receive-side verification rejects a single mutated byte, a foreign task identity, a structurally malformed seed whose hash matches, a directive whose hash disagrees with its text, an unknown extension mode, and an unsupported capability. SDK proves the projected parent text actually reaches the child. |
93
93
  | Delegate route pinning | `bg_delegate` `route`, `route_attestations` | yes | yes | | | | mutation guard | yes | Unit covers defaulting to the parent current model, explicit pinning, refusal of an unavailable route with `route_unresolved` and no substitution, refusal of a route with no declared context window with `route_capacity_unknown` rather than assuming one, and refusal when neither is available. The result package rejects a mismatched route, a mismatched attestation, and a package carrying no attestation at all. The scripted-provider gate proves an observed route drift prevents a success commit entirely. The mutation guard fails if a fallback list or first-available selection appears. |
94
- | Delegate budget and guard | `budget-plan.json`, typed `seed_budget_exceeded` / `provider_context_budget_exhausted` | yes | yes | | | | mutation guard | yes | Unit covers reserve subtraction, exact-boundary accept and one-byte-past reject, the child system prompt counted as input, multi-byte UTF-8 measured by bytes, refusal of unusable windows without defaulting, and a total runtime governor that cannot throw from inside a hook. Preflight rejection is proven to create **zero** children and **zero** artifacts for hook-contract, budget, and blank-prompt refusals, with the artifact directory verified absent. The scripted-provider gate proves an over-budget model call is blocked in a real agent loop and produces a typed terminal record with no committed result. |
95
- | Delegate tool-result spilling | `spill/` artifacts, `pi-background-tasks.delegate-receipt.v1`, `delegate_read_artifact` | yes | yes | | | | mutation guard | yes | Unit proves a 2 MB payload is written in full, hashed, and represented by a receipt carrying `(turn_sequence, source_call_index, tool_call_id)` assigned before execution, so parallel spills completing out of order cannot be mis-associated; an aggregate-cap refusal writes nothing and emits no receipt; bounded range reads return exactly the requested bytes; a range past end-of-file, a path escaping the artifact directory, and non-positive lengths are refused rather than shortened. The scripted-provider gate proves in a real agent loop that the raw payload never enters the transcript, the transcript instead carries a receipt naming the hash and stating nothing was truncated, and an over-long range read fails loudly. |
96
- | Delegate result commit and `bg_result` | `result.json` (`pi-background-tasks.delegate-result.v1`), `bg_result` | yes | yes | | | | mutation guard | yes | The single atomically-committed package is the sole answer data plane: temp-write, file fsync, and rename, with the rename as the commit point; POSIX additionally fsyncs the parent directory while Windows explicitly lacks that portable directory-entry guarantee. Unit covers round-trip verification, multi-block concatenation, exact preservation of U+2028/U+2029/emoji/unnormalized sequences, refusal of lone surrogates rather than U+FFFD substitution, detection of block-hash, aggregate-hash, declared-length, and non-strict-base64 corruption, foreign identity/seed rejection, and explicitly unavailable usage that is never reported as zero. Terminal evaluation treats a zero-exit child with no committed package as typed `child_exited_without_commit`, reports the child-recorded reason when present, and classifies a killed task as cancelled. SDK covers not-ready (non-blocking), completed, corrupted, route-drift, and unknown-task paths, and proves an oversized answer degrades to an artifact reference while an explicit inline request fails with `result_too_large_for_inline` and is never truncated. |
94
+ | Delegate budget and guard | `budget-plan.json` v3, `runtime-budget.json`, typed `seed_budget_exceeded` | yes | yes | | | | mutation guard | yes | Unit covers reserve subtraction, exact-boundary accept and one-byte-past reject, backed large-prompt family calibration with a provable every-byte-class counter-forecast, the 173,740-byte compacted-session incident byte-class fixture, protected retained-growth/finalization runway, child system prompt input, multi-byte UTF-8, and conservative fallback for small/unbacked routes. Preflight refusal creates **zero** children and **zero** artifacts. Scripted-provider coverage pins Fusion BUG-185, image-bearing structured spill fidelity, base64 range fidelity across split UTF-8, final-answer-only capture, and truthful diagnostics. An advisory overage does not self-report provider exhaustion or abort valid transport; it disables tools for graceful finalization. Runtime evidence records retained/spilled bytes, context estimates, first-request usage, and calibration underforecast. |
95
+ | Delegate tool-result spilling | `spill/` artifacts, `pi-background-tasks.delegate-receipt.v1`, `delegate_read_artifact` | yes | yes | | | | mutation guard | yes | Unit proves a 2 MB payload is written in full, hashed, and represented by a receipt carrying `(turn_sequence, source_call_index, tool_call_id)` assigned before execution, so parallel spills completing out of order cannot be mis-associated; an aggregate-cap refusal writes nothing and emits no receipt; bounded range reads return exactly the requested bytes; a range past end-of-file, a path escape, and non-positive lengths are refused. Scripted-provider coverage additionally proves repeated 32 KiB results—each below the fixed 64 KiB cap—spill proactively when retaining them would consume protected final-answer runway, while the child still commits a complete answer. |
96
+ | Delegate result commit and `bg_result` | `result.json` (`pi-background-tasks.delegate-result.v1`), `bg_result` | yes | yes | | | | mutation guard | yes | The single atomically-committed package is the sole answer data plane: temp-write, file fsync, and rename, with the rename as the commit point; POSIX additionally fsyncs the parent directory. Unit covers round-trip verification, exact encoding, corruption/identity/route rejection, explicitly unavailable usage, answer capture enforcement, zero-exit/no-commit, child-recorded failure, and cancellation. SDK covers not-ready, completed, corrupted, route-drift, unknown-task, and no-truncation delivery. Terminal diagnostics dynamically advertise only paths that exist: the merged `.pi/tasks/...output` path is reported when available, while absent delegate-local `child.stdout.txt`/`child.stderr.txt` files are never claimed. |
97
97
  | Delegate extension/isolation boundary | `extensionMode`, child argv, `--session-id`, `--session-dir`, `--tools` | yes | yes | | | | mutation guard | yes | Unit proves default/explicit isolated mode sets `--no-extensions`; ambient mode omits only that flag; both modes retain separate task-owned session identity, inspect-only model tools, the explicit denylist, disabled skills/prompt templates/themes/context files, and the package guard; Anthropic keeps attribution-before-guard in both modes. Public schema/preparation rejects unknown modes, arbitrary `extensions`/`extensionPaths`, and route substitution. Seed/task facts/launch details/manifest bind the selected mode, and ambient receipt text warns that arbitrary extension code executes outside the tool sandbox. SDK verifies argv, warning, and metadata. A fresh-process scripted-provider regression installs a provider only in a temp global extension directory: isolated mode fails unknown-provider with no result, while ambient mode resolves that exact pinned provider and commits under the guard. |
98
98
  | Fusion artifact byte immutability | `canonical-input.json`, `context-omission-ledger.json`, `budget-plan.json` | yes | | | | | shared-source guard | | A 28-case differential corpus is rendered to raw bytes and compared against a committed golden file that is never auto-updated once present, and separately against `tests/oracle/fusion-context-pre-extraction.ts`, a verbatim pre-extraction copy of the projection engine used as an **independent oracle**. Equivalence covers canonical input, ledger, and budget-plan bytes, `Object.is` comparison of budget floats including `utilization`, field-for-field accounting parity, ledger root hashes, and identical error type and message for unknown blocks and blank requests. The package guard now scans the shared `context/` modules, not only the Fusion facade, and pins both the shared divisor definition and Fusion's binding to it. |
99
99
  | Fusion model selector | `/fusion-models`, `fusion-models.json`, `FusionModelSelector` | yes | yes | yes | yes | yes | yes | | Unit covers strict config parsing, duplicates, slash-containing model IDs, stale model failures, `$current`, atomic save, inter-process lock/revision compare-and-swap, and deterministic concurrent-save conflict. Component covers all five slots, duplicate selection, searchable model list, stale display, reset/save/cancel, persistence errors, and width safety. SDK drives the real command in a synthetic TUI context without `ctx.mode` to cover old Pi compatibility and verifies duplicate `$current`/explicit model persistence; SDK also verifies headless no-UI rejection. RPC verifies non-TUI notification without hanging, PTY verifies the real selector opens in a TUI, and compatibility smoke verifies print-mode rejection for every supported Pi version. |
package/docs/INDEX.md CHANGED
@@ -142,7 +142,7 @@ Generated navigation for every package-local documentation page. This index inte
142
142
  | tool | `bg_delegate` | `tool:bg_delegate` | `src/delegate-extension.ts:340` |
143
143
  | tool | `bg_kill` | `tool:bg_kill` | `src/extension.ts:908` |
144
144
  | tool | `bg_logs` | `tool:bg_logs` | `src/extension.ts:863` |
145
- | tool | `bg_result` | `tool:bg_result` | `src/delegate-extension.ts:515` |
145
+ | tool | `bg_result` | `tool:bg_result` | `src/delegate-extension.ts:516` |
146
146
  | tool | `bg_run` | `tool:bg_run` | `src/extension.ts:682` |
147
147
  | tool | `bg_run_pi_attested` | `tool:bg_run_pi_attested` | `src/extension.ts:767` |
148
148
  | tool | `bg_status` | `tool:bg_status` | `src/extension.ts:832` |
@@ -62,7 +62,8 @@ Calibration facts in code:
62
62
  - observed large Fusion prompt corpus: 882 prompts, dated 2026-08-02;
63
63
  - Anthropic configured rate: 1.73 B/token after haircut;
64
64
  - OpenAI Codex configured rate: 2.89 B/token after haircut;
65
- - conservative/floor rates are used for delegate scope, small prompts, unbacked models, unknown providers, capacity guards, and dense-ASCII out-of-domain cases.
65
+ - delegate launch uses backed family calibration only for large prompts on routes that can hold the calibration domain; small prompts/routes, unbacked models, unknown providers, and dense-ASCII out-of-domain cases use conservative/floor rates;
66
+ - delegate runtime context estimates are advisory, while a separate provable `1.00 B/token` retained-growth budget drives explicit spilling and no-tool finalization.
66
67
 
67
68
  The dense-ASCII gate is explicitly a low-whitespace heuristic proxy, not a tokenizer guarantee. Calibration applies only when the input is in the measured domain and the route capacity can hold that domain.
68
69
 
@@ -70,9 +71,10 @@ The dense-ASCII gate is explicitly a low-whitespace heuristic proxy, not a token
70
71
 
71
72
  - `allowedInputTokens()` returns a signed number and never clamps unusable routes to zero.
72
73
  - Callers must reject unusable or too-small context windows before spawning children.
73
- - Multibyte bytes cannot bypass accounting.
74
+ - Multibyte bytes cannot bypass accounting; delegate's published provable counter-forecast charges them at the same `1.00 B/token` ceiling as every other byte class.
74
75
  - Unknown output contracts are charged separately; future output cannot be assumed to be cheap.
75
76
  - Rate-source warnings are part of the contract and should be surfaced in refusal details.
77
+ - A package-local estimate must not reject a live provider payload by subtracting hypothetical output; Fusion BUG-185 established this as a false-refusal shape. Use conservative estimates for lossless spill decisions, where a false positive preserves bytes rather than failing work.
76
78
 
77
79
  ## No silent truncation
78
80
 
@@ -13,18 +13,17 @@
13
13
  "state": "pass"
14
14
  },
15
15
  {
16
- "authored_body_sha256": "sha256:36942d62aa217dad07813c7900291adbc39b1ac859b18d94f606db0ce2e13a9f",
16
+ "authored_body_sha256": "sha256:810591c82ced086e1622be9b037db88c8a667856580895dc90cd5af54c6e2107",
17
17
  "covers_sources": [
18
18
  "src/core/context/parent-snapshot.ts",
19
19
  "src/core/context/token-budget.ts",
20
20
  "src/core/context/visible-conversation-v2.ts"
21
21
  ],
22
22
  "doc_id": "concepts/context-projection-and-budgeting",
23
- "notes": "Reviewed visible conversation transform identity, active-branch snapshotting, omission receipts and hashes, text/tool/thinking/image dispositions, token estimators, calibration, reserves, warnings, and fail-loud no-truncation behavior.",
24
23
  "rel": "docs/concepts/context-projection-and-budgeting.md",
25
24
  "required": true,
26
25
  "reviewer": "gpt-5.5-final-context-projection-reviewer",
27
- "state": "pass"
26
+ "state": "stale-authored-prose"
28
27
  },
29
28
  {
30
29
  "authored_body_sha256": "sha256:72c53dcc104f9b5efa57e38bcb21a4c3c620d5caf1571af82348f94dcafbe941",
@@ -75,7 +74,7 @@
75
74
  "state": "stale-authored-prose"
76
75
  },
77
76
  {
78
- "authored_body_sha256": "sha256:bc319ce7e62d2592c71476f950947c8ff011fb74e7f2093b0169f77d59980f18",
77
+ "authored_body_sha256": "sha256:67fbb232e66bf0b9f4c1a81b54ec5476c0aae929ed0f0777d57601afcac8e08b",
79
78
  "covers_sources": [
80
79
  "extensions/delegate-child.ts",
81
80
  "src/core/delegate/artifacts.ts",
@@ -833,7 +832,7 @@
833
832
  "image": "https://raw.githubusercontent.com/ismailsaleekh/pi-background-tasks/main/logo.png",
834
833
  "name": "pi-background-tasks",
835
834
  "type": "module",
836
- "version": "2.3.0"
835
+ "version": "2.4.0"
837
836
  },
838
837
  "public_surface_ids": [
839
838
  "command:bg",
@@ -1172,7 +1171,7 @@
1172
1171
  ],
1173
1172
  "type": "object"
1174
1173
  },
1175
- "source": "src/delegate-extension.ts:515"
1174
+ "source": "src/delegate-extension.ts:516"
1176
1175
  },
1177
1176
  {
1178
1177
  "description": "Start a named long-running shell command in the background and return immediately with a task ID and output path. By default, completed, failed, or killed terminal state is delivered automatically as <background-task-notification> and starts a follow-up agent turn; do not sleep or poll merely to wait. Output is written to .pi/tasks and model-visible logs are bounded to 50.0KB.",
@@ -24,7 +24,9 @@ Start from the symptom, verify the source-owned doc, then apply the remediation.
24
24
  | Terminal metadata/output durability failure | Fsync/close/metadata write failed | Treat terminal state as failed; inspect `DurableFileError` operation/path/cause. See `docs/subsystems/child-launch-durability-and-safety.md`. |
25
25
  | Delegate refuses with `delegate_hook_contract_unsupported` | Current Pi hook behavior does not match committed evidence | Re-run the hook characterization gate during release work and re-review the guard; do not weaken the guard. See `docs/operations/testing.md`. |
26
26
  | Delegate refuses with `route_unresolved` or `route_capacity_unknown` | Requested/default route unavailable or lacks usable context window | Pin an available provider/model with declared context window; no substitute route is selected. |
27
- | Delegate refuses with `seed_budget_exceeded` | Frozen seed plus child prompt/system prompt exceeds allowed input tokens | Use a larger-context subscription route, delegate earlier, or reduce visible parent text. Nothing was clipped. |
27
+ | Delegate refuses with `seed_budget_exceeded` | Exact child prompt plus system prompt exceeds allowed input under the backed route-family policy or conservative fallback | Use a larger-context subscription route, delegate earlier, or reduce visible parent text. Nothing was clipped. |
28
+ | Delegate spills many individually small tool results | Retaining them would consume protected final-answer runway | This is expected lossless pressure control. The full bytes are in `spill/`; inspect `runtime-budget.json`. Narrow the investigation only if receipt-driven range reads become excessive. |
29
+ | Delegate enters finalization runway | Advisory context/runway pressure disabled tools so the child can answer from gathered evidence | Let the child finish. Do not re-enable tools or treat the advisory estimate as a provider context rejection. |
28
30
  | `bg_result` says not ready | Child has not committed `result.json` yet | Wait for terminal notification or inspect later; do not poll tightly. |
29
31
  | Delegate result corruption/hash/identity mismatch | Result package does not match task/seed/route/hash contract | Treat as invalid; inspect artifact bytes. Do not synthesize an answer. |
30
32
  | Fusion model unavailable or metered route refusal | Frontier route is not admitted as a Pi subscription/OAuth route, or configured model is stale | Fix `/fusion-models` config to available subscription routes. Never route GPT/Claude-class work through metered APIs. |
@@ -37,11 +37,11 @@ This generated registry lists production environment-variable references, runtim
37
37
  | `PI_API_BASE_URL` | remove | `src/core/attested-pi-run.ts:135`<br>`src/core/fusion/pi-child.ts:98` |
38
38
  | `PI_API_KEY` | remove | `src/core/attested-pi-run.ts:135`<br>`src/core/fusion/pi-child.ts:98` |
39
39
  | `PI_AUTH_FILE` | remove | `src/core/attested-pi-run.ts:135`<br>`src/core/fusion/pi-child.ts:98` |
40
- | `PI_BG_DELEGATE_ARTIFACT_DIR` | read, write | `src/core/delegate/launch.ts:353`<br>`src/delegate-child-extension.ts:279` |
41
- | `PI_BG_DELEGATE_LAUNCH_NONCE` | read, write | `src/core/delegate/launch.ts:357`<br>`src/delegate-child-extension.ts:283` |
42
- | `PI_BG_DELEGATE_SEED_PATH` | read, write | `src/core/delegate/launch.ts:354`<br>`src/delegate-child-extension.ts:280` |
43
- | `PI_BG_DELEGATE_SEED_SHA256` | read, write | `src/core/delegate/launch.ts:355`<br>`src/delegate-child-extension.ts:281` |
44
- | `PI_BG_DELEGATE_TASK_ID` | read, write | `src/core/delegate/launch.ts:356`<br>`src/delegate-child-extension.ts:282` |
40
+ | `PI_BG_DELEGATE_ARTIFACT_DIR` | read, write | `src/core/delegate/launch.ts:353`<br>`src/delegate-child-extension.ts:384` |
41
+ | `PI_BG_DELEGATE_LAUNCH_NONCE` | read, write | `src/core/delegate/launch.ts:357`<br>`src/delegate-child-extension.ts:388` |
42
+ | `PI_BG_DELEGATE_SEED_PATH` | read, write | `src/core/delegate/launch.ts:354`<br>`src/delegate-child-extension.ts:385` |
43
+ | `PI_BG_DELEGATE_SEED_SHA256` | read, write | `src/core/delegate/launch.ts:355`<br>`src/delegate-child-extension.ts:386` |
44
+ | `PI_BG_DELEGATE_TASK_ID` | read, write | `src/core/delegate/launch.ts:356`<br>`src/delegate-child-extension.ts:387` |
45
45
  | `PI_BG_DISABLE_PI_TELEMETRY` | read | `src/core/registry.ts:195` |
46
46
  | `PI_BG_DISABLE_UPDATE_CHECK` | read | `src/extension.ts:455` |
47
47
  | `PI_BG_MAX_OUTPUT_BYTES` | read | `src/core/registry.ts:69` |
@@ -71,16 +71,14 @@ This generated registry lists production environment-variable references, runtim
71
71
  | Kind | Path/artifact | Provenance |
72
72
  | --- | --- | --- |
73
73
  | config | `fusion-models.json` | `src/core/fusion/config.ts:21` |
74
- | delegate-artifact | `budget-plan.json` | `src/core/delegate/artifacts.ts:43` |
75
- | delegate-artifact | `child-prompt.txt` | `src/core/delegate/artifacts.ts:47` |
76
- | delegate-artifact | `child.stderr.txt` | `src/core/delegate/artifacts.ts:48` |
77
- | delegate-artifact | `child.stdout.txt` | `src/core/delegate/artifacts.ts:49` |
78
- | delegate-artifact | `context-omission-ledger.json` | `src/core/delegate/artifacts.ts:42` |
74
+ | delegate-artifact | `budget-plan.json` | `src/core/delegate/artifacts.ts:44` |
75
+ | delegate-artifact | `child-prompt.txt` | `src/core/delegate/artifacts.ts:48` |
76
+ | delegate-artifact | `context-omission-ledger.json` | `src/core/delegate/artifacts.ts:43` |
79
77
  | delegate-artifact | `error.json` | `src/core/delegate/artifacts.ts:50` |
80
- | delegate-artifact | `manifest.json` | `src/core/delegate/artifacts.ts:44` |
81
- | delegate-artifact | `outcome.json` | `src/core/delegate/artifacts.ts:45` |
78
+ | delegate-artifact | `manifest.json` | `src/core/delegate/artifacts.ts:45` |
79
+ | delegate-artifact | `outcome.json` | `src/core/delegate/artifacts.ts:46` |
82
80
  | delegate-artifact | `result.json` | `src/core/delegate/result-package.ts:28` |
83
- | delegate-artifact | `seed.json` | `src/core/delegate/artifacts.ts:41` |
81
+ | delegate-artifact | `seed.json` | `src/core/delegate/artifacts.ts:42` |
84
82
  | delegate-artifact | `spill/<receipt-named-file>` | `src/core/delegate/artifacts.ts:54` |
85
83
  | directory | `.pi/delegate/<session-id>-<pid>/<task-id>/` | `src/core/delegate/artifacts.ts:160` |
86
84
  | directory | `.pi/fusion/<session-id>-<pid>/<run-id>/` | `src/core/fusion/artifacts.ts:563` |
@@ -116,17 +114,19 @@ This generated registry lists production environment-variable references, runtim
116
114
  | Schema | Provenance |
117
115
  | --- | --- |
118
116
  | `phase2.pi_task_attestation.v1` | `src/core/attested-pi-run.ts:20` |
119
- | `pi-background-tasks.delegate-budget-plan.v2` | `src/core/delegate/types.ts:21` |
120
- | `pi-background-tasks.delegate-child-terminal.v1` | `src/delegate-child-extension.ts:387` |
117
+ | `pi-background-tasks.delegate-budget-plan.v3` | `src/core/delegate/types.ts:21` |
118
+ | `pi-background-tasks.delegate-child-terminal.v1` | `src/delegate-child-extension.ts:584` |
121
119
  | `pi-background-tasks.delegate-hook-contract.v1` | `src/core/delegate/hook-contract.ts:15` |
122
120
  | `pi-background-tasks.delegate-launch.v1` | `src/delegate-extension.ts:458` |
123
121
  | `pi-background-tasks.delegate-ledger.v1` | `src/core/delegate/types.ts:16` |
124
122
  | `pi-background-tasks.delegate-manifest.v2` | `src/core/delegate/types.ts:22` |
125
- | `pi-background-tasks.delegate-outcome.v1` | `src/core/delegate/runner.ts:224` |
123
+ | `pi-background-tasks.delegate-outcome.v1` | `src/core/delegate/runner.ts:228` |
126
124
  | `pi-background-tasks.delegate-receipt.v1` | `src/core/delegate/types.ts:19` |
127
- | `pi-background-tasks.delegate-result-view.v1` | `src/delegate-extension.ts:731` |
125
+ | `pi-background-tasks.delegate-result-view.v1` | `src/delegate-extension.ts:734` |
128
126
  | `pi-background-tasks.delegate-result.v1` | `src/core/delegate/types.ts:18` |
127
+ | `pi-background-tasks.delegate-runtime-budget.v1` | `src/delegate-child-extension.ts:489` |
129
128
  | `pi-background-tasks.delegate-seed.v2` | `src/core/delegate/types.ts:15` |
129
+ | `pi-background-tasks.delegate-tool-result-content.v1` | `src/delegate-child-extension.ts:190` |
130
130
  | `pi-background-tasks.extension-request.v1` | `src/core/extension-api.ts:15` |
131
131
  | `pi-background-tasks.extension-response.v1` | `src/core/extension-api.ts:16` |
132
132
  | `pi-background-tasks.extension-terminal.v1` | `src/core/extension-api.ts:17` |
@@ -149,7 +149,7 @@ This generated registry lists production environment-variable references, runtim
149
149
  | `pi-background-tasks.fusion-merge-input.v1` | `src/core/fusion/prompts.ts:336` |
150
150
  | `pi-background-tasks.fusion-models.v1` | `src/core/fusion/types.ts:13` |
151
151
  | `pi-background-tasks.fusion-progress.v1` | `src/fusion-extension.ts:59` |
152
- | `pi-background-tasks.fusion-result-view.v1` | `src/delegate-extension.ts:772` |
152
+ | `pi-background-tasks.fusion-result-view.v1` | `src/delegate-extension.ts:775` |
153
153
  | `pi-background-tasks.fusion-result.v4` | `src/core/fusion/types.ts:19` |
154
154
  | `pi-background-tasks.fusion-result.v5` | `src/core/fusion/types.ts:20` |
155
155
  | `pi-background-tasks.fusion-runtime-guard.v2` | `src/core/fusion/child-protocol.ts:24` |
@@ -82,43 +82,52 @@ Routes are pinned once:
82
82
  - unavailable/unknown-capacity routes fail;
83
83
  - no substitution, fallback, or retry on a different route.
84
84
 
85
- Budgets use a delegate-specific conservative estimator. Constants currently documented by source/tests:
85
+ Budgeting separates admission from package-owned runtime growth. Constants currently documented by source/tests:
86
86
 
87
87
  - reserved output: `16,384` tokens;
88
88
  - framing reserve: `8,192` tokens;
89
89
  - safety reserve: `4,096` tokens;
90
90
  - minimum usable input: `8,192` tokens;
91
+ - protected finalization input runway: `32,768` tokens;
92
+ - finalization trigger inside retained-growth runway: `8,192` tokens;
91
93
  - default turns/tools/timeout: `24` / `120` / `1200s`;
92
94
  - per-result transcript cap: `64 KiB`;
93
- - aggregate tool-output cap: `64 MiB`;
94
- - answer capture cap value carried in the seed: `4 MiB`; current child code does not separately enforce this before packaging;
95
+ - aggregate raw tool-output cap: `64 MiB`;
96
+ - answer capture cap: `4 MiB`, enforced before result packaging;
95
97
  - inline answer cap: `48 KiB`.
96
98
 
97
- Launch admission measures the child system prompt plus the actual child prompt carrying the seed. Runtime guard measures retained input before every model call. An over-budget runtime call latches `provider_context_budget_exhausted`, aborts the run, and suppresses outgoing content.
99
+ Launch admission measures the child system prompt plus the exact child prompt carrying the seed. Backed large prompts use the shared family calibration; prompts or routes below the calibration domain and unknown/unbacked routes use the provable `1.00 B/token` profile. `budget-plan.json` v3 also records the provable conservative counter-forecast, protected finalization reserve, and retained-growth budget.
100
+
101
+ After launch, token measurements are advisory. Fusion BUG-185 proved that a package-local estimate must not reject a live provider payload after subtracting hypothetical output. Delegate therefore does not self-report provider exhaustion from that estimate. Pi and the provider own live context handling; a genuine provider context error remains loud. Package-owned growth is controlled before transcript entry: a tool result spills whenever it exceeds the per-result cap **or** retaining it would consume protected final-answer runway. Conservative false positives therefore create explicit hash receipts rather than failed tasks. Near the end of the runway the child disables tools and injects one finalization instruction so it can answer from evidence already gathered.
98
102
 
99
103
  ## Child guard and commit discipline
100
104
 
101
105
  The child verifies seed hash, task id, and launch nonce at extension load before the first model call. It then enforces:
102
106
 
103
- - context budget before every provider call;
104
- - per-result spill receipts before tool output enters the transcript;
105
- - aggregate tool-output cap;
107
+ - advisory retained-context measurement before every provider call, without a BUG-185-style token abort;
108
+ - route-runway-aware and per-result spill receipts before tool output enters the transcript, including structured preservation of image-bearing results;
109
+ - bounded artifact range reads returned as lossless base64 against remaining inline runway;
110
+ - aggregate raw tool-output cap;
111
+ - protected no-tool finalization when retained-growth runway becomes low;
106
112
  - turn and tool-call limits;
107
113
  - route attestation for assistant messages;
108
- - complete usage records only (missing/partial usage is `unavailable`, never zero);
114
+ - per-turn usage accumulation across the full agent loop; if any turn is missing/partial, aggregate usage is `unavailable` rather than understated or replaced by a later record;
109
115
  - accepted final stop reason `stop` only, so provider `length` stops become `child_model_output_limit` rather than partial success;
116
+ - answer capture from only the final clean-stop assistant message, never intermediate tool-use narration;
110
117
  - non-empty, non-whitespace answer text;
111
- - well-formed UTF-8 answer blocks.
118
+ - the declared answer capture cap, with no partial result on overflow;
119
+ - well-formed UTF-8 answer blocks;
120
+ - durable `runtime-budget.json` evidence containing context measurements, retained/spilled bytes, finalization state, first-request observed usage, and calibration-underforecast evidence.
112
121
 
113
122
  A terminal latch prevents later success commit after any degraded/refused condition. This avoids a hash-valid result built on silently modified context.
114
123
 
115
124
  `result.json` is the single answer data plane. It is child-written by temp file, file fsync, and rename; POSIX then fsyncs the parent directory, while Windows skips directory fsync because Node does not provide the same portable guarantee there. Final-name presence is the child commit point. No final `result.json` means no accepted answer, regardless of process exit code. `child-terminal.json` records child-side terminal failures when no success package is committed.
116
125
 
117
- After adjudication, the parent makes a best-effort durable write of `outcome.json`. This is separate from `result.json` so child and parent cannot race over one state field. An `outcome.json` write failure is currently ignored and does not change the returned adjudication, so the artifact may be absent even though evaluation completed. Child stdout/stderr are currently captured in the background task output file; although delegate artifact constants name `child.stdout.txt` and `child.stderr.txt`, current registry finalization does not populate those files in the delegate artifact directory.
126
+ After adjudication, the parent makes a best-effort durable write of `outcome.json`. This is separate from `result.json` so child and parent cannot race over one state field. An `outcome.json` write failure is currently ignored and does not change the returned adjudication, so the artifact may be absent even though evaluation completed. Child stdout/stderr are captured in the background task output file. Delegate-local `child.stdout.txt` and `child.stderr.txt` are not currently populated, and terminal reporting now lists only diagnostic paths that actually exist.
118
127
 
119
128
  ## Spill artifacts and `delegate_read_artifact`
120
129
 
121
- Oversized tool results are durably written in full under `spill/` and replaced with receipts. A failed spill withholds the original payload and latches a terminal failure; no uncommitted artifact is claimed by receipt.
130
+ Oversized tool results are durably written in full under `spill/` and replaced with receipts. A single text block is stored as exact UTF-8; malformed lone-surrogate text is rejected rather than silently converted to U+FFFD. Multi-block or image-bearing content is stored in a closed JSON envelope that preserves block boundaries, MIME types, text, and complete base64 image data. New receipts record `content_format`; historical v1 receipts without that optional field remain readable. A failed spill withholds the original payload and latches a terminal failure; no uncommitted artifact is claimed by receipt.
122
131
 
123
132
  `delegate_read_artifact` requires:
124
133
 
@@ -126,7 +135,7 @@ Oversized tool results are durably written in full under `spill/` and replaced w
126
135
  - `offset: non-negative safe integer`;
127
136
  - `length: positive safe integer`.
128
137
 
129
- It reads the whole artifact file, verifies the requested range is in bounds, and returns exactly that UTF-8-decoded range. Path escape and short reads fail loudly.
138
+ It reads the whole artifact file, verifies the requested range is in bounds, and returns the exact bytes as base64 plus offset/length metadata. Arbitrary ranges are never decoded as UTF-8, so a range that splits a multibyte sequence remains byte-exact rather than becoming U+FFFD. Path escape and short reads fail loudly.
130
139
 
131
140
  ## Retrieval contract
132
141
 
@@ -163,7 +172,7 @@ Launch / execution:
163
172
 
164
173
  Budget / limits:
165
174
 
166
- - `provider_context_budget_exhausted`
175
+ - `provider_context_budget_exhausted` (legacy terminal records only; current children do not infer provider exhaustion from an advisory estimate)
167
176
  - `aggregate_tool_output_cap`
168
177
  - `child_model_output_limit`
169
178
  - `child_capture_limit`
@@ -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.0`
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.
@@ -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
 
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.0",
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