Dedupe WhatsApp access pending and cap execManagedNe per turn.

Reuse open pending requests without re-notifying admins, clarify already-waiting replies, and block further CLI after 4 calls or 2 failures in the same turn.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-10 23:25:09 +08:00
parent d8bc08ee0d
commit 28a5f2586c
6 changed files with 332 additions and 27 deletions

View file

@ -461,15 +461,29 @@ def handle_whatsapp_access(
)
return None
pending_id = store.create_whatsapp_access_pending(
tenant_id=tenant_id,
account_id=account_id,
external_user_id=raw_jid,
push_name=push_name,
phone=phone,
request_text=text,
)
if pending_id:
pending_id = ""
already_pending = False
finder = getattr(store, "find_open_whatsapp_access_pending", None)
if callable(finder):
existing = finder(
tenant_id=tenant_id,
account_id=account_id,
external_user_id=raw_jid,
phone=phone,
)
if isinstance(existing, dict) and str(existing.get("id") or "").strip():
pending_id = str(existing.get("id") or "").strip()
already_pending = True
if not pending_id:
pending_id = store.create_whatsapp_access_pending(
tenant_id=tenant_id,
account_id=account_id,
external_user_id=raw_jid,
push_name=push_name,
phone=phone,
request_text=text,
) or ""
if pending_id and not already_pending:
_notify_admins(
store,
tenant_id=tenant_id,
@ -500,12 +514,17 @@ def handle_whatsapp_access(
{
"channel": "whatsapp",
"chat_id": inbound.external_chat_id,
"text": denied_reply_text(lang=lang, pending_id=str(pending_id or "")),
"text": denied_reply_text(
lang=lang,
pending_id=str(pending_id or ""),
already_pending=already_pending,
),
"attachments": [],
"metadata": reply_meta,
}
],
"whatsapp_access": "denied",
"whatsapp_access_already_pending": bool(already_pending),
}

View file

@ -416,6 +416,55 @@ def _load_turn_retry_forbidden_tools(store: Any, *, session_id: str, turn_uuid:
return out
def _load_turn_failed_name_counts(store: Any, *, session_id: str, turn_uuid: str) -> dict[str, int]:
"""Count failed tool results by tool name earlier in this turn."""
tu = str(turn_uuid or "").strip()
sid = str(session_id or "").strip()
out: dict[str, int] = {}
if not tu or not sid:
return out
try:
rows = store.get_messages(session_id=sid, limit=500)
except Exception:
return out
for m in rows or []:
if str(getattr(m, "role", "") or "").strip().lower() != "tool":
continue
if str(getattr(m, "turn_uuid", "") or "").strip() != tu:
continue
ep = _parse_event_payload(getattr(m, "event_payload", None))
name = str(ep.get("tool_name") or "").strip()
failed = ep.get("ok") is False
if not failed:
try:
payload = json.loads(str(getattr(m, "content", "") or "") or "{}")
except Exception:
payload = {}
failed = isinstance(payload, dict) and payload.get("ok") is False
if not name and isinstance(payload, dict):
raw_tc = getattr(m, "tool_calls", None)
if isinstance(raw_tc, str):
try:
raw_tc = json.loads(raw_tc)
except Exception:
raw_tc = None
if isinstance(raw_tc, dict):
name = str(raw_tc.get("name") or "").strip()
if failed and name:
out[name] = int(out.get(name, 0)) + 1
return out
_HEAVY_CLI_NAME_SUFFIXES = ("execmanagedne",)
_HEAVY_CLI_TURN_CALL_BUDGET = 4
_HEAVY_CLI_TURN_FAIL_BUDGET = 2
def _is_heavy_cli_tool(name: str) -> bool:
low = str(name or "").strip().lower()
return any(low.endswith(suf) for suf in _HEAVY_CLI_NAME_SUFFIXES)
def normalize_tool_result(result: Any) -> dict[str, Any]:
if isinstance(result, dict):
out = dict(result)
@ -1043,6 +1092,12 @@ class ToolExecutor:
session_id=ctx.session_id,
turn_uuid=str(ctx.turn_uuid or ""),
)
failed_name_counts = _load_turn_failed_name_counts(
ctx.store,
session_id=ctx.session_id,
turn_uuid=str(ctx.turn_uuid or ""),
)
planned_name_counts: dict[str, int] = {}
results_by_id: dict[str, tuple[dict[str, Any], int]] = {}
runnable_tool_uses: list[LLMToolCall] = []
@ -1051,7 +1106,8 @@ class ToolExecutor:
sig_seen: dict[str, int] = {}
budget = max(1, min(int(signature_budget or 2), 8))
for tc in tool_uses:
if str(tc.name or "") in retry_forbidden_tools:
tool_name = str(tc.name or "")
if tool_name in retry_forbidden_tools:
results_by_id[tc.id] = (
{
"ok": False,
@ -1072,6 +1128,55 @@ class ToolExecutor:
{"tool_name": tc.name},
)
continue
if _is_heavy_cli_tool(tool_name):
prior_calls = int(turn_tool_name_counts.get(tool_name, 0)) + int(
planned_name_counts.get(tool_name, 0)
)
prior_fails = int(failed_name_counts.get(tool_name, 0))
if prior_fails >= _HEAVY_CLI_TURN_FAIL_BUDGET:
results_by_id[tc.id] = (
{
"ok": False,
"error_code": "cli_fail_budget_exceeded",
"failure_class": "retry_guard",
"error": f"{tool_name} failed too many times this turn",
"hint": (
"execManagedNe already failed multiple times this turn. "
"Stop CLI spam: report unreachable/timeout NEs, batch remaining "
"show commands into one call with higher read_timeout_sec, or switch targets."
),
"fail_count": prior_fails,
"fail_budget": _HEAVY_CLI_TURN_FAIL_BUDGET,
},
0,
)
_trace(
"cli_fail_budget_exceeded",
{"tool_name": tool_name, "fail_count": prior_fails},
)
continue
if prior_calls >= _HEAVY_CLI_TURN_CALL_BUDGET:
results_by_id[tc.id] = (
{
"ok": False,
"error_code": "cli_call_budget_exceeded",
"failure_class": "retry_guard",
"error": f"{tool_name} call budget exceeded this turn",
"hint": (
"Too many execManagedNe calls this turn. "
"Batch commands into fewer calls, reuse prior listCliTargets ids, "
"and summarize what you already have."
),
"call_count": prior_calls,
"call_budget": _HEAVY_CLI_TURN_CALL_BUDGET,
},
0,
)
_trace(
"cli_call_budget_exceeded",
{"tool_name": tool_name, "call_count": prior_calls},
)
continue
if tc.name in _TABULAR_QUERY_TOOL_NAMES and not has_tabular_ref_in_session:
results_by_id[tc.id] = (
{
@ -1226,6 +1331,7 @@ class ToolExecutor:
continue
first_tool_call_id_by_signature[sig] = str(tc.id or "")
runnable_tool_uses.append(tc)
planned_name_counts[tool_name] = int(planned_name_counts.get(tool_name, 0)) + 1
for batch in partition_tool_use_batches(runnable_tool_uses, ctx.tools):
_check_stop()

View file

@ -343,15 +343,25 @@ def coerce_whatsapp_access_target(value: str) -> str:
return normalize_whatsapp_target(normalize_whatsapp_phone(value))
def denied_reply_text(*, lang: str, pending_id: str = "") -> str:
def denied_reply_text(*, lang: str, pending_id: str = "", already_pending: bool = False) -> str:
pid = str(pending_id or "").strip()
if str(lang or "").strip().lower().startswith("zh"):
if already_pending and pid:
return (
f"访问申请仍在等待管理员处理(编号 {pid})。"
"同意后即可使用;请稍候,无需重复发送。"
)
if pid:
return (
f"访问申请已提交(编号 {pid})。管理员同意后即可使用;"
"请稍候,无需重复发送相同请求。"
)
return "无权限:您尚未获得使用此助手的授权。请联系管理员。"
if already_pending and pid:
return (
f"Your access request is still pending (request {pid}). "
"An administrator was already notified — please wait for YES; no need to resend."
)
if pid:
return (
f"Access pending (request {pid}): an administrator was notified. "