pi-background-tasks 0.7.3 → 0.7.4

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/PUBLISHING.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Publishing pi-background-tasks
2
2
 
3
- Release checklist for npm publishing and standalone git publishing. The current release candidate is 0.7.3; version 0.7.0 introduced the Fusion public surfaces (`/fusion`, `/fusion-models`, `fusion_brainstorm`) in addition to the background-task surfaces. Do not advertise the GitHub install target until the standalone repository has the exact release commit and tag.
3
+ Release checklist for npm publishing and standalone git publishing. The current release candidate is 0.7.4; version 0.7.0 introduced the Fusion public surfaces (`/fusion`, `/fusion-models`, `fusion_brainstorm`) in addition to the background-task surfaces. Do not advertise the GitHub install target until the standalone repository has the exact release commit and tag.
4
4
 
5
5
  ## Preconditions
6
6
 
@@ -16,11 +16,14 @@ cd packages/pi-background-tasks
16
16
  npm run test
17
17
  npm run test:full
18
18
  npm run smoke
19
+ npm run smoke:large-context
19
20
  npm run pack:dry-run
20
21
  npm run test:compat
21
22
  npm view pi-background-tasks name version --json
22
23
  ```
23
24
 
25
+ `npm run test:compat` covers exact Pi `0.75.5`, `0.81.1`, `0.82.1`, and `0.83.0`. For each version it verifies the resolved `typebox` is Pi's bundled peer (not a private or nested copy) and scans the installed package bytes for TypeBox APIs removed in the 1.3.x line.
26
+
24
27
  `pi-background-tasks` is already published; bump `package.json` before each npm publish.
25
28
 
26
29
  ## Publish to npm
@@ -34,8 +37,8 @@ npm publish --access public
34
37
  Pi install smoke after publish:
35
38
 
36
39
  ```bash
37
- PI_CODING_AGENT_DIR=$(mktemp -d) pi -e npm:pi-background-tasks@0.7.3 --offline --no-tools --no-session -p "/jobs"
38
- pi install npm:pi-background-tasks@0.7.3
40
+ PI_CODING_AGENT_DIR=$(mktemp -d) pi -e npm:pi-background-tasks@0.7.4 --offline --no-tools --no-session -p "/jobs"
41
+ pi install npm:pi-background-tasks@0.7.4
39
42
  ```
40
43
 
41
44
  ## Publish to git
@@ -48,15 +51,15 @@ git status --short --branch
48
51
  git log --oneline -3
49
52
  git remote -v
50
53
  git push origin main
51
- git tag v0.7.3
52
- git push origin v0.7.3
54
+ git tag v0.7.4
55
+ git push origin v0.7.4
53
56
  ```
54
57
 
55
58
  Pi install smoke after git tag, using an isolated Pi agent directory so no local checkout or user `~/.pi` state is involved:
56
59
 
57
60
  ```bash
58
- PI_CODING_AGENT_DIR=$(mktemp -d) pi -e git:github.com/ismailsaleekh/pi-background-tasks@v0.7.3 --offline --no-tools --no-session -p "/jobs"
59
- pi install git:github.com/ismailsaleekh/pi-background-tasks@v0.7.3
61
+ PI_CODING_AGENT_DIR=$(mktemp -d) pi -e git:github.com/ismailsaleekh/pi-background-tasks@v0.7.4 --offline --no-tools --no-session -p "/jobs"
62
+ pi install git:github.com/ismailsaleekh/pi-background-tasks@v0.7.4
60
63
  ```
61
64
 
62
65
  ## pi.dev/packages
package/README.md CHANGED
@@ -9,19 +9,19 @@ This package adds named, tracked background shell jobs with durable output files
9
9
  From npm after publish:
10
10
 
11
11
  ```bash
12
- pi install npm:pi-background-tasks@0.7.3
12
+ pi install npm:pi-background-tasks@0.7.4
13
13
  ```
14
14
 
15
15
  From git after pushing this package to its standalone repository and tagging:
16
16
 
17
17
  ```bash
18
- pi install git:github.com/ismailsaleekh/pi-background-tasks@v0.7.3
18
+ pi install git:github.com/ismailsaleekh/pi-background-tasks@v0.7.4
19
19
  ```
20
20
 
21
21
  For project-local install:
22
22
 
23
23
  ```bash
24
- pi install -l npm:pi-background-tasks@0.7.3
24
+ pi install -l npm:pi-background-tasks@0.7.4
25
25
  ```
26
26
 
27
27
  ## Commands
@@ -95,7 +95,7 @@ The lookup runs at most once per session on `session_start`, is time-boxed, and
95
95
  - `bg_status` — inspect one task or all recent tasks.
96
96
  - `bg_logs` — read bounded task output.
97
97
  - `bg_kill` — stop a running task.
