Skip to content

feat: cancel timed-out tasks; stable refs that late writes cannot overwrite - #64

Draft
cryptotavares wants to merge 2 commits into
cryptotavares/mmcli-p1-06-observe-level-no-statefrom
cryptotavares/mmcli-p1-08-abort-refs
Draft

cryptotavares wants to merge 2 commits into
cryptotavares/mmcli-p1-06-observe-level-no-statefrom
cryptotavares/mmcli-p1-08-abort-refs

Conversation

@cryptotavares

Copy link
Copy Markdown
Collaborator

Description

Fixes three problems with refs and observations (tickets P1-08 and P1-09):

  1. A request that timed out kept running. It could still click or snapshot, then overwrite the refMap that the next request relied on.
  2. Every observation and discovery call renumbered refs (e1..eN in tree order). Refs an agent already held could then point at other elements, and diffs (which match by ref) reported unchanged nodes as removed and added.
  3. On mobile, a click's observation could show the old screen. On Android the unlock click returned the login screen with the button still "Unlock, busy".

Changes:

  • Cancellation: RequestQueue gives each task an AbortSignal and aborts it on timeout. ToolContext gains optional signal and setRefMap. Interaction tools, run_steps, discovery tools and the observation check the signal between steps. The observation timeout now aborts the observation instead of leaving it running.
  • RefStore (owned by the server, wraps sessionManager.setRefMap): each request takes a generation, and writes from an older or aborted request are dropped. Tools that call context.sessionManager.setRefMap directly go through it too, via a proxy. ISessionManager is unchanged.
  • Stable refs (stabilizeRefs): discovery tools and observations reuse the previous ref for the same element.
    • Key: browser = role + name + testId; mobile = testId, else role + name. Elements that share a key are matched in screen order.
    • New elements get refs above a session high-water mark, so a removed ref is never reused until launch/cleanup.
    • When a tool set refs during the request, the observation merges into them and the tool's refs win.
  • list_testids no longer reads the a11y tree or sets refs; it never returned refs.
  • Mobile:
    • The observation and describe_screen take one device snapshot for both the tree and test IDs, via the optional IPlatformDriver.captureScreen() (ScreenCapture type exported). Before, it was two.
    • After a mutating or batch call, the daemon re-snapshots every 300 ms while the screen is busy (a , busy label or a progress/activity indicator), at most 3 times or 2 s. An unchanged screen gets one retry.

Part of the mm CLI v2 effort: tickets P1-08 + P1-09 (Phase 1), combined into one PR.
Stacked on #62 (review that first).

Breaking changes

None.

  • ToolContext.signal, ToolContext.setRefMap, IPlatformDriver.captureScreen and ScreenCapture are optional additions.
  • RequestQueue.enqueue now passes the signal to its callback. Existing callbacks that take no argument keep working.
  • Refs keep their numbers across calls where they used to be renumbered, so ref numbers can grow within a session.

Consumer impact

Consumer Change needed Done on branch
Extension test/e2e/playwright/llm-workflow/ None. It does not use RequestQueue, and its setRefMap/getRefMap implementation is unchanged. n/a
Mobile tests/llm-workflow/ None (same). n/a

Proof

Tested with yalc builds in the Extension (e2e build, headless) and Mobile (dev client on iOS sim and Android emulator). The first full run used an earlier revision (e804b95). The final commit got a targeted mobile recheck plus a smoke run on all three platforms. Details are in the proof file in the mm-cli-v2 workspace.

