diff --git a/CHANGELOG.md b/CHANGELOG.md index e2e9d85c..ab05e2df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/aipass/drone/apps/drone.py b/src/aipass/drone/apps/drone.py index 01604f24..89b07d54 100644 --- a/src/aipass/drone/apps/drone.py +++ b/src/aipass/drone/apps/drone.py @@ -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 ", "Remove a custom command") table.add_row("rm [...]", "Contained safe-delete (project + tmp)") - table.add_row("--timeout ", "Override subprocess timeout (default 30s)") + table.add_row("--drone-timeout ", "Override subprocess timeout (default 30s)") table.add_row("--help", "Show this help") table.add_row("--version", "Show version") diff --git a/src/aipass/drone/apps/handlers/executor.py b/src/aipass/drone/apps/handlers/executor.py index a469bd59..55a28150 100644 --- a/src/aipass/drone/apps/handlers/executor.py +++ b/src/aipass/drone/apps/handlers/executor.py @@ -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 @ --timeout " + f" Override with: drone @ --drone-timeout " ) from e except FileNotFoundError as e: raise CommandExecutionError(f"Executable not found: {executable!r}") from e diff --git a/src/aipass/drone/tests/test_cli_routing.py b/src/aipass/drone/tests/test_cli_routing.py index 0ae425f5..1f758972 100644 --- a/src/aipass/drone/tests/test_cli_routing.py +++ b/src/aipass/drone/tests/test_cli_routing.py @@ -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 diff --git a/src/aipass/drone/tests/test_executor.py b/src/aipass/drone/tests/test_executor.py index e78bdec6..798e5001 100644 --- a/src/aipass/drone/tests/test_executor.py +++ b/src/aipass/drone/tests/test_executor.py @@ -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)