Repository navigation
feat: cancel timed-out tasks; late ref writes cannot overwrite newer refs - #64
Open
cryptotavares wants to merge 5 commits into
Conversation
cryptotavares
force-pushed
the
cryptotavares/mmcli-p1-08-abort-refs
branch
from
October 24, 2026 01:20
de9777c to
5131d82
Compare
Collaborator
Author
cryptotavares
force-pushed
the
cryptotavares/mmcli-p1-08-abort-refs
branch
from
October 24, 2026 01:32
5131d82 to
69cf663
Compare
cryptotavares
force-pushed
the
cryptotavares/mmcli-p1-08-abort-refs
branch
from
October 24, 2026 01:35
69cf663 to
7d0c565
Compare
Collaborator
Author
|
CodeQL js/resource-exhaustion on |
This was referenced Oct 24, 2026
cryptotavares
marked this pull request as ready for review
October 27, 2026 16:22
…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.
Ref renumbering (stabilizeRefs, the session high-water mark, baseline checks, merging tool and observation refs) solved a problem that rarely happens: refs are already deterministic per tree, so an unchanged screen keeps its refs and diffs as unchanged, and read commands no longer collect observations (P1-05/P1-06). When elements are inserted above an element, the diff (or the full tree) carries the new refs. Kept: - AbortSignal per queued task, and RefStore as a generation guard: a write from a timed-out or older request never reaches the session manager (`context.sessionManager.setRefMap` and `clearRefMap` go through it too). - lastObservation only when delivered. - Mobile settle after a mutating tool, now compared against the last tree the agent received (`lastScreen`: the delivered observation, else the describe_screen / full accessibility_snapshot tree). No settle when a non-batch tool set refs (it just read the screen). Changed: the observation's refMap always wins, also after a tool set refs (describe_screen --observe), so every ref in the last tree of a response resolves. Removed: ref-stability.ts, ToolContext.setRefMap, setToolRefMap, the RefStore baseline / high-water mark / key modes.
cryptotavares
force-pushed
the
cryptotavares/mmcli-p1-08-abort-refs
branch
from
October 28, 2026 11:29
6944868 to
674ef64
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack context: mm CLI v2, Phase 1, P1-08 + P1-09, combined in one PR. Base: #62 (
cryptotavares/mmcli-p1-06-observe-level-no-state); followed by #65.Problem
A queue timeout rejects the caller, but the task can keep running and overwrite another request's refs. An observation timeout also leaves collection running.
Mobile observations read test IDs and the tree from separate snapshots. A click can return the old screen or
Unlock, busy.Solution
Keep cancellation and commit fencing at separate boundaries:
RequestQueuepasses anAbortSignaland aborts it before timeout rejection. Tools check supported checkpoints. Observations use a linked controller and their own deadline.RefStore(94 lines). Each started task gets a generation. Every ref write (sessionManager.setRefMap/clearRefMapfrom tools, and the observation) checks the generation and request signal, even when a tool ignores cancellation.flowchart TD A[Queued task starts] --> B[Create signal and ref generation] B --> C[Run tool through guarded context] C --> D[Optional observation checks linked abort signal] C -->|ref write| F{Request current and not aborted?} D -->|ref write| F F -->|yes| G[Commit map] F -->|no| X[Drop write. Keep current refs.] T[Queue timeout aborts request] -.-> C T -.-> D T -.-> F classDef new fill:#fff3cd,stroke:#d39e00 classDef stop fill:#f8d7da,stroke:#c82333 class B,C,D,F,G,T new class X stopYellow boxes are new. Red boxes stop the ref write.
Tools write refs through
context.sessionManager, which the daemon proxies:setRefMapandclearRefMapgo through the request handle. Other members retain the real receiver, including#privatefields.ISessionManagerstays unchanged.Optional
captureScreen()derives both mobile outputs from one snapshot; drivers without it retain separate reads. After eligible mutations, retry busy screens and allow one unchanged-screen retry, compared against the last tree the agent received (lastScreen: the delivered observation, else thedescribe_screen/ fullaccessibility_snapshottree). Launch, cleanup, and non-batch tools that set refs skip settling.What changes
observe none)list_testidsQueue timeout errors use HTTP 500 with
TOOL_EXECUTION_FAILEDandTool execution timed out after ${effectiveTimeout}ms.MM_REQUEST_ABORTEDis the internal checkpoint response. Cancellation does not undo an action already dispatched to a driver. Refresh withdescribe-screenbefore retrying.Decisions for reviewers
lastScreenupdates only after a delivered responseReview guide
Read the files in this order:
src/server/request-queue.ts,src/types/http.tssrc/server/ref-store.tstoolSetRefs.src/server/create-server.tssetRefMap,clearRefMap), observation cancellation checkpoints,screenAfterResponse/lastScreen, batch behavior, separate diff baseline.src/tools/utils.ts,src/tools/interaction.ts,src/tools/batch.ts,src/tools/discovery-tools.tslist_testidstree-read removal.src/platform/types.ts,src/platform/mobile-platform-driver.ts,src/server/screen-settle.tsREADME.md,SKILL.mdTesting
At
6944868:yarn test52 files / 1528 tests pass; coverage 97.03 / 93.95 / 95.21 / 97.19;yarn lintpasses. Every branch above it in the stack (#65, #66, #67, #44, #68) was restacked and passesyarn test+yarn lintat its own tip.Breaking changes
None.
ToolContext.signal,IPlatformDriver.captureScreenandScreenCaptureare optional additions.RequestQueue.enqueuenow passes the signal to its callback. Existing callbacks that take no argument keep working.Consumer impact
test/e2e/playwright/llm-workflow/RequestQueue, and itssetRefMap/getRefMapimplementation is unchanged.tests/llm-workflow/Why no ref stability
The first version of this PR also renumbered refs so an element kept its number across observations (
stabilizeRefs, a session high-water mark, baseline checks, merging tool and observation refs: about 500 lines of code and 900 of tests). It was removed because the problem it solves is rare:If real runs show agents clicking stale refs, it can come back as its own ticket.
Proof
After the shrink (
6944868, run on the restacked stack tip):describe-screen(queueMs 0), then click by refdescribe-screentwice: identical refsAndroid navigation observations can still show the previous screen (the new screen mounts about 2 s later). That is the existing Android settle limit noted in #44, not changed here.
Earlier revision (with ref stability), kept for history:
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.
click --testid does-not-exist --timeout 500, thendescribe-screen, then click by refwait-for wallet-screen)click tab-bar-item-Wallet, p50 of 3)list_testidsno longer reads the tree)The Android unchanged-screen diff gap in that revision is closed by #67 and #44 (see the table above).