Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project will be documented in this file.

## [1.0.0.10] - 2026-07-07

### Changed
- Chrome now boots on demand: LinkedIn tool calls ping the CDP endpoint before attaching and launch it via `bin/browser-service start` if it's not up, waiting for CDP to come up. Session cookies persist on disk in the Chrome profile, so launchd/systemd no longer need to keep Chrome alive between calls.
- `cron.system_status` no longer logs a startup warning when Chrome's CDP endpoint is unreachable — that's now the expected steady state — and status output/JSON drop the stale "reinstall the always-on service" restart hint in favor of noting it starts on demand.

## [1.0.0.9] - 2026-07-05

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.0.9
1.0.0.10
18 changes: 6 additions & 12 deletions cron/system_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,6 @@ def probe_browser() -> dict[str, Any]:
except (json.JSONDecodeError, TimeoutError, OSError) as exc:
health_error = str(exc)

restart_hint = "bin/browser-service install"
if not managed:
restart_hint = "bin/browser-service install (or ./install.sh)"
return {
"url": url,
"reachable": reachable,
Expand All @@ -85,7 +82,7 @@ def probe_browser() -> dict[str, Any]:
"service_backend": backend,
"service_unit_path": str(unit_path) if unit_path else None,
"auto_start_on_reboot": managed,
"restart_hint": restart_hint,
"starts_on_demand": True,
}


Expand All @@ -95,15 +92,14 @@ def check_services(*, logger: logging.Logger | None = None) -> dict[str, Any]:
whichever is unreachable. Intended to run alongside the startup
"auto update check" — no email/paging, just a log line an operator
tailing server.log will see.

Chrome now boots on demand (see outreach/browser.py:_ensure_browser_running),
so it being down at rest is the expected steady state, not a health issue —
only the cron server warrants a startup warning here.
"""
log = logger or logging.getLogger(__name__)
browser = probe_browser()
cron_server = probe_cron_server()
if not browser.get("running"):
log.warning(
"Health check: browser CDP unreachable — %s",
browser.get("health_error") or "no response",
)
if not cron_server.get("running"):
log.warning(
"Health check: cron server unreachable — %s",
Expand All @@ -124,10 +120,8 @@ def format_browser_lines() -> list[str]:
lines.append(f" profile {browser.get('profile')}")
else:
err = browser.get("health_error")
hint = browser.get("restart_hint", "bin/browser-service install")
extra = f" — {err}" if err else ""
lines.append(f" Chrome CDP not running{extra}")
lines.append(f" restart: {hint}")
lines.append(f" Chrome CDP not running{extra} (starts on demand)")
if browser.get("managed"):
lines.append(
f" auto-start {browser.get('service_backend')} ({browser.get('service_unit_path')})"
Expand Down
39 changes: 39 additions & 0 deletions outreach/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,44 @@ def _pp_structure_activity_update(index: int, post: dict[str, Any]) -> dict[str,
_ZERO_TAB_CDP_ERROR = "setDownloadBehavior"


_REPO_ROOT = Path(__file__).resolve().parent.parent
_BROWSER_SERVICE = _REPO_ROOT / "bin" / "browser-service"


def _cdp_reachable(cdp_url: str, timeout: float = 2.0) -> bool:
try:
urllib.request.urlopen(f"{cdp_url}/json/version", timeout=timeout).close()
return True
except Exception:
return False


async def _ensure_browser_running(cdp_url: str) -> None:
"""
Boot Chrome on demand via bin/browser-service if the CDP endpoint isn't
reachable — so nothing needs to keep it running between tool calls.
browser-service already waits for CDP to come up before returning.
"""
if await asyncio.to_thread(_cdp_reachable, cdp_url):
return
if not _BROWSER_SERVICE.is_file():
return
logger.info("Chrome not reachable at %s — starting it on demand", cdp_url)
env = dict(os.environ)
port = urlparse(cdp_url).port
if port:
env["CDP_PORT"] = str(port)
proc = await asyncio.create_subprocess_exec(
str(_BROWSER_SERVICE),
"start",
cwd=str(_REPO_ROOT),
env=env,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
await proc.wait()


def _open_blank_tab(cdp_url: str) -> None:
"""Ask Chrome's CDP HTTP endpoint to open a tab (bypasses Playwright context creation).

Expand Down Expand Up @@ -694,6 +732,7 @@ async def _attach(self) -> None:

We never close a tab we didn't open, so the user's browsing is undisturbed.
"""
await _ensure_browser_running(self.cdp_url)
self._browser = await self._connect_with_zero_tab_retry()
self._is_attached = True

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "ebase"
version = "1.0.0.9"
version = "1.0.0.10"
description = "LinkedIn recruiting outreach that runs inside Claude Code"
readme = "README.md"
license = "MIT"
Expand Down
19 changes: 12 additions & 7 deletions testing/tests/test_system_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def test_probe_browser_unreachable() -> None:
browser = ss.probe_browser()
assert "reachable" in browser
assert "managed" in browser
assert "restart_hint" in browser
assert "starts_on_demand" in browser


def test_format_browser_lines() -> None:
Expand All @@ -31,11 +31,16 @@ def test_check_services_returns_both_probes() -> None:
def test_check_services_logs_warning_for_unreachable(
caplog: pytest.LogCaptureFixture,
) -> None:
# Chrome boots on demand now, so being down is expected and shouldn't warn —
# only the cron server's health is worth a startup warning here.
with caplog.at_level(logging.WARNING):
result = ss.check_services()
for name, key in (("browser", "browser"), ("cron_server", "cron server")):
if not result[name].get("running"):
assert any(
record.levelno == logging.WARNING and key in record.getMessage().lower()
for record in caplog.records
), f"expected a warning logged for unreachable {name}"
assert not any(
record.levelno == logging.WARNING and "browser" in record.getMessage().lower()
for record in caplog.records
)
if not result["cron_server"].get("running"):
assert any(
record.levelno == logging.WARNING and "cron server" in record.getMessage().lower()
for record in caplog.records
), "expected a warning logged for unreachable cron server"
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading