From 0529a4e7c4c78ed1df0d68ed6ab33a6184f9ad6f Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Sun, 19 Jul 2026 09:32:42 -0700 Subject: [PATCH] =?UTF-8?q?fix(ai=5Fmail):=20inbox-poller=20spawn=5Fagent?= =?UTF-8?q?=20now=20passes=20--model=20DEFAULT=5FMODEL=20(sonnet).=20The?= =?UTF-8?q?=20daemon=20asymmetry=20Vera=20flagged=20lived=20in=20ai=5Fmail?= =?UTF-8?q?'s=20own=20handlers/dispatch/daemon.py=20(name=20collision,=20n?= =?UTF-8?q?ot=20@daemon=20branch=20=E2=80=94=20their=20scheduled=20wakes?= =?UTF-8?q?=20import=20wake=5Fbranch=20directly=20and=20were=20covered=20b?= =?UTF-8?q?y=20efb530e0).=20DEFAULT=5FMODEL=20imported=20from=20wake.py,?= =?UTF-8?q?=20single=20source;=20200k=20window=20pin=20already=20covered?= =?UTF-8?q?=20via=20shared=20dispatch=5Fmonitor=20wrapper.=20Test=20assert?= =?UTF-8?q?s=20--model=20in=20captured=20spawn=20cmd.=20778=20tests=20gree?= =?UTF-8?q?n,=20seedgo=20100%,=20both=20re-run=20by=20devpulse.=20Investig?= =?UTF-8?q?ated=20by=20@daemon,=20built=20by=20@ai=5Fmail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 +++- .../ai_mail/apps/handlers/dispatch/daemon.py | 3 ++ src/aipass/ai_mail/tests/test_daemon.py | 42 +++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b791f7d..3072e334 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,8 +20,12 @@ PyPI version — not the changelog header. spawned agent — Sonnet 5 is 1M-context native, and without the pin every dispatched agent would silently inherit a 1M window. E2E-proven: a live dispatched probe reported `claude-sonnet-5` + `WINDOW=200000` from inside. -- Known gap (daemon-side, next): daemon.py `spawn_agent()` builds its claude - command with no `--model` flag, bypassing wake.py resolution entirely. +- Follow-up landed same morning: the "daemon gap" was a name collision — + the unpatched `spawn_agent()` was ai_mail's own inbox-poller + (`handlers/dispatch/daemon.py`), not the @daemon branch. It now passes + `--model DEFAULT_MODEL` (imported from wake.py, single source); the 200k pin + was already covered via the shared dispatch_monitor wrapper. @daemon's + scheduled wakes import `wake_branch` directly and were covered from the start. **fix(spawn, commons, prax, hooks)** — S304 audit fix campaign, Track A (DPLAN-0250, four owner dispatches verified + committed by devpulse): diff --git a/src/aipass/ai_mail/apps/handlers/dispatch/daemon.py b/src/aipass/ai_mail/apps/handlers/dispatch/daemon.py index 76212590..3cb671c5 100644 --- a/src/aipass/ai_mail/apps/handlers/dispatch/daemon.py +++ b/src/aipass/ai_mail/apps/handlers/dispatch/daemon.py @@ -34,6 +34,7 @@ from aipass.ai_mail.apps.handlers.json import json_handler from aipass.ai_mail.apps.handlers.dispatch.status import log_dispatch from aipass.ai_mail.apps.handlers.paths import find_repo_root from aipass.ai_mail.apps.handlers.dispatch.test_token import scan_and_ack_test_emails +from aipass.ai_mail.apps.handlers.dispatch.wake import DEFAULT_MODEL # Infrastructure paths @@ -408,6 +409,8 @@ def spawn_agent( "-c", "-p", prompt, + "--model", + DEFAULT_MODEL, "--max-turns", str(max_turns), "--permission-mode", diff --git a/src/aipass/ai_mail/tests/test_daemon.py b/src/aipass/ai_mail/tests/test_daemon.py index 96497fb1..5e9df6cc 100644 --- a/src/aipass/ai_mail/tests/test_daemon.py +++ b/src/aipass/ai_mail/tests/test_daemon.py @@ -1317,6 +1317,48 @@ def test_spawn_agent_strips_claude_env_vars(tmp_path, monkeypatch): assert captured_env.get("AIPASS_SESSION_TYPE") == "daemon" +def test_spawn_agent_claude_cmd_includes_model_flag(tmp_path): + """Poller-triggered spawn passes --model DEFAULT_MODEL to the claude invocation.""" + branch_path = tmp_path / "branch" + branch_path.mkdir() + (branch_path / "logs").mkdir() + + message = {"from": "@devpulse", "id": "msg1", "subject": "Test task"} + config = {"max_turns_per_wake": 50} + state = {"daily_counts": {}, "session_cycles": {}} + + captured_args = [] + + def capture_popen(*args, **kwargs): + captured_args.append(args[0] if args else kwargs.get("args")) + mock_proc = MagicMock() + mock_proc.pid = 22222 + return mock_proc + + with ( + patch( + "aipass.ai_mail.apps.handlers.dispatch.daemon.subprocess.Popen", + side_effect=capture_popen, + ), + patch( + "aipass.ai_mail.apps.handlers.dispatch.daemon._acquire_lock", + return_value=(True, "Lock acquired"), + ), + patch("aipass.ai_mail.apps.handlers.dispatch.daemon.log_dispatch"), + patch( + "aipass.ai_mail.apps.handlers.dispatch.daemon.send_notification", + create=True, + ), + ): + result = spawn_agent(branch_path, "@testbranch", message, config, state) + + assert result is True + monitor_cmd = captured_args[0] + assert "--model" in monitor_cmd + model_idx = monitor_cmd.index("--model") + assert monitor_cmd[model_idx + 1] == daemon_mod.DEFAULT_MODEL + + def test_spawn_agent_prompt_includes_reply_id(tmp_path): """Prompt includes explicit reply command with the dispatch email ID.""" branch_path = tmp_path / "branch"