pi-agent-browser-native 0.8.3 → 0.9.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 +36 -0
- package/README.md +12 -11
- package/dist/extensions/agent-browser/index.js +96 -320
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +1 -1
- package/dist/extensions/agent-browser/lib/argv-grammar.js +3 -3
- package/dist/extensions/agent-browser/lib/browser-journal.js +33 -12
- package/dist/extensions/agent-browser/lib/browser-transcript.js +1 -1
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +0 -3
- package/dist/extensions/agent-browser/lib/config-policy.js +0 -13
- package/dist/extensions/agent-browser/lib/config.js +3 -48
- package/dist/extensions/agent-browser/lib/electron/launch.js +4 -4
- package/dist/extensions/agent-browser/lib/execution-cwd.js +0 -16
- package/dist/extensions/agent-browser/lib/input-modes/params.js +2 -3
- package/dist/extensions/agent-browser/lib/input-modes/script.js +28 -6
- package/dist/extensions/agent-browser/lib/input-modes/types.js +2 -2
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +1 -4
- package/dist/extensions/agent-browser/lib/native-output.js +46 -0
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +0 -9
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-merge.js +22 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +2 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/execution-queue.js +86 -0
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +31 -17
- package/dist/extensions/agent-browser/lib/process.js +0 -3
- package/dist/extensions/agent-browser/lib/prompt-policy.js +16 -12
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +2 -2
- package/dist/extensions/agent-browser/lib/results/network-routes.js +1 -1
- package/dist/extensions/agent-browser/lib/results/network.js +1 -1
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +1 -1
- package/dist/extensions/agent-browser/lib/results/presentation/content.js +4 -0
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +1 -1
- package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +15 -8
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +2 -2
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +1 -1
- package/dist/extensions/agent-browser/lib/runtime.js +4 -2
- package/dist/extensions/agent-browser/lib/session-page-state.js +1 -1
- package/dist/extensions/agent-browser/lib/tool-surface.js +39 -11
- package/dist/extensions/agent-browser/lib/web-search.js +100 -79
- package/docs/ARCHITECTURE.md +17 -19
- package/docs/COMMAND_REFERENCE.md +6 -2
- package/docs/RELEASE.md +26 -20
- package/docs/REQUIREMENTS.md +5 -5
- package/docs/SESSION_STORAGE.md +4 -4
- package/docs/SUPPORT_MATRIX.md +15 -13
- package/docs/TOOL_CONTRACT.md +12 -8
- package/package.json +6 -6
- package/scripts/doctor.mjs +12 -4
|
@@ -38,7 +38,7 @@ export const GLOBAL_VALUE_FLAGS = [
|
|
|
38
38
|
"--idle-timeout",
|
|
39
39
|
"--input-mode",
|
|
40
40
|
];
|
|
41
|
-
|
|
41
|
+
const COMMAND_VALUE_FLAGS = [
|
|
42
42
|
"--allowed-origins",
|
|
43
43
|
"--baseline",
|
|
44
44
|
"--body",
|
|
@@ -79,7 +79,7 @@ export const COMMAND_VALUE_FLAGS = [
|
|
|
79
79
|
"--password",
|
|
80
80
|
"--wait-until",
|
|
81
81
|
];
|
|
82
|
-
|
|
82
|
+
const OPTIONAL_GLOBAL_VALUE_FLAGS = new Set(["--restore"]);
|
|
83
83
|
export const VALUE_FLAGS = new Set([...GLOBAL_VALUE_FLAGS, ...COMMAND_VALUE_FLAGS]);
|
|
84
84
|
export const PREVALIDATED_VALUE_FLAGS = new Set(GLOBAL_VALUE_FLAGS);
|
|
85
85
|
export const GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE = new Set(["--args", "--session"]);
|
|
@@ -246,7 +246,7 @@ export function extractRequestedRestoreKey(args, sessionName, envValue) {
|
|
|
246
246
|
}
|
|
247
247
|
return restoreKey;
|
|
248
248
|
}
|
|
249
|
-
|
|
249
|
+
function getFlagName(token) {
|
|
250
250
|
return token.split("=", 1)[0] ?? token;
|
|
251
251
|
}
|
|
252
252
|
export function isNonFlagToken(token) {
|
|
@@ -241,14 +241,7 @@ export async function readBrowserEntries(manager, physical = false) {
|
|
|
241
241
|
const header = metadata.find(entry => entry.value.type === "session");
|
|
242
242
|
if (header?.value.id !== (manager.getHeader()?.id ?? manager.getSessionId()))
|
|
243
243
|
throw new Error("Pi session journal identity does not match the active session.");
|
|
244
|
-
const publicManager = manager;
|
|
245
244
|
const selected = physical ? metadata.filter(entry => entry.value.type !== "session") : branchEntries(metadata.filter(entry => entry.value.type !== "session"), leaf);
|
|
246
|
-
if (publicManager.iterateEntryMetadata) {
|
|
247
|
-
const native = new Map([...publicManager.iterateEntryMetadata(physical ? {} : { branchFrom: leaf })].map(entry => [entry.id, entry.parentId]));
|
|
248
|
-
for (const entry of selected)
|
|
249
|
-
if (typeof entry.value.id !== "string" || native.get(entry.value.id) !== entry.value.parentId)
|
|
250
|
-
throw new Error("Published browser journal ancestry differs from the native selected boundary.");
|
|
251
|
-
}
|
|
252
245
|
const projected = [];
|
|
253
246
|
const definitions = new Map();
|
|
254
247
|
for (const entry of selected) {
|
|
@@ -285,9 +278,7 @@ export async function readBrowserEntries(manager, physical = false) {
|
|
|
285
278
|
throw new Error(`Browser snapshot ${id} has no ancestral definition; inspect and take a fresh snapshot.`);
|
|
286
279
|
// ponytail: a requested individual snapshot must fit its consumer's memory. Metadata
|
|
287
280
|
// scans skip unlimited unrelated values; upgrade with native per-value paging if needed.
|
|
288
|
-
const
|
|
289
|
-
const body = native && isRecord(native) && isRecord(native.data) ? { data: native.data }
|
|
290
|
-
: await projectJson(readRange(file, entry), [["data", "snapshot"]], Infinity);
|
|
281
|
+
const body = await projectJson(readRange(file, entry), [["data", "snapshot"]], Infinity);
|
|
291
282
|
const destination = projected.find(candidate => isRecord(candidate) && candidate.id === entry.value.id);
|
|
292
283
|
if (!isRecord(destination) || !isRecord(destination.data) || !isRecord(body?.data))
|
|
293
284
|
throw new Error("Winning browser snapshot could not be read.");
|
|
@@ -342,13 +333,22 @@ export function hasPublishedBrowserJournal(manager) {
|
|
|
342
333
|
/** Capture before waiting: native selection precedes sequential session_tree handlers. */
|
|
343
334
|
export function captureBrowserBranch(manager, isGenerationCurrent) {
|
|
344
335
|
const sessionId = manager.getSessionId();
|
|
345
|
-
|
|
336
|
+
let anchorId = manager.getLeafId();
|
|
337
|
+
let validatedAnchor;
|
|
338
|
+
let validatedLeaf;
|
|
339
|
+
const branch = { sessionId, anchorId, isCurrent() {
|
|
346
340
|
if (!isGenerationCurrent() || manager.getSessionId() !== sessionId)
|
|
347
341
|
return false;
|
|
342
|
+
if (branch.anchorId !== anchorId) {
|
|
343
|
+
validatedAnchor = validatedLeaf = undefined;
|
|
344
|
+
anchorId = branch.anchorId;
|
|
345
|
+
}
|
|
348
346
|
let leaf = manager.getLeafId();
|
|
349
347
|
// Empty selection is not a universal ancestor of other independent roots.
|
|
350
348
|
let current = branch.anchorId === null && leaf === null;
|
|
351
349
|
const seen = new Set();
|
|
350
|
+
let selected;
|
|
351
|
+
let anchor;
|
|
352
352
|
while (leaf !== null) {
|
|
353
353
|
if (typeof leaf !== "string" || seen.has(leaf))
|
|
354
354
|
throw new Error("Selected Pi journal ancestry is invalid or cyclic.");
|
|
@@ -356,10 +356,31 @@ export function captureBrowserBranch(manager, isGenerationCurrent) {
|
|
|
356
356
|
const entry = manager.getEntry(leaf);
|
|
357
357
|
if (!entry || entry.id !== leaf || entry.parentId !== null && typeof entry.parentId !== "string")
|
|
358
358
|
throw new Error("Selected Pi journal ancestry is incomplete.");
|
|
359
|
-
|
|
359
|
+
selected ??= { entry, parentId: entry.parentId };
|
|
360
|
+
const cached = leaf === branch.anchorId ? validatedAnchor : leaf === validatedLeaf?.entry.id ? validatedLeaf : undefined;
|
|
361
|
+
if (cached) {
|
|
362
|
+
// Keep the native indexed lookup: it also refreshes fork journal identity.
|
|
363
|
+
if (cached.entry === entry && cached.parentId === entry.parentId) {
|
|
364
|
+
current = true;
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
validatedAnchor = validatedLeaf = undefined;
|
|
368
|
+
}
|
|
369
|
+
if (leaf === branch.anchorId) {
|
|
360
370
|
current = true;
|
|
371
|
+
anchor = { entry, parentId: entry.parentId };
|
|
372
|
+
}
|
|
361
373
|
leaf = entry.parentId;
|
|
362
374
|
}
|
|
375
|
+
// Native entries/parents are append-only. Cache only after validating through
|
|
376
|
+
// the root (or an already validated endpoint), never merely on leaf === anchor.
|
|
377
|
+
// ponytail: behind-host mutation of an old ancestor without native reseed or
|
|
378
|
+
// generation change is outside this contract; durable file proofs
|
|
379
|
+
// remain in replay/append. Upgrade if Pi exposes a separate ancestry revision.
|
|
380
|
+
if (current && selected) {
|
|
381
|
+
validatedAnchor = anchor ?? validatedAnchor;
|
|
382
|
+
validatedLeaf = selected;
|
|
383
|
+
}
|
|
363
384
|
return current;
|
|
364
385
|
} };
|
|
365
386
|
return branch;
|
|
@@ -4,7 +4,7 @@ import { buildPageTransitionRefSnapshotInvalidation, normalizeSessionTabTarget,
|
|
|
4
4
|
import { getSessionArtifactManifestEntryKey, isPendingRecordingArtifact, isSessionArtifactManifest } from "./results/artifact-manifest.js";
|
|
5
5
|
import { extractAgentBrowserLifecycle } from "./results/presentation/common.js";
|
|
6
6
|
export const BROWSER_TRANSITION_ENTRY = "agent-browser-transition";
|
|
7
|
-
|
|
7
|
+
const BROWSER_EVENT_VERSION = 1;
|
|
8
8
|
export const BROWSER_RESULT_TOOLS = new Set([
|
|
9
9
|
"agent_browser", "agent_browser_code", "agent_browser_action", "agent_browser_qa", "agent_browser_electron",
|
|
10
10
|
"agent_browser_source", "agent_browser_network_source",
|
|
@@ -295,9 +295,6 @@ function getCommandCapability(command) {
|
|
|
295
295
|
function hasCommandCapability(command, capability) {
|
|
296
296
|
return getCommandCapability(command)?.[capability] === true;
|
|
297
297
|
}
|
|
298
|
-
export function normalizeCommandName(command) {
|
|
299
|
-
return getCommandCapability(command)?.command ?? command;
|
|
300
|
-
}
|
|
301
298
|
export function isCloseCommand(command) {
|
|
302
299
|
return hasCommandCapability(command, "closesSession");
|
|
303
300
|
}
|
|
@@ -229,19 +229,6 @@ function validateBrowserDefaultProfile(value, path, errors) {
|
|
|
229
229
|
}
|
|
230
230
|
return { name, policy: /** @type {BrowserDefaultProfilePolicy} */ (policy) };
|
|
231
231
|
}
|
|
232
|
-
/** @param {string} rawValue */
|
|
233
|
-
export function isPlaintextCredentialValue(rawValue) {
|
|
234
|
-
const trimmed = rawValue.trim();
|
|
235
|
-
return Boolean(trimmed) && !trimmed.startsWith("!") && !trimmed.startsWith("$");
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* @param {string} rawValue
|
|
239
|
-
* @param {WebSearchProvider} provider
|
|
240
|
-
*/
|
|
241
|
-
export function isProjectSafeCredentialValueForProvider(rawValue, provider) {
|
|
242
|
-
void provider;
|
|
243
|
-
return rawValue.trim().length > 0;
|
|
244
|
-
}
|
|
245
232
|
/**
|
|
246
233
|
* @param {unknown} value
|
|
247
234
|
* @param {string} path
|
|
@@ -1,53 +1,8 @@
|
|
|
1
1
|
import { exec as execCallback } from "node:child_process";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
2
|
import { promisify } from "node:util";
|
|
4
|
-
import { SECRET_COMMAND_TIMEOUT_MS,
|
|
5
|
-
export { AGENT_BROWSER_CONFIG_ENV, BRAVE_API_KEY_ENV, CONFIG_RELATIVE_PATH, DEFAULT_WEB_SEARCH_PROVIDER, EXA_API_KEY_ENV, EXA_SEARCH_TYPES, GLOBAL_CONFIG_RELATIVE_PATH, SECRET_COMMAND_TIMEOUT_MS, WEB_SEARCH_PROVIDER_CONFIG_KEYS, WEB_SEARCH_PROVIDER_DESCRIPTORS, WEB_SEARCH_PROVIDER_ENV_VARS, WEB_SEARCH_PROVIDERS, buildAgentBrowserConfigState, buildWebSearchCredentialSources, canRegisterWebSearchTool, classifyCredentialSource, formatBrowserExecutableStatus, formatBrowserProfileStatus, getAgentBrowserConfigPaths, getCredentialSourceSummary, getGlobalAgentBrowserConfigPath, getProjectAgentBrowserConfigPath, getWebSearchCredentialSource, getWebSearchProviderConfigKey, getWebSearchProviderDescriptor, getWebSearchProviderEnvVar, getWebSearchProviderLabel, getWebSearchProviderOrder, hasPotentialCredentialSource,
|
|
3
|
+
import { SECRET_COMMAND_TIMEOUT_MS, getWebSearchCredentialSource, getWebSearchProviderOrder, loadAgentBrowserConfigStateSync, resolveEnvInterpolations, } from "./config-policy.js";
|
|
4
|
+
export { AGENT_BROWSER_CONFIG_ENV, BRAVE_API_KEY_ENV, CONFIG_RELATIVE_PATH, DEFAULT_WEB_SEARCH_PROVIDER, EXA_API_KEY_ENV, EXA_SEARCH_TYPES, GLOBAL_CONFIG_RELATIVE_PATH, SECRET_COMMAND_TIMEOUT_MS, WEB_SEARCH_PROVIDER_CONFIG_KEYS, WEB_SEARCH_PROVIDER_DESCRIPTORS, WEB_SEARCH_PROVIDER_ENV_VARS, WEB_SEARCH_PROVIDERS, buildAgentBrowserConfigState, buildWebSearchCredentialSources, canRegisterWebSearchTool, classifyCredentialSource, formatBrowserExecutableStatus, formatBrowserProfileStatus, getAgentBrowserConfigPaths, getCredentialSourceSummary, getGlobalAgentBrowserConfigPath, getProjectAgentBrowserConfigPath, getWebSearchCredentialSource, getWebSearchProviderConfigKey, getWebSearchProviderDescriptor, getWebSearchProviderEnvVar, getWebSearchProviderLabel, getWebSearchProviderOrder, hasPotentialCredentialSource, isWebSearchProvider, loadAgentBrowserConfigStateSync, mergeAgentBrowserConfig, parseAgentBrowserConfigLayer, resolveEnvInterpolations, summarizeConfigFiles, validateAgentBrowserConfig, validateWebSearchProvider, } from "./config-policy.js";
|
|
6
5
|
const exec = promisify(execCallback);
|
|
7
|
-
async function readConfigLayer(path, scope, errors, warnings) {
|
|
8
|
-
let raw;
|
|
9
|
-
try {
|
|
10
|
-
raw = await readFile(path, "utf8");
|
|
11
|
-
}
|
|
12
|
-
catch (error) {
|
|
13
|
-
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
14
|
-
return undefined;
|
|
15
|
-
}
|
|
16
|
-
errors.push(`Could not read ${scope} config ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
17
|
-
return undefined;
|
|
18
|
-
}
|
|
19
|
-
return parseAgentBrowserConfigLayer(raw, path, scope, errors, warnings);
|
|
20
|
-
}
|
|
21
|
-
export async function loadAgentBrowserConfig(options = {}) {
|
|
22
|
-
const env = options.env ?? process.env;
|
|
23
|
-
const paths = getAgentBrowserConfigPaths({ cwd: options.cwd, env });
|
|
24
|
-
const includeProjectConfig = options.includeProjectConfig !== false;
|
|
25
|
-
const errors = [];
|
|
26
|
-
const warnings = [];
|
|
27
|
-
const layerCandidates = [
|
|
28
|
-
{ path: paths.global, scope: "global" },
|
|
29
|
-
...(includeProjectConfig ? [{ path: paths.project, scope: "project" }] : []),
|
|
30
|
-
...(paths.override ? [{ path: paths.override, scope: "override" }] : []),
|
|
31
|
-
];
|
|
32
|
-
const layers = [];
|
|
33
|
-
let mergedConfig = {};
|
|
34
|
-
for (const candidate of layerCandidates) {
|
|
35
|
-
const layer = await readConfigLayer(candidate.path, candidate.scope, errors, warnings);
|
|
36
|
-
if (!layer)
|
|
37
|
-
continue;
|
|
38
|
-
layers.push(layer);
|
|
39
|
-
mergedConfig = mergeAgentBrowserConfig(mergedConfig, layer.config);
|
|
40
|
-
}
|
|
41
|
-
return buildAgentBrowserConfigState({
|
|
42
|
-
env,
|
|
43
|
-
errors,
|
|
44
|
-
layers,
|
|
45
|
-
mergedConfig,
|
|
46
|
-
paths,
|
|
47
|
-
projectConfigIncluded: includeProjectConfig,
|
|
48
|
-
warnings,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
6
|
export function loadAgentBrowserConfigSync(options = {}) {
|
|
52
7
|
return loadAgentBrowserConfigStateSync(options);
|
|
53
8
|
}
|
|
@@ -70,7 +25,7 @@ async function resolveCommandCredential(rawValue, signal) {
|
|
|
70
25
|
throw new Error("Credential command failed without exposing command output. Check pi-agent-browser-config web-search status and the configured secret manager command.");
|
|
71
26
|
}
|
|
72
27
|
}
|
|
73
|
-
|
|
28
|
+
async function resolveCredentialSource(source, options = {}) {
|
|
74
29
|
if (!source)
|
|
75
30
|
return undefined;
|
|
76
31
|
let value;
|
|
@@ -6,8 +6,8 @@ import { fetchCdpJson, parseCdpTargets, parseCdpVersion, } from "./cdp.js";
|
|
|
6
6
|
import { discoverElectronApps, inspectElectronAppPath, inspectElectronExecutablePath, } from "./discovery.js";
|
|
7
7
|
import { createSecureTempDirectory, preserveSecureTempDirectory } from "../temp.js";
|
|
8
8
|
export const ELECTRON_LAUNCH_RECORD_VERSION = 1;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const ELECTRON_LAUNCH_DEFAULT_TIMEOUT_MS = 15_000;
|
|
10
|
+
const ELECTRON_LAUNCH_MAX_TIMEOUT_MS = 120_000;
|
|
11
11
|
const DEVTOOLS_ACTIVE_PORT_FILE = "DevToolsActivePort";
|
|
12
12
|
export const ELECTRON_PROFILE_DIR_PREFIX = "electron-profile-";
|
|
13
13
|
const ELECTRON_DEFAULT_APP_ARGS = ["--disable-extensions", "--no-first-run", "--no-default-browser-check"];
|
|
@@ -46,7 +46,7 @@ function policyEntryMatchesApp(entry, app) {
|
|
|
46
46
|
return false;
|
|
47
47
|
return appIdentifiers(app).some((identifier) => identifier.toLowerCase().includes(normalizedEntry));
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
function evaluateElectronLaunchPolicy(options) {
|
|
50
50
|
const denyEntry = options.deny?.find((entry) => policyEntryMatchesApp(entry, options.target));
|
|
51
51
|
if (denyEntry) {
|
|
52
52
|
return {
|
|
@@ -66,7 +66,7 @@ export function evaluateElectronLaunchPolicy(options) {
|
|
|
66
66
|
}
|
|
67
67
|
return undefined;
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
async function resolveElectronLaunchTarget(options) {
|
|
70
70
|
if (options.appPath)
|
|
71
71
|
return inspectElectronAppPath(options.appPath);
|
|
72
72
|
if (options.executablePath)
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join } from "node:path";
|
|
3
3
|
import { isRecord } from "./parsing.js";
|
|
4
|
-
import { getBrowserResultMessage } from "./browser-transcript.js";
|
|
5
|
-
import { getAgentBrowserSessionIdentityKey } from "./argv-grammar.js";
|
|
6
4
|
function isDirectoryOwner(source) {
|
|
7
5
|
if (/^(?:npm:pi-change-working-dir|git:github\.com\/fitchmultz\/pi-change-working-dir(?:\.git)?)(?:@.+)?$/.test(source.source))
|
|
8
6
|
return true;
|
|
@@ -50,17 +48,3 @@ export function getBrowserCwdError(cwd) {
|
|
|
50
48
|
}
|
|
51
49
|
return `Browser launch directory is unavailable: ${cwd}. Restore that directory, or explicitly use sessionMode: "fresh" without --session, or --config from the selected execution directory. The existing browser was left untouched.`;
|
|
52
50
|
}
|
|
53
|
-
export function restoreManagedSessionCwd(branch, sessionName, namespace, fallback) {
|
|
54
|
-
const key = getAgentBrowserSessionIdentityKey(sessionName, namespace);
|
|
55
|
-
for (const entry of [...branch].reverse()) {
|
|
56
|
-
const message = getBrowserResultMessage(entry);
|
|
57
|
-
if (!message)
|
|
58
|
-
continue;
|
|
59
|
-
const details = message.details;
|
|
60
|
-
if (!isRecord(details) || typeof details.sessionName !== "string" || typeof details.managedSessionCwd !== "string" || !isAbsolute(details.managedSessionCwd))
|
|
61
|
-
continue;
|
|
62
|
-
if (getAgentBrowserSessionIdentityKey(details.sessionName, typeof details.namespace === "string" ? details.namespace : undefined) === key)
|
|
63
|
-
return details.managedSessionCwd;
|
|
64
|
-
}
|
|
65
|
-
return fallback;
|
|
66
|
-
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { JsonSchema } from "../json-schema.js";
|
|
2
2
|
import { StringEnum as localStringEnum } from "../string-enum-schema.js";
|
|
3
3
|
import { ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS, ELECTRON_DISCOVERY_MAX_RESULTS } from "../electron/discovery.js";
|
|
4
|
-
import { AGENT_BROWSER_ELECTRON_HANDOFFS, AGENT_BROWSER_ELECTRON_TARGET_TYPES, AGENT_BROWSER_QA_LOAD_STATES, AGENT_BROWSER_SEMANTIC_ACTIONS, AGENT_BROWSER_SEMANTIC_LOCATORS, DEFAULT_SESSION_MODE, SOURCE_LOOKUP_MAX_WORKSPACE_FILES, } from "./types.js";
|
|
5
|
-
|
|
6
|
-
export const AGENT_BROWSER_CODE_MAX_TIMEOUT_MS = 300_000;
|
|
4
|
+
import { AGENT_BROWSER_CODE_MAX_TIMEOUT_MS, AGENT_BROWSER_ELECTRON_HANDOFFS, AGENT_BROWSER_ELECTRON_TARGET_TYPES, AGENT_BROWSER_QA_LOAD_STATES, AGENT_BROWSER_SEMANTIC_ACTIONS, AGENT_BROWSER_SEMANTIC_LOCATORS, DEFAULT_SESSION_MODE, SOURCE_LOOKUP_MAX_WORKSPACE_FILES, } from "./types.js";
|
|
5
|
+
const AGENT_BROWSER_ADVANCED_TOOLS = ["action", "qa", "electron", "source", "network"];
|
|
7
6
|
function outputProperties(Type) {
|
|
8
7
|
return {
|
|
9
8
|
outputPath: Type.Optional(Type.String({ description: "Result-data path, separate from browser artifact paths.", minLength: 1 })),
|
|
@@ -2,20 +2,34 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import which from "which";
|
|
5
6
|
import { isRecord } from "../parsing.js";
|
|
6
7
|
import { extractExplicitNamespace, extractExplicitSessionName, scanUpstreamGlobalFlagOccurrences } from "../argv-grammar.js";
|
|
7
8
|
import { getUpstreamEffectiveBatchSteps } from "../orchestration/batch-stdin.js";
|
|
8
9
|
import { extractUpstreamCommandTokens } from "../argv-descriptor.js";
|
|
9
10
|
import { isCloseAllCommand } from "../command-taxonomy.js";
|
|
10
11
|
import { redactSensitiveText, validateToolArgs } from "../runtime.js";
|
|
12
|
+
import { AGENT_BROWSER_CODE_MAX_TIMEOUT_MS } from "./types.js";
|
|
11
13
|
export const AGENT_BROWSER_SCRIPT_CODE_MAX_BYTES = 64 * 1_024;
|
|
12
14
|
export const AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS = 120_000;
|
|
13
15
|
export const AGENT_BROWSER_SCRIPT_NAMESPACE = "";
|
|
14
|
-
export const AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS = 300_000;
|
|
15
16
|
export const AGENT_BROWSER_SCRIPT_MAX_CALLS = 25;
|
|
16
17
|
export const AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES = 64 * 1_024;
|
|
17
18
|
export const AGENT_BROWSER_SCRIPT_IPC_MESSAGE_MAX_BYTES = 1 * 1_024 * 1_024;
|
|
18
|
-
|
|
19
|
+
const AGENT_BROWSER_SCRIPT_IPC_CUMULATIVE_MAX_BYTES = 8 * 1_024 * 1_024;
|
|
20
|
+
/**
|
|
21
|
+
* Resolve the Node runtime that hosts the permissioned code child. A Bun-compiled Pi binary
|
|
22
|
+
* points `process.execPath` at the pi executable, which rejects Node-only flags like
|
|
23
|
+
* `--permission`, so those hosts must spawn a real `node` from PATH instead.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveScriptChildNodePath(options) {
|
|
26
|
+
if (options.runtime.bun === undefined)
|
|
27
|
+
return options.execPath;
|
|
28
|
+
const nodePath = options.whichNode();
|
|
29
|
+
if (nodePath === null)
|
|
30
|
+
throw new Error("agent_browser_code requires a `node` runtime on PATH when pi runs on a Bun binary.");
|
|
31
|
+
return nodePath;
|
|
32
|
+
}
|
|
19
33
|
function findPackageRoot(startDir) {
|
|
20
34
|
let currentDir = startDir;
|
|
21
35
|
for (;;) {
|
|
@@ -34,7 +48,7 @@ function resolveScriptWorkerPath() {
|
|
|
34
48
|
return workerPath;
|
|
35
49
|
}
|
|
36
50
|
const SCRIPT_SESSION_NAME_PATTERN = /^piab-script-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
37
|
-
|
|
51
|
+
function validateAgentBrowserScriptSource(input) {
|
|
38
52
|
if (typeof input !== "string")
|
|
39
53
|
return { error: "script must be a string." };
|
|
40
54
|
const bytes = Buffer.byteLength(input, "utf8");
|
|
@@ -175,8 +189,8 @@ export async function runAgentBrowserScript(options) {
|
|
|
175
189
|
return buildFailedRun({ callCount: 0, emitCount: 0, error: compiled.error, failureCategory: "validation-error", rejectedCallCount: 0, steps: [] });
|
|
176
190
|
}
|
|
177
191
|
const timeoutMs = options.timeoutMs ?? AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS;
|
|
178
|
-
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs >
|
|
179
|
-
return buildFailedRun({ callCount: 0, emitCount: 0, error: `script timeoutMs must be between 1 and ${
|
|
192
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > AGENT_BROWSER_CODE_MAX_TIMEOUT_MS) {
|
|
193
|
+
return buildFailedRun({ callCount: 0, emitCount: 0, error: `script timeoutMs must be between 1 and ${AGENT_BROWSER_CODE_MAX_TIMEOUT_MS}.`, failureCategory: "validation-error", rejectedCallCount: 0, steps: [] });
|
|
180
194
|
}
|
|
181
195
|
if (options.signal?.aborted) {
|
|
182
196
|
return buildFailedRun({ aborted: true, callCount: 0, emitCount: 0, error: "Script execution was aborted.", failureCategory: "aborted", rejectedCallCount: 0, steps: [] });
|
|
@@ -189,7 +203,15 @@ export async function runAgentBrowserScript(options) {
|
|
|
189
203
|
const message = error instanceof Error ? error.message : "Compiled script worker is missing.";
|
|
190
204
|
return buildFailedRun({ callCount: 0, emitCount: 0, error: message, failureCategory: "missing-binary", rejectedCallCount: 0, steps: [] });
|
|
191
205
|
}
|
|
192
|
-
|
|
206
|
+
let childNodePath;
|
|
207
|
+
try {
|
|
208
|
+
childNodePath = resolveScriptChildNodePath({ runtime: { bun: process.versions.bun }, execPath: process.execPath, whichNode: () => which.sync("node", { nothrow: true }) });
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
const message = error instanceof Error ? error.message : "No Node runtime is available for the code child.";
|
|
212
|
+
return buildFailedRun({ callCount: 0, emitCount: 0, error: message, failureCategory: "missing-binary", rejectedCallCount: 0, steps: [] });
|
|
213
|
+
}
|
|
214
|
+
const child = spawn(childNodePath, [
|
|
193
215
|
"--permission",
|
|
194
216
|
"--max-old-space-size=64",
|
|
195
217
|
workerPath,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export const DEFAULT_SESSION_MODE = "auto";
|
|
2
|
+
export const AGENT_BROWSER_CODE_MAX_TIMEOUT_MS = 300_000;
|
|
2
3
|
export const AGENT_BROWSER_SEMANTIC_ACTIONS = ["check", "click", "fill", "select"];
|
|
3
4
|
export const AGENT_BROWSER_SEMANTIC_LOCATORS = ["alt", "label", "placeholder", "role", "testid", "text", "title"];
|
|
4
|
-
|
|
5
|
-
export const AGENT_BROWSER_JOB_STEP_ACTIONS = ["open", "click", "fill", "type", "select", "wait", "assertText", "assertUrl", "waitForDownload", "screenshot", "snapshot"];
|
|
5
|
+
const AGENT_BROWSER_JOB_STEP_ACTIONS = ["open", "click", "fill", "type", "select", "wait", "assertText", "assertUrl", "waitForDownload", "screenshot", "snapshot"];
|
|
6
6
|
export const AGENT_BROWSER_QA_LOAD_STATES = ["domcontentloaded", "load", "networkidle"];
|
|
7
7
|
export const AGENT_BROWSER_ELECTRON_ACTIONS = ["list", "launch", "status", "cleanup", "probe"];
|
|
8
8
|
export const AGENT_BROWSER_ELECTRON_HANDOFFS = ["connect", "tabs", "snapshot"];
|
|
@@ -82,9 +82,6 @@ const ownedManagedSessionStorage = new AsyncLocalStorage();
|
|
|
82
82
|
export async function withOwnedManagedSessionContext(context, run) {
|
|
83
83
|
return await ownedManagedSessionStorage.run(context, run);
|
|
84
84
|
}
|
|
85
|
-
export function getOwnedManagedSessionRestoreKey() {
|
|
86
|
-
return ownedManagedSessionStorage.getStore()?.restoreKey;
|
|
87
|
-
}
|
|
88
85
|
export function resolveOwnedManagedSessionContext(options) {
|
|
89
86
|
const namespace = canonicalizeAgentBrowserNamespace(options.namespace);
|
|
90
87
|
const currentNamespace = canonicalizeAgentBrowserNamespace(options.currentManagedSessionNamespace);
|
|
@@ -115,7 +112,7 @@ function hasExplicitConfigArg(args) {
|
|
|
115
112
|
function closesBrowserSession(args) {
|
|
116
113
|
return ["close", "exit", "quit"].includes(parseCommandInfo(args).command ?? "");
|
|
117
114
|
}
|
|
118
|
-
|
|
115
|
+
function agentBrowserExplicitConfigIsPresent(parentEnv = getAgentBrowserProcessEnvironment(), args = []) {
|
|
119
116
|
return hasExplicitConfigArg(args) || hasUpstreamEnvValue(parentEnv, AGENT_BROWSER_CONFIG_ENV);
|
|
120
117
|
}
|
|
121
118
|
/** Caller-selected upstream config disables the wrapper's automatic restore injection without blocking that config. */
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { JsonSchema } from "./json-schema.js";
|
|
2
|
+
import { isRecord } from "./parsing.js";
|
|
3
|
+
import { finalizeAgentBrowserFailure } from "./pi-tool-rendering.js";
|
|
4
|
+
import { isPlainTextInspectionArgs } from "./runtime.js";
|
|
5
|
+
import { projectAgentBrowserObservation, OBSERVATION_INLINE_MAX_CHARS } from "./results/presentation/content.js";
|
|
6
|
+
export const AGENT_BROWSER_NAMESPACE = {
|
|
7
|
+
name: "browser",
|
|
8
|
+
description: "Browser commands, bounded browser code, advanced UI checks, Electron, and optional web search.",
|
|
9
|
+
instructions: "Use agent_browser for native commands and batch --bail for fixed sequences. Use agent_browser_code for a fresh bounded JS cell with a persistent browser. Enable advanced direct tools with agent_browser_tools before calling them. Check success and nextActions; structured imageObservations are capture metadata, not inline images. Optional web search is available only when configured. Full browser instructions remain in the agent_browser prompt section.",
|
|
10
|
+
};
|
|
11
|
+
export const AGENT_BROWSER_OUTPUT_SCHEMA = JsonSchema.Object({
|
|
12
|
+
success: JsonSchema.Boolean(),
|
|
13
|
+
resultCategory: JsonSchema.Union([JsonSchema.Literal("success"), JsonSchema.Literal("failure")]),
|
|
14
|
+
data: JsonSchema.Optional(JsonSchema.Unsafe({})),
|
|
15
|
+
error: JsonSchema.Optional(JsonSchema.Unsafe({})),
|
|
16
|
+
summary: JsonSchema.Optional(JsonSchema.String()),
|
|
17
|
+
nextActions: JsonSchema.Optional(JsonSchema.Array(JsonSchema.Object({}))),
|
|
18
|
+
}, { additionalProperties: true });
|
|
19
|
+
/** Final public boundary, after execution, persistence, and any requested output export. */
|
|
20
|
+
export async function finalizeAgentBrowserNativeResult(result, input) {
|
|
21
|
+
const finalized = finalizeAgentBrowserFailure(result, input);
|
|
22
|
+
// Native help/version stays plain text; the entrypoint intentionally skips observation rendering for it.
|
|
23
|
+
if (isRecord(input) && Array.isArray(input.args) && isPlainTextInspectionArgs(input.args))
|
|
24
|
+
return finalized;
|
|
25
|
+
const succeeded = finalized.isError !== true;
|
|
26
|
+
// The normal entrypoint supplies the exact bounded projection, including its complete spill.
|
|
27
|
+
if (isRecord(result.structuredContent) && result.structuredContent.success === succeeded)
|
|
28
|
+
return finalized;
|
|
29
|
+
const details = isRecord(result.details) ? result.details : {};
|
|
30
|
+
const observation = projectAgentBrowserObservation(details, succeeded);
|
|
31
|
+
if (JSON.stringify(observation).length > OBSERVATION_INLINE_MAX_CHARS) {
|
|
32
|
+
const { renderAgentBrowserObservation } = await import("./results/presentation/large-output.js");
|
|
33
|
+
const rendered = await renderAgentBrowserObservation({
|
|
34
|
+
content: finalized.content, details, succeeded,
|
|
35
|
+
json: isRecord(input) && ("code" in input || Array.isArray(input.args) && input.args.includes("--json")),
|
|
36
|
+
// The caller's exact visible output stays inline (for example formatted search results); only the structured field is bounded.
|
|
37
|
+
preserveContent: true,
|
|
38
|
+
});
|
|
39
|
+
const { artifactManifest, ...renderedResult } = rendered;
|
|
40
|
+
return finalizeAgentBrowserFailure({
|
|
41
|
+
...finalized, ...renderedResult,
|
|
42
|
+
details: artifactManifest ? { ...details, artifactManifest } : details,
|
|
43
|
+
}, input);
|
|
44
|
+
}
|
|
45
|
+
return { ...finalized, structuredContent: JSON.parse(JSON.stringify(observation)) };
|
|
46
|
+
}
|
|
@@ -139,12 +139,3 @@ export function getUpstreamEffectiveBatchSteps(commandTokens, stdin) {
|
|
|
139
139
|
return argumentSteps;
|
|
140
140
|
return parseUserBatchStdin(stdin).steps ?? [];
|
|
141
141
|
}
|
|
142
|
-
export function parseValidBatchStepEntries(stdin) {
|
|
143
|
-
const parsed = parseBatchStdinJsonArray(stdin);
|
|
144
|
-
if (parsed.error || parsed.steps === undefined)
|
|
145
|
-
return [];
|
|
146
|
-
return parsed.steps.flatMap((step, index) => {
|
|
147
|
-
const validated = validateUserBatchStep(step, index);
|
|
148
|
-
return validated.ok ? [{ index, step: validated.step }] : [];
|
|
149
|
-
});
|
|
150
|
-
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getSessionArtifactManifestEntryKey, isPendingRecordingCommand, mergeSessionArtifactManifest } from "../../results/artifact-manifest.js";
|
|
2
|
+
export function mergeBrowserRunArtifactManifest(current, initial, updated) {
|
|
3
|
+
if (!updated || updated === initial)
|
|
4
|
+
return current;
|
|
5
|
+
if (current === initial)
|
|
6
|
+
return updated;
|
|
7
|
+
const initialEntries = new Map((initial?.entries ?? []).map((entry) => [getSessionArtifactManifestEntryKey(entry), entry]));
|
|
8
|
+
const changedEntries = updated.entries
|
|
9
|
+
.map((entry, index) => ({ entry, index }))
|
|
10
|
+
.filter(({ entry }) => initialEntries.get(getSessionArtifactManifestEntryKey(entry)) !== entry)
|
|
11
|
+
.sort((left, right) => left.entry.createdAtMs - right.entry.createdAtMs
|
|
12
|
+
|| Number(isPendingRecordingCommand(left.entry.command, left.entry.subcommand, left.entry.kind)) - Number(isPendingRecordingCommand(right.entry.command, right.entry.subcommand, right.entry.kind))
|
|
13
|
+
|| left.index - right.index)
|
|
14
|
+
.map(({ entry }) => entry);
|
|
15
|
+
return changedEntries.length === 0
|
|
16
|
+
? current
|
|
17
|
+
: mergeSessionArtifactManifest({
|
|
18
|
+
base: current,
|
|
19
|
+
entries: changedEntries,
|
|
20
|
+
nowMs: Math.max(Date.now(), (current?.updatedAtMs ?? 0) + 1, updated.updatedAtMs),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -566,7 +566,7 @@ export function formatQaAttachedTargetText(target) {
|
|
|
566
566
|
return undefined;
|
|
567
567
|
return ["QA attached target:", target.sessionName, target.title, target.url].filter((part) => typeof part === "string" && part.length > 0).join(" — ");
|
|
568
568
|
}
|
|
569
|
-
|
|
569
|
+
function buildQaAttachedRecoveryNextActions(sessionName) {
|
|
570
570
|
const sessionArgs = (args) => withOptionalSessionArgs(sessionName, args);
|
|
571
571
|
return [
|
|
572
572
|
buildNextToolAction({
|
|
@@ -26,13 +26,13 @@ export function buildMissingBinaryMessage() {
|
|
|
26
26
|
].join("\n");
|
|
27
27
|
}
|
|
28
28
|
const SEMANTIC_ACTION_CANDIDATE_ACTION_IDS = new Set(["try-button-name-candidate", "try-link-name-candidate"]);
|
|
29
|
-
|
|
29
|
+
function formatSemanticActionCandidateText(actions) {
|
|
30
30
|
const candidateActions = actions.filter((action) => SEMANTIC_ACTION_CANDIDATE_ACTION_IDS.has(action.id) && action.params?.args);
|
|
31
31
|
if (candidateActions.length === 0)
|
|
32
32
|
return undefined;
|
|
33
33
|
return ["Agent-browser candidate fallbacks:", ...candidateActions.map((action) => `- ${action.id}: agent_browser ${JSON.stringify({ args: action.params?.args })} — ${action.reason}`)].join("\n");
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
function buildSemanticActionCandidateActions(compiled) {
|
|
36
36
|
const commandIndex = getCompiledSemanticActionCommandIndex(compiled);
|
|
37
37
|
if (commandIndex < 0 || compiled.args[commandIndex] !== "find")
|
|
38
38
|
return [];
|
|
@@ -333,7 +333,7 @@ function resolveSemanticActionVisibleRefArgsFromSnapshot(compiled, snapshotData)
|
|
|
333
333
|
return undefined;
|
|
334
334
|
return { args: [...getCompiledSemanticActionSessionPrefix(compiled), ...resolution.args], snapshot: resolution.snapshot };
|
|
335
335
|
}
|
|
336
|
-
|
|
336
|
+
async function resolveSemanticActionVisibleRefArgs(options) {
|
|
337
337
|
if (!options.compiled || !options.sessionName)
|
|
338
338
|
return undefined;
|
|
339
339
|
const snapshotData = await runSessionCommandData({ args: ["snapshot", "-i"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName, signal: options.signal });
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "../argv-grammar.js";
|
|
3
|
+
// Serializes managed-session read/modify/write work so overlapping tool calls cannot promote stale state or close an in-use session.
|
|
4
|
+
export class AsyncExecutionQueue {
|
|
5
|
+
tail = Promise.resolve();
|
|
6
|
+
active = new AsyncLocalStorage();
|
|
7
|
+
isCurrent() { return this.active.getStore()?.active === true; }
|
|
8
|
+
run(work, signal, barrier = Promise.resolve()) {
|
|
9
|
+
if (this.isCurrent()) {
|
|
10
|
+
signal?.throwIfAborted();
|
|
11
|
+
return work();
|
|
12
|
+
}
|
|
13
|
+
const previous = this.tail;
|
|
14
|
+
let release;
|
|
15
|
+
this.tail = new Promise((resolve) => {
|
|
16
|
+
release = resolve;
|
|
17
|
+
});
|
|
18
|
+
let rejectWaiting;
|
|
19
|
+
const cancelled = new Promise((_resolve, reject) => { rejectWaiting = reject; });
|
|
20
|
+
const abortWaiting = () => rejectWaiting(signal?.reason);
|
|
21
|
+
signal?.addEventListener("abort", abortWaiting, { once: true });
|
|
22
|
+
if (signal?.aborted)
|
|
23
|
+
abortWaiting();
|
|
24
|
+
const execution = (async () => {
|
|
25
|
+
await Promise.all([previous, barrier]);
|
|
26
|
+
signal?.removeEventListener("abort", abortWaiting);
|
|
27
|
+
try {
|
|
28
|
+
signal?.throwIfAborted();
|
|
29
|
+
const scope = { active: true };
|
|
30
|
+
try {
|
|
31
|
+
return await this.active.run(scope, work);
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
scope.active = false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
release();
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
41
|
+
return signal ? Promise.race([execution, cancelled]) : execution;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export class KeyedAsyncExecutionQueue {
|
|
45
|
+
barriers = new Map();
|
|
46
|
+
entries = new Map();
|
|
47
|
+
async run(key, namespace, work, signal) {
|
|
48
|
+
const entry = this.entries.get(key) ?? { queue: new AsyncExecutionQueue(), users: 0 };
|
|
49
|
+
if (entry.queue.isCurrent())
|
|
50
|
+
return work();
|
|
51
|
+
const barrier = this.barriers.get(getAgentBrowserSessionIdentityKey("", namespace)) ?? Promise.resolve();
|
|
52
|
+
entry.users += 1;
|
|
53
|
+
this.entries.set(key, entry);
|
|
54
|
+
try {
|
|
55
|
+
return await entry.queue.run(work, signal, barrier);
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
entry.users -= 1;
|
|
59
|
+
if (entry.users === 0 && this.entries.get(key) === entry)
|
|
60
|
+
this.entries.delete(key);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async runExclusive(namespace, work) {
|
|
64
|
+
const namespaceKey = getAgentBrowserSessionIdentityKey("", namespace);
|
|
65
|
+
const previous = this.barriers.get(namespaceKey) ?? Promise.resolve();
|
|
66
|
+
let release;
|
|
67
|
+
const blocked = new Promise((resolve) => {
|
|
68
|
+
release = resolve;
|
|
69
|
+
});
|
|
70
|
+
const barrier = previous.then(() => blocked);
|
|
71
|
+
this.barriers.set(namespaceKey, barrier);
|
|
72
|
+
const drains = [...this.entries]
|
|
73
|
+
.filter(([key]) => isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
|
|
74
|
+
.map(([, { queue }]) => queue.run(async () => undefined));
|
|
75
|
+
await previous;
|
|
76
|
+
await Promise.all(drains);
|
|
77
|
+
try {
|
|
78
|
+
return await work();
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
release();
|
|
82
|
+
if (this.barriers.get(namespaceKey) === barrier)
|
|
83
|
+
this.barriers.delete(namespaceKey);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|