pi-tian-background-terminals 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tian Zuo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # pi-tian-background-terminals
2
+
3
+ A managed replacement for Pi's built-in `bash` tool.
4
+
5
+ Every model shell command follows one path: start it, wait briefly, and return
6
+ its final output if it finishes. If it outlives the initial wait, return control
7
+ to the model while the command continues as a session-scoped background
8
+ terminal. Its final result is delivered automatically exactly once.
9
+
10
+ ```text
11
+ ■ 2 background terminals running • /ps to view
12
+ ```
13
+
14
+ ## Canonical `bash` override
15
+
16
+ Pi officially supports replacing a built-in tool by registering the same name.
17
+ This extension registers `bash`, so the model no longer chooses between normal
18
+ Bash and a separate background tool. Pi may display an expected startup warning
19
+ that the built-in has been overridden.
20
+
21
+ Parameters:
22
+
23
+ - `command` — Bash script to execute.
24
+ - `timeout` — optional hard total runtime timeout in seconds. When reached, the
25
+ whole process tree is terminated. There is no default runtime timeout.
26
+ - `working_dir` — optional working directory; defaults to the current directory.
27
+ - `title` — optional short `/ps` label; defaults to a bounded one-line command.
28
+ - `yield_time_ms` — optional initial wait, default **10 seconds**, range
29
+ **250–30,000 ms**.
30
+
31
+ Behavior:
32
+
33
+ 1. Resolve Bash using Pi's normal platform logic and preserve Pi's configured
34
+ `shellPath` and `shellCommandPrefix` settings.
35
+ 2. Inject the same `PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL`,
36
+ and `PI_REASONING_LEVEL` environment values as built-in Bash.
37
+ 3. Start the command with no interactive stdin and stream bounded progress in
38
+ the normal Bash tool row.
39
+ 4. If it exits during `yield_time_ms`, return its final status and bounded
40
+ head+tail output. Non-zero exits and hard timeouts are Bash tool errors.
41
+ 5. If it remains alive, return an id such as `bt-1`. The model should continue
42
+ working rather than poll. A follow-up message wakes it exactly once when the
43
+ process exits.
44
+
45
+ There are no model-facing status, list, kill, polling, or stdin tools. The user
46
+ owns inspection and termination through `/ps`.
47
+
48
+ ### Safe foreground fallback
49
+
50
+ If the managed Effect runtime cannot initialize—or `start()` returns a typed
51
+ spawn error proving no child was created—the extension falls back to Pi's
52
+ standard foreground Bash implementation. The result includes a warning that
53
+ automatic yielding and `/ps` tracking were unavailable.
54
+
55
+ The extension deliberately never retries through fallback after a spawn,
56
+ non-zero exit, timeout, or abort. Re-executing an arbitrary shell command could
57
+ duplicate destructive side effects.
58
+
59
+ ## `/ps` viewer
60
+
61
+ While at least one terminal runs, a one-line widget renders above the editor.
62
+ `/ps` opens a two-stage full-screen overlay:
63
+
64
+ 1. **List** — every tracked terminal; `↑/↓`/`j`/`k` select, `Enter` inspect,
65
+ `x` stop the selected running terminal, `Esc` close.
66
+ 2. **Detail** — metadata, `t`-toggled stdout/stderr, live tailing, scrolling
67
+ (`↑/↓`, `PgUp/PgDn`, `g`/`G`), and `x` to stop.
68
+
69
+ ## Design
70
+
71
+ - **Automatic yielding, no polling.** Quick commands return directly; only
72
+ commands that outlive the initial wait become background work. Completion
73
+ uses `pi.sendMessage(..., { deliverAs: "followUp", triggerTurn: true })`.
74
+ - **Exactly-once completion.** A race-safe waiter token decides whether the
75
+ initial Bash call or the later follow-up owns settlement. A drain-once map
76
+ handles delivery retries without duplicates.
77
+ - **Bounded head+tail memory plus full capture.** Each stdout/stderr stream
78
+ retains a stable **256 KiB startup head** and rolling tail within a **2 MiB**
79
+ cap. Omitted middle bytes are marked. Complete output spills from byte zero
80
+ to an owner-only file (`0600` in a `0700` session directory), capped at
81
+ 256 MiB per stream.
82
+ - **Separate stdout and stderr.** Both streams are independently retained,
83
+ spilled, inspected, and formatted.
84
+ - **No interactive stdin.** Normal commands see EOF. The legacy WSL Bash
85
+ transport may receive the script over stdin, but that pipe is closed
86
+ immediately and cannot be used interactively.
87
+ - **Process-tree termination.** POSIX children use their own process group;
88
+ Windows uses `taskkill /T`. Shutdown and hard timeouts send SIGTERM and
89
+ escalate to SIGKILL after two seconds.
90
+ - **Session scoped.** `/new`, `/resume`, `/fork`, `/reload`, and quit terminate
91
+ every process tree and remove spill files.
92
+
93
+ The async core uses [Effect](https://effect.website) v4. Node
94
+ `child_process` output remains callback-driven. See
95
+ [`docs/implementation-guide.md`](./docs/implementation-guide.md) for internal
96
+ invariants.
97
+
98
+ ## Install
99
+
100
+ ```bash
101
+ pi install npm:pi-tian-background-terminals
102
+ ```
103
+
104
+ Restart Pi or run `/reload` afterwards.
105
+
106
+ ## Development
107
+
108
+ This workspace uses Effect v4 and TypeScript 7 (`tsgo`), so it is checked in
109
+ isolation:
110
+
111
+ ```bash
112
+ npm install -w pi-tian-background-terminals
113
+ cd packages/pi-background-terminals
114
+ npm run check
115
+ npm test
116
+ ```
117
+
118
+ ## Credits
119
+
120
+ Ported from [`davis7dotsh/my-pi-setup`](https://github.com/davis7dotsh/my-pi-setup/tree/main/extensions/background-terminals).
121
+
122
+ ## License
123
+
124
+ MIT
@@ -0,0 +1,430 @@
1
+ # Background terminals implementation guide
2
+
3
+ This document describes the invariants behind `pi-tian-background-terminals`.
4
+ The package overrides Pi's built-in `bash` model tool and adds one user command,
5
+ `/ps`.
6
+
7
+ ## 1. Product contract
8
+
9
+ Every model shell command goes through `bash`:
10
+
11
+ 1. Validate the command, working directory, yield wait, and optional hard
12
+ timeout.
13
+ 2. Resolve Bash and Pi's shell settings.
14
+ 3. Spawn with no interactive stdin and capture stdout/stderr separately.
15
+ 4. Wait for a bounded initial yield period (default 10 seconds, accepted range
16
+ 250–30,000 ms).
17
+ 5. If the process settles during that wait, return its final state and output in
18
+ the Bash result. A non-zero exit, kill, or timeout is a tool error.
19
+ 6. Otherwise return a terminal id and leave the process running.
20
+ 7. When a yielded process settles, deliver its final result as an exactly-once
21
+ follow-up that wakes the model.
22
+
23
+ The model has no status, list, kill, poll, or stdin tools. It should continue
24
+ working after a command yields. The user inspects and stops running terminals
25
+ with `/ps`.
26
+
27
+ A process is session-scoped. It never survives `/new`, `/resume`, `/fork`,
28
+ `/reload`, or quit.
29
+
30
+ ## 2. Why override `bash`
31
+
32
+ Pi combines built-in and extension tools by name; an extension definition wins
33
+ when it registers the same name. Registering `bash` therefore replaces the
34
+ built-in execution implementation while keeping one canonical shell tool in the
35
+ model schema.
36
+
37
+ The public schema keeps built-in compatibility:
38
+
39
+ ```text
40
+ command required string
41
+ timeout optional hard total runtime timeout in seconds
42
+ ```
43
+
44
+ It adds managed-execution controls:
45
+
46
+ ```text
47
+ working_dir optional working directory
48
+ title optional /ps label
49
+ yield_time_ms optional initial wait, 250–30,000 ms
50
+ ```
51
+
52
+ The override supplies its own prompt snippet and guidelines because Pi does not
53
+ inherit prompt metadata from built-ins. It omits custom tool renderers so Pi's
54
+ built-in Bash call/result renderers are inherited. Successful results use
55
+ `details: undefined`, which is a valid `BashToolDetails` shape. Separate full-log
56
+ paths remain in the textual stdout/stderr sections.
57
+
58
+ ## 3. Safe fallback boundary
59
+
60
+ A fallback may only occur before it is possible for a command to have run.
61
+
62
+ `getManager()` resolves and wires the Effect service before `manager.start()`.
63
+ If that resolution fails, the broken runtime is disposed and a fresh
64
+ `createBashToolDefinition(cwd, settings)` executes the command in Pi's standard
65
+ foreground Bash implementation.
66
+
67
+ `SpawnError` also carries `fallbackSafe`. It is true only for synchronous shell
68
+ resolution or `spawn()` failures where the manager can prove no child was
69
+ created. Those errors may use the same fallback without resetting an otherwise
70
+ healthy manager. Shutdown races set it false. The result is always prefixed with
71
+ a warning that background yielding and `/ps` were unavailable.
72
+
73
+ No fallback occurs after:
74
+
75
+ - `start()` creates a child;
76
+ - spawn succeeds or emits an asynchronous error;
77
+ - a command exits non-zero;
78
+ - a hard timeout;
79
+ - an abort;
80
+ - the managed concurrency cap is reached.
81
+
82
+ Retrying after any of those points could execute arbitrary side effects twice.
83
+ A failed command is a command result, not evidence that another executor should
84
+ run it again.
85
+
86
+ ## 4. Shell compatibility
87
+
88
+ The tool preserves Pi's normal Bash behavior:
89
+
90
+ - `SettingsManager` resolves trusted global/project `shellPath` and
91
+ `shellCommandPrefix` settings.
92
+ - `getShellConfig(shellPath)` selects Bash using Pi's cross-platform rules:
93
+ Bash on POSIX and Git Bash/MSYS/Cygwin-compatible Bash on Windows, with Pi's
94
+ normal fallback behavior.
95
+ - `shellCommandPrefix` is prepended only to the script sent to Bash. The
96
+ snapshot retains the exact model-supplied command.
97
+ - The tool reconstructs the same live session environment values:
98
+ `PI_SESSION_ID`, `PI_SESSION_FILE`, `PI_PROVIDER`, `PI_MODEL`, and
99
+ `PI_REASONING_LEVEL`. Inherited stale values are removed first.
100
+
101
+ Most shells receive the script as a `-c` argument and have stdin ignored.
102
+ Legacy WSL Bash may require a one-shot stdin script transport; the extension
103
+ writes the script and closes the pipe immediately. Neither path provides an
104
+ interactive input surface.
105
+
106
+ ## 5. File layout
107
+
108
+ ```text
109
+ index.ts Pi boundary, bash override, fallback, /ps
110
+ src/domain.ts Snapshot/status/error types
111
+ src/manager.ts Effect service and process lifecycle
112
+ src/output.ts Bounded head+tail stream retention
113
+ src/prompt.ts Tool metadata and model-facing formatting
114
+ src/result-delivery.ts Drain-once completion delivery map
115
+ src/runtime.ts ManagedRuntime and Effect→Promise boundary
116
+ src/ui/output-view.ts ANSI-safe wrapped output rendering
117
+ src/ui/ps.ts /ps list and detail overlays
118
+ ```
119
+
120
+ The manager owns mutable lifecycle state. Consumers receive readonly live
121
+ `TerminalSnapshot` objects whose stdout/stderr properties are getters over
122
+ `OutputBuffer`.
123
+
124
+ ## 6. Domain model
125
+
126
+ A terminal moves exactly once from `running` to one final status:
127
+
128
+ ```text
129
+ running ── exit 0 ─────────────────────────────► done
130
+ running ── non-zero exit / process error ──────► failed
131
+ running ── hard runtime deadline ──────────────► timed_out
132
+ running ── /ps stop / session teardown ────────► killed
133
+ ```
134
+
135
+ A snapshot includes:
136
+
137
+ - stable `bt-N` id;
138
+ - exact model command, bounded title, absolute cwd, and pid;
139
+ - timestamps, optional timeout, and final exit code or signal;
140
+ - separate stdout/stderr `OutputView` values;
141
+ - bounded lifecycle/spill error text.
142
+
143
+ `OutputView` exposes:
144
+
145
+ - `head`: stable startup prefix;
146
+ - `tail`: rolling recent suffix;
147
+ - `text`: head + omission marker + tail for `/ps`;
148
+ - `totalBytes` and `truncatedBytes`;
149
+ - optional complete spill-file path.
150
+
151
+ ## 7. Execution flow
152
+
153
+ `index.ts` first performs side-effect-free validation. It preserves the exact
154
+ script text and trims only to reject an empty command and derive a title.
155
+ Filesystem stat errors and non-directory paths are reported before manager
156
+ resolution.
157
+
158
+ Manager execution has two calls:
159
+
160
+ ```text
161
+ manager.start(...)
162
+ manager.waitForSettlement(id, yield_time_ms)
163
+ ```
164
+
165
+ `start` is uninterruptible between `spawn()` and registry insertion. This avoids
166
+ an abort window where a live child exists without a manager entry or scope.
167
+
168
+ During the initial wait, a per-terminal subscription emits bounded progress
169
+ updates at most every 100 ms. Exceptions thrown by the display callback are
170
+ ignored; presentation cannot affect process execution.
171
+
172
+ The initial wait is abortible. Aborting it does not kill the process. The error
173
+ identifies the terminal id, and eventual settlement remains eligible for an
174
+ automatic follow-up.
175
+
176
+ The returned result has two forms:
177
+
178
+ - **Final:** status/output are returned directly and any deferred completion for
179
+ the tiny start→wait race is consumed. Failed, killed, and timed-out final
180
+ states throw so Pi marks the Bash result as an error.
181
+ - **Yielded:** status is `running`, the id and captured startup output are
182
+ returned, and later settlement becomes a follow-up.
183
+
184
+ ## 8. Hard runtime timeout
185
+
186
+ `timeout` is independent from `yield_time_ms`:
187
+
188
+ - `yield_time_ms` controls only when the model regains control;
189
+ - `timeout` is a total process lifetime and terminates the process tree.
190
+
191
+ The accepted timeout upper bound is Node's maximum timer delay,
192
+ 2,147,483,647 ms, matching Pi's built-in Bash limit. There is no default hard
193
+ timeout.
194
+
195
+ The manager installs an unref'ed timer only after the entry is registered. If
196
+ the deadline wins before a natural `exit`, it marks the entry `timed_out` and
197
+ closes the entry scope. Natural exit metadata that already won is not rewritten
198
+ merely because descendants still hold inherited output pipes open.
199
+
200
+ Every settlement and disposal path clears the timer. Session teardown clears
201
+ all timers before closing scopes so a shutdown is reported as `killed`, not as
202
+ a coincidental timeout.
203
+
204
+ ## 9. Yield/settlement linearization
205
+
206
+ The most important race is:
207
+
208
+ ```text
209
+ initial yield wins <──► process settlement wins
210
+ ```
211
+
212
+ Returning both the Bash result and an automatic follow-up would duplicate the
213
+ same completion. Returning neither would lose it.
214
+
215
+ `waitForSettlement` registers a waiter token in `settlementWaiters` before
216
+ racing the terminal's `Deferred` against `Effect.sleep(yield_time_ms)`.
217
+
218
+ When settlement wins:
219
+
220
+ 1. `settle` marks every registered waiter token `consumed = true`.
221
+ 2. It computes the settle-hook `consumed` flag.
222
+ 3. It completes the terminal `Deferred`.
223
+ 4. It invokes the settle hook.
224
+ 5. The Bash call returns the final snapshot.
225
+
226
+ When yield wins:
227
+
228
+ 1. The timeout branch synchronously removes its waiter token.
229
+ 2. It returns the still-running snapshot.
230
+ 3. A later settlement sees no waiter and therefore queues a follow-up.
231
+
232
+ The synchronous token removal is the linearization point. Cleanup is also an
233
+ Effect finalizer, so an interrupted wait cannot leave stale interest that
234
+ suppresses a future completion.
235
+
236
+ There is a second defensive layer in `index.ts`: when a final snapshot is
237
+ returned, `resultDelivery.consume(id)` removes any result deferred during the
238
+ small gap between `start` and waiter registration.
239
+
240
+ ## 10. Exactly-once follow-up delivery
241
+
242
+ The settle hook receives `(snapshot, consumed)`:
243
+
244
+ - `consumed = true`: the initial Bash wait or an internal kill operation is
245
+ already returning the final state; do not queue a follow-up.
246
+ - `consumed = false`: copy the final snapshot into the drain-once delivery map.
247
+
248
+ The map is keyed by terminal id. `drain()` clears entries before delivery, so a
249
+ result can be sent only once. A failed `pi.sendMessage` call re-defers the same
250
+ id for a later `agent_settled` retry.
251
+
252
+ Delivery uses:
253
+
254
+ ```ts
255
+ pi.sendMessage(message, {
256
+ deliverAs: "followUp",
257
+ triggerTurn: true,
258
+ });
259
+ ```
260
+
261
+ A busy agent receives the message after its current run settles. An idle agent
262
+ is woken immediately. No model-driven polling is required.
263
+
264
+ ## 11. Head+tail output retention
265
+
266
+ Each stdout/stderr stream has a 2 MiB in-memory cap:
267
+
268
+ ```text
269
+ stable head: 256 KiB
270
+ rolling tail: remaining 1.75 MiB
271
+ ```
272
+
273
+ `OutputBuffer.push` performs these steps:
274
+
275
+ 1. Send the complete decoded chunk to the spill callback before retention.
276
+ 2. Fill the stable head until its budget is reached.
277
+ 3. Seal the head as soon as any byte extends past that budget.
278
+ 4. Append remaining bytes to the tail.
279
+ 5. Evict bytes from the front of the tail until it fits its budget.
280
+ 6. Cut only at UTF-8 code point boundaries.
281
+ 7. Compute omitted bytes as `total - retainedHead - retainedTail`.
282
+
283
+ Bounded slices are copied so a small retained head/tail cannot pin a giant
284
+ source Buffer. Once sealed, the head never changes. This preserves startup
285
+ configuration and first errors while the tail tracks recent logs.
286
+
287
+ `OutputView.text` inserts an explicit marker when the middle was omitted:
288
+
289
+ ```text
290
+ <startup head>
291
+ ... 7340032 bytes omitted ...
292
+ <recent tail>
293
+ ```
294
+
295
+ The marker itself is not counted against the retained-byte cap.
296
+
297
+ ### Model-facing truncation
298
+
299
+ The 2 MiB view is still too large for model context. `prompt.ts` applies Pi's
300
+ truncation utilities again with smaller per-result budgets, allocating one
301
+ quarter to startup head and three quarters to recent tail.
302
+
303
+ Initial Bash result budgets:
304
+
305
+ ```text
306
+ stdout: 16 KiB / 400 lines
307
+ stderr: 8 KiB / 200 lines
308
+ ```
309
+
310
+ Automatic completion budgets:
311
+
312
+ ```text
313
+ stdout: 8 KiB / 40 lines
314
+ stderr: 4 KiB / 20 lines
315
+ ```
316
+
317
+ Every model-visible path remains bounded even if a child is a firehose.
318
+
319
+ ## 12. Spill files and backpressure
320
+
321
+ Before spawning output consumers, the manager creates a private per-session
322
+ temporary directory (`0700`). Each stream writes to a separate `0600` file from
323
+ its first byte.
324
+
325
+ Spills are capped at 256 MiB per stream. If the cap or an I/O error is reached,
326
+ the full-log pointer is cleared and a bounded `errorText` note is attached.
327
+
328
+ When `WriteStream.write()` reports backpressure, the matching child stream is
329
+ paused and resumed on `drain`. In-memory retention still receives the triggering
330
+ chunk; subsequent child output is flow-controlled by Node.
331
+
332
+ Settlement waits for spill streams to flush before publishing the final
333
+ snapshot. The flush wait is bounded so a broken filesystem cannot leave a
334
+ terminal permanently running.
335
+
336
+ ## 13. Process lifecycle and termination
337
+
338
+ Children use separate stdout/stderr pipes and no interactive stdin. On POSIX,
339
+ `detached: true` gives the child its own process group. Windows uses `taskkill
340
+ /T`, with `/F` for force kill.
341
+
342
+ Termination is:
343
+
344
+ ```text
345
+ SIGTERM whole tree
346
+ wait up to 2 seconds for close
347
+ SIGKILL whole tree
348
+ bounded final close/settle wait
349
+ ```
350
+
351
+ Exit metadata is recorded on Node's `exit` event, but settlement occurs on
352
+ `close` so output has reached EOF. If descendants inherit the pipes and hold
353
+ them open after the shell exits, bounded cleanup closes the entry scope and
354
+ reaps the process group.
355
+
356
+ The entry scope is the single cleanup path for `/ps` stop, hard timeout,
357
+ pruning, internal kill calls, and runtime disposal.
358
+
359
+ ## 14. Capacity and pruning
360
+
361
+ At most eight terminals may run concurrently. Start reserves a slot
362
+ synchronously before spawning, so parallel tool calls cannot race through the
363
+ cap. Reaching the cap is an explicit error; it does not bypass management via
364
+ the foreground fallback.
365
+
366
+ The registry retains at most 32 live/settled entries for `/ps`. When over the
367
+ limit, it removes the oldest settled entries; running entries are never pruned.
368
+ Small tombstones retain final kill-report facts across pruning races.
369
+
370
+ ## 15. `/ps` UI
371
+
372
+ The manager exposes a synchronous `TerminalReadModel` for TUI rendering:
373
+
374
+ - `list`, `get`, and `size`;
375
+ - global and per-id subscriptions;
376
+ - fire-and-forget user kill;
377
+ - settle-hook registration.
378
+
379
+ The list overlay supports selection and stopping. The detail overlay provides:
380
+
381
+ - command metadata;
382
+ - stdout/stderr toggle;
383
+ - ANSI/control sanitization at render time;
384
+ - wrapped output with cached layouts keyed by `(buffer version, width)`;
385
+ - live tail pinning and scrollback.
386
+
387
+ ## 16. Session teardown
388
+
389
+ `session_shutdown` clears UI/delivery state and disposes the ManagedRuntime.
390
+ The manager finalizer runs `disposeAll`, which:
391
+
392
+ 1. marks the manager disposed;
393
+ 2. removes every registry entry;
394
+ 3. clears every runtime timeout;
395
+ 4. closes every entry scope concurrently with bounds;
396
+ 5. waits for detached cleanup fibers within the shutdown bound;
397
+ 6. removes the private spill directory.
398
+
399
+ The `disposed` check after process setup closes a child that raced with a
400
+ teardown sweep, preventing an unregistered survivor.
401
+
402
+ ## 17. Required tests
403
+
404
+ The package test suite covers:
405
+
406
+ - registration of only the `bash` override plus `/ps`;
407
+ - quick completion without a duplicate follow-up;
408
+ - yielded completion with exactly one automatic delivery;
409
+ - safe pre-spawn foreground fallback;
410
+ - non-zero commands executing only once, without fallback retry;
411
+ - Pi session environment and command-prefix preservation;
412
+ - bounded streaming updates during the initial wait;
413
+ - hard timeout status and tree termination;
414
+ - Bash-specific syntax on the resolved shell;
415
+ - abort leaving eventual completion deliverable;
416
+ - process-tree kill and SIGKILL escalation;
417
+ - session disposal and spill-directory cleanup;
418
+ - output head stability, rolling tail, UTF-8 boundaries, and omission counts;
419
+ - complete spill capture beyond the memory cap;
420
+ - drain-once result delivery;
421
+ - `/ps` selection, sanitization, wrapping, and cache behavior.
422
+
423
+ Validation commands:
424
+
425
+ ```bash
426
+ npm run check --workspace pi-tian-background-terminals
427
+ npm test --workspace pi-tian-background-terminals
428
+ npm run typecheck
429
+ npm pack --dry-run --workspace pi-tian-background-terminals
430
+ ```