Skip to content

feat(revyl): add the Revyl device-cloud provider plugin - #4092

Open
thymikee wants to merge 58 commits into
mainfrom
feat/revyl-provider
Open

thymikee wants to merge 58 commits into
mainfrom
feat/revyl-provider

Conversation

@thymikee

@thymikee thymikee commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds Revyl cloud iOS simulators and Android emulators as the separately published plugin @agent-device/revyl (packages/provider-revyl), built on the v2 provider-plugin contract.

agent-device plugins add @agent-device/revyl
export REVYL_API_KEY=...
agent-device connect revyl --platform ios --provider-app https://apps.example.com/MyApp-simulator.zip
agent-device open com.example.app && agent-device snapshot -i

The plugin drives Revyl through its device-proxy HTTP API, without Appium. It rents a device on the first device command. Each session id is derived from its lease, so release, recovery and shutdown can always name the device to stop. An unconfirmed stop keeps the lease and reports provider-cleanup-pending with the session ids.

Supported: snapshot, screenshot, click, press, fill, scroll, open, close, home, and back on Android. Every other command refuses with a typed reason.

Outside the plugin, the change does two things:

  • It moves Limrun's iOS AX-tree reader to @agent-device/capture-kit/ios-ax-tree, because Revyl answers iOS in the same shape.
  • It adds a revyl-ios-tree snapshot producer. Android snapshots stay appium-source, because Revyl returns the same uiautomator page source.

kill -9 recovery depends on #4108.

Validation

🤖 Generated with Claude Code

Rent an iOS simulator or Android emulator from Revyl's execution API and drive
it through its device-proxy actions, with no WebDriver or Appium session in
between. Ships as the separately published `@agent-device/revyl` plugin on the
v2 provider contract.

- `connect revyl` verifies the key with one cheap request and stores the
  platform, optional model and OS version, and a required public app URL.
- A lease starts the device, waits for the worker and the device, and stops the
  run it started when the start never becomes ready; release, a repeat release,
  and daemon shutdown all end the session.
- Snapshots reuse the platform readers that already parse the same XML on a
  WebDriver-backed device, so refs and selectors resolve identically.
- Touch, fill, scroll, back, home, open, deep links, close, and screenshots
  ride the device actions; every family Revyl has no action for refuses with a
  typed reason naming the substitute.
@vercel

vercel Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
agent-device-website Ready Ready Preview Oct 30, 2026 10:29pm UTC

Request Review

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.61 MB 5.61 MB +1.1 kB
Package (unpacked) 5.61 MB 5.61 MB +1.1 kB
Package (download) 1.71 MB 1.71 MB +378 B

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 42.4 ms 43.6 ms +1.2 ms
CLI --help 87.7 ms 89.1 ms +1.4 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 46 files

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

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

Comment thread packages/provider-revyl/src/runtime.ts Resolved
Comment thread packages/provider-revyl/src/runtime.ts Resolved
Comment thread packages/provider-revyl/src/runtime.ts Resolved
Comment thread packages/provider-revyl/src/runtime.ts Resolved
Comment thread packages/provider-revyl/src/snapshot.ts Resolved
Comment thread src/commands/schema/cli-help.ts Resolved
@thymikee

Copy link
Copy Markdown
Member Author

Reviewed at 93627f4. The Revyl runtime can leak billed devices on several lease paths, and the Coverage CRAP check fails on code this PR adds. There are no conflicts. The leaks, the CRAP failure and the missing live run need to be fixed before merge.

