Skip to content

feat(limrun): serve iOS alert get/wait/accept/dismiss from tree capture - #3699

Open
thymikee wants to merge 17 commits into
mainfrom
t3/limrun-ios-alert-actions
Open

thymikee wants to merge 17 commits into
mainfrom
t3/limrun-ios-alert-actions

Conversation

@thymikee

@thymikee thymikee commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Summary

Closes #3349. Across 30 files, Limrun iOS alert get/wait/accept/dismiss use a shared tree-alert driver. Started captures finish; Android permission IDs rank grants conservatively. agent-device alert accept --session qa selects an unambiguous affirmative action; dismiss refuses unsafe choices.

Validation

Published head: 92df16aa70fb3d2812e94dd15e544b57abfffa62; mergeable, zero unresolved review threads.

  • Direct pnpm check:affected --run --head 92df16aa70fb3d2812e94dd15e544b57abfffa62: run-ktnH3c passed, exit 0, exactCommit: true, unchanged clean checkout; 36 checks passed, 25 skipped, two deduplicated. Current CI: 21 successful checks, two intentional skips; Coverage, Integration and all four platform smoke lanes passed.
  • Fresh R8 reports no P0/P1/P2 findings and independently assesses the 3,270-line gross diff and deletion options. Its tests/mutants used pipelines with masked exits in the author checkout; these supplement static review, not direct execution certificates. Size acceptance remains a maintainer judgment.
  • Final-head CLI evidence: three quiet accepts and three dismisses each return alert-not-found/dispatched: no; a real location sheet selects While using, grants permissions, then returns none. Limrun CLI/daemon shows ATT visible → Allow → none; a logging loader records one accept tap, separately from the app-trigger tap. Raw responses and transport evidence and review replies are published; own proof daemons stopped and Limrun instances deleted. The shared emulator remains open.
  • One unchanged-base 4c1c78696 provider run passed 201 tests, direct exit 0; this does not explain the earlier five provider failures. Earlier iOS failure cause and PR participation remain unknown. SDK in-flight abort is unavailable; global-reset impact remains unknown. Chief-of-Staff readiness label absent.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://callstack.github.io/agent-device/pr-preview/pr-3699/

Built to branch gh-pages at 2026-10-17 02:18 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.32 MB 5.33 MB +6.8 kB
Package (unpacked) 5.32 MB 5.33 MB +6.8 kB
Package (download) 1.60 MB 1.60 MB +2.3 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 17.5 ms 18.1 ms +0.6 ms
CLI --help 48.6 ms 51.9 ms +3.3 ms

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 11 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/provider-limrun/src/alert-detection.ts
Comment thread CHANGELOG.md Outdated
Comment thread packages/provider-limrun/src/alert.ts Outdated
Comment thread packages/provider-limrun/src/alert-detection.fixtures.ts
Comment thread packages/provider-limrun/src/alert.test.ts Outdated
Comment thread packages/provider-limrun/src/ios-interactor-alert.test.ts Outdated
Comment thread packages/provider-limrun/src/ios-interactor-alert.test.ts
Comment thread packages/provider-limrun/src/alert-detection.ts Outdated
Comment thread packages/provider-limrun/src/alert-detection.fixtures.ts Outdated
Comment thread website/docs/docs/limrun.md Outdated
@thymikee

Copy link
Copy Markdown
Member Author

The Limrun alert legs duplicate the Android tree-alert driver, and that needs to change before merge (reviewed at 4a1862b). The alert get/wait/accept/dismiss behavior itself looks right.

Is there a reason confirmLimrunAlertDismissed, sameLimrunAlert, pollLimrunAlertCandidate and the response union in alert.ts are copied from confirmAndroidAlertDismissed, sameAndroidAlert, pollAndroidAlertCandidate and AndroidAlertResult in packages/platform-android/src/alert.ts? The 'alert not found' hint and the 'did not dismiss the visible alert' error and hint are copied almost word for word too. The ACCEPT/DISMISS label patterns in alert-detection.ts (lines 42-47) are the Android patterns plus a few labels. Two drivers now have to stay in sync by hand, and a fix to one, such as the transient-empty-capture success in confirm, must be made twice. The label sets already differ: 'ask app not to track' and 'allow once' exist only on Limrun, so alert dismiss acts differently on the two platforms for the same label. The rule is that every tree-based alert backend reaches poll, confirm, same-alert and response building through one backend-neutral driver, and its button roles through one label vocabulary. The driver would take {capture, findCandidate, choose, press, platform, timeoutMs}, and the shared labels would live beside it. Limrun would then supply only findLimrunAlertCandidate, chooseLimrunAlertButton and client.tap, and provider-limrun already depends on @agent-device/platform-android/mechanics, so no new package edge is needed. Could the Limrun-specific code shrink to the host-root detection plus the four interactor methods? Two things to settle first: which module owns the driver (the contracts alert contract or a mechanics export) within the eager-closure budgets, and the deadline order, since Android checks the deadline before the first read and Limrun reads first.

The two threads on Cubic's confirm-on-ambiguous-capture (alert.ts:173) and duplicate-label skipping (alert-detection.ts:152) should be settled once in that shared driver. Please also validate on a live Limrun run that alert accept on the ATT prompt taps once and a second alert get reports no alert.

These open threads still apply: pid boundary comment, CHANGELOG app dialogs, plus six lower-priority test and fixture threads. This docs thread does not apply, because the page describes how the backend acts and gives accurate user guidance; please resolve it.

The failing Smoke Tests job runs the Apple XCTest-runner simulator smoke. This diff touches only provider-limrun, the changelog and the Limrun docs page, so I read that failure as unrelated. No conflicts. Next, send the Limrun alert legs through the shared driver and vocabulary.

thymikee added a commit that referenced this pull request Oct 16, 2026
…red driver

PR #3699 review: the Limrun iOS alert legs duplicated the Android tree-alert
driver (poll, confirm, same-alert, response building) and its label
vocabulary. Both backends now reach those steps through one backend-neutral
driver, runTreeAlertAction in platform-android (exported via the mechanics
facet provider-limrun already depends on), and classify button roles
through one English vocabulary that lives in contracts/alert-contract with
the alert budgets and typed reasons (contracts owns vocabulary only; the
driver loop owns the AppError/timer mechanics R18 keeps out of contracts).

Each backend supplies only its own seams: Android wraps its dump reader
and keeps its platform-id roles, neutral/Back fallbacks, and tap/Back
presses; Limrun supplies the blank-root process-exclusive host reader,
the label-strict chooser, and client.tap. The confirm-on-ambiguous-capture
and duplicate-label threads settle once in the shared code: the three-way
alert|none|undecided observation gates confirmation in the driver, and
selectUniqueAlertButtonByRole refuses duplicated roles at the shared
choice boundary. The deadline order resolves to read-first: over a
positive window both predecessor loops already observed before waiting.
The alert-not-found and undecided refusals now carry the same typed
details.reason and dispatched: 'no' on both backends.
@thymikee

Copy link
Copy Markdown
Member Author

Reviewed a27302c. The earlier findings are fixed except the ones below, and one new regression remains in the shared button choice. The 20 checks pass at this commit, but the Android alert suites pass only because no fixture has a multi-grant permission sheet. I know of no conflicts.

chooseAndroidAlertButton now resolves accept through selectUniqueAlertButtonByRole, which returns null when two buttons share a role. The Android 11+ runtime permission dialog shows permission_allow_foreground_only_button ("While using the app") and permission_allow_one_time_button ("Only this time"). The id classifier at line 192 marks both as accept. With three buttons the single-button fallback does not fire, so alert accept throws ALERT_BUTTON_UNDECIDED, where main pressed the first accept button. The same rule makes Limrun iOS alert accept refuse on the standard location prompt ("Allow Once" / "Allow While Using App" / "Don't Allow"), so #3349 works only on single-grant sheets like ATT. Users now get a refusal on the most common Android system alert, which regresses a shipped route. The rule should be: refuse only when the capture cannot name one physical button for the action, meaning the same label at two points, not distinct labels that share a role. Please declare a preference order in the shared vocabulary in contracts/alert-contract.ts (for accept, "while using the app" / "allow while using app" before "only this time" / "allow once"), pick by that order, and refuse only on equal-rank duplicates. Please add Android and Limrun tests with the real three-button permission sheets that fail on this head.

The PR body still lists the final-head live evidence as pending, and the device route has changed since the earlier fail-before run: it now goes through the shared driver, read-first deadline order and the uniqueness boundary. Unit fixtures cannot show that the Limrun tap lands, or that the confirm leg sees none and not undecided once the sheet leaves. On a live Limrun iOS instance at the current head, please show: alert get --json on the ATT prompt reports the alert; alert accept reports handled with exactly one tap in the session request log; a second alert get reports "No alert visible"; alert dismiss on a fresh prompt presses "Ask App Not to Track"; and alert get over an in-app UIAlertController reports no alert. Please also run a location-permission prompt (iOS three-button, and Android 11+ runtime permission) to show the accept fix.

I could not read the Limrun captures behind the "no hittable key" claim, so I accepted it on consistency. I did not run tests, and I did not confirm on a live Android 11+ device that both grant buttons appear with permission_allow_* ids. The f1 regression comes from reading main and this head.

