fix(browser): a stopped turn is not a broken browser - #140
aniruddhaadak80 wants to merge 1 commit into
Conversation
openOwned records the session as broken on any failure:
} catch (error) {
await this.save(owner, { ...value, url: target, status: "error", ... }, id);
throw error;
}
One of those failures is the person asking for the turn to end. request
reports a cancelled request by throwing the signal's own abort, so a Stop
that lands while the worker is still opening the session wrote "error" to a
browser whose profile, cookies and storage are all fine.
That status is not cosmetic. The app reads it as "Needs attention", and it is
what hides the preview behind a fallback and offers "Reconnect browser"
instead of "Take control" - so stopping a turn left the session demanding a
reconnect the person did not need, and the error survived a restart because
it is persisted.
Only the caller's own abort is treated as a Stop. The 45s request timeout is
a real failure and leaves the signal untouched, so it still records, and the
new test for that side is what keeps this from becoming "never record an
error".
browserFixture's handler is now awaited so a test can model a worker that has
not answered yet, which is the state a Stop actually arrives into.
|
Closing this: #31 already fixes this exact defect, and does it with less code than mine. #31 adds } catch (error) {
+ signal?.throwIfAborted();
await this.save(
owner,
{ ...value, url: target, status: "error", updatedAt: new Date().toISOString() },
id,
);so a caller's Stop re-throws before anything is persisted. That is the same outcome as the I only noticed the overlap at the end, when I stopped trusting my earlier recon and listed the files of every open PR rather than the 60 most recent. That is on me: my first pass read as "no collisions" because the list I checked was truncated. Two things from my branch that #31 does not obviously have, offered rather than pushed:
Neither is worth a competing PR on its own, but say the word if either is useful and I will open it as a focused follow-up. |
What this changes
BrowserService.openOwnedrecords the session as broken on any failure:One of those failures is the person asking for the turn to end.
requestreports a cancelled request by throwing the signal's own abort (browser.ts:67and:84), so a Stop that lands while the worker is still opening the session wrotestatus: "error"to a browser whose profile, cookies and storage were all fine.That status is not cosmetic. In
apps/mobile/src/computer.tsxit is what::78-79):84,:104):111-112)So stopping a turn left the session demanding a reconnect nobody needed — and because the status is persisted, it survived a restart.
The other half of the rule
Only the caller's own abort is treated as a Stop:
requestcomposesAbortSignal.any([signal, AbortSignal.timeout(45000)]), so a request can also fail on the 45-second timeout. That is a real failure and it leaves the caller's signal untouched, so it still records the error — which is what the second new test pins down. Without that test this change could quietly become "never record an error", which would hide a browser that really is broken.Test fixture change
browserFixture's handler is nowawaited, so a test can model a worker that has not answered yet — which is the state a Stop actually arrives into. This is additive: existing handlers are synchronous, and awaiting a non-promise changes nothing for them. It was necessary because the pre-flightsignal?.throwIfAborted()at the top ofobserveForThread(browser.ts:182) and insideserial(:202) means an already-aborted signal never reachesopenOwnedat all, so the only way to reach the cancellation path is to abort while the request is genuinely in flight.Verification
pnpm test— baseline onmain: 340 tests, 338 pass, 0 fail, 2 skipped. After: 342 tests, 340 pass, 0 fail, 2 skipped. The two new ones intests/browser.test.ts:stopping a turn does not leave the browser marked as needing attention— the regression. It polls until the worker has actually received the request, aborts, and asserts the stored status is still"idle". I confirmed red first: againstmainit failed withexpected: 'idle' / actual: 'error', repeatedly (3/3 runs), and passes here (2/2 runs). The existing cancellation test attests/browser.test.ts:245does not catch this — it aborts inside the fetch handler after the response was already built, soopenOwnedsucceeds and the catch never runs, and it asserts a row count rather than a status.a worker that genuinely fails a turn still marks the browser as needing attention— a 500 from the worker still records"error".pnpm typecheck— exit 0.pnpm exec biome checkon the three changed files — clean.Two honest notes:
pnpm lintover the whole repo fails here onmaintoo (143 CRLF errors fromcore.autocrlf=trueon this Windows checkout). The three files I touched pass individually.worker protects all controls, validates before launch, and health reveals no sessionsfail once and then pass on three subsequent runs, including on unmodifiedmain. I could not tie it to this change and believe it is pre-existing flakiness in that test (it starts a realcreateWorkerServerand binds a port), but I am flagging it rather than assuming it away.Relationship to open PRs
browse_web.None of them touch
openOwned's catch.