Restore profile directory cleanup on close, but only for non-default
profiles. The default profile ("actionbook") preserves user state
(cookies, localStorage) across sessions. Test-generated profiles
(e.g. "profile-s-12345-0") are cleaned up automatically.
Profile deletion happens after Chrome has fully exited to avoid
EBUSY from held locks.
- browser.rs: replace inline child.kill() (no wait) with
chrome_reaper::kill_and_reap() to prevent zombie on launch timeout
- chrome_reaper.rs: remove misleading setsid reference from doc comment
Chrome processes were not being cleaned up in several scenarios:
- SessionEntry had no Drop impl, so dropping it leaked the Chrome process
- CdpSession had no close() method, so background reader/writer tasks leaked
- close/restart/shutdown held the registry lock during slow CDP/Chrome cleanup
- child.wait() in spawn_blocking was fire-and-forget (never awaited)
Changes:
- Add chrome_reaper module with centralized kill_and_reap helpers
- Add Drop impl on SessionEntry as last-resort backstop
- Add CdpSession::close() that drops the writer channel and fails pending requests
- Move all slow I/O (cdp.close(), Chrome kill/reap) outside registry lock
- Use kill_and_reap_async (awaited) instead of fire-and-forget spawn_blocking
- kill_and_reap_option takes ownership (sets to None) to prevent double-cleanup
The clap argument descriptions for <SELECTOR> fields now read
"Selector (CSS, XPath, or @ref)" instead of generic "Target element
selector" or "CSS selector". Updated 19 command structs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace exact BUILD_VERSION match with major_minor() comparison.
1.0.x-<hash> variants are now compatible as long as major.minor match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Every command that accepts a <selector> now shows an @eN ref example
and mentions that refs come from snapshot output. Updated 18 commands
across interaction and observation groups.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All equivalent invocations now show the same custom help:
actionbook / actionbook help / actionbook --help / actionbook -h
actionbook browser / actionbook browser help / actionbook browser --help
Pre-parse interception catches these before clap can emit its
auto-generated flat listing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both `actionbook --help` and `actionbook -h` now show our custom
agent-friendly help instead of clap's auto-generated output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
fix: unify browser --help with grouped help and add new command docs
- Intercept `browser --help` to show grouped listing instead of clap's
flat ungrouped output. Both `browser help` and `browser --help` now
show the same grouped view.
- Add after_help examples to 5 new observation commands: attrs, box,
styles, describe, state.
- Update grouped help listing with all 40 browser commands.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Add isSignificant(e) check in the nearby JS: keep a node only if it has
an explicit accessible name (aria-label, labelledby, label[for], closest
label), direct text node children, or is an interactive element
(button/a/input/select/textarea). Structural wrapper divs/spans with no
own text and no name are filtered to null (parent/sibling) or excluded
(children).
Uses a dedicated gna() helper (like gn() but without the innerText
fallback) so descendant text doesn't make empty wrappers appear
significant.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes from dual code review (code-reviewer + codex challenge):
- P0: Invalid ACTIONBOOK_DAEMON_IDLE_TIMEOUT_SECS now falls back to default
instead of silently disabling timeout
- P1: Replace std::thread::sleep with tokio::time::sleep in async fn
- P1: Extract parse_idle_timeout() pure function for race-free unit testing
- P2: Add ACTIONBOOK_DAEMON_HOUSEKEEPING_INTERVAL_SECS for fast E2E tests
(idle timeout tests now run in 12s instead of 72s)
- P2: Remove manual stale file cleanup from crash_recovery test
- Minor: Handle EINTR in flock with retry loop
Summary now appends [disabled], [checked], [selected] qualifiers based
on element state. Multiple qualifiers use comma separation: [disabled, checked].
Default states (enabled, unchecked, unselected) produce no qualifier.
Changes:
- describe.rs gn(): add label lookup via label[for] and closest('label')
so checkbox/radio names resolve to associated label text
- describe.rs gst(): add checked and selected to state object
- describe.rs summary builder: append qualifiers from state
- output.rs: use pre-computed summary field from response data
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the PID file + kill(pid, 0) singleton mechanism with flock-based
locking. The kernel guarantees flock atomicity (no TOCTOU race) and
auto-releases on process exit (including SIGKILL / panic=abort), solving
the root cause of 30+ orphaned daemon processes accumulating over time.
Changes:
- Use flock(LOCK_EX | LOCK_NB) on PID file for daemon singleton
- is_daemon_running() now probes flock instead of PID + kill(pid, 0)
- Losing daemon retries 3 times, checks if winner's socket is ready
- Add idle timeout (default 30min, ACTIONBOOK_DAEMON_IDLE_TIMEOUT_SECS)
- Add has_active_sessions() helper to SessionRegistry
- Add unit tests for flock, idle timeout config, registry
- Add E2E tests for concurrent start, idle timeout, crash recovery
- cargo fmt: reformat tracing::warn! in close.rs per rustfmt rules
- Remove resolve_tab_ws_url() from daemon/cdp.rs: function was never
called and contained a stale tab.id.0 reference (short ID) where
the WS URL needs the native CDP target ID
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Short tab IDs (t1, t2, ...) are user-facing labels, not CDP target IDs.
Three call sites were incorrectly passing the short ID to CDP methods:
- close.rs: extract native_id from registry before lock release;
use it for cdp.detach() and Target.closeTarget
- cookies/{list,get,set,delete,clear}.rs: use t.native_id instead of
t.id.0 when looking up the target for execute_on_tab
- session/start.rs ReuseTarget: add first_native_id field (CDP target
ID); use it for Page.navigate, /json/list lookup, and cloud registry
lookup; keep first_tab_id (short ID) for JSON response only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- TabEntry gains native_id (Chrome CDP target ID); id becomes short form
- SessionEntry.push_tab() assigns t1/t2/... sequentially and stores native ID
- get_cdp_and_target resolves short ID → native ID for CDP calls
- start/new-tab/list-tabs/status responses include both tab_id and native_tab_id
- E2E: assert_tab_id validates t\d+ format; tests updated for native_tab_id presence
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a session is closed while commands are still in flight, the error
was "cdp error: writer channel closed" or "cdp error: connection closed"
which gave no indication of the root cause. Replace with a dedicated
SessionClosed variant that outputs "SESSION_CLOSED: session was closed
while command was pending" with an actionable hint.
P1: Only delete profile directory for local sessions (those with a
Chrome child process). Cloud sessions don't own a local profile
and could share a profile name with an active local session.
P2: Release the registry lock before slow I/O (Chrome kill, recursive
directory removal) to avoid blocking concurrent session operations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- wait navigation: use two-phase detection (URL change → readyState
complete) instead of OR logic that false-positives on initial page's
readyState=complete before navigation fires
- cloud_concurrent_eval_multi_tab: mark #[ignore] — known CDP
multiplexing issue mis-routes concurrent responses across tabs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- browser close now removes the Chrome profile directory after killing
the process, preventing disk accumulation from test runs
- Remove --test-threads=1 from CI — each test already uses a unique
session/profile via unique_session(), so parallel execution is safe
- Profile isolation: each test gets its own Chrome user-data-dir,
no shared state between concurrent tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the 6-param pattern (cdp, target_id, selector, registry,
session_id, tab_id) with a TabContext struct created once per command.
Before: element::resolve_node(&cdp, &target_id, &sel, registry, &s, &t)
After: ctx.resolve_node(&sel)
TabContext bundles (cdp, target_id, registry, session, tab) and exposes
resolve_node, resolve_center, resolve_object, scroll_into_view, and
resolve_object_id. Public fields cdp/target_id for direct CDP calls.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
P1: Skip CDP DOM.resolveNode for synthetic backendNodeIds (< 0, from AX
nodes without a backing DOM element) — go straight to AX fallback.
P2: Distinguish CDP -32000 (stale node) from other errors (connection
lost, etc.) — only return REF_STALE for true staleness, propagate
other CDP errors through the standard error path.
Fallback: When backendNodeId resolution fails or is synthetic, use
Accessibility.queryAXTree with the stored role + name to find the
element in the current AX tree.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire up snapshot refs (@e1, @e5, etc.) so all commands that accept a
<selector> argument can use them. Previously resolve_ref() was a stub
returning UNSUPPORTED_OPERATION.
- Add reverse lookup (ref_to_id) to RefCache for O(1) ref→backendNodeId
- Add peek_ref_cache() to SessionRegistry for read-only access
- Implement resolve_ref: @eN → backendNodeId → objectId → nodeId via CDP
- Thread registry context through resolve_node, resolve_element_center,
and resolve_selector_object (22 call sites updated)
- Error codes: REF_NOT_FOUND, REF_STALE, INVALID_ARGUMENT
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add short alias -v to --version flag
- Add `version` subcommand that calls the same handle_version() logic
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* [packages/cli-v2]feat: implement browser storage commands (Batch 9)
Add browser local-storage and session-storage commands (list/get/set/
delete/clear) via Runtime.evaluate on window.localStorage/sessionStorage.
Tab-level addressing (--session + --tab). Shared StorageKind enum drives
JS object selection and response storage field. clear <key> removes a
specific key (action: "clear", affected: 1), matching spec §14.5.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* [packages/cli-v2]fix: correct CDP null detection in storage get
CDP represents JS null as {type:"object",subtype:"null"}, not type:"null".
Update cold-miss check to test for subtype=="null" when type is "object".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
CDP Runtime.evaluate response nests the value at result.result.value,
not result.value. Wrong pointer caused implicit-domain derivation to always
fall back to None, triggering "domain/url required" CDP error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All Network.getAllCookies, Network.setCookie, Network.deleteCookies calls
now route via execute_on_tab using the first active tab's target_id instead
of execute_browser (browser-scope). Local Chrome does not expose Network.*
at browser scope — fixes CDP error -32601 on real E2E runs.
Also defaults path to "/" in set.rs when --domain is omitted (§13.3).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
B1: when --domain is omitted, derive domain from window.location.hostname
evaluated on the first active tab; prepend leading dot for subdomain matching.
B2: normalize_domain now lowercases in addition to stripping the leading dot
to handle mixed-case domain comparisons (e.g. Example.COM vs example.com).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add browser.cookies.list/get/set/delete/clear commands with CDP-level
Network.getAllCookies, Network.setCookie, Network.deleteCookies support.
Session-level only (no --tab); list/get are observations, set/delete/clear
are actions. Includes domain filtering, field mapping (httpOnly→http_only,
sameSite→same_site, expires=-1→null), and text/JSON output formatting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause of B3 false-positive: subscribing to Network events inside
`wait network-idle` only sees requests that start AFTER the wait call.
Requests already in-flight (e.g. fetch/new Image() started before wait)
are permanently missed, so pending stays 0 and the idle quiet window
expires immediately.
Fix — session-level Network tracking:
- `CdpSession` gains `tab_net_pending: Arc<Mutex<HashMap<String, i64>>>`
- `attach()` calls `Network.enable` on the tab immediately after storing
the session ID, before any user command runs
- `reader_loop` maintains `tab_net_pending` from Network domain events:
+1 on requestWillBeSent, -1 on loadingFinished/loadingFailed/
requestServedFromCache (clamped to 0)
- New `network_pending(cdp_session_id) -> i64` returns the live counter
- `detach()` cleans up the counter entry
`network_idle.rs` simplified:
- Removes per-wait event-channel subscription (subscribe-after-request
race is gone by design)
- Polls `cdp.network_pending()` — reflects all requests since attach
- Keeps JS guard (readyState + img.complete) as secondary check
- Quiet window: pending==0 && js_idle held for 500ms
New unit test: test_network_pending_counter verifies counter increments,
decrements on terminal events, and clamps at 0.
Updated test_attach_detach to expect the new Network.enable exchange.
178/178 unit tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add event routing infrastructure to CdpSession:
- New `event_subs` field keyed by "{cdp_session_id}:{method}"
- reader_loop now routes CDP events (messages without "id") to subscribers
- `subscribe_events(cdp_session_id, method)` returns mpsc::Receiver<Value>
- `get_cdp_session_id(target_id)` looks up the flat-session ID
- 2 new unit tests: test_event_routing, test_event_not_routed_to_wrong_session
Rewrite network_idle.rs to use CDP Network domain events:
- Subscribe to requestWillBeSent/loadingFinished/loadingFailed/requestServedFromCache
before calling Network.enable (avoids the subscribe-enable race)
- Track in-flight pending count; decrement on terminal events
- Require pending == 0 AND js_idle (readyState + img.complete) for 500ms quiet
window before declaring idle
- Fixes B3 false-positive: JS-only heuristics missed non-DOM fetch/XHR/script
requests; CDP events cover all network traffic
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Performance Resource Timing entries only appear after completion so the
previous responseEnd-only filter missed in-flight requests. Add two more
layers before the Performance API check:
1. document.readyState guard (page must be complete)
2. img.complete check — detects <img> elements still loading
3. responseEnd === 0 entries (W3C Resource Timing Level 2 in-flight)
+ recent entries within 500ms idle window
Any of the three layers returning pending > 0 keeps the wait loop alive.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
B1 wait navigation: use OR instead of AND for url_changed/readyState check.
Same-URL navigations (reload) never change URL — returning on either condition
matches actionbook-rs reference and handles both cases correctly.
B2 wait condition: arrays and objects are always truthy in JS regardless of
whether they are empty. Fix Rust truthiness to use _ => true for both,
matching actionbook-rs reference (waiting.rs:173).
B3 wait network-idle: replace readyState-only check with Performance Resource
Timing API inspection. Count resources with responseEnd < 500ms ago;
network is idle only when that count reaches zero. Matches actionbook-rs
reference (waiting.rs:98-120).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>