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.
Files changed (26) hide show
  1. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/PKG-INFO +50 -14
  2. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/README.md +49 -13
  3. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/pyproject.toml +1 -1
  4. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/config.py +6 -0
  5. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/process/managed.py +14 -3
  6. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/process/manager.py +197 -66
  7. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/process/ring_buffer.py +104 -2
  8. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/server.py +52 -3
  9. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/tests/test_config.py +16 -0
  10. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/tests/test_integration.py +263 -12
  11. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/tests/test_lifecycle_hardening.py +128 -2
  12. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/tests/test_protocol.py +66 -0
  13. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/tests/test_ring_buffer.py +141 -12
  14. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/.gitignore +0 -0
  15. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/__init__.py +0 -0
  16. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/__main__.py +0 -0
  17. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/policy.py +0 -0
  18. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/process/__init__.py +0 -0
  19. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/process/spawn.py +0 -0
  20. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/security.py +0 -0
  21. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/side_effects.py +0 -0
  22. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/src/mcp_yieldshell/types.py +0 -0
  23. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/tests/__init__.py +0 -0
  24. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/tests/test_release.py +0 -0
  25. {mcp_yieldshell-0.5.0 → mcp_yieldshell-0.6.0}/tests/test_security.py +0 -0
  26. {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.5.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 retained per stdout/stderr ring buffer and returned in total across both response streams. Subject to the `YIELDSHELL_MAX_OUTPUT_BYTES` cap.
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
- * `completed`: Process finished within `yield_ms`. Returns exit code, stdout, and stderr. If the response is truncated and terminal-process retention is enabled, it also returns `process_id` so retained output can be inspected with `read`.
179
- * `backgrounded`: Process auto-yielded. Returns `process_id`, `pid`, a snapshot of initial stdout/stderr, `duration_ms`, `truncated`, and a `message` string describing that the process is running in the background.
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`, `next_seq` (byte-position cursor to use in subsequent `since_seq` reads), `truncated` flag, 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`.
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` returns `next_seq`, the position immediately after the selected-stream range covered by the response. A response cap can therefore end safely inside a drain chunk.
261
- 3. To retrieve the next output without gaps, call `read` with `since_seq` set to the previously returned `next_seq` and keep the same `streams` selection.
262
- 4. Omitting `since_seq` returns the entire contents currently stored in the buffer (clamped by `max_output_bytes`).
263
- 5. If output exceeds the ring buffer's capacity between reads, older data is evicted and `since_seq` may no longer be available. In that case, `truncated` is set to `true` and the read returns data from the earliest retained position onward. Later reads report truncation only when their requested range overlaps evicted data.
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 capacity of the ring buffers for stdout/stderr. Nonpositive or nonnumeric values use the default. |
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 retained per stdout/stderr ring buffer and returned in total across both response streams. Subject to the `YIELDSHELL_MAX_OUTPUT_BYTES` cap.
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
- * `completed`: Process finished within `yield_ms`. Returns exit code, stdout, and stderr. If the response is truncated and terminal-process retention is enabled, it also returns `process_id` so retained output can be inspected with `read`.
171
- * `backgrounded`: Process auto-yielded. Returns `process_id`, `pid`, a snapshot of initial stdout/stderr, `duration_ms`, `truncated`, and a `message` string describing that the process is running in the background.
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`, `next_seq` (byte-position cursor to use in subsequent `since_seq` reads), `truncated` flag, 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`.
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` returns `next_seq`, the position immediately after the selected-stream range covered by the response. A response cap can therefore end safely inside a drain chunk.
253
- 3. To retrieve the next output without gaps, call `read` with `since_seq` set to the previously returned `next_seq` and keep the same `streams` selection.
254
- 4. Omitting `since_seq` returns the entire contents currently stored in the buffer (clamped by `max_output_bytes`).
255
- 5. If output exceeds the ring buffer's capacity between reads, older data is evicted and `since_seq` may no longer be available. In that case, `truncated` is set to `true` and the read returns data from the earliest retained position onward. Later reads report truncation only when their requested range overlaps evicted data.
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 capacity of the ring buffers for stdout/stderr. Nonpositive or nonnumeric values use the default. |
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. |
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcp-yieldshell"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  description = "A drop-in shell MCP that auto-yields long-running commands into managed background processes."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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
- max_output_bytes: int,
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(max_output_bytes, seq_source=self._seq_source)
47
- self.stderr_buf = RingBuffer(max_output_bytes, seq_source=self._seq_source)
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]