Check Before (#62) After
click --testid does-not-exist --timeout 500, then describe-screen, then click by ref – works on ext, iOS and Android (queueMs 0, intended element clicked)
Refs from the first describe-screen after later clicks renumbered still valid on all 3 platforms
describe-screen twice: same refs no (renumbered) ext 17/17, iOS 106/106, Android 226/226
Android unlock-click observation login screen ("Unlock, busy") in 3/3 no login or busy in 4/4 (3 wallet screen, 1 tab bar only)
Unchanged-screen diff, iOS (wait-for wallet-screen) 7495 B 7518 B, diff +0 −0
Unchanged-screen diff, Android (click tab-bar-item-Wallet, p50 of 3) 7672 B, observationMs 1924 40–110 KB, observationMs 965–1001 (one snapshot instead of two)
Ext bench p50 (click / get-state / list-testids totalMs) 580 / 5 / 565 582 / 6 / 60 (list_testids no longer reads the tree)

Known gap (Android): refs no longer churn, but unchanged-screen diffs are still larger than before. Status-bar nodes (clock, signal) and zero-height layout ViewGroups change between snapshots. P1-13 (filter com.android.systemui:*) and P1-14 (drop path and layout-only nodes) cover this; I added notes to both tickets.

Unit tests
Test Files  49 passed (49)
     Tests  1560 passed (1560)
Type Errors no errors
All files | 97.13 | 94.1 | 95.4 | 97.29
yarn lint → exit 0

New:

  • RequestQueue abort tests;
  • RefStore (generations, aborted writes dropped, merge with tool refs, high-water mark, baseline);
  • stabilizeRefs (insert, remove, rename, duplicate keys in screen order, ranges, options);
  • screen-settle (busy detection, unchanged retry);
  • server tests for P1-08 AC1/AC2, P1-09 AC1/AC2, the proxy with a #private class, and mobile single snapshot and settle;
  • discovery tools returning stabilized refs.

@cryptotavares
cryptotavares added this pull request to stack #63 October 24, 2026 01:07
);
controller.abort(error);
reject(error);
}, effectiveTimeout);
@cryptotavares
cryptotavares force-pushed the cryptotavares/mmcli-p1-08-abort-refs branch from de9777c to 5131d82 Compare October 24, 2026 01:20
@cryptotavares

Copy link
Copy Markdown
Collaborator Author

Rebased onto #62 (1bbfc70). Fixed CodeQL js/resource-exhaustion in 5131d82: queued task timeouts (from request bodies) are clamped to MAX_QUEUE_TIMEOUT_MS = 600 s. Core: 49 files / 1561 tests, lint 0.

@cryptotavares
cryptotavares force-pushed the cryptotavares/mmcli-p1-08-abort-refs branch from 5131d82 to 69cf663 Compare October 24, 2026 01:32
…rwrite

A request that timed out kept running: it could still click, snapshot and
then overwrite the refMap the next request relied on. Refs were renumbered
by every observation and discovery call, so refs an agent held could point
at other elements. On mobile, a click observation could show the old screen.

- RequestQueue passes an AbortSignal to each task and aborts it on timeout.
  ToolContext gains optional `signal` and `setRefMap`. Interaction tools,
  run_steps, discovery tools and the observation check the signal between
  steps (MM_REQUEST_ABORTED, internal).
- New RefStore (server-owned, wraps sessionManager.setRefMap): each request
  takes a generation; writes from an older or aborted request are dropped.
  Tools that call context.sessionManager.setRefMap go through it too (proxy).
  ISessionManager is unchanged.
- Stable refs: discovery tools and observations reuse the previous ref for
  the same element (browser key: role + name + testId; mobile: testId, else
  role + name; elements sharing a key are matched in screen order).
  ToolContext.setRefMap returns the stabilized { refMap, nodes }, which
  describe_screen / accessibility_snapshot return. New elements get refs
  above a session high-water mark, so a removed ref is never reused until
  launch/cleanup. When a tool set refs, the observation merges into them.
- list_testids no longer reads the a11y tree or sets refs (it returns none).
- Mobile observation and describe_screen: one device snapshot for both the
  tree and test IDs (optional IPlatformDriver.captureScreen; ScreenCapture
  type exported). After a mutating or batch call, re-snapshot every 300 ms
  while the screen is busy (", busy" label, progress/activity indicator), at
  most 3 times / 2 s; an unchanged screen gets one retry.
- The observation timeout now aborts the observation instead of leaving it
  running.
CodeQL (js/resource-exhaustion) flagged the queue timer because its
duration comes from the request body. Clamp it to MAX_QUEUE_TIMEOUT_MS.
@cryptotavares
cryptotavares force-pushed the cryptotavares/mmcli-p1-08-abort-refs branch from 69cf663 to 7d0c565 Compare October 24, 2026 01:35
@cryptotavares

Copy link
Copy Markdown
Collaborator Author

CodeQL js/resource-exhaustion on request-queue.ts is the same finding that is already dismissed on main (alert #32, "used in tests"); it re-appears because the lines moved. The timeout is now clamped to MAX_QUEUE_TIMEOUT_MS (600 s) and falls back to the default for non-positive values (7d0c565), and the schemas already cap every timeoutMs. Needs a maintainer to dismiss the re-opened alert.

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.

2 participants