Skip to content

fix(provider-webdriver): allocate one WebDriver session per lease - #4130

Merged
thymikee merged 5 commits into
mainfrom
fix/sm3-webdriver-single-allocation
Nov 2, 2026
Merged

thymikee merged 5 commits into
mainfrom
fix/sm3-webdriver-single-allocation

Conversation

@thymikee

@thymikee thymikee commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Summary

Concurrent allocations of one lease could rent two billed WebDriver sessions, and teardown during creation could orphan a session. The manager now publishes one lease entry synchronously, shares its creation and disposal, refuses allocations during teardown, and reports cleanup outcomes with session identities.

Provider callback budgets cover prepared-session cleanup on disposal and failed creation, plus teardown listArtifacts. prepareSession remains unbounded; release and shutdown wait for preparation. Release awaits artifacts even when it joins a shutdown take; freeing the lease slot and shutdown() await disposal only.

Node option: createCloudWebDriverRuntime({ ...options, providerCallbackTimeoutMs: 5000 }).

Four files in packages/provider-webdriver; no scope expansion. SM-3; related #4102.

Validation

Tested head e094faebb3a6899a89b95410647fa9331ad33fa7; base f7879cf2d266ee1dd476d0342b2819bdeceb647b.

pnpm format and pnpm check:affected --run --head e094faebb3a6899a89b95410647fa9331ad33fa7 passed (exactCommit: true); receipt .tmp/validation/run-NzjuKc/receipt.json.

Both real BrowserStack races passed at 86216483fb: release and shutdown during unanswered POST /session, followed by same-lease allocation. Each created/deleted the same session ID, left zero owned sessions live, and returned the expected typed refusals. Full logs and validation receipt: review response.

This follow-up changes comments only; runtime behavior is unchanged from the live-tested head. Existing fail-first results remain in earlier review responses. GitHub owns full coverage, provider integration, and device lanes. Limitation: no-app Android sessions; no app-upload path was exercised. Independent review remains with the coordinator.

Two concurrent lease_allocate calls for the same lease id each rented a billed cloud session: the reuse check ran after an await, so neither call could see the other's session, and only the last one published was ever releasable. A release or shutdown that landed while a create was in flight also found nothing to dispose.

Publish the lease's entry before the first await so a duplicate allocation joins the in-flight create instead of starting its own. Model the entry as a discriminated union: allocating until the hub answers, then ready. Release and shutdown take the entry out of the map in one synchronous step, so whoever holds it disposes the session exactly once, including a session that finishes renting after the lease is already gone.
@vercel

vercel Bot commented Nov 1, 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 Nov 1, 2026 10:22pm UTC

Request Review

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Size Report

Metric Base Current Diff
Installed (including dependencies) 5.67 MB 5.67 MB +5.7 kB
Package (unpacked) 5.67 MB 5.67 MB +5.7 kB
Package (download) 1.72 MB 1.73 MB +1.3 kB

Startup median (7 runs, lower is better):

Scenario Base Current Diff
CLI --version 28.1 ms 29.0 ms +0.9 ms
CLI --help 57.7 ms 58.1 ms +0.4 ms

@thymikee thymikee left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No blocking findings.

I reviewed merge-base f7879cf2d266ee1dd476d0342b2819bdeceb647b through head 0f58d95ea0a7ed853978fa47c16348dcd7e75bd7. The allocating/ready entry gives each lease one published create promise; release and shutdown synchronously take ownership of that entry, then await and dispose the eventual session once. This also handles a joined allocation after release and a new allocation for the same lease while the old create is pending: the old taker disposes its session, and the old allocation cannot return a session no longer held by its entry.

The new regression tests exercise the original duplicate-create and in-flight release/shutdown failures; each would fail on the base implementation. Refusal classification uses AppError details.reason, consistent with ADR 0010. The union is a reasonable shape because it makes the two states explicit; no simpler representation improves ownership clarity. Changes stay in the owning session manager and its adjacent tests.

@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 2 files

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

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

@thymikee thymikee left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Needs changes. Reviewed exactly f7879cf2d266ee1dd476d0342b2819bdeceb647b..0f58d95ea0a7ed853978fa47c16348dcd7e75bd7.

  1. [P1] Keep a releasing allocation tracked until disposal finishes — packages/provider-webdriver/src/runtime-session.ts:141. Interleaving: allocation A is held in preparation or POST /session; release removes A's entry and waits; allocation B for the same lease now sees no entry and starts a second create. When both succeed, two billed sessions have been created for overlapping allocations of one lease. A scratch reproduction returned created=[session-1, session-2], with only session-1 deleted by that release and session-2 remaining live until a later shutdown. The same removal also means A -> release -> shutdown makes shutdown resolve with the create and cleanup still pending: the entry is absent from its snapshot. Fix: keep an owner-local releasing entry with a shared disposal promise, refuse allocation during that lifetime, and have shutdown join that promise. Remove it by identity only after disposal settles. Add regressions for replacement allocation before the old create settles and shutdown joining an in-flight release.

  2. [P2] Revalidate ready-session reuse after its new await — packages/provider-webdriver/src/runtime-session.ts:114-118. Interleaving: an initial allocation finishes; a repeated allocation captures the ready entry and suspends in rejectRefusedProfileFields; release or shutdown removes and closes that session; the repeated allocation resumes and returns heartbeat(lease), which is now undefined. The allocation resolves successfully although no session remains, bypassing the typed refusals used by the allocating branch. This is a new race: the base performed validation before looking up the live session and had no await between reuse lookup and heartbeat. The scratch reproduction confirmed a resolved undefined after one successful DELETE. Fix: after validation, require the same ready entry/session to remain current and throw the corresponding typed release/shutdown refusal otherwise. Cover both teardown paths.

  3. [P2] Do not report disposal success without its outcome — packages/provider-webdriver/src/runtime-session.ts:127-129, :500-502. Interleaving: allocation is pending; shutdown takes its entry; create succeeds; DELETE fails. The allocation nevertheless throws with disposedSession: true and the hint Nothing to clean up: agent-device stopped the session it was creating. Shutdown discards closeSession's warnings, so the caller gets neither the DELETE failure nor the remote session identity needed for manual cleanup. The scratch reproduction confirmed this exact error after a rejected DELETE. Even a slow successful DELETE can still be pending when this success claim is emitted. The release refusal makes the same premature claim and advises another billed allocation. Fix: share/await the disposal outcome, preserve the allocation refusal as primary, attach failed-cleanup evidence and session identifiers as structured secondary diagnostics per ADR 0010, and choose a hint that reflects confirmed versus uncertain cleanup. Keep the existing failed-DELETE sessionId retention and no-create-retry policy.

Validation: pnpm install --frozen-lockfile && pnpm build passed. Ran the package unit suite once: pnpm exec vitest run --project unit-core packages/provider-webdriver — 27 files / 311 tests passed. Separate scratch probes imported the unmodified runtime and reproduced the findings. A 480-schedule action/settlement matrix covering successful and rejected creates found no duplicate or missing disposal attempts when every earlier release and final shutdown were awaited; it does not resolve shutdown returning before a release finishes. No CI wait. Source checkout is clean.

The three added regressions do detect the original base defects by source comparison: the base can create twice, release finds no pending session, and shutdown snapshots no pending session. I did not rerun them on the base. They omit the interleavings above. The production diff stays in the owning session manager. COMMAND_FAILED plus details.reason fits ADR 0010 and survives normalizeError; no text matching or new global error-code registration is needed. The owner-local union is appropriate, but it currently omits the releasing lifetime. A single lease entry holding the creation and shared disposal promises could also express this without a separate framework; the hand-built deferred and extensive narrating comments are simplifiable, not additional blockers. Intentional stale-owner retention, failed-DELETE client identity, and unretried ambiguous creates remain unchanged.

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Reviewed 0f58d95. I found three defects in the session lifecycle. Checks are still running (Smoke Tests and Coverage; Coverage runs the new allocation tests, so it overlaps this diff). There are no conflicts. Before merge, each lease entry must stay in the map until its disposal settles, and the live-hub evidence below is still missing.

Shutdown can finish while a release is still running. Release deletes the entry and then awaits the creation at L142. Shutdown snapshots the map, no longer sees that entry, and resolves at once, while the create, DELETE and provider cleanup are still pending. If the process exits then, a billed hub session stays alive until the provider reaps it. The rule: an entry stays in the map until its disposal settles, and allocate, release and shutdown all see it. Please add a releasing state with one shared disposal promise and the reason that started it (release or shutdown). Release swaps the entry to releasing synchronously and deletes it by identity after closeSession settles. Shutdown snapshots every entry, including releasing ones, and joins their disposal. Allocate refuses while an entry is releasing. This matches the Revyl rule at packages/provider-revyl/src/runtime.ts:150-160 and :436. Please add regressions for release then same-lease allocate, and release then shutdown, while the create is held.

A repeat allocation can resolve successfully with no session. It reads a ready entry at L109 and suspends in the profile-field refusal at L117, which awaits a dynamic import. A release or shutdown can take and close the session in that gap. On resume, heartbeat at L118 returns undefined, so lease_allocate answers ok with no provider data for a session that was just deleted. The caller then drives a device that no longer exists and gets no typed refusal. The base had no await between the lookup and heartbeat, so this window is new. The rule: run the profile-field refusal once, before the entry lookup, for every caller, so the lookup and publish stay synchronous after that single await, as in Revyl allocate at packages/provider-revyl/src/runtime.ts:257-277. Then the refusals at L117, L123 and L251 can go.