98
- - `fusion_brainstorm({prompt})` — always-active tool that runs the Fusion workflow and returns the exact merged text as the tool result for the parent agent to consume, with the exact Pi `Usage` shape attached when the host supports tool-result usage: token fields plus complete `cost.input`, `cost.output`, `cost.cacheRead`, `cost.cacheWrite`, and `cost.total`. Its closed public schema has exactly one required parameter, `prompt`; extra keys are rejected. It has no eligibility, quota, routine, or justification gate. Tool context capture excludes the current assistant tool-call leaf when Pi is executing that `fusion_brainstorm` call, so the nested children do not see the in-progress tool call or sibling calls.
98
+ - `fusion_brainstorm({prompt})` — always-active tool that runs the Fusion workflow and returns the exact merged text as the tool result for the parent agent to consume, with the exact Pi `Usage` shape attached when the host supports tool-result usage: token fields plus complete `cost.input`, `cost.output`, `cost.cacheRead`, `cost.cacheWrite`, and `cost.total`. Its closed public schema has exactly one required parameter, `prompt`; extra keys are rejected. It has no eligibility, quota, routine, or justification gate. Tool context capture excludes the current assistant tool-call leaf when Pi is executing that `fusion_brainstorm` call, so the nested children do not see the in-progress tool call or sibling calls. Children receive the documented conversation projection described under [Conversation context policy](#conversation-context-policy): visible user/assistant text verbatim, with thinking and tool payloads replaced by explicit hash-accounted omission receipts. Because the prompt is composed by the parent agent, it is treated as authoritative and self-contained.
99
99
 
100
100
  `bg_run` requires a concise `name` for the footer dock, the shell `command`, and required `isAgent: boolean`. Set `isAgent: true` only when the background task launches an LLM/agent process (for example `pi -p ...` or `pi --mode json ...`); set `isAgent: false` for scripts, tests, dev servers, sleeps, and ordinary shell commands. It defaults both `notifyOnCompletion` and `triggerOnCompletion` to `true`. With those defaults, `bg_run` returns immediately, the agent continues only independent useful work or ends its current turn instead of sleeping or polling, and a durable `background-task-notification` for completed, failed, or killed state automatically starts a follow-up turn. The launch receipt states the effective notification/wake behavior explicitly. `bg_status` and `bg_logs` remain available for user-requested inspection, deliberately disabled completion delivery, concrete hang diagnosis, or reading output after the terminal event; they are not waiting primitives, and the terminal notification does not need status reconfirmation. Setting `triggerOnCompletion: false` keeps the notification but prevents it from starting an agent turn. Setting `notifyOnCompletion: false` suppresses both notification and wake-up even if `triggerOnCompletion` is true.
101
101
 
@@ -118,7 +118,50 @@ fusion-models.json
118
118
 
119
119
  Missing config means all five slots are `$current`. Malformed config, stale explicit models, unavailable current models, and concurrent selector write conflicts fail loudly before child inference. Selector saves use an inter-process lock plus revision re-read before rename so simultaneous dialogs cannot silently overwrite each other. Candidate identities are anonymized before evaluation; provider/model metadata stays in local artifacts, not in evaluator prompts.
120
120
 
121
- Progress is surfaced through `fusion` status updates, TUI cancellable loader UI for `/fusion`, and partial `fusion_brainstorm` tool updates. Session shutdown or reload tracks the whole invocation from entry, aborts live or initializing Fusion runs, and waits for cleanup. Captured conversation context is serialized as a full text transcript with no summarization-oriented 2,000-character tool-result truncation. Image bytes/base64 are not forwarded to Fusion children; user/tool-result image blocks are replaced in the transcript with `[Image omitted from fusion text transcript: <mime-type>]`, so visual information must be described in text if the children need it. Provider context-window failures remain loud child failures rather than hidden local truncation.
121
+ Progress is surfaced through `fusion` status updates, TUI cancellable loader UI for `/fusion`, and partial `fusion_brainstorm` tool updates. Session shutdown or reload tracks the whole invocation from entry, aborts live or initializing Fusion runs, and waits for cleanup.
122
+
123
+ ### Conversation context policy
124
+
125
+ Fusion children receive a **versioned conversation projection**, not a raw execution transcript. The canonical input schema is `pi-background-tasks.fusion-input.v2` and every run states exactly what was included and what was omitted.
126
+
127
+ The projection transform (`visible-conversation-ledger-v1`) is shared by both entry points:
128
+
129
+ | Content | Disposition |
130
+ |---|---|
131
+ | User text | included verbatim, never clipped |
132
+ | Assistant text | included verbatim, never clipped |
133
+ | User image blocks | `[Image omitted from fusion text transcript: <mime-type>]` marker |
134
+ | Assistant thinking | excluded; recorded as an omission receipt |
135
+ | Tool-call arguments | excluded; recorded as an omission receipt |
136
+ | Tool-result payloads | excluded; recorded as an omission receipt |
137
+ | Tool-result images | excluded; recorded as an omission receipt (never raw bytes) |
138
+ | Active `fusion_brainstorm` call and its sibling calls | scope-excluded from the branch |
139
+
140
+ Omissions are **explicit, deterministic, and auditable** — never silent. Each omitted event produces a ledger row with its kind, exact byte count, and SHA-256 of the omitted bytes; contiguous omissions collapse into source-ordered `omitted_activity` receipts carrying counts, byte totals, and a run hash. The complete ledger is persisted as `context-omission-ledger.json`. **No head, tail, or preview of an omitted payload is ever forwarded** (`tool_payload_preview_bytes` is `0`), because an arbitrary prefix is usually irrelevant and can leak secrets or carry tool-output prompt injection. Repeated construction is byte-identical, so hashes are stable.
141
+
142
+ Two entry points share the transform but differ in request authority:
143
+
144
+ | Entry point | Policy id | `request.authority` |
145
+ |---|---|---|
146
+ | `fusion_brainstorm({prompt})` | `fusion-tool-explicit-v1` | `explicit_text` — the prompt is authoritative and self-contained |
147
+ | `/fusion [prompt]` | `fusion-command-conversation-v1` | `directive_over_projected_conversation` |
148
+
149
+ **Documented limitation:** facts that exist only inside omitted tool output are not available to Fusion children. Restate any required finding as visible conversation text, or include it in the `fusion_brainstorm` prompt. Children are instructed to say so plainly rather than guess. No model-generated summarization is used as hidden preprocessing.
150
+
151
+ ### Stage budgets
152
+
153
+ Every prompt-expansion stage — candidate, evaluator, evaluation repair, and merger — is size-checked **before any child process is created**. Safety is based on the **smallest** configured model's context window, never the largest, so adding one small-context slot cannot be masked by large-context siblings.
154
+
155
+ The input token bound is `ceil(utf8Bytes / 2)`. This is a ceiling, not an estimate: across 159 real large Fusion prompts the densest observed ratio was 3.552 bytes per input token, so the divisor keeps roughly a 1.7x margin and also bounds dense non-ASCII input.
156
+
157
+ Downstream growth is guaranteed by two cooperating layers rather than by an assumption:
158
+
159
+ 1. **Enforced output contracts.** Each stage has a maximum response size measured in *JSON-rendered transfer bytes* — the bytes the response actually costs once a later stage embeds it, escaping included (candidate 48 KiB, evaluator 64 KiB, merger 64 KiB, repair diagnostics 8 KiB). Measuring the rendered form removes any escaping guess: quotes, backslashes, and newlines expand 2x and control characters up to 6x, so a raw-byte contract would not bound the embedded size. A response over its contract is a loud `child_output_cap` failure; it is preserved in the run artifacts and is never sliced, truncated, or forwarded.
160
+ 2. **An exact reserve.** The canonical input must leave room for the widest stage — the evaluation repair, which embeds three candidate answers, the invalid evaluator output, and diagnostics — plus fixed wrapper overhead. Because the contracts above are already rendered-byte bounds, this is an exact sum, not a raw size inflated by an estimated factor. The reserve is converted to tokens with the *same* `ceil(bytes / 2)` function used to measure prompts; reserving output *tokens* directly would understate the cost of re-embedding those bytes.
161
+
162
+ Each route additionally reserves its output contract, 4,096 framing, and 4,096 safety tokens. Because every step is uniformly conservative, the policy requires roughly a **168,000-token context window per configured slot**. Smaller routes are rejected at configuration time with an actionable error naming the requirement, rather than being accepted and failing later at the provider. Route capacities and the pre-candidate feasibility decision are persisted as `budget-plan.json`.
163
+
164
+ If an input still exceeds the safe budget, Fusion fails with a typed `prompt_budget_exceeded` error naming the stage, measured bytes, measured token upper bound, allowed tokens, the limiting configured model and its context window, and concrete remediation. **Zero children are launched** when preflight rejects. Provider context-window failures remain loud child failures; there is no hidden local truncation and no silent fallback anywhere in this path.
122
165
 
123
166
  ## Extension EventBus API
124
167
 
@@ -162,7 +205,7 @@ Fusion writes private debugging artifacts under:
162
205
  .pi/fusion/<session-id>-<pid>/<run-id>/
163
206
  ```
164
207
 
165
- Each run contains `manifest.json`, `canonical-input.json`, candidate/evaluation/merge prompts, raw child JSONL events, stderr, responses, `blind-candidates.json`, `evaluation.json`, `merged.md`, and `error.json` for failed/cancelled runs. Artifact files are written by private temp-file/fsync/rename, and v2 manifests persist cumulative child usage plus per-attempt observed usage/model data for successful, failed, and cancelled child attempts. Every usage record preserves the complete Pi cost breakdown; the same exact shape is cloned into `fusion_brainstorm` tool results so newer Pi hosts can calculate and replay footer/session statistics safely. These artifacts are local evidence only; they are not shown in `/jobs` or the background-task dock.
208
+ Each run contains `manifest.json`, `canonical-input.json`, `context-omission-ledger.json`, `budget-plan.json`, candidate/evaluation/merge prompts, raw child JSONL events, stderr, responses, `blind-candidates.json`, `evaluation.json`, `merged.md`, and `error.json` for failed/cancelled runs. Persisted stage prompts are byte-identical to the exact bytes written to that child's stdin. `context-omission-ledger.json` carries the complete source-ordered omission ledger, and `budget-plan.json` records every configured route's capacity plus the pre-candidate feasibility decision, so a rejected run is as auditable as a successful one. Artifact files are written by private temp-file/fsync/rename, and v2 manifests persist cumulative child usage plus per-attempt observed usage/model data for successful, failed, and cancelled child attempts. Every usage record preserves the complete Pi cost breakdown; the same exact shape is cloned into `fusion_brainstorm` tool results so newer Pi hosts can calculate and replay footer/session statistics safely. These artifacts are local evidence only; they are not shown in `/jobs` or the background-task dock.
166
209
 
167
210
  For attested Pi tasks only, the task id is `b` plus 32 random hex characters (128 bits) and additional flat siblings are written in the same directory:
168
211
 
@@ -175,6 +218,23 @@ For attested Pi tasks only, the task id is `b` plus 32 random hex characters (12
175
218
 
176
219
  The attestation sidecar uses `schema_version: "phase2.pi_task_attestation.v1"` and is written last, after metadata/output/events/stderr/wrapper/report bytes are closed and hashed. An attested task does not become externally visible as `completed` until final metadata and the sidecar are durable. These are local runtime artifacts and should remain gitignored.
177
220
 
221
+ ## Durability model
222
+
223
+ All task metadata, task output/events/stderr, attestation sidecars, Fusion artifacts, and the global `fusion-models.json` are written through one shared primitive in `src/core/durable-fs.ts`.
224
+
225
+ The invariant is: **open the file once with write intent, write and `fsync` through that same writable handle, then close.** A pathname is never reopened merely to flush it. This is required for Windows correctness — `FlushFileBuffers` needs a handle with write access, so flushing through a reopened read-only handle fails with `EPERM: operation not permitted, fsync` and previously broke every background task on Windows.
226
+
227
+ Two write modes are provided:
228
+
229
+ | Mode | Flags | Used for |
230
+ |---|---|---|
231
+ | Direct durable write | `w`, inherited mode | task output, events, stderr |
232
+ | Atomic replacement | private `0o600` temp with `wx`, then rename | task metadata, attestations, Fusion artifacts/manifests, model config |
233
+
234
+ Failure handling is loud and typed. Every open, write, `fsync`, rename, close, and temp-cleanup failure raises `DurableFileError` carrying the failing operation, path, native error code, and original cause. **A failed `fsync` is never tolerated or downgraded**, so the package cannot report durable state it did not actually achieve. A close or cleanup failure is attached as a secondary `cleanupFailures` entry and never replaces the primary error, and a temporary file is only removed when that same call created it.
235
+
236
+ **Platform limitation.** After the rename, the parent directory is itself `fsync`ed on POSIX so the new directory entry is crash-durable. Node exposes no portable equivalent on Windows, so that step is skipped there: file contents are still explicitly flushed before the rename and rename failures remain fatal, but the package cannot offer the same crash-durability guarantee for the directory entry on Windows. When a failure occurs after a successful rename, `DurableFileError.renameCompleted` is `true`, meaning the replacement may already be visible even though the operation reported an error.
237
+
178
238
  ## Safety model
179
239
 
180
240
  - Commands are spawned and tracked with `child_process.spawn`; the package does not rely on shell `&`.
@@ -183,9 +243,21 @@ The attestation sidecar uses `schema_version: "phase2.pi_task_attestation.v1"` a
183
243
  - stdout/stderr are captured to task output files.
184
244
  - Model-visible logs are bounded and point to full output files.
185
245
  - POSIX process groups are used for process-tree kill where possible, with child-process fallback.
246
+ - On Windows, process-tree termination uses `%SystemRoot%\\System32\\taskkill.exe` directly (never `PATH`) with a two-stage `/T` then `/T /F` flow. Stage one is a logical Windows termination request, not SIGTERM and not a guaranteed graceful shutdown: console processes may ignore it, and re-parented descendants can escape the tree. Strong containment would require Windows Job Objects via a native component, which is out of scope.
186
247
  - Running tasks are cleaned up on Pi session shutdown/reload.
248
+ - Child Pi processes are never launched through a shell. On Windows the npm `pi.cmd` shim is deliberately not executed, because a batch shim cannot preserve argument bytes safely; the package resolves the Pi package's own CLI entry and launches it with `process.execPath` instead. Batch, PowerShell, and extensionless launch targets are rejected loudly rather than executed, and a launch target resolving outside the Pi package root is rejected.
187
249
  - Cross-Pi-restart process reattachment and Ctrl+B backgrounding of already-running foreground tools are intentionally out of scope.
188
250
 
251
+ ### Windows shell and telemetry
252
+
253
+ `cmd.exe` remains the default Windows shell, so existing commands written for `cmd` keep working unchanged. The generic `SHELL` variable is deliberately ignored on Windows, because honouring it would silently change the command language for `%VAR%`, `set`, `dir`, backslash paths, and quoting.
254
+
255
+ A POSIX shell is available as an explicit opt-in via `PI_BG_SHELL=bash`, with an optional absolute `PI_BG_SHELL_PATH`. Invalid values fail loudly rather than falling back. Bash is invoked with `-c` and never `-lc`, because a login shell runs profile scripts whose banner output would be captured into task output.
256
+
257
+ **Documented limitation:** `isAgent: true` Pi-agent telemetry wrapping works by installing a POSIX shell function that intercepts `pi`. That mechanism has no safe `cmd.exe` equivalent, so under the Windows `cmd` dialect the command is left byte-for-byte unchanged and telemetry is reported as unavailable with the reason `win32-cmd-cannot-safely-intercept-pi-argv`. Zero usage is never synthesized, and no warning text is injected into captured command output. Use `PI_BG_SHELL=bash` on Windows when agent telemetry is required.
258
+
259
+ On Windows, argument lists are checked against the 32,767-code-unit command-line limit before a child is created, so an oversized invocation fails loudly with the measured length rather than producing a confusing native spawn error. Argument contents are never included in that error.
260
+
189
261
  ## Development and QA
190
262
 
191
263
  Default QA gate:
package/TESTING.md CHANGED
@@ -43,12 +43,21 @@ Smoke/release checks:
43
43
 
44
44
  ```bash
45
45
  npm run smoke
46
+ npm run smoke:large-context
46
47
  npm run pack:dry-run
47
48
  npm run test:compat
48
49
  ```
49
50
 
50
51
  Current smoke is `tsx scripts/smoke.ts`. It creates a temporary Pi agent/session directory, sets offline/telemetry-suppression environment variables, and runs the package entrypoint with `/jobs`.
51
52
 
53
+ `npm run smoke:large-context` is the Fusion context-policy evidence harness. It rebuilds the byte composition of the production failure (696,929 B tool results, 251,508 B tool arguments, 34,959 B user text, 24,733 B assistant text, 10,303 B thinking) as a real `SessionManager` branch, then prints:
54
+
55
+ - the pre-fix full-transcript canonical input and the fact that it is rejected against the panel's smallest route;
56
+ - the post-fix projected canonical input with full omission accounting and a byte-identical rebuild check;
57
+ - the measured candidate, evaluator, evaluation-repair, and merger prompt sizes against the allowed input budget, using the largest real candidate answer (45,434 B) and evaluator output (54,829 B) observed in `.pi/fusion`.
58
+
59
+ It performs no inference and spawns no child, so it is safe to run offline and costs nothing. It exits non-zero if any stage would exceed the budget.
60
+
52
61
  Smoke proves loadability only; completion requires `npm run test`, `npm run test:full`, `npm run pack:dry-run`, and the release-only compatibility gate when preparing a release.
53
62
 
54
63
  ## Required isolated environment
@@ -77,13 +86,13 @@ npm run test:rpc
77
86
  npm run test:agent-loop
78
87
  ```
79
88
 
80
- The Fusion SDK/RPC/scripted-provider tests install a deterministic fake child `pi` in a temp `PATH` from `tests/helpers/fusion-fake-pi.ts`. Parent Pi remains the real SDK/RPC runtime; only direct child `pi --mode text` calls with the package-owned private compact metadata extension are intercepted. `PI_CODING_AGENT_DIR` is pointed at the temp agent directory so `fusion-models.json` is never read from the user's real global Pi directory. Fusion context coverage is intentionally text-transcript coverage: tests verify long text is not summarization-truncated and image-containing session entries are represented by `[Image omitted from fusion text transcript: <mime-type>]` markers without forwarding raw image bytes/base64 to child Pi prompts. The release-only `npm run test:compat` packs the package, installs exact supported Pi versions, runs `/jobs`, runs `/fusion` through the installed package entrypoint with the fake child Pi, verifies five child invocations, and verifies `/fusion-models` rejects non-TUI mode. It then drives the current host Pi through a real RPC `fusion_brainstorm` parent-agent loop, checks the persisted tool result carries the complete Pi `Usage.cost` object, invokes `get_session_stats` (the same aggregation boundary used by the TUI footer), reopens the durable session, and verifies identical token/cost totals. All parent and child inference remains deterministic and local.
89
+ The Fusion SDK/RPC/scripted-provider tests install a deterministic fake child `pi` in a temp `PATH` from `tests/helpers/fusion-fake-pi.ts`. Parent Pi remains the real SDK/RPC runtime; only direct child `pi --mode text` calls with the package-owned private compact metadata extension are intercepted. `PI_CODING_AGENT_DIR` is pointed at the temp agent directory so `fusion-models.json` is never read from the user's real global Pi directory. Fusion context coverage is conversation-projection coverage. `tests/unit/fusion-context-prompts.test.ts` verifies that a synthetic session carrying more than 1 MB of tool arguments/results still yields a small canonical input, that user and assistant text survive verbatim, that thinking and tool payloads never appear (including no head/tail/preview sentinel), that omission counts, byte totals, and hashes are exact and stable, that repeated construction is byte-identical, that the active `fusion_brainstorm` leaf and sibling calls stay scope-excluded, that images remain marker-only or ledger-only with no raw base64 in child prompts, and that every retained source block receives exactly one disposition. `tests/unit/fusion-budget.test.ts` covers stage budgets: the limiting model is the smallest configured route (including when it is the evaluator rather than a candidate), unknown or too-small capacities fail before spawn, boundary prompts pass at exactly the limit and fail one byte past it, the child system prompt counts as input, dense multi-byte UTF-8 cannot bypass byte accounting, and candidate, evaluator, evaluation-repair, and merger expansions are each rejected before their child is spawned with zero partial launches. `tests/package/typebox-compat.test.ts` pins the TypeBox posture and compiles nullable-array schemas. The release-only `npm run test:compat` packs the package, installs exact supported Pi versions, runs `/jobs`, runs `/fusion` through the installed package entrypoint with the fake child Pi, verifies five child invocations, verifies `/fusion-models` rejects non-TUI mode, asserts the resolved `typebox` is Pi's bundled peer rather than a private or nested copy, and scans the installed package bytes for TypeBox APIs removed in the 1.3.x line. It then drives the current host Pi through a real RPC `fusion_brainstorm` parent-agent loop, checks the persisted tool result carries the complete Pi `Usage.cost` object, invokes `get_session_stats` (the same aggregation boundary used by the TUI footer), reopens the durable session, and verifies identical token/cost totals. All parent and child inference remains deterministic and local.
81
90
 
82
91
  ## Coverage summary
83
92
 
84
93
  Implemented coverage includes:
85
94
 
86
- - tools: `bg_run`, `bg_run_pi_attested`, `bg_status`, `bg_logs`, `bg_kill`, `fusion_brainstorm`, including required `isAgent` schema/runtime validation, the event-driven no-sleep/no-poll system-prompt contract, truthful launch receipts for all four notification/wake combinations, non-terminating `bg_run` compatibility, point-in-time status/log guidance, durable terminal-notification authority, attested direct Pi spawn validation, Fusion exact merged tool result delivery/progress/details/context exclusion, text-transcript image omission markers with raw image data excluded from child prompts, unknown/ambiguous IDs, completed-kill failure, legacy no-name preparation, head/tail truncation, and notification on/off behavior
95
+ - tools: `bg_run`, `bg_run_pi_attested`, `bg_status`, `bg_logs`, `bg_kill`, `fusion_brainstorm`, including required `isAgent` schema/runtime validation, the event-driven no-sleep/no-poll system-prompt contract, truthful launch receipts for all four notification/wake combinations, non-terminating `bg_run` compatibility, point-in-time status/log guidance, durable terminal-notification authority, attested direct Pi spawn validation, Fusion exact merged tool result delivery/progress/details/context exclusion, versioned conversation projection with explicit hash-accounted tool/thinking omissions, pre-spawn stage budget rejection for all four expansion stages, image omission markers with raw image data excluded from child prompts, unknown/ambiguous IDs, completed-kill failure, legacy no-name preparation, head/tail truncation, and notification on/off behavior
87
96
  - commands: `/bg`, `/jobs`, `/logs`, `/kill`, `/tasks`, `/bg-tasks`, `/bg-clear`, `/bg-update`, `/fusion`, `/fusion-models` discovery, happy paths, `/fusion` direct custom-message delivery, `/fusion` editor/cancel flow, `/fusion-models` TUI save and non-TUI rejection, `/bg --agent` parsing, finished-notice clearing, malformed `/bg`, unknown/ambiguous IDs, completed-task `/kill`, byte-limit normalization, and RPC no-hang fallback behavior
88
97
  - update-available notice: semver parse/compare/precedence, `formatUpdateSegment`, npm/`package.json` payload narrowing, and injected-fetch success/404/throw/timeout (unit); localhost-registry footer segment (idle + appended to an active footer), `/bg-update` non-installing instructions, and opt-out/offline/already-current/registry-failure no-segment-and-no-throw paths (SDK); `/bg-update` discovery and offline instructions (RPC). The check is one-shot on `session_start`, time-boxed, offline-safe, gated by `PI_OFFLINE`/`PI_BG_DISABLE_UPDATE_CHECK`, and `PI_BG_REGISTRY_URL` overrides the registry endpoint
89
98
  - shortcut/UI: component coverage for focused dock list/detail/key handling, detail output-tail scrolling (arrow/page scroll, follow-pause-on-scroll, `lines X–Y of N` position indicator, resume-follow-at-bottom, and no-scroll when output fits), empty/history/unread states, paging, close aliases, stop/stop-all/rerun/path actions, missing output files; SDK coverage for explicit `/bg-clear` finished-notice clearing, `/bg-clear` footer hinting, optional `Ctrl+Alt+C` fallback shortcut registration, and mixed failed/stopped/done/focused footer status; RPC coverage that `/bg-clear` works as a terminal-independent clear path; and PTY coverage for `/tasks`, `/bg-tasks`, real `Shift+Down`, arrows, page keys, detail/back/history/stop/stop-all/rerun/path/close, failed unread badges, and running/completed/failed/killed rerun paths
@@ -91,7 +100,8 @@ Implemented coverage includes:
91
100
  - extension EventBus API: unit coverage for `pi-background-tasks:request:v1`/`response:v1` closed-frame validation, exact capability handshake, malformed payload rejection, unknown keys, unknown operations, duplicate request IDs, missing `session_start`, shutdown refusal, unsubscribe, strict terminal frame shape, and response-barrier ordering; registry coverage for exactly-one terminal publication after durable metadata plus loud/retriable terminal delivery failure; SDK coverage with a shared real `createEventBus()` loading the actual extension, starting `printf api-ok`, reading bounded logs, observing exactly one terminal event after the run response, and killing a real sleep task without model/provider calls
92
101
  - attested Pi producer: unit/SDK coverage for 128-bit attested task ids, exact direct argv/cwd, ModelRegistry OAuth observation without secrets, raw Pi session/message events, separate stderr, prompt/report/source hashes, authority start/finish commit/tree/clean checks, atomic metadata serialization, completion visibility only after the sidecar is durable, malformed event rejection, and no attestation sidecar for ordinary tasks
93
102
  - agent activity transcript: pure `parseAgentActivity`/`formatAgentActivityLine` coverage (assistant text, reasoning, tool start with arg summary, silent successful tool end, `✗ tool failed` errors, truncation, invalid/non-activity narrowing); registry-unit coverage that wrapped-agent stdout is reconstructed across split chunks into the human-readable transcript while telemetry/activity control JSON is stripped from the output file (telemetry fields still updated), stderr passes through, and the trailing partial line is flushed on finalize; SDK coverage that fake and real child `pi --mode json` runs surface `→ tool`/`✗ tool failed`/assistant text in `bg_logs` with no control JSON leaking into the visible output
94
- - safety: kill, already-finished kill failure, timeout failure, spawn failure, low output-cap failure, multi-task shutdown cleanup, process-group kill fallback, Windows child-kill behavior, SIGKILL escalation, duplicate finalization/notification races, metadata/notification failure handling, and pruning
103
+ - durability: `tests/unit/durable-fs.test.ts` covers the shared `src/core/durable-fs.ts` primitive through an injectable operations seam plus real-filesystem integration — single-open write/sync/close ordering, exclusive `wx` temp creation at `0o600`, direct `w` writes with inherited mode, the rule that a pathname is never reopened to flush it, temp ownership (a colliding temp created by another writer is never deleted), primary-versus-cleanup error precedence for write/sync/close/rename/remove failures, `renameCompleted` after a post-rename directory failure, the Windows directory-sync skip, atomic concurrent replacement, and fatal (never tolerated) `fsync` failures. `tests/package/package.test.ts` adds a mutation guard that fails if a read/read-write handle is used for `.sync()`, a path-based `fsyncFile` helper returns, or a sync failure is silently swallowed
104
+ - safety: kill, already-finished kill failure, timeout failure, spawn failure, low output-cap failure, multi-task shutdown cleanup, POSIX process-group kill fallback, Windows `taskkill /T` then `/T /F` tree termination with shared soft attempts, soft-abort-on-force, exit-128 race tolerance, loud force failures, no root-only fallback, SIGKILL escalation that terminates instead of re-arming (a SIGKILL never schedules a further escalation, and concurrent stop requests share exactly one escalation timer that is cleared on finalize), duplicate finalization/notification races, metadata/notification failure handling, and pruning
95
105
  - agent loop: deterministic scripted-provider coverage against `extensions/background-tasks.ts` for actual event-driven `bg_run` behavior. The provider observes the effective system prompt, public tool descriptions, and real launch receipt and deliberately emits the pre-fix `bg_status` poll if any contract layer is absent; the passing path proves one launch, no sleep/status/log polling, one durable terminal notification, and exactly one follow-up turn. It also covers notification-only `triggerOnCompletion:false`, `/bg` display-only behavior, `notifyOnCompletion:false`, failed-task notification error fields, and parent-model `fusion_brainstorm` tool use followed by normal parent response
96
106
  - package: manifest, docs, `pi.extensions`, exported `src/core/extension-api.ts`, peer dependency/import parity, packed runtime files, tarball-install smoke, direct-completion import bans, test/helper/script/artifact exclusion, isolated offline npm installation, exact-version compatibility, and current-host persisted/replayed tool-usage safety
97
107
 
package/TEST_PLAN.md CHANGED
@@ -18,7 +18,7 @@ This package follows:
18
18
  | Shortcuts | `Shift+Down`; optional fallback `Ctrl+Alt+C` |
19
19
  | Custom UI | footer status + focused bottom dock overlay |
20
20
  | Custom provider | no |
21
- | Runtime files/state | `.pi/tasks/<session-id>-<pid>/<task-id>.output`, `.pi/tasks/<session-id>-<pid>/<task-id>.json`; attested Pi opt-in adds `.pi-events.jsonl`, `.stderr`, `.pi-telemetry-wrapper.cjs`, `.attestation.json`; Fusion adds private `.pi/fusion/<session-id>-<pid>/<run-id>/` manifests/prompts/events/stderr/responses/evaluation/merged/error artifacts plus global `fusion-models.json` |
21
+ | Runtime files/state | `.pi/tasks/<session-id>-<pid>/<task-id>.output`, `.pi/tasks/<session-id>-<pid>/<task-id>.json`; attested Pi opt-in adds `.pi-events.jsonl`, `.stderr`, `.pi-telemetry-wrapper.cjs`, `.attestation.json`; Fusion adds private `.pi/fusion/<session-id>-<pid>/<run-id>/` manifests/prompts/events/stderr/responses/evaluation/merged/error artifacts plus `context-omission-ledger.json` and `budget-plan.json` plus global `fusion-models.json` |
22
22
 
23
23
  ## Required gates
24
24
 
@@ -34,7 +34,8 @@ This package follows:
34
34
  | Scripted provider | `npm run test:agent-loop` | full gate | implemented |
35
35
  | Pack dry run | `npm run pack:dry-run` | release gate | implemented |
36
36
  | Smoke | `npm run smoke` | no | implemented; isolated load-only |
37
- | Compatibility | `npm run test:compat` | release gate | implemented; exact Pi `0.75.5`, `0.81.1`, `0.82.1` pack/install plus `/jobs`, `/fusion`, and `/fusion-models` surfaces, followed by a current-host real `fusion_brainstorm` session-stat and replay witness |
37
+ | Large-context smoke | `npm run smoke:large-context` | release gate | implemented; rebuilds the production failure byte composition, proves the pre-fix input is rejected and the post-fix projection fits all four stages against the smallest configured route, with no inference and no child spawn |
38
+ | Compatibility | `npm run test:compat` | release gate | implemented; exact Pi `0.75.5`, `0.81.1`, `0.82.1`, `0.83.0` pack/install plus `/jobs`, `/fusion`, and `/fusion-models` surfaces, per-version bundled-TypeBox peer verification, removed-TypeBox-API scan of installed package bytes, followed by a current-host real `fusion_brainstorm` session-stat and replay witness |
38
39
 
39
40
  ## Feature coverage matrix
40
41
 
@@ -50,8 +51,11 @@ This package follows:
50
51
  | Inspect task status | `bg_status` | | yes | | | | | yes | SDK uses exact IDs for deterministic lifecycle observations and verifies the model-facing contract calls status a point-in-time inspection rather than a wait primitive. Scripted-provider ordinary waiting makes zero status calls. |
51
52
  | Read task logs | `bg_logs` | yes | yes | | | | | yes | SDK verifies bounded content and the model-facing no-wait-loop contract. Scripted-provider ordinary waiting makes zero log calls. |
52
53
  | Stop task from LLM tool | `bg_kill` | | yes | | | | | | Covers running kill and already-finished loud failure. |
53
- | Fusion command direct result | `/fusion`, `fusion-result` custom message | yes | yes | yes | | yes | yes | | Core unit covers deterministic full text-transcript context construction, explicit image omission markers without raw image data, direct final-text child argv/stdin/compact-metadata parsing, exact multi-block answer reconstruction, artifacts, pre-abort handling, and orchestration. SDK runs real `/fusion` against a fake child `pi`, verifies exactly five child text-mode invocations, hidden prompt persistence, visible exact merged custom message, no parent assistant rewrite, renderer output, progress/status behavior, no-argument editor flow, editor cancellation, and malformed config causing zero children. RPC verifies command discovery, `/fusion <prompt>` with U+2028/U+2029 content, custom-message result emission, no parent `agent_start`, no-argument editor protocol, malformed config/child failure visibility, and child isolation flags. PTY verifies `/fusion` renders the exact merged answer directly in a real TUI. Compatibility smoke runs `/fusion` through every supported Pi version with the installed package entrypoint. |
54
+ | Fusion command direct result | `/fusion`, `fusion-result` custom message | yes | yes | yes | | yes | yes | | Core unit covers deterministic v2 conversation-projection construction, explicit image omission markers without raw image data, direct final-text child argv/stdin/compact-metadata parsing, exact multi-block answer reconstruction, artifacts, pre-abort handling, and orchestration. SDK runs real `/fusion` against a fake child `pi`, verifies exactly five child text-mode invocations, hidden prompt persistence, visible exact merged custom message, no parent assistant rewrite, renderer output, progress/status behavior, no-argument editor flow, editor cancellation, and malformed config causing zero children. RPC verifies command discovery, `/fusion <prompt>` with U+2028/U+2029 content, custom-message result emission, no parent `agent_start`, no-argument editor protocol, malformed config/child failure visibility, and child isolation flags. PTY verifies `/fusion` renders the exact merged answer directly in a real TUI. Compatibility smoke runs `/fusion` through every supported Pi version with the installed package entrypoint. |
54
55
  | Fusion tool result | `fusion_brainstorm({prompt})` | yes | yes | | | current-host stats/replay | yes | yes | Tool is registered at load and re-added on `session_start`; no eligibility/quota/routine/justification gates are implemented. BUG-182 unit/SDK coverage pins the exact host `Usage` contract, complete cost-component aggregation, rejection of legacy `costTotal`, v2 child/result/manifest schemas, exact merged text, progress, context exclusion, and final details. The release compatibility gate drives a real current-host RPC agent through the tool, verifies persisted tool usage, invokes the same session-stat path used by the footer, then reopens the durable session and verifies identical stats. Scripted-provider coverage proves normal parent consumption and evaluator schema repair. |
56
+ | Fusion conversation projection | canonical input `fusion-input.v2`, `context-omission-ledger.json` | yes | yes | yes | | | yes | | Unit covers a >1 MB synthetic tool-heavy session staying within budget, verbatim user/assistant text, thinking exclusion, zero tool-payload preview bytes (head/tail sentinels absent), exact and stable omission counts/byte totals/hashes, byte-identical repeated construction, hash change on omitted-payload mutation without exposing the payload, contiguous omission-run collapsing with dense source-ordered ledger indices, active-tool-call-leaf and sibling-call exclusion, marker-only user images plus ledger-only tool-result images with no base64, per-entry-point policy ids and request authority, and the exactly-one-disposition property for every retained block. SDK verifies the exact child stdin carries `conversation_projection` and never `conversation_transcript`. RPC verifies the v2 request object. Package tests assert the ledger artifact ships in the documented artifact set. |
57
+ | Fusion stage budgets | `budget-plan.json`, typed `prompt_budget_exceeded` | yes | | | | | | | Unit covers the conservative bytes-per-token bound, smallest-configured-route limiting selection (candidate or evaluator), rejection of unknown/zero/negative/too-small context windows, the reserved canonical-input share, boundary accept at exactly the limit and reject one byte past, the child system prompt counted as input, multi-byte UTF-8 byte accounting, pre-spawn rejection with zero children for candidate, evaluator, evaluation-repair, and merger expansion, safe prompts completing all five calls, persisted route/plan snapshots including negative slack on rejection, and the reproduced 1 MB failure shape now fitting the smallest configured budget. Errors carry stage, measured size, allowed size, limiting model, and remediation in both structured detail and message text. |
58
+ | Pi/TypeBox compatibility | `peerDependencies`, packed bytes | yes | | | | | yes | | Unit pins typebox as a `"*"` peer that is neither a runtime nor bundled dependency, requires the resolved TypeBox to be Pi 0.83's 1.3.x line, requires all four supported Pi/TUI lines in the peer range, scans all package TypeScript for the seven removed TypeBox APIs, and compiles the shipped tool schema plus nullable-array/nullable-string and optional-field shapes under TypeBox 1.3. The release compat gate repeats the removed-API scan against installed package bytes per Pi version. |
55
59
  | 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. |
56
60
  | Fusion child isolation/lifecycle | child `pi --mode text`, private compact metadata extension, shutdown cleanup, `.pi/fusion` artifacts | yes | yes | yes | | current-host stats/replay | yes | | BUG-180 coverage proves final-text transport, reasoning exclusion, strict response reconstruction, unchanged caps, isolation, process cleanup, failure artifacts, and lifecycle semantics. BUG-182 upgrades the private compact metadata and artifact manifest to v2, preserves all token and cost components, centralizes clone/add/empty operations, rejects missing/legacy/unknown/non-finite cost shapes, and proves successful plus failed/cancelled attempt costs aggregate exactly once. SDK/RPC verify public child isolation; session shutdown tracks initializing and live runs. |
57
61
  | Extension request/response service | `pi-background-tasks:request:v1` → `pi-background-tasks:response:v1` | yes | yes | | | | yes | | Unit covers closed-frame validation, capability handshake, unknown keys, unknown operation, duplicate request IDs, missing `session_start`, shutdown refusal, strict `run.payload`, strict malformed frames, and unsubscribe. SDK loads the real extension with a shared `createEventBus()`, starts `printf api-ok`, reads bounded logs, lists status, starts and kills a real sleep task, and checks malformed/unknown/duplicate controls without model/provider calls. Package tests assert `src/core/extension-api.ts` ships. |
@@ -73,12 +77,13 @@ This package follows:
73
77
  | Shortcut opens dock | `Shift+Down` | | registration | | | yes | | | PTY sends xterm `ESC [ 1 ; 2 B`. |
74
78
  | Clear finished notices | `/bg-clear`, optional `Ctrl+Alt+C` fallback | | yes | yes | | | | | `/bg-clear` is the canonical terminal-independent path and is advertised in the footer. SDK invokes the slash-command handler and verifies fallback shortcut registration; RPC verifies `/bg-clear` clears finished notices; finished notices remain until explicit clear. |
75
79
  | Update-available footer notice | `⬆ v<latest> /bg-update` footer segment + `/bg-update` command | yes | yes | yes | | | | | Unit covers semver parse/compare/precedence, `isNewerVersion`, `formatUpdateSegment`, npm/`package.json` payload narrowing, injected-fetch success/404/throw/timeout, and `package.json` read/degrade. SDK uses a localhost registry to verify the idle and append-to-active footer segment, `/bg-update` non-installing instructions, and that opt-out (`PI_BG_DISABLE_UPDATE_CHECK=1`), offline (`PI_OFFLINE=1`), already-current, and registry-failure paths render no segment and never throw. RPC verifies `/bg-update` discovery and offline non-installing instructions. The check is one-shot per `session_start`, time-boxed, offline-safe, and never runs on the status tick. |
80
+ | Durable file writes | `src/core/durable-fs.ts` (task metadata, output/events/stderr, attestations, Fusion artifacts, `fusion-models.json`) | yes | | | | | mutation guard | | Unit covers single-open write/sync/close ordering, exclusive `wx` temp at `0o600`, direct `w` writes with inherited mode, never reopening a pathname to flush it, temp ownership (never deleting another writer's colliding temp), primary-versus-cleanup error precedence across write/sync/close/rename/remove, `renameCompleted` after post-rename directory failure, the Windows directory-sync skip, real-filesystem overwrite/atomic-replace/concurrency, and fatal `fsync` failures. Package mutation guard fails if a read/read-write handle is used for `.sync()`, a path-based `fsyncFile` returns, or a sync failure is silently swallowed. Fixes the Windows `EPERM: operation not permitted, fsync` reported in PR #1. |
76
81
  | Runtime output files | `.pi/tasks/...output` | yes | yes | | | | | | SDK asserts existence. |
77
82
  | Runtime metadata files | `.pi/tasks/...json` | yes | yes | | | | | | SDK asserts shape/status/name/context usage; registry unit tests cover metadata failure/update ordering. |
78
83
  | Timeout kills task | `timeoutSeconds` | | yes | | | | | | SDK. |
79
84
  | Output cap kills task | `PI_BG_MAX_OUTPUT_BYTES` | | | yes | | | | | RPC runs with a low cap and asserts failed status/log notice. |
80
85
  | Shutdown cleanup | `session_shutdown` | yes | yes | | | | | | SDK asserts multiple running tasks become killed; registry tests cover shared stop/wait behavior. |
81
- | Process lifecycle/races | registry core | yes | yes | yes | | yes | | yes | Unit tests cover process-group fallback, Windows fallback, SIGKILL escalation, duplicate finalization/notification races, notification/metadata failures, pruning, malformed telemetry, split telemetry chunks, large telemetry records above the old 16KiB buffer, and wrapped-agent transcript/telemetry separation with split-chunk and trailing-partial flush; SDK/RPC cover runtime spawn/timeout/output-cap/shutdown; scripted provider covers wakeup integration. |
86
+ | Process lifecycle/races | registry core | yes | yes | yes | | yes | | yes | Unit tests cover POSIX process-group fallback, Windows `taskkill /T` then `/T /F` tree termination, SIGKILL escalation, duplicate finalization/notification races, notification/metadata failures, pruning, malformed telemetry, split telemetry chunks, large telemetry records above the old 16KiB buffer, and wrapped-agent transcript/telemetry separation with split-chunk and trailing-partial flush; SDK/RPC cover runtime spawn/timeout/output-cap/shutdown; scripted provider covers wakeup integration. |
82
87
  | Package manifest | `package.json` | | | | | | yes | | Keywords, `pi.extensions`, files, local smoke/compat scripts, and direct-completion import bans. |
83
88
  | Pack contents | `npm pack --dry-run` | | | | | | yes | | Runtime files included; tests, fake child helpers, release-only scripts, artifacts, nested tarballs, and `node_modules` excluded. Default tarball install test uses isolated HOME/XDG/npm cache and offline peer-ignore install from the local tarball. |
84
89
 
@@ -91,7 +96,7 @@ Lane A residual hardening is now covered by automated tests. No remaining harden
91
96
  | Extracted process registry | `src/core/registry.ts` has direct unit coverage for state transitions and injected spawn/kill/platform behavior. |
92
97
  | Agent/script classification | Unit tests cover `isAgent:true` wrapping, `isAgent:false` non-wrapping, `PI_BG_DISABLE_PI_TELEMETRY`, and non-interceptable path-qualified `pi`; SDK verifies required tool schema/runtime validation and real marked Pi telemetry. |
93
98
  | Process lifecycle/races | Unit tests cover duplicate error/close finalization, output-cap races, duplicate-notification prevention, waiter resolution via stop paths, metadata failure logging, and notification failure reset. |
94
- | Process-tree kill safety | Unit tests cover POSIX process-group kill, child fallback, both-fail loud errors, SIGTERM idempotency, SIGKILL escalation, and Windows child-kill/shell invocation. |
99
+ | Process-tree kill safety | Unit tests cover POSIX process-group kill, child fallback, both-fail loud errors, SIGTERM idempotency, SIGKILL escalation, terminal-SIGKILL semantics (a SIGKILL escalation never re-arms another timer, so a child outliving the grace window cannot produce an unbounded SIGKILL loop), single-escalation-timer behaviour for concurrent stop requests, Windows `taskkill /T` then `/T /F` ordering, duplicate graceful sharing, soft-abort-on-force, explicit force without a timer, exit-128 race recording, soft-failure escalation, loud force failure, no Windows root-only fallback, bounded helper output capture, System32 resolution with no `PATH`, and terminal metadata waiting for in-flight force. |
95
100
  | Pruning | Unit tests cover oldest-finished pruning while preserving running tasks. |
96
101
  | Completion follow-up turns | `test:agent-loop` registers a deterministic scripted provider against the shipped extension path and verifies event-driven `bg_run` yielding with no sleep/status/log polling, exactly one default wakeup, notification-only `triggerOnCompletion:false`, `/bg` display-only behavior, `notifyOnCompletion:false`, and failed notification error fields. |
97
102
  | PTY secondary keys | `test:pty` covers arrows, page keys, `a`/`K`, `R`, `c`, `/bg-tasks`, failed/unread badges, multiple-task ordering, and rerun paths for running/completed/failed/killed tasks. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-background-tasks",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "Pi extension package for background shell tasks plus five-model fusion brainstorming: bg_run tools, /bg commands, /fusion, fusion_brainstorm, model selector UI, bounded logs, kill/timeout safety, and completion wakeups.",
5
5
  "type": "module",
6
6
  "license": "ISC",
@@ -47,9 +47,11 @@
47
47
  "test:package": "tsx --test tests/package/**/*.test.ts",
48
48
  "test": "npm run typecheck && npm run test:type-safety && npm run test:unit && npm run test:sdk && npm run test:rpc && npm run test:component && npm run test:package",
49
49
  "test:pty": "tsx --test tests/pty/**/*.test.ts",
50
+ "test:windows": "tsx --test tests/windows/**/*.test.ts",
50
51
  "test:agent-loop": "tsx --test --test-concurrency=1 tests/scripted-provider/**/*.test.ts",
51
52
  "test:full": "npm run test && npm run test:pty && npm run test:agent-loop",
52
53
  "smoke": "tsx scripts/smoke.ts",
54
+ "smoke:large-context": "tsx scripts/large-context-smoke.ts",
53
55
  "pack:dry-run": "npm pack --dry-run",
54
56
  "lint": "cd ../.. && npm run quality:ts:lint",
55
57
  "format:check": "cd ../.. && npm run quality:ts:format",
@@ -61,17 +63,17 @@
61
63
  ]
62
64
  },
