mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
62001cf210
* refactor(record): derive session recording from the publication lifecycle `SessionState.recordSession` stored an answer the script-publication aggregate already contained. Every writer set both, but nothing made them agree, and #1533 was the consequence: a `--save-script` ingress re-armed the flag behind an ABORTED status, and a bare `close` published a recording the caller had been told was aborted. That fix routed every write through one rule, which made the two agree without making disagreement unrepresentable. The field remained a second source of truth, and its doc comments had to carry the invariant that a type could enforce. Remove the field and derive the answer. `isRecordingPublication` reads recording off the lifecycle: ordinary authoring records only while ARMED; a repair transaction records for its whole lifetime, terminal statuses included. That last clause is deliberately exact rather than merely safe — `armRepairStep` armed the old flag and neither `abortRepair` nor `commitRepair` ever cleared it, so narrowing it would silently stop evidence capture for a committed repair. Whether it should is a real question, and a behavior change, so it is left alone here. What this buys, beyond one less field: - `buildNextOpenSession` and `finalizeOrdinaryCloseScript` make no recording decision at all now, so no surface can arm recording without moving the lifecycle that authorizes it. - The writer's publication gate is answered entirely by the aggregate. Its separate ABORTED check is gone: a terminal authoring lifecycle is already not recording, so one question replaces two that could disagree. - The R7 ownership ratchet drops from 23 writer-owned fields / 29 owner claims to 22 / 26, and the layering manifest loses the entry whose comment documented the smell ("deliberately set on its own by paths that record without arming a publication"). Behavior-preserving: the derivation reproduces what the flag held at every transition. The test fixtures that armed `recordSession` with no publication state described a shape production stopped producing at #1478; they now carry the lifecycle that causes recording. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PFW9gJqz1wEHoowkdd1nFW * test(close-script): flush queued event-log writes before removing the tmp root CI failed the Coverage lane with ENOTEMPTY removing the test's tmp root, in `afterEach` rather than in an assertion. `SessionStore.recordAction` QUEUES its event-log append (`queueEventLogWrite`) instead of writing it, and every close path in this file records an action. Nothing awaited that write, so `fs.rmSync(root, {recursive: true})` could race it: the pending append recreates `<root>/sessions/<name>/` while rmSync is walking, and the final rmdir fails ENOTEMPTY. It needs CI's parallel load to lose the race — the file passes 12/12 in isolation locally. Await `flushSessionEventLogWrites()` before removing. The hazard is latent in any test that records actions and then removes its tmp root; this fixes the file that failed rather than sweeping the pattern, which deserves its own change. Not added to the #1419 contention-retry list: that list requires a concrete spawn/wait mechanism named per entry, and this file has none. The race was a real teardown bug, not lane contention. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PFW9gJqz1wEHoowkdd1nFW * docs: correct ADR 0016 on recording vs publication for repair Review caught a real overstatement. The amendment claimed evidence capture and publication authorization are "the same question asked of the same state". That holds for ordinary authoring — ARMED both records and publishes, ABORTED and PUBLISHED do neither — but not for repair: `isRecordingPublication` is true for every repair status including `committed` and `aborted`, while the writer additionally applies `isRepairArmedWriteBlocked`, refusing a committed transaction and one that is not yet committable. State it as it is: both decisions derive from the same aggregate, but they remain distinct predicates, and collapsing them would republish a committed repair or commit an incomplete prefix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PFW9gJqz1wEHoowkdd1nFW --------- Co-authored-by: Claude <noreply@anthropic.com>
16 KiB
16 KiB
Changelog
Unreleased
- Internal: session recording is now derived from the script-publication lifecycle instead of being stored beside it.
SessionState.recordSessionis removed;isRecordingPublicationanswers the question from the aggregate (ordinary authoring records only while ARMED, a repair transaction records for its whole lifetime). The stored flag was a second source of truth that handler surfaces set directly, which is how #1533's aborted-recording drift arose — no surface can now arm recording without moving the lifecycle that authorizes it, and the script writer's publication gate is answered entirely by the aggregate. Behavior-preserving: the derivation reproduces what the flag held at every transition. - Fixed: a script recording aborted by a second
openis no longer published by a later bareclose(#1533).open <app> --save-scriptfollowed by a second successfulopenterminates the recording and warns "Script publication was aborted…", andclose --save-scriptcorrectly refuses it with "Retry with plain close; it will tear down the session without writing." But when that secondopenitself carried--save-script, the flag re-armed recording behind the terminal status, and a bareclosethen wrote the full session log to disk — publishing a recording the caller had been told was aborted, and breaking the promise the refusal makes. An aborted authoring lifecycle is now terminal by construction:--save-scriptarms nothing on any surface that handles it — the re-open builder, the close finalizer, and the recorded-action ingress — and the script writer refuses the lifecycle from every path that reaches it (bareclose, teardown, idle-reap, active publication). This also stops an aborted session from paying recording-time costs it can never publish: a re-opened aborted recording no longer keeps the direct iOS selector fast paths forclickandgetdisabled. Armed recordings, published recordings, and every repair transaction are unaffected. agent-device mcpnow serves the stateless MCP2026-07-28revision alongside the handshake-based revisions it already spoke, as the spec's "dual-era server". Modern clients probeserver/discover, which advertises the supported revisions, the tools capability, and server identity; their requests declare a protocol version in_meta, and their results carryresultType: "complete"plus_meta["io.modelcontextprotocol/serverInfo"].tools/listandserver/discovernow return thettlMs/cacheScopecache hints, so a client can cache the 55-tool, ~223KB tool list for an hour instead of re-fetching it on every start; the list was already emitted in a deterministic (sorted) order, which is the other half of what makes it cacheable. Each revision is answered on its own wire contract: a request declaring2025-11-25or2025-06-18through modern framing still gets the legacy result shape, andinitializenever agrees to2026-07-28, which has no handshake to establish. A declared revision this server does not implement is rejected withUnsupportedProtocolVersionError(-32022) naming the ones it does, rather than being served under a version the client did not ask for, and modern framing that omits its requiredprotocolVersion/clientCapabilitiesmetadata — or supplies aclientInfothat is not a validImplementation— is rejected as invalid params.initializeandpingwere removed in2026-07-28, so a modern-framed call to either is answered-32601rather than served inside aresultType: "complete"envelope. Responses to legacy clients are unchanged byte-for-byte —initializeandpingare still served, and no cache,resultType, or_metafield is added to their results. Nothing here affects the CLI, Node, or daemon surfaces: the stdio transport, the tool set, and every tool's input/output schema are untouched.- Fixed: the MCP
initializehandshake now answers with the protocol revision the client requested when it is one this server implements, instead of always answering2025-11-25. A client pinned to2025-06-18was told to speak a revision it had not asked for, which the lifecycle contract answers by disconnecting. agent-device help workflowis now a compact ~8KB card instead of a ~41KB dump; the same depth still exists, split intohelp scripting(save-script, secret-safe fills, batch JSON, replay divergence/repair, recording) andhelp gestures(multi-touch shapes and platform quirks), plus a few paragraphs folded into the topics that already owned the subject (help debugging,help physical-device,help validate). Everyhelp <topic>first line is nowagent-device <version> — <topic>so an agent can read the installed version from its mandatory first help read instead of a separateagent-device --versioncall.- Cloud iOS (BrowserStack, AWS Device Farm):
snapshotanddiffno longer fail withSESSION_NOT_FOUNDon a live provider session (#1658). The app-session guard they ran belongs to the local XCUITest runner, which must attach to a target app; a cloud capture reads the provider's own driver session and needs no app identity, so the guard now applies to local Apple targets only. Relatedly, a cloud iOSopen com.example.appnow records that bundle id on the session — the provider path skips local app resolution (no simctl/devicectl reaches a hosted device), and used to drop an explicitly spelled bundle id along with it, leaving the session with no app identity at all. Opening a second bundle id replaces the first, matching the local path, where an explicitly spelled target always wins over the session's current app; deep links, display names, and bareopenstill keep the app already tracked. - Cloud
fill(BrowserStack, AWS Device Farm) now witnesses that the field it tapped actually holds text-entry focus before sending its keys, instead of dispatching tap and keys in back-to-back requests (#1658). A WebView input — an OAuth/SSO page in a Safari view controller, for example — takes first responder asynchronously, so the keys used to land with nothing focused whilefillstill answered "Filled N chars"; tapping and filling as two separate commands worked only because the round trip between them gave the field time to focus. The witness is the focused element's own geometry:fillpolls the active element and proceeds only once it contains the point it tapped, which is the one signal that identifies which field took focus. Keyboard visibility cannot — it reads the same before and after a second fill into an already-open form, so it could not tell a focused password field from the email field the previous fill left focused. The response disclosestextEntryReadiness:focused-element, orkeyboard-shownwhen the driver has no active-element route but the keyboard rose from hidden after the tap. Both describe a fill that witnessed focus before typing; there is deliberately no value for typing without evidence, because nothing renders this field and such a value would reach a caller as an ordinary success. Breaking: when focus cannot be witnessed, cloudfillnow FAILS withCOMMAND_FAILED/text_entry_focus_not_observedand sends no keys, instead of typing into whatever holds first responder and answering "Filled N chars" — a fill with no witness must not read as a filled field. That covers a tap that focused nothing, a keyboard already up on a driver that cannot name the focused field, and a driver that reports neither (text_entry_focus_unobservable, which points atpress+typeas the deliberate way to enter text unwitnessed). Only a positively classified unimplemented route counts as unsupported, so a dead session, an auth rejection, or a grid outage surfaces instead of degrading into a blind text entry. - Changed mutating selector ambiguity semantics (press/click/fill/longpress): duplicate accessibility wrappers collapse only when every match forms one ancestor-descendant chain resolving to the same actionable node. Matches in distinct subtrees now fail fast with
AMBIGUOUS_MATCHand a bounded, immediately reusable candidate-ref frame; visible/depth/area geometry no longer silently picks a mutation target. The direct iOS XCTest path now counts raw exact matches before hittability and delegates ambiguity to the same runtime rule. AppControlBench provenance: element-14 ran on 0.20.5; this change is intended for 0.20.7+, and comparative benchmark reports should note that it can replace a wrong-success recovery loop with one candidate-pick turn while occasionally adding that turn for genuinely distinct duplicates. scrollandbacknow accept--settle(with--settle-quietand--timeout), collapsing scroll-then-observe and back-then-observe into one call (#1638). The response carries the same settled payload the touch commands return — verdict, changed-lines diff with fresh refs on added lines, the unchanged-interactive tail, andrefsGenerationwhen the settled tree was stored — and is best-effort: it never fails the action. One difference is deliberate:scroll/backresolve no element, so the diff baseline is the session's stored pre-action tree ("the last tree you observed") rather than a freshly resolved pre-action capture. Both commands now also preserve the daemon on timeout, like the other settle-capable commands.- Security: repository
./agent-device.jsonnow accepts only project-safe automation defaults. It rejects daemon endpoint/auth/transport/server settings, tenant/run/lease selectors, provider/cloud and Metro connection fields, headers, executable reporter modules, local write destinations, and other operator-controlled values before local module loading or any daemon health/RPC request. Put remote endpoint and token together in protected CI environment variables, user config, an explicit--configfile, or the existingconnect/--remote-configworkflow. Daemon auth tokens no longer travel in serialized command flags. viewportis now rejected during capability admission on Apple targets instead of reaching the device and failing inside dispatch. No Apple backend can resize a screen — simulator and device geometry is fixed by the selected device type — soviewporton iOS/iPadOS/tvOS/macOS now fails withUNSUPPORTED_OPERATION,viewport is not supported on this device, and a hint pointing at--platform weband at picking a different simulator.capabilitiesno longer advertisesviewporton Apple targets. Web viewport resizing (agent-device viewport 1280 900 --platform web) is unchanged, and Android was already denied.--save-scriptis now accepted only by the commands that declare it —open,close, andreplay. A hand-built daemon request (or abatchstep) that setsaveScripton any other command, such asrecordortrace, used to arm script publication and could write a.adartifact; it is now rejected withINVALID_ARGSbefore the request reaches admission, the device, or any handler. CLI, Node, and MCP usage of--save-scripton its documented commands is unchanged.diff screenshotno longer runs the retired best-effort OCR and non-text analyzers. Their optionalocrandnonTextDeltasfields remain in the result type for source compatibility but are no longer emitted; use the baseline/current images and diff artifact with vision for qualitative interpretation.- Breaking: removed the deprecated
--session-lockedand--session-lock-conflictsflags. Use--session-lock reject|stripinstead; passing either old flag now fails withUnknown flag: ... Use --session-lock reject|strip instead. - Breaking: removed the
replay export --formatflag.replay exportalways writes Maestro YAML. - Breaking: removed the unused
LeaseAllocatePayload,LeaseHeartbeatPayload, andLeaseReleasePayloadtype exports fromagent-device/contracts. Lease request metadata is fully described byDaemonRequestMeta. - Maestro compat:
assertVisibleandassertNotVisiblenow acceptchildOffor ancestor scoping, matchingtapOn(#1294). - Breaking: removed deprecated gesture duration and rotate velocity inputs (#1218).
swipe x1 y1 x2 y2no longer accepts a trailingdurationMspositional; usegesture pan x1 y1 (x2-x1) (y2-y1) durationMsfor deliberate timed drags.- Maestro
swipeoperations with a duration continue to normalize togesture panwith theendpoint-holdexecution profile, preserving the Maestro-compatible fast-swipe-then-hold behavior on iOS. gesture fling direction x yno longer accepts a trailingdurationMspositional; usegesture panfor timed movement.gesture swipe presetno longer accepts a trailingdurationMspositional; usegesture panfor timed movement.gesture rotate degrees [x] [y]no longer accepts a trailingvelocitypositional; rotation pacing is derived fromdegrees.- MCP/Node schemas no longer advertise
velocityordurationMsonswipe/fling/gesture swipe;durationMsremains ongesture panandgesture transform. - A
.adscript that still carries a removed positional now fails when the script is parsed, before the replay executes any device action, naming the line and its rewrite (for exampleswipe accepts 4 arguments: x1 y1 x2 y2 (line 6). The trailing durationMs positional was removed: use "gesture pan 197 650 0 -350 300" ...). Previously the script ran up to that step and then failed as a replay divergence. - Published the gesture migration guide covering CLI, Node.js, MCP, and saved
.adrecordings, plus the deprecation policy the next such removal follows (#1216). replay exportnow writes an explicitduration: 100forswipe— the canonical fling duration — instead of omitting it and letting Maestro apply its own 400ms default. Maestro flows replayed byagent-deviceare unaffected; a timed Maestroswipestill normalizes togesture panwith theendpoint-holdprofile.
- Breaking: the deprecated
rotateCLI command alias has been removed. Useorientationinstead; invokingrotatenow fails withrotate was renamed to orientation; for the two-finger gesture use: gesture rotate. - Breaking (ADR 0014, session ref-frame lifetime): a mutation through an
@refnow expires the session's ref frame, so a later ref mutation without a fresh observation fails closed with a typeddetails.reason(ref_frame_expired,ref_generation_mismatch,plain_ref_requires_complete_frame, orref_not_issued) instead of acting on a possibly-navigated screen. A ref-oriented sequence that performs several mutations must re-snapshotbetween them, consume an honestly issued--settleref in pinned@eN~s<gen>form, or use selectors. Enforcement applies on every platform, not just iOS. Legacy hand-written.adscripts that reuse several bare refs from one snapshot must capture between mutations or use selectors. - Ref reads resolve against the authorized ref frame's source tree, so an internal read-only capture (including Android freshness) can no longer retarget an admitted
@refby positional coincidence. Read-only ref consumers keep the structured staleness warning while the frame retains the ref's evidence.
0.15.0
- Breaking:
appsdiscovery and public app-list helpers now default to user-installed apps. Use--allorfilter: 'all'to include system/OEM apps. - Breaking: removed the
agent-device/android-appspublic subpath. Use the Android app helpers fromagent-device/android-adb. - Breaking: removed the
agent-device/daemonpublic subpath. Useagent-device/contractsfor daemon request/response types. - Breaking: removed public local ADB bypass/selection helpers such as
spawnAndroidAdbBySerialandresolveAndroidAdbProvider; usecreateLocalAndroidAdbProvider(device)or pass providers directly to the helpers fromagent-device/android-adb. - Added Android ADB provider helpers for exec, stream, clipboard, keyboard, app lifecycle, logcat, and port reverse workflows.