feat(ai_mail): dispatch default Sonnet 5 — alias pass-through + 200k context pin. MODEL_MAP replaced by KNOWN_MODEL_ALIASES pass-through (CLI resolves latest-in-class, map can never go stale), DEFAULT_MODEL opus->sonnet, dispatch_monitor pins CLAUDE_CODE_AUTO_COMPACT_WINDOW=200000 per spawn (Sonnet 5 is 1M-native — without pin agents inherit 1M window). E2E: live @cli dispatch probe reported claude-sonnet-5 + WINDOW=200000 from inside the spawned session. 777 tests green, seedgo 100%, both re-run by devpulse. Daemon spawn_agent --model gap confirmed, handled separately. Patrick ruling S326, built by @ai_mail

This commit is contained in:
AIOSAI
2026-07-19 09:15:04 -07:00
parent a7108e8504
commit efb530e0b2
4 changed files with 34 additions and 25 deletions
+12
View File
@@ -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):
@@ -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)
@@ -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")
+13 -15
View File
@@ -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 ------------------------------------------