From efb530e0b2362275ff3ac02831fa1f6c95fb66f7 Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Sun, 19 Jul 2026 09:15:04 -0700 Subject: [PATCH] =?UTF-8?q?feat(ai=5Fmail):=20dispatch=20default=20Sonnet?= =?UTF-8?q?=205=20=E2=80=94=20alias=20pass-through=20+=20200k=20context=20?= =?UTF-8?q?pin.=20MODEL=5FMAP=20replaced=20by=20KNOWN=5FMODEL=5FALIASES=20?= =?UTF-8?q?pass-through=20(CLI=20resolves=20latest-in-class,=20map=20can?= =?UTF-8?q?=20never=20go=20stale),=20DEFAULT=5FMODEL=20opus->sonnet,=20dis?= =?UTF-8?q?patch=5Fmonitor=20pins=20CLAUDE=5FCODE=5FAUTO=5FCOMPACT=5FWINDO?= =?UTF-8?q?W=3D200000=20per=20spawn=20(Sonnet=205=20is=201M-native=20?= =?UTF-8?q?=E2=80=94=20without=20pin=20agents=20inherit=201M=20window).=20?= =?UTF-8?q?E2E:=20live=20@cli=20dispatch=20probe=20reported=20claude-sonne?= =?UTF-8?q?t-5=20+=20WINDOW=3D200000=20from=20inside=20the=20spawned=20ses?= =?UTF-8?q?sion.=20777=20tests=20green,=20seedgo=20100%,=20both=20re-run?= =?UTF-8?q?=20by=20devpulse.=20Daemon=20spawn=5Fagent=20--model=20gap=20co?= =?UTF-8?q?nfirmed,=20handled=20separately.=20Patrick=20ruling=20S326,=20b?= =?UTF-8?q?uilt=20by=20@ai=5Fmail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 12 ++++++++ .../handlers/dispatch/dispatch_monitor.py | 3 ++ .../ai_mail/apps/handlers/dispatch/wake.py | 16 ++++------- src/aipass/ai_mail/tests/test_wake.py | 28 +++++++++---------- 4 files changed, 34 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 922837f3..8b791f7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,18 @@ PyPI version — not the changelog header. ## [2026-07-19] +**feat(ai_mail)** — dispatched agents default to Sonnet 5 (Patrick ruling S326): + +- wake.py model resolution passes bare aliases (`sonnet`/`opus`/`haiku`) + straight to the Claude CLI, which resolves latest-in-class — the pinned-ID + MODEL_MAP is gone and can never go stale again. Default flips opus → sonnet. +- dispatch_monitor pins `CLAUDE_CODE_AUTO_COMPACT_WINDOW=200000` on every + 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. + **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/dispatch_monitor.py b/src/aipass/ai_mail/apps/handlers/dispatch/dispatch_monitor.py index 60d32d4e..bcdf12ca 100644 --- a/src/aipass/ai_mail/apps/handlers/dispatch/dispatch_monitor.py +++ b/src/aipass/ai_mail/apps/handlers/dispatch/dispatch_monitor.py @@ -449,6 +449,9 @@ def main(): for key in list(spawn_env.keys()): if key.startswith("CLAUDE") or key == "AIPASS_BOT_ID": spawn_env.pop(key) + # Pin agent context window to 200k (Sonnet 5 is 1M native; without this, + # agents inherit 1M which causes cost + runaway risk). + spawn_env["CLAUDE_CODE_AUTO_COMPACT_WINDOW"] = "200000" # Strip caller identity vars to prevent dispatch context leakage. spawn_env.pop("AIPASS_CALLER_BRANCH", None) spawn_env.pop("AIPASS_CALLER_CWD", None) diff --git a/src/aipass/ai_mail/apps/handlers/dispatch/wake.py b/src/aipass/ai_mail/apps/handlers/dispatch/wake.py index 9501aac7..aacc7b8b 100644 --- a/src/aipass/ai_mail/apps/handlers/dispatch/wake.py +++ b/src/aipass/ai_mail/apps/handlers/dispatch/wake.py @@ -63,13 +63,9 @@ MONITOR_SCRIPT = Path(__file__).parent / "dispatch_monitor.py" # Default prompt when no custom message provided DEFAULT_PROMPT = "Hi. Check inbox, process new emails, update memories when done." -# Model shorthand mapping -MODEL_MAP = { - "sonnet": "claude-sonnet-4-6", - "opus": "claude-opus-4-6", - "haiku": "claude-haiku-4-5-20251001", -} -DEFAULT_MODEL = "opus" +# Model aliases — passed directly to claude CLI which resolves latest-in-class. +KNOWN_MODEL_ALIASES: frozenset = frozenset({"sonnet", "opus", "haiku"}) +DEFAULT_MODEL = "sonnet" # Branches that cannot be woken manually by cross-branch drone commands. # Dispatch-send path (dispatch.py._orchestrate_dispatch_send) bypasses this check. @@ -591,8 +587,8 @@ def wake_branch( config = _load_config() max_turns = config.get("max_turns_per_wake", 100) - # Resolve model: shorthand -> full ID, or pass through if already a full ID - resolved_model = MODEL_MAP.get(model or DEFAULT_MODEL, model or MODEL_MAP[DEFAULT_MODEL]) + # Pass model directly to CLI — aliases resolve latest-in-class automatically + resolved_model = model or DEFAULT_MODEL lock_file_path = str(branch_path / ".ai_mail.local" / ".dispatch.lock") if custom_message: @@ -781,7 +777,7 @@ if __name__ == "__main__": print(" --fresh Start fresh session (claude -p) instead of resuming (claude -c -p)") print(" --auto Respect autonomous_pause (used by daemon). Manual wake ignores it.") print(" --sender @branch Set return-to-sender for bounce emails (default: @devpulse)") - print(" --model NAME Model to use: opus (default), sonnet, haiku, or full model ID") + print(" --model NAME Model to use: sonnet (default), opus, haiku, or full model ID") print() print("Output: Step-by-step status of the dispatch pipeline:") print(" ✅ resolve → @branch found at /path/to/branch") diff --git a/src/aipass/ai_mail/tests/test_wake.py b/src/aipass/ai_mail/tests/test_wake.py index e7cef1de..eda7b98d 100644 --- a/src/aipass/ai_mail/tests/test_wake.py +++ b/src/aipass/ai_mail/tests/test_wake.py @@ -29,7 +29,7 @@ from aipass.ai_mail.apps.handlers.dispatch.wake import ( _find_claude_bin, resolve_branch, DispatchStatus, - MODEL_MAP, + KNOWN_MODEL_ALIASES, DEFAULT_MODEL, _acquire_lock, _load_config, @@ -572,24 +572,22 @@ def _fake_open_factory(real_status_path, mapping): # --- Model flag tests --------------------------------------------------- -def test_model_map_has_expected_entries(): - """MODEL_MAP should contain sonnet, opus, haiku shorthand mappings.""" - assert "sonnet" in MODEL_MAP - assert "opus" in MODEL_MAP - assert "haiku" in MODEL_MAP - assert "claude-sonnet-4-6" in MODEL_MAP["sonnet"] - assert "claude-opus-4-6" in MODEL_MAP["opus"] +def test_known_model_aliases_has_expected_entries(): + """KNOWN_MODEL_ALIASES should contain sonnet, opus, haiku.""" + assert "sonnet" in KNOWN_MODEL_ALIASES + assert "opus" in KNOWN_MODEL_ALIASES + assert "haiku" in KNOWN_MODEL_ALIASES -def test_default_model_is_opus(): - """Default model should be opus.""" - assert DEFAULT_MODEL == "opus" +def test_default_model_is_sonnet(): + """Default model should be sonnet.""" + assert DEFAULT_MODEL == "sonnet" -def test_model_map_values_are_full_ids(): - """All MODEL_MAP values should be full claude model IDs.""" - for key, value in MODEL_MAP.items(): - assert value.startswith("claude-"), f"{key} -> {value} doesn't start with 'claude-'" +def test_known_model_aliases_are_bare_names(): + """All KNOWN_MODEL_ALIASES should be bare alias names (no 'claude-' prefix).""" + for alias in KNOWN_MODEL_ALIASES: + assert not alias.startswith("claude-"), f"{alias} should be a bare alias" # --- _find_claude_bin tests ------------------------------------------