fix(drone): namespace subprocess timeout to --drone-timeout — plain --timeout passes through to modules (watchdog 600s regression, live repro x2). Regression test, 879 green, CHANGELOG both fixes

This commit is contained in:
AIOSAI
2026-07-16 22:04:03 -07:00
parent b8f6fb8dad
commit e412cfed14
5 changed files with 48 additions and 20 deletions
+19
View File
@@ -73,6 +73,25 @@ PyPI version — not the changelog header.
### Fixed
- **Plan-number memory search hits the exact plan.** Searching a plan ID
('DPLAN-0244', 'fplan 0332' — any case, dash or space) now pins the exact
plan as the top result at 100%, via a metadata lookup on the vector store's
source-file field instead of embedding similarity (which treats all plan IDs
as near-identical strings and never surfaced the target). Patrick ruling:
searching a plan number must return that plan first. Semantic search quality
for normal queries is unchanged. Also purged 193 junk vectors — throwaway
probe/flaky test plans from scratchpad sessions (dv4 batch, probe_test_plan,
throwaway_e2e_proof) that had leaked into the store. 1011 memory tests green.
- **drone --timeout collision: router flag swallowed module flags.** The
DPLAN-0245 subprocess-timeout flag consumed the first `--timeout` token
anywhere in argv, so module-level flags silently vanished — watchdog's
`--timeout 1800` never arrived and long watches died at the 600s default
(live repro x2). Drone's flag is now namespaced `--drone-timeout`; plain
`--timeout` passes through untouched to the target module, with a regression
test pinning the passthrough. Per-command overrides intact. 879 drone tests
green, seedgo 100%.
- **@memory command routing eaten by the new governance module.** The
governance module shipped in Track 2 had the wrong `handle_command`
signature (`args: list` instead of `command: str, args: list`) and always
+5 -5
View File
@@ -52,16 +52,16 @@ INTERACTIVE_BRANCHES = ("cli", "backup")
def _extract_timeout(args: list[str]) -> tuple[list[str], int | None]:
"""Extract --timeout N from an arg list. Returns (cleaned_args, timeout_or_None)."""
if "--timeout" not in args:
"""Extract --drone-timeout N from an arg list. Returns (cleaned_args, timeout_or_None)."""
if "--drone-timeout" not in args:
return args, None
idx = args.index("--timeout")
idx = args.index("--drone-timeout")
if idx + 1 >= len(args):
return args, None
try:
timeout = int(args[idx + 1])
except ValueError:
logger.info("--timeout value %r is not an integer, ignoring", args[idx + 1])
logger.info("--drone-timeout value %r is not an integer, ignoring", args[idx + 1])
return args, None
return args[:idx] + args[idx + 2 :], timeout
@@ -107,7 +107,7 @@ def show_help() -> None:
table.add_row("list", "List registered custom commands")
table.add_row("remove <name>", "Remove a custom command")
table.add_row("rm <path> [<path>...]", "Contained safe-delete (project + tmp)")
table.add_row("--timeout <seconds>", "Override subprocess timeout (default 30s)")
table.add_row("--drone-timeout <seconds>", "Override subprocess timeout (default 30s)")
table.add_row("--help", "Show this help")
table.add_row("--version", "Show version")
+1 -1
View File
@@ -107,7 +107,7 @@ def execute_command(
except subprocess.TimeoutExpired as e:
raise CommandExecutionError(
f"Command timed out after {timeout}s: {' '.join(full_cmd)}\n"
f" Override with: drone @<target> <command> --timeout <seconds>"
f" Override with: drone @<target> <command> --drone-timeout <seconds>"
) from e
except FileNotFoundError as e:
raise CommandExecutionError(f"Executable not found: {executable!r}") from e
+21 -12
View File
@@ -911,10 +911,10 @@ class TestAipassIntercept:
class TestExtractTimeout:
"""Tests for --timeout flag extraction from arg lists."""
"""Tests for --drone-timeout flag extraction from arg lists."""
def test_no_flag(self) -> None:
"""Args without --timeout pass through unchanged."""
"""Args without --drone-timeout pass through unchanged."""
from aipass.drone.apps.drone import _extract_timeout
args = ["close", "FPLAN-0313"]
@@ -923,43 +923,43 @@ class TestExtractTimeout:
assert timeout is None
def test_flag_at_end(self) -> None:
"""--timeout N at end of args is extracted."""
"""--drone-timeout N at end of args is extracted."""
from aipass.drone.apps.drone import _extract_timeout
cleaned, timeout = _extract_timeout(["process-plans", "--timeout", "120"])
cleaned, timeout = _extract_timeout(["process-plans", "--drone-timeout", "120"])
assert cleaned == ["process-plans"]
assert timeout == 120
def test_flag_at_start(self) -> None:
"""--timeout N at start of args is extracted."""
"""--drone-timeout N at start of args is extracted."""
from aipass.drone.apps.drone import _extract_timeout
cleaned, timeout = _extract_timeout(["--timeout", "90", "close", "FPLAN-0313"])
cleaned, timeout = _extract_timeout(["--drone-timeout", "90", "close", "FPLAN-0313"])
assert cleaned == ["close", "FPLAN-0313"]
assert timeout == 90
def test_flag_in_middle(self) -> None:
"""--timeout N in the middle of args is extracted."""
"""--drone-timeout N in the middle of args is extracted."""
from aipass.drone.apps.drone import _extract_timeout
cleaned, timeout = _extract_timeout(["close", "--timeout", "60", "FPLAN-0313"])
cleaned, timeout = _extract_timeout(["close", "--drone-timeout", "60", "FPLAN-0313"])
assert cleaned == ["close", "FPLAN-0313"]
assert timeout == 60
def test_flag_without_value(self) -> None:
"""--timeout at end with no value returns None and leaves args."""
"""--drone-timeout at end with no value returns None and leaves args."""
from aipass.drone.apps.drone import _extract_timeout
args = ["close", "--timeout"]
args = ["close", "--drone-timeout"]
cleaned, timeout = _extract_timeout(args)
assert cleaned == args
assert timeout is None
def test_flag_non_integer_value(self) -> None:
"""--timeout with non-integer value returns None and leaves args."""
"""--drone-timeout with non-integer value returns None and leaves args."""
from aipass.drone.apps.drone import _extract_timeout
args = ["close", "--timeout", "abc"]
args = ["close", "--drone-timeout", "abc"]
cleaned, timeout = _extract_timeout(args)
assert cleaned == args
assert timeout is None
@@ -971,3 +971,12 @@ class TestExtractTimeout:
cleaned, timeout = _extract_timeout([])
assert cleaned == []
assert timeout is None
def test_plain_timeout_passes_through(self) -> None:
"""--timeout (without drone- prefix) is NOT consumed — passes to target."""
from aipass.drone.apps.drone import _extract_timeout
args = ["watchdog", "agent", "@memory", "--timeout", "1800"]
cleaned, timeout = _extract_timeout(args)
assert cleaned == args
assert timeout is None
+2 -2
View File
@@ -446,11 +446,11 @@ class TestTimeoutErrorMessage:
def test_timeout_error_includes_override_hint(self, temp_test_dir: Path):
"""The timeout error message mentions --timeout."""
with pytest.raises(CommandExecutionError, match="--timeout") as exc_info:
with pytest.raises(CommandExecutionError, match="--drone-timeout") as exc_info:
execute_command(
sys.executable,
["-c", "import time; time.sleep(10)"],
cwd=str(temp_test_dir),
timeout=1,
)
assert "--timeout" in str(exc_info.value)
assert "--drone-timeout" in str(exc_info.value)