negotium 0.1.37 → 0.1.39
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 +19 -0
- package/dist/agent-helpers.js +15637 -5674
- package/dist/agent-helpers.js.map +85 -37
- package/dist/browser-runtime.js +1600 -0
- package/dist/browser-runtime.js.map +28 -0
- package/dist/hosted-agent.js +8 -8
- package/dist/hosted-agent.js.map +6 -5
- package/dist/main.js +3790 -3113
- package/dist/main.js.map +33 -29
- package/dist/mcp-factories.js +15141 -301
- package/dist/mcp-factories.js.map +100 -7
- package/dist/prompts.js +117 -49
- package/dist/prompts.js.map +3 -3
- package/dist/query-runtime.js +310 -1
- package/dist/query-runtime.js.map +10 -5
- package/dist/runtime/src/agents/archiver.ts +407 -296
- package/dist/runtime/src/agents/mcp-tools/ask-user.ts +440 -274
- package/dist/runtime/src/agents/mcp-tools/self-config.ts +139 -65
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +1037 -816
- package/dist/runtime/src/agents/public-helpers.ts +82 -0
- package/dist/runtime/src/agents/self-config-core.ts +574 -278
- package/dist/runtime/src/agents/topic-cleanup.ts +167 -129
- package/dist/runtime/src/index.ts +4 -1
- package/dist/runtime/src/mcp/factories/index.ts +9 -0
- package/dist/runtime/src/mcp/session-comm/topic-catalog.ts +132 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +23 -57
- package/dist/runtime/src/platform/mcp-config.ts +2 -4
- package/dist/runtime/src/platform/playwright/browser-processes.ts +3 -2
- package/dist/runtime/src/platform/playwright/capability.ts +5 -0
- package/dist/runtime/src/platform/playwright/manager.ts +380 -66
- package/dist/runtime/src/platform/playwright/public-runtime.ts +45 -0
- package/dist/runtime/src/platform/playwright/transport-probe.ts +89 -0
- package/dist/runtime/src/prompts/builders.ts +192 -51
- package/dist/runtime/src/query/active-rooms.ts +2 -0
- package/dist/runtime/src/query/public-runtime.ts +8 -0
- package/dist/runtime/src/query/state.ts +69 -33
- package/dist/runtime/src/query/types.ts +1 -0
- package/dist/runtime/src/runtime/playwright-turn-abort.ts +59 -0
- package/dist/runtime/src/runtime/turn-event-stream.ts +23 -6
- package/dist/runtime/src/runtime/turn-runner.ts +6 -1
- package/dist/runtime/src/version.ts +1 -1
- package/dist/types/packages/core/src/agents/api-topic-agent-switch.d.ts +38 -0
- package/dist/types/packages/core/src/agents/archiver.d.ts +52 -16
- package/dist/types/packages/core/src/agents/mcp-tools/ask-user.d.ts +75 -4
- package/dist/types/packages/core/src/agents/mcp-tools/self-config.d.ts +17 -0
- package/dist/types/packages/core/src/agents/mcp-tools/spawn-subagent.d.ts +112 -25
- package/dist/types/packages/core/src/agents/public-helpers.d.ts +7 -0
- package/dist/types/packages/core/src/agents/self-config-core.d.ts +185 -0
- package/dist/types/packages/core/src/agents/topic-cleanup.d.ts +30 -24
- package/dist/types/packages/core/src/mcp/factories/index.d.ts +1 -0
- package/dist/types/packages/core/src/mcp/session-comm/topic-catalog.d.ts +46 -0
- package/dist/types/packages/core/src/platform/mcp-config.d.ts +2 -1
- package/dist/types/packages/core/src/platform/playwright/capability.d.ts +1 -0
- package/dist/types/packages/core/src/platform/playwright/manager.d.ts +65 -5
- package/dist/types/packages/core/src/platform/playwright/public-runtime.d.ts +4 -0
- package/dist/types/packages/core/src/platform/playwright/transport-probe.d.ts +13 -0
- package/dist/types/packages/core/src/prompts/builders.d.ts +29 -3
- package/dist/types/packages/core/src/query/active-rooms.d.ts +2 -0
- package/dist/types/packages/core/src/query/public-runtime.d.ts +1 -0
- package/dist/types/packages/core/src/query/state.d.ts +13 -0
- package/dist/types/packages/core/src/query/types.d.ts +2 -1
- package/dist/types/packages/core/src/runtime/playwright-turn-abort.d.ts +12 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/package.json +6 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ChildProcess, execFileSync, spawn } from "node:child_process";
|
|
2
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import {
|
|
4
4
|
cpSync,
|
|
5
5
|
existsSync,
|
|
@@ -14,6 +14,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
14
14
|
import {
|
|
15
15
|
BROWSER_PROFILES_DIR,
|
|
16
16
|
BROWSER_RS_BIN,
|
|
17
|
+
type BrowserProxyConfig,
|
|
17
18
|
PATCHRIGHT_MCP_BIN,
|
|
18
19
|
PLAYWRIGHT_BASE_PORT,
|
|
19
20
|
PLAYWRIGHT_MAX_PORT,
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
type HeadedPlaywrightSpawnSpec,
|
|
37
38
|
resolveHeadedPlaywrightSpawn,
|
|
38
39
|
} from "#platform/playwright/headed-launch";
|
|
40
|
+
import { probePlaywrightMcpTransports } from "#platform/playwright/transport-probe";
|
|
39
41
|
import { sanitizeTopicName } from "#security/sanitize";
|
|
40
42
|
import {
|
|
41
43
|
assignTopicBrowserProfile,
|
|
@@ -51,11 +53,48 @@ import {
|
|
|
51
53
|
waitForChildProcessSpawnError,
|
|
52
54
|
} from "./manager-utils";
|
|
53
55
|
|
|
56
|
+
export interface PlaywrightProfileBinding {
|
|
57
|
+
readonly instanceKey: string;
|
|
58
|
+
readonly ownerId: string;
|
|
59
|
+
readonly profile: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PlaywrightChildEnvironmentContext {
|
|
63
|
+
instanceKey: string;
|
|
64
|
+
ownerId: string;
|
|
65
|
+
capability: string;
|
|
66
|
+
proxy: BrowserProxyConfig | null;
|
|
67
|
+
browserRsBin?: string;
|
|
68
|
+
environment: NodeJS.ProcessEnv;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface PlaywrightManagerHost {
|
|
72
|
+
readonly portsDir: string;
|
|
73
|
+
readonly basePort: number;
|
|
74
|
+
readonly maxPort: number;
|
|
75
|
+
readonly browserBin: string;
|
|
76
|
+
readonly fallbackBrowserBin: string;
|
|
77
|
+
readonly browserRsBin?: string;
|
|
78
|
+
readonly resolveProxy: () => BrowserProxyConfig | null;
|
|
79
|
+
readonly resolveTopicBinding: (userId: string, topic?: string) => PlaywrightProfileBinding;
|
|
80
|
+
readonly resolveNamedBinding: (ownerId: string, rawProfile: string) => PlaywrightProfileBinding;
|
|
81
|
+
readonly resolveInstanceDataDir: (instanceKey: string) => string;
|
|
82
|
+
readonly createChildEnvironment: (
|
|
83
|
+
context: PlaywrightChildEnvironmentContext,
|
|
84
|
+
) => NodeJS.ProcessEnv;
|
|
85
|
+
/** Reap an orphan that owns this exact host-managed data directory. */
|
|
86
|
+
readonly cleanupBrowserProcessesForDataDir: (userDataDir: string) => void;
|
|
87
|
+
/** Reap host-managed browsers that are not represented by the supplied live directories. */
|
|
88
|
+
readonly reapOrphanBrowsers: (liveUserDataDirs: Iterable<string>) => void;
|
|
89
|
+
}
|
|
90
|
+
|
|
54
91
|
export {
|
|
55
92
|
cleanupZombiePlaywright,
|
|
56
93
|
isBrowserJanitorOwner,
|
|
94
|
+
reapOrphanBrowsers,
|
|
57
95
|
selectOrphanBrowserPids,
|
|
58
96
|
} from "#platform/playwright/browser-processes";
|
|
97
|
+
export { probePlaywrightMcpTransports } from "#platform/playwright/transport-probe";
|
|
59
98
|
export {
|
|
60
99
|
browserProcessMatchesExpectedProfile,
|
|
61
100
|
extractUserDataDirArg,
|
|
@@ -68,14 +107,57 @@ export { PLAYWRIGHT_PORTS_DIR };
|
|
|
68
107
|
|
|
69
108
|
/** Multiple topics assigned to one profile reuse one browser process. */
|
|
70
109
|
export function makeInstanceKey(userId: string, topic: string | undefined): string {
|
|
71
|
-
|
|
110
|
+
return resolvePlaywrightTopicBinding(userId, topic).instanceKey;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function resolvePlaywrightTopicBinding(
|
|
114
|
+
userId: string,
|
|
115
|
+
topic: string | undefined,
|
|
116
|
+
): PlaywrightProfileBinding {
|
|
117
|
+
return validateProfileBinding(managerHost.resolveTopicBinding(userId, topic));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function defaultTopicBinding(userId: string, topic: string | undefined): PlaywrightProfileBinding {
|
|
121
|
+
if (!topic) return managerHost.resolveNamedBinding(userId, "default");
|
|
72
122
|
const ownerId = getBrowserProfileOwner(topic, userId);
|
|
73
123
|
const profile = migrateLegacyTopicProfile(ownerId, topic);
|
|
74
|
-
return
|
|
124
|
+
return managerHost.resolveNamedBinding(ownerId, profile);
|
|
75
125
|
}
|
|
76
126
|
|
|
77
127
|
export function makeBrowserProfileInstanceKey(ownerId: string, rawProfile: string): string {
|
|
78
|
-
return
|
|
128
|
+
return resolvePlaywrightProfileBinding(ownerId, rawProfile).instanceKey;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function resolvePlaywrightProfileBinding(
|
|
132
|
+
ownerId: string,
|
|
133
|
+
rawProfile: string,
|
|
134
|
+
): PlaywrightProfileBinding {
|
|
135
|
+
return validateProfileBinding(managerHost.resolveNamedBinding(ownerId, rawProfile));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function validateProfileBinding(binding: PlaywrightProfileBinding): PlaywrightProfileBinding {
|
|
139
|
+
if (
|
|
140
|
+
!binding ||
|
|
141
|
+
typeof binding.instanceKey !== "string" ||
|
|
142
|
+
!binding.instanceKey.trim() ||
|
|
143
|
+
typeof binding.ownerId !== "string" ||
|
|
144
|
+
!binding.ownerId.trim() ||
|
|
145
|
+
typeof binding.profile !== "string" ||
|
|
146
|
+
!binding.profile.trim()
|
|
147
|
+
) {
|
|
148
|
+
throw new Error("invalid Playwright profile binding");
|
|
149
|
+
}
|
|
150
|
+
return Object.freeze({ ...binding });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function defaultNamedBinding(ownerId: string, rawProfile: string): PlaywrightProfileBinding {
|
|
154
|
+
const profile = normalizeBrowserProfileName(rawProfile);
|
|
155
|
+
const instanceKey = `profile:${encodeURIComponent(ownerId)}:${profile}`;
|
|
156
|
+
return {
|
|
157
|
+
instanceKey,
|
|
158
|
+
ownerId,
|
|
159
|
+
profile,
|
|
160
|
+
};
|
|
79
161
|
}
|
|
80
162
|
|
|
81
163
|
export function legacyBrowserProfileName(topic: string): string {
|
|
@@ -90,7 +172,7 @@ function migrateLegacyTopicProfile(ownerId: string, topic: string): string {
|
|
|
90
172
|
if (!existsSync(legacyDir)) return current;
|
|
91
173
|
|
|
92
174
|
const profile = legacyBrowserProfileName(topic);
|
|
93
|
-
const profileDir =
|
|
175
|
+
const profileDir = defaultProfileDir(ownerId, profile);
|
|
94
176
|
mkdirSync(dirname(profileDir), { recursive: true });
|
|
95
177
|
if (!existsSync(profileDir)) renameSync(legacyDir, profileDir);
|
|
96
178
|
assignTopicBrowserProfile({ topicId: topic, actorUserId: ownerId, profile });
|
|
@@ -98,6 +180,88 @@ function migrateLegacyTopicProfile(ownerId: string, topic: string): string {
|
|
|
98
180
|
return profile;
|
|
99
181
|
}
|
|
100
182
|
|
|
183
|
+
function defaultChildEnvironment(context: PlaywrightChildEnvironmentContext): NodeJS.ProcessEnv {
|
|
184
|
+
const { capability, ownerId, proxy, browserRsBin, environment } = context;
|
|
185
|
+
return {
|
|
186
|
+
...environment,
|
|
187
|
+
NEGOTIUM_BROWSER_CAPABILITY: capability,
|
|
188
|
+
NEGOTIUM_BROWSER_VAULT_USER_ID: ownerId,
|
|
189
|
+
...(browserRsBin && !proxy ? { NEGOTIUM_BROWSER_RS_BIN: browserRsBin } : {}),
|
|
190
|
+
...(proxy
|
|
191
|
+
? {
|
|
192
|
+
NEGOTIUM_BROWSER_PROXY_SERVER: proxy.server,
|
|
193
|
+
...(proxy.username ? { NEGOTIUM_BROWSER_PROXY_USERNAME: proxy.username } : {}),
|
|
194
|
+
...(proxy.password ? { NEGOTIUM_BROWSER_PROXY_PASSWORD: proxy.password } : {}),
|
|
195
|
+
...(proxy.bypass ? { NEGOTIUM_BROWSER_PROXY_BYPASS: proxy.bypass } : {}),
|
|
196
|
+
}
|
|
197
|
+
: {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const defaultManagerHost: Readonly<PlaywrightManagerHost> = Object.freeze({
|
|
202
|
+
portsDir: PLAYWRIGHT_PORTS_DIR,
|
|
203
|
+
basePort: PLAYWRIGHT_BASE_PORT,
|
|
204
|
+
maxPort: PLAYWRIGHT_MAX_PORT,
|
|
205
|
+
browserBin: PLAYWRIGHT_MCP_BIN,
|
|
206
|
+
fallbackBrowserBin: PATCHRIGHT_MCP_BIN,
|
|
207
|
+
browserRsBin: BROWSER_RS_BIN,
|
|
208
|
+
resolveProxy: resolveBrowserProxy,
|
|
209
|
+
resolveTopicBinding: defaultTopicBinding,
|
|
210
|
+
resolveNamedBinding: defaultNamedBinding,
|
|
211
|
+
resolveInstanceDataDir(instanceKey: string) {
|
|
212
|
+
const { ownerId, profile } = parseInstanceKey(instanceKey);
|
|
213
|
+
return defaultProfileDir(ownerId, profile);
|
|
214
|
+
},
|
|
215
|
+
createChildEnvironment: defaultChildEnvironment,
|
|
216
|
+
cleanupBrowserProcessesForDataDir: killBrowserProcsForUserDataDir,
|
|
217
|
+
reapOrphanBrowsers,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
let managerHost: Readonly<PlaywrightManagerHost> = defaultManagerHost;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Configure product-specific profile storage and browser launch glue.
|
|
224
|
+
* Call once during bootstrap, before any browser instance is started.
|
|
225
|
+
*/
|
|
226
|
+
export function configurePlaywrightManagerHost(
|
|
227
|
+
overrides: Partial<PlaywrightManagerHost>,
|
|
228
|
+
): Readonly<PlaywrightManagerHost> {
|
|
229
|
+
if (instances.size > 0 || spawning.size > 0 || pinnedInstances.size > 0) {
|
|
230
|
+
throw new Error("cannot configure Playwright manager while browser instances are active");
|
|
231
|
+
}
|
|
232
|
+
const next = { ...managerHost, ...overrides };
|
|
233
|
+
if (!next.portsDir || !Number.isInteger(next.basePort) || !Number.isInteger(next.maxPort)) {
|
|
234
|
+
throw new Error("invalid Playwright manager paths or port range");
|
|
235
|
+
}
|
|
236
|
+
if (next.basePort < 1 || next.maxPort > 65_535 || next.basePort > next.maxPort) {
|
|
237
|
+
throw new Error("invalid Playwright manager port range");
|
|
238
|
+
}
|
|
239
|
+
if (!next.browserBin || !next.fallbackBrowserBin) {
|
|
240
|
+
throw new Error("Playwright manager browser binaries are required");
|
|
241
|
+
}
|
|
242
|
+
if (
|
|
243
|
+
next.resolveInstanceDataDir !== defaultManagerHost.resolveInstanceDataDir &&
|
|
244
|
+
(next.cleanupBrowserProcessesForDataDir ===
|
|
245
|
+
defaultManagerHost.cleanupBrowserProcessesForDataDir ||
|
|
246
|
+
next.reapOrphanBrowsers === defaultManagerHost.reapOrphanBrowsers)
|
|
247
|
+
) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
"custom Playwright profile paths require host crash cleanup and orphan sweep hooks",
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
managerHost = Object.freeze(next);
|
|
253
|
+
return managerHost;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function getPlaywrightManagerHost(): Readonly<PlaywrightManagerHost> {
|
|
257
|
+
return managerHost;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Restore Negotium's built-in browser host after all instances are stopped. */
|
|
261
|
+
export function resetPlaywrightManagerHost(): void {
|
|
262
|
+
configurePlaywrightManagerHost(defaultManagerHost);
|
|
263
|
+
}
|
|
264
|
+
|
|
101
265
|
interface InstanceKeyParts {
|
|
102
266
|
ownerId: string;
|
|
103
267
|
profile: string;
|
|
@@ -118,8 +282,8 @@ function portFileName(instanceKey: string): string {
|
|
|
118
282
|
|
|
119
283
|
function writePortFile(instanceKey: string, port: number) {
|
|
120
284
|
try {
|
|
121
|
-
mkdirSync(
|
|
122
|
-
writeFileSync(join(
|
|
285
|
+
mkdirSync(managerHost.portsDir, { recursive: true });
|
|
286
|
+
writeFileSync(join(managerHost.portsDir, portFileName(instanceKey)), String(port));
|
|
123
287
|
} catch (e) {
|
|
124
288
|
logger.warn({ err: e, instanceKey, port }, "Failed to save playwright port file");
|
|
125
289
|
}
|
|
@@ -127,7 +291,7 @@ function writePortFile(instanceKey: string, port: number) {
|
|
|
127
291
|
|
|
128
292
|
function deletePortFile(instanceKey: string) {
|
|
129
293
|
try {
|
|
130
|
-
unlinkSync(join(
|
|
294
|
+
unlinkSync(join(managerHost.portsDir, portFileName(instanceKey)));
|
|
131
295
|
} catch (e) {
|
|
132
296
|
logger.warn({ err: e, instanceKey }, "Failed to delete playwright port file");
|
|
133
297
|
}
|
|
@@ -136,7 +300,7 @@ function deletePortFile(instanceKey: string) {
|
|
|
136
300
|
function readPortFile(instanceKey: string): number | null {
|
|
137
301
|
try {
|
|
138
302
|
const port = Number.parseInt(
|
|
139
|
-
readFileSync(join(
|
|
303
|
+
readFileSync(join(managerHost.portsDir, portFileName(instanceKey)), "utf8").trim(),
|
|
140
304
|
10,
|
|
141
305
|
);
|
|
142
306
|
return Number.isInteger(port) ? port : null;
|
|
@@ -145,14 +309,12 @@ function readPortFile(instanceKey: string): number | null {
|
|
|
145
309
|
}
|
|
146
310
|
}
|
|
147
311
|
|
|
148
|
-
const BASE_PORT = PLAYWRIGHT_BASE_PORT;
|
|
149
|
-
const MAX_PORT = PLAYWRIGHT_MAX_PORT;
|
|
150
312
|
const MAX_IDLE_MS = 2 * 60 * 60 * 1000; // 2 hours idle → eligible for eviction
|
|
151
313
|
|
|
152
314
|
interface PlaywrightInstance {
|
|
153
315
|
process: ChildProcess;
|
|
154
316
|
port: number;
|
|
155
|
-
|
|
317
|
+
ownerId: string;
|
|
156
318
|
startedAt: number;
|
|
157
319
|
lastUsedAt: number;
|
|
158
320
|
capability: string;
|
|
@@ -160,18 +322,33 @@ interface PlaywrightInstance {
|
|
|
160
322
|
|
|
161
323
|
const instances = new Map<string, PlaywrightInstance>();
|
|
162
324
|
|
|
325
|
+
function assertInstanceOwner(instanceKey: string, ownerId: string): void {
|
|
326
|
+
const existing = instances.get(instanceKey);
|
|
327
|
+
if (existing && existing.ownerId !== ownerId) {
|
|
328
|
+
throw new Error(
|
|
329
|
+
`Playwright profile binding owner changed for "${instanceKey}" (${existing.ownerId} -> ${ownerId})`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
163
334
|
// Track used ports to avoid collisions
|
|
164
335
|
const usedPorts = new Set<number>();
|
|
165
336
|
|
|
166
337
|
// Prevent concurrent spawns for the same key
|
|
167
338
|
const spawning = new Map<string, Promise<number | null>>();
|
|
168
339
|
|
|
340
|
+
export interface PlaywrightMaintenanceControl {
|
|
341
|
+
/** Stop one of the locked instances before mutating its profile directory. */
|
|
342
|
+
stopInstance(instanceKey: string): Promise<boolean>;
|
|
343
|
+
}
|
|
344
|
+
|
|
169
345
|
/** Serialize destructive profile maintenance with normal browser startup. */
|
|
170
346
|
export async function withPlaywrightInstanceMaintenance<T>(
|
|
171
347
|
rawKeys: string[],
|
|
172
|
-
operation: () => Promise<T>,
|
|
348
|
+
operation: (control: PlaywrightMaintenanceControl) => Promise<T>,
|
|
173
349
|
): Promise<T> {
|
|
174
350
|
const keys = [...new Set(rawKeys)].sort();
|
|
351
|
+
const lockedKeys = new Set(keys);
|
|
175
352
|
for (;;) {
|
|
176
353
|
const pending = keys
|
|
177
354
|
.map((key) => spawning.get(key))
|
|
@@ -190,7 +367,32 @@ export async function withPlaywrightInstanceMaintenance<T>(
|
|
|
190
367
|
for (const key of keys) spawning.set(key, barrier);
|
|
191
368
|
|
|
192
369
|
try {
|
|
193
|
-
return await operation(
|
|
370
|
+
return await operation({
|
|
371
|
+
async stopInstance(instanceKey) {
|
|
372
|
+
if (!lockedKeys.has(instanceKey)) {
|
|
373
|
+
throw new Error(`Playwright maintenance does not own instance "${instanceKey}"`);
|
|
374
|
+
}
|
|
375
|
+
const process = instances.get(instanceKey)?.process;
|
|
376
|
+
if (!process) return false;
|
|
377
|
+
killInstance(instanceKey);
|
|
378
|
+
if (!(await waitForChildProcessExit(process, 3000))) {
|
|
379
|
+
logger.warn(
|
|
380
|
+
{ instanceKey, pid: process.pid },
|
|
381
|
+
"Playwright MCP ignored SIGTERM during profile maintenance",
|
|
382
|
+
);
|
|
383
|
+
try {
|
|
384
|
+
process.kill("SIGKILL");
|
|
385
|
+
} catch (error) {
|
|
386
|
+
logger.warn(
|
|
387
|
+
{ err: error, instanceKey, pid: process.pid },
|
|
388
|
+
"Failed to SIGKILL Playwright MCP during profile maintenance",
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
await waitForChildProcessExit(process, 1000);
|
|
392
|
+
}
|
|
393
|
+
return true;
|
|
394
|
+
},
|
|
395
|
+
});
|
|
194
396
|
} finally {
|
|
195
397
|
for (const key of keys) {
|
|
196
398
|
if (spawning.get(key) === barrier) spawning.delete(key);
|
|
@@ -200,6 +402,36 @@ export async function withPlaywrightInstanceMaintenance<T>(
|
|
|
200
402
|
}
|
|
201
403
|
}
|
|
202
404
|
|
|
405
|
+
/** Stop one managed instance under the same serialization used by startup and profile mutation. */
|
|
406
|
+
export function stopPlaywrightInstance(instanceKey: string): Promise<boolean> {
|
|
407
|
+
return withPlaywrightInstanceMaintenance([instanceKey], ({ stopInstance }) =>
|
|
408
|
+
stopInstance(instanceKey),
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Run owner-aware named-profile maintenance under one lifecycle barrier. */
|
|
413
|
+
export function withPlaywrightProfileMaintenance<T>(
|
|
414
|
+
ownerId: string,
|
|
415
|
+
rawProfile: string,
|
|
416
|
+
operation: (
|
|
417
|
+
binding: PlaywrightProfileBinding,
|
|
418
|
+
control: PlaywrightMaintenanceControl,
|
|
419
|
+
) => Promise<T>,
|
|
420
|
+
): Promise<T> {
|
|
421
|
+
const binding = resolvePlaywrightProfileBinding(ownerId, rawProfile);
|
|
422
|
+
return withPlaywrightInstanceMaintenance([binding.instanceKey], async (control) => {
|
|
423
|
+
assertInstanceOwner(binding.instanceKey, binding.ownerId);
|
|
424
|
+
return operation(binding, control);
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Stop a named profile before deleting or replacing its host-managed data directory. */
|
|
429
|
+
export function stopPlaywrightProfile(ownerId: string, rawProfile: string): Promise<boolean> {
|
|
430
|
+
return withPlaywrightProfileMaintenance(ownerId, rawProfile, (binding, { stopInstance }) =>
|
|
431
|
+
stopInstance(binding.instanceKey),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
203
435
|
// A shared profile may have several concurrent turns. Reference counts keep an
|
|
204
436
|
// idle sweep from evicting the process until every borrower has finished.
|
|
205
437
|
const pinnedInstances = new Map<string, number>();
|
|
@@ -219,16 +451,48 @@ export function getPlaywrightCapability(instanceKey: string): string | undefined
|
|
|
219
451
|
return instances.get(instanceKey)?.capability;
|
|
220
452
|
}
|
|
221
453
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
454
|
+
/** Resolve a live browser capability back to its owning user. */
|
|
455
|
+
export function resolvePlaywrightCapabilityOwner(capability: string): string | undefined {
|
|
456
|
+
if (!capability) return undefined;
|
|
457
|
+
const provided = Buffer.from(capability);
|
|
458
|
+
for (const instance of instances.values()) {
|
|
459
|
+
const expected = Buffer.from(instance.capability);
|
|
460
|
+
if (provided.length === expected.length && timingSafeEqual(provided, expected)) {
|
|
461
|
+
return instance.ownerId;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Run the configured host's path-bounded orphan sweep against current live instances. */
|
|
468
|
+
export function reapPlaywrightOrphans(): void {
|
|
469
|
+
managerHost.reapOrphanBrowsers([...instances.keys()].map(resolveUserDataDir));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// --- Browser MCP failure notification callback ---
|
|
473
|
+
// Fired when a managed process fails or its transport becomes unhealthy.
|
|
474
|
+
// Intentional cleanup paths do not notify consumers.
|
|
475
|
+
export interface PlaywrightFailure {
|
|
476
|
+
reason: "exit" | "process-error" | "unhealthy";
|
|
477
|
+
code?: number | null;
|
|
478
|
+
error?: string;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
type PlaywrightFailureHandler = (instanceKey: string, failure: PlaywrightFailure) => void;
|
|
482
|
+
const playwrightFailureHandlers = new Set<PlaywrightFailureHandler>();
|
|
483
|
+
export function onPlaywrightFailure(handler: PlaywrightFailureHandler): () => void {
|
|
484
|
+
playwrightFailureHandlers.add(handler);
|
|
485
|
+
return () => playwrightFailureHandlers.delete(handler);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function notifyPlaywrightFailure(instanceKey: string, failure: PlaywrightFailure): void {
|
|
489
|
+
for (const handler of playwrightFailureHandlers) {
|
|
490
|
+
try {
|
|
491
|
+
handler(instanceKey, failure);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
logger.warn({ err, instanceKey, failure }, "Playwright failure handler failed");
|
|
494
|
+
}
|
|
495
|
+
}
|
|
232
496
|
}
|
|
233
497
|
|
|
234
498
|
/**
|
|
@@ -265,7 +529,7 @@ function evictIdleInstance(): number | null {
|
|
|
265
529
|
* mistake a live topic Playwright for an orphan and SIGKILL it.
|
|
266
530
|
*/
|
|
267
531
|
async function allocatePort(expectedUserDataDir?: string): Promise<number> {
|
|
268
|
-
for (let port =
|
|
532
|
+
for (let port = managerHost.basePort; port <= managerHost.maxPort; port++) {
|
|
269
533
|
if (usedPorts.has(port)) continue;
|
|
270
534
|
usedPorts.add(port); // Reserve immediately before any await to prevent concurrent allocation
|
|
271
535
|
|
|
@@ -289,7 +553,12 @@ async function allocatePort(expectedUserDataDir?: string): Promise<number> {
|
|
|
289
553
|
// until the old listener is actually gone, and recheck every candidate in
|
|
290
554
|
// case another allocator claimed a different port while we were waiting.
|
|
291
555
|
await waitForPortRelease(evictedPort);
|
|
292
|
-
const reusablePort = selectReusablePort(
|
|
556
|
+
const reusablePort = selectReusablePort(
|
|
557
|
+
managerHost.basePort,
|
|
558
|
+
managerHost.maxPort,
|
|
559
|
+
usedPorts,
|
|
560
|
+
isPortInUse,
|
|
561
|
+
);
|
|
293
562
|
if (reusablePort !== null) {
|
|
294
563
|
usedPorts.add(reusablePort);
|
|
295
564
|
return reusablePort;
|
|
@@ -297,7 +566,7 @@ async function allocatePort(expectedUserDataDir?: string): Promise<number> {
|
|
|
297
566
|
}
|
|
298
567
|
|
|
299
568
|
throw new Error(
|
|
300
|
-
`No available ports for Playwright MCP (${instances.size} active instances, range ${
|
|
569
|
+
`No available ports for Playwright MCP (${instances.size} active instances, range ${managerHost.basePort}-${managerHost.maxPort})`,
|
|
301
570
|
);
|
|
302
571
|
}
|
|
303
572
|
|
|
@@ -310,14 +579,13 @@ function ownerDirectory(ownerId: string): string {
|
|
|
310
579
|
return `${sanitizeTopicName(ownerId).slice(0, 24)}_${digest}`;
|
|
311
580
|
}
|
|
312
581
|
|
|
313
|
-
function
|
|
582
|
+
function defaultProfileDir(ownerId: string, profile: string): string {
|
|
314
583
|
return resolve(BROWSER_PROFILES_DIR, "profiles", ownerDirectory(ownerId), profile);
|
|
315
584
|
}
|
|
316
585
|
|
|
317
586
|
/** Resolve the shared profile userDataDir for an instanceKey. */
|
|
318
587
|
function resolveUserDataDir(instanceKey: string): string {
|
|
319
|
-
|
|
320
|
-
return resolveProfileDir(ownerId, profile);
|
|
588
|
+
return managerHost.resolveInstanceDataDir(instanceKey);
|
|
321
589
|
}
|
|
322
590
|
|
|
323
591
|
/**
|
|
@@ -366,15 +634,15 @@ function killInstance(instanceKey: string, opts?: { keepPort?: boolean }) {
|
|
|
366
634
|
/**
|
|
367
635
|
* Spawn a Playwright MCP process for the given instanceKey.
|
|
368
636
|
* @param instanceKey Unique key identifying this instance.
|
|
369
|
-
* @param
|
|
637
|
+
* @param ownerId Canonical profile owner ID (stored in the instance record).
|
|
370
638
|
* @param reservedPort When provided the process binds to this already-reserved
|
|
371
639
|
* port (restart path). When omitted a fresh port is allocated.
|
|
372
640
|
*/
|
|
373
641
|
async function spawnPlaywright(
|
|
374
642
|
instanceKey: string,
|
|
375
|
-
|
|
643
|
+
ownerId: string,
|
|
376
644
|
reservedPort?: number,
|
|
377
|
-
browserBin =
|
|
645
|
+
browserBin = managerHost.browserBin,
|
|
378
646
|
allowFallback = true,
|
|
379
647
|
): Promise<number> {
|
|
380
648
|
const userDataDir = resolveUserDataDir(instanceKey);
|
|
@@ -412,22 +680,22 @@ async function spawnPlaywright(
|
|
|
412
680
|
// argv so the credentials never surface in `ps`/`/proc` command lines. The
|
|
413
681
|
// launcher (scripts/mcp-patchright-http.mjs) reads these NEGOTIUM_BROWSER_PROXY_*
|
|
414
682
|
// vars and hands them to Playwright's per-context proxy option.
|
|
415
|
-
const proxy =
|
|
683
|
+
const proxy = managerHost.resolveProxy();
|
|
416
684
|
const capability = randomBytes(32).toString("hex");
|
|
417
685
|
const childEnv = {
|
|
418
|
-
...
|
|
686
|
+
...managerHost.createChildEnvironment({
|
|
687
|
+
instanceKey,
|
|
688
|
+
ownerId,
|
|
689
|
+
capability,
|
|
690
|
+
proxy,
|
|
691
|
+
browserRsBin: managerHost.browserRsBin,
|
|
692
|
+
environment: process.env,
|
|
693
|
+
}),
|
|
419
694
|
NEGOTIUM_BROWSER_CAPABILITY: capability,
|
|
420
|
-
NEGOTIUM_BROWSER_VAULT_USER_ID: userId,
|
|
421
|
-
...(BROWSER_RS_BIN && !proxy ? { NEGOTIUM_BROWSER_RS_BIN: BROWSER_RS_BIN } : {}),
|
|
422
|
-
...(proxy
|
|
423
|
-
? {
|
|
424
|
-
NEGOTIUM_BROWSER_PROXY_SERVER: proxy.server,
|
|
425
|
-
...(proxy.username ? { NEGOTIUM_BROWSER_PROXY_USERNAME: proxy.username } : {}),
|
|
426
|
-
...(proxy.password ? { NEGOTIUM_BROWSER_PROXY_PASSWORD: proxy.password } : {}),
|
|
427
|
-
...(proxy.bypass ? { NEGOTIUM_BROWSER_PROXY_BYPASS: proxy.bypass } : {}),
|
|
428
|
-
}
|
|
429
|
-
: {}),
|
|
430
695
|
};
|
|
696
|
+
// Every supported wrapper authenticates owner-scoped transports with this
|
|
697
|
+
// capability. Product-specific environment hooks may add Vault callbacks,
|
|
698
|
+
// but cannot remove transport authentication.
|
|
431
699
|
if (proxy) {
|
|
432
700
|
logger.info({ instanceKey, proxyServer: proxy.server }, "Browser egress proxy enabled");
|
|
433
701
|
}
|
|
@@ -461,7 +729,7 @@ async function spawnPlaywright(
|
|
|
461
729
|
// removes these listeners before signalling, so this only runs on real deaths.
|
|
462
730
|
const reapCrashedBrowser = () => {
|
|
463
731
|
const userDataDir = resolveUserDataDir(instanceKey);
|
|
464
|
-
|
|
732
|
+
managerHost.cleanupBrowserProcessesForDataDir(userDataDir);
|
|
465
733
|
cleanSingletonFiles(userDataDir);
|
|
466
734
|
};
|
|
467
735
|
|
|
@@ -471,6 +739,10 @@ async function spawnPlaywright(
|
|
|
471
739
|
releasePort(port);
|
|
472
740
|
instances.delete(instanceKey);
|
|
473
741
|
reapCrashedBrowser();
|
|
742
|
+
notifyPlaywrightFailure(instanceKey, {
|
|
743
|
+
reason: "process-error",
|
|
744
|
+
error: err instanceof Error ? err.message : String(err),
|
|
745
|
+
});
|
|
474
746
|
}
|
|
475
747
|
});
|
|
476
748
|
|
|
@@ -486,7 +758,7 @@ async function spawnPlaywright(
|
|
|
486
758
|
releasePort(port);
|
|
487
759
|
instances.delete(instanceKey);
|
|
488
760
|
reapCrashedBrowser();
|
|
489
|
-
|
|
761
|
+
notifyPlaywrightFailure(instanceKey, { reason: "exit", code });
|
|
490
762
|
}
|
|
491
763
|
});
|
|
492
764
|
|
|
@@ -494,7 +766,7 @@ async function spawnPlaywright(
|
|
|
494
766
|
instances.set(instanceKey, {
|
|
495
767
|
process: proc,
|
|
496
768
|
port,
|
|
497
|
-
|
|
769
|
+
ownerId,
|
|
498
770
|
startedAt: now,
|
|
499
771
|
lastUsedAt: now,
|
|
500
772
|
capability,
|
|
@@ -502,18 +774,26 @@ async function spawnPlaywright(
|
|
|
502
774
|
|
|
503
775
|
const ready = await Promise.race([
|
|
504
776
|
(async () =>
|
|
505
|
-
(await waitForServer(port, 10_000)) &&
|
|
777
|
+
(await waitForServer(port, 10_000)) &&
|
|
778
|
+
(await supportsOwnerCleanup(port, capability)) &&
|
|
779
|
+
(await probePlaywrightMcpTransports(port, capability)))(),
|
|
506
780
|
spawnError,
|
|
507
781
|
]);
|
|
508
782
|
if (!ready) {
|
|
509
783
|
const exitCode = proc.exitCode;
|
|
510
784
|
killInstance(instanceKey);
|
|
511
|
-
if (allowFallback && browserBin !==
|
|
785
|
+
if (allowFallback && browserBin !== managerHost.fallbackBrowserBin) {
|
|
512
786
|
logger.warn(
|
|
513
|
-
{ instanceKey, browserBin, fallback:
|
|
787
|
+
{ instanceKey, browserBin, fallback: managerHost.fallbackBrowserBin },
|
|
514
788
|
"Preferred browser MCP unavailable or lacks owner isolation; using Patchright fallback",
|
|
515
789
|
);
|
|
516
|
-
return spawnPlaywright(
|
|
790
|
+
return spawnPlaywright(
|
|
791
|
+
instanceKey,
|
|
792
|
+
ownerId,
|
|
793
|
+
undefined,
|
|
794
|
+
managerHost.fallbackBrowserBin,
|
|
795
|
+
false,
|
|
796
|
+
);
|
|
517
797
|
}
|
|
518
798
|
throw new Error(
|
|
519
799
|
`Playwright MCP failed health check after spawn on port ${port}` +
|
|
@@ -553,12 +833,14 @@ async function supportsOwnerCleanup(port: number, capability: string): Promise<b
|
|
|
553
833
|
* - If not running → spawn
|
|
554
834
|
*/
|
|
555
835
|
export async function ensurePlaywright(userId: string, topic?: string): Promise<number> {
|
|
556
|
-
const
|
|
836
|
+
const binding = resolvePlaywrightTopicBinding(userId, topic);
|
|
837
|
+
const { instanceKey, ownerId } = binding;
|
|
557
838
|
|
|
558
839
|
// If a spawn/restart is already in progress for this key, wait for it
|
|
559
840
|
const inProgress = spawning.get(instanceKey);
|
|
560
841
|
if (inProgress) {
|
|
561
842
|
const port = await inProgress;
|
|
843
|
+
assertInstanceOwner(instanceKey, ownerId);
|
|
562
844
|
if (port !== null) return port;
|
|
563
845
|
// Restart failed — re-enter: by now another caller may have registered a
|
|
564
846
|
// newer attempt to join, otherwise we start one ourselves.
|
|
@@ -575,24 +857,30 @@ export async function ensurePlaywright(userId: string, topic?: string): Promise<
|
|
|
575
857
|
// a single-threaded event loop.
|
|
576
858
|
const promise = (async (): Promise<number> => {
|
|
577
859
|
const existing = instances.get(instanceKey);
|
|
860
|
+
assertInstanceOwner(instanceKey, ownerId);
|
|
578
861
|
|
|
579
862
|
if (existing && !existing.process.killed && existing.process.exitCode === null) {
|
|
580
|
-
if (await
|
|
863
|
+
if (await probePlaywrightMcpTransports(existing.port, existing.capability)) {
|
|
581
864
|
existing.lastUsedAt = Date.now();
|
|
582
865
|
return existing.port;
|
|
583
866
|
}
|
|
584
867
|
logger.warn({ instanceKey }, "Playwright MCP unresponsive, restarting");
|
|
585
868
|
const oldPort = existing.port;
|
|
586
869
|
killInstance(instanceKey);
|
|
870
|
+
notifyPlaywrightFailure(instanceKey, { reason: "unhealthy" });
|
|
587
871
|
await waitForPortRelease(oldPort);
|
|
588
872
|
cleanSingletonFiles(resolveUserDataDir(instanceKey));
|
|
589
873
|
} else if (existing) {
|
|
590
874
|
releasePort(existing.port);
|
|
591
875
|
instances.delete(instanceKey);
|
|
592
876
|
cleanSingletonFiles(resolveUserDataDir(instanceKey));
|
|
877
|
+
notifyPlaywrightFailure(instanceKey, {
|
|
878
|
+
reason: "exit",
|
|
879
|
+
code: existing.process.exitCode,
|
|
880
|
+
});
|
|
593
881
|
}
|
|
594
882
|
|
|
595
|
-
return spawnPlaywright(instanceKey,
|
|
883
|
+
return spawnPlaywright(instanceKey, ownerId);
|
|
596
884
|
})().finally(() => spawning.delete(instanceKey));
|
|
597
885
|
spawning.set(instanceKey, promise);
|
|
598
886
|
return promise;
|
|
@@ -608,11 +896,16 @@ async function waitForPortRelease(port: number, timeoutMs = 3000): Promise<void>
|
|
|
608
896
|
|
|
609
897
|
/** Start or reuse a named shared profile before assigning more topics to it. */
|
|
610
898
|
export async function ensureBrowserProfile(ownerId: string, rawProfile: string): Promise<number> {
|
|
611
|
-
const
|
|
899
|
+
const binding = resolvePlaywrightProfileBinding(ownerId, rawProfile);
|
|
900
|
+
const instanceKey = binding.instanceKey;
|
|
612
901
|
const inProgress = spawning.get(instanceKey);
|
|
613
902
|
if (inProgress) {
|
|
614
903
|
const port = await inProgress;
|
|
615
|
-
|
|
904
|
+
assertInstanceOwner(instanceKey, binding.ownerId);
|
|
905
|
+
const capability = instances.get(instanceKey)?.capability;
|
|
906
|
+
if (port !== null && capability && (await probePlaywrightMcpTransports(port, capability))) {
|
|
907
|
+
return port;
|
|
908
|
+
}
|
|
616
909
|
if (port === null) return ensureBrowserProfile(ownerId, rawProfile);
|
|
617
910
|
throw new Error(`Browser profile "${rawProfile}" failed to start.`);
|
|
618
911
|
}
|
|
@@ -620,6 +913,7 @@ export async function ensureBrowserProfile(ownerId: string, rawProfile: string):
|
|
|
620
913
|
const promise = (async (): Promise<number> => {
|
|
621
914
|
const publishedPort = readPortFile(instanceKey);
|
|
622
915
|
const existing = instances.get(instanceKey);
|
|
916
|
+
assertInstanceOwner(instanceKey, binding.ownerId);
|
|
623
917
|
if (publishedPort !== null && !existing) {
|
|
624
918
|
// A port file without an in-memory instance belongs to a previous runtime.
|
|
625
919
|
// Its per-process capability and ChildProcess handle cannot be recovered,
|
|
@@ -635,17 +929,27 @@ export async function ensureBrowserProfile(ownerId: string, rawProfile: string):
|
|
|
635
929
|
}
|
|
636
930
|
|
|
637
931
|
if (existing && !existing.process.killed && existing.process.exitCode === null) {
|
|
638
|
-
if (await
|
|
932
|
+
if (await probePlaywrightMcpTransports(existing.port, existing.capability)) {
|
|
639
933
|
existing.lastUsedAt = Date.now();
|
|
640
934
|
return existing.port;
|
|
641
935
|
}
|
|
642
936
|
const oldPort = existing.port;
|
|
643
937
|
killInstance(instanceKey);
|
|
938
|
+
notifyPlaywrightFailure(instanceKey, { reason: "unhealthy" });
|
|
644
939
|
await waitForPortRelease(oldPort);
|
|
645
940
|
cleanSingletonFiles(resolveUserDataDir(instanceKey));
|
|
941
|
+
} else if (existing) {
|
|
942
|
+
releasePort(existing.port);
|
|
943
|
+
instances.delete(instanceKey);
|
|
944
|
+
cleanSingletonFiles(resolveUserDataDir(instanceKey));
|
|
945
|
+
notifyPlaywrightFailure(instanceKey, {
|
|
946
|
+
reason: "exit",
|
|
947
|
+
code: existing.process.exitCode,
|
|
948
|
+
});
|
|
646
949
|
}
|
|
647
|
-
const port = await spawnPlaywright(instanceKey, ownerId);
|
|
648
|
-
|
|
950
|
+
const port = await spawnPlaywright(instanceKey, binding.ownerId);
|
|
951
|
+
const capability = instances.get(instanceKey)?.capability;
|
|
952
|
+
if (!capability || !(await probePlaywrightMcpTransports(port, capability))) {
|
|
649
953
|
killInstance(instanceKey);
|
|
650
954
|
throw new Error(`Browser profile "${rawProfile}" did not pass its health check.`);
|
|
651
955
|
}
|
|
@@ -661,9 +965,11 @@ export async function closeBrowserOwnerTabs(
|
|
|
661
965
|
rawProfile: string,
|
|
662
966
|
owner: string,
|
|
663
967
|
): Promise<number> {
|
|
664
|
-
const
|
|
968
|
+
const binding = resolvePlaywrightProfileBinding(ownerId, rawProfile);
|
|
969
|
+
const instanceKey = binding.instanceKey;
|
|
665
970
|
const inProgress = spawning.get(instanceKey);
|
|
666
971
|
if (inProgress) await inProgress;
|
|
972
|
+
assertInstanceOwner(instanceKey, binding.ownerId);
|
|
667
973
|
const instance = instances.get(instanceKey);
|
|
668
974
|
if (!instance) return 0;
|
|
669
975
|
const port = instance.port;
|
|
@@ -751,18 +1057,26 @@ export async function cloneProfileForChild(opts: {
|
|
|
751
1057
|
const srcOwner = getBrowserProfileOwner(opts.srcTopic, opts.userId);
|
|
752
1058
|
const dstOwner = getBrowserProfileOwner(opts.dstTopic, opts.userId);
|
|
753
1059
|
if (srcOwner !== dstOwner) {
|
|
754
|
-
const
|
|
1060
|
+
const srcBinding = resolvePlaywrightProfileBinding(
|
|
1061
|
+
srcOwner,
|
|
1062
|
+
getTopicBrowserProfile(opts.srcTopic),
|
|
1063
|
+
);
|
|
1064
|
+
const dstBinding = resolvePlaywrightProfileBinding(
|
|
1065
|
+
dstOwner,
|
|
1066
|
+
getTopicBrowserProfile(opts.dstTopic),
|
|
1067
|
+
);
|
|
755
1068
|
return {
|
|
756
1069
|
copied: false,
|
|
757
|
-
srcDir:
|
|
758
|
-
dstDir,
|
|
1070
|
+
srcDir: resolveUserDataDir(srcBinding.instanceKey),
|
|
1071
|
+
dstDir: resolveUserDataDir(dstBinding.instanceKey),
|
|
759
1072
|
reason: "cross-owner-fresh-profile",
|
|
760
1073
|
};
|
|
761
1074
|
}
|
|
762
1075
|
if (srcOwner === dstOwner && hasBrowserProfileTopic(opts.dstTopic)) {
|
|
763
1076
|
const profile = getTopicBrowserProfile(opts.srcTopic);
|
|
764
1077
|
assignTopicBrowserProfile({ topicId: opts.dstTopic, actorUserId: dstOwner, profile });
|
|
765
|
-
const
|
|
1078
|
+
const binding = resolvePlaywrightProfileBinding(srcOwner, profile);
|
|
1079
|
+
const sharedDir = resolveUserDataDir(binding.instanceKey);
|
|
766
1080
|
return {
|
|
767
1081
|
copied: false,
|
|
768
1082
|
srcDir: sharedDir,
|
|
@@ -891,7 +1205,7 @@ setInterval(
|
|
|
891
1205
|
() => {
|
|
892
1206
|
while (evictIdleInstance() !== null) {}
|
|
893
1207
|
try {
|
|
894
|
-
|
|
1208
|
+
reapPlaywrightOrphans();
|
|
895
1209
|
} catch (e) {
|
|
896
1210
|
logger.debug({ err: e }, "reapOrphanBrowsers sweep failed");
|
|
897
1211
|
}
|