A refused caller can be told cleanup succeeded when it did not. When a release or shutdown has taken the entry, allocate throws as soon as the creation resolves, while the disposal may still be pending or may fail. Even so, the refusal sets disposedSession: true with the hint "Nothing to clean up" (L500-502), and the release variant says the session is already stopped (L478). Shutdown also drops closeSession warnings, because Promise.allSettled at L195 ignores the close result. If DELETE /session fails, the caller holds a structured claim of cleanup and no session id to stop by hand. The daemon reports released only when the provider returned no warnings (packages/daemon/src/sessions/handlers/lease.ts:317-319). The rule: a refusal may claim cleanup only after the disposal it names has settled with no warnings. Please await the entry's shared disposal promise before throwing, set disposedSession from its outcome, attach the session ids and warnings on failure, and take the reason from the entry rather than the global shutDown flag.

The open inline threads still apply: the release-before-creation delete (P1), the reason from global flag (P2), and the refusal inside shared creation (P2). The three fixes above resolve all of them. No thread can be resolved yet.

Could the whole fix stay in WebDriverSessionManager by copying the Revyl rule exactly? That means one profile-field await before the lookup, one entry type with allocating, ready and releasing{reason, disposal} states, allocate refusing on a releasing entry with that entry's reason after awaiting its disposal, and shutdown joining every disposal. I think that fixes the three defects and the three threads in about the same number of lines. It also makes the later move onto the shared lease table in #4102 a pure move. Nothing needs to change first for this fix.

Not blocking, take or leave: add a test where the first POST /session returns 500, the first allocate rejects, and a second allocate of the same lease creates session-1 (nothing asserts the identity-based clear at L232-236 today); consider linking #4102 in the PR body, since this copy of Revyl's entry logic uses a weaker delete-before-disposal rule; and optionally bound a joining caller by its own signal and deadline, as Revyl does with revylCallerBudget, instead of the starter's deadline at L324-326.

For live validation, the PR body reports a BrowserStack run (created=1 deleted=1) but no receipt, and that run bypassed the daemon lease_allocate route and the prepareSession upload path. After the fixes, please re-run against the real hub through createCloudWebDriverRuntime and attach the log. It should cover (a) concurrent allocate, (b) release while the create is in flight and then a same-lease allocate before the release settles, and (c) release in flight and then shutdown. For each case, the hub's created and deleted session-id lists should match with zero live sessions at the end, and the typed reason each refused caller received should be shown. I read the code and did not run the tests or these interleavings. I also did not confirm that the process exits right after runtime shutdown resolves, which the billing impact in the first finding assumes.

…ends

A release took a lease out of the manager's map and then waited on the hub, so a fresh allocation in that window saw a free lease and rented a second billed session the release never saw, and a shutdown that arrived there answered with the first session still live.

A repeated allocation validated its flags after reading the live session, so a teardown landing inside that await left the caller told the lease was held by a session that no longer existed. A shutdown whose DELETE was refused reported a clean close, and the allocation refused for that session was told nothing needed cleaning up.

A lease entry now keeps owning its session while it is being ended: the take and the one disposal it starts are recorded in the same synchronous step, the entry leaves the map only once that disposal settles, and a second release or a shutdown joins it. An allocation reports the disposal it waited on, naming the session and whether its end is confirmed, pending, or failed; a shutdown that could not stop a session now fails the way a plugin cleanup failure is reported at close. Every allocation is refused for its own refused profile flags before it joins or publishes anything.
@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Thanks for the update. At 8b2bdb0 the three round-1 code issues are fixed: a racing allocate can no longer start a second create, the refusal reason comes from the entry's own take, and a refused joiner can no longer poison the create it joins. One piece of live evidence is still missing. All 19 checks pass at this commit, and the unit lanes run the changed allocation tests. Before merge, please attach the live run described below.

The round-1 review asked for live case (b): a release issued while the real POST /session is still in flight, then a same-lease allocate before that release settles. The live table covers concurrent allocate, an allocate that arrives while a release's DELETE is held after the create finished, and shutdown joining that held release. No live run issues a release or shutdown while the hub create is pending (runtime-session.ts#L357). That is the headline bug, and BrowserStack creates take tens of seconds, so it is the widest real window to orphan a billed session. Only the fetch-level fake hub proves it today. The live results are also counts only, with no log or session-id lists to check. Please re-run the existing harness against the real hub: issue allocate, wait until POST /session is sent but unanswered, then issue release and a same-lease allocate. Repeat once with shutdown in place of release. For each run, attach the log showing equal created and deleted session-id lists with zero live sessions at the end, the first allocate refused with provider_session_released_during_allocation (or provider_session_shutdown_during_allocation) with cleanup confirmed and the created providerSessionId, and the racing allocate refused with provider_session_release_in_progress.

Not blocking, and fine to take or leave: take.settled is set in the same callback that removes the entry from the map, so refusalForTeardownInProgress never sees it set. The take.settled?.… fallbacks and the uncertain hint branch there cannot fire. Deleting settled, always reporting cleanup pending with take.providerSessionId, and dropping that branch would also remove most of the hint duplication with teardownHint.

Open threads: these still apply, all non-blocking: the test polling budget, artifact listing in shutdown, and merged shutdown warnings. Two lower-priority threads on the teardown refusal wording also still apply. These do not apply and can be resolved: repeat-allocate envelope is the same answer heartbeat returns, behind the same guard; disposal rejection handler is moot because every awaited step in disposeTakenEntry is already caught; in-flight create visibility matches base behavior, since sessions are published only after the create answers. These are fixed at this head and can also be resolved: second create on racing allocate, global shutdown flag in refusal reason, and refused joiner validation.

One limit on what I checked: I did not run the new tests. I judged the claim that 6 failed at 0f58d95 plausible from reading only.

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Round 2 — review #5726962994 addressed

New head: 8b2bdb05f5cd37d18d91932c27027784fbe2f65b (one commit on top of the reviewed head 0f58d95ea0a7ed853978fa47c16348dcd7e75bd7).

Each finding got a regression test first; all six new cases were recorded failing at the reviewed head (Tests 6 failed | 3 passed (9) in packages/provider-webdriver/src/runtime-session-allocation.test.ts) before the fix. With the fix: Tests 9 passed (9). No finding rejected.

P1 — runtime-session.ts:141 release permits a second concurrent create and hides pending disposal from shutdown

Accepted. The reviewed code deleted the lease's entry and only then awaited the hub, so the lease was ownerless for the whole window the disposal waited through. Ownership now lives in the entry itself: release and shutdown record the take ({ by, providerSessionId, disposal, settled }) and the single disposal it starts in one synchronous step, and the entry leaves entriesByLeaseId only once that disposal settles. Consequences:

  • an allocation arriving mid-release is refused instead of renting a second session — DEVICE_IN_USE / details.reason: 'provider_session_release_in_progress' (ADR 0010: the lease slot is contended, and the same call succeeds once the release finishes), carrying the session id and cleanup;
  • a second release and a shutdown that arrive in that window join the in-flight disposal, so shutdown cannot answer while a session of this provider is live;
  • the allocation whose session was taken reads its refusal from its own entry's taken, so the reason names the teardown that actually happened, with cleanup: 'confirmed' | 'uncertain' | 'pending' and providerSessionId in the details.

Tests: an allocation that arrives during a release still disposing creates no second session (failed at the reviewed head with the racing allocation reaching a second create), plus a release during creation disposes the session that creation makes and shutdown during creation disposes the session that creation makes (both failed: the refusal carried no session id).

Live evidence against the real BrowserStack hub (Samsung Galaxy S21, osVersion 11), same harness at both heads:

head scenario POST /session DELETE racing allocate
0f58d95e (reviewed) allocate during in-flight release 2 2 resolved with a second real session
8b2bdb05f5 (this push) allocate during in-flight release 1 1 rejected DEVICE_IN_USE / provider_session_release_in_progress, cleanup: pending

At the new head the hub's DELETE was still held when the racing allocate, the repeat allocate and the shutdown were all issued; pendingBeforeAnswer { release: true, shutdown: true, deleted: 0 } — neither the release nor shutdown answered while the session was live, and after the delete was released created=1 deleted=1. Concurrent-allocate on one lease: created=1 deleted=1 at both heads (that half was already covered by round 1).

P2 — runtime-session.ts:118 ready-session reuse can resolve successfully after teardown

