browser39 watch — Real-Time Agent IPC
browser39 watch monitors a JSONL commands file for new appended lines, processes each command through the browser engine, and writes results to an output file. This enables real-time, file-based communication between any agent process and browser39.
Usage
# Create the commands file (must exist before starting)
touch commands.jsonl
# Start watching
browser39 watch commands.jsonl --output results.jsonl
The watcher runs until it receives a quit action or the process is killed.
How it works
- On startup, processes any existing lines in the commands file
- Watches the file's parent directory for modifications (FSEvents on macOS, inotify on Linux)
- On each modification, reads only new complete lines appended since the last read
- Dispatches each command through the browser engine
- Writes results atomically (single
write()+fsync()per line) to the output file - Rotates the output file at 10MB
Partial lines (no trailing newline) are held until the next write completes them.
Appending commands
From another process (Python, shell, Node, etc.), append JSONL commands:
# Fetch a page
echo '{"id":"1","action":"fetch","v":1,"seq":1,"url":"https://example.com"}' >> commands.jsonl
# List links
echo '{"id":"2","action":"links","v":1,"seq":2}' >> commands.jsonl
# Follow a link by index
echo '{"id":"3","action":"fetch","v":1,"seq":3,"index":0}' >> commands.jsonl
# Navigate back
echo '{"id":"4","action":"back","v":1,"seq":4}' >> commands.jsonl
# Session info / heartbeat
echo '{"id":"5","action":"info","v":1,"seq":5}' >> commands.jsonl
# Shut down
echo '{"id":"6","action":"quit","v":1,"seq":6}' >> commands.jsonl
Command envelope
Every command line must be valid JSON with these fields:
{"id": "unique-id", "action": "fetch", "v": 1, "seq": 1, ...action fields}
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier, echoed in result |
action | string | Action to perform |
v | integer | Protocol version (always 1) |
seq | integer | Monotonically increasing sequence number |
The seq must be strictly greater than the previous command's seq. Out-of-order commands are rejected with INVALID_COMMAND.
Result envelope
Each result is one JSON line in the output file:
{"id": "unique-id", "ok": true, "seq": 1, ...result fields}
On error:
{"id": "unique-id", "ok": false, "seq": 1, "code": "NO_PAGE", "error": "no page loaded"}
Available actions
| Action | Description |
|---|---|
fetch | Load a URL, follow a link by index or text |
links | List links on current page (no network request) |
dom_query | Query DOM via CSS selector or JavaScript |
fill | Fill form fields by CSS selector |
submit | Submit a form by CSS selector |
cookies | List cookies (optionally filtered by domain) |
set_cookie | Set a cookie |
delete_cookie | Delete a cookie by name and domain |
storage_get | Get a localStorage value |
storage_set | Set a localStorage value |
storage_delete | Delete a localStorage key |
storage_list | List localStorage entries |
storage_clear | Clear localStorage for an origin |
back | Navigate back in history |
forward | Navigate forward in history |
info | Session state and liveness heartbeat |
config | Set run-level config (e.g. step_delay) |
quit | Shut down the watcher |
See jsonl-protocol.md for the full protocol specification including all action fields and options.
Config action
Set run-level options at any point (typically first command):
{"id":"cfg","action":"config","v":1,"seq":0,"step_delay":0.5}
step_delay adds a pause between commands — either a fixed number of seconds or a [min, max] range for random delays:
{"step_delay": [0.5, 2.0]}
Python example
import json
import time
COMMANDS = "commands.jsonl"
RESULTS = "results.jsonl"
def send(action, seq, **fields):
cmd = {"id": f"cmd-{seq}", "action": action, "v": 1, "seq": seq, **fields}
with open(COMMANDS, "a") as f:
f.write(json.dumps(cmd) + "\n")
f.flush()
def read_results():
with open(RESULTS) as f:
return [json.loads(line) for line in f if line.strip()]
# Fetch a page
send("fetch", 1, url="https://example.com")
time.sleep(1)
# Get links
send("links", 2)
time.sleep(0.5)
# Done
send("quit", 3)
time.sleep(0.5)
for r in read_results():
print(f"seq={r['seq']} ok={r['ok']}")
Differences from batch mode
browser39 batch | browser39 watch | |
|---|---|---|
| Reads | All lines at startup | New lines continuously |
| Exits | After processing all lines | On quit action |
| File must exist | Yes | Yes |
| Use case | Script-driven, one-shot | Long-running agent IPC |