Host-agent console showed OCR/UI-tree overlay boxes misaligned with the
displayed screenshot. Two independent causes, both confirmed with real
task data and pixel-level measurement of a user-provided screenshot:
1. perception/ui_parser.py parses XCUITest UI-tree bounds as iOS logical
points, while scene_builder.py's Scene.width/height (via infer_png_size)
and OCR bounds are in screenshot pixels, never reconciled (2.0x on
Retina devices). build_scene() now detects the scale from the first
x==0,y==0 UI element and rescales OCR bounds down to points-space,
reporting Scene.width/height in points too. No-op for Android, where
UiAutomator2 bounds already match pixels 1:1. This also fixes tap()
landing at the wrong location for OCR-matched text, and lets the IOU
fusion between UI-tree and OCR elements actually fire on iOS.
2. runtime/task.py captured `scene` (OCR/UI-tree data) before the LLM
planning call, but re-captured `before_screenshot` for each step
afterward - a real time gap during which on-screen content (e.g. a
keyboard) could shift, producing a directional drift between the
overlay and the displayed image. The first step of each plan batch
now reuses the screenshot already taken for planning instead of
capturing a new one; later steps in a multi-step batch still take a
fresh capture (left unresolved, scoped out by request).
Regression tests added for both the scale reconciliation (using real
828x1792 vs 414x896 numbers) and the screenshot reuse behavior.
Remove the 20-step execution limit that was causing "max steps exceeded" errors for long-running tasks. Increase the default max_steps to 999999 in all configurations, effectively removing the practical limit while maintaining the safety mechanism.
Changes:
- runtime/task.py: TaskRunnerConfig.max_steps 20 → 999999
- agents/collab_runner.py: CollaborativeTaskRunnerConfig.max_steps 20 → 999999
- storage/device_config.py: DEFAULT_MAX_STEPS 20 → 999999
Fixes issue 3: the host-agent console showed OCR results but never real
UI-tree data, because _ui_tree_nodes() checked for a get_ui_tree/ui_tree
tool action that has never existed anywhere in the codebase.
- storage/timeline.py: add a ui_tree_results field to TimelineRecord and
Timeline.append(), mirroring the existing ocr_results field.
- runtime/task.py: _append_timeline() now extracts scene.elements with
source == "ui" into ui_tree_results (scene_builder.build_scene() already
preserved these; they were just never persisted).
- host_agent/web/app.py: _ui_tree_nodes() reads the new field directly
instead of the dead tool-action check. New _overlay_payload() exposes
each step's scene dimensions and fused element list for client-side
rendering.
- task_detail.html: adds a toggle to overlay OCR (orange) and UI-tree
(blue) bounding boxes on the before-action screenshot, plus a visual
marker for the actually executed action (tap circle, or an animated
swipe path) using an SVG viewBox so no manual coordinate-scaling JS is
needed. Legacy/incomplete records degrade to no overlay, never an error.
Also corrects openspec/specs/runtime-task-evidence and
host-agent-console-task-pages, which had encoded the same nonexistent-tool
assumption, via the new host-agent-console-visual-evidence change.
600 tests passing; ruff/compileall/openspec validate all clean.
paddlepaddle has no Python 3.14 (cp314) wheel on PyPI, so host-agent
deployments on 3.14 can never install it, causing OCR to fail at
runtime with RuntimeError. Pin the workspace to Python 3.13 across
all pyproject.toml files, the Docker base image, and the Jenkins CI
image; regenerate uv.lock against 3.13.
Also fixes a pre-existing Python-2-style `except X, Y:` syntax error
(invalid in all Python 3.x) in runtime/task.py and
packages/cloud-platform/cloud/{sql_repository,internal_api/api}.py,
introduced in 22d37ca9 and unrelated to this change's scope, which
blocked the full test suite from collecting on any interpreter
version.
openspec change: downgrade-python-3-13-paddleocr
Host Agent now persists step-level execution detail locally (via a real
TaskMetadataStore/Timeline wired into TaskRunner) and reports a bounded
in-progress snapshot piggybacked on lease renewal. Cloud persists that
snapshot per active assignment and exposes it through the existing task
list/detail query path; Cloud Console renders it as a live badge. Host
Agent's local console gains authenticated, read-only task list and
detail/timeline pages (same-origin, server-rendered) with inlined
screenshots.
Also fixes a pre-existing gap in the shared Timeline: the actual
per-step LLM prompt is now recorded instead of the task goal, benefiting
both Runtime and Host Agent consoles. When a host uses the cloud planner
transport, each decide call's prompt and resulting tool decision are
durably logged in a new planner_decision_log table (with bounded
retention) and browsable from Cloud Console; direct-transport hosts
explicitly surface a "not reported" state.
Includes Alembic migrations 0008 (progress columns on scheduled_tasks)
and 0009 (planner_decision_log), bounded Host-Agent-local retention,
dual-backend repository parity, and Vitest + pytest coverage. Task 6.5
(manual end-to-end device verification) remains.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces the stub Planner's fixed describe_screen/[] behavior with a real
decision-maker: AIPlanner uses native tool/function calling (Anthropic or
OpenAI, pluggable via AI_PLANNER_PROVIDER) to select exactly one grounded
action per turn, with an explicit finish_task(success, reason) tool for
completion/failure instead of an ambiguous "no tool call" signal. Default
disabled (AI_PLANNER_ENABLED=false) and additive; TaskRunner falls back to
the existing stub Planner unchanged when disabled.
Amends CONSTITUTION.md's Perception Boundary with one narrow exception:
only the AI Planner may receive the current step's raw screenshot bytes
alongside Scene, for vision-grounded coordinate grounding. Also fixes a
latent gap in TaskRunner.run(): observe/plan exceptions are now caught per
iteration and turned into a failed task with a failure_reason, instead of
propagating uncaught.
openspec change: ai-planner-runtime.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
WorldModel routed all state through a single mutable _current_task_id,
so concurrent tasks sharing one instance could corrupt each other's
state. start_task() now returns a TaskWorldView handle scoped to that
task; TaskRunner.run() threads it through as a local variable instead
of reading self.world_model implicitly. Also extracts _start_world_view/
_record_step_result as reusable TaskRunner methods for composed runners.
openspec: world-model capability, archived change world-model-runtime