Accepted, fixed structurally rather than by adding a post-validation check (matching the reviewer's own note). allocate now runs rejectRefusedProfileFields(req) first, before the map lookup and before any entry is published or joined; nothing after the lookup awaits, so the ready-session answer and the session it describes are the same synchronous step and a teardown cannot land between them. createSessionForLease no longer validates flags — every allocation is refused for its own flags before it bills anything, and a caller that joins someone else's create is no longer refused for the other caller's flags (it is refused for its own, before it joins).

Tests (all failed at the reviewed head, where the repeat allocation resolved session-2 / a stale success):

  • a repeated allocation cannot answer that a released lease is alive — the hub holds the release's DELETE, so the release is provably still ending the session at the moment the repeat allocation answers; it is refused provider_session_release_in_progress with cleanup: pending, and bills nothing.
  • a repeated allocation cannot answer that a shut down lease is alive — same shape on shutdown.
  • an allocation refused for its own flags leaves the create it joined alone — covers the flags move: the joined create still resolves with its session, the refused caller gets INVALID_ARGS for its own flags, one create total.

P2 — runtime-session.ts:129 shutdown claims successful disposal when DELETE fails

Accepted. Disposal now reports cleanup: 'confirmed' | 'uncertain' | 'none', derived from the provider's answer rather than assumed, and it is threaded to both surfaces:

  • the refused allocation carries cleanup plus cleanupWarnings (so cleanup: 'uncertain' when the DELETE was refused, never 'none');
  • shutdown() now throws COMMAND_FAILED / details.reason: 'provider_session_cleanup_incomplete' naming every session id it could not end (providerSessionIds, cleanup: 'uncertain', cleanupWarnings) instead of resolving. The plugin registry captures a throwing runtime.shutdown() into plugins.failed, which is what the daemon's shutdown report and the provider_plugin_cleanup_failed diagnostic read, so the failure reaches an operator rather than being dropped. No retry or extra delete is attempted — the create-retry policy is untouched, and the live created/deleted counts above are unchanged by this.

Test a session shutdown could not stop fails the shutdown and the refused allocation failed at the reviewed head: shutdown resolved as clean and the refused allocation was told cleanup: 'none' while the hub had refused the DELETE.

Validation at 8b2bdb05f5

pnpm check:affected --run --head in the foreground, status: passed, exit 0, exactCommit: true at this head — format, lint, typecheck, layering, fallow, build, vitest-related (304 tests) and unit (16 757 tests) all passed; device/replay/web-smoke ids skipped as not affected. Receipt: /Users/thymikee/.t3/worktrees/agent-device/t3-c4bef626/.tmp/validation/run-9dCIop/receipt.json. Focused: pnpm exec vitest run --project unit-core packages/provider-webdriver → 27 files, 317 tests passed; daemon session and http projects also green. Scratch harnesses deleted, working tree clean.

Honest note: provider shutdown has a 5 s budget (DAEMON_PLUGIN_SHUTDOWN_TIMEOUT_MS) while a held POST /session can run longer than that. A create disposed after that budget still gets disposed and its delete still goes out, but the daemon close reports the plugin as pending rather than failed. That budget is owned by the daemon, outside this PR's scope.

Two windows stayed open on the reviewed head.

An allocation validated its profile fields before it published the lease's
entry, so a release issued while that check was pending found no entry,
answered that the lease held nothing, and the create that resumed billed a
session no route was left to stop. Cloud leases without a device key are not
serialized by the daemon's lease registry, so both calls can be in flight at
once. The entry is now published before the allocation's first await, and the
publishing caller's own flags are refused by the create it stands on, as its
first step, so a call that cannot be honoured still bills nothing. A caller
that joined an existing entry is still refused for its own flags, and its live
session answer is still read with no await after the lookup.

Ending a session also waited on the plugin's artifact lookup, which has no
bound of its own. A custom listArtifacts that never answered kept the lease
DEVICE_IN_USE for every later caller and was awaited by daemon shutdown.
Ownership now ends once the remote DELETE and the provider's cleanup settle;
the artifact report is started at that point and joined only by the release
that carries it. Both provider callbacks the manager awaits are bounded by
providerCallbackTimeoutMs, defaulting to this package's one-round-trip budget,
and a cleanup that outlives it is reported as PROVIDER_CLEANUP_FAILED with the
session's outcome uncertain rather than holding the lease.

Also: a disposal that could not be observed at all now reports itself instead
of hanging every waiter; the reuse answer and heartbeat share one envelope
builder; and a shutdown's hint names the shutdown rather than a release.

@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 3 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 thymikee left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Needs changes. Reviewed 0f58d95ea0a7ed853978fa47c16348dcd7e75bd7..8b2bdb05f5cd37d18d91932c27027784fbe2f65b, including daemon lease callers and plugin shutdown handling.

  1. [P1] Track allocation admission before the first await — packages/provider-webdriver/src/runtime-session.ts:144-151. Moving validation before publication opens a new release window: call allocate(lease), then release while rejectRefusedProfileFields() is pending. release() sees no entry and resolves; validation then resumes and creates a billed session for the released lease. This regresses the reviewed head, which published the allocating entry before awaiting validation. It is reachable through daemon lease callers for cloud leases without deviceKey, where LeaseRegistry.runDeviceMutation() does not serialize them. A scratch probe through the actual handleLeaseCommands and LeaseRegistry, with the unmodified runtime and a fake WebDriver hub, returned release={ok:true,data:{released:true}}, allocationOk=true, sessionId=session-1, activeLeases=0, and hub events [POST]: the remote session survives a successful release with no active daemon lease. Fix: make validation synchronous before lookup/publication, or track pending admission so release joins or invalidates it. Preserve per-caller validation and the ready-session path's lack of a post-lookup await. Add a regression that releases immediately after allocation starts, before provider preparation begins.

  2. [P2] Bound callbacks that now retain the releasing entry — packages/provider-webdriver/src/runtime-session.ts:373-377. take() only removes the entry and resolves its shared disposal after disposeTakenEntry() finishes, but provider cleanup() and listArtifacts() have no manager-enforced bound. These callbacks are supported by the exported WebDriver plugin runtime options. If a plugin's artifact lookup never settles, even after successful DELETE and provider cleanup, every later allocation of that lease returns DEVICE_IN_USE forever, repeated release never completes, and shutdown joins the same stuck disposal. A probe holding only listArtifacts confirmed [POST, DELETE] with release and shutdown still pending and allocation refused with cleanup: pending; answering that callback lets both finish. Shutdown now also performs artifact lookup, whereas the reviewed head's shutdown did not. The bundled BrowserStack and AWS implementations have their own I/O bounds (15 s artifact HTTP and 30 s host commands), but custom callbacks have none; the daemon's 5 s plugin-close budget reports pending and does not settle this runtime disposal. Fix: bound provider callbacks and keep optional artifact collection from indefinitely controlling lease ownership or shutdown completion after resource cleanup has settled. Cover a hanging artifact callback separately from a held DELETE.

Round-1 dispositions:

  • P1 release ownership: fixed after an entry exists: a take is recorded synchronously, mid-release allocation is refused, repeated release/shutdown join one disposal, and map removal is identity-checked after settlement. Finding 1 above concerns the newly introduced window before entry publication.
  • P2 ready reuse: the stale ready-entry answer is fixed: validation precedes lookup and ready reuse returns without another await. Each joining caller validates its own flags.
  • P2 failed DELETE on shutdown: the runtime now reports cleanup: uncertain and throws COMMAND_FAILED / provider_session_cleanup_incomplete with session ids. Registry cleanUp() catches shutdown errors, still runs plugin dispose, and closes registrations concurrently; the daemon records failed plugins and continues finalization. Existing report projection retains code/message/hint (including ids in the message), rather than the full cleanup details.

DEVICE_IN_USE / provider_session_release_in_progress correctly describes contention and normalizes to retriable: true; daemon allocation does not automatically retry it, and a refused reused allocation leaves the original lease alone. The other new refusals use existing COMMAND_FAILED with details.reason, consistent with ADR 0010; no production consumer text-matches these messages. Stale device ownership remains retained through release and cleared at shutdown; failed DELETE still leaves the client's sessionId intact. No create retry was added.

Validation: provider unit suite run once — pnpm exec vitest run --project unit-core packages/provider-webdriver: 27 files, 317 tests passed. Scratch probes imported unchanged source; no source edits, commits, or pushes. Reviewed checkout remains clean. No CI wait.

@apex-sentinel-staging

apex-sentinel-staging Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Apex Sentinel

Verdict: approve

Since the last review: 6 new · 1 still open · 4 no longer raised

7 finding(s): 3 minor, 4 nit. Verdict: approve. reached max rounds (1).

🟡 Minor (3)

Where Finding
packages/provider-webdriver/src/runtime-session.ts:244 🆕 shutdown() went from never throwing to throwing, with no caller or doc updated
packages/provider-webdriver/src/runtime-session.ts:268 🆕 ownedDeviceIds gains a device id after shutdown() cleared it, so a closed manager still owns a device
packages/provider-webdriver/src/runtime-session.ts:442 🆕 A failed provider cleanup is reported as a session still billing
🔵 Nit (4)
Where Finding
packages/provider-webdriver/src/runtime-session.ts:96 🆕 DEFAULT_PROVIDER_CALLBACK_TIMEOUT_MS only matches the round-trip budget by coincidence
packages/provider-webdriver/src/runtime-session.ts:674 🆕 Refusal details switch shape (array vs code-keyed map) between the shutdown race and every other refusal
packages/provider-webdriver/src/runtime-session.ts:188 🆕 A release that joins a shutdown-initiated take still waits on the artifact lookup
packages/provider-webdriver/src/runtime-session.ts:711 Post-shutdown refusal reuses the during-allocation reason

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.

@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 8b2bdb0. The verdict and the full list are in the summary comment.

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

Tip: Get a Slack DM when cubic finishes reviewing your PRs. Connect Slack

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

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Round-3 dispositions — head 7490b82b000422baf792fb729ca2feda2c39fc54

Both findings from review #5727286449 are fixed, and every Apex Sentinel item is dispositioned below (4 accepted, 1 declined with a reason). One commit, one push: 7490b82b00.

Finding 1 (P1) — allocation admission before the first await: fixed

The preferred route was checked first and is blocked by an enforced invariant: rejectRefusedProviderProfileFields lives in @agent-device/contracts/provider-profile-fields, and making the call synchronous means a static value import, which scripts/__tests__/eager-closure-budgets.test.ts refuses for this package — both packages/provider-webdriver/src/index.ts and connection-verification.ts grew by that module (+1 each, 17 eager top-level statements), and scripts/layering/check.ts keeps dynamic edges out of scope for R5/R80 by decision (#3280), i.e. the dynamic import is the policy, not an oversight. So this takes the fallback from the review: the entry is published before the allocation's first await.

allocate now splits by whether the caller owns the entry:

  • No entry yet → beginAllocation publishes it in the same synchronous step as the lookup, with no await at all on this route, so the lease is owned from the moment it is asked for. The publishing caller's own flags are refused inside the create it stands on, as its first statement, before prepareSession and before POST /session — so an allocation that cannot be honoured still bills nothing and never reaches the provider.
  • Joining someone else's entry → the caller is refused for its own flags (it does not inherit the create's answer), and the lookup is re-read after that await. Nothing awaits after that lookup except joining the create, so the ready-session answer stays await-free: a release or shutdown landing during the flag check is the outcome the caller reports instead of a session that has already been ended.

Regression (runtime-session-allocation.test.ts): a release issued before provider preparation begins disposes the session the allocation rents — allocate and release issued in one synchronous step. hub.created is still [] when the release arrives; the release takes the admission entry, resolves { provider, providerSessionId: 'session-1' }, the allocation is refused provider_session_released_during_allocation with cleanup: 'confirmed', and hub.deleted is ['session-1']. No billed session survives a release that reported success.

One extra test, an allocation refused for its own flags bills nothing before it reaches the provider, locks the moved validation: INVALID_ARGS / --provider-app, hub.created == [], preparation.started == 0. It passes at the previous head too — it guards the new placement rather than being one of the fail-first regressions.

Finding 2 (P2) — ownership ends with the billed resources, provider callbacks bounded: fixed

  • take() returns the WebDriverLeaseTake instead of only its disposal promise. The disposal resolves, and the entry leaves entriesByLeaseId, once the remote DELETE and the provider's cleanup() settle. Artifact collection is out of the ownership path: it starts at that settle point as take.artifacts and is joined only by release(), so the release envelope still carries cloudArtifacts (the integration lanes assert status: 'ready') while neither the lease nor shutdown() waits on it. shutdown() awaits take.disposal alone. artifacts is removed from WebDriverDisposalOutcome.
  • New providerCallback(label, run) bounds every provider callback the manager awaits — a prepared session's cleanup and the disposal's artifact lookup — with options.providerCallbackTimeoutMs, defaulting to DEFAULT_PROVIDER_CALLBACK_TIMEOUT_MS = 30_000, this package's existing one-round-trip budget (webdriver-transport.ts:145). The timer is unref'd and a too-slow callback's late outcome is swallowed, so it cannot surface as an unhandled rejection. A cleanup that outlives its budget is reported as cleanup: 'uncertain' + PROVIDER_CLEANUP_FAILED naming the budget, and the lease is given back.
  • The bound applies to the disposal path only: safeListArtifacts(session, withinCallbackBudget = false), so artifacts as a caller's own query is left to the provider's own bound.

Regressions: an artifact callback that never answers holds neither the lease nor shutdown — with the lookup hanging, session-1 is deleted, a re-allocation returns session-2, shutdown() resolves, and the release still answers with providerSessionId: 'session-1' and cloudArtifacts.status: 'unavailable'. a provider cleanup that outlives its budget ends the disposal as uncertain — PROVIDER_CLEANUP_FAILED naming 40ms, a re-allocation succeeds, and a later session the hub does end still closes cleanly.

Fail-first record for the three finding regressions, same command at 8b2bdb05f5: Tests 3 failed | 9 passed (12) — release-before-preparation resolved value: undefined; mid-artifact-hang re-allocation was refused provider_session_release_in_progress; the cleanup-budget test timed out. At 7490b82b00: Tests 13 passed (13).

Apex Sentinel items

Where Disposition
runtime-session.ts:153 — reuse answer drops the envelope heartbeat() shares Accepted. liveLeaseEnvelope() is now the single builder behind both the heartbeat answer and the reuse answer of allocate, so lease_allocate and lease_heartbeat cannot drift for recordProviderSession.
runtime-session.ts:338 — disposal .then had no rejection handler Accepted. disposeTakenEntry is handled on both branches. The rejection branch settles cleanup: 'uncertain' with PROVIDER_CLEANUP_FAILED, resolves the disposal, and still removes the entry, so a disposal this manager lost sight of is reported rather than hanging release, shutdown and every later allocation of that lease.
runtime-session.ts:487 — an in-flight create is invisible to readers Declined, deliberately unchanged. While the create is in flight there is no session id, endpoint or device to hand out: findSessionForDevice, getInteractor and listCloudArtifacts have nothing valid to return, and making heartbeat answer for an unsettled create would make the daemon recordProviderSession for a session that may never exist and point callers at releasing a session that does not exist. The invariant this PR owns is that no billed session can outlive a route able to end it — which the admission entry above now guarantees — and the reviewer notes this window matches the predecessor. Widening reader visibility is a separate behaviour change, and the minimal-change instruction for this round rules it out.
runtime-session.ts:592 — refusalForTeardownInProgress duplicates the cleanup/hint logic Partly accepted. Both branches now read cleanup and providerSessionId from the take exactly once, so those two facts cannot drift. The two builders stay separate on purpose: the refusals differ in error code (COMMAND_FAILED vs DEVICE_IN_USE, ADR 0010), reason, message and hint audience — the in-progress caller created nothing and is told about the lease, while the teardown caller's own session died and needs teardownHint. Collapsing them means passing code, reason, message and hint as four mode arguments to one builder to share two lines.
runtime-session.ts:640 — teardownHint names "that release" on a shutdown Accepted, with your patch: teardownHint(provider, by, cleanup) names the route that is actually running.

Validation

pnpm check:affected --run --head 7490b82b00, foreground: status: passed, exit 0, exactCommit: true. Format, lint, typecheck, layering, fallow, build, vitest-related (304 tests) and unit (16 760 passed, 1 skipped) all passed; device, replay and web-smoke ids skipped as not affected. Receipt: .tmp/validation/run-Ud5g4z/receipt.json. Focused: pnpm exec vitest run --project unit-core packages/provider-webdriver → 27 files, 320 passed; --project provider-integration on cloud-webdriver-runtime.test.ts + cloud-webdriver-provider-adapters.test.ts → 15 passed, so the release envelope still carries ready cloudArtifacts. No user-facing help or docs change: no documented flag, command or output changed.

Carried forward from round 2, unchanged: plugin close has a 5 s budget (DAEMON_PLUGIN_SHUTDOWN_TIMEOUT_MS) while a held create may run longer, so a create disposed after that budget reads as a pending plugin in the shutdown report.

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Coordinator check of round 3 (8b2bdb05f5..7490b82b00). This replaces a fourth full review, per the round cap.

Both round-2 findings are resolved:

  • P1: the allocation that publishes the entry no longer awaits before beginAllocation. Its own flag check is now the first statement of the create, so a release that lands there takes the entry, and nothing is billed. A joining caller validates its own flags, re-reads the map, and has no await between that lookup and its ready answer.
  • P2: the entry now leaves the map once DELETE and the bounded cleanup() settle. The artifact lookup is bounded and is joined only by release(), so it no longer holds the lease or a daemon close. A cleanup that runs past its budget reports uncertain and gives the lease back.

Non-blocking notes, for follow-up only:

  1. settle() starts the artifact lookup for shutdown takes too. Nothing joins that lookup, and it can run after the plugin is disposed. The rejection is caught, so the only cost is a call nobody waits for. Gating it on take.by === 'release' would remove it.
  2. A caller that joins an entry whose create fails on the publisher's own flags receives the publisher's flag refusal. This needs two concurrent allocations of one lease with different flags.
  3. prepareSession is a plugin callback with no manager bound, and disposeTakenEntry awaits entry.creation. A hanging prepareSession therefore holds the lease, but it already blocked allocate before this PR, so this is not a regression.

No blocking findings.

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Most of the earlier findings (#4130 (comment)) are fixed at 7490b82, and one still needs a live run. All 18 checks pass, and there are no conflicts.

The earlier review asked for a live run of the main race: a release or shutdown while the real hub's POST /session is pending and the caller waits on entry.creation at runtime-session.ts:173. The latest push does not add that run. It also moved admission validation into the create (:296), and every live row predates that move (the newest is 8b2bdb0). Only the fetch-level fake hub covers this route at head. BrowserStack creates take tens of seconds, so this is the widest window to orphan a billed session. Please run the existing harness through createCloudWebDriverRuntime against the real hub at 7490b82. Run 1: allocate, wait until the harness logs the POST /session request with no response, issue release, then send a same-lease allocate before the release settles. Run 2: the same with shutdown in place of release. Attach each log with the created and deleted session-id lists. The lists must be equal and zero sessions must be live at the end. The first allocate must be refused with provider_session_released_during_allocation (run 2: provider_session_shutdown_during_allocation), cleanup confirmed, and the created providerSessionId. The racing allocate must be refused with DEVICE_IN_USE / provider_session_release_in_progress in run 1 and provider_session_shutdown_during_allocation in run 2.

Not blocking, take or leave: (1) the createSessionForLease doc at :289 says a release that took the entry has nothing to end, but the function never reads entry.taken, so the create still rents the session and the release deletes it (the test at :347 asserts deleted == ['session-1']), so please reword the doc to say the release disposes the session the create rents, or skip POST /session when the entry is already taken; (2) a joiner can be refused for a flag it never sent, because the shared create at :296 now validates the publisher's flags, so two concurrent allocations where A passes --provider-app and B passes valid flags make B await A's create and receive A's INVALID_ARGS, and the rule should be that each caller is answered only for its own flags, for example by keeping the validation promise separate from the create so a joiner re-runs the lookup on an admission refusal; (3) settle() at :357 starts safeListArtifacts for shutdown takes too, but only release joins it, so start it only when take.by === 'release'; (4) DEFAULT_PROVIDER_CALLBACK_TIMEOUT_MS at :109 copies the 30_000 default from webdriver-transport.ts:145 and adds a public providerCallbackTimeoutMs option that only tests set, so please derive it from requestPolicy?.timeoutMs ?? 30_000.

Would it be simpler to drop the public providerCallbackTimeoutMs option, bound safeListArtifacts unconditionally (which removes the withinCallbackBudget boolean), and route every manager-awaited plugin callback through one providerCallback: prepareSession, the create-failure prepared.cleanup at :502, the post-create cleanup, and listArtifacts? Then the PR body's "every callback is bounded" claim would be true and the timeout question would be settled once.

On the open inline threads: #4130 (comment) (unbounded failure cleanup) still applies, and it is not blocking because the bundled BrowserStack and AWS callbacks have their own I/O bounds, but the doc at runtime.ts:106-111 overclaims. #4130 (comment) (waitFor yield cap) and #4130 (comment) (warnings keyed by code) still apply and are not blocking. #4130 (comment) (duplicate uncertain hint) still applies; it is lower priority. These do not apply any more and can be resolved: #4130 (comment) (heartbeat and reuse answers now return liveLeaseEnvelope()), #4130 (comment) (take() now settles cleanup as uncertain and removes the entry), #4130 (comment) (liveSession reports only a created session, as base did, and the author declined with that reason), #4130 (comment) (teardownHint now names release or shutdown), and #4130 (comment) (shutdown now awaits only take.disposal).

I did not run the new or changed tests; the fail-first at 8b2bdb0 and the pass at head are judged from reading the code. The ordering in note (2) is reasoned from the code and not reproduced. I did not re-trace the daemon lease handlers or plugin close, which are unchanged since the earlier review, and I did not inspect the round-2 live logs (the PR body gives counts only).

Before merge, please attach the two live real-hub runs at 7490b82 with equal created and deleted session-id lists, zero live sessions, and the typed refusals with cleanup confirmed.

… warnings per session

Two review threads on the reviewed head.

The new provider-callback budget covered a session's disposal but not the
hand-back a create owes when the hub refuses it. That cleanup is the plugin's
own code too, and no session exists yet for a release or a shutdown to end, so
a custom cleanup that never answered held the allocation, the lease behind it,
and daemon shutdown forever. It now runs through the same bounded providerCallback.

A close that could not stop several sessions collapsed their warnings into one
map keyed by code, so two sessions refused by the provider were reported with
one reason. Each warning now travels as its own entry with the session id it is
about, so every session in the shutdown error stays diagnosable.

The allocation tests stopped polling the clock for a step to happen: waiting for
a create to reach the provider, for the hub to end a session, and for an
artifact lookup to start are all arrival signals now, each resolved in the step
that reaches it, with one watchdog that only fires when a step never comes. A
busy CI worker can no longer fail a test whose allocation is still progressing.

@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: 6 new findings on 8621648. The verdict and the full list are in the summary comment.

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Round-4 dispositions — head 86216483fb980ab3d9c6fe6ddd5a64b6cfa60041

Three inline threads were left after triage. All three were real at 7490b82b; each is fixed, each thread has an in-thread reply and is resolved. One commit, one push: 86216483fb.

Thread Where Disposition
discussion_r4434966109 runtime-session-allocation.test.ts — test polls 200 scheduler yields for prepareSession Fixed (test-only). Replaced the bounded poll with an arrival notification: startProviderPreparation keeps a waiter list and resolves each waiter in the same step that registers a create. The two other clock-polling waits this file had became signals too — hub.deletedArrived('session-1') and startSignal() / awaitSignal(...) for the artifact lookup — and untilSettled's wall-clock loop is gone. One unref'd watchdog (ARRIVAL_BUDGET_MS = 2_000) remains and only fires when a step never arrives at all, so a slow or instrumented worker can no longer fail a test whose allocation is still progressing.
discussion_r4434966117 runtime-session.ts — shutdownLeftSessionsRunning collapsed warnings by code Fixed. cleanupWarnings on the shutdown error is now one entry per warning, each carrying its providerSessionId, code and message, so two sessions the provider refused to end report two refusals. A refusal for a single session keeps the code-keyed shape (one session's warnings already carry distinct codes), so details.cleanupWarnings for refusalForTeardown is unchanged. New regression a shutdown that could not stop two sessions names both refusals: fails at 7490b82b with warnings.map is not a function (the field was a map), passes here.
discussion_r4435167374 runtime.ts / createSessionWithPreparedCleanup — cubic P1: prepared.cleanup on a failed create was not bounded Fixed. The failure hand-back now runs through the same bounded providerCallback as the disposal's cleanup. This was the one cleanup the allocation itself must not wait on forever: no session exists yet, so neither release nor shutdown can take it over, and a hanging plugin cleanup held the allocation, the lease behind it, and daemon shutdown. The create's own refusal is still what the caller gets, with the timed-out hand-back in details.cleanupError; providerCallbackTimeoutMs's doc comment now names both routes. New regression a create the hub refused still answers when its provider cleanup hangs: times out at 5 s at 7490b82b (the allocation never answered), passes here, where the next allocation reaches the hub again instead of a contention refusal and shutdown() resolves.

Fail-first record, same command (pnpm exec vitest run --project unit-core packages/provider-webdriver/src/runtime-session-allocation.test.ts): at 7490b82b with the final test text → Tests 2 failed | 13 passed (15); at 86216483fb → Tests 15 passed (15). The thread-1 item is test flakiness, not a behaviour break, so it carries no fail-first test; the arrival signal replaced the poll outright.

Validation: pnpm check:affected --run --head 86216483fb, foreground — status: passed, exit 0, exactCommit: true; format, lint, typecheck, layering, fallow, build, vitest-related (304) and unit (16 762 passed, 1 skipped) all green; device/replay/web-smoke ids skipped as not affected. Receipt: .tmp/validation/run-H59C6j/receipt.json. Focused: --project unit-core packages/provider-webdriver → 27 files, 323 passed; --project unit-core packages/plugins packages/daemon/src/server/http packages/provider-webdriver src/provider-webdriver.test.ts → 70 files, 618 passed (the shutdown report's cleanupWarnings shape has no consumer outside this package and its tests). No user-facing help or docs change: no documented flag, command or output changed. PR body Summary and Validation updated to this head.

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Thanks for the update. At 8621648 the code looks right, and the earlier review's code findings are fixed. One piece of live evidence is still missing. CI is green: 19 checks pass, and your check:affected receipt at this head also passed. There are no conflicts.

The previous review asked for two live real-hub runs: a release, and then a shutdown, issued while BrowserStack's POST /session is still pending and the allocation waits on entry.creation (runtime-session.ts:173, disposal at :388). This update adds neither run. Only the fetch-level fake hub covers this route, and every live row predates the move of admission validation into the create (the newest row is 8b2bdb0). A real create takes tens of seconds, so this is the widest window to orphan a billed cloud session, and nothing yet shows that a teardown inside it ends exactly the session the create rents. Please run the existing createCloudWebDriverRuntime harness against the real hub at 8621648. In run 1, allocate, wait until the harness logs POST /session with no response yet, call release, then send a same-lease allocate before the release settles. Run 2 is the same with shutdown in place of release. For each run, attach the log with the created and deleted session-id lists. The lists must be equal, and zero sessions must be live at the end. The first allocate must be refused with provider_session_released_during_allocation (run 2: provider_session_shutdown_during_allocation), with cleanup 'confirmed' and the created providerSessionId. The racing allocate must be refused with DEVICE_IN_USE / provider_session_release_in_progress (run 1) or provider_session_shutdown_during_allocation (run 2).

Not blocking, and fine to take or leave: the rewritten providerCallbackTimeoutMs doc (runtime.ts:106) and the PR summary say a plugin callback that never answers cannot hold a lease open or delay a close, but prepareSession is awaited without a bound (runtime-session.ts:525-531) and disposeTakenEntry awaits entry.creation (:388), so release and shutdown still wait on a hung prepareSession. Either name only the bounded callbacks (cleanup on both routes, listArtifacts), or route prepareSession through providerCallback and dispose the session when the budget elapses.

On the open inline threads: three are fixed at this head and can be resolved: the create-failure cleanup timeout (#4130 (comment)), the waiter-list wait (#4130 (comment)), and the per-session shutdown warnings (#4130 (comment)). Six more need no change, with a one-line reason each. The shared during-allocation reason after shutdown stays distinguishable through cleanup 'none' and its own hint (#4130 (comment)). The shutdown rejection is reported as plugins.failed while other plugins still close (#4130 (comment)). The ownedDeviceIds.add runs before the clear because both chain on the same creation (#4130 (comment)). The 'uncertain' result for a failed prepared.cleanup hand-back is deliberate (#4130 (comment)). The second 30_000 literal was deferred as a non-blocking note (#4130 (comment)). The two cleanupWarnings shapes are each lossless for the reason that carries them (#4130 (comment)). One thread still applies as a doc rewording only: a release that joins a shutdown's take still awaits take.artifacts, so the take doc at :72-74 and :353-360 overclaims (#4130 (comment)).

I did not run the changed tests; fail-first at 7490b82 and pass at head are judged from reading the code. I did not inspect the earlier live logs or the validation receipts, because the PR body gives only counts. Before merge, please attach the two live real-hub runs at 8621648 described above.

@thymikee

thymikee commented Nov 1, 2026 •

Copy link
Copy Markdown
Member Author

Review follow-up for the 2026-11-01 12:34 maintainer comment. New head: e094faebb3a6899a89b95410647fa9331ad33fa7 (documentation only after 86216483fb980ab3d9c6fe6ddd5a64b6cfa60041).

  1. Two live real-hub runs: passed at 86216483fb. Both used createCloudWebDriverRuntime with native fetch to https://hub-cloud.browserstack.com/wd/hub/, Samsung Galaxy S21 / Android 11, and no-app capabilities. No fake hub, held response, or create retry. Each teardown was invoked while the real POST remained unanswered; the same-lease allocate followed before teardown settled. Release: created = deleted = [942bd37d58a28bb67efc75b73f15d7569e60896c]; shutdown: created = deleted = [9037ba0b8cdd9a77879f19eb9839e46f61c89f1b]. Each run ended with zero owned sessions live and a 404 from a post-delete hub probe. The first allocate returned its expected teardown refusal with cleanup: confirmed and that exact providerSessionId. The racing allocate returned DEVICE_IN_USE / provider_session_release_in_progress for release, and COMMAND_FAILED / provider_session_shutdown_during_allocation for shutdown, while cleanup was still pending. Full timestamped logs are below. Credentials were sourced from the main checkout's absolute .env path without copying or printing them. The scratch harness under /private/tmp was deleted after both runs. Limitation: these calls exercise the runtime facade and actual hub creation/disposal, not daemon dispatch or app upload.

  2. Callback docs and PR summary: corrected. The budget covers prepared-session cleanup on disposal and failed creation, and teardown listArtifacts. prepareSession is explicitly unbounded by this option, so release and shutdown can still wait for preparation. No new bounding was added. The nearby disposal comment was corrected too.

  3. Take/artifact docs: corrected; r4435396104 answered and resolved. Freeing the lease slot and shutdown() await disposal. A release also awaits take.artifacts, including a release that joins a take started by shutdown. Runtime behavior is unchanged.

  4. All listed threads answered and resolved.

  • r4435167374: fixed at 86216483; failed-create cleanup uses the callback budget.
  • r4434966109: fixed at 86216483; preparation uses arrival notification rather than bounded polling.
  • r4434966117: fixed at 86216483; shutdown preserves warnings per session.
  • r4434976541: no change; post-shutdown refusal is distinguishable by cleanup: none and its restart hint.
  • r4435396086: no change; shutdown failures appear in plugins.failed while other plugins close.
  • r4435396090: no change; ownedDeviceIds.add precedes clear because both chain on the same creation.
  • r4435396094: no change; uncertain cleanup after failed provider hand-back is deliberate.
  • r4435396097: no change to the second 30_000 literal; deduplication remains deferred as a non-blocking note.
  • r4435396100: no change; each warning shape is lossless for the reason carrying it.

Validation at the new head: pnpm format passed. pnpm check:affected --run --head e094faebb3a6899a89b95410647fa9331ad33fa7 passed, exit 0, exactCommit: true, with a clean checkout. Format, lint, typecheck, layering, Fallow, build, related tests (304 passed), and unit tests (16,762 passed; 1 skipped) passed. Web smoke and device/replay checks were skipped under the receipt's GitHub-authoritative or manual-lane rules; full coverage, provider integration, and device lanes remain GitHub-owned. Receipt: .tmp/validation/run-NzjuKc/receipt.json. No behavioral deviation occurred in either live run, and this follow-up changes comments only, so no new fail-first test applies. Existing fail-first evidence remains in the prior round's response. Independent review remains with the coordinator; no child reviewer was started.

Complete live log at 8621648
{"at":"2026-11-01T21:11:15.847Z","event":"run start","head":"86216483fb980ab3d9c6fe6ddd5a64b6cfa60041","by":"release","leaseId":"sm3-4130-release-1793567475847","device":"Samsung Galaxy S21","osVersion":"11","route":"createCloudWebDriverRuntime.leaseLifecycle","preparation":"no-app capabilities; no app upload","transport":"native fetch to real BrowserStack; no held responses or fake hub"}
{"at":"2026-11-01T21:11:15.953Z","event":"POST /session sent; response pending","by":"release"}
{"at":"2026-11-01T21:11:16.114Z","event":"teardown invoked","by":"release","postPending":true}
{"at":"2026-11-01T21:11:16.115Z","event":"same-lease allocate invoked","by":"release","postPending":true,"teardownSettled":false}
{"at":"2026-11-01T21:11:16.115Z","event":"racing allocate outcome","by":"release","status":"rejected","code":"DEVICE_IN_USE","details":{"reason":"provider_session_release_in_progress","provider":"browserstack","leaseId":"sm3-4130-release-1793567475847","runId":"pr4130-86216483fb","cleanup":"pending","hint":"Wait for this lease to finish releasing, then allocate it again."},"postPending":true,"teardownSettled":false}
{"at":"2026-11-01T21:11:37.078Z","event":"POST /session answered","by":"release","status":200,"providerSessionId":"942bd37d58a28bb67efc75b73f15d7569e60896c"}
{"at":"2026-11-01T21:11:37.080Z","event":"DELETE /session sent","by":"release","providerSessionId":"942bd37d58a28bb67efc75b73f15d7569e60896c"}
{"at":"2026-11-01T21:11:37.277Z","event":"DELETE /session answered","by":"release","status":200}
{"at":"2026-11-01T21:11:37.278Z","event":"first allocate outcome","by":"release","status":"rejected","code":"COMMAND_FAILED","details":{"reason":"provider_session_released_during_allocation","provider":"browserstack","leaseId":"sm3-4130-release-1793567475847","runId":"pr4130-86216483fb","providerSessionId":"942bd37d58a28bb67efc75b73f15d7569e60896c","cleanup":"confirmed","hint":"Allocate the lease again; agent-device stopped the session this allocation created."}}
{"at":"2026-11-01T21:11:37.278Z","event":"teardown outcome","by":"release","status":"resolved","value":{"provider":"browserstack","providerSessionId":"942bd37d58a28bb67efc75b73f15d7569e60896c"}}
{"at":"2026-11-01T21:11:37.313Z","event":"post-delete hub probe","by":"release","providerSessionId":"942bd37d58a28bb67efc75b73f15d7569e60896c","status":404}
{"at":"2026-11-01T21:11:37.313Z","event":"run PASS","head":"86216483fb980ab3d9c6fe6ddd5a64b6cfa60041","by":"release","created":["942bd37d58a28bb67efc75b73f15d7569e60896c"],"deleted":["942bd37d58a28bb67efc75b73f15d7569e60896c"],"live":[],"liveCount":0}
{"at":"2026-11-01T21:11:37.313Z","event":"final owned-session accounting","by":"release","created":["942bd37d58a28bb67efc75b73f15d7569e60896c"],"deleted":["942bd37d58a28bb67efc75b73f15d7569e60896c"],"live":[]}
{"at":"2026-11-01T21:11:37.313Z","event":"run start","head":"86216483fb980ab3d9c6fe6ddd5a64b6cfa60041","by":"shutdown","leaseId":"sm3-4130-shutdown-1793567497313","device":"Samsung Galaxy S21","osVersion":"11","route":"createCloudWebDriverRuntime.leaseLifecycle","preparation":"no-app capabilities; no app upload","transport":"native fetch to real BrowserStack; no held responses or fake hub"}
{"at":"2026-11-01T21:11:37.314Z","event":"POST /session sent; response pending","by":"shutdown"}
{"at":"2026-11-01T21:11:37.414Z","event":"teardown invoked","by":"shutdown","postPending":true}
{"at":"2026-11-01T21:11:37.420Z","event":"same-lease allocate invoked","by":"shutdown","postPending":true,"teardownSettled":false}
{"at":"2026-11-01T21:11:37.420Z","event":"racing allocate outcome","by":"shutdown","status":"rejected","code":"COMMAND_FAILED","details":{"reason":"provider_session_shutdown_during_allocation","provider":"browserstack","leaseId":"sm3-4130-shutdown-1793567497313","runId":"pr4130-86216483fb","cleanup":"pending","hint":"agent-device is still stopping the session this allocation created. Wait for that shutdown to finish before allocating the lease again."},"postPending":true,"teardownSettled":false}
{"at":"2026-11-01T21:11:58.970Z","event":"POST /session answered","by":"shutdown","status":200,"providerSessionId":"9037ba0b8cdd9a77879f19eb9839e46f61c89f1b"}
{"at":"2026-11-01T21:11:58.971Z","event":"DELETE /session sent","by":"shutdown","providerSessionId":"9037ba0b8cdd9a77879f19eb9839e46f61c89f1b"}
{"at":"2026-11-01T21:11:59.108Z","event":"DELETE /session answered","by":"shutdown","status":200}
{"at":"2026-11-01T21:11:59.108Z","event":"first allocate outcome","by":"shutdown","status":"rejected","code":"COMMAND_FAILED","details":{"reason":"provider_session_shutdown_during_allocation","provider":"browserstack","leaseId":"sm3-4130-shutdown-1793567497313","runId":"pr4130-86216483fb","providerSessionId":"9037ba0b8cdd9a77879f19eb9839e46f61c89f1b","cleanup":"confirmed","hint":"Allocate the lease again; agent-device stopped the session this allocation created."}}
{"at":"2026-11-01T21:11:59.108Z","event":"teardown outcome","by":"shutdown","status":"resolved"}
{"at":"2026-11-01T21:11:59.144Z","event":"post-delete hub probe","by":"shutdown","providerSessionId":"9037ba0b8cdd9a77879f19eb9839e46f61c89f1b","status":404}
{"at":"2026-11-01T21:11:59.144Z","event":"run PASS","head":"86216483fb980ab3d9c6fe6ddd5a64b6cfa60041","by":"shutdown","created":["9037ba0b8cdd9a77879f19eb9839e46f61c89f1b"],"deleted":["9037ba0b8cdd9a77879f19eb9839e46f61c89f1b"],"live":[],"liveCount":0}
{"at":"2026-11-01T21:11:59.144Z","event":"final owned-session accounting","by":"shutdown","created":["9037ba0b8cdd9a77879f19eb9839e46f61c89f1b"],"deleted":["9037ba0b8cdd9a77879f19eb9839e46f61c89f1b"],"live":[]}
Complete exact-head validation receipt
{
  "version": 1,
  "cwd": "/Users/thymikee/.t3/worktrees/agent-device/t3-c4bef626",
  "startedAt": "2026-11-01T21:12:53.309Z",
  "status": "passed",
  "exitCode": 0,
  "refs": {
    "base": "origin/main",
    "head": "e094faebb3",
    "resolvedBase": "343b7fcf41e2514038ad52d07aec51169e0a750e",
    "resolvedHead": "e094faebb3a6899a89b95410647fa9331ad33fa7",
    "mergeBase": "f7879cf2d266ee1dd476d0342b2819bdeceb647b"
  },
  "checkoutChanged": false,
  "exactCommit": true,
  "results": [
    {
      "id": "format",
      "status": "passed",
      "command": [
        "pnpm",
        "run",
        "format:check"
      ],
      "logPath": "/Users/thymikee/.t3/worktrees/agent-device/t3-c4bef626/.tmp/validation/run-NzjuKc/0.log",
      "exitCode": 0,
      "durationMs": 1140,
      "testReportPaths": [],
      "tests": []
    },
    {
      "id": "lint",
      "status": "passed",
      "command": [
        "pnpm",
        "run",
        "lint"
      ],
      "logPath": "/Users/thymikee/.t3/worktrees/agent-device/t3-c4bef626/.tmp/validation/run-NzjuKc/1.log",
      "exitCode": 0,
      "durationMs": 3486,
      "testReportPaths": [],
      "tests": []
    },
    {
      "id": "typecheck",
      "status": "passed",
      "command": [
        "pnpm",
        "run",
        "typecheck"
      ],
      "logPath": "/Users/thymikee/.t3/worktrees/agent-device/t3-c4bef626/.tmp/validation/run-NzjuKc/2.log",
      "exitCode": 0,
      "durationMs": 1904,
      "testReportPaths": [],
      "tests": []
    },
    {
      "id": "layering",
      "status": "passed",
      "command": [
        "pnpm",
        "run",
        "check:layering"
      ],
      "logPath": "/Users/thymikee/.t3/worktrees/agent-device/t3-c4bef626/.tmp/validation/run-NzjuKc/3.log",
      "exitCode": 0,
      "durationMs": 33227,
      "testReportPaths": [],
      "tests": []
    },
    {
      "id": "fallow",
      "status": "passed",
      "command": [
        "pnpm",
        "run",
        "check:fallow",
        "--base",
        "343b7fcf41e2514038ad52d07aec51169e0a750e"
      ],
      "logPath": "/Users/thymikee/.t3/worktrees/agent-device/t3-c4bef626/.tmp/validation/run-NzjuKc/4.log",
      "exitCode": 0,
      "durationMs": 13680,
      "testReportPaths": [],
      "tests": []
    },
    {
      "id": "build",
      "status": "passed",
      "command": [
        "pnpm",
        "run",
        "build"
      ],
      "logPath": "/Users/thymikee/.t3/worktrees/agent-device/t3-c4bef626/.tmp/validation/run-NzjuKc/5.log",
      "exitCode": 0,
      "durationMs": 10134,
      "testReportPaths": [],
      "tests": []
    },
    {
      "id": "vitest-related",
      "status": "passed",
      "command": [
        "pnpm",
        "exec",
        "vitest",
        "related",
        "--run",
        "--passWithNoTests",
        "--project=!unit-core",
        "--project=!fuzz-worker",
        "packages/provider-webdriver/src/profile-fields.fixtures.ts",
        "packages/provider-webdriver/src/runtime-session-allocation.test.ts",
        "packages/provider-webdriver/src/runtime-session.ts",
        "packages/provider-webdriver/src/runtime.ts"
      ],
      "logPath": "/Users/thymikee/.t3/worktrees/agent-device/t3-c4bef626/.tmp/validation/run-NzjuKc/6.log",
      "exitCode": 0,
      "durationMs": 56467,
      "testReportPaths": [
        "/Users/thymikee/.t3/worktrees/agent-device/t3-c4bef626/.tmp/validation/run-NzjuKc/6.tests.28070.json"
      ],
      "tests": [
        {
          "passed": 304,
          "failed": 0,
          "skipped": 0,
          "pending": 0,
          "failedModules": 0,
          "unhandledErrors": 0,
          "failures": [],
          "omittedFailures": 0
        }
      ]
    },
    {
      "id": "unit",
      "status": "passed",
      "command": [
        "pnpm",
        "run",
        "check:unit"
      ],
      "logPath": "/Users/thymikee/.t3/worktrees/agent-device/t3-c4bef626/.tmp/validation/run-NzjuKc/7.log",
      "exitCode": 0,
      "durationMs": 254131,
      "testReportPaths": [
        "/Users/thymikee/.t3/worktrees/agent-device/t3-c4bef626/.tmp/validation/run-NzjuKc/7.tests.36047.json"
      ],
      "tests": [
        {
          "passed": 16762,
          "failed": 0,
          "skipped": 1,
          "pending": 0,
          "failedModules": 0,
          "unhandledErrors": 0,
          "failures": [],
          "omittedFailures": 0
        }
      ]
    },
    {
      "id": "web-smoke",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Apple / Smoke Tests, Integration Tests, Lint, Typecheck & Package, Perf Nightly / Android Command Perf, Perf Nightly / iOS Command Perf)"
    },
    {
      "id": "replay-ios",
      "status": "skipped",
      "reason": "parked, workflow_dispatch only (Replay Manual / iOS Replay Suite)"
    },
    {
      "id": "replay-ios-device",
      "status": "skipped",
      "reason": "parked, workflow_dispatch only (Replay Manual / iOS Replay Suite)"
    },
    {
      "id": "replay-macos",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Apple / Smoke Tests)"
    },
    {
      "id": "replay-linux",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Linux / Smoke Tests)"
    },
    {
      "id": "linux-command-evidence",
      "status": "skipped",
      "reason": "GitHub-authoritative (jobs: Linux / Smoke Tests)"
    },
    {
      "id": "replay-android",
      "status": "skipped",
      "reason": "parked, workflow_dispatch only (Replay Manual / Android Full Emulator Suite)"
    }
  ],
  "initial": {
    "head": "e094faebb3a6899a89b95410647fa9331ad33fa7",
    "status": "",
    "fingerprint": "e292d347bb696042f040fc1394e4e87be89d052e96cfcbcd6a4964d2b2ca3190"
  },
  "changedFiles": [
    "packages/provider-webdriver/src/profile-fields.fixtures.ts",
    "packages/provider-webdriver/src/runtime-session-allocation.test.ts",
    "packages/provider-webdriver/src/runtime-session.ts",
    "packages/provider-webdriver/src/runtime.ts"
  ],
  "plan": {
    "failOpen": false,
    "checks": [
      "format",
      "lint",
      "typecheck",
      "layering",
      "fallow",
      "build",
      "vitest-related",
      "unit",
      "web-smoke",
      "replay-ios",
      "replay-ios-device",
      "replay-macos",
      "replay-linux",
      "linux-command-evidence",
      "replay-android"
    ],
    "reasons": [
      {
        "check": "format",
        "path": "packages/provider-webdriver/src/profile-fields.fixtures.ts",
        "rule": "gate:format",
        "detail": "oxfmt covers packages/"
      },
      {
        "check": "lint",
        "path": "packages/provider-webdriver/src/profile-fields.fixtures.ts",
        "rule": "gate:lint",
        "detail": "oxlint covers packages/"
      },
      {
        "check": "typecheck",
        "path": "packages/provider-webdriver/src/profile-fields.fixtures.ts",
        "rule": "gate:typecheck",
        "detail": "tsc includes packages/"
      },
      {
        "check": "fallow",
        "path": "packages/provider-webdriver/src/profile-fields.fixtures.ts",
        "rule": "gate:fallow",
        "detail": "fallow audits changed TypeScript for dead code"
      },
      {
        "check": "layering",
        "path": "packages/provider-webdriver/src/profile-fields.fixtures.ts",
        "rule": "package-src",
        "detail": "layering R11 guards workspace package boundaries"
      },
      {
        "check": "vitest-related",
        "path": "packages/provider-webdriver/src/profile-fields.fixtures.ts",
        "rule": "vitest:related",
        "detail": "Vitest resolves affected tests through its static module graph"
      },
      {
        "check": "build",
        "path": "packages/provider-webdriver/src/profile-fields.fixtures.ts",
        "rule": "package-src",
        "detail": "package source is bundled into the published artifact"
      },
      {
        "check": "unit",
        "path": "packages/provider-webdriver/src/profile-fields.fixtures.ts",
        "rule": "own:unit-text-gates",
        "detail": "unit-suite gates read production sources as text, which the Vitest module graph cannot trace"
      },
      {
        "check": "replay-ios",
        "path": "packages/provider-webdriver/src/profile-fields.fixtures.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "replay-ios-device",
        "path": "packages/provider-webdriver/src/profile-fields.fixtures.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "replay-macos",
        "path": "packages/provider-webdriver/src/profile-fields.fixtures.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "replay-android",
        "path": "packages/provider-webdriver/src/profile-fields.fixtures.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "replay-linux",
        "path": "packages/provider-webdriver/src/profile-fields.fixtures.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "linux-command-evidence",
        "path": "packages/provider-webdriver/src/profile-fields.fixtures.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "web-smoke",
        "path": "packages/provider-webdriver/src/profile-fields.fixtures.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "format",
        "path": "packages/provider-webdriver/src/runtime-session-allocation.test.ts",
        "rule": "gate:format",
        "detail": "oxfmt covers packages/"
      },
      {
        "check": "lint",
        "path": "packages/provider-webdriver/src/runtime-session-allocation.test.ts",
        "rule": "gate:lint",
        "detail": "oxlint covers packages/"
      },
      {
        "check": "typecheck",
        "path": "packages/provider-webdriver/src/runtime-session-allocation.test.ts",
        "rule": "gate:typecheck",
        "detail": "tsc includes packages/"
      },
      {
        "check": "fallow",
        "path": "packages/provider-webdriver/src/runtime-session-allocation.test.ts",
        "rule": "gate:fallow",
        "detail": "fallow audits changed TypeScript for dead code"
      },
      {
        "check": "layering",
        "path": "packages/provider-webdriver/src/runtime-session-allocation.test.ts",
        "rule": "package-src",
        "detail": "layering R11 guards workspace package boundaries"
      },
      {
        "check": "vitest-related",
        "path": "packages/provider-webdriver/src/runtime-session-allocation.test.ts",
        "rule": "vitest:related",
        "detail": "Vitest resolves affected tests through its static module graph"
      },
      {
        "check": "format",
        "path": "packages/provider-webdriver/src/runtime-session.ts",
        "rule": "gate:format",
        "detail": "oxfmt covers packages/"
      },
      {
        "check": "lint",
        "path": "packages/provider-webdriver/src/runtime-session.ts",
        "rule": "gate:lint",
        "detail": "oxlint covers packages/"
      },
      {
        "check": "typecheck",
        "path": "packages/provider-webdriver/src/runtime-session.ts",
        "rule": "gate:typecheck",
        "detail": "tsc includes packages/"
      },
      {
        "check": "fallow",
        "path": "packages/provider-webdriver/src/runtime-session.ts",
        "rule": "gate:fallow",
        "detail": "fallow audits changed TypeScript for dead code"
      },
      {
        "check": "layering",
        "path": "packages/provider-webdriver/src/runtime-session.ts",
        "rule": "package-src",
        "detail": "layering R11 guards workspace package boundaries"
      },
      {
        "check": "vitest-related",
        "path": "packages/provider-webdriver/src/runtime-session.ts",
        "rule": "vitest:related",
        "detail": "Vitest resolves affected tests through its static module graph"
      },
      {
        "check": "build",
        "path": "packages/provider-webdriver/src/runtime-session.ts",
        "rule": "package-src",
        "detail": "package source is bundled into the published artifact"
      },
      {
        "check": "unit",
        "path": "packages/provider-webdriver/src/runtime-session.ts",
        "rule": "own:unit-text-gates",
        "detail": "unit-suite gates read production sources as text, which the Vitest module graph cannot trace"
      },
      {
        "check": "replay-ios",
        "path": "packages/provider-webdriver/src/runtime-session.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "replay-ios-device",
        "path": "packages/provider-webdriver/src/runtime-session.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "replay-macos",
        "path": "packages/provider-webdriver/src/runtime-session.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "replay-android",
        "path": "packages/provider-webdriver/src/runtime-session.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "replay-linux",
        "path": "packages/provider-webdriver/src/runtime-session.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "linux-command-evidence",
        "path": "packages/provider-webdriver/src/runtime-session.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "web-smoke",
        "path": "packages/provider-webdriver/src/runtime-session.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "format",
        "path": "packages/provider-webdriver/src/runtime.ts",
        "rule": "gate:format",
        "detail": "oxfmt covers packages/"
      },
      {
        "check": "lint",
        "path": "packages/provider-webdriver/src/runtime.ts",
        "rule": "gate:lint",
        "detail": "oxlint covers packages/"
      },
      {
        "check": "typecheck",
        "path": "packages/provider-webdriver/src/runtime.ts",
        "rule": "gate:typecheck",
        "detail": "tsc includes packages/"
      },
      {
        "check": "fallow",
        "path": "packages/provider-webdriver/src/runtime.ts",
        "rule": "gate:fallow",
        "detail": "fallow audits changed TypeScript for dead code"
      },
      {
        "check": "layering",
        "path": "packages/provider-webdriver/src/runtime.ts",
        "rule": "package-src",
        "detail": "layering R11 guards workspace package boundaries"
      },
      {
        "check": "vitest-related",
        "path": "packages/provider-webdriver/src/runtime.ts",
        "rule": "vitest:related",
        "detail": "Vitest resolves affected tests through its static module graph"
      },
      {
        "check": "build",
        "path": "packages/provider-webdriver/src/runtime.ts",
        "rule": "package-src",
        "detail": "package source is bundled into the published artifact"
      },
      {
        "check": "unit",
        "path": "packages/provider-webdriver/src/runtime.ts",
        "rule": "own:unit-text-gates",
        "detail": "unit-suite gates read production sources as text, which the Vitest module graph cannot trace"
      },
      {
        "check": "replay-ios",
        "path": "packages/provider-webdriver/src/runtime.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "replay-ios-device",
        "path": "packages/provider-webdriver/src/runtime.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "replay-macos",
        "path": "packages/provider-webdriver/src/runtime.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "replay-android",
        "path": "packages/provider-webdriver/src/runtime.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "replay-linux",
        "path": "packages/provider-webdriver/src/runtime.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "linux-command-evidence",
        "path": "packages/provider-webdriver/src/runtime.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      },
      {
        "check": "web-smoke",
        "path": "packages/provider-webdriver/src/runtime.ts",
        "rule": "own:device-lane:shared",
        "detail": "shared runtime surface: every device lane drives the CLI and daemon through it"
      }
    ],
    "failOpenReasons": [],
    "docsOnlyPaths": []
  },
  "final": {
    "head": "e094faebb3a6899a89b95410647fa9331ad33fa7",
    "status": "",
    "fingerprint": "e292d347bb696042f040fc1394e4e87be89d052e96cfcbcd6a4964d2b2ca3190"
  },
  "finishedAt": "2026-11-01T22:22:21.378Z"
}

@thymikee

thymikee commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

I reviewed e094fae and found no code problems. The earlier review at 8621648 asked for live evidence on the facade route, and the author-supplied runs now cover it, so that request is met. The delta since 8621648 changes comments only in runtime-session.ts and runtime.ts.

The live runs are author logs from a harness I could not rerun, and I did not query BrowserStack to confirm both session ids are ended. They exercise createCloudWebDriverRuntime with no-app Android capabilities, not daemon dispatch or the app-upload prepareSession path. They ran at 8621648, which is fine here because the delta is comments only. I did not run the tests myself.

On the open inline threads: the failed-create cleanup thread (#4130 (comment)) still applies, and two lower-priority threads still apply. These threads do not apply and can be resolved: #4130 (comment) (the post-shutdown refusal carries cleanup 'none' and its own restart hint), #4130 (comment) (a shutdown() rejection lands in plugins.failed and other plugins still close), #4130 (comment) (the owned-id add runs before shutdown's clear), #4130 (comment) ('uncertain' after a failed hand-back is deliberate), #4130 (comment) (the duplicate 30_000 value is the same and deferred), #4130 (comment) (each cleanupWarnings shape is lossless for its case), and #4130 (comment) (the docs now match release()).

Smoke Tests, Repo Guards & Compatibility and Coverage are still running. All 19 checks were green at 8621648, and no code on their routes changed since. Once those three finish green at e094fae, nothing else stands in the way of merge.

@thymikee thymikee added the ready-for-human Valid work that needs human implementation, judgment, or maintainer merge label Nov 1, 2026
@thymikee
thymikee merged commit d121599 into main Nov 2, 2026
18 of 19 checks passed
@thymikee
thymikee deleted the fix/sm3-webdriver-single-allocation branch November 2, 2026 06:38
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.

1 participant