Files
wehub-resource-sync 4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

126 lines
5.7 KiB
Python

"""Interactive-shell session: SessionCore plus terminal UI state.
Extends :class:`~core.agent_harness.session.session_core.SessionCore` with the
shell-only facets (``terminal`` UI/background state and the ``alerts`` inbox) and
the methods that drive them.
"""
from __future__ import annotations
import re
import time
from dataclasses import dataclass, field
from config.constants.prompts import SUGGESTED_PROMPT_AFTER_FAILED_SYNTHETIC_TEST
from core.agent_harness.session.session_core import SessionCore
from core.domain.alerts.inbox import IncomingAlert
from surfaces.interactive_shell.session.alert_inbox import SessionAlertInbox
from surfaces.interactive_shell.session.terminal_session import TerminalSession
_SCENARIO_FLAG_RE = re.compile(r"--scenario\s+(\S+)")
_SYNTHETIC_SCENARIO_ID_RE = re.compile(r"^\d{3}-[a-z0-9][a-z0-9-]*$")
def _scenario_id_from_synthetic_label(label: str) -> str:
"""Extract a scenario id from a synthetic command or ``suite:scenario`` label."""
match = _SCENARIO_FLAG_RE.search(label)
if match is not None:
candidate = match.group(1).strip()
return candidate if _SYNTHETIC_SCENARIO_ID_RE.fullmatch(candidate) else ""
if ":" in label:
candidate = label.rsplit(":", 1)[-1].strip()
return candidate if _SYNTHETIC_SCENARIO_ID_RE.fullmatch(candidate) else ""
return ""
@dataclass
class Session(SessionCore):
"""Per-REPL-process session: :class:`SessionCore` plus interactive-shell state.
Adds the shell-only ``terminal`` facet (UI/theme/prompt-toolkit/background)
and the ``alerts`` inbox on top of the surface-agnostic core.
"""
terminal: TerminalSession = field(default_factory=TerminalSession)
"""Interactive-shell (terminal) session facet — shell-only UI/theme/background state.
Always present (empty for non-shell sessions) so shell code needs no None-guard;
``core``/``gateway``/``tools`` consumers ignore it. Holds the theme, prompt-toolkit,
pending-prompt/stdin, background-jobs, and metrics clusters (#3690)."""
alerts: SessionAlertInbox = field(default_factory=SessionAlertInbox)
"""Inbox of externally-received alerts (shell alert listener → ``/status``).
A surface facet: the bounded alert list + cap live on ``SessionAlertInbox`` so
core-session consumers that never touch alerts don't see the field."""
def suggest_synthetic_failure_follow_up(self, *, label: str = "") -> None:
"""Queue RCA prefill after a failed synthetic run and refresh the active prompt."""
self.terminal.pending_prompt_default = SUGGESTED_PROMPT_AFTER_FAILED_SYNTHETIC_TEST
self.terminal.notify_prompt_changed()
self._bind_last_synthetic_observation(_scenario_id_from_synthetic_label(label))
self.terminal.notify_prompt_changed()
def _bind_last_synthetic_observation(self, scenario_id: str) -> None:
"""Point ``last_synthetic_observation_path`` (a core field) at the run's latest.json.
Synthetic-run UX, so it lives on the shell session rather than the core.
"""
if not scenario_id:
self.last_synthetic_observation_path = None
return
# Shared path constant lives in config so core and surfaces stay decoupled.
try:
from config.constants.paths import SYNTHETIC_SCENARIOS_DIR
except Exception:
self.last_synthetic_observation_path = None
return
latest = SYNTHETIC_SCENARIOS_DIR / "_observations" / scenario_id / "latest.json"
for _ in range(8):
if latest.is_file():
self.last_synthetic_observation_path = str(latest.resolve())
return
time.sleep(0.06)
self.last_synthetic_observation_path = None
def record_incoming_alert(self, alert: IncomingAlert) -> None:
"""Append a full IncomingAlert with all metadata to session history.
Also stores the alert in the ``alerts`` inbox facet (bounded FIFO), preserving
received_at, severity, source, and alert_name so /status displays accurate
timestamps and future uses have complete data.
"""
self.history.append({"type": "incoming_alert", "text": alert.text, "ok": True})
self.storage.append_turn(self, "incoming_alert", alert.text)
self.alerts.add(alert)
def clear(self, *, rotate_identity: bool = True) -> None:
"""Reset the session — core state plus the shell facets — for /new and /resume."""
self.terminal.history_generation += 1
super().clear(rotate_identity=rotate_identity)
self.alerts.clear()
self.terminal.metrics.reset()
self.terminal.pending_prompt_default = None
self.terminal.pending_prompt_autosubmit = False
self.terminal.exclusive_stdin_active = False
self.terminal.agent_turn_executed_slashes.clear()
self.terminal.background_mode_enabled = False
self.terminal.background_investigations.clear()
# Preserve notification channel prefs across /new like trust_mode.
# Only reset when the user explicitly changes them via /background notify.
with self.terminal._background_notices_lock:
self.terminal.background_notices.clear()
# trust_mode and reasoning_effort are intentionally preserved across /new
def release_resources(self) -> None:
"""Cancel background work and drop loop-owned UI references for teardown.
Extends :meth:`SessionCore.release_resources` (which cancels the
integration-warm task) with the shell facet's own teardown.
"""
super().release_resources()
with self.terminal._background_notices_lock:
self.terminal.background_notices.clear()
self.terminal.prompt_refresh_fn = None
self.terminal.fleet_sampler_starter = None