The general agent writes via UI puppeteering + the model loop (fire-rate ~50%, 13-52s); the deterministic alternative (cookie-borrow -> the site's OWN write API -> typed receipt) lived only in isolated MCP shims, and only Reddit's was genuinely API-first (X's shim is UI-driving). site_write_registry unifies it: has_api_write(domain,action) + api_write() route a write to a per-domain adapter (Reddit reuses reddit_writes, no fork), time it, and return a typed WriteResult (ok + the site's id/permalink receipt + latency); any failure becomes a typed ok=False so the agent falls back to the UI path, never a crash. A site with no adapter is a clean miss; adding one (X GraphQL, etc.) is one row. LIVE-VALIDATED end to end on Reddit through the registry: comment ok=True + reversible delete ok=True 196ms, verified [deleted] (deterministic, no captcha/DOM/selector, ~50-190x faster than UI). 6 unit tests (routing/receipt/fail-safe), linter clean, no import cycles. NOT yet wired as a browser-agent MCP tool (that path isn't live-testable on the wedged dev bench); receipt-string extraction returned 'ok' on the live run (write succeeded + verified reversible; id/permalink parse is a follow-up).
The reveal opener regex only matched 'create a post', missing bare 'Create' and 'New thread' / 'Start a thread' buttons that real compose surfaces use. Adds those; safe by construction (a broadened trigger can never produce a false success, a fill against a real composer is still required, so a trigger that opens a signup/join upsell instead of a composer yields found:false). Validated CDP-direct: LinkedIn-post and YouTube-comment still flip (no regression), and Threads (which gates posting behind a 'Join Threads' upsell for this session) correctly does NOT false-fill
The executeJavaScript path is SUSPENDED by Electron until a page stops loading, so on a page whose trackers never let it settle (Reddit, LinkedIn) every in-page command queues into the 15s backend timeout = the mount-poll wedge that has blocked every agent-loop run. CDP Runtime.evaluate runs in the browser process, out of the renderer's load-blocked event loop, so it does not suspend. This reuses the already-in-production CDP bridge (sendCdp -> send-cdp-command -> wc.debugger.sendCommand, keyed by getWebContentsId, with attach + per-wc serialization + timeout already built) that the AX/click commands use; only the eval path was left on executeJavaScript. No-regression by construction: on any CDP INFRA failure (debugger can't attach because DevTools or a remote-debugging port holds the webContents, or the bridge errors) it falls back to the original executeJavaScript path, and a real page-side exception still surfaces as a throw; window.__OSW_CDP_EVAL__ = false forces the old path. Unwrap logic extracted to cdpEval.ts + 6 unit tests. NOTE not live-demonstrable on a --remote-debugging-port bench: that port globally blocks wc.debugger.attach, so the whole app CDP path (this AND the existing list_interactives) times out there; production and normal dev do not use the port, so attach works and the existing CDP commands prove it
Item 1: when the finder's execCommand insertText read-back fails (editors that reject synthetic input), retype the same text as real OS-level key events via wv.sendInputEvent, then verify from the marked element OR document.activeElement (editors like Reddit swap the node on activation, staling the selector). Item 3: when an open-first reveal navigates to a new document (Reddit thread, TikTok video, GitHub issue), the send-script re-perceives after a load beat and calls the finder once more on the destination, bounded to 2 tries. 4 new tests (cross-nav retry fires / no wasted retry) + structural tests split into test_browser_reveal.py to stay under the 300-line cap. Live real-path validation of the sendInputEvent leg is owed on a healthy rig: the eric/browser agent loop wedges at fresh-card webview mount (15.25s cap, renderer starvation, predates the suspend fix) at low load too