Compare commits

...
Author SHA1 Message Date
leeguooooo 7159012173 chore(release): bump to 0.27.0-fork.6 — defensive-guard timeouts + accurate CDP tip 2026-05-09 10:04:25 +09:00
leeguooooo 1b3d41e579 fix(timeout): cap defensive CDP guards so click can't hang multi-minute
Reported: a single `click @ref` could hang 5+ minutes, with multiple
queued click invocations adding up to 7+ minutes — worst case 30s
timeout × 3 CDP calls × N parallel processes:

  - verify_ref_identity (Accessibility.getPartialAXTree)  →  default 30s
  - resolveNode / getBoxModel                              →  default 30s
  - wait_for_paint_settled (Runtime.evaluate awaitPromise) →  default 30s

The latter two are best-effort defenses added in fork.3-5 to fix SPA
race / DOM-reuse bugs. They should never block a real click for
30s — the unguarded code path was always faster than the guarded
path-that-hangs.

  - verify_ref_identity   capped at 1s   (skips check on timeout)
  - wait_for_paint_settled capped at 500ms (skips wait on timeout)

Both skip-on-timeout intentionally: the worst case is the click
behaves like fork.2 (race-prone but fast), which is strictly better
than the user pkilling stuck processes.

Also rewrites the misleading "Chrome 144+ chrome://inspect tip" in
the auto-connect failure message — the toggle exposes target
discovery only, not the /json/version HTTP API the auto-connect
flow expects (verified by user: lsof shows :9222 listening but
curl /json/version returns 404).
2026-05-09 10:04:14 +09:00
leeguooooo dbf272ced7 fix(docker): catch parallel-build failures + stop using glob in cp
Two latent bugs in the release pipeline that conspired to ship a stale
linux-x64 binary in 0.27.0-fork.5 (only caught by manually grepping
the embedded version string):

1. build-linux ran x64 and arm64 in parallel and used a single
   `wait $PID1 $PID2` to join them. That command waits for both, but
   its exit code is the LAST waited pid only — so if x64 silently
   broke and arm64 succeeded, the outer script exited 0 and shipped
   whatever was already in /output from the previous release. Now we
   wait on each pid individually and exit 1 on either failure.

2. build-single's cp used `agent-browser*` which globs to BOTH the
   binary and its `.d` dependency file. When two sources are passed,
   cp requires the destination to be a directory. We weren't, so cp
   exited non-zero with "Not a directory" and the build script
   shrugged it off because the next line was `chmod ... || true`.
   Now we resolve a single explicit source path.
