timeline-browser-transport

Use when relaunching the CDP browser or its extension goes silent.

SKILL.md

GitHub

Timeline Browser Transport

The bookmark pipeline drives the person's own browser over CDP, and Hermes Control reaches that same browser through an unpacked extension. Both are transport, not application logic: when they break, every downstream symptom looks like a data bug.

No handle, list id or session id belongs in this file. Tab identity comes from the gitignored identifier map (Documentation/Reference/Configure.md); a command takes a slug, never an id.

A command reaches the browser through Transport, never Browser/

Source/Transport/Transport.ts is the seam. A command declares what it needs (transport: "read" | "write" | "browser"), receives a CommandContext, and calls context.transport. Source/Browser/ is the CDP implementation detail: the client, the selectors and the session, reachable only from Source/Transport/CDP/.

A new browser command therefore adds nothing to Browser/ and imports nothing from it. If the interface has no member for what the command needs, that is the interface's gap, not a reason to reach past it. A command that names Transport/CDP or Transport/View is a defect; nothing in the tree tests for it, so the rule is held by review.

--transport view answers read commands from the shadow store with the same JSON shapes and refuses everything else (read-only-transport for a write, unsupported-transport for a tab, a page or a scrape). It is never a fallback: with the debug port closed, tab list fails cdp-unreachable instead of answering from the archive.

When to Use

  • Relaunching the browser instances.
  • curl -s http://127.0.0.1:9222/json/version answers nothing.
  • tab_* tools report that no browser is connected.
  • A relaunch "succeeded" but the extension is missing afterwards.

Two instances, two profiles

A second debug instance needs its own --user-data-dir. Chromium locks each profile directory, so relaunching on a directory already in use starts no second process and silently drops the flags. Launch both explicitly, so each is identifiable by its own command line rather than by assumption:

# the person's normal browser
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" \
    --user-data-dir="$HOME/Library/Application Support/BraveSoftware/Brave-Browser" \
    --no-first-run --no-default-browser-check

# the CDP instance
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser" \
    --remote-debugging-port=9222 \
    --user-data-dir="$HOME/.hermes/cache/scratch/brave-cdp-profile" \
    --load-extension="$HOME/<path-to-extension>/dist" \
    --disable-features=DisableLoadExtensionCommandLineSwitch \
    --no-first-run --no-default-browser-check

Quit gracefully (kill -TERM <main-pid>) rather than force-killing: the profile writes its session on a clean exit. Confirm the port is free before relaunching with lsof -nP -iTCP:9222 -sTCP:LISTEN.

Chromium 156+ ignores --load-extension

Without --disable-features=DisableLoadExtensionCommandLineSwitch the launch succeeds with no extension loaded and no error. The profile keeps a ghost registration: the id sits in Secure Preferences → extensions.settings with location: 4 (unpacked) and neither path nor manifest, and every chrome-extension://<id>/… tab renders as a dead page whose title is the raw URL and whose chrome.storage is undefined - the limited window.chrome object, not an extension context.

An extension that a previous session was using can therefore disappear purely because a relaunch omitted the flag. Always re-probe after a relaunch instead of assuming the old targets survived.

Prove the extension loaded

The background worker is the only positive signal; the launch exit code is not:

curl -s --max-time 5 http://127.0.0.1:9222/json/list \
    | grep -o 'chrome-extension://[a-z]*/background.js'

Poll more than once. An MV3 background worker is absent while idle, so a single empty result is not a verdict.

A command-line extension never starts its own heartbeat

--load-extension does not fire onInstalled, so an extension whose periodic work is a chrome.alarms job never creates that alarm. It loads, its worker sleeps, and it stays silent indefinitely.

grep 'browser-control/register' $HERMES_HOME/logs/gateway.log | tail -1

Observed on a --load-extension relaunch: the background worker was present, yet no browser-control/register request arrived for 12+ minutes. Opening the extension's sidepanel.html with PUT /json/new did not change that - it renders as a plain tab with no extension APIs. The reliable trigger is the person clicking the toolbar icon, which opens the real side panel and starts the heartbeat. Report that as the remaining step; do not call the extension healthy because its target exists.

Restoring the tab set

A profile with session.restore_on_startup unset opens a new tab page, not the previous session, so a relaunch loses the person's tabs even though the session files exist. Reopen the known set and let the titles prove the session:

curl -s -X PUT "http://127.0.0.1:9222/json/new?<URL-encoded-URL>" > /dev/null

A logged-in title (the person's handle, not @undefined) is the evidence that the profile still carries the session. Titles that stay as raw URLs mean the page has not finished loading, not that the session is gone - re-read the list after a few seconds.

Claim → test

Claim Test
A browser is exposing CDP curl -s http://127.0.0.1:9222/json/version returns a Protocol-Version
Which profile owns the port PID=$(lsof -nP -iTCP:9222 -sTCP:LISTEN -t | head -1); ps -o command= -p "$PID"
Both instances are up ps -eo command | grep '[B]rave Browser' | grep -o -- '--user-data-dir=[^ ]*' | sort | uniq -c
The extension actually loaded curl -s http://127.0.0.1:9222/json/list | grep -o 'chrome-extension://[a-z]*/background.js'
The extension is talking to Hermes grep 'browser-control/register' $HERMES_HOME/logs/gateway.log | tail -1 shows a recent timestamp
Tabs came back and are logged in curl -s http://127.0.0.1:9222/json/list shows resolved titles, not @undefined