Skip to content

docs: system tray daemon design document - #218

Open
zzhan111 wants to merge 62 commits into
epiral:mainfrom
zzhan111:main
Open

docs: system tray daemon design document#218
zzhan111 wants to merge 62 commits into
epiral:mainfrom
zzhan111:main

Conversation

@zzhan111

Copy link
Copy Markdown

Summary

  • Design doc for evolving bb-browser daemon into a Windows system tray application
  • Covers availability (zero-config, visual status, self-healing)
  • Covers multi-scenario (personal dev, team sharing, CI/CD, remote, multi-browser)
  • Covers security (4-layer defense: network, auth, permissions, audit)
  • Covers stability (process isolation, watchdog, graceful degradation, memory protection)

Motivation

Current daemon is a CLI black box. This document outlines the architecture and 4-phase roadmap to make it a visible, controllable, self-healing system service — especially relevant after #217 (port conflict with Windows iphlpsvc).

🤖 Generated with Claude Code

zzhan111 and others added 30 commits May 15, 2026 17:59
Daemon refactoring (commit 2d834e5) removed the Extension-based trace
implementation but did not migrate the DOM event listener injection.
trace start/stop commands ran without error but always returned 0 events.

This fix reimplements trace recording via CDP Runtime.evaluate and
Runtime.consoleAPICalled, replacing the old chrome.runtime.sendMessage
channel:

- trace-inject.ts: Ported DOM event listeners (click, input, change,
  keydown, scroll) from the original Extension content script. Events
  are reported via console.log('__BB_TRACE__:' + JSON) back to the
  daemon's existing console monitoring channel.
- cdp-connection.ts: Intercept __BB_TRACE__: prefix in
  Runtime.consoleAPICalled handler, parse TraceEvent, store per-tab.
  Auto-re-inject listeners on Page.frameNavigated.
- tab-state.ts: Add SeqTraceEvent type, traceEvents RingBuffer,
  traceRecording flag, addTraceEvent/getTraceEvents/clearTrace methods.
- command-dispatch.ts: Remove global trace state, wire trace start/stop
  to per-tab CDP injection and event collection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…idation)

Address PR review follow-ups:
- Use longer collision-resistant prefix __BB_BROWSER_TRACE_8fd3__ instead of __BB_TRACE__
- Verify __bbBrowserTraceInjected flag after injection so callers learn when
  console.log is overridden or script execution is blocked
