Repository navigation
Conversation
resolvePath() passed path.slice(1) to resolve(), so "~/agent-skills"
became resolve(home, "/agent-skills"), which is "/agent-skills": an
EGO_BROWSER_AGENT_WORKSPACE written with ~/ pointed at the filesystem
root. "~other" was also rewritten to "$HOME/other". Only "~" and "~/..."
("~\..." on Windows) now expand; anything else resolves as written.
Prettier also rewraps one statement in loadEnvFile; the quality gate
checks every changed file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RegExp.prototype.test() on a /g or /y pattern leaves lastIndex at the end of the last match. Passing the same pattern to another waitForURL() made its first check start part-way into the URL and fail on a page that matched; the failure reset lastIndex, so the wait only matched on the next poll, 25 ms later, and returned false when the timeout was shorter than that. Reset lastIndex before each test, as networkMatches() in the same file already does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…resh ensureRefMapForRef() guarded the automatic re-snapshot with a boolean. A second @ref action that arrived while the first refresh was in flight returned at once and resolved against the still-empty map, failing with "Unknown ref". Share the in-flight refresh instead: every waiter gets its result or its error, and the next action after a failure retries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
locator.evaluate() and evaluateAll() on an @ref called Runtime.callFunctionOn with awaitPromise: false, so an async pageFunction returned its Promise serialized as {} and a rejection was lost. evaluateAll() over CSS and role locators already awaited. resolveAndCall() takes an awaitPromise option and these two reads set it; every other element action keeps its current options. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parseLocator() stripped one internal:nth= prefix and then one
internal:last prefix, so locator(".item").nth(1).first() left
"internal:nth=1;loc=css:.item" to be parsed as CSS and failed with
"Invalid selector"; nth(1).last() and last().last() failed the same way.
Peel every selection prefix. Each selection leaves at most one element,
so the innermost one picks the element and a role locator keeps its AX
path. An outer nth(n) with n > 0 can only match nothing; it falls back to
the full query, which reports a transient miss.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…egan The completion waiter accepted any GUID until the awaiting code had resumed and recorded the begin event. A completion or cancel of another download could therefore end the wait: the facade came back before its own file was written, or the wrong download was reported as canceled. The begin and completion waiters each had their own timer. When one wait failed (timeout, or ensureSession()/setDownloadBehavior() rejecting) the other stayed registered and rejected later with no handler. Under Node's default unhandled-rejection mode that ends the process, even when the script caught the error from waitForEvent(). Record the GUID synchronously in the begin-event callback, and drop both subscriptions and the single timer however the wait ends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fill() ignored exceptionDetails from its focus, clear and input/change calls; cdp() reports a function that throws in the page as a result, not a rejection. On a target that is neither a form field nor contenteditable, the clear step's "fill target is not editable" was dropped and Input.insertText typed the value into whatever element had focus. Check each call the way resolveAndCall() does. The handle is still released by withHandle(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
startScreencast() set activeRecording only after its awaits. Two overlapping starts both succeeded and the first recording was never stopped; stopScreencast() during startup found nothing to stop and the recording then ran on. stopScreencast() cleared the slot before finalizing, so a second stop() resolved before the file was written and a new start() could begin meanwhile. Reserve the slot before the first await, keep it until finalization ends, and share one stop promise among callers. A startup failure after the encoder was created now stops the encoder too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Eight independent defects in the
ego-browserruntime, one commit each. Every commit adds tests that fail againstmainand pass with its fix. The commits touch disjoint files, so each one can be reviewed or reverted on its own.Related issue
None: issues are disabled on this repository, so each defect is described below.
Changes
1.
~/paths lost the home directory (src/env.ts)resolvePath("~/agent-skills")calledresolve(home, "/agent-skills"), which returns/agent-skills. AnEGO_BROWSER_AGENT_WORKSPACEwritten with~/therefore pointed at the filesystem root, and~otherwas rewritten to$HOME/other. Now only~and~/…(~\…on Windows) expand, by joining the rest onto the home directory, so~//skillsworks too; any other path resolves as written. Prettier also rewraps one statement inloadEnvFile, because the quality gate checks whole changed files.On
main,resolvePath("~/skills/ego-browser")returns/skills/ego-browser.2.
waitForURLmissed a match on the second use of a/gor/ypattern (src/driver/waits.ts)RegExp.prototype.test()leaveslastIndexof a global or sticky pattern at the end of the last match. When the same pattern was passed to anotherwaitForURL(), its first check started part-way into the URL and failed on a page that matched. That failure resetlastIndex, so the wait matched only on the next poll, 25 ms later, and returnedfalseif the timeout ran out first.lastIndexis now reset before each test, asnetworkMatches()in the same file already does.On
main, with a 20 ms timeout, the secondwaitForURL(/target/g)on the same URL returnsfalse.3. Concurrent first
@refactions failed with "Unknown ref" (src/ref-state.ts)When the ref map is empty, the first
@refaction takes a snapshot to rebuild it. The guard was a boolean, so a second action that arrived during that refresh returned at once and resolved against the still-empty map. The in-flight refresh is now shared. Every caller waits for it and gets its error if it fails, and the next action after a failure retries.On
main, the second of two concurrentelementCenter("@7")calls rejects withUnknown ref: 7.4. Async callbacks in
locator.evaluate()andevaluateAll()on refs returned{}(src/driver/locator.ts,src/driver/element-ops.ts)Both paths called
Runtime.callFunctionOnwithawaitPromise: false. An asyncpageFunctiontherefore returned its Promise serialized as{}, and a rejection inside it was lost.evaluateAll()over CSS and role locators already awaited.resolveAndCall()gains anawaitPromiseoption, set only by these two reads; every other element action keeps its current options.On
main, an async callback that resolves to"Ready"yields{}, and one that throws does not reject.5. Chained
nth(),first()andlast()failed with "Invalid selector" (src/element-resolver.ts)parseLocator()removed oneinternal:nth=prefix and then oneinternal:last;prefix.page.locator(".item").nth(1).first()thus leftinternal:nth=1;loc=css:.itemto be parsed as CSS;nth(1).last()andlast().last()failed the same way. It now removes every selection prefix. Each selection leaves at most one element, so the innermost one picks the element, and a role locator keeps its AX path. An outernth(n)withn > 0can only match nothing; it falls back to the full query, which reports a transient miss.On
main,internal:nth=0;internal:nth=1;loc=css:.itemfails withInvalid selector: internal:nth=1;loc=css:.item.6.
waitForEvent('download')could end on another download, and a caught failure could still end the process (src/driver/downloads.ts)The completion waiter accepted any GUID until the awaiting code had resumed and recorded the begin event. A completion or cancel of a different download could therefore end the wait: the facade came back before its own file was written, or the wrong download was reported as canceled.
The begin and completion waiters each had their own timer. When the wait failed (a timeout, or
ensureSession()orsetDownloadBehavior()rejecting), the other waiter stayed registered and rejected later with no handler. Under Node's default unhandled-rejection mode that ends the process, even when the script caught the error:The GUID is now recorded synchronously in the begin-event callback, and both subscriptions and the single timer are removed however the wait ends.
7.
fill()typed into the focused element when a page-side step threw (src/driver/keyboard.ts)cdp()reports a function that throws in the page as a result carryingexceptionDetails, not as a rejection, andfill()never checked its threeRuntime.callFunctionOncalls. On a target that is neither a form field nor contenteditable, the clear step'sfill target is not editablewas dropped, andInput.insertTextsent the value to whatever element had focus. Each step is now checked the wayresolveAndCall()already does, sofill()rejects with the page error; the handle is still released. WithclearFirst: falsethe clear step, and with it that check, does not run; that path is unchanged.On
main,fill()resolves even when the focus, clear or event step throws.8. Screencast start and stop raced (
src/driver/screencast.ts)startScreencast()claimed the recording slot only after its awaits. Two overlapping starts both succeeded and the first recording was never stopped; astopScreencast()during startup found nothing to stop, and the recording then ran on.stopScreencast()released the slot before finalizing, so a secondstop()resolved before the WebM was written and a newstart()could begin in the meantime.The slot is now reserved before the first await and held until finalization ends, and concurrent stops share one promise. A startup failure after the encoder was created now stops the encoder too.
On
main, the second of two overlapping starts is fulfilled instead of rejected, and an earlystop()resolves before startup finishes.Verification
Run from
package/ego-browseron Node 22 or later.mainnpm test(build, typecheck, tests)main's sourcenpm testat each of the 8 commitsnpm run validate:site-skillsprettier --checkon the changed filesnpm testinpackage/ego-linuxon this build, as thetest-linuxjob runs it¹
startScreencast releases its reservation after session startup failsalready passes onmain; it covers the new reservation path.Impact
Public helper behavior changes in three places:
fill()rejects when one of its page-side steps throws, for example on a target that is not editable, instead of typing into another element.locator.evaluate()andevaluateAll()on an@refresolve to an async callback's value.~but not~/(for example~other) is no longer rewritten to$HOME/other.No agent-facing documentation, site learning, installation flow or CI changes.
🤖 Generated with Claude Code