mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
ab7d56e0ba
Restore 'start a dev server, use it in a later call' as an explicit opt-in after #816 made bash reap its whole process group on return. The surface mirrors the dominant coding-agent convention: bash(run_in_background=true) returns a bash_N handle immediately; bash_output(id, filter?) returns only output produced since the previous read plus status and exit code; kill_shell(id) terminates the shell's whole process group. - Per-session BackgroundShellRegistry: capped rolling line buffer with drop-oldest gap accounting, exit-order record pruning, owner scoping for task_agents (shells reaped when the agent finishes), liveness-guarded group kills (a stale pgid is never signalled), budgeted teardown joins. - Exit notices ride a shared external-event rail (sanitize, soft cap, channel 'any', idle wake) now common to watch fires; a new 'quiet' NudgeQueue channel lets a user cancel defer pending notices without letting them re-wake the stopped workstream, and failed wake delivery re-queues external notices seq- and predicate-intact without re-arming the wake gate. - The bash_output filter runs in a killable subprocess: sre holds the GIL for an entire search, so no in-process timeout can bound a hostile pattern. Scrubbed child env, pinned UTF-8 pipes, honest timeout-vs- helper-failure error taxonomy, per-line match window with explicit clipping notes; a failed filter never consumes the delta. - run_in_background rides the bash intent-judge projection; bash_output is exempt from the repeat warning but still recorded so interleaved polls keep breaking other tools' streaks; all bash boolean args share one lenient coercion dialect. - Shells survive generation cancel and die with the workstream: every teardown path funnels through ChatSession.close(); CLI exit and the server lifespan now close every loaded session, signal-first and Ctrl-C-safe, so nothing detached outlives a graceful shutdown.
44 lines
1.2 KiB
Python
44 lines
1.2 KiB
Python
"""Shared process/polling helpers for the bash + background-shell suites.
|
|
|
|
One copy instead of three: ``test_bash_tool_background_hang``,
|
|
``test_background_shells`` and ``test_bash_background_tool`` all assert on
|
|
process liveness and poll for asynchronous state. Leading underscore so
|
|
pytest doesn't collect it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import os
|
|
import signal
|
|
import time
|
|
|
|
|
|
def pid_alive(pid: int) -> bool:
|
|
try:
|
|
os.kill(pid, 0)
|
|
except ProcessLookupError:
|
|
return False
|
|
except PermissionError:
|
|
return True
|
|
return True
|
|
|
|
|
|
def kill_pid(pid: int) -> None:
|
|
with contextlib.suppress(OSError):
|
|
os.kill(pid, signal.SIGKILL)
|
|
|
|
|
|
def poll_until(predicate, timeout=10.0, interval=0.05):
|
|
"""Poll ``predicate`` until truthy or ``timeout``; RETURNS the last value
|
|
(falsy on timeout — assert at the call site). Deliberately named apart
|
|
from ``tests/_helpers.wait_until``, which RAISES on timeout: two
|
|
same-named helpers with opposite failure semantics invite silently-green
|
|
tests."""
|
|
deadline = time.monotonic() + timeout
|
|
value = predicate()
|
|
while not value and time.monotonic() < deadline:
|
|
time.sleep(interval)
|
|
value = predicate()
|
|
return value
|