Merge pull request #481 from AIOSAI/work/ai_mail_dplan0158

fix(ai_mail): DPLAN-0158 Phase 1 — explicit reply in daemon prompt
This commit is contained in:
AIPass
2026-04-26 22:39:14 -07:00
committed by GitHub
2 changed files with 141 additions and 3 deletions
@@ -313,9 +313,25 @@ def spawn_agent(
lock_file_path = str(branch_path / ".ai_mail.local" / ".dispatch.lock")
# Prompt — never interpolate sender-controlled content into the prompt.
# The agent reads inbox.json as data, not as instructions (DPLAN-0155 M1).
prompt = "Hi. Check your inbox for new dispatch emails and execute the task. Send confirmation when done."
# Prompt — only interpolate system-generated metadata (id, sender email).
# Free-form fields (subject, body) stay in inbox.json (DPLAN-0155 M1).
msg_id = message.get("id", "")
safe_id = msg_id if msg_id.isalnum() and len(msg_id) <= 12 else ""
sender_addr = message.get("from", "")
safe_sender = sender_addr if sender_addr.startswith("@") and sender_addr[1:].replace("_", "").isalnum() else ""
if safe_id:
reply_cmd = f'drone @ai_mail reply {safe_id} "your results summary"'
reply_instr = f" When done, reply via: {reply_cmd}. This is required — do not skip the reply step."
else:
reply_instr = (
" When done, reply to the dispatch email via drone @ai_mail reply <id> with your results."
" This is required — do not skip the reply step."
)
sender_note = f" Dispatch from {safe_sender}." if safe_sender else ""
prompt = "Hi. Check inbox, process new emails, update memories when done." + sender_note + reply_instr
claude_cmd = [
"claude",
+122
View File
@@ -1335,6 +1335,128 @@ def test_spawn_agent_strips_claude_env_vars(tmp_path, monkeypatch):
assert captured_env.get("AIPASS_SESSION_TYPE") == "daemon"
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"
branch_path.mkdir()
(branch_path / "logs").mkdir()
message = {"from": "@devpulse", "id": "abc12345", "subject": "Test task"}
config = {"max_turns_per_wake": 50}
state = {"daily_counts": {}, "session_cycles": {}}
captured_cmd = []
def capture_popen(cmd, *args, **kwargs):
captured_cmd.extend(cmd)
mock_proc = MagicMock()
mock_proc.pid = 99999
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,
),
):
spawn_agent(branch_path, "@testbranch", message, config, state)
prompt_idx = captured_cmd.index("-p") + 1
prompt = captured_cmd[prompt_idx]
assert "drone @ai_mail reply abc12345" in prompt
assert "required" in prompt.lower()
def test_spawn_agent_prompt_includes_sender(tmp_path):
"""Prompt includes sender address from the dispatch email."""
branch_path = tmp_path / "branch"
branch_path.mkdir()
(branch_path / "logs").mkdir()
message = {"from": "@devpulse", "id": "abc12345", "subject": "Test task"}
config = {"max_turns_per_wake": 50}
state = {"daily_counts": {}, "session_cycles": {}}
captured_cmd = []
def capture_popen(cmd, *args, **kwargs):
captured_cmd.extend(cmd)
mock_proc = MagicMock()
mock_proc.pid = 99999
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,
),
):
spawn_agent(branch_path, "@testbranch", message, config, state)
prompt_idx = captured_cmd.index("-p") + 1
prompt = captured_cmd[prompt_idx]
assert "@devpulse" in prompt
def test_spawn_agent_prompt_fallback_without_id(tmp_path):
"""Without a valid ID, prompt uses generic reply instruction."""
branch_path = tmp_path / "branch"
branch_path.mkdir()
(branch_path / "logs").mkdir()
message = {"from": "@devpulse", "id": "", "subject": "Test task"}
config = {"max_turns_per_wake": 50}
state = {"daily_counts": {}, "session_cycles": {}}
captured_cmd = []
def capture_popen(cmd, *args, **kwargs):
captured_cmd.extend(cmd)
mock_proc = MagicMock()
mock_proc.pid = 99999
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,
),
):
spawn_agent(branch_path, "@testbranch", message, config, state)
prompt_idx = captured_cmd.index("-p") + 1
prompt = captured_cmd[prompt_idx]
assert "reply <id>" in prompt
assert "required" in prompt.lower()
# ---- run_daemon tests -------------------------------------------