Old configs (without version field) may have incompatible browser.mode values
like "isolate". On load, the config is now parsed as raw TOML first to check
for a version field. If missing, the old config is backed up to config.toml.bak
and a new config is created with version=1, browser.mode forced to "local",
and all other compatible fields copied over.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Refs are tied to backendNodeId, so they persist across snapshots as long
as the DOM node stays the same. This is a key affordance for agents
chaining commands without re-snapshotting after every step.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(cli): add help examples for logs, query, and storage commands
Add after_help with usage examples to the remaining commands that were
missing them: logs console, logs errors, query, and all five storage
subcommands (list, get, set, delete, clear). Also update the browser
help listing with new sections for Logs, Wait, Cookies, Storage, and
Query so all commands are discoverable from `actionbook browser help`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(cli): show query nth syntax with required <n> argument in browser help
Codex review caught that `query nth` requires an index before the
selector, but the help listed it grouped as `query one|all|nth|count
<selector>`. Split into two lines so nth shows `query nth <n> <selector>`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Catch CDP -32000 "DOM Error while querying" from DOM.querySelector
and return a friendly ELEMENT_NOT_FOUND with the selector in details.
For @-prefixed selectors, hint that the correct format is @eN.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Select an <option> by its snapshot ref (@eN) instead of value or text.
Resolves the ref to a remote object and passes it directly via CDP
arguments, avoiding intermediate value lookups. Includes mutual
exclusivity with --by-text, updated text output, and e2e tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codex review caught that `query nth` requires an index before the
selector, but the help listed it grouped as `query one|all|nth|count
<selector>`. Split into two lines so nth shows `query nth <n> <selector>`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.
- restart_kills_old_chrome_and_spawns_new: verify old Chrome PIDs are
dead after restart and new PIDs are different from old ones
- double_close_returns_not_found: verify second close returns
SESSION_NOT_FOUND without panic or crash
- 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
Add after_help with usage examples to the remaining commands that were
missing them: logs console, logs errors, query, and all five storage
subcommands (list, get, set, delete, clear). Also update the browser
help listing with new sections for Logs, Wait, Cookies, Storage, and
Query so all commands are discoverable from `actionbook browser help`.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Two fixes for remaining flaky E2E failures (5-10 random tests per run):
1. Limit parallel threads to 8 (package.json test:e2e) to reduce Chrome
process contention and CPU pressure during parallel test runs.
2. Add wait_page_ready() helper that polls document.readyState every 200ms
(up to 2s) after session start/goto. Called in all 14 local start_session()
functions and both harness helpers. Prevents tests from interacting with
pages before DOM is ready under high parallel load.
Result: 441 passed, 0 failed (was 5-10 random failures per run).
14 test files defined local start_session() without unique_session(),
causing all parallel tests to share the default profile and hit
SESSION_STARTING rejections (238 failures).
Fix: add unique_session("s") to each local start_session(), passing
--set-session-id and --profile to browser start. This gives each test
its own isolated Chrome profile, matching the pattern in harness.rs.
Result: 238 failures → 3 flaky (unrelated to session isolation).
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.