2026-05-09 04:30:20 +09:00
7 changed files with 53 additions and 17 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ dependencies = [
[[package]]
name = "agent-browser-stealth"
version = "0.27.0-fork.5"
version = "0.27.0-fork.6"
dependencies = [
"aes-gcm",
"async-trait",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "agent-browser-stealth"
version = "0.27.0-fork.5"
version = "0.27.0-fork.6"
edition = "2021"
description = "Fast browser automation CLI for AI agents"
license = "Apache-2.0"
+6 -4
View File
@@ -1607,8 +1607,9 @@ async fn auto_launch(state: &mut DaemonState) -> Result<(), String> {
To let agent-browser work with your existing Chrome (recommended):\n\
{}\n\n\
Or start a standalone browser with: agent-browser --launch open <url>\n\n\
Tip: On Chrome 144+, you can enable CDP without restarting:\n\
Open chrome://inspect/#remote-debugging and toggle it on.",
Note: chrome://inspect/#remote-debugging only enables remote *target discovery* — \
it does NOT expose the standard CDP HTTP API on /json/version. \
A full restart with --remote-debugging-port=<port> is required.",
chrome_relaunch_hint(),
));
}
@@ -2135,8 +2136,9 @@ async fn handle_launch(cmd: &Value, state: &mut DaemonState) -> Result<Value, St
To let agent-browser work with your existing Chrome (recommended):\n\
{}\n\n\
Or start a standalone browser with: agent-browser --launch open <url>\n\n\
Tip: On Chrome 144+, you can enable CDP without restarting:\n\
Open chrome://inspect/#remote-debugging and toggle it on.",
Note: chrome://inspect/#remote-debugging only enables remote *target discovery* — \
it does NOT expose the standard CDP HTTP API on /json/version. \
A full restart with --remote-debugging-port=<port> is required.",
chrome_relaunch_hint(),
));
}
+17 -3
View File
@@ -396,9 +396,23 @@ async fn verify_ref_identity(
"backendNodeId": backend_node_id,
"fetchRelatives": false,
});
let resp: Result<GetFullAXTreeResult, String> = client
.send_command_typed("Accessibility.getPartialAXTree", &params, Some(session_id))
.await;
// Tight 1s timeout: this is a defensive guard, not a critical path.
// The default 30s CDP timeout was the dominant factor in the
// "click hangs 5+ minutes" report — three CDP calls (verify +
// resolveNode + paint-settle) at 30s each, multiplied by parallel
// click invocations queueing on the daemon, totalled multi-minute
// user-visible hangs. Cap our own helper so a stuck AX query
// doesn't make `click` worse than the no-guard version was.
let resp: Result<GetFullAXTreeResult, String> = match tokio::time::timeout(
std::time::Duration::from_secs(1),
client.send_command_typed("Accessibility.getPartialAXTree", &params, Some(session_id)),
)
.await
{
Ok(r) => r,
// Timeout: skip identity verification rather than block the click.
Err(_) => return Ok(()),
};
let Ok(tree) = resp else {
// Node likely gone; let the box-model call fail and trigger fallback.
return Ok(());
+12 -4
View File
@@ -903,8 +903,15 @@ async fn wait_for_paint_settled(client: &CdpClient, session_id: &str) {
requestAnimationFrame(() => \
requestAnimationFrame(() => \
queueMicrotask(() => resolve(true)))))";
let _ = client
.send_command_typed::<_, Value>(
// Tight 500ms timeout. RAF normally fires at 16ms, two RAFs total ~33ms.
// If the tab is hidden / throttled / page is doing something pathological
// and RAF doesn't fire in 500ms, we'd rather return now than stall the
// user's click. Without this cap, a stuck RAF inherited the default 30s
// CDP timeout and was the main contributor to the "click hangs 5+ min"
// user report.
let _ = tokio::time::timeout(
std::time::Duration::from_millis(500),
client.send_command_typed::<_, Value>(
"Runtime.evaluate",
&EvaluateParams {
expression: script.to_string(),
@@ -912,8 +919,9 @@ async fn wait_for_paint_settled(client: &CdpClient, session_id: &str) {
await_promise: Some(true),
},
Some(session_id),
)
.await;
),
)
.await;
}
async fn dispatch_click(
+15 -3
View File
@@ -25,8 +25,14 @@ services:
(echo "→ Linux ARM64" && cargo zigbuild --release --target aarch64-unknown-linux-gnu && cp /build/target/aarch64-unknown-linux-gnu/release/agent-browser /output/agent-browser-linux-arm64 && chmod +x /output/agent-browser-linux-arm64 && echo "✓ Linux ARM64 done") &
PID2=$!
# Wait for both to complete
wait $PID1 $PID2
# Wait for both and check exit codes individually — without this
# the outer script exits 0 even if one of the parallel builds
# failed, silently leaving a stale binary in /output from the
# previous release. Caused 0.27.0-fork.5 to ship with a stale
# linux-x64 binary at the first publish attempt until caught
# manually by checking the embedded version string.
wait $PID1 || { echo "✗ Linux x64 build failed"; exit 1; }
wait $PID2 || { echo "✗ Linux ARM64 build failed"; exit 1; }
echo ""
echo "✓ Linux platforms built successfully!"
@@ -67,8 +73,14 @@ services:
- OUTPUT_NAME=${OUTPUT_NAME:-agent-browser-linux-x64}
command: |
-c '
set -e
cargo zigbuild --release --target $TARGET
cp /build/target/$TARGET/release/agent-browser* /output/$OUTPUT_NAME
# Copy the binary explicitly. The previous `agent-browser*` glob
# matched the binary AND its `.d` dependency file, which made cp
# treat the destination as a directory and silently failed.
SRC="/build/target/$TARGET/release/agent-browser"
if [ -f "$SRC.exe" ]; then SRC="$SRC.exe"; fi
cp "$SRC" "/output/$OUTPUT_NAME"
chmod +x /output/$OUTPUT_NAME 2>/dev/null || true
echo "✓ Built $OUTPUT_NAME"
'
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "agent-browser-stealth",
"version": "0.27.0-fork.5",
"version": "0.27.0-fork.6",
"description": "Browser automation CLI for AI agents — stealth fork with anti-detection",
"type": "module",
"files": [