63
65
  "peerDependencies": {
64
- "@earendil-works/pi-coding-agent": "^0.75.5 || ^0.81.1 || ^0.82.1",
65
- "@earendil-works/pi-tui": "^0.75.5 || ^0.81.1 || ^0.82.1",
66
- "typebox": "^1.1.38"
66
+ "@earendil-works/pi-coding-agent": "^0.75.5 || ^0.81.1 || ^0.82.1 || ^0.83.0",
67
+ "@earendil-works/pi-tui": "^0.75.5 || ^0.81.1 || ^0.82.1 || ^0.83.0",
68
+ "typebox": "*"
67
69
  },
68
70
  "devDependencies": {
69
- "@earendil-works/pi-ai": "^0.75.5",
70
- "@earendil-works/pi-coding-agent": "^0.75.5",
71
- "@earendil-works/pi-tui": "^0.75.5",
71
+ "@earendil-works/pi-ai": "^0.83.0",
72
+ "@earendil-works/pi-coding-agent": "^0.83.0",
73
+ "@earendil-works/pi-tui": "^0.83.0",
72
74
  "@types/node": "^24.0.0",
73
75
  "tsx": "^4.19.0",
74
- "typebox": "^1.1.38",
76
+ "typebox": "^1.3.7",
75
77
  "typescript": "^5.9.0"
76
78
  },
