| Age | Commit message (Collapse) | Author |
|
* fix: reconnect to Chrome only on browser demand
* fix(daemon): gate stale CDP bridge reconnect attempts
* Harden bridge reconnect lifecycle against stale socket callbacks
* fix(daemon): handle stale attempt failures without deferred settlement
* test: align waitFor assertions and stale-socket case labels
* fix(daemon): re-check socket after connect wait in handleRequest
Restores the post-wait re-check from the original PR branch (dropped during
the main rebase merge); TypeScript needs the local narrowing before send.
* fix(daemon): reject in-flight requests when an open connection drops
`failCurrentAttempt` delegated callback rejection to `settleAttempt`, but an
attempt that reached `open` had already settled, so the close path returned
early and never answered its in-flight requests. They hung until the 10s
command timeout instead of failing immediately, which the on-demand reconnect
change had promised to preserve.
Reject the attempt's callbacks directly in `failCurrentAttempt`, before the
close handler notifies clients.
Move the connection-lifecycle tests out of test/manual/ — which nothing runs —
into test/daemon/, where `npm test` picks them up, and convert the hand-rolled
pass/fail counters to node:test. Adds a case covering the drop path above; it
fails without the fix.
---------
Co-authored-by: Hermes Subagent <hermes-subagent@example.com>
Co-authored-by: Hermes Agent <agent@example.com>
Co-authored-by: uttkarsh-26 <uttkarsh@users.noreply.github.com>
Co-authored-by: amankumarsingh77 <amankumarsingh7702@gmail.com>
|
|
IdMultiplexer and EventRouter were the only classes in src/; they are now
createIdMultiplexer/createEventRouter closures like every other factory
here. The multiplexer kept {clientId, localId} per daemon id while the
bridge kept {send, clientId, localId, timer, isAttach} under the same key
— two maps of overlapping data, and mux.resolve()'s return value was
discarded at three of four call sites. One map now holds the union.
That merge fixes a real leak: removeClient cleared the multiplexer but
never the callbacks map, so a disconnected client's timers stayed armed
and later fired a response at a socket that was gone. clearClient now
cancels them, and a test pins it.
handleRequest polled every 250ms for up to 15s per request when Chrome
was down; N queued requests now await one connect signal.
server.ts uses its own protocol's serialize/deserialize instead of
hand-rolled JSON, and broadcast no longer routes through `this` — the
only `this` in the codebase, and the one thing that breaks when the
server object is destructured.
cdp/event-queue.ts held two unrelated concerns; the request/timeout half
moves to cdp/pending-requests.ts. cdp/daemon-transport.ts is the client
half of the daemon protocol, so it moves to daemon/transport.ts.
test/daemon/ needs adding to the test glob to run at all — there was no
daemon test directory before because none of this was reachable without
live Chrome.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
isInternalUrl, SPECIAL_KEYS, andThen and mapErr had no callers in src or
test. BrowserState.remoteBrowserId was parsed on restore and never
written — the field that is actually read lives on DaemonStatus.
IpcServer.disconnectClient and IpcServer.clients() were unreachable, as
was EventRouter.allClients and a write-only lineCount counter.
DAEMON_STALE_SOCKET_CLEANUP was a hardcoded true with an if around it, so
the unlink now runs unconditionally.
CdpBridge.isAlive and getSessionOwner look equally dead from src/ alone,
but test/manual/bridge-test.ts drives both — they are the only seams that
suite has, so they stay.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Drops leading blank lines where a file header was removed, collapses
double blank lines, and closes the empty catch/handler blocks whose only
content had been a comment. Whitespace only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Removes module banners, section dividers, reference lists, and comments
restating the code. Keeps one-line notes for the Windows named-pipe
endpoint, the Windows unlink no-op, the do-not-unlink-on-rejection rule,
the `.cmd` shell requirement, shell-quoting of spaced paths, and the
in-place client promotion.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Types the event-queue iterator's TReturn as undefined so the terminal
result needs no cast; declares the isError field the pi runtime reads by
duck-typing; binds socket to a local const in the daemon transport;
replaces the @ts-ignore on the optional sharp import with a non-literal
specifier so src/ still typechecks where sharp was not installed.
Adds src/util/guards.ts as the single home for narrowing over unknown —
isRecord plus scalar/array readers — and converts the two private
isObject copies in cdp/types.ts and daemon/protocol.ts along with every
inline `as Record<string, unknown>` in domains/js.ts and domains/render.ts.
Gives Accessibility.getFullAXTree a real result schema, closing the last
unvalidated CDP boundary: only nodeId is required, every object carries
additionalProperties, and each AX value stays Unknown, so the tree
decodes as a whole or fails as a whole rather than being filtered.
Guards the five tool-result `details` readers, the daemon attach
response, the persisted session state, the browser-namespace flag, and
the fs errno narrowing, each feeding an already-present failure path.
The daemon-transport socket binding is not a bug fix: send() runs
synchronously inside sendWithTimeout's promise executor, so socket cannot
be reassigned between the check and the write. It removes the assertion
and makes the write immune to a later refactor that adds an await.
Two assertions survive by design and are documented in the task report:
the never->unknown re-widening in util/tool.ts, which the type-erased
tool registry makes unavoidable, and the AsyncFunction constructor lookup
in domains/js.ts, which JavaScript exposes no typed binding for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
Ten sites cast or implicitly typed JSON.parse output; JSON.parse("null")
would have satisfied all of them. parseJson returns a Result and validates
against a typebox schema. js.ts keeps unknown — user script output has no
fixed shape. daemon/bridge.ts assigned JSON.parse straight to CdpRawMessage
with no guard at all; it now uses isCdpRawMessage like the transport does.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|
isDaemonRunning() only checked file existence of the Unix socket.
A dead daemon leaves a stale socket, causing ensureDaemon() to skip
spawning and client.start() to fail with 'Chrome not connected'.
Replace with a liveness probe: connect, register, verify ack, then
disconnect. On failure, clean up the stale socket.
Also makes bridge handleRequest async to poll for Chrome connection
for up to 15s before rejecting.
|
|
* feat: Unix socket daemon with auto-reconnect for persistent Chrome CDP
Eliminates the 'Allow Remote Debugging' Chrome prompt that appeared on
every pi session by multiplexing pi instances through a single persistent
daemon process.
Key additions:
- Unix socket IPC (net.createServer + readline JSON framing)
- CdpBridge with ID multiplexer + event router
- DaemonTransport implementing CdpTransport interface
- Auto-spawn with ensureDaemon() (lazy, checks socket)
Fixes since initial implementation:
- Bridge auto-reconnects to Chrome with exponential backoff (1s-60s).
Previously the bridge tried once at startup and permanently failed
if Chrome wasn't ready — causing 'Chrome not connected' on every
browser tool call.
- Daemon spawn uses project-local tsx binary instead of global --import
flag, fixing silent spawn failure (ERR_MODULE_NOT_FOUND).
- Stale ctx crash: persistState in onOwnershipChange wrapped in
try/catch to prevent CDP event consumer crashes.
49 test assertions across 4 test files. Zero TypeScript errors.
* feat: on-demand browser initialization
Browser daemon no longer auto-starts on session_start. Instead:
- session_start creates the client lazily but does NOT connect or spawn
the daemon. Zero Chrome prompts for non-browser pi sessions.
- Browser tool wrapper checks for daemon socket before each call.
Socket missing → clear error: 'Run /browser-setup first'.
Socket exists → proceeds silently (daemon already running).
- /browser-setup is the explicit, one-time init point. It spawns
the daemon and connects to Chrome only when the user opts in.
- SKILL.md updated with Connection Setup instructions telling agents
to direct users to /browser-setup when socket is missing.
* fix: checkChromeRunning on macOS — match by substring not exact
ps -A -o comm= returns full executable paths on modern macOS
(e.g. /Applications/Google Chrome.app/Contents/MacOS/Google Chrome),
not the basename. The old exact-match check (browserProcesses.includes(l))
never matched, causing false 'No browser instance running' errors.
Changed to substring includes() with helper/renderer/crashpad filtering.
* feat: browser_setup as agent-callable tool
The agent can now self-recover from 'Browser harness not initialized'
errors by calling browser_setup directly — no user mediation needed.
- Extracted shared performSetup() from the slash command into setup.ts
- Created src/domains/setup.ts with ensureAlive:false to bypass the
socket guard (this tool IS the setup, can't require itself)
- Registered browser_setup in the tool registry
- Updated SKILL.md: agent calls browser_setup directly, not ask user
- Removed stale test/manual/spawn-test.ts
* release: v0.6.0 — per-tab isolation, Unix socket daemon, on-demand init
|