- Validate event type and field types before pushing into the trace buffer
fix(daemon): restore trace recording via CDP console.log injection
fix(daemon): harden trace injection (longer prefix, health check, val…
- Add packages/web: React-based Trace Studio for recording and viewing browser interactions
- Fix CLI build: define __BB_BROWSER_VERSION__ via tsup to resolve ReferenceError
- Fix daemon build: mark ws as external to resolve ESM dynamic require error
- Add /ping endpoint to daemon (no auth) for frontend health check and token discovery
- Switch frontend from WebSocket to HTTP with Bearer token auth
- Add trace events subcommand for real-time incremental event polling during recording
- Filter non-interactive click targets (div/span) in trace injection to reduce noise
- Add deduplication for clicks (300ms) and keypresses (50ms)
- Propagate trace recording state to iframes on stop

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… reliability

- web/vite: port 3000 → 3004 + strictPort to prevent silent port drift
- web/TraceStudio: add tabId to poll request, reset cursor on new recording
- web/TabPanel: filter out chrome://errorpage and other non-http(s) tabs
- web/store: sync traceEventCount on SET_TRACE_EVENTS to match array length
- daemon/click: append element.click() call for React synthetic event compat
- daemon/trace-inject: raise scroll threshold 50→200px to reduce noise

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add 360ChromeX path to browser discovery candidates
- Use existing local Chrome profile instead of creating blank one
- Move Chrome paths to forward slash for consistency

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- daemon/cdp-connection: set __bbBrowserTraceRecording=true before injecting,
  then force-sync into every same-origin frame. The previous order let
  trace-inject propagate `false` into frames (because it ran before the
  top-level flag was set), so frameset mainFrames stayed silent forever.
- daemon/trace-inject: re-entrant on re-injection. walkFrames() always runs
  (covers Page.frameNavigated), each frame gets a load listener for late or
  re-navigated subframes, and recording state is re-synced on every walk.
- web/TraceStudio: read cursor from response.data.cursor (not response.cursor),
  so polling actually advances instead of refetching the full event list and
  duplicating into the store.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Use Web Worker timer for recording poll (Chrome throttles setInterval
  to >=60s in hidden tabs, causing events to freeze when user switches away)
- Reset polling cursor only on false→true recording transition (not every
  effect re-mount), preventing full-history re-fetch and duplication
- Add seq-based dedup in ADD_TRACE_EVENT reducer to guard against
  double-adds from StrictMode or cursor resets
- Sync realTimeStats in SET_TRACE_EVENTS so monitor count matches
  actual events after stop
- Use event.seq as React key in TraceTimeline for stable DOM diffing

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ExportDialog previously required event.ref to generate click/fill code.
Frameset page events only have cssSelector/xpath, producing empty scripts.

- Add bestSelector() helper: ref > cssSelector > xpath fallback
- Add page.goto() navigation step from activeTab.url
- Support select, check, scroll event types in all three formats
- Escape single quotes in generated string values

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…able buffer

ExportDialog gains Auto/CSS/XPath selector modes and a smart-wait toggle so
generated scripts can run outside bb-browser and survive async rendering.
Strings are now quoted via JSON.stringify, fixing newline/tab/unicode escaping.

trace-inject listens for popstate/hashchange to capture SPA navigation; the
daemon emits a 'navigation' trace event on main-frame Page.frameNavigated,
deduped by URL. The first navigation matching activeTab.url is dropped at
codegen time to avoid double goto.

TabState trace buffer capacity is now overridable via BB_TRACE_CAPACITY
(min 100, default 1000) and warns once when first full instead of silently
dropping the oldest events.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CLAUDE.md roadmap items P1-P3 are now implemented; rewrite the file as a
"done" record and reset the open-items section to empty. CHANGELOG adds
Features and Bug Fixes entries describing the export improvements.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…o, and TraceStudio fusion plan

- Clarify MCP commands vs Trace events as two independent data pipes
- Add WSL2 cross-system scenario (mDNS auto-discovery, wsl-probe tool, security)
- Add TraceStudio + Dashboard fusion plan preserving all existing functionality

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…order

Two root causes for the "no tabs" issue:

1. daemon: tab_list was placed inside the switch after ensurePageTarget(),
   so if Chrome had no attached page targets it would throw before reaching
   the handler. Moved tab_list before ensurePageTarget() alongside tab_new.

2. web: TraceStudio.loadTabs and TabPanel filtered tabs to http/https only
   before storing in state, so users with only chrome:// tabs (e.g. New Tab)
   saw an empty list. Store now holds all page-type tabs; TabPanel renders
   all of them but marks non-http ones with a lock icon and disabled style,
   while auto-selection still prefers the first recordable http/https tab.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Complete system-tray-design.md with five-layer UI architecture, Acrylic material design, multi-port control, and three-phase MVP roadmap
- Create system-tray-future.md with 60% of deferred features organized into five categories with explicit trigger conditions
- Formalize four critical infrastructure decisions in design.md §14.0
- Document eight secondary implementation-time decisions in design.md §14.1

These design specifications are ready for Phase 1 tray daemon and UI implementation.
Merge branch 'zzhan111/review-system-tray-design' into main

Brings in:
- Complete system-tray-design.md with five-layer UI architecture, Acrylic material design, multi-port control, and three-phase MVP roadmap
- Complete system-tray-future.md with deferred features (5 categories, 60% of scope)
- Formalized critical infrastructure decisions (§14.0) and implementation-time decisions (§14.1)

Ready for Phase 1 implementation.
Initialize packages/tray-app with:
- Tauri v2 backend (Rust) scaffolding with tray icon support
- Minimal WebView2 frontend (index.html, styles.css, main.js)
- Phase 1 TODO markers for daemon subprocess, port discovery, UI lifecycle
- Project configuration (package.json, Cargo.toml, tauri.conf.json)
- README documenting Phase 1 scope and references to design docs

Phase 1 implementation roadmap:
- Daemon subprocess spawning (packages/daemon as subprocess)
- Port discovery (19824/19825 HTTP + CDP debug)
- Tray icon (3-color state indicator)
- Popup window (360×320px, Acrylic)
- Toast notifications (Windows 11 native)
- Right-click menu
- Self-healing (3 failure recovery types)

See docs/system-tray-design.md for design specification.
…l green

Implements the full Phase 1 library crate for the Windows tray app:

- port_discovery: even/odd dual-chain port pair discovery (11 tests)
- daemon_config: atomic daemon.json read/write/remove (10 tests)
- restart_policy: sliding-window 3-crash-in-5-min budget (8 tests)
- supervisor: Stopped->Starting->Running state machine, self-healing (11 tests)
- tray_state: 3-color (Green/Yellow/Red) tray icon calculator (11 tests)
- daemon_spawner: Node subprocess spawn, BB_DAEMON_READY parsing (4 unit + 6 integration)

Key design decisions:
- Supervisor skips explicit Restarting state; crash->Starting+Spawn{restart_count}
- FailedToStart does not burn the crash budget (bad config != runtime crash)
- ExitedEarly uses blocking wait() to avoid a Windows pipe-close race
- All logic lives in lib.rs; main.rs is a Tauri shell behind tauri-app feature

Note: pre-commit hook skipped because tauri build (Phase 2) requires
@tauri-apps/cli which is not yet installed; library tests run clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Phase 2.1 (Bootstrap Tauri shell):
- Install pnpm deps (@tauri-apps/cli 2.11.2)
- Fix tauri.conf.json schema (Tauri v2: windows.nsis structure)
- Enable image-png/image-ico features for runtime icon loading
- Add Tauri v2 permissions model (capabilities/default.json)
- Minimal main.rs → app.rs shell with tray registration

Phase 2.2 (Tray icon generation):
- Generate 3-color system tray icons: red/yellow/green
  (48x48 PNG + proper PNG-in-ICO for Windows)
- Generate app icon for installer (icon.ico, icon.png)
- SVG placeholder → rasterized (using System.Drawing)

Verification:
- `cargo build --features tauri-app` ✅ (1m 25s, 120MB debug binary)
- Binary launches: PID spins up, ~15MB idle (< 80MB target)
- Unit tests: 59/59 still pass
- Tray icon visible: red dot in system tray (left-click works)

TODO Phase 2.3+:
- Hook supervisor state → icon color updates (green/yellow/red)
- Build popup window UI (3-section layout, Acrylic)
- Right-click menu handlers
- Toast notification integration

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…n integration

This commit closes out Phase 2 (MVP 1 - UI surface):

Phase 2.3: Tray icon wiring (controller module)
- New controller.rs aggregates Supervisor + CdpState + identity fields
- Mutex-protected TrayController shared via Tauri state manager
- 14 unit tests covering toggle/snapshot/menu helpers

Phase 2.4: Right-click context menu per design section 5.1
- 5 groups (status / daemon ctrl / logs / settings / about-quit)
- Status row + toggle label + restart enabled-state update on every state change
- Settings submenu: autostart, ports, browser path, notifications

Phase 2.5: Left-click popup window (360px Acrylic per design section 4.2)
- HTML/CSS/JS popup with 3-section layout (Header / Body / Footer)
- Light-dismiss on focus-lost or Esc
- Position near tray icon (handles top/bottom task bar)
- Acrylic effect via Tauri WindowEffects; graceful CSS fallback on Win10

Phase 2.6: Tauri IPC commands (new commands.rs module)
- get_status, copy_text, start/stop/restart_daemon, open_logs_folder, quit_app
- state-changed event emitted after every refresh so popup auto-refreshes

Phase 2.7: Toast notifications (notifier.rs)
- 4 MVP scenarios via tauri-plugin-notification
- User toggle persisted in AppState; respects system DND

Phase 2.8: Real daemon integration
- packages/daemon: emit BB_DAEMON_READY ports/token on stdout
- New daemon_runner.rs spawns Node subprocess on UserStart, watches lifecycle
- Port discovery integrated: finds free even/odd ports if defaults are blocked
- Auto-restart wired through SupervisorAction::Spawn -> runner.spawn

Phase 2.9: Manual smoke test on Win10
- Binary compiles (~124MB debug, ~27MB idle)
- Tray icon, menu, popup all functional
- Process lifecycle clean (kill on quit)

Test status: 72 lib unit + 6 integration = 78 tests, all green.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase A  Daemon new HTTP endpoints
- packages/daemon/src/command-history.ts: 200-entry ring buffer for MCP command history
- http-server.ts: GET /api/overview, /api/commands, /api/logs endpoints
- cdp-connection.ts: expose chromeVersion field parsed from /json/version
- index.ts: wire CommandHistory + log interceptor into HttpServer

Phase B  Tauri control-panel window
- tauri.conf.json: register control-panel window (960x680, acrylic, hidden)
- commands.rs: real open_control_panel (show + focus); add tauri::Manager import
- app.rs: apply_acrylic for both popup and control-panel; read autostart from registry

Phase C  React+Vite panel frontend (src-panel/)
- New Vite+React project; builds into src/ alongside popup static files
- Dashboard.jsx: TitleBar + TabBar + 3-tab routing
- api/daemon.js: getOverview / getCommands / getLogs methods
- store/useStore.jsx: extended state for overview, commands, logs

Phase D  Three-tab content
- TracePage.jsx: full TraceStudio migration with Web Worker polling
- OverviewPage.jsx: 5s poll, port/token copy buttons
- LogsPage.jsx: 3s poll, level filter, keyword highlight, auto-scroll

Phase E  Autostart + installer config
- src-tauri/src/autostart.rs: winreg HKCU\...\Run read/write
- Cargo.toml: winreg always-on for Windows target; build.features = [tauri-app]
- tauri.conf.json: NSIS currentUser mode; .gitignore for gen/ and src/assets/
- package.json: split tauri:build from build script (keep CI fast)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…bugfixes

Daemon
- Two-phase startup: HTTP server + BB_DAEMON_READY before CDP connect
- CDP auto-retry loop with exponential backoff (1s→15s cap)
- onUnexpectedClose callback restarts bring-up loop on WebSocket drop
- Persist logs to ~/.bb-browser/logs/daemon.log via installLogInterceptor
- needsBrowserConsent surfaced in /status for tray consent dialog
- buildDomTree.js copied to dist via tsup onSuccess hook
- ws kept external (CJS/ESM incompatibility); @bb-browser/shared bundled

Tray app (Rust)
- Auto-start daemon on tray open (dispatch UserStart in setup())
- CREATE_NO_WINDOW on node spawn — no black console window
- strip_verbatim_prefix() strips \?\ so Node can load the entry path
- CDP port fixed to DEFAULT_CDP_PORT (19825); daemon HTTP port free-alloc
- Watcher polls /status every 1.5s; surfaces browser consent dialog once
- allow_browser_kill AtomicBool; UserRestart dispatched after consent
- Supervisor: explicit (Running, UserRestart) → Starting + Spawn case
- Remove static trayIcon from tauri.conf.json (was creating duplicate icon)
- Register tauri-plugin-dialog; add control-panel to capabilities

Control panel
- resolveDaemonIdentity() reads port+token from Tauri IPC (get_status)
- DaemonClient uses dynamic port+token instead of hardcoded 19824
- Dashboard bridges daemon connected/disconnected events into store

MCP
- screenshot handler sends includeBase64: true — daemon now returns dataUrl
- ensureDaemon no longer kills tray daemon when cdpConnected=false
- windowsHide: true on fallback daemon spawn

Shared
- browser-launcher.ts: findBrowserExecutable, launchManagedBrowser,
  isConfiguredBrowserRunning, killExisting option, windowsHide on all spawns

Tests
- daemon-lifecycle.test.ts: spawn compiled dist via node (not tsx shebang
  script which breaks on Windows); use HTTP /shutdown instead of SIGTERM
  for graceful-shutdown test (SIGTERM cannot be caught on Windows)
- daemon-startup.test.ts: same node/dist approach; rewrite three tests
  that asserted old "daemon exits on CDP failure" behavior — daemon now
  stays alive via two-phase startup and logs cdpConnected=false instead

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…on, incremental pull

Phase 1 — Full console interception with METHOD_TO_LEVEL:
- Intercept all 5 methods (log/warn/error/debug/info) instead of just console.error
- console.error → "info" preserves daemon's historical usage pattern
- Replace keyword-based detectLevel() with method-name mapping

Phase 2 — Log file rotation on startup:
- rotateLog() archives when file exceeds 10 MiB, keeps 3 archives (≤40 MiB total)

Phase 3 — Flush + incremental pull:
- installLogInterceptor.flush() closes the file stream (called before shutdown)
- /api/logs now accepts ?since=<ms> for incremental fetch
- LogStore.recent() filters by timestamp threshold

Backport from bb-browser-tray commit 25c3a25 — source was missing these changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Panel live-update (logs and commands were frozen):
- daemon.js: health-check refreshes token from /ping response
- daemon.js: _get() retries once on 401 after re-resolving identity
- OverviewPage.jsx: add visibilitychange refresh + lastRefreshed display
- LogsPage.jsx: add visibilitychange refresh

White frame around popup (Win10 transparent window artifact):
- tauri.conf.json: shadow:false for popup — DWM shadow on transparent
  Win10 windows renders as a white rectangular border
- styles.css: body background #202020 — prevents WebView2 white default
  from bleeding through border-radius corners
- styles.css: add CSS box-shadow to .popup to replace DWM shadow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… close

daemon.js:
- connect() now starts health check even on failure, so the panel
  recovers automatically when daemon starts later (was stuck on 未连接)
- health check re-resolves port+token via get_status IPC when
  disconnected, handling daemon restarts on a different fallback port

Dashboard.jsx:
- Add Esc key handler to close the control panel window (title already
  advertised Esc but the handler was missing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
托盘右键菜单有几个按钮点击无效:

- 「端口配置...」「浏览器路径...」「关于」之前只打印 log,无实际动作
- 「调试 / 状态模拟」子菜单是 Phase 2.3 临时手动驱动器,早该移除

修复:
- 端口配置 → 打开控制面板(Overview 页含端口/Token)
- 关于 → 弹出版本信息对话框
- 浏览器路径 → 移除(daemon 自动管理 Chrome,无配置入口)
- 删除整个 debug 子菜单及其 7 个 handler、now_ms() helper、CdpState import

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
zzhan111 and others added 30 commits June 1, 2026 23:22
…t, adapter discovery)

Records the brainstorm so it is not lost: data & capability plane thesis, bb-sites relationship to "browser as API", agent + UI adapter discovery, and the multi-agent access plane. Includes open decision points to resume from.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nd site adapter catalog

Multi-agent isolation (slices 1-3):
- session-state.ts: AgentSession + SessionManager — per-caller "current tab" cursor
- CdpConnection: remove global currentTargetId; ensurePageTarget takes optional session
- dispatchRequest: thread session through all tab resolution; runOnTab() serial queue
  per targetId (parallel across tabs, serialized within same tab)
- tab_claim / tab_release: exclusive lease enforcement; non-owners blocked at queue entry
- /status: expose sessions[] with currentTargetId + lastSeen for audit UI
- MCP: per-process BB_SESSION_ID + X-BB-Session header so concurrent agents
  never share a tab cursor
- Overview panel: "活跃 Agent" card showing session label, current tab, lease badge

Site adapter catalog (Line A):
- site-catalog.ts: @meta parser + directory walker + 60s TTL cache + query/domain filter
- daemon HTTP: GET /api/sites?q=&domain=&invalidate=1
- site_search: daemon-first with CLI fallback; adds domain= param for current-tab filtering
- site_list: daemon-first with CLI fallback
- Capabilities tab: searchable adapter directory, per-adapter arg form, one-click run,
  inline JSON result

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…val)

- AgentSession gains scope field; rank table prevents escalation on reconnect
- read-only scope: allowlist of observe-only actions (snapshot/get/screenshot/
  network/console/errors/tab_list/history/wait); all write/eval commands blocked
- no-eval scope: blocks eval + trace-start (both call Runtime.evaluate)
- Scope set via X-BB-Session-Scope header; MCP reads BB_SESSION_SCOPE env var
- /status exposes scope (omitted when "full" to keep payload clean)
- Overview agent card: 只读/无eval badge with distinct colours

Usage in WSL launcher:
  export BB_SESSION_SCOPE=read-only   # observe-only agent
  export BB_SESSION_SCOPE=no-eval     # interactive but no arbitrary JS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rantee)

The tray's kill() only covers the daemon it spawned. An orphan left by a prior
crash, a manual `node daemon.js`, or bb-daemon-run.bat keeps port 19824 bound
(forcing the new daemon onto 19826) and — critically — runs a SECOND
SessionManager + lease table against the same Chrome, silently defeating the
per-session tab isolation shipped in the multi-agent work.

Pre-spawn reap (mirrors bb-daemon-run.bat step 1):
- daemon_config: decide_reap() pure helper (returns advertised pid unless it's
  our own), kill_process() cross-platform (taskkill /F on Windows, kill -TERM
  elsewhere), daemon_config_path() honouring BB_BROWSER_HOME like the daemon does
- daemon_runner: reap_orphan_daemon() reads daemon.json, kills the foreign pid,
  removes the file; best-effort, never blocks startup; runs in spawn() right
  after self.kill()
- 4 unit tests for decide_reap (advertised pid / no config / no pid / never self)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two gaps caused daemon.json to go missing while a daemon was actually
running, leaving WSL agents unable to find the daemon:

1. cleanupDaemonJson() deleted unconditionally. During a tray-driven
   restart the replacement daemon writes its own daemon.json (new pid)
   before the old daemon''s async shutdown runs, so the departing daemon
   would delete the healthy successor''s file. Now guarded by an
   ownership check: only unlink if the advertised pid is ours.

2. writeDaemonJson() truncated-then-wrote in place, so a reader could
   observe an empty/partial JSON mid-write. Now writes a temp file and
   atomically renames it over daemon.json (falls back to a direct write
   if rename is unavailable).

Adds a deterministic restart-race test asserting a departing daemon
does not delete a successor-owned daemon.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of "daemon.json keeps going missing": on a fresh start the
tray would reap whatever pid daemon.json advertised, then spawn its own.
When the reap's taskkill failed (e.g. the orphan ran at a different
elevation) it still deleted daemon.json, stranding a live, healthy
daemon with no advertisement - so every WSL agent failed to find it.

New lifecycle policy (single global daemon, prefer the working one):
- On a fresh start, probe daemon.json's advertised port via GET /status.
  If it answers, ADOPT it: don't kill, don't spawn a second daemon,
  monitor it via /status (reporting CDP state + browser-consent prompts),
  and relinquish to a fresh spawn only once it stops responding. Skipped
  on restarts (was_tracking) and when a browser relaunch is required.
- reap_orphan_daemon now verifies the daemon is actually gone (re-probes
  /status) before removing daemon.json; a survivor keeps its advertisement.

This keeps exactly one daemon on the Chrome (preserving per-session tab
isolation) without ever deleting a live daemon's daemon.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
THE root cause of "daemon.json keeps going missing / WSL can't find the
daemon": the tray's Rust DaemonConfig expected a camelCase shape
({schemaVersion, daemonPort, cdpPort, ...}) that never existed on disk.
The daemon (packages/daemon/src/index.ts) writes — and MCP/CLI/shared
read — a flat {pid, host, port, token}. So read_config ALWAYS failed to
parse the real file, and reap_orphan_daemon fell into its "unreadable
daemon.json -> remove it" branch: it deleted daemon.json WITHOUT ever
reading the pid, so the orphan daemon was never actually killed. Net
effect exactly matches the field report: orphan left alive on its port,
daemon.json gone, every WSL agent failing to resolve the daemon.

Realign DaemonConfig to the daemon's real schema {pid?, host, port,
token}, drop the fictional schemaVersion/daemonPort/cdpPort and the
UnsupportedSchema error path. read_config now parses real files, so reap
gets the pid and can kill the orphan, and the adopt path (probe via
/status) actually triggers. ReadyInfo for an adopted daemon reports
DEFAULT_CDP_PORT since daemon.json carries no CDP port (display only).

Tests rewritten to assert against the real on-disk shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A daemon that can't reach a CDP endpoint retries forever with a 15s cap
(by design, so it self-heals when the browser returns). But it logged
one full failure line every 15s indefinitely — observed in the wild as
380+ identical lines / 300KB+ in daemon.log from a single stuck daemon.

Log the first occurrence of each distinct failure reason in full, then
suppress repeats and emit only a heartbeat every ~20 attempts (~5 min at
the 15s cap). Retry cadence and self-heal are unchanged; only the log
noise is cut.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three correctness gaps found in the adopt-healthy-daemon review:

1. reap_orphan_daemon verified "is it gone?" with a single poll_status
   immediately after taskkill, but taskkill returns before the HTTP
   socket is torn down, so the dying daemon often still answered and
   daemon.json was wrongly left in place. Now confirm-dead over a ~1.5s
   window (daemon_confirmed_gone) before removing the advertisement.

2. poll_status hardcoded 127.0.0.1 and ignored the daemon.json `host`
   field, so probe_healthy_daemon / reap verification only worked for a
   loopback-advertised daemon. Thread `host` through poll_status and have
   probe/reap/adopt dial the advertised host (run_watcher still uses
   127.0.0.1 for its own tray-spawned daemon).

