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 +21 -0
- package/README.md +124 -0
- package/docs/implementation-guide.md +430 -0
- package/index.ts +584 -0
- package/package.json +59 -0
- package/src/domain.ts +102 -0
- package/src/manager.ts +1070 -0
- package/src/output.ts +163 -0
- package/src/prompt.ts +195 -0
- package/src/result-delivery.ts +27 -0
- package/src/runtime.ts +41 -0
- package/src/ui/output-view.ts +79 -0
- package/src/ui/ps.ts +624 -0
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
|
+
```
|