fix(council): verify CDP endpoint identity before attaching; recover orphaned results - #71
Draft
intellegix wants to merge 1 commit into
Draft
fix(council): verify CDP endpoint identity before attaching; recover orphaned results#71intellegix wants to merge 1 commit into
intellegix wants to merge 1 commit into
Conversation
…orphaned results
Machine-wide Perplexity research outage on 2026-08-07: four consecutive
research_query runs failed at ~128s with "Page.wait_for_timeout: Target page,
context or browser has been closed".
Root cause: the runner attached to the wrong Chrome. _start_via_cdp() treated
"port 9222 answers /json/version" as proof the session keeper was alive. Three
facts lined up: the PerplexitySessionKeeper task has been Disabled since 07-30;
a stale session_keeper.cdp from 08-02 still pointed at 9222 recording a PID dead
since the reboot; and browser-relay/relay.mjs (the /takeover phone relay) claimed
port 9222 at 12:57 with a throwaway C:\Temp\igx-cdp-profile. From 12:57 the health
check passed against the relay's cookie-less Chrome, so every run navigated to a
logged-out wall and was torn down ~128s later. Last success 11:49 (pre-reboot,
launch path); relay up 12:57; first failure 13:19.
A reachable port was being treated as a trusted port.
council_browser.py — prove endpoint identity before attaching, layered so an
indeterminate probe degrades to the next check rather than blocking a healthy
keeper:
- recorded PID must be alive (stale .cdp file is deleted)
- DevToolsActivePort GUID in the keeper's profile must match the endpoint's
webSocketDebuggerUrl (canonical launch-time/discovery-time match)
- failing that, the port owner's command line must name session_keeper_profile
- post-attach, the context must carry a Perplexity auth cookie — which also
catches a live keeper whose login has expired
The "synthesize a .cdp file because 9222 answers" fast path is gated behind the
same ownership check. Any refusal falls back to local launch, which is what was
working before 12:57.
council_query.py — invocation_id is a fresh UUID per call, so a caller dying
orphans a completed result (council_fae5c9e0.json). Results now carry a
query_fingerprint and run_browser_query checks for a matching, usable, recent
result before re-running. Age comes from the recorded timestamp rather than file
mtime, so re-saving a recovered orphan cannot keep it alive indefinitely.
Verified: first post-patch run completed in 159.5s where the two runs before it
failed at 128s. Gates unit-tested against live machine state — refuse the relay's
Chrome, wrong GUID and wrong port; accept a simulated healthy keeper. Orphan
recovery verified against the real council_fae5c9e0.json.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VsT8c4wZQYv2fRN6szvEQ6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the machine-wide Perplexity research outage of 2026-08-07 (four consecutive
research_queryfailures at ~128s, blocking the mandated research pipeline for~17 live sessions).
Root cause — the runner attached to the wrong Chrome
_start_via_cdp()treated "port 9222 answers/json/version" as proof the sessionkeeper was alive. It isn't — any process can serve plausible JSON on a port. Three
facts lined up:
PerplexitySessionKeeperhas been Disabled since 07-30 (1451 missed runs).Research kept working because CDP attach quietly failed and the runner fell back
to its local-launch path.
session_keeper.cdpfrom 08-02 still pointed at 9222, recording a PIDdead since the reboot. Nothing invalidated it.
browser-relay/relay.mjs(the/takeoverphone relay) claimed port 9222 at12:57, on a throwaway
C:\Temp\igx-cdp-profile.From 12:57 the health check passed against the relay's Chrome. Every run attached to
a profile with zero Perplexity cookies, hit a logged-out wall, and was torn down ~128s
later. Timeline is exact: last success 11:49 (pre-reboot), relay up 12:57,
first failure 13:19.
This also explains the misleading symptoms:
BROWSER_BUSYwithactive: null(slotsleaked by runs dying inside the doomed attach), and "a dead browser that killed two
other sessions' queries" (attach closes every
/search/page in the shared context).The reboot was a trigger, not the cause. A reachable port was being treated as a
trusted port.
Changes
council_browser.py— prove endpoint identity before attaching, layered so anindeterminate probe degrades to the next check rather than blocking a healthy keeper:
.cdpfile is deleted)DevToolsActivePortGUID in the keeper's profile must match the endpoint'swebSocketDebuggerUrl— the canonical launch-time↔discovery-time matchsession_keeper_profilea live keeper whose login expired, previously indistinguishable from this failure
The "synthesize a
.cdpfile because 9222 answers" fast path is gated behind the sameownership check. Any refusal falls back to local launch.
council_query.py—invocation_idis a fresh UUID per call, so a caller dyingorphans a completed result (this happened today:
council_fae5c9e0.json). Results nowcarry a
query_fingerprint;run_browser_querychecks for a matching, usable, recentresult before re-running. Age comes from the recorded
timestamprather than filemtime, so re-saving a recovered orphan can't keep it alive indefinitely. 2h window,
bounded scan, error stubs never served, opt out with
COUNCIL_NO_ORPHAN_RECOVERY=1.Verification
Patch landed 14:07:55 PT. From
perplexity-queue.json:stale-PID and foreign-owner paths; refuse wrong-GUID and wrong-port; accept a
simulated healthy keeper, so the CDP path still works when the keeper returns.
council_fae5c9e0.json— recovers theidentical synthesis, respects the 2h window, immune to mtime reset, returns
Nonefor an unseen query.
repeated query returned instantly from cache.
Not changed here (flagged for a deliberate decision)
session_keeper.pyandbrowser-relay/relay.mjs,whose header documents 9222 as its takeover contract. If the keeper is re-enabled
while the relay is up, the keeper will fail to bind. Latent collision; needs an
explicit port-registry assignment.
BROWSER_BUSYvsBROWSER_DEADstill aren't distinguished caller-side.🤖 Generated with Claude Code
https://claude.ai/code/session_01VsT8c4wZQYv2fRN6szvEQ6