Simplicity question: could the shared driver and vocabulary stay, with the choice boundary as a declared role-plus-preference table in contracts/alert-contract.ts, refusing only on equal-rank labels seen twice? Each backend would then keep its own fallback (Android neutral/Back, Limrun none) without inheriting a stricter rule. Limrun alert-detection.ts is also about half module and function prose, which could shrink to the stated invariants.

On the open Cubic threads: these are fixed at this head and can be resolved: blank-root pid subtree, changelog residual, Android dismiss retry, typed refusal reason, driver reason test, one-tap dismiss test, and fixture comment. The label dedupe thread is fixed too, but the fix went too far into role-level uniqueness, as described above. Two threads do not apply: hittable:true because captures omit the key and only an explicit false vetoes, and limrun.md wording because the page describes backend behavior accurately.

Before merge, please restore single-press accept on multi-grant permission sheets with regression tests, then post the live Limrun ATT evidence.

@thymikee
thymikee marked this pull request as draft October 16, 2026 09:50
@thymikee
thymikee marked this pull request as ready for review October 16, 2026 11:33

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/platform-android/src/alert.ts Outdated
Comment thread packages/platform-android/src/alert.ts Outdated
Comment thread packages/provider-limrun/src/alert.ts
Comment thread packages/provider-limrun/src/ios-interactor-alert.test.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 10 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/platform-android/src/alert-detection.ts Outdated
Comment thread packages/contracts/src/alert-contract.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 13 files (changes from recent commits).

Requires human review: Auto-approval blocked because this review re-detected 2 unresolved issues already reported by Cubic.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread website/docs/docs/commands.md Outdated
Comment thread packages/platform-android/src/input-actions.ts Outdated
Comment thread CHANGELOG.md Outdated
Comment thread packages/platform-android/src/alert.ts Outdated
@thymikee

Copy link
Copy Markdown
Member Author

The earlier gaps from a27302c are fixed at 8d5e9d2: the preference table now lives in the shared alert contract, each backend keeps its own fallback, and the regression tests cover the earlier cases. Live proof at this head is still missing, and the cancellation work added in this round goes beyond what #3349 asks for.

The device route changed again in this round: ranked choice, window-guarded observer, signal-gated press, and a separate confirm window. The PR body says final-head live verification is still outstanding, and unit fakes cannot show that the Limrun tap lands once or that confirm sees none after the sheet leaves. So the Limrun iOS alert accept/dismiss route from #3349 and the changed Android permission route have no live proof at the head that will ship. Please run at 8d5e9d2 (or the next head) on a live Limrun iOS instance. On an ATT prompt, alert get --json should show status visible, alert accept should report handled with exactly one tap in the session request log, and a second alert get should show status none. On a fresh ATT prompt, alert dismiss should press "Ask App Not to Track". On a location prompt, alert accept should press "Allow While Using App". With an in-app UIAlertController, alert get should report none. On Android 11+, alert accept on the runtime permission sheet should press "While using the app".

Is the cancellation and window apparatus needed for #3349? It adds a second feature to the shared driver: request signals through every seam, wrapped errors keyed on the window-expired reason, dispatched: 'unknown', signal threading through Android press, back and snapshot, and a new status response field. On Limrun it cannot bound the transport, and the PR body says the late-capture case it targets still reproduces. Could this PR keep the shared driver, the ranked vocabulary and the Limrun legs, and check deadline.isExpired() once after each capture inside treeAlertObserver, so a late Limrun capture is not acted on? The signal threading, the unknown typing, the status field and the AbortSignal.timeout windows could then move to a follow-up with their own evidence. Signal-based bounds only apply to Limrun once the SDK exposes abort on elementTree and tap.

Not blocking, and you can take or leave these: alertSeamInterrupted is declared to return an error but throws in its canceled branch, and the request signal reaches the backends through two channels (runnerContext.signal on Android, options.signal on Limrun), so returning the error and reading the signal from AlertInteractorOptions alone would be tidier.

On the open threads from other reviewers, these still apply: the Limrun capture ignoring the seam signal, same-label buttons on Android, unranked accept filtered before the tie check, the docs on alert-window-expired, the changelog claim about bounded budgets and the AbortSignal.timeout overflow. The adb executor options thread does not apply, because every production caller passes only {signal}, so you can resolve it, or narrow the type to {signal} to close it.

CI is still pending: all 9 non-passing jobs are queued or in progress, with no failure logs yet. The diff overlaps Lint/Typecheck, Coverage and possibly Android smoke, while the Apple smoke lanes do not overlap it. I did not run the tests, and I did not reproduce the "20 ms wait returns alert at 59 ms" case. Before merge, this needs the live Limrun iOS and Android 11+ runs at the final head, and a decision on moving the cancellation work to its own PR.

thymikee added a commit that referenced this pull request Oct 16, 2026
…red driver

PR #3699 review: the Limrun iOS alert legs duplicated the Android tree-alert
driver (poll, confirm, same-alert, response building) and its label
vocabulary. Both backends now reach those steps through one backend-neutral
driver, runTreeAlertAction in platform-android (exported via the mechanics
facet provider-limrun already depends on), and classify button roles
through one English vocabulary that lives in contracts/alert-contract with
the alert budgets and typed reasons (contracts owns vocabulary only; the
driver loop owns the AppError/timer mechanics R18 keeps out of contracts).

Each backend supplies only its own seams: Android wraps its dump reader
and keeps its platform-id roles, neutral/Back fallbacks, and tap/Back
presses; Limrun supplies the blank-root process-exclusive host reader,
the label-strict chooser, and client.tap. The confirm-on-ambiguous-capture
and duplicate-label threads settle once in the shared code: the three-way
alert|none|undecided observation gates confirmation in the driver, and
selectUniqueAlertButtonByRole refuses duplicated roles at the shared
choice boundary. The deadline order resolves to read-first: over a
positive window both predecessor loops already observed before waiting.
The alert-not-found and undecided refusals now carry the same typed
details.reason and dispatched: 'no' on both backends.
@thymikee
thymikee force-pushed the t3/limrun-ios-alert-actions branch from 8d5e9d2 to 482cff1 Compare October 16, 2026 12:52
@thymikee

Copy link
Copy Markdown
Member Author

At 482cff1, the earlier gaps are mostly closed, but two defects remain, and the Android smoke check now fails because of one of them. The earlier a27302c review asked for a status field and for signal-bound captures and presses. Both now exist, and the tests cover the status field. The PR also has merge conflicts with main.

The Android smoke step dismiss Android native alert fails with reason alert-window-expired and the message "the action budget closed before an alert resolved". The cause is in treeAlertObserver at https://github.com/callstack/agent-device/blob/482cff1/packages/platform-android/src/alert.ts#L250. It guards the first probe with a 2 s window. A single helper snapshot that takes longer than 2 s is killed or refused, and accept or dismiss never presses. On main, the 2 s retry only bounded polling, so the first observation always finished and then acted. The doc block says the first observation is unguarded, but the code does the opposite, and no test covers this refusal. alert wait <ms> will fail the same way for short windows. The rule: a budget bounds how long the leg keeps polling. It never refuses or cuts off an observation that started inside the window. Let pollUntilSettled's deadline own the window, and pass only the request signal into capture and press. Please add a driven-clock test where the first capture takes 2.5 s and accept still presses once.

Localized Android permission sheets also regress, at https://github.com/callstack/agent-device/blob/482cff1/packages/contracts/src/alert-contract.ts#L118. selectRankedAlertButtonByRole drops candidates whose label is not in the English table before it checks for a tie. On Android, permission_allow_* ids give the accept role from the id alone. A Spanish sheet ('Mientras la app está en uso' / 'Solo esta vez') then has two accepts and zero ranked ones, so alert accept refuses where main tapped the foreground-only grant. I did not observe this on a device. I reasoned it from classifyAndroidAlertButtonById. Labels such as Android 10's 'Allow only while using the app' are also dropped, so 'Allow all the time' wins, which is the opposite of the table's intent. The rule: every role-carrying candidate gets a rank from its owning backend, and an unranked role counts in the tie check instead of being dropped. For Android, that means a rank by permission id: foreground-only, then always, then one-time. Please add a test with localized labels on permission_allow_* ids.

Could the PR get smaller? Keep the preference table, but rank by backend-declared keys: label rank for Limrun, permission-id rank for Android. Then drop the per-capture window code (treeAlertWindow, captureAlertState, alertSeamInterrupted, the window-expired reason and its phrasings, the press-signal checks). Let pollUntilSettled bound polling, thread only the request signal into capture and press, and document that Limrun's non-abortable SDK can overrun by one capture. That removes most of the ~320 added lines in alert.ts and fixes the first defect. The status field could ship separately across all alert backends if you want it. Nothing has to change first, since the cancellation work already goes beyond #3349.

