Skip to content

Harden the browser companion and support parallel action pages - #22

Open
LeZergan wants to merge 1 commit into
devnoname120:mainfrom
LeZergan:pr/browser-companion
Open

LeZergan wants to merge 1 commit into
devnoname120:mainfrom
LeZergan:pr/browser-companion

Conversation

@LeZergan

@LeZergan LeZergan commented Nov 12, 2026 •

Copy link
Copy Markdown

Summary

Split out of #20, as requested. This PR covers the browser companion (TypeScript) plus the small native protocol change it needs. It is based on current main (v2.3.29) and merged with upstream's recent companion work, so there are no conflicts.

Changes

Parallel action pages (protocol)

  • browser-pool.ts can run up to four fixed ChatGPT pages in one private Chrome profile. Each conversation has at most one action at a time.
  • Maintenance (capabilities, account plan, connector refresh, login) waits for every page to be idle. Global HTTP 429 back-off and the actual Send spacing stay shared across pages.
  • Health gains the optional fields actionCapacity, availableActionSlots and activeActionIds. A companion that leaves them out behaves exactly as before.
  • GET /v1/health?ready=1 waits for fresh proof that a session is idle. Ordinary health stays bounded and passive.
  • The schema hash and fixtures were regenerated. The native side (companion.rs) validates the new fields.
  • The default is still one page. The native configuration for more pages comes in a follow-up PR.

Windows Chrome

  • windows-chrome-recovery.ts recovers an orphaned or stale Chrome left on the private profile.
  • windows-priority.ts normalizes renderer process priority.
  • renderer-memory.ts bounds renderer memory on long sessions.
  • shutdown.ts adds an orderly shutdown.
  • process_is_alive on Windows now waits on the process handle. Before, an exited process whose handle was still open counted as alive.

Send reliability

  • conversation-readiness.ts: waits for ChatGPT's editor to hydrate after navigation (long chats can take a while). A missing editor is not treated as a sign-out.
  • editor-recovery.ts: refreshes a stuck editor once before Send, if the opt-in recovery setting is enabled.
  • draft-recovery.ts: journals our own unsent drafts, so a retry can reclaim them without discarding anything the user typed.
  • stop-controls.ts: verifies the Stop and End-turn controls.
  • submission-network.ts: optional network proof of a Send.
  • approval-monitor.ts: watches for tool-approval cards.
  • Rate-limit cooldowns and Retry-After survive a companion restart. Upstream's transcript-load rate-limit detection (conversation_load_rate_limited) is kept alongside them.

Lighter, cheaper page observation

  • observation.ts bounds read-only DOM reads, and browser-window.ts adds a short-lived passive health cache. Together they stop polls from competing with an action for Chrome's main thread.
  • A cancelled read can keep its window lease until it settles, instead of tearing down a live browser.
  • minimal-page.ts, presentation*.ts: optional Simplified and Bones presentation levels (off by default).

Merged with upstream

  • Upstream's acknowledge-then-preflight admission, pairing-window reservation (refresh-pairing, release-pairing), chat-mode compatibility, conversation-limit detection, model-picker refactor and stage diagnostics are all kept.
  • Tests that assumed the old synchronous preflight were updated to wait for the asynchronous one.

Testing

  • bun test ./tests (Bun 1.4.0, Windows 11): 309 pass, 0 fail. The other 161 tests need a live Chrome and are skipped by design.
  • tsc --noEmit: passes.
  • The protocol fixture generator reproduces the committed fixtures.
  • cargo check --all-targets on this branch alone: passes.

🤖 Generated with Claude Code

Adds an optional pool of up to four fixed ChatGPT pages in one private Chrome
profile (one action per conversation; maintenance drains every page; shared
429 back-off and Send spacing), with optional health fields describing
capacity. The companion still defaults to one page.

Also recovers orphaned Windows Chrome, normalizes renderer priority, waits for
editor hydration instead of treating a missing editor as sign-out, journals
owned unsent drafts, bounds read-only DOM observations and passive health
polls, persists Retry-After across restarts, and adds optional Simplified and
Bones presentation levels. Upstream's admission, pairing-window reservation,
chat-mode, conversation-limit and transcript rate-limit handling are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@devnoname120 devnoname120 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1105978, including pooled admission/idempotency, shared navigation/Send budgets, page ownership, draft handling and packaging. Requesting changes for reproducible regressions.

The strongest blocker is the connector-selection caret regression: the existing native pairing test passes on main and fails on this head before Send. In an isolated control, restoring the caret to the composer end immediately after the new row click makes the same test pass; that temporary diagnostic change was reverted. A second native regression prematurely times out maintenance requests queued behind a healthy connector refresh. Please repair these before merging the reliability/pool changes.

Validation: bun test ./tests: 305 passed, 163 skipped, 2 failed (new package staging tests on macOS). Typecheck, build and all four boundary tests passed. The native chrome-surface and conversation-approval suites passed (44 tests); the pairing suite's first failure causes cascading failures, so I do not count the later cascade as independent defects. Separately reran the first pairing case: main passes, this head fails, the caret control passes. The account-settings suite passes all 7 tests on main; this head reproduces the same maintenance failure seen in CI. Current CI also reports two billing-plan assertion failures that must be reconciled.

Recommendation: not ready. The ownership/retained-cleanup work is substantial and has meaningful tests, but the common pairing path regresses and the native browser tests need to be green.

const selected = await activeComposer(page, 30_000, signal);
// Click the verified app row directly. Enter in the composer can submit the
// mention as a message if React closes/rebuilds the menu between checks.
await exactRow.click({ timeout:CONNECTOR_ACTION_TIMEOUT_MS, signal });

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Restore the composer caret after clicking the connector row

Changing selection from keyboard confirmation to a row click leaves the editor's selection at the old query position. insertPlainTextIntoComposer preserves any collapsed selection already inside the editor, so the subsequent prompt can be inserted before the connector chip rather than at the intended end. On macOS the existing "pairing waits for enabled Send and binds to the new answer after history hydration" test now fails with external_interference before Send: the projected text is the exact prompt followed by an extra newline/space around the retained chip. The same test passes on main. As a control, moving the range to the composer end immediately after this click makes it pass on this head. Keep the click's safety benefit, but explicitly establish the insertion caret and cover this with the native pairing regression.

queuedAbort=()=>reject(new BrowserCompanionError('Browser action was cancelled','cancelled'));
if(signal?.aborted){queuedAbort();return;}
signal?.addEventListener('abort',queuedAbort,{once:true});
queuedTimer=setTimeout(()=>reject(new BrowserCompanionError('Browser companion is busy; a previous operation still owns the shared window')),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Do not expire queued maintenance before its valid predecessor finishes

The queue deadline is based on the waiting operation's timeoutMs, even when the current owner has a longer legitimate deadline. Connector refresh can hold its lease for 120 seconds, but an account-plan/reveal request queued behind it expires much earlier. The existing native account-settings scenario demonstrates this without a stuck operation: operationMs=1500, cleanupMs=100, hardStopMs=100 gives queued callers 1700 ms, while refreshDelay=2200 is valid and pendingRefresh completes normally. This head throws "a previous operation still owns the shared window"; the corresponding main test passes. Preserve bounded cancellation/abandoned-entry ordering while allowing the supported predecessor to settle, and rerun the queued plan/reveal regression.

const root = fs.realpathSync(scratch);
const source = fs.realpathSync(unpacked);
const target = path.resolve(appRoot);
const within = candidate => {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Compare staging paths in a consistent real-path namespace

root and source are realpath-resolved, but target remains only path.resolve(appRoot). With a symlinked temporary directory, these name the same scratch tree using different prefixes, so within(target) rejects a valid destination. On macOS the new package-stage tests fail under the normal /var -> /private/var temporary-directory spelling: bun test ./tests reports two failures, including the basic successful-move case. Resolve the target's existing parent chain into the same namespace before the containment check while retaining the linked-parent rejection; the normal and escaped-parent tests should both pass on macOS as well as Windows.

This branch has not been deployed

No deployments
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