refactor: unify agent history and harden runtime diagnostics

Preserve user guidance through history compression, stabilize Flash index targets, and retire withdrawn checks without applying stale verdicts.

Improve runtime diagnostics, tool responses, and history rendering. Simplify documentation and comments.

Validation: 2076 tests passed in the full suite; 381 affected tests and 26 frontend tests passed after cleanup. Ruff and the quality ratchet passed.
This commit is contained in:
somew1nd committed 2026-09-09 21:35:40 -07:00
1 parent e983180b5b
commit 5d92939d91
127 files changed
+13738 -1988

No files matched your search

+182 -44
View File
@@ -15,6 +15,8 @@
"""Utilities for handling app locking and initial app launch logic."""
import asyncio
from dataclasses import dataclass, field
import re
from artemis.context import AppLaunchResult, ArtemisContext
from artemis.controllers.platform_specific_commands_controller import (
@@ -28,43 +30,180 @@ from artemis.utils.logger import get_logger
logger = get_logger(__name__)
SYSTEM_OVERLAYS_VALID_FOR_LAUNCH = {
"com.google.android.permissioncontroller",
"com.android.permissioncontroller",
"com.google.android.packageinstaller",
"com.android.packageinstaller",
}
# ``ActivityRecord{<hash> u<user> <package>/<activity> t<task id>}`` as printed by
# ``dumpsys activity activities`` on every line that names an activity.
_ACTIVITY_RECORD_RE = re.compile(
r"ActivityRecord\{[0-9a-f]+ u\d+ (?P<package>[\w.]+)/(?P<activity>[\w.$]+) t(?P<task>\d+)\}"
)
# ``* Task{<hash> #<id> type=... A=<uid>:<affinity> ... visible=true ...}`` task headers
# (``* TaskRecord{<hash> #<id> A=<affinity> ...}`` on Android 11 and older).
_TASK_HEADER_RE = re.compile(r"^\s*\* Task(?:Record)?\{[0-9a-f]+ #(?P<task>\d+)(?P<attrs>[^}]*)\}")
# Device-side pre-filter: the full dump runs to hundreds of KB on a busy device;
# only the task headers, history entries and resumed-activity lines are parsed.
_FOREGROUND_DUMP_CMD = (
"dumpsys activity activities | grep -E"
r" '^ *\* (Task|TaskRecord)\{|^ *\* Hist |ResumedActivity'"
)
# ``* Hist #<index>: ActivityRecord{...}`` entries; index 0 is the task's base activity.
_HIST_RE = re.compile(r"^\s*\* Hist\s+#(?P<index>\d+): (?P<record>ActivityRecord\{[^}]*\})")
# ``topResumedActivity=ActivityRecord{...}`` (per task, top task first) and the global
# ``ResumedActivity: ActivityRecord{...}`` line; the first one seen is the foreground one.
_RESUMED_RE = re.compile(r"ResumedActivity[:=]\s*(?P<record>ActivityRecord\{[^}]*\})")
def get_focused_task_package(ctx: ArtemisContext) -> str | None:
"""Inspect the active/focused task stack to find the package affinity of the top task.
@dataclass(frozen=True)
class ForegroundTask:
"""What ``dumpsys activity activities`` says is on top of the display.
Uses adb shell dumpsys activity activities to extract this info.
``resumed_package`` is the package of the resumed activity (what the user sees).
``base_package`` is the package of the task's root activity (``Hist #0``), and
``packages`` holds every package that has an activity in that task, so an app whose
task currently shows a helper activity from another package (Settings search,
permission dialogs, account pickers, system update pages, ...) is still
recognised as the foreground app.
"""
task_id: int | None = None
affinity: str | None = None
resumed_component: str | None = None
resumed_package: str | None = None
base_package: str | None = None
packages: frozenset[str] = field(default_factory=frozenset)
def owns(self, app_package: str) -> bool:
"""Whether ``app_package`` is the app the foreground task belongs to.
The task affinity is the app's own claim on the task (by default the
package name, or a package-prefixed name such as
``com.android.settings.root``); it stays with the task even when the
app's root activity has been destroyed and only a companion activity
(e.g. the Settings search UI) remains in the history."""
affinity = self.affinity or ""
return (
app_package == self.resumed_package
or app_package == self.base_package
or app_package in self.packages
or affinity == app_package
or affinity.startswith(app_package + ".")
)
def describe(self) -> str:
task = f"task #{self.task_id}" if self.task_id is not None else "task ?"
details = [f"base={self.base_package}", f"resumed={self.resumed_component}"]
if self.affinity:
details.append(f"affinity={self.affinity}")
others = sorted(self.packages - {self.base_package, self.resumed_package, None})
if others:
details.append(f"also={','.join(others)}")
return f"{task} ({', '.join(details)})"
def parse_foreground_task(dump: str) -> ForegroundTask | None:
"""Parse ``dumpsys activity activities`` output into the foreground task summary.
The foreground task is the one holding the resumed activity; when no activity is
resumed (mid-transition) the first visible task with history entries is used.
Returns None when the dump contains no task information at all.
"""
task_attrs: dict[int, str] = {}
task_order: list[int] = []
task_activities: dict[int, list[tuple[int, str, str]]] = {}
resumed: re.Match[str] | None = None
for line in dump.splitlines():
header = _TASK_HEADER_RE.match(line)
if header:
task_id = int(header.group("task"))
if task_id not in task_attrs:
task_attrs[task_id] = header.group("attrs")
task_order.append(task_id)
continue
hist = _HIST_RE.match(line)
if hist:
record = _ACTIVITY_RECORD_RE.search(hist.group("record"))
if record:
task_activities.setdefault(int(record.group("task")), []).append(
(int(hist.group("index")), record.group("package"), record.group("activity"))
)
continue
if resumed is None:
match = _RESUMED_RE.search(line)
if match:
resumed = _ACTIVITY_RECORD_RE.search(match.group("record"))
if not task_attrs and resumed is None:
return None
task_id: int | None = None
if resumed is not None:
task_id = int(resumed.group("task"))
else:
for candidate in task_order:
if "visible=true" in task_attrs[candidate] and candidate in task_activities:
task_id = candidate
break
activities = sorted(task_activities.get(task_id, []))
affinity_match = re.search(r"\bA=(?:\d+:)?(\S+)", task_attrs.get(task_id, ""))
return ForegroundTask(
task_id=task_id,
affinity=affinity_match.group(1) if affinity_match else None,
resumed_component=(
f"{resumed.group('package')}/{resumed.group('activity')}" if resumed else None
),
resumed_package=resumed.group("package") if resumed else None,
base_package=activities[0][1] if activities else None,
packages=frozenset(package for _, package, _ in activities),
)
def get_foreground_task(ctx: ArtemisContext) -> ForegroundTask | None:
"""Read the foreground task from ``dumpsys activity activities`` on the device.
Blocking (a synchronous ADB shell round-trip); async callers go through
:func:`get_foreground_task_async` so the event loop keeps serving.
"""
try:
device = get_adb_device(ctx)
output = str(device.shell("dumpsys activity activities | grep topDisplayFocusedRootTask"))
if "topDisplayFocusedRootTask=" in output:
segment = output.split("topDisplayFocusedRootTask=")[-1]
if "A=" in segment:
affinity = segment.split("A=")[-1].split("}")[0].split()[0].strip()
if ":" in affinity:
return affinity.split(":")[-1]
return affinity
if "{" in segment and "}" in segment:
braces_content = segment.split("{")[-1].split("}")[0]
tokens = braces_content.split()
for token in tokens:
if "." in token and not token.startswith("#") and not token.isdigit():
if ":" in token:
return token.split(":")[-1]
return token
return None
if device is None:
return None
dump = str(device.shell(_FOREGROUND_DUMP_CMD))
if "Task" not in dump:
# No grep on the device (or nothing matched): parse the full dump.
dump = str(device.shell("dumpsys activity activities"))
return parse_foreground_task(dump)
except Exception as e:
logger.debug(f"Failed to retrieve focused task package via dumpsys: {e}")
logger.debug(f"Failed to retrieve foreground task via dumpsys: {e}")
return None
async def get_foreground_task_async(ctx: ArtemisContext) -> ForegroundTask | None:
""":func:`get_foreground_task` off the event loop."""
return await asyncio.to_thread(get_foreground_task, ctx)
async def _observe_foreground(
ctx: ArtemisContext, app_package: str
) -> tuple[bool, str | None, ForegroundTask | None]:
"""Report whether ``app_package`` is in the foreground.
Fast path: the focused window's package equals the target. Otherwise the app also
counts as foreground when the top task belongs to it (see ``ForegroundTask``).
A ``None`` focused package means the window manager is mid-transition and is
reported as "not yet" without consulting the task stack.
Returns (is_foreground, focused_package, foreground_task); the task is only read
when the fast path fails on a non-null focused package.
"""
current_package = await get_current_foreground_package_async(ctx)
if current_package == app_package:
return True, current_package, None
if current_package is None:
return False, None, None
task = await get_foreground_task_async(ctx)
return task is not None and task.owns(app_package), current_package, task
async def _poll_for_app_ready(
ctx: ArtemisContext,
app_package: str,
@@ -88,38 +227,37 @@ async def _poll_for_app_ready(
polls = int(max_poll_seconds / poll_interval)
for i in range(polls):
current_package = await get_current_foreground_package_async(ctx)
ready, current_package, task = await _observe_foreground(ctx, app_package)
if current_package == app_package:
logger.success(f"App {app_package} is ready (took ~{i * poll_interval:.1f}s)")
return True, None
if current_package in SYSTEM_OVERLAYS_VALID_FOR_LAUNCH:
task_package = get_focused_task_package(ctx)
if task_package == app_package:
if ready:
if current_package == app_package:
logger.success(f"App {app_package} is ready (took ~{i * poll_interval:.1f}s)")
else:
logger.success(
f"App {app_package} launch succeeded (system overlay"
f" '{current_package}' focused, but top task stack affinity"
f" matches expected app package '{task_package}', took"
f" ~{i * poll_interval:.1f}s)"
f"App {app_package} is ready (focused window belongs to"
f" '{current_package}', but the foreground {task.describe()} is owned"
f" by {app_package}, took ~{i * poll_interval:.1f}s)"
)
return True, None
return True, None
if current_package is None:
logger.debug(f"Poll {i + 1}/{polls}: App loading (mCurrentFocus=null)...")
else:
logger.debug(
f"Poll {i + 1}/{polls}: Wrong app in foreground (expected"
f" '{app_package}', got '{current_package}'). Still waiting..."
f" '{app_package}', got '{current_package}', foreground"
f" {task.describe() if task else 'task unknown'}). Still waiting..."
)
if i < polls - 1:
await asyncio.sleep(poll_interval)
current_package = await get_current_foreground_package_async(ctx)
task = await get_foreground_task_async(ctx)
error_msg = (
f"Timeout waiting for {app_package} to load after {max_poll_seconds}s. "
f"Current foreground: {current_package}"
f"Current foreground: {current_package}; foreground"
f" {task.describe() if task else 'task unknown'}"
)
logger.error(error_msg)
return False, error_msg
@@ -225,10 +363,10 @@ async def _handle_initial_app_launch(
logger.info(f"Starting initial app launch for package: {locked_app_package}")
try:
current_package = await get_current_foreground_package_async(ctx)
already_foreground, current_package, _ = await _observe_foreground(ctx, locked_app_package)
logger.info(f"Current foreground app: {current_package}")
if current_package == locked_app_package:
if already_foreground:
logger.info(f"App {locked_app_package} is already in foreground")
return AppLaunchResult(
locked_app_package=locked_app_package,