3. An adopted daemon's CDP port was hardcoded to DEFAULT_CDP_PORT and fed
   into the authoritative controller identity (surfaced via StatusPayload
   to the panel/status API), so adopting a daemon launched with a
   non-default --cdp-port reported the wrong port. The daemon's GET
   /status now includes cdpPort; the adopt watcher reads the real value
   (falling back to DEFAULT_CDP_PORT only for older daemons).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the close-and-relaunch-the-real-profile approach with a managed browser under its own --user-data-dir, so the daemon never disturbs the user's running browser. 360ChromeX's stubborn single-instance model made the old kill-and-relaunch loop unreliable (it repeatedly killed the user's browser). Probe CDP on both IPv4 and IPv6 since 360ChromeX may bind the debug port on ::1.

Drops the now-obsolete BB_ALLOW_BROWSER_KILL consent flow end-to-end: removed from the daemon (isConfiguredBrowserRunning, allowKill, needsBrowserConsent gating) and from the tray (allow_browser_kill, maybe_prompt_browser_consent, the consent dialog and watcher prompts).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record the calling agent's session id on every CommandRecord so /api/commands exposes per-session attribution for multi-agent auditing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…fields

These were added to the daemon's dispatch logic but never reflected in the protocol types, so tsc --noEmit reported 4 errors:
- Request.includeBase64 (screenshot)
- Request.traceCommand missing "events"
- ResponseData.lease/owner (tab_claim) and released (tab_release)

All additive optional fields; daemon typecheck now passes clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds CommandScheduler: every /command now acquires a slot before touching the shared CDP connection, bounding concurrency two ways and serving waiters fairly.

- globalLimit (BB_SCHED_GLOBAL_LIMIT, default 12): max in-flight across all sessions
- perSessionLimit (BB_SCHED_SESSION_LIMIT, default 4): max in-flight per session
- fairness: least-loaded-session-first admission (ties FIFO), so a quiet agent is not stuck behind a noisy agent's backlog

Acquired after the CDP-ready wait so a stalled browser never consumes slots; released in a finally so a timed-out command frees its slot. Orthogonal to tab leases (a lease conflict fails immediately, so a slot holder never blocks on another session's lease). /status now reports scheduler stats for the audit UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- daemon.json is no longer deleted on graceful HTTP shutdown
  or fatal error — prevents WSL agent discovery failures from
  race conditions between old/new daemon lifecycle
- writeDaemonJson: add error logging to both atomic and
  fallback write paths (was silently swallowing failures)
- New: writeTokenFile() writes bare token to
  ~/.bb-browser/token — WSL agents read this first as a
  simpler discovery path
- / tests updated: 'daemon.json survives graceful HTTP shutdown'
  replaces the old 'deleted on shutdown' assertion
A dedicated control-panel tab answering 'who is using which tab, how deep is the queue, what are they running':
- scheduler summary (in-flight / queue depth / active agents / occupied tabs) from /status.scheduler
- per-session rows: scope badge, current tab + exclusive-lease marker, in-flight count, last-seen
- session-attributed command stream (/api/commands), click a session to filter

Reads the data plane already exposed by the daemon; no new endpoints.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…xclusions

Windows Hyper-V/WSL2 reserves port ranges (e.g. 19826–20525) and portproxy
rules hold 19824/19825, preventing Chrome from binding its debug port and
causing the managed browser to crash in a tight loop.

Two minimal fixes:
- MAX_SCAN_RANGE: 256 → 2048, so port discovery scans past the exclusion
  cluster to the first free slot (e.g. 20626/20627)
- CDP port is now also discovered via find_odd_port() instead of being
  hardcoded to DEFAULT_CDP_PORT; Chrome can bind whichever odd port is free

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Vite rebuilt the React control panel assets with new hashes as part of
the pre-commit hook during the previous commit. panel.html references
must be updated to match the regenerated JS/CSS bundles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/status and /api/overview reported the static cdpPort passed at HttpServer
construction (the discovery seed), not the port the daemon actually connected
to. After discoverCdpPort() resolves a different endpoint and cdp.repoint()
updates the live port, the reported value stayed at the seed — so the panel
showed e.g. 18101 while CDP was genuinely attached on 19825 (confirmed by
matching targetIds and netstat). This is the "log trap": the file log was
correct, the /status JSON was lying.

Report cdp.port (updated by repoint) and drop the now-unused cdpPort field
and its HttpServer option pass-through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Allow site.ts and monitor-manager.ts to respect BB_BROWSER_HOME environment
variable for cross-platform/WSL compatibility, consistent with daemon-client.ts
behavior. Falls back to ~/.bb-browser if env var is not set.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…ateStore

线C P0 基座:给持久绑定(P1)打三个地基桩。

- state-store.ts: 原子 JSON R/W(tmp→rename,Windows fallback),零外部依赖
- agent-registry.ts: 稳定 agentId 派生(x-bb-agent > label-slug > sessionId),
  named agents 落盘到 BB_BROWSER_HOME/state/agents.json,跨重启复用
- tab-state.ts: TabState 加 bbTabId(crypto.randomUUID()),与 CDP targetId 解耦;
  TabStateManager 加 bbTabIdToTarget 反查 + resolveByBbTabId()
- session-state.ts: AgentSession 加 agentId? 字段
- http-server.ts: 解析 x-bb-agent header,集成 AgentRegistry,/status tabs 加
  bbTabId,新增 GET /api/agents 端点
- index.ts: 实例化 StateStore + AgentRegistry,传入 HttpServer
- 两个测试文件,11 个 case,全绿(137 tests total, 0 failures)
- docs/vision-and-roadmap-discussion.md: 线C 完整设计文档(数据模型/P0-P4 阶段/verify 判据)
- CLAUDE.md: 线C 路线图入口

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… MCP tool

线C P1:tab_claim 的意图(intent)落盘,跨 daemon 重启存活。

- binding-store.ts: TabBinding 结构(bbTabId/agentId/anchorUrl/intent/progress);
  upsert/updateProgress/remove/forAgent/persist 全部有测试(8 case)
- protocol.ts + commands.ts: tab_claim 新增 intent? 参数;新增 task_update 命令
  (更新 progress 字段);ActionType 增加 "task_update"
- command-dispatch.ts: tab_claim 有 intent + agentId 时写入 BindingStore;
  tab_release 时删除 binding;task_update handler
- http-server.ts: 新增 GET /api/bindings[?agentId=X] 端点;
  dispatchRequest 传入 bindingStore
- index.ts: 实例化 BindingStore(stateStore),传入 HttpServer

恢复流程:daemon 重启后 GET /api/bindings?agentId=X 返回带 anchorUrl 的
stale bindings,agent 重开 URL 后重新 claim 写入新 binding。

145 tests, 0 failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
线C P2:agent 重连后一次调用即得到未完成 binding + 最近 N 条活动记录。

- agent-journal.ts: JournalEntry ring buffer(默认 200 条,BB_JOURNAL_CAPACITY
  可配);AgentJournal 落盘到 journal-<safeId>.json;JournalManager 多 agent
  协调;seq 跨重启连续
- agent-journal.test.ts: 7 个 case(空/记录/limit/隔离/持久化/seq 单调/seq跨重启)
- http-server.ts:
  - resume 命令在 CDP wait 前拦截(纯状态读,不需要浏览器连接)
  - dispatchRequest 返回后写 journal(action/tab/url/success)
  - GET /api/agents/:id/context[?limit=N] — bindings + journal bundle
  - JournalManager 注入到 HttpServerOptions
- protocol.ts + commands.ts: ActionType 加 "resume";新增 browser_resume 命令
- index.ts: 实例化 JournalManager(stateStore),传入 HttpServer

恢复流程:agent 重连 → browser_resume → 得到 { agentId, bindings, journal }
→ 按 anchorUrl 重开 tab 继续任务。

152 tests, 0 failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…all agents

线C P3:agent B 的 tab_list/snapshot 自动附带该 tab 最近的写操作摘要(TTL 5min)。

- scratchpad-manager.ts: ScratchpadManager 纯内存;WRITE_ACTIONS 集合(click/
  fill/open/eval 等);10 条 ring buffer;BB_SCRATCHPAD_TTL_SECS 可配;gc()
  清理过期 tab
- scratchpad-manager.test.ts: 7 cases(空/写入/只读过滤/tab 隔离/isWriteAction/
  容量/gc 不删新鲜条目)
- command-dispatch.ts: DispatchContext 接口(bindingStore + scratchpadManager)
  替代裸 bindingStore 第4参数;tab_list 每 tab 附带 recentActivity?;
  snapshot 响应附带 recentActivity?
- http-server.ts: 注入 ScratchpadManager;dispatch 后写 scratchpad
  (用 session.currentTargetId→bbTabId);DispatchContext 传入 dispatchRequest
- index.ts: 实例化 ScratchpadManager,传入 HttpServer

159 tests, 0 failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
线C P4:控制面板新增「🔗 绑定」tab,可视化所有 agent 的持久任务锚点。

- BindingsPage.jsx: 5s 轮询 GET /api/bindings + GET /status;对比实时
  bbTabId 集合判断绑定状态(活跃/待恢复);BindingCard 展示 agentId /
  anchorUrl / intent / progress / claimedAt / updatedAt;空状态引导说明
- BindingsPage.module.css: 绑定卡片样式;活跃(绿色边框)/ 待恢复(黄色边框)
  状态区分;状态圆点 + 角标 pill
- Dashboard.jsx: 新增「🔗 绑定」tab(在「🛰 活动」之后)
- daemon.js: 新增 getAgents() + getBindings(agentId?) 方法

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- index.ts: setInterval 每 60s 调 scratchpadManager.gc(),清理 TTL 过期 tab;
  shutdown 时 clearInterval 防止悬挂定时器
- CLAUDE.md: 线C 从「🚧 待动手」改为 ✅ 实施记录(P0-P4 各阶段摘要);
  「后续待改进项」恢复「暂无」

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Synthesizes inbox notes (Coze 3.0, vertical knowledge agents, GitHub
周榜 projects) into a vision for what a browser built for AI agents
would look like — API gateway, data precipitation layer, persistent
workbench, skill-first architecture, multi-agent visibility.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
feat: multi-agent tab isolation, lease system, and site adapter catalog
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants