fix: route internal-turn approval cards to visible channels; feed provider errors back to the model

Approval cards raised by company-mode internal scheduling turns (review/report
work items, session ids like `<root>:review::<wid>::vN`) were posted to the
turn's own session channel, which the UI deliberately hides. The card silently
timed out after 300s and the work item parked on AWAITING_HUMAN, so users saw
only the gate card and never the approval prompt. ws_handler now detects these
internal turns and routes their escalation cards to origin_task_id, the root
session's primary task channel, or the activity channel — never the hidden one.
Also unblocks the previously dead origin/session fallbacks in the resolver.

Unclassified LLM stream failures (e.g. provider content-filter rejections like
"input may contain sensitive information") used to hit a blind truncate-retry
loop that replayed the identical payload for a dozen-plus consecutive failures.
runtime_v2 now feeds the provider's verbatim error text back into the
conversation as a "[runtime notice]" system message so the model can adapt
(rephrase, drop quotes, change tack), bounded at 2 feedback retries (counter
resets on any successful stream) plus one context-reset attempt, then fails
honestly with the real error. The blind truncate path remains only for
classified tool-protocol errors.

Verified: new end-to-end tests for recover-after-notice and bounded-failure;
runtime_v2 + ws_handler + escalation/approval + company-mode suites all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
LZH-YS1998
2026-07-07 18:22:49 +08:00
parent 12817a4e60
commit a30fa7588d
3 changed files with 238 additions and 4 deletions
+42
View File
@@ -1960,6 +1960,9 @@ class WSHandler:
logger.warning(f"Failed to resolve escalation task mapping for {source_task_id}: {e}")
task = None
if task is not None:
internal_turn_target = self._company_internal_turn_escalation_target(task)
if internal_turn_target is not None:
return internal_turn_target or None
ui_task_id = self._ui_task_id_for_task(task)
if ui_task_id:
return ui_task_id
@@ -1975,6 +1978,45 @@ class WSHandler:
return source_task_id
def _company_internal_turn_escalation_target(self, task: Any | None) -> str | None:
"""Visible routing target for escalations raised by internal
company-mode scheduling turns.
Review/report turn work items get composite ids (``review::<wid>::vN``),
so their runtime tasks carry session ids shaped like
``<root_session>:review::<wid>::vN``. The UI deliberately hides those
session channels, so an approval card posted to the turn's own channel
can never be seen or answered it silently times out and the work item
parks on AWAITING_HUMAN.
Returns None when ``task`` is not such an internal turn (caller keeps
its normal resolution), the primary task id of the run's root session
when resolvable, or "" when the turn is internal but no visible session
is known (caller should fall back to the activity channel rather than
the hidden channel).
"""
if task is None:
return None
session_id = str(getattr(task, "session_id", "") or "").strip()
root_session_id, sep, suffix = session_id.partition(":")
if not sep or "::" not in suffix:
return None
metadata = dict(getattr(task, "metadata", {}) or {})
origin_task_id = str(metadata.get("origin_task_id") or "").strip()
task_id = str(getattr(task, "id", "") or "").strip()
if origin_task_id and origin_task_id != task_id:
return origin_task_id
for candidate_session_id in (
root_session_id,
str(getattr(task, "parent_session_id", "") or "").strip(),
):
if not candidate_session_id:
continue
mapped_task_id = str(self._session_to_task.get(candidate_session_id) or "").strip()
if mapped_task_id and mapped_task_id != task_id:
return mapped_task_id
return ""
@staticmethod
def _pending_escalation_matches_task(record: dict[str, Any], task_id: str | None) -> bool:
task_key = str(task_id or "").strip()