wtagent 0.1.0-alpha.0 → 0.1.0-alpha.2
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/README.md +14 -10
- package/docs/technical-design.md +11 -6
- package/package.json +1 -1
- package/src/browser/cdp-browser.js +235 -52
- package/src/browser/cdp-state.js +459 -0
- package/src/browser/chatgpt-web-adapter.js +201 -26
- package/src/browser/fake-web-model-adapter.js +3 -1
- package/src/cli/main.js +54 -25
- package/src/cli/prompt-input.js +152 -0
- package/src/cli/render-events.js +12 -0
- package/src/runtime/agent-runtime.js +26 -4
- package/src/session/agent-session.js +4 -0
package/README.md
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
# WTAgent
|
|
2
2
|
|
|
3
|
-
Turn
|
|
3
|
+
Turn GPT Web into a local CLI agent.
|
|
4
4
|
|
|
5
|
-
把
|
|
5
|
+
把 GPT 网页聊天变成一个本地 CLI Agent。
|
|
6
6
|
|
|
7
7
|
[中文](#中文) · [English](#english)
|
|
8
8
|
|
|
9
9
|
## 中文
|
|
10
10
|
|
|
11
|
-
WTAgent
|
|
11
|
+
WTAgent 把 GPT 网页聊天连接到本地工具,提供类似 Codex 的 CLI Agent 体验。当前首个适配器支持 ChatGPT Web。
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
网页 GPT 负责思考,WTAgent 在本机:
|
|
14
14
|
|
|
15
15
|
- 读取和修改本地文件
|
|
16
16
|
- 调用本地 Shell,运行构建、测试和开发服务
|
|
17
17
|
|
|
18
|
-
不需要 OpenAI API Key。WTAgent 使用独立的 Chrome Profile
|
|
18
|
+
不需要 OpenAI API Key,也不要求 ChatGPT Pro。WTAgent 使用独立的 Chrome Profile 保存你的网页登录状态,并使用该账号在网页上实际可用的模型和额度;如果账号拥有 Pro,它会成为额外优势。
|
|
19
19
|
|
|
20
20
|
### 快速开始
|
|
21
21
|
|
|
@@ -40,18 +40,20 @@ wtagent "检查这个项目并修复测试"
|
|
|
40
40
|
wtagent -C ./my-project "创建一个网站"
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
之后直接在终端中继续对话。WTAgent 会把任务交给
|
|
43
|
+
之后直接在终端中继续对话。WTAgent 会把任务交给 GPT Web,并在本地执行文件和 Shell 操作。使用 `↑` / `↓` 浏览本次 CLI 会话的历史输入。
|
|
44
|
+
|
|
45
|
+
按 `Ctrl+C` 或 `Ctrl+D` 可退出并关闭专用 Chrome。若上一次异常退出留下了 Chrome,WTAgent 会在验证其 CDP 身份后复用并接管它。
|
|
44
46
|
|
|
45
47
|
## English
|
|
46
48
|
|
|
47
|
-
WTAgent
|
|
49
|
+
WTAgent connects GPT Web chat to local tools, providing a Codex-like CLI agent experience. The first adapter currently supports ChatGPT Web.
|
|
48
50
|
|
|
49
|
-
|
|
51
|
+
GPT Web handles reasoning. WTAgent runs locally to:
|
|
50
52
|
|
|
51
53
|
- Read and edit local files
|
|
52
54
|
- Run shell commands, builds, tests, and development servers
|
|
53
55
|
|
|
54
|
-
No OpenAI API key is required. WTAgent stores
|
|
56
|
+
No OpenAI API key or ChatGPT Pro subscription is required. WTAgent stores your web login in a dedicated Chrome profile and uses the models and quota actually available to that account. Pro is an optional bonus when the account has it.
|
|
55
57
|
|
|
56
58
|
### Quick start
|
|
57
59
|
|
|
@@ -76,7 +78,9 @@ wtagent "inspect this project and fix the tests"
|
|
|
76
78
|
wtagent -C ./my-project "build a website"
|
|
77
79
|
```
|
|
78
80
|
|
|
79
|
-
Continue chatting in the terminal. WTAgent sends tasks to
|
|
81
|
+
Continue chatting in the terminal. WTAgent sends tasks to GPT Web and executes file and shell operations locally. Use `↑` / `↓` to browse input history from the current CLI session.
|
|
82
|
+
|
|
83
|
+
Press `Ctrl+C` or `Ctrl+D` to exit and close the dedicated Chrome. If an abnormal exit leaves Chrome running, WTAgent verifies and adopts that CDP instance on the next start.
|
|
80
84
|
|
|
81
85
|
## License
|
|
82
86
|
|
package/docs/technical-design.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# WTAgent:
|
|
1
|
+
# WTAgent:GPT Web 本地工具 Agent 技术方案
|
|
2
2
|
|
|
3
3
|
## 1. 结论
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- 用户在终端中选择项目目录并输入开发任务。
|
|
8
8
|
- CLI 启动一个独立、可见、持久化 Profile 的 Chrome。
|
|
9
|
-
- 用户首次在该 Chrome 中手动登录自己的 ChatGPT Pro
|
|
9
|
+
- 用户首次在该 Chrome 中手动登录自己的 ChatGPT Web 账号;Pro 不是前提,只是账号可用时的额外模型与额度。
|
|
10
10
|
- ChatGPT Web 只负责推理;本地 Runtime 负责工具、权限、状态和恢复。
|
|
11
11
|
- 双方通过普通聊天文本中的自定义 XML 交换工具调用和结果。
|
|
12
12
|
- Runtime 循环执行“网页回复 → 解析工具 → 本地执行 → 回填结果”,直到任务完成。
|
|
@@ -593,7 +593,7 @@ sessions/<session-id>/
|
|
|
593
593
|
tool-output.jsonl
|
|
594
594
|
```
|
|
595
595
|
|
|
596
|
-
`session.json` 保存项目根目录、ChatGPT 会话 URL
|
|
596
|
+
`session.json` 保存项目根目录、ChatGPT 会话 URL、最近确认的 assistant message ID、最近确认的实际模式、当前 run phase、最近 turn、等待回填的工具结果和副作用恢复日志。它没有不可继续的 `completed task` 状态;`done=true` 只结束当前 run,Session 回到 `idle`。
|
|
597
597
|
|
|
598
598
|
`rollout-*.jsonl` 从创建时起直接使用 **Codex rollout 风格**:首行 `session_meta`,后续每行 `{timestamp, type: "response_item", payload}`,payload 采用 OpenAI Responses 形状(`message` / `function_call` / `function_call_output`)。
|
|
599
599
|
|
|
@@ -626,8 +626,11 @@ sessionId + assistantMessageIdentity + normalizedToolCall
|
|
|
626
626
|
|
|
627
627
|
### 9.3 浏览器恢复
|
|
628
628
|
|
|
629
|
-
- Chrome
|
|
629
|
+
- Chrome 仍在:根据专用 Profile、PID、CDP 端口和健康检查验证身份,复用浏览器并创建新的 Page。
|
|
630
630
|
- Chrome 崩溃:用相同 Profile 重启并打开会话 URL。
|
|
631
|
+
- 同一 CLI 进程中的 follow-up 保持在当前会话 Page,不重复导航;跨进程恢复需要等待本地记录的最近 assistant message ID 出现在 DOM 后才能发送。
|
|
632
|
+
- 每次发送记录已有消息 ID 和新 user turn,只接受位于该 user turn 之后的新 assistant turn;无法建立可靠消息身份时超时并保存诊断,禁止退化为基于数量或文本变化猜测。
|
|
633
|
+
- 同一 Profile 同时只允许一个 WTAgent CLI Session;启动和接管过程使用 Profile 级互斥锁。
|
|
631
634
|
- 登录失效或出现验证:进入 `AUTH_REQUIRED`/`PAUSED`,让用户接管。
|
|
632
635
|
- 会话页面丢失:从本地记录打开原会话;无法恢复时创建新的本地 Session 和新的网页对话,不向旧 rollout 继续追加。
|
|
633
636
|
|
|
@@ -639,6 +642,8 @@ sessionId + assistantMessageIdentity + normalizedToolCall
|
|
|
639
642
|
2. 当前可用工具及参数 Schema。
|
|
640
643
|
3. 用户任务、项目根目录语义和执行边界。
|
|
641
644
|
|
|
645
|
+
同一网页会话中的普通 follow-up 只发送新的用户输入,并在末尾追加短 `<system_reminder>`;不得重复发送完整 `<agent_protocol>`、工具目录或初始任务。只有没有新用户输入的中断恢复流程可以发送完整 resume scaffold。
|
|
646
|
+
|
|
642
647
|
关键规则:
|
|
643
648
|
|
|
644
649
|
- 一轮最多一个工具调用。
|
|
@@ -686,8 +691,8 @@ Session 界面展示:
|
|
|
686
691
|
|
|
687
692
|
用户可随时:
|
|
688
693
|
|
|
689
|
-
- `Ctrl+C`
|
|
690
|
-
-
|
|
694
|
+
- `Ctrl+C` 或 `Ctrl+D` 退出 CLI,并关闭专用 Chrome;
|
|
695
|
+
- 若关闭未完成,保留经过验证的 CDP 状态,下一次启动复用后再次执行关闭;
|
|
691
696
|
- 输入补充指令;
|
|
692
697
|
- 打开浏览器人工接管;
|
|
693
698
|
- 恢复自动化。
|
package/package.json
CHANGED
|
@@ -2,6 +2,16 @@ import net from "node:net";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { chromium } from "playwright-core";
|
|
4
4
|
import { killProcessTree } from "../tools/process-utils.js";
|
|
5
|
+
import {
|
|
6
|
+
acquireCdpProfileLock,
|
|
7
|
+
discoverReusableCdpState,
|
|
8
|
+
fetchCdpVersion,
|
|
9
|
+
processMatchesCdpState,
|
|
10
|
+
reapStaleProfileChrome,
|
|
11
|
+
removeCdpState,
|
|
12
|
+
saveCdpState,
|
|
13
|
+
waitForProcessExit,
|
|
14
|
+
} from "./cdp-state.js";
|
|
5
15
|
|
|
6
16
|
async function reservePort() {
|
|
7
17
|
return await new Promise((resolve, reject) => {
|
|
@@ -22,6 +32,47 @@ async function reservePort() {
|
|
|
22
32
|
});
|
|
23
33
|
}
|
|
24
34
|
|
|
35
|
+
async function settleWithin(promise, timeoutMs) {
|
|
36
|
+
let timer;
|
|
37
|
+
try {
|
|
38
|
+
await Promise.race([
|
|
39
|
+
Promise.resolve(promise).catch(() => null),
|
|
40
|
+
new Promise((resolve) => {
|
|
41
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
42
|
+
}),
|
|
43
|
+
]);
|
|
44
|
+
} finally {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Races a promise against a timeout. On timeout the returned promise rejects
|
|
50
|
+
// with a TIMEOUT-tagged error. Used to bound connectOverCDP + the first CDP
|
|
51
|
+
// round-trip: a Chrome whose profile is locked by a stale instance answers the
|
|
52
|
+
// WS handshake but never finishes protocol init, so an unbounded connect hangs
|
|
53
|
+
// for the full Playwright default (30s) before failing.
|
|
54
|
+
class CdpTimeoutError extends Error {
|
|
55
|
+
constructor(message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "CdpTimeoutError";
|
|
58
|
+
this.code = "CDP_CONNECT_TIMEOUT";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function withTimeout(promise, timeoutMs, message) {
|
|
63
|
+
let timer;
|
|
64
|
+
try {
|
|
65
|
+
return await Promise.race([
|
|
66
|
+
promise,
|
|
67
|
+
new Promise((_, reject) => {
|
|
68
|
+
timer = setTimeout(() => reject(new CdpTimeoutError(message)), timeoutMs);
|
|
69
|
+
}),
|
|
70
|
+
]);
|
|
71
|
+
} finally {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
25
76
|
async function waitForCdp({
|
|
26
77
|
endpoint,
|
|
27
78
|
child,
|
|
@@ -42,7 +93,7 @@ async function waitForCdp({
|
|
|
42
93
|
if (response.ok) {
|
|
43
94
|
const version = await response.json();
|
|
44
95
|
if (version.webSocketDebuggerUrl) {
|
|
45
|
-
return;
|
|
96
|
+
return version;
|
|
46
97
|
}
|
|
47
98
|
}
|
|
48
99
|
} catch (error) {
|
|
@@ -57,21 +108,6 @@ async function waitForCdp({
|
|
|
57
108
|
);
|
|
58
109
|
}
|
|
59
110
|
|
|
60
|
-
async function waitForExit(child, timeoutMs) {
|
|
61
|
-
if (child.exitCode != null || child.signalCode != null) {
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return await new Promise((resolve) => {
|
|
66
|
-
const timer = setTimeout(() => resolve(false), timeoutMs);
|
|
67
|
-
timer.unref?.();
|
|
68
|
-
child.once("close", () => {
|
|
69
|
-
clearTimeout(timer);
|
|
70
|
-
resolve(true);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
111
|
// Sets the OS window state (e.g. "minimized" / "normal") of the window hosting
|
|
76
112
|
// `page` via the CDP Browser domain. On macOS the Chromium launch flags for
|
|
77
113
|
// minimizing (--start-minimized) and off-screen positioning are ignored or
|
|
@@ -101,41 +137,140 @@ export async function launchAndConnectCdpChrome({
|
|
|
101
137
|
profileDir,
|
|
102
138
|
url = "about:blank",
|
|
103
139
|
minimized = false,
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
140
|
+
}, {
|
|
141
|
+
acquireProfileLock = acquireCdpProfileLock,
|
|
142
|
+
connectOverCDP = (endpoint) => chromium.connectOverCDP(endpoint),
|
|
143
|
+
discoverReusable = discoverReusableCdpState,
|
|
144
|
+
fetchVersion = fetchCdpVersion,
|
|
145
|
+
killTree = killProcessTree,
|
|
146
|
+
matchesState = processMatchesCdpState,
|
|
147
|
+
reapStale = reapStaleProfileChrome,
|
|
148
|
+
removeState = removeCdpState,
|
|
149
|
+
reserveCdpPort = reservePort,
|
|
150
|
+
saveState = saveCdpState,
|
|
151
|
+
spawnChrome = spawn,
|
|
152
|
+
waitForExit = waitForProcessExit,
|
|
153
|
+
waitForReady = waitForCdp,
|
|
154
|
+
connectTimeoutMs = 20_000,
|
|
155
|
+
} = {}) {
|
|
156
|
+
const releaseProfileLock = await acquireProfileLock(profileDir);
|
|
157
|
+
let child = null;
|
|
158
|
+
let state = null;
|
|
159
|
+
let browser = null;
|
|
160
|
+
let closePromise = null;
|
|
161
|
+
let reused = false;
|
|
124
162
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
});
|
|
163
|
+
async function findReusable() {
|
|
164
|
+
return await discoverReusable(profileDir, { fetchVersion });
|
|
165
|
+
}
|
|
129
166
|
|
|
130
167
|
try {
|
|
131
|
-
await
|
|
132
|
-
|
|
133
|
-
|
|
168
|
+
state = await findReusable();
|
|
169
|
+
if (state) {
|
|
170
|
+
reused = true;
|
|
171
|
+
} else {
|
|
172
|
+
await removeState(profileDir);
|
|
173
|
+
// A prior instance may have died leaving renderer children (and Chrome's
|
|
174
|
+
// SingletonLock) still holding this profile. Reap those stale holders
|
|
175
|
+
// before launching, or the new Chrome hangs during profile init and
|
|
176
|
+
// connectOverCDP times out.
|
|
177
|
+
await reapStale(profileDir, { fetchVersion, killTree }).catch(() => null);
|
|
178
|
+
const port = await reserveCdpPort();
|
|
179
|
+
const endpoint = `http://127.0.0.1:${port}`;
|
|
180
|
+
child = spawnChrome(
|
|
181
|
+
executablePath,
|
|
182
|
+
[
|
|
183
|
+
`--remote-debugging-port=${port}`,
|
|
184
|
+
"--remote-debugging-address=127.0.0.1",
|
|
185
|
+
`--user-data-dir=${profileDir}`,
|
|
186
|
+
"--profile-directory=Default",
|
|
187
|
+
"--no-first-run",
|
|
188
|
+
"--no-default-browser-check",
|
|
189
|
+
url,
|
|
190
|
+
],
|
|
191
|
+
{
|
|
192
|
+
detached: process.platform !== "win32",
|
|
193
|
+
stdio: "ignore",
|
|
194
|
+
windowsHide: false,
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
await new Promise((resolve, reject) => {
|
|
199
|
+
child.once("spawn", resolve);
|
|
200
|
+
child.once("error", reject);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const version = await waitForReady({ endpoint, child });
|
|
205
|
+
state = await saveState(profileDir, {
|
|
206
|
+
pid: child.pid,
|
|
207
|
+
port,
|
|
208
|
+
endpoint,
|
|
209
|
+
profileDir,
|
|
210
|
+
browser: version.Browser ?? null,
|
|
211
|
+
webSocketDebuggerUrl: version.webSocketDebuggerUrl,
|
|
212
|
+
});
|
|
213
|
+
} catch (error) {
|
|
214
|
+
// Chrome may forward the URL to an existing process using the same
|
|
215
|
+
// profile and then exit successfully. Re-scan after that handoff and
|
|
216
|
+
// adopt the verified live CDP instance instead of reporting a false
|
|
217
|
+
// launch failure.
|
|
218
|
+
if (child.exitCode === 0 && child.signalCode == null) {
|
|
219
|
+
state = await findReusable();
|
|
220
|
+
if (state) {
|
|
221
|
+
reused = true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!state) {
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Bound the connect + first CDP round-trip. If Chrome's profile is held by
|
|
231
|
+
// a stale instance, the WS connects but protocol init never completes;
|
|
232
|
+
// without this guard Playwright hangs ~30s and leaves a dirty CDP state.
|
|
233
|
+
let context;
|
|
234
|
+
try {
|
|
235
|
+
browser = await withTimeout(
|
|
236
|
+
connectOverCDP(state.endpoint),
|
|
237
|
+
connectTimeoutMs,
|
|
238
|
+
`Timed out connecting to Chrome CDP at ${state.endpoint} after ${connectTimeoutMs}ms.`,
|
|
239
|
+
);
|
|
240
|
+
// contexts() forces a real protocol round-trip, so it hangs too when the
|
|
241
|
+
// browser main thread is stuck — keep it inside the timeout budget.
|
|
242
|
+
const contexts = await withTimeout(
|
|
243
|
+
Promise.resolve().then(() => browser.contexts()),
|
|
244
|
+
connectTimeoutMs,
|
|
245
|
+
`Timed out reading Chrome browser context at ${state.endpoint}.`,
|
|
246
|
+
);
|
|
247
|
+
context = contexts[0];
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (error instanceof CdpTimeoutError) {
|
|
250
|
+
// The verified-but-unusable instance we launched is a dead end. Kill it
|
|
251
|
+
// (only if we own it) and drop its CDP state so the next run starts
|
|
252
|
+
// clean instead of trying to reuse a hung endpoint.
|
|
253
|
+
await browser?.close().catch(() => null);
|
|
254
|
+
if (!reused && child?.pid) {
|
|
255
|
+
await killTree(child.pid).catch(() => null);
|
|
256
|
+
}
|
|
257
|
+
await removeState(profileDir, state).catch(() => null);
|
|
258
|
+
throw new Error(
|
|
259
|
+
`${error.message} The Chrome profile may be held by another instance. `
|
|
260
|
+
+ "Close other windows using this profile, or run `wtagent logout` to reset it, then retry.",
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
134
265
|
if (!context) {
|
|
135
266
|
throw new Error("Chrome CDP connection did not expose a browser context.");
|
|
136
267
|
}
|
|
137
268
|
|
|
138
|
-
|
|
269
|
+
// A reused browser may still contain the previous conversation. Keep it
|
|
270
|
+
// intact and create a fresh target for this CLI session.
|
|
271
|
+
const page = reused
|
|
272
|
+
? await context.newPage()
|
|
273
|
+
: context.pages()[0] ?? await context.newPage();
|
|
139
274
|
if (minimized) {
|
|
140
275
|
await setWindowState(context, page, "minimized");
|
|
141
276
|
}
|
|
@@ -144,27 +279,75 @@ export async function launchAndConnectCdpChrome({
|
|
|
144
279
|
browser,
|
|
145
280
|
context,
|
|
146
281
|
child,
|
|
147
|
-
endpoint,
|
|
282
|
+
endpoint: state.endpoint,
|
|
283
|
+
page,
|
|
284
|
+
pid: state.pid,
|
|
285
|
+
reused,
|
|
148
286
|
// Minimize / restore the visible window on demand. The runtime restores
|
|
149
287
|
// the window when it needs the user (manual login, CAPTCHA) and
|
|
150
288
|
// re-minimizes afterward. Uses the live current page each time so it
|
|
151
289
|
// targets the window the user is actually looking at.
|
|
152
290
|
async minimize() {
|
|
153
|
-
return await setWindowState(context,
|
|
291
|
+
return await setWindowState(context, page, "minimized");
|
|
154
292
|
},
|
|
155
293
|
async restore() {
|
|
156
|
-
return await setWindowState(context,
|
|
294
|
+
return await setWindowState(context, page, "normal");
|
|
157
295
|
},
|
|
158
296
|
async close() {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
await killProcessTree(child.pid);
|
|
162
|
-
await waitForExit(child, 2_000);
|
|
297
|
+
if (closePromise) {
|
|
298
|
+
return await closePromise;
|
|
163
299
|
}
|
|
300
|
+
closePromise = (async () => {
|
|
301
|
+
let exited = false;
|
|
302
|
+
try {
|
|
303
|
+
// browser.close() on a connectOverCDP browser only disconnects the
|
|
304
|
+
// Playwright transport. Browser.close asks Chrome itself to exit.
|
|
305
|
+
const session = await browser.newBrowserCDPSession()
|
|
306
|
+
.catch(() => null);
|
|
307
|
+
if (session) {
|
|
308
|
+
await settleWithin(session.send("Browser.close"), 1_500);
|
|
309
|
+
await settleWithin(session.detach(), 500);
|
|
310
|
+
}
|
|
311
|
+
exited = await waitForExit(state.pid, 3_000);
|
|
312
|
+
await settleWithin(browser.close(), 1_500);
|
|
313
|
+
|
|
314
|
+
if (!exited) {
|
|
315
|
+
const childStillOwnsPid = child?.pid === state.pid
|
|
316
|
+
&& child.exitCode == null
|
|
317
|
+
&& child.signalCode == null;
|
|
318
|
+
const safeToKill = childStillOwnsPid
|
|
319
|
+
|| await matchesState(state);
|
|
320
|
+
if (safeToKill) {
|
|
321
|
+
await killTree(state.pid);
|
|
322
|
+
exited = await waitForExit(state.pid, 3_000);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (exited) {
|
|
327
|
+
await removeState(profileDir, state);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Chrome pid=${state.pid} did not exit; its verified CDP state `
|
|
332
|
+
+ "was kept so the next WTAgent run can reuse it.",
|
|
333
|
+
);
|
|
334
|
+
} finally {
|
|
335
|
+
await releaseProfileLock();
|
|
336
|
+
}
|
|
337
|
+
})();
|
|
338
|
+
return await closePromise;
|
|
164
339
|
},
|
|
165
340
|
};
|
|
166
341
|
} catch (error) {
|
|
167
|
-
await
|
|
342
|
+
await browser?.close().catch(() => null);
|
|
343
|
+
if (
|
|
344
|
+
child
|
|
345
|
+
&& child.exitCode == null
|
|
346
|
+
&& child.signalCode == null
|
|
347
|
+
) {
|
|
348
|
+
await killTree(child.pid);
|
|
349
|
+
}
|
|
350
|
+
await releaseProfileLock();
|
|
168
351
|
throw error;
|
|
169
352
|
}
|
|
170
353
|
}
|