Skip to content

iOS: decode the targetActivation stamp for commands that consume no capture (press <x> <y>, live @ref) #2694

Description

@thymikee

Split out of #2682 / PR #2693 by adversarial review.

What is already shipped

The Apple runner stamps targetActivation on the response of every accepted command that had to re-activate the session app (RunnerTests+CommandExecution.swift, executeAccepted). PR #2691 decodes it for captures; PR #2693 discloses it on the daemon routes that consumed a capture: snapshot, find, get, is, wait, and an interaction whose target tree was captured for it.

The gap

A command that consumes no capture — press <x> <y>, or press @ref answered from a live ref frame — still answers with no disclosure even when the runner paid for a foreground repair to serve it. The stamp is on that command's runner response and is dropped at the Apple interaction projection: only packages/platform-apple/src/runner/snapshot-presentation.ts decodes it today.

This is the ADR 0012 category: a repair happened and the response does not say so. Silence must not be read as "no repair". website/docs/docs/commands.md ("Foreground repairs on iOS") states that boundary until this closes.

Shape of the fix

  1. Decode the stamped fact where runner responses are read for every command, not just snapshots: packages/platform-apple/src/runner/runner-session.ts (the shared parseRunnerResponse seam that already reads the runnerMainThreadBusy stamp) plus whatever per-command projection needs to let it through.
  2. Give interaction results a carrier to the daemon response for it, and append the disclosure through buildInteractionResponseData (the shared interaction response builder) so no interaction verb can drop it.
  3. Cover both commands in the live lane test/integration/smoke-ios-target-activation.test.ts (now wired into ios.yml and replays-manual.yml), which asserts the disclosure for capture-consuming commands today.

Acceptance

  • press <x> <y> and press @ref after an off-app handoff each carry targetActivation and the appended warning for the repair they paid for.
  • A command that performed no repair stays silent, and the docs' claim is inverted from "stays silent" back to "silence means no repair" — the current wording must be updated in the same PR that closes this.

Not in scope

Foreground ownership: otherActiveApplicationPid remains a liveness claim; activeApplications exposes no ordering, so no field here proves which app owned the screen.

Activity

  1. added 12 commits that reference this issue on Sep 19, 2026
  2. thymikee commented on Sep 20, 2026

    @thymikee
    MemberAuthor

    Second member of this family, found while verifying #2693 on a simulator with the reviewer's own handoff (xcrun simctl openurl <UDID> https://example.com, Safari foreground, then one command):

    $ agent-device wait stable 4000     # capture-consuming
    Warning: The session app was not foreground when this command arrived (prior state runningBackground), …
    
    $ agent-device wait text "Example"  # findText-based
    (nothing)
    

    The runner did pay for the repair in both cases. .findText returns Response(ok: true, data: DataPayload(found: found)) (RunnerTests+CommandExecution.swift:2007-2012) and executeAccepted stamps whatever the switch returned (:928-933, response.stampingTargetActivation(fact)), so the fact is on the wire. It dies one level up, at the projection this issue's item 1 does not name: packages/platform-apple/src/interactor.ts:78-86 casts the response to { found?: boolean } and returns { found: result?.found === true }, so everything the same response carried is discarded before the daemon ever sees it. From there src/commands/interaction/runtime/wait-text.ts:42-43 only ever gets a boolean, and a satisfied wait text answers with no disclosure at all. A timed-out wait text does disclose when its surface-decoration capture re-activated — that path was covered in #2693 — which makes the satisfied case look intentional unless you read the projection.

    So item 1 ("decode the stamped fact where runner responses are read for every command") has to survive two projections, not one: the interaction projection named here, and this text-read projection whose return type has no room for a fact. I'd suggest the acceptance list name it explicitly:

    • A text-read command answered without a capture — wait text satisfied on the first poll after an off-app handoff — carries targetActivation and the appended warning, or has an explicit reason why a text read is not a repair bill.

    Same non-goal holds: otherActiveApplicationPid stays a liveness claim. Flagging rather than folding it into #2693, which is open as it stands and would need a second projection change beyond its stated scope.

  3. added 2 commits that reference this issue on Sep 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions