docs(shell): note the PTY layer's 30s yield ceiling

The SDK clamps every PTY yield to 30s and floors an empty poll at 5s. Record that where the defaults are set, and tell the agent a slower command still backgrounds so it harvests it with one poll per 30s instead of asking for an unreachable yield.
This commit is contained in:
Alex Schapiro 2026-08-25 18:47:56 +00:00
parent 3fea23de7e
commit 7ef555ad19
3 changed files with 19 additions and 8 deletions

View file

@ -188,9 +188,11 @@ EFFICIENCY TACTICS:
the instant there is new output or the process exits, or give the original
`exec_command` a bigger `yield_time_ms` up front so it doesn't background.
- When you expect a command to take a while (a scan like `nmap`/`nuclei`/`ffuf`,
a build, a long crawl), estimate its runtime and pass that as
`yield_time_ms` on the first `exec_command` — one call that waits beats a
backgrounded process you then poll for many turns. Default is 30s.
a build, a long crawl), pass the time you expect to need as `yield_time_ms`
on the first `exec_command` — one call that waits beats a backgrounded
process you then poll for many turns. The shell yields at most 30s per call
(the default), so anything slower than that still backgrounds: harvest it
with one `write_stdin(chars="")` poll per 30s rather than many short ones.
- Before importing a third-party Python library, make sure it is installed. The
sandbox's `python3` runs inside a preconfigured virtualenv that ships
`requests`, `httpx`, `beautifulsoup4` (bs4), `lxml`, `pyjwt`, and

View file

@ -109,13 +109,18 @@ class ShellSettings(BaseSettings):
after only 250ms on a ``write_stdin`` poll and 10s on ``exec_command``.
Raising these defaults lets one call return a meaningful result instead of a
no-op round-trip. An explicit ``yield_time_ms`` from the model always wins.
The SDK's PTY layer clamps every yield to 30s, so a larger value here would
be silently ineffective; keep both yields at or below that ceiling.
"""
model_config = _BASE_CONFIG
# Default yield for exec_command when the model omits yield_time_ms.
# Default yield for exec_command when the model omits yield_time_ms. 30s is
# the most the PTY layer honours, so this sits right at that ceiling.
exec_yield_ms: int = Field(default=30_000, gt=0, alias="STRIX_SHELL_EXEC_YIELD_MS")
# Default yield for an empty (polling) write_stdin call.
# Default yield for an empty (polling) write_stdin call. The SDK already
# floors an empty poll at 5s; this trades a little latency for far fewer turns.
write_stdin_poll_yield_ms: int = Field(
default=20_000, gt=0, alias="STRIX_SHELL_WRITE_STDIN_POLL_YIELD_MS"
)

View file

@ -164,16 +164,20 @@ async def test_wrap_exec_command_default_does_not_depend_on_the_binary(cmd: str)
@pytest.mark.asyncio
async def test_wrap_exec_command_preserves_long_explicit_yield() -> None:
async def test_wrap_exec_command_preserves_longer_explicit_yield() -> None:
"""A slow command gets the yield the agent asked for, not a guessed one.
The SDK's PTY layer clamps anything above 30s, so a longer wait than that
cannot be bought with a bigger argument."""
captured: dict[str, str] = {}
wrapped = factory._wrap_exec_command(_capturing_exec_tool(captured))
await wrapped.on_invoke_tool(
cast("Any", None),
json.dumps({"cmd": "nmap -p- example.com", "yield_time_ms": 300_000}),
json.dumps({"cmd": "nmap -p- example.com", "yield_time_ms": 25_000}),
)
assert json.loads(captured["raw_input"])["yield_time_ms"] == 300_000
assert json.loads(captured["raw_input"])["yield_time_ms"] == 25_000
@pytest.mark.asyncio