Skip to content

feat(desktop): self-healing element resolution via structural fingerprints - #32

Merged
rrader26 merged 1 commit into
mainfrom
feat/self-healing-elements
May 12, 2026
Merged

feat(desktop): self-healing element resolution via structural fingerprints#32
rrader26 merged 1 commit into
mainfrom
feat/self-healing-elements

Conversation

@rrader26

Copy link
Copy Markdown
Contributor

Summary

  • New library primitive: `computeFingerprint` + `findByFingerprint` in `src/desktop/fingerprint.ts`.
  • Two new MCP tools: `agentmark_desktop_fingerprint` and `agentmark_desktop_find_by_fingerprint`.
  • Fixes the "stale element_id" problem that's about to bite the Recipes feature (PR feat(plugins): Recipes Pack — durable named playbooks #31).

The problem

Recipe steps reference elements by `action_id`, which the snapshot binding resolves to a backend `element_id`. Those IDs are sometimes stable (UIA AutomationId), sometimes regenerated each launch, sometimes drift between app versions. An agent that saved "click `element_id=btn_save_4711`" yesterday may find that exact ID missing today even though the Save button is right there.

Today: recipe step silently fails. Today + this PR: agent computes a fingerprint at recipe-save time, falls back to fingerprint match at replay time.

How fingerprints work

A compact structural signature per element. Hand-tuned for tolerance:

  • Required match: `role` (button vs text_input is never a tolerable drift)
  • Primary signal: `name` (Jaccard-bigram partial credit for minor drift like "Save" → "Save...")
  • Disambiguation: parent role + name; immediate sibling roles + names
  • Tiebreakers: placeholder, value, depth

Scoring is 0–100. Default acceptance threshold: 60 (exact role+name match clears it; sibling+parent context pushes scores toward 100).

v0 scope

The primitives only. The agent calls these explicitly:
```

  1. At recipe-save time:
    agentmark_desktop_fingerprint { desktop_id, action_id: "act_btn_save" }
    → stores the fingerprint in the recipe step args
  2. At replay time, if a step's action_id resolution fails:
    agentmark_desktop_find_by_fingerprint { desktop_id, fingerprint: {...} }
    → returns the best-matching current element_id + confidence score
    ```

Automatic transparent healing inside `agentmark_desktop_execute` is a v1 follow-up — keeps the agent in the loop and makes the recovery path visible in the reasoning chain (which has been the consistent design principle of every plugin in this series).

Test plan

  • `pnpm build` clean
  • `pnpm test` — 421 pass / 10 skip (16 new: fingerprint computation, scoring under various drift scenarios, ID-rotation matching, sibling-context disambiguation, tool integration)
  • Manual: save a Recipe targeting a NowCerts field by fingerprint; rebuild the app's element tree (different launch); confirm fingerprint match resolves to the same logical field

🤖 Generated with Claude Code

…rints

The problem Recipes (PR #31) exposes: a step references an element by
backend element_id (resolved from action_id via the snapshot binding).
Those IDs are sometimes stable, sometimes regenerated. An agent that
saved "click element_id=btn_save_4711" yesterday may find that exact
ID missing today even though the Save button is right there.

The fix: compute a structural signature (role + name + parent context +
adjacent siblings) for any element. Store it with your Recipe. When
replay finds the original element_id missing, search the current
snapshot for the best fingerprint match.

Library piece — `src/desktop/fingerprint.ts`:
  - computeFingerprint(capture, elementId)
  - findByFingerprint(capture, fingerprint, { minScore? })
  - scoreFingerprintMatch(target, candidate)
All exported from `@thinkfleet/agentmark`.

New MCP tools (2):
  agentmark_desktop_fingerprint           compute fingerprint for an action_id
  agentmark_desktop_find_by_fingerprint   resolve a fingerprint to a current element

Scoring: role match is required (score 0 otherwise). Exact name match
adds 60. Partial name (Jaccard over bigrams ≥0.8) adds 40. Parent
role+name adds 15. Each matching sibling adds 5. Placeholder match
adds 5. Default acceptance threshold: 60/100. Caller can lower for
permissive matching.

v0 scope: the primitives only. The agent calls
agentmark_desktop_fingerprint at recipe-save time and embeds the
result in the recipe's step args; at replay time it calls
agentmark_desktop_find_by_fingerprint when an action_id resolution
fails. Automatic transparent healing inside agentmark_desktop_execute
is a v1 follow-up — keeps the agent in the loop and the recovery path
visible in the reasoning chain.

Tests (16 new, 421 total):
  - computeFingerprint shape + null for unknown ids
  - scoreFingerprintMatch: role gate, exact match, partial-name credit,
    parent+sibling weights pushing toward 100
  - findByFingerprint: identical-structure ID-rotation match,
    no-match returns null, sibling context disambiguates duplicates,
    custom min_score honoured
  - Tool integration: fingerprint resolves through the snapshot binding,
    find_by_fingerprint round-trips, missing args error path, unknown
    fingerprint returns isError + found=false

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@rrader26
rrader26 merged commit 1728230 into main May 12, 2026
4 checks passed
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