codex-control-chrome-mcp 1.3.3 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +11 -0
- package/docs/architecture.md +38 -1
- package/docs/troubleshooting.md +109 -0
- package/package.json +1 -1
- package/src/mcp-server.js +75 -20
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ This project follows semantic versioning.
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.4.1] - 2026-07-04
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- The self-heal now recognizes every string the extension uses for a lost debugger attachment, not just Chrome's `Debugger is not attached`. A concurrency race soak (`scripts/concurrency-check.mjs --inject-detach`, which detaches a tab mid-command) surfaced two more that were slipping through unhealed: `Debugger unattached` (the extension's own `se`-map check) and `Detached while handling command.` (detached mid-command). `isNotAttachedError` now matches all three, so `withSession` recovers them too. Under the race soak this took unrecovered "not attached" failures from a handful to zero.
|
|
14
|
+
|
|
15
|
+
## [1.4.0] - 2026-07-04
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- The debugger attach lifecycle now mirrors the official Codex `browser-client.mjs` discipline instead of trusting the extension's idempotent `attach`. A new `ensureAttached` attaches and then issues `Target.setAutoAttach` (`autoAttach`/`flatten`), which keeps child frames/OOPIFs in the session and doubles as a real liveness probe; `withSession` wraps every CDP tool (`chrome_navigate`/`chrome_evaluate`/`chrome_screenshot`/`chrome_cdp_send`) so a stale or lost attachment — whether it surfaces at the probe or mid-command — is healed once via `detach` → re-attach → retry. `chrome_attach_tab` now verifies the attachment with a probe and reports the real result instead of a hardcoded `{ok:true}`. This supersedes the narrower per-tool self-heal added in 1.3.1/1.3.3.
|
|
20
|
+
- `turn_id` is now stable for the lifetime of the MCP process instead of freshly minted on every request. The extension scopes attach/lease state by `session_id` + `turn_id` (`resolveSession(t).attach(requireTurnId(t))`), so a churning turn re-keyed leases and reset session state on every call. One controller uses one stable turn, matching the official client's turn model.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `scripts/concurrency-check.mjs`: a concurrency soak harness that runs N real client sessions against the live bridge and reports unrecovered "not attached" failures (the regression signal) and how many self-heals engaged.
|
|
25
|
+
- [`docs/troubleshooting.md`](docs/troubleshooting.md): a debugging playbook and the reference points (official `browser-client.mjs`, the extension service worker's `se`/attach internals) for future debugger/attach investigations. `docs/architecture.md` gained a "Debugger Attach Lifecycle" section.
|
|
26
|
+
|
|
9
27
|
## [1.3.3] - 2026-07-04
|
|
10
28
|
|
|
11
29
|
### Fixed
|
package/README.md
CHANGED
|
@@ -120,6 +120,7 @@ Because the control entrypoint is inside the normal Chrome profile, existing coo
|
|
|
120
120
|
Read more:
|
|
121
121
|
|
|
122
122
|
- [Architecture](./docs/architecture.md)
|
|
123
|
+
- [Troubleshooting & Debugging Playbook](./docs/troubleshooting.md)
|
|
123
124
|
- [Install And Uninstall](./docs/install.md)
|
|
124
125
|
- [Official Socket Probe Notes](./docs/socket-probe.md)
|
|
125
126
|
- [Release Process](./docs/release.md)
|
|
@@ -166,6 +167,16 @@ npx -y codex-control-chrome-mcp@latest uninstall-native-host
|
|
|
166
167
|
|
|
167
168
|
If tab or CDP tools fail, use the bundled skill workflow: list tabs again, claim only current tab IDs, attach before raw CDP calls, and read CDP events after enabling the relevant domain.
|
|
168
169
|
|
|
170
|
+
### "Debugger is not attached" / flaky CDP under concurrency
|
|
171
|
+
|
|
172
|
+
CDP tools self-heal a stale debugger attachment since 1.4.0 (attach is verified,
|
|
173
|
+
and a lost attachment is recovered with `detach` → re-attach → retry). If you
|
|
174
|
+
still see attach flakiness — most likely when several browser-control stacks
|
|
175
|
+
share one Chrome and contend for the single debugger per tab — see the
|
|
176
|
+
[Troubleshooting & Debugging Playbook](./docs/troubleshooting.md) for the root
|
|
177
|
+
cause, reference points (official client, extension service worker), and the
|
|
178
|
+
`scripts/concurrency-check.mjs` soak test.
|
|
179
|
+
|
|
169
180
|
See [Install And Uninstall](./docs/install.md#troubleshooting) for the full per-browser checklist (the `registered` flag and host `classification`).
|
|
170
181
|
|
|
171
182
|
## License
|
package/docs/architecture.md
CHANGED
|
@@ -170,6 +170,33 @@ CDP events come back as notifications:
|
|
|
170
170
|
}
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
+
## Debugger Attach Lifecycle
|
|
174
|
+
|
|
175
|
+
The extension's `attach` is idempotent on an in-memory set (`background.js`:
|
|
176
|
+
`if(!se.has(t)) chrome.debugger.attach(...)`). If a tab's real debugger is torn
|
|
177
|
+
down out-of-band (a competing debugger client, DevTools, a renderer/process
|
|
178
|
+
swap) and that flag goes stale, `attach` reports success without re-attaching and
|
|
179
|
+
the next CDP command fails with `Debugger is not attached`.
|
|
180
|
+
|
|
181
|
+
Since 1.4.0 the client mirrors the official `browser-client.mjs` discipline
|
|
182
|
+
instead of trusting that `attach`:
|
|
183
|
+
|
|
184
|
+
- `ensureAttached(tabId)` — `attach`, then `Target.setAutoAttach`
|
|
185
|
+
(`autoAttach`/`flatten`), which keeps child frames/OOPIFs in the session and
|
|
186
|
+
doubles as a real liveness probe that surfaces a stale attach.
|
|
187
|
+
- `withSession(tabId, run)` — `ensureAttached` → run the CDP sequence → on
|
|
188
|
+
"not attached" (from the probe or mid-command) `detach` (clears the stale
|
|
189
|
+
flag) → re-`ensureAttached` → retry once. Every CDP tool
|
|
190
|
+
(`chrome_navigate`/`evaluate`/`screenshot`/`cdp_send`) goes through it, and
|
|
191
|
+
`chrome_attach_tab` verifies with a probe instead of returning a hardcoded ok.
|
|
192
|
+
- A **stable per-process `turn_id`**. The extension scopes attach/lease state by
|
|
193
|
+
`session_id` + `turn_id` (`resolveSession(t).attach(requireTurnId(t))`);
|
|
194
|
+
minting a fresh turn per request churned that state.
|
|
195
|
+
|
|
196
|
+
When a debugger/attach issue recurs, start from
|
|
197
|
+
[`docs/troubleshooting.md`](./troubleshooting.md) — it has the reference points
|
|
198
|
+
(official client, extension service worker) and the debugging playbook.
|
|
199
|
+
|
|
173
200
|
## Reusing Codex Logic
|
|
174
201
|
|
|
175
202
|
The preferred long-term reuse point is Codex's `browser-client.mjs`, not the official `extension-host` socket.
|
|
@@ -187,7 +214,17 @@ globalThis.nodeRepl.setResponseMeta()
|
|
|
187
214
|
|
|
188
215
|
This project can shim those APIs and point `nativePipe.createConnection()` at its own bridge socket. That lets the vendored `browser-client.mjs` reuse its existing high-level browser logic while avoiding Codex App as a dependency.
|
|
189
216
|
|
|
190
|
-
The initial implementation exposes direct MCP tools first.
|
|
217
|
+
The initial implementation exposes direct MCP tools first.
|
|
218
|
+
|
|
219
|
+
As of 1.4.0 we chose to **align the lifecycle discipline in our own small client**
|
|
220
|
+
(see "Debugger Attach Lifecycle" above) rather than vendor `browser-client.mjs`.
|
|
221
|
+
Vendoring the 983 KB minified bundle into this MIT-licensed, published package
|
|
222
|
+
would redistribute OpenAI's proprietary code (a licensing question), couple us to
|
|
223
|
+
a specific Codex build, and add an opaque, unreviewable dependency. Re-implementing
|
|
224
|
+
the key mechanisms (`ensureAttached`, `Target.setAutoAttach`, stable turn,
|
|
225
|
+
verified attach, self-heal) gets the same behavior while staying MIT-clean and
|
|
226
|
+
reviewable. Full vendoring remains an option only if the redistribution question
|
|
227
|
+
is resolved.
|
|
191
228
|
|
|
192
229
|
## Runtime And Launch Model
|
|
193
230
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Troubleshooting & Debugging Playbook
|
|
2
|
+
|
|
3
|
+
Where to look when something breaks and the fix isn't obvious — especially
|
|
4
|
+
around the Chrome debugger attach lifecycle, which is the trickiest part of this
|
|
5
|
+
project because we drive `chrome.debugger` **through** the Codex extension rather
|
|
6
|
+
than owning it.
|
|
7
|
+
|
|
8
|
+
## Reference points (read these first)
|
|
9
|
+
|
|
10
|
+
When a debugger/CDP behavior is confusing, the ground truth lives in three
|
|
11
|
+
places, none of which are this repo's tool layer:
|
|
12
|
+
|
|
13
|
+
1. **The official Codex browser client** —
|
|
14
|
+
`~/.codex/plugins/cache/openai-bundled/chrome/latest/scripts/browser-client.mjs`
|
|
15
|
+
(minified). This is how Codex App itself drives the browser. It is the
|
|
16
|
+
reference implementation we align to. Grep it for the lifecycle discipline:
|
|
17
|
+
`ensureAttached`, `Target.setAutoAttach`, `runAttachedFrameSessionInitialization`,
|
|
18
|
+
`preserveDebuggerOnTimeout`, `Target.closeTarget`. Its only export is
|
|
19
|
+
`setupBrowserRuntime`; it expects a `globalThis.nodeRepl` runtime
|
|
20
|
+
(`nativePipe.createConnection`, `requestMeta["x-codex-turn-metadata"]`, `env`,
|
|
21
|
+
`cwd`, `config`, `emitImage`, `setResponseMeta`).
|
|
22
|
+
2. **The extension service worker** —
|
|
23
|
+
`~/Library/Application Support/Google/Chrome/Default/Extensions/hehggadaopoacecdllhhajmbjkdcmajg/<version>/background.js`
|
|
24
|
+
(minified). This is what actually calls `chrome.debugger.*`. Key internals:
|
|
25
|
+
- `function cn(t){ ... if(!se.has(t)){ chrome.debugger.attach({tabId:t},"1.3") } }`
|
|
26
|
+
— `attach` is **idempotent on the in-memory `se` map**. If `se` still has a
|
|
27
|
+
tab whose real debugger was torn down out-of-band, `attach` reports success
|
|
28
|
+
without re-attaching. That stale flag is the root of "Debugger is not
|
|
29
|
+
attached".
|
|
30
|
+
- `chrome.debugger.onDetach.addListener(t=>{ se.delete(t.tabId) })` — clears
|
|
31
|
+
`se` on detach, but can miss races / certain eviction types, leaving `se`
|
|
32
|
+
stale.
|
|
33
|
+
- `resolveSession(t).attach(requireTurnId(t), t)` — attach/lease state is
|
|
34
|
+
scoped by `session_id` + `turn_id`; leases are `{sessionId, turnId, instanceId}`.
|
|
35
|
+
This is why a churning `turn_id` destabilizes session state (see below).
|
|
36
|
+
3. **This repo's design** — [`docs/architecture.md`](./architecture.md), especially
|
|
37
|
+
"Coexistence With Codex App" (the same-name proxy) and "Reusing Codex Logic".
|
|
38
|
+
|
|
39
|
+
## The "Debugger is not attached" failure (root cause)
|
|
40
|
+
|
|
41
|
+
Three layers stack up:
|
|
42
|
+
|
|
43
|
+
1. **Trigger (environment):** a tab's `chrome.debugger` is torn down by a
|
|
44
|
+
competing debugger client (another session, DevTools, a renderer/process
|
|
45
|
+
swap). Only one debugger client is allowed per tab. Running multiple
|
|
46
|
+
browser-control stacks (Codex App + several MCP sessions + Claude's extension)
|
|
47
|
+
makes this common.
|
|
48
|
+
2. **Extension bug:** after that teardown, `se` can stay stale, so `attach`
|
|
49
|
+
lies (`if(!se.has(t))` short-circuit) and the next `sendCommand` throws
|
|
50
|
+
`Debugger is not attached`. A `detach` clears the flag (it may itself throw
|
|
51
|
+
"not attached" — expected), after which `attach` re-attaches for real.
|
|
52
|
+
3. **Our client:** the pre-1.4.0 client fired `attach` and trusted the `{ok}`,
|
|
53
|
+
with no verification and no recovery, so a recoverable state became a dead
|
|
54
|
+
end.
|
|
55
|
+
|
|
56
|
+
The 1.4.0 client mirrors the official lifecycle discipline: `ensureAttached`
|
|
57
|
+
(attach + `Target.setAutoAttach`, which doubles as a real liveness probe),
|
|
58
|
+
`withSession` (ensure → run → on "not attached" `detach` → re-`ensureAttached` →
|
|
59
|
+
retry once), verified `chrome_attach_tab` (no more hardcoded `{ok:true}`), and a
|
|
60
|
+
**stable per-process `turn_id`** instead of a fresh one per request. See
|
|
61
|
+
`src/mcp-server.js` (`ensureAttached` / `withSession`) and `test/mcp-server.test.js`.
|
|
62
|
+
|
|
63
|
+
## Debugging playbook
|
|
64
|
+
|
|
65
|
+
Techniques that isolated the bug, roughly in order:
|
|
66
|
+
|
|
67
|
+
- **Is it concurrency?** `chrome_bridge_status` → `clients` is the number of MCP
|
|
68
|
+
sessions sharing the native host. `proxy: true` means Codex App is proxied
|
|
69
|
+
through us too.
|
|
70
|
+
- **Which stacks are running?** `ps aux | grep -iE "codex|native-host"` — look
|
|
71
|
+
for multiple `codex-control-chrome-mcp` instances (global vs `_npx` cache),
|
|
72
|
+
the official `Codex for Chrome` host, and Claude's extension host.
|
|
73
|
+
- **Tab-specific or global?** Open a **fresh** tab and try it. If the new tab
|
|
74
|
+
works but the old one doesn't, the old tab's attach state is corrupted (stale
|
|
75
|
+
`se`), not the whole extension.
|
|
76
|
+
- **Manual recovery / confirm the mechanism:** on a wedged tab, `chrome_detach_tab`
|
|
77
|
+
(it will error with "not attached" — that's expected and clears the stale flag),
|
|
78
|
+
then `chrome_attach_tab`, then retry. If that revives it, it's the stale-attach bug.
|
|
79
|
+
- **What did the page/debugger do?** `chrome_read_events` dumps buffered CDP
|
|
80
|
+
notifications (Page/Runtime lifecycle). Note: events are **broadcast to all MCP
|
|
81
|
+
clients**, so under concurrency you will see other sessions' tabs too. Large
|
|
82
|
+
dumps are written to a file — analyze with `jq '.events[].params.method'`.
|
|
83
|
+
- **Extension-side detach reasons** are NOT on disk: `chrome.debugger.onDetach`
|
|
84
|
+
is a callback, not a CDP event, so `Inspector.detached` won't appear in
|
|
85
|
+
`chrome_read_events`. To see it you'd need the extension service worker console.
|
|
86
|
+
- **Codex App logs:** `~/Library/Logs/com.openai.codex/<date>/codex-desktop-*.log`.
|
|
87
|
+
- **Concurrency soak:** `node scripts/concurrency-check.mjs [sessions] [rounds]`
|
|
88
|
+
runs N real sessions concurrently through the create → navigate → raw
|
|
89
|
+
`cdp_send` → evaluate → finalize flow and reports unrecovered "not attached"
|
|
90
|
+
(must be 0) plus how many self-heals engaged.
|
|
91
|
+
|
|
92
|
+
## Known limitations
|
|
93
|
+
|
|
94
|
+
- **Session isolation holds, the debugger is shared.** Concurrent sessions can't
|
|
95
|
+
see or close each other's tabs (leases are per `session_id`), but they contend
|
|
96
|
+
for the one debugger-per-tab, so contention raises the eviction rate. The
|
|
97
|
+
self-heal recovers one-shot commands; it does not remove the contention.
|
|
98
|
+
- **Streaming CDP is fragile under contention.** Flows that need the debugger to
|
|
99
|
+
stay attached over time (e.g. `Network.enable` then reading events) can lose
|
|
100
|
+
the attachment mid-stream. Prefer page-side APIs (Resource Timing /
|
|
101
|
+
`PerformanceObserver` via `chrome_evaluate`) for network/DOM observation.
|
|
102
|
+
- **Live tools run the installed package, not this repo.** Editing `src/` does
|
|
103
|
+
nothing until `npm i -g codex-control-chrome-mcp@<version>` **and** the MCP
|
|
104
|
+
client restarts (and the extension reconnects). Beware stale `_npx` copies.
|
|
105
|
+
|
|
106
|
+
## Runtime/release gotchas
|
|
107
|
+
|
|
108
|
+
See [`docs/release.md`](./release.md). Publishing is `git push` of a `v*` tag →
|
|
109
|
+
`.github/workflows/publish.yml` publishes to npm via OIDC (no local `npm login`).
|
package/package.json
CHANGED
package/src/mcp-server.js
CHANGED
|
@@ -60,9 +60,14 @@ function browserLabel(browser) {
|
|
|
60
60
|
return browser === "edge" ? "Edge" : "Chrome";
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
class ChromeUseRuntime {
|
|
63
|
+
export class ChromeUseRuntime {
|
|
64
64
|
constructor() {
|
|
65
65
|
this.sessionId = `codex-control-chrome-mcp-${crypto.randomUUID()}`;
|
|
66
|
+
// Stable per-process turn id. The Codex extension scopes attach/lease state
|
|
67
|
+
// by (session_id, turn_id); minting a fresh turn per request (as we used to)
|
|
68
|
+
// churned that state and re-keyed tab leases on every call. We are a single
|
|
69
|
+
// controller, so one stable turn mirrors the official client's turn model.
|
|
70
|
+
this.turnId = `turn-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
|
|
66
71
|
this.bridge = null;
|
|
67
72
|
}
|
|
68
73
|
|
|
@@ -76,7 +81,7 @@ class ChromeUseRuntime {
|
|
|
76
81
|
sessionParams() {
|
|
77
82
|
return {
|
|
78
83
|
session_id: this.sessionId,
|
|
79
|
-
turn_id:
|
|
84
|
+
turn_id: this.turnId,
|
|
80
85
|
session_context: "codex-control-chrome-mcp",
|
|
81
86
|
};
|
|
82
87
|
}
|
|
@@ -187,7 +192,16 @@ export function registerTools(server, runtime) {
|
|
|
187
192
|
},
|
|
188
193
|
},
|
|
189
194
|
async ({ tabId }) => {
|
|
190
|
-
|
|
195
|
+
// Verify the attachment for real instead of trusting the extension's
|
|
196
|
+
// idempotent `attach` (which can report success on a stale flag). The
|
|
197
|
+
// probe runs a trivial evaluate; withSession heals a stale attach first.
|
|
198
|
+
await withSession(runtime, tabId, () =>
|
|
199
|
+
executeCdp(runtime, {
|
|
200
|
+
tabId,
|
|
201
|
+
method: "Runtime.evaluate",
|
|
202
|
+
params: { expression: "1", returnByValue: true },
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
191
205
|
return toolResult({ ok: true });
|
|
192
206
|
},
|
|
193
207
|
);
|
|
@@ -218,7 +232,7 @@ export function registerTools(server, runtime) {
|
|
|
218
232
|
},
|
|
219
233
|
},
|
|
220
234
|
async ({ tabId, method, params, timeoutMs }) =>
|
|
221
|
-
toolResult(await
|
|
235
|
+
toolResult(await withSession(runtime, tabId, () =>
|
|
222
236
|
executeCdp(runtime, { tabId, method, params, timeoutMs }),
|
|
223
237
|
)),
|
|
224
238
|
);
|
|
@@ -233,7 +247,7 @@ export function registerTools(server, runtime) {
|
|
|
233
247
|
},
|
|
234
248
|
},
|
|
235
249
|
async ({ tabId, url }) =>
|
|
236
|
-
toolResult(await
|
|
250
|
+
toolResult(await withSession(runtime, tabId, async () => {
|
|
237
251
|
await executeCdp(runtime, { tabId, method: "Page.enable", params: {} });
|
|
238
252
|
return await executeCdp(runtime, {
|
|
239
253
|
tabId,
|
|
@@ -254,7 +268,7 @@ export function registerTools(server, runtime) {
|
|
|
254
268
|
},
|
|
255
269
|
},
|
|
256
270
|
async ({ tabId, expression, awaitPromise }) =>
|
|
257
|
-
toolResult(await
|
|
271
|
+
toolResult(await withSession(runtime, tabId, async () => {
|
|
258
272
|
await executeCdp(runtime, { tabId, method: "Runtime.enable", params: {} });
|
|
259
273
|
return await executeCdp(runtime, {
|
|
260
274
|
tabId,
|
|
@@ -278,7 +292,7 @@ export function registerTools(server, runtime) {
|
|
|
278
292
|
},
|
|
279
293
|
},
|
|
280
294
|
async ({ tabId, format }) =>
|
|
281
|
-
toolResult(await
|
|
295
|
+
toolResult(await withSession(runtime, tabId, async () => {
|
|
282
296
|
await executeCdp(runtime, { tabId, method: "Page.enable", params: {} });
|
|
283
297
|
return await executeCdp(runtime, {
|
|
284
298
|
tabId,
|
|
@@ -323,35 +337,76 @@ export function registerTools(server, runtime) {
|
|
|
323
337
|
}
|
|
324
338
|
|
|
325
339
|
/**
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
* The
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
* re-attaches for real. We retry the sequence once on that specific error.
|
|
340
|
+
* Bring `tabId` to a known-attached state, mirroring the official Codex
|
|
341
|
+
* browser-client's discipline instead of trusting the extension's idempotent
|
|
342
|
+
* `attach`. The extension only calls `chrome.debugger.attach` when the tab is
|
|
343
|
+
* absent from its in-memory set (background.js: `if(!se.has(t)) attach()`), so
|
|
344
|
+
* after an out-of-band detach that flag can go stale and `attach` reports
|
|
345
|
+
* success with no live debugger. `Target.setAutoAttach` is a real CDP command:
|
|
346
|
+
* it keeps child frames/OOPIFs in the session (as the official client does) and
|
|
347
|
+
* doubles as a liveness probe that surfaces a stale attach as "not attached".
|
|
335
348
|
*/
|
|
336
|
-
|
|
349
|
+
async function ensureAttached(runtime, tabId) {
|
|
337
350
|
await runtime.request("attach", { tabId });
|
|
338
351
|
try {
|
|
352
|
+
await executeCdp(runtime, {
|
|
353
|
+
tabId,
|
|
354
|
+
method: "Target.setAutoAttach",
|
|
355
|
+
params: { autoAttach: true, flatten: true, waitForDebuggerOnStart: false },
|
|
356
|
+
});
|
|
357
|
+
} catch (error) {
|
|
358
|
+
// A stale attach surfaces here as "not attached"; re-throw so withSession
|
|
359
|
+
// heals it. Any other setAutoAttach failure is best-effort (some targets
|
|
360
|
+
// reject it) and must not block the real command.
|
|
361
|
+
if (isNotAttachedError(error)) throw error;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Ensure the tab is attached, run a CDP sequence, and self-heal once if the
|
|
367
|
+
* debugger is torn down out-of-band (a competing debugger client, DevTools, or a
|
|
368
|
+
* renderer/process swap) — whether that surfaces during the attach probe or
|
|
369
|
+
* mid-command. Recovery is a `detach`, which clears the extension's stale attach
|
|
370
|
+
* flag (it may itself throw "not attached" — expected, and ignored), then a real
|
|
371
|
+
* re-attach via ensureAttached, then one retry.
|
|
372
|
+
*/
|
|
373
|
+
/**
|
|
374
|
+
* Diagnostics for the self-heal path: `heals` counts how often a stale/lost
|
|
375
|
+
* attachment was recovered, `unrecovered` how often recovery still failed.
|
|
376
|
+
* Used by scripts/concurrency-check.mjs to observe behavior under contention.
|
|
377
|
+
*/
|
|
378
|
+
export const sessionStats = { heals: 0, unrecovered: 0 };
|
|
379
|
+
|
|
380
|
+
export async function withSession(runtime, tabId, run) {
|
|
381
|
+
try {
|
|
382
|
+
await ensureAttached(runtime, tabId);
|
|
339
383
|
return await run();
|
|
340
384
|
} catch (error) {
|
|
341
385
|
if (!isNotAttachedError(error)) throw error;
|
|
386
|
+
sessionStats.heals += 1;
|
|
342
387
|
try {
|
|
343
388
|
await runtime.request("detach", { tabId });
|
|
344
389
|
} catch {
|
|
345
390
|
// Detach throws when the debugger is already gone; we only need its side
|
|
346
391
|
// effect of clearing the extension's stale attach flag.
|
|
347
392
|
}
|
|
348
|
-
|
|
349
|
-
|
|
393
|
+
try {
|
|
394
|
+
await ensureAttached(runtime, tabId);
|
|
395
|
+
return await run();
|
|
396
|
+
} catch (retryError) {
|
|
397
|
+
if (isNotAttachedError(retryError)) sessionStats.unrecovered += 1;
|
|
398
|
+
throw retryError;
|
|
399
|
+
}
|
|
350
400
|
}
|
|
351
401
|
}
|
|
352
402
|
|
|
403
|
+
// The extension surfaces a lost debugger attachment with several distinct
|
|
404
|
+
// strings, all of which are recoverable by a detach + re-attach:
|
|
405
|
+
// - "Debugger is not attached to the tab with id: N" (Chrome, via sendCommand)
|
|
406
|
+
// - "Debugger unattached" (extension's own se-check)
|
|
407
|
+
// - "Detached while handling command." (detached mid-command)
|
|
353
408
|
export function isNotAttachedError(error) {
|
|
354
|
-
return /not attached/i.test(error?.message ?? String(error ?? ""));
|
|
409
|
+
return /not attached|unattached|detached while/i.test(error?.message ?? String(error ?? ""));
|
|
355
410
|
}
|
|
356
411
|
|
|
357
412
|
async function executeCdp(runtime, { tabId, method, params = {}, timeoutMs }) {
|