77
79
  "engines": {
@@ -1,8 +1,7 @@
1
1
  import { spawn as nodeSpawn, type SpawnOptions } from 'node:child_process';
2
2
  import { createHash, randomBytes } from 'node:crypto';
3
- import { closeSync, fsyncSync, openSync, renameSync, writeFileSync } from 'node:fs';
4
- import { mkdir, open, readFile, realpath, stat, writeFile } from 'node:fs/promises';
5
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
3
+ import { mkdir, readFile, realpath, stat } from 'node:fs/promises';
4
+ import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
6
5
  import type { Api, Model } from '@earendil-works/pi-ai';
7
6
  import type {
8
7
  BackgroundTaskChildProcess,
@@ -10,6 +9,13 @@ import type {
10
9
  BackgroundTaskSpawn,
11
10
  } from './registry.js';
12
11
  import { isJsonObject, parseJsonText, type BgTaskSnapshot, type JsonObject } from './common.js';
12
+ import { replaceFileDurable, writeFileDurable } from './durable-fs.js';
13
+ import {
14
+ assertWindowsCommandLineWithinLimit,
15
+ piLaunchArgv,
16
+ resolvePiLaunch,
17
+ type PiLaunchSpec,
18
+ } from './pi-launch.js';
13
19
 
14
20
  export const PI_TASK_ATTESTATION_SCHEMA_VERSION = 'phase2.pi_task_attestation.v1';
15
21
  export const ATTESTED_TASK_ID_PATTERN = /^b[0-9a-f]{32}$/;
@@ -414,40 +420,13 @@ function sortJson(value: unknown): unknown {
414
420
  );
415
421
  }
416
422
 
417
- async function fsyncFile(path: string): Promise<void> {
418
- const handle = await open(path, 'r');
419
- try {
420
- await handle.sync();
421
- } finally {
422
- await handle.close();
423
- }
424
- }
425
-
426
- async function fsyncDirectory(path: string): Promise<void> {
427
- if (process.platform === 'win32') return;
428
- const fd = openSync(path, 'r');
429
- try {
430
- fsyncSync(fd);
431
- } finally {
432
- closeSync(fd);
433
- }
434
- }
435
-
436
423
  export async function writeFileFsynced(path: string, data: Buffer | string): Promise<void> {
437
424
  await mkdir(dirname(path), { recursive: true });
438
- await writeFile(path, data);
439
- await fsyncFile(path);
425
+ await writeFileDurable(path, data);
440
426
  }
441
427
 
442
428
  export async function writeJsonAtomic(path: string, value: unknown): Promise<void> {
443
- const tmp = join(
444
- dirname(path),
445
- `.${basename(path)}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`,
446
- );
447
- writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
448
- await fsyncFile(tmp);
449
- renameSync(tmp, path);
450
- await fsyncDirectory(dirname(path));
429
+ await replaceFileDurable(path, `${JSON.stringify(value, null, 2)}\n`);
451
430
  }
452
431
 
453
432
  export async function closeAndFsyncOutputStream(
@@ -481,12 +460,17 @@ export function spawnAndCapturePi(
481
460
  spawnImpl: BackgroundTaskSpawn,
482
461
  argv: string[],
483
462
  options: SpawnOptions,
463
+ platform: NodeJS.Platform = process.platform,
464
+ launchOverride?: PiLaunchSpec | undefined,
484
465
  ): { child: BackgroundTaskChildProcess; stdoutChunks: Buffer[]; stderrChunks: Buffer[] } {
485
466
  const stdoutChunks: Buffer[] = [];
486
467
  const stderrChunks: Buffer[] = [];
487
- const executable = argv[0];
488
- if (!executable) throw new Error('Attested Pi argv is empty');
489
- const child = spawnImpl(executable, argv.slice(1), options);
468
+ const logicalExecutable = argv[0];
469
+ if (logicalExecutable !== 'pi') throw new Error('Attested Pi argv must start with pi');
470
+ const piArgs = argv.slice(1);
471
+ const launch = launchOverride ?? resolvePiLaunch({ platform });
472
+ assertWindowsCommandLineWithinLimit(launch, piArgs, platform, 'attested-pi-run');
473
+ const child = spawnImpl(launch.executable, piLaunchArgv(launch, piArgs), options);
490
474
  child.stdout?.on('data', (chunk: Buffer | string) => {
491
475
  stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, 'utf8'));
492
476
  });