Release forgets the session before Revyl confirms the stop (https://github.com/callstack/agent-device/blob/93627f4/packages/provider-revyl/src/runtime.ts#L212). If /stop fails, the daemon retries, finds no session and records the lease as released. A transient Revyl error then leaves the device running until the 30-minute idle timeout. Please make a session leave the lease table only after Revyl confirmed the stop, as limrun release does, so the daemon retry reaches it.

Recovery cannot stop anything after a daemon restart (https://github.com/callstack/agent-device/blob/93627f4/packages/provider-revyl/src/runtime.ts#L110). The in-memory map is empty, so recoverExpiredLease returns without contacting Revyl, and start_device carries no lease tag (https://github.com/callstack/agent-device/blob/93627f4/packages/provider-revyl/src/revyl-session.ts#L52). Every lease that outlives a restart leaks its device while recovery reports success. Please give recovery a durable handle, such as a lease tag or the persisted run id, and stop by it when the map misses. If that is not possible, throw so the lease stays pending.

start_device can start a device that nothing stops (https://github.com/callstack/agent-device/blob/93627f4/packages/provider-revyl/src/revyl-session.ts#L60). It is a side-effecting POST sent with the cancel signal and a 120 s timeout, so a Ctrl-C or timeout after Revyl accepted it throws before workflow_run_id is known. Line 62 also throws without stopping when only session_id came back, and the test near packages/provider-revyl/src/runtime.test.ts:121 asserts zero stops there. Every device Revyl may have started must be stopped or recorded: let the POST settle, stop by session_id when one exists, and flip that test to assert the stop.

When readiness fails, the stop error is discarded (https://github.com/callstack/agent-device/blob/93627f4/packages/provider-revyl/src/revyl-session.ts#L86). No session is stored, so nothing retries, and the user sees only provider-not-ready. Please surface the cleanup failure with the run id, or keep the run in the lease table so release and shutdown retry it.

Does the health probe fail on the normal attach path? The module comment says the worker answers 503 until the device is attached (https://github.com/callstack/agent-device/blob/93627f4/packages/provider-revyl/src/revyl-session.ts#L99), but getWorkerHealth goes through json(), which retries 503 twice and then throws provider-unconfirmed. That aborts allocation for any device that takes more than about 2 s to attach. Please treat 503 as not-ready inside the readiness loop and add a fixture mode that answers 503 for several polls.

LeaseLifecycleContext.deadline is ignored (https://github.com/callstack/agent-device/blob/93627f4/packages/provider-revyl/src/runtime.ts#L205). A 240 s ready wait plus a 120 s start can run 360 s, past the client's lease_allocate budget, so the device comes up after the caller gave up. Please bound start_device and the readiness polls by the smaller of the provider timeout and context.deadline, and stop the run when it passes, as provider-webdriver does.

Every device-proxy endpoint, body shape and response field is checked only against a fake written from the same assumptions, and the PR states no real Revyl credentials were used. A wrong action name or body shape would fail only on a real device, and the leaks above are unobserved on the real backend. Please attach a live run from a packed install on iOS and Android: connect, open reaching device_connected with the run id, snapshot, click by ref, fill, scroll, screenshot decoding as PNG, back and home on Android, close, disconnect, and Revyl's own status showing the run stopped. Also show Ctrl-C during open leaving no orphaned run, and a kill -9 of the daemon with a live lease followed by restart and expiry recovery showing the run stopped. Please redact the key and run URLs.

The Coverage CRAP check fails on revylFailureReason (https://github.com/callstack/agent-device/blob/93627f4/packages/provider-revyl/src/revyl-client.ts#L350), which is new in this PR (CRAP 38.1, cyclomatic 8, 22% coverage). appErrorCode, revylFailureReason and uncertainStatus classify the same status ranges three times. One status table returning code, reason and uncertain, with fixture tests for 401, 404, 409, 503 and 400, fixes the gate without an exemption.

Not blocking, take or leave: reject a stop body with success === false or request_accepted === false (revyl-client.ts:171); overlay apiKey and apiUrl independently and declare each env variable unless its own option replaces it (index.ts:46-58); track in-flight allocations and await them in shutdown (runtime.ts:149); refuse the bind with provider-session-unavailable when the session is not live (platform-runtime.ts:93); refuse or send the scroll duration and fill delay (interactor.ts:127,177); hash the canonical backend origin in runtime-instance.ts:14; delete the test-only runtime options and overload (runtime.ts:42); export one setup used by both index.ts and plugin.ts and drop the casts; remove the unused captureScreenshot member (snapshot.ts:12); add Revyl to the cli-help.ts:662-664 rules and the compare table, drop the artifacts listing at line 553, show startAgentDevice({ plugins: [revyl()] }) in revyl.md:112, and leave the Plugin installation paragraph reorder out of this PR; add lifecycle tests for a failing stop, release retry, abort during start, restart before recovery and 503 health.

Could the provider lease table be shared instead of re-implemented here? One table extracted from limrun could own allocate-once per lease, in-flight tracking, stop-then-forget release, a shutdown drain, deadline bounding and a recovery hook, plus a shared providerRuntimeInstance fingerprint next to providerRuntimeOwner. Revyl would then supply only start, stop, find-by-lease and the interactor, and most of the findings above would disappear. That means extracting the table and helper from provider-limrun first, with limrun migrated, and landing Revyl on top. Within this PR, the test-only options, the duplicated v1 runtime construction, the unused reader member and the three status classifiers are a few hundred lines to delete.

Two inline threads are fixed at this commit and can be resolved: the fixture overrides map #4092 (comment) and the scrypt owner derivation #4092 (comment). The P1 threads that still apply: start failure leaves session #4092 (comment); shutdown clears first #4092 (comment); restart recovery #4092 (comment); release deletes before stop #4092 (comment); deadline ignored #4092 (comment); late set after shutdown #4092 (comment); health 503 retried #4092 (comment); docs example omits plugin #4092 (comment); readiness deadline check #4092 (comment); start cancel signal #4092 (comment); swallowed cleanup error #4092 (comment); stop body success #4092 (comment). The P2 threads that still apply: compare table #4092 (comment); connection test echo #4092 (comment); incomplete PNG fixture #4092 (comment); snapshot signal recheck #4092 (comment); raw backendUrl hash #4092 (comment); non-live bind ready #4092 (comment); non-live interaction ops #4092 (comment); private https hosts #4092 (comment); artifacts listing #4092 (comment); apiUrl without apiKey #4092 (comment); smoke test url #4092 (comment); retention rule #4092 (comment); lease id segments #4092 (comment); fill delay dropped #4092 (comment); scroll duration reported #4092 (comment); refusal reason assertion #4092 (comment); operation catalog coverage #4092 (comment); test title mismatch #4092 (comment); trimmed value mismatch #4092 (comment); 404 mapping #4092 (comment); unknown fixture route #4092 (comment); health platform check #4092 (comment); scripted status repeat #4092 (comment); verify skips refused fields #4092 (comment). The P2 thread on a 200 without a success flag #4092 (comment) needs the live run to settle. Five lower-priority threads also still apply.

thymikee and others added 6 commits October 30, 2026 10:01
…one proved

A lease that expired while the daemon was down left a Revyl session nobody
could name: the id came from Revyl's answer, so only the process that started
the device knew it. The runtime now derives the `start_device.session_id` from
the lease id, so recovery in a restarted daemon can name and stop that session,
and a backend that echoes a different id is refused instead of being trusted.

Release and shutdown now report what they proved. A session is forgotten only
after Revyl confirms the stop; an unconfirmed one comes back as a warning on
`release`, and as `provider-cleanup-pending` on recovery and shutdown. A 404 or
410 on a session route reads as released. A start that finishes while shutdown is
already running stops the device it just rented rather than dropping it.

The client reads a failure as the status and route that produced it, replays only
a request with no side effect, and refuses a lifecycle answer that says no.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Three places reported a command as doing more than Revyl can do.

`fill --delay-ms` was dropped, so a paced fill arrived as one burst. It is now
paced for real: one `/input` per character with the field cleared on the first,
and a fill refused partway discloses how many characters went out.

`scroll --duration-ms` was echoed back in the report although Revyl's `/drag`
body carries endpoints only and no duration field exists. The report names no
duration now.

`platform-runtime` bound its operations from the device shape and reported facts
from the session, so a binding for a session that had ended still exposed device
actions. The projection is filtered through the facts now, and a closed session
binds nothing. A capture the caller aborted no longer reports a snapshot it
finished building afterwards, and the per-cell facts matrix is asserted for every
operation the catalog declares.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…y time

The connect path refused the provider flags Revyl has no field for, but verify
did not, so a hand-authored profile could connect with `--provider-timezone` and
fail only when the first lease was allocated. Both routes now run the same
declaration, and the device model and OS version go through one canonicalizer, so
what the profile stores and what `start_device` receives cannot differ by
whitespace.

An app URL is refused when Revyl's fetcher could never read it: a host path, plain
HTTP, credentials in the URL, or a host only the caller's own network can answer.

`apiUrl` was applied only when `apiKey` was supplied too, and an environment URL
left the fingerprint whenever a key option was present. Each option now replaces
only its own variable, and only a variable the plugin still reads from the
environment is declared for fingerprinting. The package smoke answers 404 for any
route but the one verification call, so a stray request is a failure rather than
whatever the fixture happened to return.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The setup example named `startAgentDevice({ revyl: ... })`, which is not the API:
the plugin is passed in `plugins` and the client is built from the returned scope.
Both calls are spelled out now, next to what the CLI does for the same profile.

`device-clouds.md` compares Revyl in the same columns as the other clouds, and the
versioned CLI help names it beside the other provider profiles.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… calls

Both plugin entries built the runtime themselves, with casts to hide the shape one
of them returned; `setupRevylPlugin` is the one construction, and the version 1
entry returns what the legacy adapter reads out of it. The `fetch` option on
`RevylRuntimeOptions` was reachable only from tests, which drive the runtime
through the fixture backend instead, so it is gone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The registration is what both entries need, so the no-registration overload — which
only the fixture reached — is gone, and the fixture takes `.runtime` from the same
call. A test asserts `setup` builds the owner from the credential options rather
than the environment.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@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 31 files (changes from recent commits).

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

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

@thymikee

Copy link
Copy Markdown
Member Author

This follow-up reviews 846850a. Most of the earlier blocks are fixed, but three cleanup leaks remain and the live Revyl run is still missing. The code needs changes before merge. CI is green: 24 checks, none failing, and the earlier Coverage CRAP failure is gone. There are no conflicts.

Fixed since 93627f4: the readiness deadline, in-flight allocation tracking, the derived-id stop on a missing run id, strict stop/cancel acceptance and the docs example. Still open:

  1. An unconfirmed /stop (5xx, unreachable, or success:false) makes release() return a REVYL_SESSION_STOP_FAILED warning instead of throwing (runtime.ts:341). The daemon's releaseLease and releaseSessionLease drop the registry lease whenever the provider call returns, so a repeat lease_release or disconnect never reaches provider.release, and expiry never reaches recoverExpiredLease. A transient Revyl error on disconnect leaves the device rented and billed until daemon shutdown or Revyl's 30-minute idle timeout, while the CLI reports the lease released. This is the stop-then-forget leak from the last review, moved into the daemon registry. The rule: a lease leaves the registry only after the provider confirmed its session ended (stop accepted, or 404/410 on the session route). Throw provider-cleanup-pending with providerSessionId on an unconfirmed stop, as provider-limrun does (terminateSession throws before sessions.delete, runtime.ts:390-396). Then confirmedReleases can go, since a repeat stop of the derived id gets 404/410, which already reads as released. runtime.test.ts:110 calls runtime.release twice directly, so it passes on today's behavior. Please prove it through the daemon lease_release route: the first release with a failing stop leaves the lease in the registry, and the second release stops it.

  2. submitRevylStart rethrows any start_device failure without a stop (revyl-session.ts:142). That covers request_timeout after the 120 s client timeout (whose own hint says Revyl may have carried out the request), a 5xx, a dropped connection, and a caller abort. The runtime chose the session id at line 64 before the POST, yet settleFailedAllocation releases the lease and nothing names the session. A start that Revyl accepted but answered late, the slow-pool case the 240 s budget exists for, rents a device that nothing stops or reports. The rule: every exit after the POST was sent either ends the derived session or reports provider-cleanup-pending naming it. Please wrap the POST so any failure stops sessionId through one shared stop helper where 404/410 counts as released. The runtime's stopSession (runtime.ts:370-385) already has that rule, so move it next to stopRevylSession and use it from cleanupAfterFailedStart, stopOrReport and shutdown too. Because the id no longer depends on the answer, start_device can then carry the caller's signal and the deadline bound. Please add fixture cases for a start_device timeout and a 503 that assert the stop of the derived id.

  3. An allocation whose start finishes after shutdown began stops its device with .catch(() => undefined) and throws runtime-shutting-down without the session id (runtime.ts:257). If Revyl is unreachable while the daemon shuts down during open, the device stays billed and nothing reports it. Please use the same stop helper as in point 2, and throw provider-cleanup-pending with providerSessionId and runId when the stop is not confirmed.

  4. The restart-recovery fix depends on Revyl accepting a caller-chosen start_device.session_id and stopping by it (session-identity.ts:16). Every device-proxy action name, body and response field is also checked only against a fake built from the same assumptions, and the PR body says no real credentials were used. If Revyl ignores or rewrites session_id, every derived-id stop returns 404, which the runtime reads as released, so recovery would report success while the device stays rented. Please attach a redacted live run from a packed install (agent-device plugins add from the tarball) on iOS and on Android. It should show connect; open reaching device_connected with the run id and a start_device response that echoes the derived session_id; snapshot -i; click by ref; fill; scroll; a screenshot that decodes as PNG; back and home on Android; and close and disconnect with Revyl showing the session stopped. It should also show three failure paths. Ctrl-C during open leaves no session listed in Revyl. A daemon kill -9 with a live lease, then restart and lease expiry, logs provider_lease_expired_released with Revyl showing the derived session stopped. A disconnect while /stop is failing leaves the lease retained and retried (after point 1).

Not blocking, and fine to take or leave: shutdown's provider-cleanup-pending (runtime.ts:214) lists every session instead of the ones whose stop failed, and counts a 404/410 stop as a failure unlike release and recovery, so report only the failed ids through the shared stop helper; and the example comment at revyl.md:146 says releasing the lease means a failure cannot leave a device rented, which stays true once point 1 is fixed.

Is the session id now fixed before start_device is sent, so the start, release, recovery and shutdown branching can collapse onto one helper, endLeaseSession(leaseId, runId?)? It would resolve when Revyl confirms the stop or answers 404/410, and otherwise throw provider-cleanup-pending naming the id. Called from every exit after the POST (any start failure, cancellation, readiness failure, identity mismatch, shutdown race) and from release, recovery and shutdown, it would delete confirmedReleases, the warnings channel, the stopOrReport/cleanupAfterFailedStart split and the swallow at runtime.ts:257. Release would throw on an unconfirmed stop as limrun does, so the daemon registry and the expired-lease releaser own the retry. Longer term, the question from the last review still stands: could one provider lease table be extracted from provider-limrun (allocate-once per lease, in-flight tracking, stop-then-forget release, shutdown drain, deadline bounding, recovery hook), so Revyl supplies only start, stop-by-lease and the interactor? At about 3,350 production lines, with roughly 600 more in this delta, the growth is hard to justify while the leak class is patched site by site. What would have to change first is an answer on the shared table. If it is taken, extract it from limrun with limrun migrated first and land Revyl on top. If not, the in-PR collapse above needs no prerequisite.

Open inline threads that still apply. P1: start_device ignores cancellation and the deadline #4092 (comment), and the mismatch check needs a run id #4092 (comment), sessions dropped before stop #4092 (comment), and the release retry path #4092 (comment). P2: reason assertions #4092 (comment), abort test #4092 (comment), shutdown tests #4092 (comment), close skipped #4092 (comment), 169/8 range #4092 (comment), worker connection route #4092 (comment), undefined session text #4092 (comment), fill cancellation #4092 (comment), session-gone mapping #4092 (comment). Two lower-priority threads also still apply. These threads are fixed and already resolved: no run id now stops the session #4092 (comment); derived id stop #4092 (comment); readiness deadline #4092 (comment); in-flight allocations #4092 (comment); 503/409 not ready #4092 (comment); docs example #4092 (comment); poll deadline #4092 (comment); POST no longer aborted #4092 (comment); cleanup-pending ids #4092 (comment); stop success:false #4092 (comment); docs column #4092 (comment); route list asserted #4092 (comment); PNG fixture #4092 (comment); snapshot abort check #4092 (comment); backend URL hash #4092 (comment); admitted operations #4092 (comment); admitted operations #4092 (comment); private hosts #4092 (comment); help text #4092 (comment); env overlay #4092 (comment); smoke 404 #4092 (comment); help rotation rule #4092 (comment); three segments #4092 (comment); paced input #4092 (comment); durationMs removed #4092 (comment); refusal reasons #4092 (comment); exhaustive cells #4092 (comment); four targets #4092 (comment); shared text flag #4092 (comment); session-gone route #4092 (comment); error bodies refused #4092 (comment); unknown paths 404 #4092 (comment); platform from start #4092 (comment); status per poll #4092 (comment); verify order #4092 (comment); client origin test #4092 (comment); captureScreenshot removed #4092 (comment); perf cell #4092 (comment); screenshot route #4092 (comment); help profiles #4092 (comment).

Before merge, release must keep the lease on an unconfirmed stop, the start and shutdown paths must stop or report the derived session, and the live iOS and Android run from point 4 must be attached. I did not run the package tests or a live Revyl backend. I judged test validity by reading them, and I did not trace whether a live lease at kill -9 reaches recovery after restart, which the live run in point 4 would settle.

thymikee and others added 8 commits October 30, 2026 12:09
`appErrorCode`, `revylFailureReason` and `uncertainStatus` classified the same
status ranges three times, and each new call site needed all three. One table
now answers a status with its code, its reason, whether the verdict came back,
and whether the transport may replay the request, so a new status is one row.

The same table also decides the retry loop, which is what lets the two readiness
reads opt out of it: the runtime owns the poll beat and the deadline, so a
transport replay in front of `health` hides Revyl's "worker not attached" for
two seconds per poll and stalls the wait being polled for.

`getWorkerConnection` addresses a run by id, so it is a session route: Revyl's
answer that there is no such run is the same SESSION_NOT_FOUND `/stop` and
`cancel` report, not a request that was never routed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ndle

Four start paths could leave a device rented or report the wrong session:

- `start_device` ignored the caller's cancellation and whole-allocation
  deadline, so a slow rent held `allocate` long after the lease request
  expired. The wait is now bounded by the shorter of that deadline and the
  client's own start timeout, and what the caller gave up on is stopped by the
  session id the request carried, so the device it may have rented is not left
  running and the caller still gets its cancellation.
- A start that echoed a different `session_id` stopped and reported the id this
  runtime derived rather than the one Revyl named, which is the only handle that
  addresses the device. Cleanup and the failure now both name Revyl's id.
- A start that failed to hand back a run stopped the derived id too, so a backend
  that kept its own identity left that session rented.
- Every start cleanup path read Revyl's typed 404/410 as a failed release, even
  though the session being gone is the outcome cleanup wanted. All of them now
  share one gone-as-released rule with release and shutdown.

`shutdown` no longer empties the session table before stopping: a session leaves
it only once Revyl confirmed the stop, so the release that follows a failed
shutdown retries the same session and reports the warning instead of recording a
release that never happened. The test that claimed that now releases the lease
and asserts both the warning and the second stop.

The readiness failure also said 240000 ms even when the caller had allowed far
less, so the budget it reports is the one that actually ran out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…anceled

`fill --delay-ms` slept between characters without the command's AbortSignal, so
canceling a paced fill could wait out the delay and type another character on a
device its caller had already given up on. The interactor now carries the runner
signal the command came in with: it ends each pause, is checked before the next
character goes out, and every device-proxy action travels on it. The report of
how much text landed still comes from the same counter, so a canceled fill says
where it stopped.

`captureRevylSnapshot` takes the same signal for the read it starts, and the
check that follows the read is now covered by a test that lands the hierarchy
after the abort, which is the race the earlier test could not reach: aborting the
in-flight fetch rejects before the tree ever reaches the parser.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
`getInteractor` dropped the runner context it was given, so an interactor built
for one command knew nothing about that command's cancellation. It now passes the
runner's signal on, which is what lets a paced fill and the reads behind it stop
when the caller hangs up.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The paced fill reads one character, waits, reads the next. A test that only
covered the pause could not tell a canceled fill from one that finished, so this
one aborts while a pause is open and counts the input actions that follow: a
character typed after the caller hung up fails the test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Revyl downloads the build from its own network, so an app URL that only the
caller's network can serve is refused before a device is rented. The check rejected
all of `169.x.x.x`, which turned a public host such as `169.1.2.3` away as if it
were link-local. Only `169.254/16` is link-local, so only that is refused, and the
test now carries a public 169 address beside the link-local one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The test says a device outside the Revyl id space is refused as an open and close
target, but read the reason on only two of the five cells. A refusal that moved to
another category would have kept the test green, so all five are checked now.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… plugin help where it was

The embed example released the lease and then closed the host in one `finally`, so a
release that threw left the embedded daemon alive with the session still rented. The
close now runs in its own `finally`, on every exit path.

In the remote help, the plugin-installation paragraph is back where it was: the
rendered text keeps its first-screen rules inside the first 30 lines, and the
rewrap pushed the MCP rule past them. The Revyl lines themselves are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@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 18 files (changes from recent commits).

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

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

The bootstrap line listed `artifacts` for every provider in it, including the ones
that declare no artifact support. Now that Revyl is in that list, the command list
stops short of it and the sentence says artifacts are provider-dependent, which is
what the earlier review asked for and what the plugin's own facts report.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@thymikee

Copy link
Copy Markdown
Member Author

I reviewed b3d0002 against the earlier review. The start-timeout bound, the echoed-id stop, the shutdown bookkeeping and several smaller points are fixed, but three lease-cleanup defects and the missing live run remain. I read the code and tests only. I did not run the package tests, CI or a live Revyl backend.

An unconfirmed /stop still makes release() return warnings instead of throwing (runtime.ts#L333). The daemon then drops the registry lease whenever the provider call returns (lease.ts#L262-L264). A repeat lease_release or disconnect never reaches provider.release, and expiry never routes the lease to recoverExpiredLease. So a transient Revyl error on close leaves the device rented and billed until daemon shutdown or Revyl's 30-minute idle timeout, while the CLI says the lease was released. The rule: a lease leaves the daemon registry only after Revyl confirmed the session ended (stop accepted, or 404/410 on the session route). On an unconfirmed stop, throw provider-cleanup-pending with providerSessionId, as provider-limrun does and as recoverExpiredLease already does at runtime.ts#L129-L141. That makes confirmedReleases and the warnings channel unnecessary. The new shutdown test calls runtime.leaseLifecycle.release directly, so it locks in the warning contract. Please prove the rule through the daemon lease_release route: the first release with a failing stop leaves the registry lease, and the second release stops the session.

New in this change: when the caller's signal or deadline ends the race, or the request's own AbortSignal.timeout fires, only the local wait stops (revyl-session.ts#L186). The start_device POST may still be running at Revyl. The catch sends /stop for the derived id at once. If Revyl has not registered that session yet, the stop answers 404/410, and stopStartedSession (#L285) reads that as released. The daemon then drops the lease (lease.ts#L277-L278), Revyl finishes the start, and nothing names the device. Shutdown does not wait for the orphaned start either, because the in-flight entry settles with the race (runtime.ts#L221-L229). Ctrl-C during open, or a deadline shorter than Revyl's start time, can leave a billed device while the CLI reports a canceled start. The rule: a stop cannot confirm the end of a session whose start_device request has not settled. Either keep the start registered as in-flight and stop the derived id only after it settles (bounded by the 120 s client timeout), or treat a 404/410 on the abandon path as unconfirmed and throw provider-cleanup-pending naming the derived id. The fixture cannot show this today. It records started before its delay and answers every /stop with success (revyl-backend.fixtures.ts#L114-L140). Please add a mode where the session does not exist until the delayed start answers, and assert the device is stopped after that.

Still unaddressed: a start that becomes ready after shutdown began runs stopRevylSession(...).catch(() => undefined) and then throws runtime-shutting-down with no session id (runtime.ts#L250). The daemon releases the registry lease. If Revyl is unreachable during shutdown, the device stays billed and nothing names it. Please route this through the same end-session path as release and start cleanup, and throw provider-cleanup-pending with providerSessionId and runId when the stop is not confirmed.

The PR body still reports no live run, so the Revyl contract is checked only against a fake built from the same assumptions (session-identity.ts#L16). If Revyl ignores the caller-chosen session_id, or registers it only when start_device answers, every derived-id stop gets 404, which reads as released. Recovery and cancellation would then report success while the device stays rented. Please attach a redacted live run from a packed install (agent-device plugins add <tarball>) on iOS and on Android. It should show connect; open reaching device_connected with the start_device response echoing the derived session_id; snapshot -i; click by ref; fill; scroll; a PNG screenshot; back and home on Android; and close and disconnect with Revyl showing the session stopped. It should also show three failure paths. First, Ctrl-C while start_device is in flight leaves no session listed in Revyl after the start would have finished. Second, a kill -9 with a live lease, then restart and expiry, logs provider_lease_expired_released with the session stopped in Revyl. Third, a disconnect while /stop fails keeps the lease and retries it. The Ctrl-C run is the only way to settle the ordering question in the second finding, because no artifact documents whether Revyl keeps processing a start after the client disconnects. I did not trace whether a lease live at kill -9 reaches recoverExpiredLease after restart.

Not blocking, and fine to take or leave: stopStartedSession (revyl-session.ts#L277) repeats the stop decision in stopSession (runtime.ts#L363-L378), so one helper next to stopRevylSession would serve both; and the start's own timeout travels as request.signal, which the client checks before its timeout (revyl-client.ts#L248-L249), so a start that hits its bound reports request_canceled instead of request_timeout with its hint. Passing the bound as the client's timeoutMs would fix that.

The previous question about one endLeaseSession helper and a shared lease table has no answer yet, and this change adds more per-site code: callerWait with its settled flag, the startMayHaveRented classifier, a second stop wrapper and a fifth throwIfStartStopFailed site. Could endRevylSession(client, {sessionId, runId?}) next to stopRevylSession replace them? It would await Revyl's answer, treat 404/410 as released only once no start_device for that id is in flight, and otherwise throw provider-cleanup-pending naming the id. Every start exit, release, recovery and shutdown would call it, and an abandoned start would stay in flight until it settles. That removes confirmedReleases, RevylReleaseWarning/RevylReleaseOutcome, stopStartedSession and the swallow, and it turns the three defects above into one rule. Production code is now about 3,530 lines. Separately, a maintainer answer is needed on whether a shared provider lease table is extracted from provider-limrun before Revyl lands. If it is, migrate limrun first and land Revyl on it. If not, the collapse above needs no prerequisite.

The cubic-dev-ai threads on untyped abort errors in the interactor, the screenshot call missing the signal and unreturned warnings on stop still apply, as does one lower-priority thread on connection.ts doc placement. These threads are fixed at this commit and already resolved: r4422245692 (start bounded by deadline and signal), r4422245702 (echoed id stopped), r4422245714 (reason asserted on all cells), r4422245720 (late capture after abort), r4422245728 (session kept until stop confirmed), r4422245736 (nested host.close()), r4422245740 (169.254 check), r4422245748 (session route on worker connection), r4422245752 (required session id, mismatch names echoed id), r4422245755 (signal checked per character; the untyped-error remainder is the open thread above), r4422245763 (gone read as released), r4422245768 (readiness reads do not retry), r4422245774 (effective budget reported) and r4421232530 (shutdown no longer clears sessions first).

The checks are still running on b3d0002: Smoke Tests (two jobs) and Coverage are in progress, and none has failed. This change touches their routes, since Coverage runs the provider-revyl unit tests and the CRAP gate over revyl-session.ts and runtime.ts, and Smoke runs CLI help, which cli-help.ts changes. Read any failure there as related until shown otherwise. There are no conflicts. Before merge, release() must throw provider-cleanup-pending on an unconfirmed stop so the daemon keeps the lease, ideally through one end-session helper that also covers the abandoned-start race and the shutdown race. Then the live iOS and Android packed-install run needs to be attached.

thymikee and others added 2 commits October 30, 2026 12:36
…s and the screenshot

Two places stopped at the seam the command signal had:

- A paced fill canceled during one of its pauses rejected with the timer's own
  `AbortError`, so the caller saw a bare cancellation instead of Revyl's typed
  reason, and the count of characters already on the device was lost with it. The
  wait's failure is now translated before the dispatched-step evidence is added.
- `screenshot` called the worker without the signal, so a canceled capture left the
  Revyl request running and wrote the PNG after the caller had given up.

The connection factory's documentation is moved back onto `createRevylConnection`,
which the declaration of `RevylConnection` had pushed off the function.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…rented a device

`startMayHaveRented` carried its own list of reason strings, so the same statuses
were classified a fourth time and any new reason silently became "may have rented".
It now asks the status table the question it actually has: did Revyl answer with a
settled refusal, or is the verdict missing? `revylFailureRefusedTheRequest` answers
that from the failure's own status, and the unreachable and canceled failures, which
carry no status, answer it as unknown.

Coverage CRAP named this function (cyclomatic 6, no coverage). Both branches are
driven now: a start Revyl answers 401 stops nothing, and one it answers 500 is
stopped by the session the request carried. The classifier itself is tested against
a settled 409, an unconfirmed 503 and a backend that never answered.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@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 8 files (changes from recent commits).

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

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

@apex-sentinel-staging

apex-sentinel-staging Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Apex Sentinel

Verdict: request_changes

Since the last review: 7 new · 0 still open · 8 no longer raised

7 finding(s): 1 major, 2 minor, 4 nit. Verdict: request_changes. reached max rounds (1).

Required fixes

  • Revyl's Android snapshot publishes provenance appium-source for a capture with no Appium session

🟠 Major (1)

Where Finding
packages/provider-revyl/src/snapshot.ts:118 🆕 Revyl's Android snapshot publishes provenance appium-source for a capture with no Appium session

🟡 Minor (2)

Where Finding
packages/capture-kit/src/ios-ax-tree.ts:158 🆕 Viewport evidence for Limrun changed reason from 'not-provided' to 'invalid' for an incomplete frame
packages/provider-revyl/src/interactor.ts:363 🆕 Screenshot density and scroll geometry use the health screen read once at readiness, so a landscape capture is resized with the wrong ratio
🔵 Nit (4)
Where Finding
packages/capture-kit/src/ios-ax-tree.ts:155 🆕 resolveIosAxTreeViewport treats a partially-provided screen box as no screen at all
packages/capture-kit/src/ios-ax-tree.ts:65 🆕 role_description bypasses scalarText, so a null in the provider's JSON publishes instead of being omitted
packages/provider-revyl/src/revyl-session.ts:463 🆕 The run-cancel cleanup path is unreachable, and it ends more than a session
packages/provider-revyl/src/session-allocation.ts:88 🆕 revylTextFlag silently drops a non-string flag value
Not in the reviewers' prompt (1 file)
  • pnpm-lock.yaml: generated or a lockfile, so its lines were left out

Reply under an inline comment with @apex-sentinel-staging note ... or @apex-sentinel-staging pivot ... to re-check that finding.
Comment @apex-sentinel-staging review on this conversation to run a full review.

thymikee and others added 8 commits October 30, 2026 19:05
…nched

Revyl answers /back only on Android, so the iOS cell refuses it with the
app's own back control as the substitute. A relaunch now ends the running
app directly, so the close guard no longer trusts the command's resolved app
and refuses every named close the session cannot be ending.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…joins

A release, a recovery and a shutdown that overlap now join the lease's one
in-flight /stop instead of each sending their own; a failed stop clears it so
the next caller retries. An allocation that joins a lease started for a
different app URL, device model or OS version is refused rather than handed
that device. The fixture tracks Revyl sessions per session id.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…e docs

A timed-out or canceled request is no longer replayed, and the replay reads
the request's own route. The iOS docs name the accessibility tree and say
back is Android-only, and the cloud page keeps Limrun's allocating commands
apart from Revyl's.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Revyl captures an iOS simulator at its native scale (3x), while the host
reports every iOS simulator screenshot at the requested density, 1x by
default, and printed a 1320x2868 capture as @1x. The capture is rescaled from
the density health's point size implies, the same normalization a local
simulator screenshot gets.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The host keeps only a provider failure's code, message and hint, so a
shutdown that could not stop a Revyl device now names each session id in its
message and hint, through one cleanup-pending builder that also carries the
run id. A first allocation that gives up names the derived session id, since
the daemon releases that lease without asking the provider and a failed late
stop would otherwise have no report at all.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@thymikee

Copy link
Copy Markdown
Member Author

Thanks for the review of 3269324. Everything below is at 465f42e. The live iOS and Android run is in the next comment.

Shutdown names every unconfirmed session. Fixed in 8a9c55c. Every cleanup-pending report now comes from one builder, revylCleanupPending in revyl-session.ts. It puts each session id in the message (Revyl did not release session <id>.) and in the hint, and keeps providerSessionIds, causes, and runId when the entry knows it. The new test src/provider-plugin-policy-revyl.test.ts drives this through startProviderRegistry(...).close() with a backend whose /stop fails. It asserts that the failure the host keeps names the session in its message and hint. This also covers the non-blocking note: the three builders are now one, and the release report carries the entry's runId.

A first allocation that gives up names the derived session. Also in 8a9c55c. revylCallerGaveUp takes the session id and puts it in details.providerSessionId and in the hint, for both the cancel and the deadline exit. The join passes the entry's rented id, or revylSessionIdForLease(leaseId) before the start has answered. The readiness phase passes the id it issued. The cancel and timeout tests in runtime.test.ts assert the derived id in the thrown details.

The live run. See the next comment. Two results changed code:

  • On iOS, hierarchy returns AX node JSON, not XML (fa655e3).
  • iOS screenshots arrive at 3x (b535a57).

kill -9 recovery needed a daemon change, so it is a separate PR: #4108. Until now the daemon persisted a provider lease only after it had expired and its release had failed. The new PR journals live leases, so a restart can recover them.

The other open threads.

  • Concurrent stops: fb603fa. The entry shares one in-flight /stop.
  • The "snapshot or open" example: 1edb276.
  • The fixture: fb603fa, which tracks sessions per id.
  • scryptSync: I kept it and explained why on the thread.

All threads are resolved. Each reply names the commit that fixed it, or the reason it was declined.

Also new since your review:

  • The close guard no longer trusts the command's resolved app. A relaunch now ends the running app itself.
  • A joining allocation for a different app or device is refused.
  • A request that timed out or was canceled is not replayed.

@thymikee

Copy link
Copy Markdown
Member Author

Live run against the real Revyl backend (https://backend.revyl.ai), 2026-10-30. Two findings from it changed the code.

Setup. pnpm pack of agent-device and @agent-device/revyl at b535a57, published to a local Verdaccio registry. The CLI was installed with npm i -g --prefix and the plugin with agent-device plugins add @agent-device/[email protected]; plugins add resolves names from a registry, so it does not take a tarball path. daemon status showed revyl [store] @agent-device/[email protected] - active. The key came from a Revyl partner connection. Revyl's state was read from GET /api/v1/execution/device-sessions/active and /history. Apps: Appium's ApiDemos-debug.apk on Android and UIKitCatalog-iphonesimulator.zip on iOS.

Backend behavior the cleanup rule depends on.

  • start_device keeps the caller's session_id. Proxy log: POST /start_device sent session_id=8e92ffb4-… response session_id=8e92ffb4-… echoed=true.
  • A stop by the derived id ends that session. revylSessionIdForLease(<lease>) matched Revyl's id in every run, for example 8ce21ad1-b75b-4660-aeb9-96cb1f0a92b7.
  • The real /stop answer is {success: true, request_accepted: true, session_settled: false, device_released: false, …}, and history shows the session completed seconds later. So device_released: false is a normal answer, not a refusal. This settles the "200 without a success flag" thread.

Happy path, Android.

  • connect verified the key without renting a device.
  • open io.appium.android.apis --relaunch rented the device and launched the app in 19 s.
  • snapshot -i returned 22 nodes, about 2.7 s per capture.
  • Click by ref (@e20) and by selector (label="Controls") both landed.
  • scroll up and down worked. fill @e13 "hello revyl" set the field.
  • The screenshot was a 1080x2400 PNG with the right content.
  • back and home worked. close released the session, and history shows it completed.

Happy path, iOS (after the fixes below).

  • open com.example.apple-samplecode.UICatalog --relaunch took 30 s.
  • snapshot -i returned the 19-row list.
  • Clicking @e16 opened Text Fields. fill @e5 "hello revyl" set the field. scroll and home worked.
  • The screenshot is 440x956 @1x by default and 1320x2868 @3x with --pixel-density 3.
  • back refuses with "Revyl has a back button only on Android".
  • close com.other.app refuses. close then left nothing active on Revyl.

Failure paths.

  1. Ctrl-C during start_device.
    • At 2 s, while start_device was in flight: Revyl created the session and ended it without starting it. Nothing was active afterwards.
    • At 7 s: Revyl stopped the session 1.3 s after it started.
  2. kill -9 with a live lease, then a restart. This needed fix(daemon): recover provider leases a killed daemon left rented #4108: the daemon kept live leases only in memory, so before that change no restart could recover anything.
    • With the change, the session was running at 18:25:49 when the daemon was killed.
    • session list restarted the daemon, which adopted live-provider-leases.json.
    • Revyl history shows the session completed at 18:25:51.
  3. disconnect while /stop fails. A loopback proxy in front of Revyl (REVYL_API_URL=http://127.0.0.1:8787) answered 503 to every /stop.
    • disconnect returned released: false, the daemon kept the lease, and Revyl still showed the session running.
    • With the failure lifted, the lease expired 60 s later and the daemon retried. The proxy log shows /stop -> 200 at 18:37:27, and history shows completed at 18:37:27.
    • One thing outside this PR: disconnect still removes the local connection when the release fails, for every provider. A second disconnect therefore has nothing to retry, and the retry comes from lease expiry.

What the run changed.

  • On iOS, hierarchy returns a JSON array of AX nodes in points, not XCUITest XML. Every iOS snapshot failed. Limrun's reader for that shape moved to @agent-device/capture-kit/ios-ax-tree, and both providers use it (fa655e39).
  • Revyl rejects /back on iOS, so the iOS cell refuses it (53d684bd).
  • iOS captures arrive at 3x but were reported as @1x. They are now rescaled to the requested density, the same way a local simulator capture is (b535a57a).

One open question for Revyl: iOS health reports supports_explicit_drag_duration: true, but neither the docs nor Revyl's CLI name a duration field for /drag, so scroll --duration-ms still refuses.

@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 49 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/capture-kit/src/ios-ax-tree.ts Resolved
Comment thread packages/provider-revyl/src/snapshot.ts Resolved
Comment thread packages/capture-kit/src/ios-ax-tree.ts Resolved
Comment thread packages/contracts/src/ios-snapshot.ts Resolved
The shared AX reader treats a frame with a non-finite component as no rect,
reports such a root as no viewport, and keeps the node's subrole. Revyl
refuses an iOS hierarchy holding a non-node member, and an iOS screenshot
whose source density it cannot know. The fixture tracks the session Revyl
answered with and resolves run cancels to it, and the capture-kit producer
matrices cover the Revyl tree.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@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

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Apex Sentinel: 5 new findings on 8c285ea. The verdict and the full list are in the summary comment.

Comment thread packages/capture-kit/src/ios-ax-tree.ts Resolved
Comment thread packages/provider-revyl/src/snapshot.ts Resolved
Comment thread packages/provider-revyl/src/snapshot.ts Resolved
Comment thread packages/capture-kit/src/ios-ax-tree.ts Resolved
…root frame

The shared provider capability row has one owner, the moved viewport reader
keeps its comment, and a Limrun test pins the subrole the shared reader now
publishes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@thymikee

Copy link
Copy Markdown
Member Author

I reviewed 8c285ea and found no code problems that must be fixed before merge. The findings from the earlier review are fixed in this commit. I did not reproduce the live Revyl run: it is attested in the author's comment at b535a57, which predates the error-text change (8a9c55c) and the two stricter iOS readers in 8c285ea. I also did not run package tests, check:affected, or the daemon lease_allocate cancel route, so the give-up details are proven by the provider-level thrown error, not through the daemon response.

Not blocking, take or leave: 8c285ea added two iOS refusals on the device path, provider-viewport-unavailable when health has no screen size (interactor.ts:366-374) and a recursive node check in parseIosTree. Neither should fire on the observed backend, since the live iOS screenshot was rescaled and the captured hierarchy uses children: [] and subrole: null, which the check accepts. One iOS snapshot -i and screenshot at 8c285ea from the packed install would confirm that.

Checks are still running: Smoke Tests (run 41113880220) and the second review check have no result for 8c285ea yet. Smoke drives the local Apple and Android runners, and this diff only adds revyl-ios-tree rows to the provenance, acquisition and stable-capture tables, so a failure there is likely unrelated. Please re-check any failure for a snapshot provenance or stable-capture symptom. There are no conflicts. Kill -9 recovery is proven only with #4108 applied, and #4108 is still open with a residual window for a canceled first allocation that leaves the journal while its start may still run. Before merge, the checks must finish green and #4108 should land, or you should accept that kill -9 recovery comes with it, so a rented device cannot outlive a killed daemon's lease.

On the open review threads: the P2 on the test fixture's /stop handling still applies (#4092 (comment)). It is test-only: revyl-backend.fixtures.ts:135-138 drops a session on any 200 /stop or cancel, even when success or request_accepted is false. I could not read whether it is resolved, because the thread list I had is capped at 100. No lower-priority threads still apply. These no longer apply (resolve any still open): #4092 (comment) (rect check now requires finite numbers), #4092 (comment) (fixture maps cancel to the started session), #4092 (comment) (missing screen size now refuses), #4092 (comment) (tree nodes validated recursively), #4092 (comment) (subrole null becomes undefined), #4092 (comment) (fixture records scripted session_id), #4092 (comment) and #4092 (comment) (comments say 'every producer'), #4092 (comment) (revyl-ios-tree added to both tests), #4092 (comment) (one shared in-flight /stop), #4092 (comment) (docs say Revyl allocates on first command), #4092 (comment) (sessions tracked per id), #4092 (comment) (scryptSync runs once per runtime; #4102 owns the helper), #4092 (comment) (named close guarded by launchedApp), #4092 (comment) (only provider-unreachable is replayed), #4092 (comment) (start request checked before join), #4092 (comment) (host resolves refs to a point; live click and fill landed), #4092 (comment) (two different validation stages, not a duplicate).

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Apex Sentinel: 9 new findings on fb96485. The verdict and the full list are in the summary comment.

Comment thread packages/provider-revyl/src/plugin.ts Resolved
Comment thread packages/provider-revyl/src/snapshot.ts Resolved
Comment thread packages/capture-kit/src/ios-ax-tree.ts Resolved
…onstants

A root frame that is present but not finite is invalid viewport evidence, not
absent. The shared provider capability row is frozen, Revyl's two attribution
headers share one constant, an IPv4-mapped loopback counts as loopback for a
local REVYL_API_URL, and the settings facts comment says which cell refuses.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

@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 6 files (changes from recent commits).

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

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

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Apex Sentinel: 8 new findings on d8377f5. The verdict and the full list are in the summary comment.

Comment thread packages/capture-kit/src/ios-ax-tree.ts Resolved
Comment thread packages/provider-revyl/src/snapshot.ts Resolved
Comment thread packages/provider-revyl/src/snapshot.ts Resolved
Comment thread packages/capture-kit/src/ios-ax-tree.ts Resolved
Comment thread packages/provider-revyl/src/snapshot.ts Resolved
…s not dispatched

The push refusal has one owner, RevylPlatform has one declaration, and the
shared reader says an incomplete root frame is invalid.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@thymikee

Copy link
Copy Markdown
Member Author

The code in 861ed36 looks good, and the findings from the earlier review (#4092 (comment)) are fixed. Not blocking, and fine to take or leave: the fixture's lifecycleAccepted (packages/provider-revyl/src/revyl-backend.fixtures.ts:314) should end a session exactly when the client would accept the answer, which is success === true && (request_accepted ?? true) === true on /stop and /status/cancel only, and no test refuses a stop today; the "tree root reported, else the device's screen box" viewport rule in packages/provider-revyl/src/snapshot.ts:68 now exists twice (it also lives in provider-limrun/src/ios-snapshot-adapter.ts:36-58), so capture-kit's ios-ax-tree could own one resolveIosAxTreeViewport(tree, fallbackScreen) for both providers; and isLoopbackHostname in packages/provider-revyl/src/revyl-client.ts:527 repeats the daemon client's net.BlockList check, so net.BlockList with the same three subnets would match it.

Two Cubic P2 threads are fixed at this head and already resolved: the lifecycle fixture gate (#4092 (comment), with the missing-success gap above as the only leftover) and the loopback range (#4092 (comment)). Of the Apex Sentinel threads, these are fixed at this head and already resolved: #4092 (comment), #4092 (comment), #4092 (comment), #4092 (comment), #4092 (comment), #4092 (comment), #4092 (comment), #4092 (comment), #4092 (comment), #4092 (comment) and #4092 (comment). These do not apply, with the reason for each:

The Smoke Tests run on 861ed36 is still in progress with no failure yet. It drives the local Apple and macOS runners, and this delta only touches provider producers, so a failure would likely be unrelated unless it shows a provider snapshot provenance symptom. Before merge, that run must finish green, and #4108 must land (or you accept that kill -9 recovery ships with it) so a rented device cannot outlive a killed daemon's lease. I did not run package tests, check:affected or Smoke; this review comes from reading the head code and tests. The live Revyl run is from b535a57, plus the #4108 kill -9 run. It should still cover the lease routes, because this delta does not touch lease or session-end code. The new iOS viewport fallback has not run live, so a run where the tree has no usable root frame would prove it.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Oct 30, 2026
capture-kit's resolveIosAxTreeViewport now owns the whole rule both providers
use (the tree root when usable, else the device's screen box), so Limrun and
Revyl no longer each spell it. Revyl's loopback check uses the same
net.BlockList ranges as the daemon client, and the fixture ends a session
exactly when the client accepts a /stop, with a test for a refused 200.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@thymikee

Copy link
Copy Markdown
Member Author

Thanks. All three optional notes are taken in 86c4c7f:

  • capture-kit's resolveIosAxTreeViewport(tree, screen) owns the whole "tree root, else the device's screen box" rule, and both Limrun and Revyl call it.
  • Revyl's loopback check uses the same net.BlockList ranges as the daemon client.
  • The fixture ends a session exactly when the client accepts the answer (success === true && (request_accepted ?? true) === true). A new runtime test refuses a stop inside a 200, checks the lease is kept, and then checks that the accepted retry ends it.

Live run at the current head, from a packed install. 86c4c7f plus #4108's commits, installed through a local registry:

  • iOS, UIKitCatalog: open --relaunch, then snapshot -i returned the 20-node list.
  • screenshot wrote 440x956 @1x.
  • close left Revyl with nothing active.

The stricter iOS readers added since b535a57 (the node validation and the screen-size refusal) did not fire on the real backend. The viewport fallback could not be forced live, because Revyl's iOS tree always reported a usable root frame. Its rule is covered by capture-kit tests.

For kill -9, the run on #4108 shows the restarted daemon adopting the orphaned lease 23 ms after taking the lock, and Revyl recording the session completed three seconds after the kill. The same run showed a refused second runtime leaving the journal and the device untouched.

@apex-sentinel-staging apex-sentinel-staging Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Apex Sentinel: 7 new findings on 86c4c7f. The verdict and the full list are in the summary comment.

Comment thread packages/provider-revyl/src/snapshot.ts Resolved
Comment thread packages/capture-kit/src/ios-ax-tree.ts Resolved
Comment thread packages/capture-kit/src/ios-ax-tree.ts Resolved
Comment thread packages/capture-kit/src/ios-ax-tree.ts Resolved
…the dead run cancel

A screenshot's source density came from the width alone, which a rotated app
interface swaps. The run-cancel fallback in stopRevylSession was unreachable,
because every rented session carries the issued session id, so the cancel
call, its type and its fixture route go. The shared reader also treats a
half-filled screen box as invalid and reads role_description as scalar text.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

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

ready-for-human Valid work that needs human implementation, judgment, or maintainer merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants