mcp-yieldshell 0.5.0__tar.gz → 0.6.0__tar.gz
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.
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/PKG-INFO +50 -14
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/README.md +49 -13
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/pyproject.toml +1 -1
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/config.py +6 -0
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/process/managed.py +14 -3
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/process/manager.py +197 -66
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/process/ring_buffer.py +104 -2
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/server.py +52 -3
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/tests/test_config.py +16 -0
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/tests/test_integration.py +263 -12
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/tests/test_lifecycle_hardening.py +128 -2
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/tests/test_protocol.py +66 -0
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/tests/test_ring_buffer.py +141 -12
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/.gitignore +0 -0
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/__init__.py +0 -0
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/__main__.py +0 -0
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/policy.py +0 -0
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/process/__init__.py +0 -0
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/process/spawn.py +0 -0
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/security.py +0 -0
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/side_effects.py +0 -0
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/types.py +0 -0
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/tests/__init__.py +0 -0
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/tests/test_release.py +0 -0
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/tests/test_security.py +0 -0
- {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/tests/test_side_effects.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-yieldshell
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: A drop-in shell MCP that auto-yields long-running commands into managed background processes.
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: mcp<2,>=1.9.0
|
|
@@ -155,7 +155,7 @@ Execute a shell command. If the command runs longer than `yield_ms`, it yields a
|
|
|
155
155
|
* `name` (string, optional): A human-readable label/name to identify this process.
|
|
156
156
|
* `yield_ms` (integer, optional): Milliseconds to wait before yielding execution to background. Both omitted and explicit values are clamped to the lesser of `YIELDSHELL_MAX_YIELD_MS` and the transport-safe 55,000ms ceiling. Defaults to `YIELDSHELL_DEFAULT_YIELD_MS` (30,000ms).
|
|
157
157
|
* `timeout_ms` (integer, optional): Total execution runtime limit in milliseconds. Process is terminated if it runs longer than this. Defaults to `YIELDSHELL_DEFAULT_TIMEOUT_MS` (3,600,000ms). Pass `0` explicitly for unlimited execution.
|
|
158
|
-
* `max_output_bytes` (integer, optional): Maximum bytes
|
|
158
|
+
* `max_output_bytes` (integer, optional): Maximum bytes returned in total across both response streams. Subject to the `YIELDSHELL_MAX_OUTPUT_BYTES` cap. This bounds the response only — retention is controlled separately by `YIELDSHELL_MAX_BUFFER_BYTES`, so output withheld here stays readable via `read`.
|
|
159
159
|
|
|
160
160
|
* **Side-Effects Guide**:
|
|
161
161
|
* `side_effects` is required and must be a non-empty list. Declare every plausible side-effect category before running the command.
|
|
@@ -175,8 +175,9 @@ Execute a shell command. If the command runs longer than `yield_ms`, it yields a
|
|
|
175
175
|
* Inline code execution: prefer writing the content to a reviewable workspace file (for example `scripts/migrate.sql` or `tools/build.sh`) and run it in a small, inspectable step. Declaring `side_effects=["RUNS_INLINE_CODE"]` is rejected under the default policy; operators can clear that default with `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS=,`.
|
|
176
176
|
|
|
177
177
|
* **Output Statuses**:
|
|
178
|
-
*
|
|
179
|
-
* `
|
|
178
|
+
* Every status returns the cursor fields described under [Byte Cursors & Incremental Reads](#byte-cursors--incremental-reads): `start_seq`, `next_seq`, `latest_seq`, `capped`, and `evicted`. The inline snapshot advances the process's server-side read cursor, so a following `read` or `wait` continues after it rather than repeating it.
|
|
179
|
+
* `completed`: Process finished within `yield_ms`. Returns exit code, stdout, and stderr. If output was withheld and terminal-process retention is enabled, it also returns `process_id` so the remainder can be inspected with `read`.
|
|
180
|
+
* `backgrounded`: Process auto-yielded. Returns `process_id`, `pid`, a snapshot of initial stdout/stderr, `duration_ms`, the cursor fields, and a `message` string describing that the process is running in the background.
|
|
180
181
|
* `timed_out`: Process exceeded `timeout_ms` and was terminated.
|
|
181
182
|
* `stopped`: Process was explicitly terminated.
|
|
182
183
|
* `failed_to_start`: Command could not be spawned (e.g., bad directory or policy violation).
|
|
@@ -190,12 +191,15 @@ Read stdout and/or stderr output from a running or completed background process.
|
|
|
190
191
|
|
|
191
192
|
* **Parameters**:
|
|
192
193
|
* `process_id` (string, **required**): Unique identifier of the process.
|
|
193
|
-
* `since_seq` (integer, optional): Byte-position cursor returned as `next_seq` by the previous read. Enables lossless incremental log polling.
|
|
194
|
+
* `since_seq` (integer, optional): Byte-position cursor returned as `next_seq` by the previous read. Enables lossless incremental log polling. When omitted, the read resumes from the server-side cursor instead (see [Byte Cursors & Incremental Reads](#byte-cursors--incremental-reads)); pass `since_seq=1` to re-read from the beginning.
|
|
194
195
|
* `max_output_bytes` (integer, optional): Clamps total output returned across the selected streams. Defaults to the server cap.
|
|
195
196
|
* `streams` (string, default: `"both"`): The streams to read. Options: `"both"`, `"stdout"`, or `"stderr"`.
|
|
197
|
+
* `tail_lines` (integer, optional): Return only the newest N lines instead of reading forward. Useful for monitoring a noisy build or test run without paging through it. Mutually exclusive with `since_seq`; must be positive. If the requested tail exceeds `max_output_bytes`, the newest bytes are kept rather than the oldest.
|
|
196
198
|
|
|
197
199
|
* **Returns**:
|
|
198
|
-
* `process_id`, `status`, `exit_code`, `signal`,
|
|
200
|
+
* `process_id`, `status`, `exit_code`, `signal`, and `stdin_error` when initial input delivery failed. `stdout` and `stderr` text are included based on the `streams` filter — `"both"` includes both, `"stdout"` includes only `stdout`, and `"stderr"` includes only `stderr`.
|
|
201
|
+
* Cursor fields: `start_seq` (position of the first byte returned), `next_seq` (cursor to use in the next `since_seq` read), and `latest_seq` (how far output has advanced overall — when it exceeds `next_seq`, more is waiting).
|
|
202
|
+
* Withheld-output flags: `capped` means more output is available and reading again continues from `next_seq`; `evicted` means output fell out of the ring buffer before it was read and cannot be recovered. When either is set, a `hint` field names the concrete follow-up call.
|
|
199
203
|
|
|
200
204
|
### `write`
|
|
201
205
|
Write text input to the standard input (`stdin`) of a running process.
|
|
@@ -213,13 +217,32 @@ Block execution until the process exits or the wait timeout expires. This allows
|
|
|
213
217
|
|
|
214
218
|
* **Parameters**:
|
|
215
219
|
* `process_id` (string, **required**): Unique identifier of the process.
|
|
216
|
-
* `timeout_ms` (integer, default: `55000`): Maximum time to wait.
|
|
220
|
+
* `timeout_ms` (integer, default: `55000`): Maximum time to wait. Never terminates the process.
|
|
217
221
|
* `max_output_bytes` (integer, optional): Maximum total output bytes to return across stdout and stderr.
|
|
222
|
+
* `since_seq` (integer, optional): Byte-position cursor from a previous response. When omitted, `wait` resumes from the server-side cursor, so polling in a loop returns each byte exactly once without any cursor bookkeeping.
|
|
223
|
+
* `tail_lines` (integer, optional): Return only the newest N lines. Mutually exclusive with `since_seq`.
|
|
224
|
+
|
|
225
|
+
* **Returns**: the same cursor and withheld-output fields as `read`, plus:
|
|
226
|
+
* `wait_result`: `"exited"` when no live work remains, or `"deadline_reached"` when the wait budget ran out first. This is what distinguishes a still-running process from a wait that gave up — `status` alone cannot.
|
|
227
|
+
* `waited_ms`: time actually spent waiting.
|
|
228
|
+
* `max_wait_ms`: the effective budget after capping, so a clamped request is visible rather than silent.
|
|
218
229
|
|
|
219
230
|
* **Important**: If the wait timeout expires, `wait` returns the current status but **does not kill** the process. It continues running in the background.
|
|
220
|
-
* The effective wait duration is capped at 55 seconds to stay well under typical MCP request timeouts, even if a larger `timeout_ms` is requested.
|
|
231
|
+
* The effective wait duration is capped at 55 seconds to stay well under typical MCP request timeouts, even if a larger `timeout_ms` is requested. The response reports that cap in `max_wait_ms`.
|
|
221
232
|
* `wait` treats the managed process group disappearing as completion. If the tracked shell exits while descendants in its process group remain alive, the record continues to report `running`. For normal process-group completion, stdout/stderr are drained before the response is returned. Final drain waits are bounded so inherited pipes cannot block a request indefinitely.
|
|
222
233
|
|
|
234
|
+
### Timing Parameters at a Glance
|
|
235
|
+
|
|
236
|
+
Three separate timers control different things. Only one of them ever terminates a process:
|
|
237
|
+
|
|
238
|
+
| Parameter | Meaning | Terminates the process? | Default | Effective cap |
|
|
239
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
240
|
+
| `exec(yield_ms)` | How long `exec` stays inline before handing back a `process_id` and letting the command continue in the background. | No | 30,000ms | Lesser of `YIELDSHELL_MAX_YIELD_MS` and 55,000ms |
|
|
241
|
+
| `exec(timeout_ms)` | Total execution limit. On expiry the process group is sent `SIGTERM`, then `SIGKILL`, and the status becomes `timed_out`. | **Yes** | 3,600,000ms | None (`0` means unlimited) |
|
|
242
|
+
| `wait(timeout_ms)` | Maximum time a single `wait` call blocks. On expiry it returns `wait_result: "deadline_reached"` and the process keeps running. | No | 55,000ms | 55,000ms |
|
|
243
|
+
|
|
244
|
+
If `wait` returns with `status: "running"`, check `wait_result`: `"deadline_reached"` means the wait budget expired while the tracked shell or one of its descendants was still alive. The process keeps running and can be polled again.
|
|
245
|
+
|
|
223
246
|
### `stop`
|
|
224
247
|
Gracefully terminate or force kill a running process.
|
|
225
248
|
|
|
@@ -237,6 +260,10 @@ Terminal records are retained temporarily for inspection. Before each otherwise
|
|
|
237
260
|
* `include_completed` (boolean, default: `true`): If `false`, finished/stopped processes are excluded from the output.
|
|
238
261
|
* `limit` (integer, default: `50`): Maximum number of entries.
|
|
239
262
|
* **Returns**: `processes` — a list of process summary objects, each containing: `process_id`, `pid`, `name`, `command`, `cwd`, `status`, `exit_code`, `signal`, `started_at`, `ended_at`, `duration_ms`, `stdout_bytes`, `stderr_bytes`, and `stdin_error` (`null` unless initial input delivery failed).
|
|
263
|
+
* Activity fields help distinguish a genuine hang from quiet work:
|
|
264
|
+
* `last_output_at`: wall-clock time output last arrived on either stream, or `null` if none has.
|
|
265
|
+
* `idle_ms`: milliseconds since that moment, or `null` if no output has arrived. A large and growing `idle_ms` on a `running` process is the signal that it may be stuck.
|
|
266
|
+
* `latest_seq`: the process-wide cursor position just past the newest captured byte. Comparing it to a `next_seq` you hold shows how much output is waiting.
|
|
240
267
|
|
|
241
268
|
### Error Responses
|
|
242
269
|
|
|
@@ -256,14 +283,22 @@ Prune completed, stopped, timed-out, and failed process records to free memory.
|
|
|
256
283
|
|
|
257
284
|
To avoid sending duplicate data over the MCP protocol (which can consume context window space), the server implements a byte-position polling protocol:
|
|
258
285
|
|
|
259
|
-
1. Every stdout/stderr byte receives a unique position in a cursor shared by both streams.
|
|
260
|
-
2. `read`
|
|
261
|
-
3.
|
|
262
|
-
4.
|
|
263
|
-
5.
|
|
286
|
+
1. Every stdout/stderr byte receives a unique position in a cursor shared by both streams. Positions start at 1 and only ever increase.
|
|
287
|
+
2. `read`, `wait`, and `exec` all return `next_seq`, the position immediately after the range covered by the response. A response cap can therefore end safely inside a drain chunk.
|
|
288
|
+
3. **You do not have to track cursors.** A `read` or `wait` with no `since_seq` and no `tail_lines` resumes from where the last such call stopped, then advances a server-side cursor held per process. Polling in a loop therefore returns each byte exactly once. `exec`'s inline snapshot advances the same cursor, so a following `wait` continues after it.
|
|
289
|
+
4. To drive the cursor yourself instead, pass the previous `next_seq` back as `since_seq` and keep the same `streams` selection.
|
|
290
|
+
5. **A cursorless read consumes.** Once output has been delivered, a later cursorless read will not return it again. To inspect output that was already delivered, pass `since_seq=1` to re-read from the beginning, or use `tail_lines`.
|
|
291
|
+
6. Requests that pass `since_seq`, pass `tail_lines`, or narrow `streams` are treated as **out-of-band**: they neither consult nor advance the server-side cursor. This means a peek at the tail or at stderr cannot silently skip output the polling stream has not seen yet.
|
|
292
|
+
7. Two distinct flags report output the response did not carry, and they call for different responses:
|
|
293
|
+
* `capped` — the response cap stopped the read short. The remainder is still buffered; read again to continue, or pass the returned `next_seq` as `since_seq`. The `hint` field spells out the call.
|
|
294
|
+
* `evicted` — output exceeded the ring buffer's capacity before it was read and is permanently gone. Retrying cannot recover it; poll more often or raise `YIELDSHELL_MAX_BUFFER_BYTES`. Reads report eviction only when their requested range overlaps evicted data.
|
|
295
|
+
8. `latest_seq` reports how far output has advanced overall. `latest_seq > next_seq` means more is already waiting; `latest_seq == next_seq` means the caller is fully caught up.
|
|
296
|
+
9. Because retention (`YIELDSHELL_MAX_BUFFER_BYTES`, 256 KB per stream) is much larger than the per-response cap (`YIELDSHELL_MAX_OUTPUT_BYTES`, 20 KB), a cursor normally stays resolvable across polling gaps.
|
|
264
297
|
|
|
265
298
|
Cursor boundaries preserve valid UTF-8 characters. If a single character is larger than a very small requested page, that page may exceed `max_output_bytes` by at most three bytes so the cursor can make progress without corrupting the character.
|
|
266
299
|
|
|
300
|
+
Positions are never reused and never reset. Eviction does not rewind them: it advances the oldest retained position while `next_seq` keeps climbing, so a cursor is safe to hold indefinitely. A cursor pointing at data that has since been evicted is not an error — the read returns data from the earliest retained position onward and sets `evicted` to `true`, and `start_seq` shows where the returned range actually begins. Positions are scoped to one process; a cursor from one `process_id` is meaningless for another.
|
|
301
|
+
|
|
267
302
|
Incremental cursors are scoped to the selected `streams` value. Keep the same stream selection while advancing a cursor; switching from `stdout`-only or `stderr`-only polling to another selection can intentionally skip bytes from the previously unselected stream.
|
|
268
303
|
|
|
269
304
|
When a process exits normally, `exec`/`wait` responses include output drained through stdout/stderr EOF. If a descendant keeps inherited stdout/stderr open after the tracked process exits, the server stops waiting on those inherited pipes to avoid indefinite blocking; output written only by that descendant after the tracked process exits is not part of the managed process result.
|
|
@@ -278,7 +313,8 @@ Configure the server by setting these environment variables prior to launch:
|
|
|
278
313
|
|---|---|---|
|
|
279
314
|
| `YIELDSHELL_DEFAULT_CWD` | Current directory | The fallback working directory for commands. |
|
|
280
315
|
| `YIELDSHELL_ALLOWED_CWDS` | *(none)* | A list of allowed directory paths separated by `os.pathsep` (e.g., `:` on UNIX, `;` on Windows). If set, all command execution paths must resolve inside one of these roots. |
|
|
281
|
-
| `YIELDSHELL_MAX_OUTPUT_BYTES` | `20000` | The default and maximum
|
|
316
|
+
| `YIELDSHELL_MAX_OUTPUT_BYTES` | `20000` | The default and maximum bytes any single response may return across stdout and stderr. This is a response cap only; it does not limit what is retained. Nonpositive or nonnumeric values use the default. |
|
|
317
|
+
| `YIELDSHELL_MAX_BUFFER_BYTES` | `262144` | Capacity of each per-process stdout/stderr ring buffer. Retention is deliberately larger than one response so `since_seq` cursors and `tail_lines` still resolve after a gap between polls. Raise it for very chatty commands, at a cost of roughly this value × 2 streams × live processes in memory. Nonpositive or nonnumeric values use the default. |
|
|
282
318
|
| `YIELDSHELL_MAX_PROCESSES` | `50` | Maximum concurrent live managed process groups, including descendants that outlive a completed shell. Spawning a new command when this limit is reached returns `failed_to_start`. Nonpositive or nonnumeric values use the default. |
|
|
283
319
|
| `YIELDSHELL_DEFAULT_YIELD_MS` | `30000` | Fallback delay before auto-yielding. Effective yields are also capped at 55,000ms. |
|
|
284
320
|
| `YIELDSHELL_MAX_YIELD_MS` | `300000` | Configured maximum for `yield_ms`; the effective maximum is the lesser of this value and 55,000ms. |
|
|
@@ -147,7 +147,7 @@ Execute a shell command. If the command runs longer than `yield_ms`, it yields a
|
|
|
147
147
|
* `name` (string, optional): A human-readable label/name to identify this process.
|
|
148
148
|
* `yield_ms` (integer, optional): Milliseconds to wait before yielding execution to background. Both omitted and explicit values are clamped to the lesser of `YIELDSHELL_MAX_YIELD_MS` and the transport-safe 55,000ms ceiling. Defaults to `YIELDSHELL_DEFAULT_YIELD_MS` (30,000ms).
|
|
149
149
|
* `timeout_ms` (integer, optional): Total execution runtime limit in milliseconds. Process is terminated if it runs longer than this. Defaults to `YIELDSHELL_DEFAULT_TIMEOUT_MS` (3,600,000ms). Pass `0` explicitly for unlimited execution.
|
|
150
|
-
* `max_output_bytes` (integer, optional): Maximum bytes
|
|
150
|
+
* `max_output_bytes` (integer, optional): Maximum bytes returned in total across both response streams. Subject to the `YIELDSHELL_MAX_OUTPUT_BYTES` cap. This bounds the response only — retention is controlled separately by `YIELDSHELL_MAX_BUFFER_BYTES`, so output withheld here stays readable via `read`.
|
|
151
151
|
|
|
152
152
|
* **Side-Effects Guide**:
|
|
153
153
|
* `side_effects` is required and must be a non-empty list. Declare every plausible side-effect category before running the command.
|
|
@@ -167,8 +167,9 @@ Execute a shell command. If the command runs longer than `yield_ms`, it yields a
|
|
|
167
167
|
* Inline code execution: prefer writing the content to a reviewable workspace file (for example `scripts/migrate.sql` or `tools/build.sh`) and run it in a small, inspectable step. Declaring `side_effects=["RUNS_INLINE_CODE"]` is rejected under the default policy; operators can clear that default with `MCP_YIELDSHELL_BLOCKED_SIDE_EFFECTS=,`.
|
|
168
168
|
|
|
169
169
|
* **Output Statuses**:
|
|
170
|
-
*
|
|
171
|
-
* `
|
|
170
|
+
* Every status returns the cursor fields described under [Byte Cursors & Incremental Reads](#byte-cursors--incremental-reads): `start_seq`, `next_seq`, `latest_seq`, `capped`, and `evicted`. The inline snapshot advances the process's server-side read cursor, so a following `read` or `wait` continues after it rather than repeating it.
|
|
171
|
+
* `completed`: Process finished within `yield_ms`. Returns exit code, stdout, and stderr. If output was withheld and terminal-process retention is enabled, it also returns `process_id` so the remainder can be inspected with `read`.
|
|
172
|
+
* `backgrounded`: Process auto-yielded. Returns `process_id`, `pid`, a snapshot of initial stdout/stderr, `duration_ms`, the cursor fields, and a `message` string describing that the process is running in the background.
|
|
172
173
|
* `timed_out`: Process exceeded `timeout_ms` and was terminated.
|
|
173
174
|
* `stopped`: Process was explicitly terminated.
|
|
174
175
|
* `failed_to_start`: Command could not be spawned (e.g., bad directory or policy violation).
|
|
@@ -182,12 +183,15 @@ Read stdout and/or stderr output from a running or completed background process.
|
|
|
182
183
|
|
|
183
184
|
* **Parameters**:
|
|
184
185
|
* `process_id` (string, **required**): Unique identifier of the process.
|
|
185
|
-
* `since_seq` (integer, optional): Byte-position cursor returned as `next_seq` by the previous read. Enables lossless incremental log polling.
|
|
186
|
+
* `since_seq` (integer, optional): Byte-position cursor returned as `next_seq` by the previous read. Enables lossless incremental log polling. When omitted, the read resumes from the server-side cursor instead (see [Byte Cursors & Incremental Reads](#byte-cursors--incremental-reads)); pass `since_seq=1` to re-read from the beginning.
|
|
186
187
|
* `max_output_bytes` (integer, optional): Clamps total output returned across the selected streams. Defaults to the server cap.
|
|
187
188
|
* `streams` (string, default: `"both"`): The streams to read. Options: `"both"`, `"stdout"`, or `"stderr"`.
|
|
189
|
+
* `tail_lines` (integer, optional): Return only the newest N lines instead of reading forward. Useful for monitoring a noisy build or test run without paging through it. Mutually exclusive with `since_seq`; must be positive. If the requested tail exceeds `max_output_bytes`, the newest bytes are kept rather than the oldest.
|
|
188
190
|
|
|
189
191
|
* **Returns**:
|
|
190
|
-
* `process_id`, `status`, `exit_code`, `signal`,
|
|
192
|
+
* `process_id`, `status`, `exit_code`, `signal`, and `stdin_error` when initial input delivery failed. `stdout` and `stderr` text are included based on the `streams` filter — `"both"` includes both, `"stdout"` includes only `stdout`, and `"stderr"` includes only `stderr`.
|
|
193
|
+
* Cursor fields: `start_seq` (position of the first byte returned), `next_seq` (cursor to use in the next `since_seq` read), and `latest_seq` (how far output has advanced overall — when it exceeds `next_seq`, more is waiting).
|
|
194
|
+
* Withheld-output flags: `capped` means more output is available and reading again continues from `next_seq`; `evicted` means output fell out of the ring buffer before it was read and cannot be recovered. When either is set, a `hint` field names the concrete follow-up call.
|
|
191
195
|
|
|
192
196
|
### `write`
|
|
193
197
|
Write text input to the standard input (`stdin`) of a running process.
|
|
@@ -205,13 +209,32 @@ Block execution until the process exits or the wait timeout expires. This allows
|
|
|
205
209
|
|
|
206
210
|
* **Parameters**:
|
|
207
211
|
* `process_id` (string, **required**): Unique identifier of the process.
|
|
208
|
-
* `timeout_ms` (integer, default: `55000`): Maximum time to wait.
|
|
212
|
+
* `timeout_ms` (integer, default: `55000`): Maximum time to wait. Never terminates the process.
|
|
209
213
|
* `max_output_bytes` (integer, optional): Maximum total output bytes to return across stdout and stderr.
|
|
214
|
+
* `since_seq` (integer, optional): Byte-position cursor from a previous response. When omitted, `wait` resumes from the server-side cursor, so polling in a loop returns each byte exactly once without any cursor bookkeeping.
|
|
215
|
+
* `tail_lines` (integer, optional): Return only the newest N lines. Mutually exclusive with `since_seq`.
|
|
216
|
+
|
|
217
|
+
* **Returns**: the same cursor and withheld-output fields as `read`, plus:
|
|
218
|
+
* `wait_result`: `"exited"` when no live work remains, or `"deadline_reached"` when the wait budget ran out first. This is what distinguishes a still-running process from a wait that gave up — `status` alone cannot.
|
|
219
|
+
* `waited_ms`: time actually spent waiting.
|
|
220
|
+
* `max_wait_ms`: the effective budget after capping, so a clamped request is visible rather than silent.
|
|
210
221
|
|
|
211
222
|
* **Important**: If the wait timeout expires, `wait` returns the current status but **does not kill** the process. It continues running in the background.
|
|
212
|
-
* The effective wait duration is capped at 55 seconds to stay well under typical MCP request timeouts, even if a larger `timeout_ms` is requested.
|
|
223
|
+
* The effective wait duration is capped at 55 seconds to stay well under typical MCP request timeouts, even if a larger `timeout_ms` is requested. The response reports that cap in `max_wait_ms`.
|
|
213
224
|
* `wait` treats the managed process group disappearing as completion. If the tracked shell exits while descendants in its process group remain alive, the record continues to report `running`. For normal process-group completion, stdout/stderr are drained before the response is returned. Final drain waits are bounded so inherited pipes cannot block a request indefinitely.
|
|
214
225
|
|
|
226
|
+
### Timing Parameters at a Glance
|
|
227
|
+
|
|
228
|
+
Three separate timers control different things. Only one of them ever terminates a process:
|
|
229
|
+
|
|
230
|
+
| Parameter | Meaning | Terminates the process? | Default | Effective cap |
|
|
231
|
+
| :--- | :--- | :--- | :--- | :--- |
|
|
232
|
+
| `exec(yield_ms)` | How long `exec` stays inline before handing back a `process_id` and letting the command continue in the background. | No | 30,000ms | Lesser of `YIELDSHELL_MAX_YIELD_MS` and 55,000ms |
|
|
233
|
+
| `exec(timeout_ms)` | Total execution limit. On expiry the process group is sent `SIGTERM`, then `SIGKILL`, and the status becomes `timed_out`. | **Yes** | 3,600,000ms | None (`0` means unlimited) |
|
|
234
|
+
| `wait(timeout_ms)` | Maximum time a single `wait` call blocks. On expiry it returns `wait_result: "deadline_reached"` and the process keeps running. | No | 55,000ms | 55,000ms |
|
|
235
|
+
|
|
236
|
+
If `wait` returns with `status: "running"`, check `wait_result`: `"deadline_reached"` means the wait budget expired while the tracked shell or one of its descendants was still alive. The process keeps running and can be polled again.
|
|
237
|
+
|
|
215
238
|
### `stop`
|
|
216
239
|
Gracefully terminate or force kill a running process.
|
|
217
240
|
|
|
@@ -229,6 +252,10 @@ Terminal records are retained temporarily for inspection. Before each otherwise
|
|
|
229
252
|
* `include_completed` (boolean, default: `true`): If `false`, finished/stopped processes are excluded from the output.
|
|
230
253
|
* `limit` (integer, default: `50`): Maximum number of entries.
|
|
231
254
|
* **Returns**: `processes` — a list of process summary objects, each containing: `process_id`, `pid`, `name`, `command`, `cwd`, `status`, `exit_code`, `signal`, `started_at`, `ended_at`, `duration_ms`, `stdout_bytes`, `stderr_bytes`, and `stdin_error` (`null` unless initial input delivery failed).
|
|
255
|
+
* Activity fields help distinguish a genuine hang from quiet work:
|
|
256
|
+
* `last_output_at`: wall-clock time output last arrived on either stream, or `null` if none has.
|
|
257
|
+
* `idle_ms`: milliseconds since that moment, or `null` if no output has arrived. A large and growing `idle_ms` on a `running` process is the signal that it may be stuck.
|
|
258
|
+
* `latest_seq`: the process-wide cursor position just past the newest captured byte. Comparing it to a `next_seq` you hold shows how much output is waiting.
|
|
232
259
|
|
|
233
260
|
### Error Responses
|
|
234
261
|
|
|
@@ -248,14 +275,22 @@ Prune completed, stopped, timed-out, and failed process records to free memory.
|
|
|
248
275
|
|
|
249
276
|
To avoid sending duplicate data over the MCP protocol (which can consume context window space), the server implements a byte-position polling protocol:
|
|
250
277
|
|
|
251
|
-
1. Every stdout/stderr byte receives a unique position in a cursor shared by both streams.
|
|
252
|
-
2. `read`
|
|
253
|
-
3.
|
|
254
|
-
4.
|
|
255
|
-
5.
|
|
278
|
+
1. Every stdout/stderr byte receives a unique position in a cursor shared by both streams. Positions start at 1 and only ever increase.
|
|
279
|
+
2. `read`, `wait`, and `exec` all return `next_seq`, the position immediately after the range covered by the response. A response cap can therefore end safely inside a drain chunk.
|
|
280
|
+
3. **You do not have to track cursors.** A `read` or `wait` with no `since_seq` and no `tail_lines` resumes from where the last such call stopped, then advances a server-side cursor held per process. Polling in a loop therefore returns each byte exactly once. `exec`'s inline snapshot advances the same cursor, so a following `wait` continues after it.
|
|
281
|
+
4. To drive the cursor yourself instead, pass the previous `next_seq` back as `since_seq` and keep the same `streams` selection.
|
|
282
|
+
5. **A cursorless read consumes.** Once output has been delivered, a later cursorless read will not return it again. To inspect output that was already delivered, pass `since_seq=1` to re-read from the beginning, or use `tail_lines`.
|
|
283
|
+
6. Requests that pass `since_seq`, pass `tail_lines`, or narrow `streams` are treated as **out-of-band**: they neither consult nor advance the server-side cursor. This means a peek at the tail or at stderr cannot silently skip output the polling stream has not seen yet.
|
|
284
|
+
7. Two distinct flags report output the response did not carry, and they call for different responses:
|
|
285
|
+
* `capped` — the response cap stopped the read short. The remainder is still buffered; read again to continue, or pass the returned `next_seq` as `since_seq`. The `hint` field spells out the call.
|
|
286
|
+
* `evicted` — output exceeded the ring buffer's capacity before it was read and is permanently gone. Retrying cannot recover it; poll more often or raise `YIELDSHELL_MAX_BUFFER_BYTES`. Reads report eviction only when their requested range overlaps evicted data.
|
|
287
|
+
8. `latest_seq` reports how far output has advanced overall. `latest_seq > next_seq` means more is already waiting; `latest_seq == next_seq` means the caller is fully caught up.
|
|
288
|
+
9. Because retention (`YIELDSHELL_MAX_BUFFER_BYTES`, 256 KB per stream) is much larger than the per-response cap (`YIELDSHELL_MAX_OUTPUT_BYTES`, 20 KB), a cursor normally stays resolvable across polling gaps.
|
|
256
289
|
|
|
257
290
|
Cursor boundaries preserve valid UTF-8 characters. If a single character is larger than a very small requested page, that page may exceed `max_output_bytes` by at most three bytes so the cursor can make progress without corrupting the character.
|
|
258
291
|
|
|
292
|
+
Positions are never reused and never reset. Eviction does not rewind them: it advances the oldest retained position while `next_seq` keeps climbing, so a cursor is safe to hold indefinitely. A cursor pointing at data that has since been evicted is not an error — the read returns data from the earliest retained position onward and sets `evicted` to `true`, and `start_seq` shows where the returned range actually begins. Positions are scoped to one process; a cursor from one `process_id` is meaningless for another.
|
|
293
|
+
|
|
259
294
|
Incremental cursors are scoped to the selected `streams` value. Keep the same stream selection while advancing a cursor; switching from `stdout`-only or `stderr`-only polling to another selection can intentionally skip bytes from the previously unselected stream.
|
|
260
295
|
|
|
261
296
|
When a process exits normally, `exec`/`wait` responses include output drained through stdout/stderr EOF. If a descendant keeps inherited stdout/stderr open after the tracked process exits, the server stops waiting on those inherited pipes to avoid indefinite blocking; output written only by that descendant after the tracked process exits is not part of the managed process result.
|
|
@@ -270,7 +305,8 @@ Configure the server by setting these environment variables prior to launch:
|
|
|
270
305
|
|---|---|---|
|
|
271
306
|
| `YIELDSHELL_DEFAULT_CWD` | Current directory | The fallback working directory for commands. |
|
|
272
307
|
| `YIELDSHELL_ALLOWED_CWDS` | *(none)* | A list of allowed directory paths separated by `os.pathsep` (e.g., `:` on UNIX, `;` on Windows). If set, all command execution paths must resolve inside one of these roots. |
|
|
273
|
-
| `YIELDSHELL_MAX_OUTPUT_BYTES` | `20000` | The default and maximum
|
|
308
|
+
| `YIELDSHELL_MAX_OUTPUT_BYTES` | `20000` | The default and maximum bytes any single response may return across stdout and stderr. This is a response cap only; it does not limit what is retained. Nonpositive or nonnumeric values use the default. |
|
|
309
|
+
| `YIELDSHELL_MAX_BUFFER_BYTES` | `262144` | Capacity of each per-process stdout/stderr ring buffer. Retention is deliberately larger than one response so `since_seq` cursors and `tail_lines` still resolve after a gap between polls. Raise it for very chatty commands, at a cost of roughly this value × 2 streams × live processes in memory. Nonpositive or nonnumeric values use the default. |
|
|
274
310
|
| `YIELDSHELL_MAX_PROCESSES` | `50` | Maximum concurrent live managed process groups, including descendants that outlive a completed shell. Spawning a new command when this limit is reached returns `failed_to_start`. Nonpositive or nonnumeric values use the default. |
|
|
275
311
|
| `YIELDSHELL_DEFAULT_YIELD_MS` | `30000` | Fallback delay before auto-yielding. Effective yields are also capped at 55,000ms. |
|
|
276
312
|
| `YIELDSHELL_MAX_YIELD_MS` | `300000` | Configured maximum for `yield_ms`; the effective maximum is the lesser of this value and 55,000ms. |
|
|
@@ -23,6 +23,12 @@ class Config:
|
|
|
23
23
|
self.max_output_bytes: int = _parse_positive_int(
|
|
24
24
|
os.environ.get("YIELDSHELL_MAX_OUTPUT_BYTES", ""), 20000
|
|
25
25
|
)
|
|
26
|
+
# Retention is independent of the per-response cap: a process keeps
|
|
27
|
+
# far more output than any single response returns, so incremental
|
|
28
|
+
# cursors and tail reads still resolve after a quiet polling gap.
|
|
29
|
+
self.max_buffer_bytes: int = _parse_positive_int(
|
|
30
|
+
os.environ.get("YIELDSHELL_MAX_BUFFER_BYTES", ""), 262144
|
|
31
|
+
)
|
|
26
32
|
self.max_processes: int = _parse_positive_int(
|
|
27
33
|
os.environ.get("YIELDSHELL_MAX_PROCESSES", ""), 50
|
|
28
34
|
)
|
|
@@ -25,6 +25,8 @@ class ManagedProcess:
|
|
|
25
25
|
"stdin_error",
|
|
26
26
|
"process_group_id",
|
|
27
27
|
"process_group_exited",
|
|
28
|
+
"last_output_at",
|
|
29
|
+
"read_cursor",
|
|
28
30
|
"_seq_source",
|
|
29
31
|
"_timeout_triggered",
|
|
30
32
|
"sensitive_env",
|
|
@@ -34,7 +36,7 @@ class ManagedProcess:
|
|
|
34
36
|
self,
|
|
35
37
|
info: ProcessInfo,
|
|
36
38
|
proc: asyncio.subprocess.Process,
|
|
37
|
-
|
|
39
|
+
max_buffer_bytes: int,
|
|
38
40
|
process_group_id: int | None = None,
|
|
39
41
|
sensitive_env: SensitiveEnv = (),
|
|
40
42
|
) -> None:
|
|
@@ -42,9 +44,13 @@ class ManagedProcess:
|
|
|
42
44
|
self.proc = proc
|
|
43
45
|
self.process_group_id = process_group_id
|
|
44
46
|
self.process_group_exited = False
|
|
47
|
+
self.last_output_at: float | None = None
|
|
48
|
+
# Server-side resume point for reads that pass no explicit cursor, so
|
|
49
|
+
# a caller can poll without tracking next_seq itself.
|
|
50
|
+
self.read_cursor: int = 1
|
|
45
51
|
self._seq_source: list[int] = [1]
|
|
46
|
-
self.stdout_buf = RingBuffer(
|
|
47
|
-
self.stderr_buf = RingBuffer(
|
|
52
|
+
self.stdout_buf = RingBuffer(max_buffer_bytes, seq_source=self._seq_source)
|
|
53
|
+
self.stderr_buf = RingBuffer(max_buffer_bytes, seq_source=self._seq_source)
|
|
48
54
|
self.drain_stdout: asyncio.Task[None] | None = None
|
|
49
55
|
self.drain_stderr: asyncio.Task[None] | None = None
|
|
50
56
|
self.completion_event = asyncio.Event()
|
|
@@ -55,3 +61,8 @@ class ManagedProcess:
|
|
|
55
61
|
self.stdin_error: str | None = None
|
|
56
62
|
self._timeout_triggered = False
|
|
57
63
|
self.sensitive_env = sensitive_env
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def latest_seq(self) -> int:
|
|
67
|
+
"""Position just past the newest byte captured on either stream."""
|
|
68
|
+
return self._seq_source[0]
|