The open inline threads on same-label buttons in alert-detection.ts (#3699 (comment)), unranked candidates in alert-contract.ts (#3699 (comment)), and the commands.md alert-not-found wording (#3699 (comment)) still apply. One more thread on AbortSignal.timeout range (#3699 (comment)) also applies, and it goes away with the first fix. These threads are fixed at this head and can be resolved: the status field (#3699 (comment)), signal-bound captures (#3699 (comment)), late Limrun captures (#3699 (comment)), the single elementTree call assertion (#3699 (comment)), and the CHANGELOG wording (#3699 (comment)). This thread does not apply and can be resolved: #3699 (comment), because the only callers pass { signal } and nobody passes allowFailure, so it is a type-width point only.

The failing Android smoke step looks related to this change. Only the new actOnTreeAlert catch branch emits that message, after src/core/interactors/android.ts started passing the 2 s signal into snapshotAndroid. I did not read the job's request log, so the capture time is inferred from the error shape. Bundle Size and Coverage were still pending, and I ran no tests.

Before merge, please stop the per-capture window from refusing the first Android capture, resolve the conflicts, and then show two live runs at the final head. First, the Android smoke automation-system scenario must pass 'dismiss Android native alert', and alert accept on an Android 11+ location permission sheet must report button 'While using the app' with one tap. Second, a Limrun iOS run must show alert get with status visible on a location or ATT sheet, then alert accept returning alertHandled with button 'Allow While Using App' (or 'Allow' for ATT), then a follow-up alert get with status none.

thymikee added a commit that referenced this pull request Oct 16, 2026
…red driver

PR #3699 review: the Limrun iOS alert legs duplicated the Android tree-alert
driver (poll, confirm, same-alert, response building) and its label
vocabulary. Both backends now reach those steps through one backend-neutral
driver, runTreeAlertAction in platform-android (exported via the mechanics
facet provider-limrun already depends on), and classify button roles
through one English vocabulary that lives in contracts/alert-contract with
the alert budgets and typed reasons (contracts owns vocabulary only; the
driver loop owns the AppError/timer mechanics R18 keeps out of contracts).

Each backend supplies only its own seams: Android wraps its dump reader
and keeps its platform-id roles, neutral/Back fallbacks, and tap/Back
presses; Limrun supplies the blank-root process-exclusive host reader,
the label-strict chooser, and client.tap. The confirm-on-ambiguous-capture
and duplicate-label threads settle once in the shared code: the three-way
alert|none|undecided observation gates confirmation in the driver, and
selectUniqueAlertButtonByRole refuses duplicated roles at the shared
choice boundary. The deadline order resolves to read-first: over a
positive window both predecessor loops already observed before waiting.
The alert-not-found and undecided refusals now carry the same typed
details.reason and dispatched: 'no' on both backends.
@thymikee
thymikee force-pushed the t3/limrun-ios-alert-actions branch from 482cff1 to 5b1e1c1 Compare October 16, 2026 14:00

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 5 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/platform-android/src/alert.ts Outdated
Comment thread packages/platform-android/src/alert.ts Outdated
thymikee added a commit that referenced this pull request Oct 16, 2026
…red driver

PR #3699 review: the Limrun iOS alert legs duplicated the Android tree-alert
driver (poll, confirm, same-alert, response building) and its label
vocabulary. Both backends now reach those steps through one backend-neutral
driver, runTreeAlertAction in platform-android (exported via the mechanics
facet provider-limrun already depends on), and classify button roles
through one English vocabulary that lives in contracts/alert-contract with
the alert budgets and typed reasons (contracts owns vocabulary only; the
driver loop owns the AppError/timer mechanics R18 keeps out of contracts).

Each backend supplies only its own seams: Android wraps its dump reader
and keeps its platform-id roles, neutral/Back fallbacks, and tap/Back
presses; Limrun supplies the blank-root process-exclusive host reader,
the label-strict chooser, and client.tap. The confirm-on-ambiguous-capture
and duplicate-label threads settle once in the shared code: the three-way
alert|none|undecided observation gates confirmation in the driver, and
selectUniqueAlertButtonByRole refuses duplicated roles at the shared
choice boundary. The deadline order resolves to read-first: over a
positive window both predecessor loops already observed before waiting.
The alert-not-found and undecided refusals now carry the same typed
details.reason and dispatched: 'no' on both backends.
@thymikee
thymikee force-pushed the t3/limrun-ios-alert-actions branch from 2482e24 to f584e8a Compare October 16, 2026 15:30
@thymikee

Copy link
Copy Markdown
Member Author

Head f584e8a1 (rebased onto current main) addresses the two defects from your 482cff1 comment and carries the receipts you asked for.

Android smoke step. The dismiss Android native alert failure at 5b1e1c1 had two causes, both fixed. The session's alert probes ran without the app-backed helperSessionScope, so each probe cold-started the snapshot helper inside the 2 s window that bounds it — the request log shows the post-press confirm capture aborted at 2.3 s while the canary proves the dialog was dismissed. Alert reads now scope like every other app-backed read. And the wording lied: with no confirm capture ever answering, "did not dismiss the visible alert" accused a dialog that was already gone. Now only the latest post-press capture that answered and names the same alert earns that failure; an unconfirmed press says exactly that, and every spent-budget/cut-off exit carries typed alert-window-expired (the two newest cubic threads are fixed here with fail-before pins).

Live proofs at this head (repo-built CLI / instrumented real client seam, isolated state):

  • Android 11+ live ACCESS_FINE_LOCATION sheet: accept → button: "While using the app" (declared preference over "Only this time"), dumpsys granted=true, second get none; dismiss on no-alert refuses with alert-not-found/dispatched: no and zero taps.
  • Limrun iOS: app-owned UIAlertController → get none, accept refuses with zero taps; ATT accept taps exactly once (counter at the client seam), second get none; fresh-bundle dismiss taps the denying choice once — never authorizes; three-button location sheet accepts "Allow While Using App" with one tap, second get none.

Receipts: direct gate pnpm check:affected --run --head f584e8a15… passed with exactCommit: true on a clean checkout; CI on this head 16 pass / 0 fail while writing this; all seven review threads have per-thread dispositions. A fresh independent design review of this head (including the smallest-interface assessment of the ~1.1k production lines and the window/observer seam) is running; I'll fold in what it finds rather than declare it done.

Kept honest, not widened: Limrun SDK 0.64.1 exposes no abort on elementTree/tap, so its guards answer post-capture and pre-press and an already-sent request runs on — stated in code, docs, and CHANGELOG rather than papered over; budgets are capped at MAX_ALERT_WINDOW_MS, not clamped; the residual ownership limit (an app dialog under a blanked root is not separable by capture facts) still resolves to refusal, never a guess. Native Swift alert choice stays #3627.

@thymikee

Copy link
Copy Markdown
Member Author

Coverage run 38924441176 at f584e8a1 failed one real test, and the driver — not the oracle — was wrong. confirmTreeAlertDismissed's cutoff catch downgraded every thrown window-expiry to "could not be confirmed", discarding the post-press captures that had answered. On CI the final confirm capture starts inside the 2 s window and lands past it (throws), so an answered same-alert sighting got erased and the error claimed the latest check never answered a dialog that two answered reads had just re-named. Locally the same timeline ends via the never-captures sentinel path, which honors the observation — that dual-path divergence is why check:affected at the exact head passed while Coverage failed (the affected selection doesn't run test/integration/provider-scenarios, and I didn't run it locally; my error).

Fixed at f4b928974: the still-visible claim rides the latest capture that answered, at both the thrown and the sentinel exit; "could not be confirmed" stays reserved for a phase where no completed post-press read named the alert — the 5b1e1c1 artifact timeline, where no confirm capture ever answered, still yields unconfirmed. The undecided-overrules-earlier-match case keeps expecting unconfirmed (undecided is itself an answered observation). The integration oracle's expected message is unchanged; my hung-after-match unit pin was flipped to the CI timeline as the detecting test. 957726b0d batches the round-1 review's three P3 items (wait-only [timeout] docs, a mutation-checked dispatched: unknown press pin, _device nit, one dedup helper). Round-2 independent review of both commits is running; the direct gate for 957726b0d is queued in host serialization and its receipt will be posted here.

@thymikee

Copy link
Copy Markdown
Member Author

Receipts for 957726b0d, per the disclosure above:

  • Direct gate: pnpm check:affected --run --head 957726b0de7e2049b556a0b58efceca0e4af31a6 — passed, exactCommit: true, checkout unchanged (.tmp/validation/run-Sh88Va/receipt.json). Its selection lists provider-integration as deduplicated, so that suite also ran explicitly (197/197) here and in the reviewer's checkout.
  • Independent round-2 review (/tmp/3349-review-9577-report.md): merge-ready, no new findings. Verified independently: the fix is at the driver's error-classification branch with the integration oracle unmodified; the thrown-cutoff and sentinel paths cannot disagree (same predicate, both pinned — the f584 gap was exactly the missing thrown+same-alert pin, which now has fail-before evidence); the 2482e24 zero-answered-confirms timeline still yields unconfirmed on the thrown path; dispatched: 'unknown' re-confirmed mutation-checked by the reviewer; messages byte-compared with no drift; production growth of the two commits is +12 net lines with one duplication removed. Round 1's three P3 items are closed each with a detecting artifact.
  • CI at this head: 17 pass, 0 fail (final iOS smoke matrix finishing; parent watches).

No further changes planned on my side for the alert family at this head.

@thymikee

thymikee commented Oct 16, 2026 •

Copy link
Copy Markdown
Member Author

Corrected Android live proof at 957726b0d, replacing the earlier run's precondition attribution.

The earlier proof script (android-live-f584b.sh) ran adb shell pm reset-permissions com.example.locprobe, which AOSP applies globally — the package argument does not scope it. That violated the no-global-privacy-resets rule; its artifact and disclosure stand, and no global undo was attempted (the scoped revoke re-revokes the probe's own two permissions).

Corrected run: emulator-5554 via T3 device_list/device_open (unclaimed lane), CLI built at clean 957726b0de7e2049b556a0b58efceca0e4af31a6 (git status empty), isolated --session/--state-dir. Preconditions only: own-bundle pm revoke of FINE and COARSE location (exits 0/0; grants true before, false after the revokes), own-bundle force-stop; no reset-permissions, no logcat -c.

Results (raw JSON + per-command exits in /private/tmp/3349-proof-957/): alert get → status: visible, the five-button multi-grant sheet (exit 0); alert accept → handled: true, button: "While using the app", coords (540,1443) (exit 0); dumpsys after: FINE+COARSE granted=true; second alert get → status: none (exit 0); alert dismiss on the quiet screen refused pre-press with typed alert-window-expired, dispatched: no (exit 1) — the actual typed pre-press cutoff result; detailed request timing was not retained. Note: no client-seam tap counter exists on this public CLI route, so no tap-count claim is made here; the exactly-once rule is unit-pinned.

Limits honestly kept: this Android leg does not instrument tap counts; the Limrun iOS instrumented-seam proof remains banked at the earlier head (PROBE_EXIT 0), whose attribution to that head stands — the newer commits change only the cut-off error path.

thymikee added a commit that referenced this pull request Oct 16, 2026
…red driver

PR #3699 review: the Limrun iOS alert legs duplicated the Android tree-alert
driver (poll, confirm, same-alert, response building) and its label
vocabulary. Both backends now reach those steps through one backend-neutral
driver, runTreeAlertAction in platform-android (exported via the mechanics
facet provider-limrun already depends on), and classify button roles
through one English vocabulary that lives in contracts/alert-contract with
the alert budgets and typed reasons (contracts owns vocabulary only; the
driver loop owns the AppError/timer mechanics R18 keeps out of contracts).

Each backend supplies only its own seams: Android wraps its dump reader
and keeps its platform-id roles, neutral/Back fallbacks, and tap/Back
presses; Limrun supplies the blank-root process-exclusive host reader,
the label-strict chooser, and client.tap. The confirm-on-ambiguous-capture
and duplicate-label threads settle once in the shared code: the three-way
alert|none|undecided observation gates confirmation in the driver, and
selectUniqueAlertButtonByRole refuses duplicated roles at the shared
choice boundary. The deadline order resolves to read-first: over a
positive window both predecessor loops already observed before waiting.
The alert-not-found and undecided refusals now carry the same typed
details.reason and dispatched: 'no' on both backends.
@thymikee
thymikee force-pushed the t3/limrun-ios-alert-actions branch from 957726b to 326aa3b Compare October 16, 2026 17:23
thymikee added a commit that referenced this pull request Oct 16, 2026
…red driver

PR #3699 review: the Limrun iOS alert legs duplicated the Android tree-alert
driver (poll, confirm, same-alert, response building) and its label
vocabulary. Both backends now reach those steps through one backend-neutral
driver, runTreeAlertAction in platform-android (exported via the mechanics
facet provider-limrun already depends on), and classify button roles
through one English vocabulary that lives in contracts/alert-contract with
the alert budgets and typed reasons (contracts owns vocabulary only; the
driver loop owns the AppError/timer mechanics R18 keeps out of contracts).

Each backend supplies only its own seams: Android wraps its dump reader
and keeps its platform-id roles, neutral/Back fallbacks, and tap/Back
presses; Limrun supplies the blank-root process-exclusive host reader,
the label-strict chooser, and client.tap. The confirm-on-ambiguous-capture
and duplicate-label threads settle once in the shared code: the three-way
alert|none|undecided observation gates confirmation in the driver, and
selectUniqueAlertButtonByRole refuses duplicated roles at the shared
choice boundary. The deadline order resolves to read-first: over a
positive window both predecessor loops already observed before waiting.
The alert-not-found and undecided refusals now carry the same typed
details.reason and dispatched: 'no' on both backends.
@thymikee
thymikee force-pushed the t3/limrun-ios-alert-actions branch from 326aa3b to 60aec19 Compare October 16, 2026 18:18
Limrun iOS direct sessions refused all four alert legs with
unsupported-provider-mode even though the session's element-tree capture
already shows the system alert and its touch seam can press its buttons.
Issue #3349.

Detect the alert from the capture: a system alert arrives as its own
blank-label AXApplication root hosting text and pressable buttons, while
the app's own UIAlertController rides the app's named root, so app-owned
dialogs stay app UI. Accept and dismiss press exactly the button whose
label states the asked-for choice and confirm the alert is gone; an
ambiguous choice is reported, never guessed, and dismiss never
authorizes tracking. The four legs bind through the same catalog and
facts the Android tree-based legs already use.
Review follow-up on the alert legs: a sheet must offer at least two
pressable choices, launch-icon buttons never qualify, a capture whose
host sheets disagree is refused instead of shadowing one with the
other, and an action fires only when exactly one button states its
choice. The legs' poll/confirm budgets now spend through an injected
sleep seam so their tests drive the clock, a malformed timeout fails as
a bounded default instead of an unbound loop, and the grant vocabulary
covers the iOS variants (Allow Once / While Using App / All the Time)
and Close App. User docs and CHANGELOG updated.
…red driver

PR #3699 review: the Limrun iOS alert legs duplicated the Android tree-alert
driver (poll, confirm, same-alert, response building) and its label
vocabulary. Both backends now reach those steps through one backend-neutral
driver, runTreeAlertAction in platform-android (exported via the mechanics
facet provider-limrun already depends on), and classify button roles
through one English vocabulary that lives in contracts/alert-contract with
the alert budgets and typed reasons (contracts owns vocabulary only; the
driver loop owns the AppError/timer mechanics R18 keeps out of contracts).

Each backend supplies only its own seams: Android wraps its dump reader
and keeps its platform-id roles, neutral/Back fallbacks, and tap/Back
presses; Limrun supplies the blank-root process-exclusive host reader,
the label-strict chooser, and client.tap. The confirm-on-ambiguous-capture
and duplicate-label threads settle once in the shared code: the three-way
alert|none|undecided observation gates confirmation in the driver, and
selectUniqueAlertButtonByRole refuses duplicated roles at the shared
choice boundary. The deadline order resolves to read-first: over a
positive window both predecessor loops already observed before waiting.
The alert-not-found and undecided refusals now carry the same typed
details.reason and dispatched: 'no' on both backends.
origin/main migrated the Android alert loops to pollUntilSettled/Deadline
and moved timing into fake timers at the test edge. The shared driver
adopts the same shape: poll/confirm run on the deadline primitive, so the
driver-owned sleep seam is dead weight and goes — Limrun's tests now drive
the clock the way the migrated Android tests do.
…y catch-name lint

Adds the maintainer's live-validated sequence at the unit seam: accept
taps exactly once, and a following get on the same seam reports no alert.
Renames catch params to satisfy oxlint's catch-error-name (three
findings the affected gate reported).
The unique-role choice boundary refused exactly the sheets the fix is
for: Android 11+'s permission_allow_foreground_only_button and
permission_allow_one_time_button both classify accept, and iOS's
'Allow Once'/'Allow While Using App' sheet refused the same way. The
shared vocabulary is now a per-role preference table: distinct grants
resolve by declared rank (a lasting grant before a one-time grant), and
the acting legs refuse only when the capture names no single physical
button — the same label at two points, or vocabulary-free multiplicity.
Android keeps its neutral-button/Back fallback, Limrun keeps none.
… owns

Three review findings on the shared tree-alert driver, plus the test
that masked a polling loop:

- alert get on a tree backend answered undecided exactly like absence
  (alert: null, message-only difference). The result now carries a typed
  status (visible/none/undecided) so clients switch on states, not text.
- The budgets bounded only the poll sleeps: a capture or press that hung
  could outrun the leg. Each leg now builds a window (deadline clock plus
  the joined window/caller signal) that reaches the capture and press
  seams; the Android legs hand it to the snapshot capture and the adb
  input sends, which kill their child on abort. A capture that lands
  once its window closed is refused on a typed alert-window-expired
  reason and never consumed or pressed; a press cut off mid-flight says
  dispatched: unknown, one refused before dispatch says no.
- Limrun's WebSocket transport exposes no abort on elementTree/tap
  (checked against the shipped typings), so the consumption rule is what
  keeps it honest, and the SDK gap is stated rather than papered over.
- The awaitAlert first-capture test now asserts the capture count; its
  same-alert fixture had made it vacuous.
The shared tree-alert driver consumed an answer from a poll that started
after its window closed (the loop tests settled before remaining, and an
expired-at-start probe rode unguarded), and its first probe paid the
Android helper cold start inside the action budget, aborting the very
read that leg was about to press on — the executed smoke failure. The
first observation is now read-first, unarmed, consumed once, and arms
the window behind it; a later probe starting past the deadline never
captures at all, and the press rides the armed window.

- contracts: a mix of ranked and unranked role candidates refuses
  instead of letting the one known label win (selectRankedAlertButton-
  ByRole); MAX_ALERT_WINDOW_MS names the timer ceiling the daemon
  refuses above (INVALID_ARGS) and the driver defaults above.
- android: same-label dedup keys on the physical center, so nested
  nodes collapse and two distinct OKs reach the shared refusal; the
  input dispatch option type owns cancellation only, and the tap
  forward picks fields so allowFailure cannot reach the transport.
- docs: alert-window-expired/none/undecided reasons stated exactly per
  phase, Limrun's non-abortable transport described as post-capture and
  pre-press guards with the hung-request limit named.

Prose at the driver trims to invariants.
…confirmed press

The executed Android smoke failure at 5b1e1c1: the session's alert probes
ran without the app-backed helper session scope, so each probe cold-started
the snapshot helper inside the window that bounds it — the request log shows
the post-press confirm capture aborted at 2.3s ("operation was aborted due
to timeout") while the canary proves the dialog was in fact dismissed. Alert
reads now carry the same helperSessionScope as every other app-backed read.

The failure also proved the wording lied: with no confirm capture ever
answering, "did not dismiss the visible alert"/"dialog is still visible"
accused a dialog that was already gone. Only a re-read naming the same alert
earns the still-visible failure; an unconfirmed press now says exactly that,
on both the cut-off and the spent-budget exits, with a hint to run alert get
before retrying. The same-alert stuck-alert failure keeps its message.

Pinned: interactor scoping regression (fail-before against 5b1e1c1) and
the three Limrun confirm-phase tests moved to the truthful split; the
Android still-visible pin keeps its message via the same-alert path.
Two review findings on the confirm split, one batch:

- The spent-budget exits of confirmTreeAlertDismissed omitted
  details.reason, while commands.md promises every post-press failure
  branches on alert-window-expired. Both unconfirmed and still-visible
  failures now carry the typed reason, pinned in the Android and Limrun
  suites.
- The same-alert flag latched on the first matching capture, so a later
  undecided re-read still reported the dialog as currently visible.
  Each probe now replaces the verdict with its own observation: the
  still-visible failure requires the latest capture that answered to
  name the same alert, and a fresher undecided read exits as an
  unconfirmed press. A probe whose capture never answered is not a
  completed observation, so the thrown cutoff keeps saying only that
  the press went unconfirmed, at any earlier match.

Pinned fail-before at the pre-fix alert.ts: same-alert then undecided
to budget-end, and same-alert then a hung capture, both expect the
unconfirmed message and the typed reason. The helper-scope comment is
trimmed to the non-obvious constraint per AGENTS.
…swered sighting

The Coverage run at f584e8a (38924441176) failed the maintainer-owned
provider-integration scenario 'Android alert accept fails when the dialog
stays visible' — a suite the affected gate's related-test selection does
not reach, so the local exact-head receipt never ran it. The driver, not
the oracle, was wrong: f584's confirm catch downgraded every thrown cutoff
to unconfirmed, discarding the captures that had answered before it. On CI
the final confirm capture starts inside the 2s window and lands past it
(the thrown path); locally it starts after the deadline (the sentinel
path), which is why the same commit passed one and failed the other. The
dialog had been re-named by answered reads; the error claimed the check
never answered.

The latest-completed-observation rule from review thread r4296070770 and
from commands.md is: the latest capture that ANSWERED carries the claim.
A hung probe is not an observation, so the cutoff now reports
still-visible when an answered capture named the alert, and unconfirmed
only when none did — the 2482e24 smoke case, with no answer at all. The
undecided-overrules-match case is unchanged: undecided is an answered
observation. Pin flipped to the CI timeline (fail-before at f584's catch);
all 128 alert-family tests and the 197-test provider-integration project
pass at this working tree.
The independent review of f584e8a returned merge-ready with three
follow-up-scale findings, batched here:

- The documented `dispatched: 'unknown'` press-cutoff failure had no test.
  Pinned in the Android suite with the real transport shape: the adb fake
  hangs the tap until the 2s action window closes and dies with the
  signal; the failure must disclose unknown, never 'no'. Mutation-checked:
  flipping the disclosure to 'no' fails the pin.
- The `[timeout]` positional is honored by `alert wait` only — the Apple
  acting legs pass fixed runner budgets and the tree legs run their own
  lookup/confirm windows; commands.md now says the argument is wait-only
  per backend instead of leaving the silence to surprise callers.
- `void device;` in limrunAlertOperationFacts follows the `_device`
  convention; the two byte-identical visible-after-press exits collapse
  into one helper, keeping the two hints that genuinely differ (latest
  read hung vs budget ended unresolved).

No behavior change for any message, reason, or tap.
@thymikee
thymikee force-pushed the t3/limrun-ios-alert-actions branch from 60aec19 to be48a28 Compare October 16, 2026 19:12
@thymikee

Copy link
Copy Markdown
Member Author

Thanks for the update. Two issues from the earlier review at 482cff1 are fixed, and two defects remain at be48a28. CI is green on 20 checks, and the earlier Coverage failure on the confirm path is gone since 71087fb. I ran no tests and no devices, so the points below come from reading the code and your 957726b and f584 runs.

Fixed since 482cff1: the spent-budget confirm exits, the confirm probe wrapper, the button dedup, the unranked tie check and the window cap are addressed at this head.

First, the budget rule is still only half applied in https://github.com/callstack/agent-device/blob/be48a28/packages/platform-android/src/alert.ts#L270. A probe that started inside the window must finish. Only the first observation has that now. treeAlertObserver passes window.signal into captureAlertState, and the post-capture isExpired check throws the same error on Limrun. Android captures take 0.5-1.5 s at a 300 ms interval. On a quiet screen the last probe usually crosses the 2 s deadline, so actOnTreeAlert answers alert-window-expired instead of alert-not-found. Your 957726b run shows this for dismiss, and your f584 run shows alert-not-found, so the reason depends on timing. Clients that branch on details.reason will misroute, and commands.md promises alert-not-found. Please apply the whole rule in treeAlertObserver and captureAlertState: a started capture finishes, and only caller cancellation stops it. Let the poll deadline end the loop. Then lookup ends in none or undecided, and the window-expired lookup branch, the window signal on capture and the post-capture refusal can go. A driven-clock test would pin it: a quiet screen with captures near 800 ms across the 2 s lookup must answer alert-not-found. The current code should fail that test.

Second, accept now refuses where main pressed, in https://github.com/callstack/agent-device/blob/be48a28/packages/contracts/src/alert-contract.ts#L140. The shared tie check rejects any unranked candidate. A localized Android 11+ sheet ("Mientras la app está en uso" and "Solo esta vez") now returns null from chooseAndroidAlertButton, so accept fails with alert_button_undecided. The Android 10 sheet does the same, because "Allow only while using the app" is not in the preference table. Main tapped the first permission_allow button. CHANGELOG line 10 says refusal happens only for one label at two points, so this change is undocumented. The rule should be that every role-carrying candidate gets a rank from its owning backend before the tie check. Please let chooseAndroidAlertButton supply an id-derived rank (foreground only, then always, then one-time) and use label rank only where no id exists. A test with Spanish labels on the foreground and one-time ids that expects the foreground grant would cover it. If you prefer to keep the refusal, please state it in the CHANGELOG and commands.md.

Could this be simpler? Both findings come from the per-capture window: treeAlertWindow, the arm/start state, the delegating clock, the window-closed marker, lastCaptureNamesAlert and the second post-press failure phrasing. Would it work to keep the shared driver and the preference table, rank through backend-declared keys (permission id on Android, label on Limrun), and let pollUntilSettled bound polling? Then only the request signal reaches capture and press, and the docs say a Limrun capture can overrun by one call. That would also let you move the driver out of platform-android into a neutral module, so the iOS provider does not depend on the Android package. It would remove most of the roughly 320 driver lines and bring the production growth under the 700-line threshold. The extra cancellation goes beyond #3349, which asks only that Limrun serve the four legs.

The open inline threads still stand for the shared tie check (#3699 (comment), which is the localized-sheet issue above) and for the commands.md reason split (#3699 (comment), which the code does not yet honor on a quiet screen). These are fixed at this head and you can resolve them: #3699 (comment), #3699 (comment), #3699 (comment) and #3699 (comment). The thread at #3699 (comment) does not apply, because @limrun/api exposes no abort and the limit is documented.

After the fix, please rerun both proofs at the final head. On the Android emulator, with no alert on screen, alert dismiss and alert accept must return details.reason alert-not-found with dispatched no, three runs in a row. On an Android 11+ location sheet, alert accept must report button "While using the app", and a second alert get must report status none. On Limrun iOS, alert get --json must show status visible on an ATT or location sheet. Then alert accept must return alertHandled with button "Allow" or "Allow While Using App" and one tap in the request log, and a follow-up alert get must show status none. The current Limrun proof is from f584, before the confirm-path changes in 83b6 and 71087.

Before merge, the tree-alert driver must consume every probe that started inside its window, so a quiet-screen accept or dismiss always answers alert-not-found. Then the live Android and Limrun proofs must be rerun at that head.

The maintainer review at be48a28 named two defects and a design
simplification; this applies all three.

Budget rule (whole): the alert budget now bounds only how long a leg
polls. pollUntilSettled awaits every probe it starts, the deadline ends
the loop, and only the caller's cancellation stops a capture or press in
flight. A quiet screen answers alert-not-found with dispatched:no on
every backend, never a reason that depends on which capture crossed the
deadline; the late positive that used to be refused is consumed and
pressed, per the rule the maintainer asked for and this supersedes the
earlier late-answer-refusal contract.

Grant ranking: the shared choice boundary takes a backend rank first and
the label vocabulary only where the backend declares nothing. Android
declares the permissioncontroller grant ids (foreground-only, always,
one-time), so a Spanish Android 11+ sheet and the Android 10 foreground
copy resolve to the lasting grant instead of refusing; equal ranks and
candidates no rank reaches keep refusing.

Design: the driver moved to capture-kit/alert-tree-driver (Limrun no
longer depends on platform-android), treeAlertWindow/arm/start, the
delegating clock, the window-closed sentinel, lastCaptureNamesAlert, and
the exception paths are gone; net production diff vs be48a28 is -161
lines. Docs and CHANGELOG state the chosen behavior.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 15 files (changes from recent commits).

You’re at about 94% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/provider-limrun/src/alert.test.ts
Comment thread packages/capture-kit/src/alert-tree-driver.ts
Comment thread packages/contracts/src/alert-contract.ts Outdated
Comment thread packages/platform-android/src/__tests__/alert.test.ts Outdated
Comment thread packages/contracts/src/alert-contract.test.ts
Comment thread packages/provider-limrun/src/alert.ts
Comment thread website/docs/docs/commands.md Outdated
Comment thread website/docs/docs/commands.md Outdated
Comment thread packages/platform-android/src/__tests__/alert-detection.test.ts
Comment thread packages/capture-kit/src/alert-tree-driver.ts
…st-press seam, rank only on one scale

Follow-up to the review at 5cfe029:

- platform-android: load the shared tree-alert driver behind the alert
  entry's function-scoped import (types stay static), so mechanics.ts keeps
  its merge-base eager closure (177->176, the gate's owning-edge rule).
- capture-kit driver: a caller cancel is typed dispatched:'unknown' from
  the press onward even when a non-abortable transport RESOLVES after the
  cancel or the cancel lands in the confirm window, and a cancel that rode
  a straddling lookup probe refuses cleanly (dispatched:'no', no press).
- contracts: selectRankedAlertButtonByRole refuses pairs mixing backend
  id-ranks with label-table positions — different scales are not
  comparable; same-source conflicts still let the owning backend win.
- tests: deterministic started-gated cancel pins (no unreachable mock
  branches), Limrun no-abort capability pins, backend-rank-override and
  mixed-scale pins at the contract seam, faithful vendor-id fixture.
- docs: correct the CHANGELOG/commands/driver claims about Limrun
  cancellation (started calls run to their answer; the leg ends at the
  next seam cancel can reach), scope the quiet-screen guarantee to
  accept/dismiss, and limit the details.reason promise to driver
  refusals; fix the stale platform-android module comment.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 10 files (changes from recent commits).

You’re at about 95% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/platform-android/src/alert.ts Outdated
Comment thread website/docs/docs/commands.md Outdated
Comment thread packages/provider-limrun/src/alert.test.ts
…confirm-never-started pin

- platform-android/alert.ts: drop the false comparison to how the Limrun
  provider loads its own alert file; the comment now states only the real
  constraint — a static driver import would grow the mechanics facade past
  its merge-base eager-closure budget (ADR-0019).
- commands.md: replace 'the next seam cancellation can reach' with what
  the reader observes: a Limrun request already sent is not stoppable;
  cancellation lands once that request answers; restarting the instance
  session clears a stall. Human-rule facts unchanged.
- alert.test.ts: the resolves-after-cancel pin also asserts the leg ended
  before any confirm read started (elementTree exactly once).
…he stall docs exhaustive

R6-review P3 follow-ups (report /tmp/3349-r6-review-report-3e1e.md):

- capture-kit driver: a capture whose transport died with the caller's
  cancellation exits through observeTreeAlert, which is always pre-press
  (acting legs never press before the lookup returns) — it now carries the
  clean dispatched:'no' like the other pre-press exit instead of omitting
  the key; both backend pins assert it. Pins fail without the key.
- capture-kit driver: correct the seam comment's attribution — the caller
  learns of its cancellation from its own client transport; the daemon's
  answer lands once the leg settles on the started probe's answer.
- commands.md: the Limrun stall sentence enumerates the SDK's own ~30 s
  request timeout (verified against @limrun/api@0.64.1 sendRequest) as a
  route the unanswered request resolves on, instead of reading exhaustive.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 4 files (changes from recent commits).

You’re at about 95% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/capture-kit/src/alert-tree-driver.ts Outdated
…utes that end here

R7 P3 / inline r4300817984: 'always a pre-press exit' overstated the
catch's reach — confirmTreeAlertGone polls through the same
observeTreeAlert, so a cancel mid-confirm-probe reaches it post-press
(harmless: the actOnTreeAlert press/confirm catch re-types it to
dispatched:'unknown', which owns that invariant). The comment now states
where each typed answer is owned instead of asserting a false
reachability fact. No behavior change.
@thymikee

thymikee commented Oct 17, 2026 •

Copy link
Copy Markdown
Member Author

Final live proofs at the certified head 92df16aa70fb3d2812e94dd15e544b57abfffa62, with the raw answers and the honest method limits.

Cert chain for this head (all earlier heads superseded): exact gate pnpm check:affected --run --head 92df16aa70… run-ktnH3c PASS, exactCommit: true, clean window 02:17:35→02:23:32Z; CI green at this head (21 SUCCESS / 2 intentional SKIP / 0 FAIL — an observation of the lane, not retroactive clearance of the 3e1e failure, whose cause and participation both remain unproven in either direction); independent review rounds found no P0/P1/P2 at this head. The diff is comment-only since 5fc23229 (+2/−3, the fix above).

Android (emulator-5554, own session and3349d92df, own state dir; head + clean tree asserted in the run log)

Quiet screen, six acting commands on a screen with no alert — every raw answer identical in content (only diagnosticId/logPath differ):

alert dismiss x3 and alert accept x3, each:
  "success": false, "code": "COMMAND_FAILED", "message": "alert not found",
  "details": { "reason": "alert-not-found", "dispatched": "no" }

Never alert-window-expired on any of the six — the budget rule holds live. Quiet alert get reported status: "none".

Android 11+ location sheet, fresh grant state (pm revoke on own bundle only, both granted=false verified before start):

  • the live sheet's own uiautomator dump carries the backend-owned grant ids the id-rank table reads: permission_allow_foreground_only_button, permission_allow_one_time_button, permission_deny_and_dont_ask_again_button (+ accuracy radios);
  • alert get → status: "visible", title "Allow LocProbe to access this device's location?", packageName: com.google.android.permissioncontroller;
  • alert accept → handled: true, "button": "While using the app" (foreground-only id-rank chosen over one-time, coordinates 540,1443);
  • permissions after accept: FINE granted=true, COARSE granted=true;
  • follow-up alert get → status: "none".

Cleanup was own-bundle only (pm revoke + force-stop, exits logged); no pm reset-permissions, no logcat -c. Raw evidence preserved BEFORE cleanup: all JSONs plus the session's per-request NDJSONs (the six failure flushes + events.ndjson) copied out of the state dir.

Limrun, real CLI + daemon route (fresh session lr3349g92df)

All commands through the real CLI at this head (bin/agent-device.mjs after a clean pnpm build, dirty 0) over connect limrun --platform ios → install → open → CLI press (app trigger) → alert get → alert accept → alert get. Credentials were sourced from the main checkout's .env by absolute path, never echoed.

  1. alert get → status: "visible": ATT prompt, buttons ["Ask App Not to Track", "Allow"], source: "permission".
  2. alert accept → handled: true, "button": "Allow", coordinates {201, 592}.
  3. follow-up alert get → status: "none".

Transport evidence: the full session's outgoing Limrun WebSocket frames, 13 total, every line tagged with the pid of the actual daemon process (process.command verified = this checkout's dist/src/internal/daemon.js, pid 53800):

02:54:11.595  out  deviceInfo
02:54:12.049  out  listApps
02:54:12.390  out  appInstallation
02:54:20.382  out  listApps
02:54:21.034  out  launchApp
02:54:26.957  out  elementTree   (snapshot before the trigger press)
02:54:27.838  out  elementTree   (press: selector resolution)
02:54:28.053  out  tap           (press: Request tracking)
02:54:33.558  out  elementTree   (alert get #1)
02:54:33.924  out  elementTree   (alert accept: lookup)
02:54:34.071  out  tap           (alert accept: the one press)
02:54:34.333  out  elementTree   (alert accept: post-press confirm)
02:54:37.712  out  elementTree   (alert get #2)

The frames are the complete ordered transport record of one sequential script (connect → install → open → snapshot → press → get → accept → get); the bracket notes state which command each frame positionally belongs to, since the transport log carries the SDK request id but not the daemon request id. The session contains exactly two taps and they attribute 1:1 to the only two commands that report a tap, by sequential CLI-process ordering (the coordinates below come from CLI responses; the transport logger did not retain frame coordinates): the app-trigger press label="Request tracking" at (201, 451) → the tap at 02:54:28.053, and alert accept (Allow, 201, 592) → the tap at 02:54:34.071. The accept command's own transport shape is exactly lookup capture → one tap → confirm capture; the other four elementTree frames belong to the snapshot, the trigger press's selector resolution, and the two alert get reads. No duplicate press ever left the daemon.

Method limits, stated plainly:

  • the daemon's per-request NDJSON records daemon-internal phases only and persists for failures/debug, so the successful Limrun run's copied session contains events.ndjson only — no per-request Limrun logs are claimed here; the tap counts above come from the transport log, not the request logs.
  • that transport log was captured by running the real daemon under an in-memory ESM loader hook (NODE_OPTIONS=--import, inherited by the daemon spawn) that only appends a log line per send() and delegates; installed files were untouched and the shipped code path was not modified. It is proof instrumentation, not a product feature.
  • the run's window markers were whole-second granularity, so the one-tap claim does not rest on window-bounded timestamps — it rests on the sequential script ordering and the total of two outgoing tap frames; the CLI reports each command’s coordinates separately.

Instance hygiene: disconnect exit 0; both instances created by this campaign's Limrun runs (the instrumented proof instance and the earlier trial run at this head whose transport hook was not yet capturing frames) were deleted by id through the API — both deletions confirmed by command output. The trial run is disclosed for completeness: it showed the same functional sequence (visible → accept Allow → none) but produced no transport log, so the fresh instrumented re-run above is the cited proof. No other tenant resources were touched.

The Android quiet-screen rule, the id-ranked grant choice, the ATT accept path, and the one-tap transport shape are all now demonstrated live at the final head.

Raw JSON and transport appendix

The following are the saved command responses, including diagnostics. Limrun warning-banner lines precede its JSON files locally; only those banners are omitted below. Transport frames are unedited. Individual command exits are recorded in the run logs; no whole-script pipeline exit is asserted.

Android q-get:

{
  "success": true,
  "data": {
    "kind": "alertStatus",
    "platform": "android",
    "action": "get",
    "alert": null,
    "status": "none",
    "message": "No alert visible"
  }
}

Android q-dismiss-1:

{
  "success": false,
  "error": {
    "code": "COMMAND_FAILED",
    "message": "alert not found",
    "hint": "If a sheet is visible in snapshot but alert reports no alert, it is likely app-owned UI. Use snapshot -i and press the visible label/ref.",
    "diagnosticId": "mvbsbxvu-d6e4b6fd",
    "logPath": "/private/tmp/3349-ad-state-d92df/sessions/and3349d92df/requests/0d482aa87f911f07.ndjson",
    "diagnosticsRecord": {
      "session": "and3349d92df",
      "requestId": "0d482aa87f911f07"
    },
    "details": {
      "reason": "alert-not-found",
      "dispatched": "no"
    }
  }
}

Android q-dismiss-2:

{
  "success": false,
  "error": {
    "code": "COMMAND_FAILED",
    "message": "alert not found",
    "hint": "If a sheet is visible in snapshot but alert reports no alert, it is likely app-owned UI. Use snapshot -i and press the visible label/ref.",
    "diagnosticId": "mvbsc12i-c9cee6ed",
    "logPath": "/private/tmp/3349-ad-state-d92df/sessions/and3349d92df/requests/b9d798492ec50b85.ndjson",
    "diagnosticsRecord": {
      "session": "and3349d92df",
      "requestId": "b9d798492ec50b85"
    },
    "details": {
      "reason": "alert-not-found",
      "dispatched": "no"
    }
  }
}

Android q-dismiss-3:

{
  "success": false,
  "error": {
    "code": "COMMAND_FAILED",
    "message": "alert not found",
    "hint": "If a sheet is visible in snapshot but alert reports no alert, it is likely app-owned UI. Use snapshot -i and press the visible label/ref.",
    "diagnosticId": "mvbsc45j-42e7e83a",
    "logPath": "/private/tmp/3349-ad-state-d92df/sessions/and3349d92df/requests/0f6a88a6dc0fd638.ndjson",
    "diagnosticsRecord": {
      "session": "and3349d92df",
      "requestId": "0f6a88a6dc0fd638"
    },
    "details": {
      "reason": "alert-not-found",
      "dispatched": "no"
    }
  }
}

Android q-accept-1:

{
  "success": false,
  "error": {
    "code": "COMMAND_FAILED",
    "message": "alert not found",
    "hint": "If a sheet is visible in snapshot but alert reports no alert, it is likely app-owned UI. Use snapshot -i and press the visible label/ref.",
    "diagnosticId": "mvbsc76y-ce777959",
    "logPath": "/private/tmp/3349-ad-state-d92df/sessions/and3349d92df/requests/a2fcc21b914698b3.ndjson",
    "diagnosticsRecord": {
      "session": "and3349d92df",
      "requestId": "a2fcc21b914698b3"
    },
    "details": {
      "reason": "alert-not-found",
      "dispatched": "no"
    }
  }
}

Android q-accept-2:

{
  "success": false,
  "error": {
    "code": "COMMAND_FAILED",
    "message": "alert not found",
    "hint": "If a sheet is visible in snapshot but alert reports no alert, it is likely app-owned UI. Use snapshot -i and press the visible label/ref.",
    "diagnosticId": "mvbsca98-8e8c7106",
    "logPath": "/private/tmp/3349-ad-state-d92df/sessions/and3349d92df/requests/b033d4295989f37e.ndjson",
    "diagnosticsRecord": {
      "session": "and3349d92df",
      "requestId": "b033d4295989f37e"
    },
    "details": {
      "reason": "alert-not-found",
      "dispatched": "no"
    }
  }
}

Android q-accept-3:

{
  "success": false,
  "error": {
    "code": "COMMAND_FAILED",
    "message": "alert not found",
    "hint": "If a sheet is visible in snapshot but alert reports no alert, it is likely app-owned UI. Use snapshot -i and press the visible label/ref.",
    "diagnosticId": "mvbscdal-cc252c3d",
    "logPath": "/private/tmp/3349-ad-state-d92df/sessions/and3349d92df/requests/6234b7dea722a16e.ndjson",
    "diagnosticsRecord": {
      "session": "and3349d92df",
      "requestId": "6234b7dea722a16e"
    },
    "details": {
      "reason": "alert-not-found",
      "dispatched": "no"
    }
  }
}

Android get1:

{
  "success": true,
  "data": {
    "kind": "alertStatus",
    "platform": "android",
    "action": "get",
    "alert": {
      "title": "Allow LocProbe to access this device’s location?",
      "buttons": [
        "Precise",
        "Approximate",
        "While using the app",
        "Only this time",
        "Don’t allow"
      ],
      "platform": "android",
      "source": "permission",
      "packageName": "com.google.android.permissioncontroller"
    },
    "status": "visible",
    "message": "Alert visible"
  }
}

Android accept:

{
  "success": true,
  "data": {
    "kind": "alertHandled",
    "platform": "android",
    "action": "accept",
    "handled": true,
    "alert": {
      "title": "Allow LocProbe to access this device’s location?",
      "buttons": [
        "Precise",
        "Approximate",
        "While using the app",
        "Only this time",
        "Don’t allow"
      ],
      "platform": "android",
      "source": "permission",
      "packageName": "com.google.android.permissioncontroller"
    },
    "button": "While using the app",
    "coordinates": {
      "x": 540,
      "y": 1443
    },
    "message": "Alert accepted"
  }
}

Android get2:

{
  "success": true,
  "data": {
    "kind": "alertStatus",
    "platform": "android",
    "action": "get",
    "alert": null,
    "status": "none",
    "message": "No alert visible"
  }
}

Limrun get1:

{
  "success": true,
  "data": {
    "kind": "alertStatus",
    "platform": "ios",
    "action": "get",
    "alert": {
      "title": "Allow “ATTProbe” to track your activity across other companies’ apps and websites?",
      "message": "This build asks for tracking authorization on purpose.",
      "buttons": [
        "Ask App Not to Track",
        "Allow"
      ],
      "platform": "ios",
      "source": "permission"
    },
    "status": "visible",
    "message": "Alert visible"
  }
}

Limrun accept:

{
  "success": true,
  "data": {
    "kind": "alertHandled",
    "platform": "ios",
    "action": "accept",
    "handled": true,
    "alert": {
      "title": "Allow “ATTProbe” to track your activity across other companies’ apps and websites?",
      "message": "This build asks for tracking authorization on purpose.",
      "buttons": [
        "Ask App Not to Track",
        "Allow"
      ],
      "platform": "ios",
      "source": "permission"
    },
    "button": "Allow",
    "coordinates": {
      "x": 201,
      "y": 592
    },
    "message": "Alert accepted"
  }
}

Limrun get2:

{
  "success": true,
  "data": {
    "kind": "alertStatus",
    "platform": "ios",
    "action": "get",
    "alert": null,
    "status": "none",
    "message": "No alert visible"
  }
}

Full transport NDJSON (metadata: type/id/PID/time; no coordinates or daemon request IDs were captured):

{"ts":"2026-10-17T02:54:11.595Z","dir":"out","type":"deviceInfo","id":"ts-client-1792205651595-7s1do5","pid":53800}
{"ts":"2026-10-17T02:54:12.049Z","dir":"out","type":"listApps","id":"ts-client-1792205652049-nnrgn","pid":53800}
{"ts":"2026-10-17T02:54:12.390Z","dir":"out","type":"appInstallation","id":"ts-client-1792205652390-5bd7wn","pid":53800}
{"ts":"2026-10-17T02:54:20.382Z","dir":"out","type":"listApps","id":"ts-client-1792205660381-tmnvaj","pid":53800}
{"ts":"2026-10-17T02:54:21.034Z","dir":"out","type":"launchApp","id":"ts-client-1792205661034-1aa8z","pid":53800}
{"ts":"2026-10-17T02:54:26.957Z","dir":"out","type":"elementTree","id":"ts-client-1792205666957-uk6sgr","pid":53800}
{"ts":"2026-10-17T02:54:27.838Z","dir":"out","type":"elementTree","id":"ts-client-1792205667838-meav5o","pid":53800}
{"ts":"2026-10-17T02:54:28.053Z","dir":"out","type":"tap","id":"ts-client-1792205668053-34cgwg","pid":53800}
{"ts":"2026-10-17T02:54:33.558Z","dir":"out","type":"elementTree","id":"ts-client-1792205673558-lwyalh","pid":53800}
{"ts":"2026-10-17T02:54:33.924Z","dir":"out","type":"elementTree","id":"ts-client-1792205673924-cstsd7","pid":53800}
{"ts":"2026-10-17T02:54:34.071Z","dir":"out","type":"tap","id":"ts-client-1792205674071-fs1nnb","pid":53800}
{"ts":"2026-10-17T02:54:34.333Z","dir":"out","type":"elementTree","id":"ts-client-1792205674332-b9t88qm","pid":53800}
{"ts":"2026-10-17T02:54:37.712Z","dir":"out","type":"elementTree","id":"ts-client-1792205677712-hu78gc","pid":53800}

@thymikee

thymikee commented Oct 17, 2026 •

Copy link
Copy Markdown
Member Author

Reply to the review at be48a28 — everything asked there is now implemented and both proofs rerun at the final head 92df16aa (full raw receipts: #3699 (comment)).

1. Budget rule applied whole. The per-capture window machinery is gone entirely — treeAlertWindow, the arm/start state, the window-closed marker, the post-capture isExpired refusal and the window-expired lookup branch all deleted (zero occurrences in the driver or the Android route). pollUntilSettled bounds only how long a leg polls; every capture that started inside the budget finishes and its answer counts; only the caller's signal reaches capture and press. alert-window-expired survives only as the post-press confirm exit; pre-press it is unreachable by construction. Pinned with a driven clock (a quiet screen with ~900 ms captures straddling the 2 s lookup answers alert-not-found), and live at the final head: quiet alert dismiss ×3 and alert accept ×3 on the emulator, every one answering reason: alert-not-found, dispatched: "no", none alert-window-expired.

2. Id-derived ranks instead of refusal. chooseAndroidAlertButton now ranks through the permissioncontroller grant ids — foreground-only, then always, then one-time — and label rank is used only where no id exists. Mixed-rank pairs (a backend id vs a label position) refuse, since the scales are uncomparable; same-scale conflicts let the backend id win. The Spanish case you named is a shipped test: "Mientras la app está en uso" on the foreground id + "Solo esta vez" on the one-time id resolves to the foreground grant; the Android 10 label resolves too. Live: a fresh Android 11+ location sheet on the API 35 emulator (grant ids preserved in the sheet's own uiautomator dump) accepted with "button": "While using the app", follow-up alert get → status: none. CHANGELOG line 10 states the rule and the narrow refusal shapes; commands.md matches the code's reason split.

3. Simpler shape, mostly as proposed. The driver moved out of platform-android into a neutral module (@agent-device/capture-kit/alert-tree-driver), so the iOS provider no longer depends on the Android package; ranking goes through backend-declared keys (permission id on Android, label on Limrun); pollUntilSettled bounds polling and only the request signal reaches capture/press. What did not shrink to the suggested floor: gross is 3270 lines (+770 over the ~2500 budget; production net +916, the driver itself 447). Honest accounting rather than a waiver ask — the remaining production growth is the typed cancel/window exits each backend must answer with, the id-rank table, and the Limrun tree-alert leg itself; ~1900 of the gross is tests pinning the two defects above at exactly the interleavings you described (resolves-after-cancel, straddling capture, localized sheet). I kept those pins deliberately; say the word if you want any of that trimmed.

4. Limrun proof rerun at the final head, real CLI/daemon route. Fresh instance at 92df16aa: alert get --json → status: visible on the ATT prompt, alert accept → alertHandled with "button": "Allow", follow-up alert get → status: none, and the transport record shows accept's single tap (session totals: exactly two tap frames — one for the app-trigger press at its reported coordinates, one for accept at (201, 592); accept's own frames are lookup capture → one tap → confirm capture). One method disclosure: the per-request NDJSON persists daemon-internal phases only (no provider-SDK frames), so the tap count comes from a transport-level log captured by running the daemon under a non-invasive send()-logging hook — instrumentation only, shipped code untouched; successful requests' per-request logs were not preserved (events only). Instance hygiene: disconnected, both campaign instances deleted by id.

5. One open item, stated without clearance. The 3e1e3ae6 iOS smoke failure (MAIN_THREAD_WORK_ABANDONED operation=command_execution slice=3.0 mid one alert read in the deep-link scenario) stays unresolved on both sides: root cause unknown, and this PR's participation neither proven nor excluded — the changed seams are not on the executed path of that command on the Apple runner, but that is a static path assessment, not runtime exoneration, and today's CI green at 92df16aa (21/0) is an observation of this head's lanes, not retroactive clearance of 3e1e. A bounded artifact survey against the exact base head's retained successful run (byte-identical deep-link command, four alert reads ok=1, no slice=3.0 abandon) is the extent of what retained evidence supports — n=1 both sides. The old f584 Android proof's precondition violation (a global pm reset-permissions in an abandoned script run) stands disclosed; the Android proofs above are post-fix runs at 92df16aa with own-bundle pm revoke only.

@thymikee

Copy link
Copy Markdown
Member Author

The earlier review of be48a28 asked that every role-carrying alert candidate get a rank from its owning backend. At 92df16a the location ids now have ranks, the mixed-source rank pairs are refused, the cancel and abort tests reach their paths, and the doc wording is fixed. The remaining problem is the Android media sheet, which still has no ranks. CI is green (23 checks, 0 not passing), and no conflicts are known.

classifyAndroidAlertButtonById (alert-detection.ts:227) gives the accept role to every id matching /id\/permission_allow/, but ANDROID_GRANT_ID_RANKS (line 121) ranks only permission_allow, _foreground_only, _always/_all_time and _one_time. On the Android 14+ partial media sheet (READ_MEDIA_IMAGES), I believe permission_allow_selected_button ("Select photos and videos") and permission_allow_all_button ("Allow all") are both accept by id with no backend rank. The label fallback ranks "Allow all" but leaves the other null, so selectRankedAlertButtonByRole (alert-contract.ts:156) returns null and alert accept fails with alert_button_undecided. Main pressed the first accept button there. On English Android 14+ photo and video sheets, accept would now refuse. commands.md says accept presses the lasting grant, and the CHANGELOG names only the localized Android 11+ and Android 10 sheets. The rule to satisfy: every id the Android classifier maps to a role carries a rank in the same declaration. Could the accept role and the rank come from one table of permissioncontroller ids, so an accept-by-id without a rank cannot exist? That table would need allow_all (lasting) ranked before allow_selected. Please add a test with the AOSP media-sheet ids that expects "Allow all". If the refusal is intended, please name the media sheet in the CHANGELOG and commands.md. I took these two ids from platform knowledge, not from a fixture in this repo or a live dump, so please confirm them against a real Android 14 sheet.

Not blocking, and you can take or leave these: commands.md (line 403) says any cancel from the press onward reports dispatched: unknown, yet a cancel that arrives while a confirm elementTree is in flight lets that capture finish (alert-tree-driver.ts:206), so a "none" answer reports alertHandled; either check driver.signal after confirm settles or scope the sentence to "unless the confirm already proved the alert gone", and the comment in ios-interactor-alert.test.ts (line 14) still says the driver comes from platform-android's mechanics facet when it now lives in @agent-device/capture-kit/alert-tree-driver, and the narration comments in alert-tree-driver.ts (for example the "THE tree-based alert driver" header) could shrink to the invariants.

Of the earlier inline threads, the ones on the unbounded wait for a non-abortable Limrun call (thread) and on wait ignoring sawUndecided (thread) are resolved as intended, and the rest are fixed at 92df16a, so you can resolve them: confirm-loop abort check, mixed-source rank pairs, tap test abort timing, backend-rank contract test, alert-not-found scope, capture failures without a reason, vendor id test, platform-android header, commands.md wording, cancel elementTree count, post-press comment; the stale capture-kit wording thread is fixed in alert.ts but the same wording remains in ios-interactor-alert.test.ts, covered by the note above.

The live Android and Limrun proofs are your reported runs in this comment; I did not reproduce them, and the Limrun one-tap evidence comes from a send()-logging loader hook, not the request log. I ran no tests here and did not check that the straddle regression goes red on be48a28. Before merge, the media-sheet rank needs to land with its test, or the CHANGELOG and commands.md need to document the refusal.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Limrun iOS: alert returns unsupported-provider-mode although the alert is in the element tree

1 participant