pi-background-tasks 2.3.0 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/TESTING.md +10 -6
- package/TEST_PLAN.md +8 -8
- package/docs/INDEX.md +1 -1
- package/docs/concepts/context-projection-and-budgeting.md +4 -2
- package/docs/manifest.json +6 -7
- package/docs/operations/configuration.md +8 -8
- package/docs/operations/troubleshooting.md +5 -3
- package/docs/reference/runtime-contracts.md +47 -47
- package/docs/subsystems/delegation.md +22 -13
- package/docs/subsystems/docs-freshness-gate.md +3 -3
- package/docs/subsystems/fusion.md +6 -6
- package/docs/tools/bg_delegate.md +12 -7
- package/docs/tools/bg_result.md +2 -2
- package/docs/tools/fusion_research.md +2 -2
- package/package.json +1 -1
- package/src/core/context/token-budget.ts +16 -3
- package/src/core/delegate/artifacts.ts +14 -12
- package/src/core/delegate/budget.ts +78 -33
- package/src/core/delegate/launch.ts +4 -2
- package/src/core/delegate/result-package.ts +16 -0
- package/src/core/delegate/runner.ts +21 -2
- package/src/core/delegate/types.ts +12 -1
- package/src/core/fusion/child-protocol.ts +3 -3
- package/src/core/fusion/pi-child.ts +12 -5
- package/src/core/fusion/web-fetch.ts +72 -5
- package/src/core/update-check.ts +13 -1
- package/src/delegate-child-extension.ts +368 -69
- package/src/delegate-extension.ts +5 -2
|
@@ -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.
|
|
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:
|
|
20
|
+
- Schema IDs extracted: 46
|
|
21
21
|
- Environment variable references extracted: 49
|
|
22
|
-
- Behavioral attestation receipts not passing:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
190
|
-
-
|
|
191
|
-
-
|
|
192
|
-
-
|
|
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
|
-
|
|
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
|
|
package/docs/tools/bg_result.md
CHANGED
|
@@ -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:
|
|
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
|
|
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
|
|
151
|
-
- uses
|
|
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
|
+
"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 =
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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:
|
|
262
|
-
remediation:
|
|
263
|
-
'
|
|
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
|
-
*
|
|
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
|
|
35
|
-
* fit
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
|
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: '
|
|
187
|
-
segments
|
|
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 =
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
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
|
-
*
|
|
336
|
+
* Advisory runtime measurement for one prospective model call.
|
|
295
337
|
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
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: '
|
|
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
|
|
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
|
-
|
|
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
|
|
280
|
+
preserved,
|
|
262
281
|
remediation: [
|
|
263
|
-
|
|
282
|
+
diagnostic,
|
|
264
283
|
'No partial answer is returned; nothing was truncated to look like success.',
|
|
265
284
|
],
|
|
266
285
|
},
|