pi-agent-browser-native 0.3.0 → 0.5.0
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 +127 -0
- package/README.md +63 -20
- package/dist/extensions/agent-browser/index.js +787 -105
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
- package/dist/extensions/agent-browser/lib/argv-grammar.js +44 -2
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
- package/dist/extensions/agent-browser/lib/command-policy.js +1 -1
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -2
- package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -4
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
- package/dist/extensions/agent-browser/lib/input-modes/params.js +22 -23
- package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
- package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +8 -0
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -1
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -36
- package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
- package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +47 -29
- package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -24
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +26 -25
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +17 -3
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -4
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +129 -32
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +191 -55
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +57 -29
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +15 -11
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
- package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
- package/dist/extensions/agent-browser/lib/playbook.js +18 -15
- package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
- package/dist/extensions/agent-browser/lib/process-identity.js +4 -4
- package/dist/extensions/agent-browser/lib/process.js +131 -41
- package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +62 -5
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
- package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
- package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -0
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -1
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -11
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
- package/dist/extensions/agent-browser/lib/results/presentation.js +6 -3
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -0
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -8
- package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
- package/dist/extensions/agent-browser/lib/runtime.js +116 -39
- package/dist/extensions/agent-browser/lib/session-page-state.js +62 -10
- package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
- package/dist/extensions/agent-browser/script-worker.js +169 -0
- package/dist/scripts/agent-browser-target.mjs +3 -0
- package/docs/ARCHITECTURE.md +40 -21
- package/docs/COMMAND_REFERENCE.md +90 -35
- package/docs/RELEASE.md +3 -3
- package/docs/REQUIREMENTS.md +4 -2
- package/docs/SUPPORT_MATRIX.md +26 -19
- package/docs/TOOL_CONTRACT.md +93 -52
- package/package.json +3 -1
- package/platform-smoke.config.mjs +2 -2
- package/scripts/agent-browser-capability-baseline.mjs +24 -6
- package/scripts/agent-browser-target.mjs +3 -0
- package/scripts/build.mjs +41 -0
- package/scripts/doctor.mjs +7 -6
- package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
- package/scripts/platform-smoke/targets.mjs +12 -6
|
@@ -2,26 +2,37 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { Text } from "@earendil-works/pi-tui";
|
|
5
|
+
import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./lib/batch-lifecycle.js";
|
|
5
6
|
import { PROJECT_RULE_PROMPT, buildBrowserDefaultProfileGuideline, buildBrowserExecutablePathGuideline, buildToolPromptGuidelines, } from "./lib/playbook.js";
|
|
6
7
|
import { SessionPageState } from "./lib/session-page-state.js";
|
|
7
|
-
import { canUseHeadlessCompatibilityUserAgent, createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName,
|
|
8
|
-
import { extractExplicitNamespace, extractExplicitSessionName, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace } from "./lib/argv-grammar.js";
|
|
8
|
+
import { canUseHeadlessCompatibilityUserAgent, createEphemeralSessionSeed, createFreshSessionName, createImplicitSessionName, extractUpstreamCommandTokens, getImplicitSessionCloseTimeoutMs, getImplicitSessionIdleTimeoutMs, isRestorableManagedSessionName, restoreManagedSessionStateFromBranch, validateToolArgs, redactSensitiveText, isPlainTextInspectionArgs, } from "./lib/runtime.js";
|
|
9
|
+
import { extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, resolveAgentBrowserNamespace } from "./lib/argv-grammar.js";
|
|
10
|
+
import { parseArgvDescriptor } from "./lib/argv-descriptor.js";
|
|
11
|
+
import { needsManagedSession } from "./lib/command-policy.js";
|
|
9
12
|
import { cleanupManagedSessionRestoreConfig, ManagedSessionRestoreState } from "./lib/managed-session-restore.js";
|
|
10
13
|
import { isRecord } from "./lib/parsing.js";
|
|
14
|
+
import { runAgentBrowserProcess } from "./lib/process.js";
|
|
15
|
+
import { getAgentBrowserProcessEnvironment, withIsolatedAgentBrowserEnvironment } from "./lib/process-environment.js";
|
|
16
|
+
import { TARGET_AGENT_BROWSER_VERSION, TARGET_AGENT_BROWSER_VERSION_LABEL, getAgentBrowserVersionValidationError, parseAgentBrowserVersionOutput, } from "./lib/upstream-version.js";
|
|
11
17
|
import { buildPromptPolicy, getLatestUserPrompt, shouldAppendBrowserSystemPrompt } from "./lib/prompt-policy.js";
|
|
12
|
-
import { isCloseCommand } from "./lib/command-taxonomy.js";
|
|
18
|
+
import { isCloseAllCommand, isCloseCommand } from "./lib/command-taxonomy.js";
|
|
13
19
|
import { hasLaunchScopedFlagToken } from "./lib/launch-scoped-flags.js";
|
|
14
20
|
import { cleanupSecureTempArtifacts } from "./lib/temp.js";
|
|
15
21
|
import { AGENT_BROWSER_PARAMS } from "./lib/input-modes/params.js";
|
|
22
|
+
import { AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS, AGENT_BROWSER_SCRIPT_NAMESPACE, createAgentBrowserScriptSessionName, isAgentBrowserScriptSessionName, runAgentBrowserScript, } from "./lib/input-modes/script.js";
|
|
16
23
|
import { parseAllowedDomainsPolicyFromArgs } from "./lib/navigation-policy.js";
|
|
17
24
|
import { closeManagedSession, getSessionContextKey, runAgentBrowserTool } from "./lib/orchestration/browser-run/index.js";
|
|
25
|
+
import { canonicalizeExplicitArtifactDestination, getExplicitArtifactDestination } from "./lib/orchestration/browser-run/artifact-paths.js";
|
|
18
26
|
import { findElectronLaunchRecordForSession, getActiveElectronRecords } from "./lib/orchestration/browser-run/session-state.js";
|
|
19
|
-
import {
|
|
27
|
+
import { parseBatchCommandArgument, parseUserBatchStdin } from "./lib/orchestration/batch-stdin.js";
|
|
20
28
|
import { ELECTRON_POST_COMMAND_STATUS_SETTLE_MS, ELECTRON_PROFILE_ISOLATION_DETAILS, cleanupActiveElectronHostLaunches, handleElectronHostInput, restoreElectronLaunchRecordsFromBranch, } from "./lib/orchestration/electron-host/index.js";
|
|
21
29
|
import { buildValidationFailureResult, resolveAgentBrowserInput } from "./lib/orchestration/input-plan.js";
|
|
22
|
-
import { applyAgentBrowserOutputPath, getAgentBrowserOutputPathValidationError } from "./lib/orchestration/output-file.js";
|
|
23
|
-
import {
|
|
30
|
+
import { applyAgentBrowserOutputPath, getAgentBrowserOutputPathValidationError, normalizeRequestedOutputPath } from "./lib/orchestration/output-file.js";
|
|
31
|
+
import { appendScriptSessionLease, buildScriptBrowserEnvelope, buildScriptToolResult, getScriptSessionLeasesFromBranch } from "./lib/orchestration/script-mode.js";
|
|
32
|
+
import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isPendingRecordingCommand, isSessionArtifactManifest, mergeSessionArtifactManifest, retirePendingRecordingManifestEntries } from "./lib/results/artifact-manifest.js";
|
|
33
|
+
import { appendUniqueAgentBrowserNextActions, applyNamespaceToNextActions, applySessionToNextActions, buildNextToolAction } from "./lib/results/next-actions.js";
|
|
24
34
|
import { canRegisterWebSearchTool, loadAgentBrowserConfigSync } from "./lib/config.js";
|
|
35
|
+
import { appendRecordingReservationTransition, applyRecordingArtifactsToReservations, restoreRecordingReservationStateFromBranch, retireRecordingReservation, } from "./lib/recording-reservations.js";
|
|
25
36
|
import { createAgentBrowserWebSearchTool } from "./lib/web-search.js";
|
|
26
37
|
import { isDirectAgentBrowserBashAllowed, isHarmlessAgentBrowserInspectionCommand, looksLikeDirectAgentBrowserBash, } from "./lib/bash-guard.js";
|
|
27
38
|
import { AgentBrowserResultComponent, buildAgentBrowserToolResultPatch, formatAgentBrowserRenderCall, formatAgentBrowserRenderResult, } from "./lib/pi-tool-rendering.js";
|
|
@@ -30,22 +41,94 @@ function isBashToolCallEvent(event) {
|
|
|
30
41
|
return false;
|
|
31
42
|
return typeof event.input.command === "string";
|
|
32
43
|
}
|
|
33
|
-
function
|
|
34
|
-
const commandTokens =
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
function getArtifactCommandSteps(args, stdin) {
|
|
45
|
+
const commandTokens = extractUpstreamCommandTokens(args);
|
|
46
|
+
const batch = commandTokens[0] === "batch";
|
|
47
|
+
if (!batch)
|
|
48
|
+
return { batch, steps: commandTokens.length > 0 ? [commandTokens] : [] };
|
|
49
|
+
const steps = [];
|
|
50
|
+
for (const command of commandTokens.slice(1)) {
|
|
51
|
+
if (command === "--bail")
|
|
52
|
+
continue;
|
|
53
|
+
const parsed = parseBatchCommandArgument(command);
|
|
54
|
+
if (parsed.error || !parsed.step)
|
|
55
|
+
return { batch, error: `Unsupported batch step ${steps.length + 1}: ${parsed.error ?? "command could not be parsed safely"}`, steps };
|
|
56
|
+
steps.push(parsed.step);
|
|
37
57
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
58
|
+
// Upstream executes raw argument steps exclusively when any exist, so ignored
|
|
59
|
+
// stdin must not add artifact/lifecycle steps or fail this preflight.
|
|
60
|
+
if (steps.length > 0)
|
|
61
|
+
return { batch, steps };
|
|
62
|
+
const parsed = parseUserBatchStdin(stdin);
|
|
63
|
+
return parsed.error ? { batch, error: parsed.error, steps } : { batch, steps: parsed.steps ?? [] };
|
|
64
|
+
}
|
|
65
|
+
function getArtifactPreflightValidationError(options) {
|
|
66
|
+
const { batch, error, steps } = getArtifactCommandSteps(options.args, options.stdin);
|
|
67
|
+
if (error)
|
|
68
|
+
return error;
|
|
69
|
+
const activeRecordingDestinations = new Set();
|
|
70
|
+
const cleanupOnly = steps.length > 0 && steps.every((step) => {
|
|
71
|
+
const [command, subcommand] = extractUpstreamCommandTokens(step);
|
|
72
|
+
return isCloseCommand(command) || (command === "record" && subcommand === "stop");
|
|
73
|
+
});
|
|
74
|
+
for (const reservation of options.activeRecordingReservations ?? []) {
|
|
75
|
+
try {
|
|
76
|
+
activeRecordingDestinations.add(canonicalizeExplicitArtifactDestination(reservation.cwd, reservation.absolutePath));
|
|
77
|
+
}
|
|
78
|
+
catch (canonicalizationError) {
|
|
79
|
+
if (!cleanupOnly)
|
|
80
|
+
return canonicalizationError instanceof Error ? canonicalizationError.message : "An active recording destination could not be resolved safely.";
|
|
81
|
+
}
|
|
41
82
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
83
|
+
let canonicalOutputPath;
|
|
84
|
+
if (options.outputPath) {
|
|
85
|
+
try {
|
|
86
|
+
canonicalOutputPath = canonicalizeExplicitArtifactDestination(options.cwd, normalizeRequestedOutputPath(options.outputPath));
|
|
87
|
+
if (activeRecordingDestinations.has(canonicalOutputPath)) {
|
|
88
|
+
return `Unsupported outputPath: ${options.outputPath} is reserved by an active recording. Stop that recording first or use a distinct path.`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (canonicalizationError) {
|
|
92
|
+
return canonicalizationError instanceof Error ? canonicalizationError.message : `outputPath ${options.outputPath} could not be resolved safely.`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const artifactDestinations = new Map();
|
|
96
|
+
let sawBatchClose = false;
|
|
97
|
+
for (const [index, step] of steps.entries()) {
|
|
98
|
+
const commandStep = extractUpstreamCommandTokens(step);
|
|
99
|
+
if (batch) {
|
|
100
|
+
const stepValidationError = validateToolArgs(step);
|
|
101
|
+
if (stepValidationError)
|
|
102
|
+
return `Unsupported batch step ${index + 1}: ${stepValidationError}`;
|
|
103
|
+
if (sawBatchClose && commandStep[0] === "record" && (commandStep[1] === "start" || commandStep[1] === "restart")) {
|
|
104
|
+
return `Unsupported batch step ${index + 1}: record ${commandStep[1]} cannot follow close, quit, or exit in one upstream batch because upstream can report success without starting a recording. Split the close and recording into separate agent_browser calls.`;
|
|
105
|
+
}
|
|
106
|
+
if (isCloseCommand(commandStep[0]))
|
|
107
|
+
sawBatchClose = true;
|
|
108
|
+
}
|
|
109
|
+
const artifactDestination = getExplicitArtifactDestination(commandStep);
|
|
110
|
+
if (artifactDestination) {
|
|
111
|
+
let canonicalDestination;
|
|
112
|
+
try {
|
|
113
|
+
canonicalDestination = canonicalizeExplicitArtifactDestination(options.cwd, artifactDestination);
|
|
114
|
+
}
|
|
115
|
+
catch (canonicalizationError) {
|
|
116
|
+
return canonicalizationError instanceof Error ? canonicalizationError.message : `Artifact destination ${artifactDestination} could not be resolved safely.`;
|
|
117
|
+
}
|
|
118
|
+
if (canonicalOutputPath === canonicalDestination) {
|
|
119
|
+
return `Unsupported outputPath: ${options.outputPath} resolves to the same destination as artifact path ${artifactDestination}. Use distinct paths so the tool-result JSON cannot overwrite the browser artifact.`;
|
|
120
|
+
}
|
|
121
|
+
if (activeRecordingDestinations.has(canonicalDestination)) {
|
|
122
|
+
const prefix = batch ? `Unsupported batch artifact destination in step ${index + 1}` : "Unsupported artifact destination";
|
|
123
|
+
return `${prefix}: ${artifactDestination} is reserved by an active recording. Stop that recording first or use a distinct path.`;
|
|
124
|
+
}
|
|
125
|
+
const priorStep = artifactDestinations.get(canonicalDestination);
|
|
126
|
+
if (priorStep !== undefined) {
|
|
127
|
+
return `Unsupported batch artifact destination in step ${index + 1}: ${artifactDestination} is already written by step ${priorStep + 1}. Use distinct paths or split the batch so each artifact can be verified independently.`;
|
|
128
|
+
}
|
|
129
|
+
artifactDestinations.set(canonicalDestination, index);
|
|
130
|
+
}
|
|
131
|
+
if (batch && commandStep[0] === "screenshot" && step.includes("--annotate")) {
|
|
49
132
|
return [
|
|
50
133
|
`Unsupported batch screenshot annotation in step ${index + 1}: put --annotate in top-level args, not inside the batch step.`,
|
|
51
134
|
`Use: { "args": ["--annotate", "batch"], "stdin": "[[\\"screenshot\\",\\"/path/to/image.png\\"]]" }`,
|
|
@@ -54,6 +137,51 @@ function getBatchPreflightValidationError(args, stdin) {
|
|
|
54
137
|
}
|
|
55
138
|
return undefined;
|
|
56
139
|
}
|
|
140
|
+
function commandClosesAllSessions(args, stdin) {
|
|
141
|
+
const parsed = getArtifactCommandSteps(args, stdin);
|
|
142
|
+
return !parsed.error && parsed.steps.some((step) => isCloseAllCommand(extractUpstreamCommandTokens(step)));
|
|
143
|
+
}
|
|
144
|
+
function commandTouchesArtifactLifecycle(args, stdin, outputPath) {
|
|
145
|
+
if (outputPath)
|
|
146
|
+
return true;
|
|
147
|
+
const parsed = getArtifactCommandSteps(args, stdin);
|
|
148
|
+
if (parsed.error)
|
|
149
|
+
return true;
|
|
150
|
+
return parsed.steps.some((step) => {
|
|
151
|
+
const commandStep = extractUpstreamCommandTokens(step);
|
|
152
|
+
return getExplicitArtifactDestination(commandStep) !== undefined || commandStep[0] === "record" || commandStep[0] === "screenshot" || isCloseCommand(commandStep[0]);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function isResultFileArtifact(artifact) {
|
|
156
|
+
return isRecord(artifact)
|
|
157
|
+
&& typeof artifact.absolutePath === "string"
|
|
158
|
+
&& typeof artifact.kind === "string"
|
|
159
|
+
&& typeof artifact.path === "string";
|
|
160
|
+
}
|
|
161
|
+
function getResultFileArtifacts(result) {
|
|
162
|
+
const details = isRecord(result.details) ? result.details : undefined;
|
|
163
|
+
return Array.isArray(details?.artifacts) ? details.artifacts.filter(isResultFileArtifact) : [];
|
|
164
|
+
}
|
|
165
|
+
function reportsNoRecordingInProgress(value) {
|
|
166
|
+
try {
|
|
167
|
+
return /no recording in progress/i.test(JSON.stringify(value));
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function batchStepReportsNoRecordingInProgress(step) {
|
|
174
|
+
if (!isRecord(step) || step.success !== false)
|
|
175
|
+
return false;
|
|
176
|
+
const command = Array.isArray(step.command) && step.command.every((token) => typeof token === "string") ? extractUpstreamCommandTokens(step.command) : [];
|
|
177
|
+
return command[0] === "record" && command[1] === "stop" && reportsNoRecordingInProgress(step);
|
|
178
|
+
}
|
|
179
|
+
function resultReportsNoRecordingInProgress(result) {
|
|
180
|
+
if (result.isError !== true)
|
|
181
|
+
return false;
|
|
182
|
+
const details = isRecord(result.details) ? result.details : undefined;
|
|
183
|
+
return details?.command === "record" && details.subcommand === "stop" && reportsNoRecordingInProgress(result.content);
|
|
184
|
+
}
|
|
57
185
|
function restoreArtifactManifestFromBranch(branch) {
|
|
58
186
|
let restoredManifest;
|
|
59
187
|
for (const entry of branch) {
|
|
@@ -69,6 +197,12 @@ function restoreArtifactManifestFromBranch(branch) {
|
|
|
69
197
|
}
|
|
70
198
|
return restoredManifest;
|
|
71
199
|
}
|
|
200
|
+
function getRecognizedCompatibilityWorkaround(value) {
|
|
201
|
+
const workaround = isRecord(value) ? value : undefined;
|
|
202
|
+
return (workaround?.id === "chatgpt-headless-user-agent" || workaround?.id === "cloudflare-headless-user-agent") && typeof workaround.reason === "string"
|
|
203
|
+
? { id: workaround.id, reason: workaround.reason }
|
|
204
|
+
: undefined;
|
|
205
|
+
}
|
|
72
206
|
function restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, sessionName, namespace) {
|
|
73
207
|
let restored;
|
|
74
208
|
const targetKey = getSessionContextKey(sessionName, namespace);
|
|
@@ -81,12 +215,9 @@ function restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, sessionN
|
|
|
81
215
|
const details = isRecord(message.details) ? message.details : undefined;
|
|
82
216
|
if (!details)
|
|
83
217
|
continue;
|
|
84
|
-
const workaround = isRecord(details.compatibilityWorkaround) ? details.compatibilityWorkaround : undefined;
|
|
85
218
|
if (getSessionContextKey(typeof details.sessionName === "string" ? details.sessionName : undefined, typeof details.namespace === "string" ? details.namespace : undefined) !== targetKey)
|
|
86
219
|
continue;
|
|
87
|
-
const recognizedWorkaround = (
|
|
88
|
-
? { id: workaround.id, reason: workaround.reason }
|
|
89
|
-
: undefined;
|
|
220
|
+
const recognizedWorkaround = getRecognizedCompatibilityWorkaround(details.compatibilityWorkaround);
|
|
90
221
|
const succeeded = getSuccessfulToolResult(details, message);
|
|
91
222
|
const outcome = getManagedSessionOutcome(details);
|
|
92
223
|
const activeAfterFailure = recognizedWorkaround
|
|
@@ -163,9 +294,21 @@ function getToolResultArgs(details) {
|
|
|
163
294
|
return details.effectiveArgs;
|
|
164
295
|
return [];
|
|
165
296
|
}
|
|
166
|
-
function
|
|
297
|
+
function detailsReportCloseAllApplied(details, succeeded) {
|
|
298
|
+
const args = getToolResultArgs(details);
|
|
299
|
+
return details.closeAllApplied === true
|
|
300
|
+
|| (succeeded && isCloseAllCommand(extractUpstreamCommandTokens(args)))
|
|
301
|
+
|| batchHasSuccessfulCloseAll(details.batchSteps);
|
|
302
|
+
}
|
|
303
|
+
function deleteIdentityKeysInNamespace(entries, namespace) {
|
|
304
|
+
for (const key of entries.keys()) {
|
|
305
|
+
if (isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
|
|
306
|
+
entries.delete(key);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function isAttachedBrowserInvocation(args, env = getAgentBrowserProcessEnvironment()) {
|
|
167
310
|
const autoConnectEnv = env.AGENT_BROWSER_AUTO_CONNECT;
|
|
168
|
-
return
|
|
311
|
+
return extractUpstreamCommandTokens(args)[0] === "connect"
|
|
169
312
|
|| hasLaunchScopedFlagToken(args, "--cdp")
|
|
170
313
|
|| hasLaunchScopedFlagToken(args, "--auto-connect")
|
|
171
314
|
|| env.AGENT_BROWSER_CDP !== undefined
|
|
@@ -184,15 +327,32 @@ function restoreAttachedSessionKeysFromBranch(branch) {
|
|
|
184
327
|
continue;
|
|
185
328
|
const managedSessionOutcome = isRecord(details.managedSessionOutcome) ? details.managedSessionOutcome : undefined;
|
|
186
329
|
const retainedFailedAttachment = details.attachedBrowserSession === true && managedSessionOutcome?.activeAfter === true;
|
|
187
|
-
|
|
188
|
-
|
|
330
|
+
const succeeded = getSuccessfulToolResult(details, message);
|
|
331
|
+
const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
|
|
332
|
+
const terminalBatchClose = batchCloseLifecycle?.endsClosed === true;
|
|
189
333
|
const args = getToolResultArgs(details);
|
|
334
|
+
const namespace = typeof details.namespace === "string" ? details.namespace : extractExplicitNamespace(args);
|
|
190
335
|
const sessionName = typeof details.sessionName === "string" ? details.sessionName : extractExplicitSessionName(args);
|
|
336
|
+
const electron = isRecord(details.electron) ? details.electron : undefined;
|
|
337
|
+
const cleanup = isRecord(electron?.cleanup) ? electron.cleanup : undefined;
|
|
338
|
+
for (const cleanupResult of Array.isArray(cleanup?.results) ? cleanup.results : []) {
|
|
339
|
+
for (const identity of getCleanupResultClosedManagedSessionIdentities(cleanupResult, namespace)) {
|
|
340
|
+
attachedSessionKeys.delete(getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (detailsReportCloseAllApplied(details, succeeded)) {
|
|
344
|
+
deleteIdentityKeysInNamespace(attachedSessionKeys, namespace);
|
|
345
|
+
if (sessionName && details.attachedBrowserSession === true && batchCloseLifecycle?.endsClosed === false) {
|
|
346
|
+
attachedSessionKeys.add(getSessionContextKey(sessionName, namespace) ?? sessionName);
|
|
347
|
+
}
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (!succeeded && !retainedFailedAttachment && !terminalBatchClose)
|
|
351
|
+
continue;
|
|
191
352
|
if (!sessionName)
|
|
192
353
|
continue;
|
|
193
|
-
const namespace = typeof details.namespace === "string" ? details.namespace : extractExplicitNamespace(args);
|
|
194
354
|
const sessionKey = getSessionContextKey(sessionName, namespace) ?? sessionName;
|
|
195
|
-
if (isCloseCommand(
|
|
355
|
+
if ((succeeded && isCloseCommand(extractUpstreamCommandTokens(args)[0])) || terminalBatchClose)
|
|
196
356
|
attachedSessionKeys.delete(sessionKey);
|
|
197
357
|
else if (details.attachedBrowserSession === true || isAttachedBrowserInvocation(args, {}))
|
|
198
358
|
attachedSessionKeys.add(sessionKey);
|
|
@@ -211,12 +371,15 @@ function restoreAllowedDomainsBySessionFromBranch(branch) {
|
|
|
211
371
|
if (!details)
|
|
212
372
|
continue;
|
|
213
373
|
const succeeded = getSuccessfulToolResult(details, message);
|
|
374
|
+
const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
|
|
214
375
|
const args = getToolResultArgs(details);
|
|
215
|
-
const command = typeof details.command === "string" ? details.command :
|
|
376
|
+
const command = typeof details.command === "string" ? details.command : extractUpstreamCommandTokens(args)[0];
|
|
216
377
|
const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
|
|
217
378
|
const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
|
|
218
379
|
const sessionKey = getSessionContextKey(sessionName, namespace);
|
|
219
380
|
const explicitSessionName = extractExplicitSessionName(args);
|
|
381
|
+
if (detailsReportCloseAllApplied(details, succeeded))
|
|
382
|
+
deleteIdentityKeysInNamespace(restoredPolicies, namespace);
|
|
220
383
|
const outcome = getManagedSessionOutcome(details);
|
|
221
384
|
const outcomeSucceeded = outcome?.succeeded === true;
|
|
222
385
|
const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
|
|
@@ -233,7 +396,7 @@ function restoreAllowedDomainsBySessionFromBranch(branch) {
|
|
|
233
396
|
if (replacedSessionName)
|
|
234
397
|
restoredPolicies.delete(getSessionContextKey(replacedSessionName, replacedSessionNamespace) ?? replacedSessionName);
|
|
235
398
|
}
|
|
236
|
-
if (succeeded && isCloseCommand(command)) {
|
|
399
|
+
if ((succeeded && isCloseCommand(command)) || batchCloseLifecycle) {
|
|
237
400
|
const closedSessionName = explicitSessionName ?? sessionName ?? outcomeAttemptedSessionName ?? outcomeCurrentSessionName;
|
|
238
401
|
if (closedSessionName)
|
|
239
402
|
restoredPolicies.delete(getSessionContextKey(closedSessionName, namespace) ?? closedSessionName);
|
|
@@ -242,13 +405,14 @@ function restoreAllowedDomainsBySessionFromBranch(branch) {
|
|
|
242
405
|
const cleanup = isRecord(electron?.cleanup) ? electron.cleanup : undefined;
|
|
243
406
|
const cleanupResults = Array.isArray(cleanup?.results) ? cleanup.results : [];
|
|
244
407
|
for (const cleanupResult of cleanupResults) {
|
|
245
|
-
for (const
|
|
246
|
-
restoredPolicies.delete(
|
|
408
|
+
for (const identity of getCleanupResultClosedManagedSessionIdentities(cleanupResult, namespace)) {
|
|
409
|
+
restoredPolicies.delete(getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName);
|
|
410
|
+
}
|
|
247
411
|
}
|
|
248
412
|
const outcomeKeepsSessionCurrent = outcome?.activeAfter === true
|
|
249
413
|
&& (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged")
|
|
250
414
|
&& outcomeCurrentSessionName === sessionName;
|
|
251
|
-
const policy = (succeeded || outcomeKeepsSessionCurrent) && sessionKey && !isCloseCommand(command) ? parseAllowedDomainsPolicyFromArgs(args) : undefined;
|
|
415
|
+
const policy = (succeeded || outcomeKeepsSessionCurrent) && sessionKey && !isCloseCommand(command) && batchCloseLifecycle?.endsClosed !== true ? parseAllowedDomainsPolicyFromArgs(args) : undefined;
|
|
252
416
|
if (policy && sessionKey)
|
|
253
417
|
restoredPolicies.set(sessionKey, policy);
|
|
254
418
|
}
|
|
@@ -260,9 +424,12 @@ function trackOwnedManagedSession(sessions, sessionName, cwd, options = {}) {
|
|
|
260
424
|
const key = getSessionContextKey(sessionName, options.namespace) ?? sessionName;
|
|
261
425
|
const existing = sessions.get(key);
|
|
262
426
|
const branchOwned = existing && !existing.branchOwned ? false : options.branchOwned === true;
|
|
427
|
+
const compatibilityWorkaround = Object.hasOwn(options, "compatibilityWorkaround")
|
|
428
|
+
? options.compatibilityWorkaround
|
|
429
|
+
: existing?.compatibilityWorkaround;
|
|
263
430
|
const headedManagedAutosaveDisabled = options.headedManagedAutosaveDisabled ?? existing?.headedManagedAutosaveDisabled;
|
|
264
431
|
const headedManagedAutosaveInterval = options.headedManagedAutosaveInterval ?? existing?.headedManagedAutosaveInterval;
|
|
265
|
-
sessions.set(key, { branchOwned, cwd, headedManagedAutosaveDisabled, headedManagedAutosaveInterval, namespace: options.namespace, sessionName });
|
|
432
|
+
sessions.set(key, { branchOwned, compatibilityWorkaround, cwd, headedManagedAutosaveDisabled, headedManagedAutosaveInterval, namespace: options.namespace, sessionName });
|
|
266
433
|
}
|
|
267
434
|
function untrackOwnedManagedSession(sessions, sessionName, namespace) {
|
|
268
435
|
if (!sessionName)
|
|
@@ -291,20 +458,21 @@ function syncOwnedManagedSessionsFromResult(sessions, result, cwd) {
|
|
|
291
458
|
const status = typeof outcome.status === "string" ? outcome.status : undefined;
|
|
292
459
|
const currentSessionName = typeof outcome.currentSessionName === "string" ? outcome.currentSessionName : undefined;
|
|
293
460
|
const attemptedSessionName = typeof outcome.attemptedSessionName === "string" ? outcome.attemptedSessionName : undefined;
|
|
461
|
+
const namespace = isRecord(details) && typeof details.namespace === "string" ? details.namespace : undefined;
|
|
294
462
|
if (outcome.activeAfter === true && (status === "created" || status === "replaced" || status === "unchanged")) {
|
|
295
|
-
const namespace = isRecord(details) && typeof details.namespace === "string" ? details.namespace : undefined;
|
|
296
463
|
trackOwnedManagedSession(sessions, currentSessionName, cwd, {
|
|
464
|
+
compatibilityWorkaround: getRecognizedCompatibilityWorkaround(details?.compatibilityWorkaround),
|
|
297
465
|
headedManagedAutosaveDisabled: details?.managedSessionHeadedAutosaveDisabled === true,
|
|
298
466
|
headedManagedAutosaveInterval: typeof details?.managedSessionHeadedAutosaveInterval === "string" ? details.managedSessionHeadedAutosaveInterval : undefined,
|
|
299
467
|
namespace,
|
|
300
468
|
});
|
|
301
469
|
}
|
|
302
470
|
if (succeeded && status === "closed") {
|
|
303
|
-
untrackOwnedManagedSession(sessions, attemptedSessionName ?? currentSessionName);
|
|
471
|
+
untrackOwnedManagedSession(sessions, attemptedSessionName ?? currentSessionName, namespace);
|
|
304
472
|
}
|
|
305
473
|
}
|
|
306
|
-
function getTouchedElectronLaunchIds(sessionName, records) {
|
|
307
|
-
const record = findElectronLaunchRecordForSession(sessionName, records);
|
|
474
|
+
function getTouchedElectronLaunchIds(sessionName, records, namespace) {
|
|
475
|
+
const record = findElectronLaunchRecordForSession(sessionName, records, namespace);
|
|
308
476
|
return record ? new Set([record.launchId]) : undefined;
|
|
309
477
|
}
|
|
310
478
|
function mergeActiveElectronLaunchRecords(target, source, options = {}) {
|
|
@@ -372,10 +540,10 @@ function getElectronHostLaunchRecordsForInput(options) {
|
|
|
372
540
|
}
|
|
373
541
|
return options.branchRecords;
|
|
374
542
|
}
|
|
375
|
-
function
|
|
543
|
+
function getCleanupResultClosedManagedSessionIdentities(result, fallbackNamespace) {
|
|
376
544
|
if (!isRecord(result) || !Array.isArray(result.steps))
|
|
377
545
|
return [];
|
|
378
|
-
const
|
|
546
|
+
const identities = new Map();
|
|
379
547
|
const record = isRecord(result.record) ? result.record : undefined;
|
|
380
548
|
for (const step of result.steps) {
|
|
381
549
|
if (!isRecord(step) || step.resource !== "managed-session")
|
|
@@ -385,24 +553,29 @@ function getCleanupResultClosedManagedSessionNames(result) {
|
|
|
385
553
|
const sessionName = typeof step.sessionName === "string"
|
|
386
554
|
? step.sessionName
|
|
387
555
|
: typeof record?.sessionName === "string" ? record.sessionName : undefined;
|
|
556
|
+
const namespace = typeof step.namespace === "string"
|
|
557
|
+
? step.namespace
|
|
558
|
+
: typeof record?.namespace === "string" ? record.namespace : fallbackNamespace;
|
|
388
559
|
if (sessionName)
|
|
389
|
-
|
|
560
|
+
identities.set(getSessionContextKey(sessionName, namespace) ?? sessionName, { namespace, sessionName });
|
|
390
561
|
}
|
|
391
|
-
return [...
|
|
562
|
+
return [...identities.values()];
|
|
392
563
|
}
|
|
393
|
-
function
|
|
394
|
-
const
|
|
564
|
+
function getCleanupResultsClosedManagedSessionIdentities(cleanupResults, fallbackNamespace) {
|
|
565
|
+
const identities = new Map();
|
|
395
566
|
for (const result of cleanupResults) {
|
|
396
|
-
for (const
|
|
397
|
-
|
|
567
|
+
for (const identity of getCleanupResultClosedManagedSessionIdentities(result, fallbackNamespace)) {
|
|
568
|
+
identities.set(getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName, identity);
|
|
569
|
+
}
|
|
398
570
|
}
|
|
399
|
-
return [...
|
|
571
|
+
return [...identities.values()];
|
|
400
572
|
}
|
|
401
573
|
function isElectronLaunchRecord(value) {
|
|
402
574
|
if (!isRecord(value))
|
|
403
575
|
return false;
|
|
404
576
|
return value.version === 1
|
|
405
577
|
&& value.launchedByWrapper === true
|
|
578
|
+
&& (value.namespace === undefined || typeof value.namespace === "string")
|
|
406
579
|
&& typeof value.launchId === "string"
|
|
407
580
|
&& typeof value.appName === "string"
|
|
408
581
|
&& typeof value.executablePath === "string"
|
|
@@ -460,18 +633,20 @@ function collectBranchManagedResourceEvents(branch) {
|
|
|
460
633
|
eventRank += 1;
|
|
461
634
|
const succeeded = getSuccessfulToolResult(details, message);
|
|
462
635
|
const args = Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string") ? details.args : [];
|
|
463
|
-
const command = typeof details.command === "string" ? details.command :
|
|
636
|
+
const command = typeof details.command === "string" ? details.command : extractUpstreamCommandTokens(args)[0];
|
|
464
637
|
const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
|
|
465
638
|
const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
|
|
466
639
|
const sessionMode = details.sessionMode === "fresh" || details.sessionMode === "auto" ? details.sessionMode : undefined;
|
|
467
640
|
const usedImplicitSession = details.usedImplicitSession === true;
|
|
468
641
|
const explicitSessionName = extractExplicitSessionName(args);
|
|
642
|
+
const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
|
|
643
|
+
const closeAllApplied = detailsReportCloseAllApplied(details, succeeded);
|
|
469
644
|
const outcome = getManagedSessionOutcome(details);
|
|
470
645
|
const outcomeSucceeded = outcome?.succeeded === true;
|
|
471
646
|
const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
|
|
472
647
|
const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
|
|
473
648
|
const outcomeAttemptedSessionName = typeof outcome?.attemptedSessionName === "string" ? outcome.attemptedSessionName : undefined;
|
|
474
|
-
if (
|
|
649
|
+
if (outcome?.activeAfter === true && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged")) {
|
|
475
650
|
setBranchManagedSessionActive(events, outcomeCurrentSessionName, namespace, eventRank);
|
|
476
651
|
}
|
|
477
652
|
if (outcomeSucceeded && outcomeStatus === "closed") {
|
|
@@ -487,6 +662,14 @@ function collectBranchManagedResourceEvents(branch) {
|
|
|
487
662
|
if (succeeded && isCloseCommand(command)) {
|
|
488
663
|
setBranchRankForString(events.managedSessionCloseRanks, getSessionContextKey(explicitSessionName ?? sessionName ?? outcomeAttemptedSessionName ?? outcomeCurrentSessionName, namespace), eventRank);
|
|
489
664
|
}
|
|
665
|
+
if (closeAllApplied) {
|
|
666
|
+
const retainedSessionKey = batchCloseLifecycle?.endsClosed === false ? getSessionContextKey(sessionName, namespace) : undefined;
|
|
667
|
+
for (const sessionKey of events.managedSessionActiveIdentities.keys()) {
|
|
668
|
+
if (sessionKey !== retainedSessionKey && isAgentBrowserSessionIdentityKeyInNamespace(sessionKey, namespace)) {
|
|
669
|
+
events.managedSessionCloseRanks.set(sessionKey, eventRank);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
490
673
|
const electron = isRecord(details.electron) ? details.electron : undefined;
|
|
491
674
|
const launch = electron && isElectronLaunchRecord(electron.launch) ? electron.launch : undefined;
|
|
492
675
|
if (launch && getActiveElectronRecords(new Map([[launch.launchId, launch]])).length > 0) {
|
|
@@ -503,8 +686,8 @@ function collectBranchManagedResourceEvents(branch) {
|
|
|
503
686
|
if (isRecord(cleanupResult) && isElectronLaunchRecord(cleanupResult.record)) {
|
|
504
687
|
events.electronLaunchCleanupRanks.set(cleanupResult.record.launchId, eventRank);
|
|
505
688
|
}
|
|
506
|
-
for (const
|
|
507
|
-
events.managedSessionCloseRanks.set(
|
|
689
|
+
for (const identity of getCleanupResultClosedManagedSessionIdentities(cleanupResult, namespace)) {
|
|
690
|
+
events.managedSessionCloseRanks.set(getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName, eventRank);
|
|
508
691
|
}
|
|
509
692
|
}
|
|
510
693
|
}
|
|
@@ -523,23 +706,25 @@ function getCleanupResultsPreservedUserDataDirs(cleanupResults) {
|
|
|
523
706
|
}
|
|
524
707
|
return [...userDataDirs];
|
|
525
708
|
}
|
|
526
|
-
function syncElectronCleanupManagedSessions(sessions, cleanupResults) {
|
|
527
|
-
for (const
|
|
528
|
-
untrackOwnedManagedSession(sessions, sessionName);
|
|
709
|
+
function syncElectronCleanupManagedSessions(sessions, cleanupResults, fallbackNamespace) {
|
|
710
|
+
for (const identity of getCleanupResultsClosedManagedSessionIdentities(cleanupResults, fallbackNamespace)) {
|
|
711
|
+
untrackOwnedManagedSession(sessions, identity.sessionName, identity.namespace);
|
|
529
712
|
}
|
|
530
713
|
}
|
|
531
|
-
async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessionName, timeoutMs, attachedSessionKeys, keepNamespace) {
|
|
714
|
+
async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessionName, timeoutMs, attachedSessionKeys, keepNamespace, onClosed) {
|
|
532
715
|
const keepKey = getSessionContextKey(keepSessionName, keepNamespace);
|
|
533
716
|
for (const [key, owner] of [...sessions]) {
|
|
534
717
|
if (key === keepKey)
|
|
535
718
|
continue;
|
|
536
719
|
const error = await closeManagedSession({ cwd: owner.cwd, headedManagedAutosaveInterval: owner.headedManagedAutosaveInterval, namespace: owner.namespace, preserveAttachedBrowserSession: attachedSessionKeys.has(key), restoreState, sessionName: owner.sessionName, timeoutMs });
|
|
537
|
-
if (!error)
|
|
720
|
+
if (!error) {
|
|
538
721
|
sessions.delete(key);
|
|
722
|
+
onClosed?.(owner);
|
|
723
|
+
}
|
|
539
724
|
}
|
|
540
725
|
}
|
|
541
|
-
async function closeOwnedManagedSessions(sessions, restoreState, timeoutMs, attachedSessionKeys) {
|
|
542
|
-
await closeOwnedManagedSessionsExcept(sessions, restoreState, undefined, timeoutMs, attachedSessionKeys);
|
|
726
|
+
async function closeOwnedManagedSessions(sessions, restoreState, timeoutMs, attachedSessionKeys, onClosed) {
|
|
727
|
+
await closeOwnedManagedSessionsExcept(sessions, restoreState, undefined, timeoutMs, attachedSessionKeys, undefined, onClosed);
|
|
543
728
|
}
|
|
544
729
|
function getOffBranchOwnedElectronLaunchRecords(ownedRecords, branchRecords) {
|
|
545
730
|
const activeBranchLaunchIds = new Set(getActiveElectronRecords(branchRecords).map((record) => record.launchId));
|
|
@@ -579,14 +764,19 @@ class AsyncExecutionQueue {
|
|
|
579
764
|
})();
|
|
580
765
|
}
|
|
581
766
|
}
|
|
582
|
-
class KeyedAsyncExecutionQueue {
|
|
767
|
+
export class KeyedAsyncExecutionQueue {
|
|
768
|
+
barriers = new Map();
|
|
583
769
|
entries = new Map();
|
|
584
|
-
async run(key, work) {
|
|
770
|
+
async run(key, namespace, work) {
|
|
585
771
|
const entry = this.entries.get(key) ?? { queue: new AsyncExecutionQueue(), users: 0 };
|
|
772
|
+
const barrier = this.barriers.get(getAgentBrowserSessionIdentityKey("", namespace)) ?? Promise.resolve();
|
|
586
773
|
entry.users += 1;
|
|
587
774
|
this.entries.set(key, entry);
|
|
588
775
|
try {
|
|
589
|
-
return await entry.queue.run(
|
|
776
|
+
return await entry.queue.run(async () => {
|
|
777
|
+
await barrier;
|
|
778
|
+
return await work();
|
|
779
|
+
});
|
|
590
780
|
}
|
|
591
781
|
finally {
|
|
592
782
|
entry.users -= 1;
|
|
@@ -594,6 +784,29 @@ class KeyedAsyncExecutionQueue {
|
|
|
594
784
|
this.entries.delete(key);
|
|
595
785
|
}
|
|
596
786
|
}
|
|
787
|
+
async runExclusive(namespace, work) {
|
|
788
|
+
const namespaceKey = getAgentBrowserSessionIdentityKey("", namespace);
|
|
789
|
+
const previous = this.barriers.get(namespaceKey) ?? Promise.resolve();
|
|
790
|
+
let release;
|
|
791
|
+
const blocked = new Promise((resolve) => {
|
|
792
|
+
release = resolve;
|
|
793
|
+
});
|
|
794
|
+
const barrier = previous.then(() => blocked);
|
|
795
|
+
this.barriers.set(namespaceKey, barrier);
|
|
796
|
+
const drains = [...this.entries]
|
|
797
|
+
.filter(([key]) => isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
|
|
798
|
+
.map(([, { queue }]) => queue.run(async () => undefined));
|
|
799
|
+
await previous;
|
|
800
|
+
await Promise.all(drains);
|
|
801
|
+
try {
|
|
802
|
+
return await work();
|
|
803
|
+
}
|
|
804
|
+
finally {
|
|
805
|
+
release();
|
|
806
|
+
if (this.barriers.get(namespaceKey) === barrier)
|
|
807
|
+
this.barriers.delete(namespaceKey);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
597
810
|
}
|
|
598
811
|
function mergeBrowserRunMap(current, initial, updated) {
|
|
599
812
|
if (updated === initial)
|
|
@@ -609,11 +822,19 @@ function mergeBrowserRunMap(current, initial, updated) {
|
|
|
609
822
|
}
|
|
610
823
|
return merged;
|
|
611
824
|
}
|
|
612
|
-
function mergeBrowserRunArtifactManifest(current, initial, updated) {
|
|
825
|
+
export function mergeBrowserRunArtifactManifest(current, initial, updated) {
|
|
613
826
|
if (!updated || updated === initial)
|
|
614
827
|
return current;
|
|
828
|
+
if (current === initial)
|
|
829
|
+
return updated;
|
|
615
830
|
const initialEntries = new Map((initial?.entries ?? []).map((entry) => [getSessionArtifactManifestEntryKey(entry), entry]));
|
|
616
|
-
const changedEntries = updated.entries
|
|
831
|
+
const changedEntries = updated.entries
|
|
832
|
+
.map((entry, index) => ({ entry, index }))
|
|
833
|
+
.filter(({ entry }) => initialEntries.get(getSessionArtifactManifestEntryKey(entry)) !== entry)
|
|
834
|
+
.sort((left, right) => left.entry.createdAtMs - right.entry.createdAtMs
|
|
835
|
+
|| Number(isPendingRecordingCommand(left.entry.command, left.entry.subcommand, left.entry.kind)) - Number(isPendingRecordingCommand(right.entry.command, right.entry.subcommand, right.entry.kind))
|
|
836
|
+
|| left.index - right.index)
|
|
837
|
+
.map(({ entry }) => entry);
|
|
617
838
|
return changedEntries.length === 0
|
|
618
839
|
? current
|
|
619
840
|
: mergeSessionArtifactManifest({
|
|
@@ -681,6 +902,9 @@ export default function agentBrowserExtension(pi) {
|
|
|
681
902
|
let sessionPageState = new SessionPageState();
|
|
682
903
|
let traceOwners = new Map();
|
|
683
904
|
let artifactManifest;
|
|
905
|
+
let activeRecordingReservations = new Map();
|
|
906
|
+
let recordingSessionTombstones = new Map();
|
|
907
|
+
let recordingSessionTombstonesToPersist = new Map();
|
|
684
908
|
let allowedDomainsBySession = new Map();
|
|
685
909
|
let attachedSessionKeys = new Set();
|
|
686
910
|
let networkRoutesBySession = new Map();
|
|
@@ -691,9 +915,160 @@ export default function agentBrowserExtension(pi) {
|
|
|
691
915
|
const managedSessionRestoreState = new ManagedSessionRestoreState();
|
|
692
916
|
const ownedManagedSessions = new Map();
|
|
693
917
|
const managedSessionExecutionQueue = new AsyncExecutionQueue();
|
|
918
|
+
const artifactExecutionQueue = new AsyncExecutionQueue();
|
|
694
919
|
const callerOwnedSessionExecutionQueues = new KeyedAsyncExecutionQueue();
|
|
920
|
+
const activeScriptControllers = new Set();
|
|
921
|
+
const activeScriptExecutions = new Set();
|
|
695
922
|
let branchRestoreGeneration = 0;
|
|
696
923
|
let branchStateGeneration = 0;
|
|
924
|
+
const validatedUpstreamPathKeys = new Set();
|
|
925
|
+
const appendRecordingTransitions = (transitions) => {
|
|
926
|
+
for (const transition of transitions) {
|
|
927
|
+
const key = getAgentBrowserSessionIdentityKey(transition.reservation.sessionName, transition.reservation.namespace);
|
|
928
|
+
if (transition.state === "active") {
|
|
929
|
+
recordingSessionTombstones.delete(key);
|
|
930
|
+
recordingSessionTombstonesToPersist.delete(key);
|
|
931
|
+
}
|
|
932
|
+
else {
|
|
933
|
+
recordingSessionTombstones.set(key, transition.reservation);
|
|
934
|
+
}
|
|
935
|
+
try {
|
|
936
|
+
appendRecordingReservationTransition(pi, transition);
|
|
937
|
+
recordingSessionTombstonesToPersist.delete(key);
|
|
938
|
+
}
|
|
939
|
+
catch {
|
|
940
|
+
if (transition.state === "closed")
|
|
941
|
+
recordingSessionTombstonesToPersist.set(key, transition.reservation);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
const appendActiveRecordingCleanupAction = (result, reservation) => {
|
|
946
|
+
if (result.isError !== true)
|
|
947
|
+
return result;
|
|
948
|
+
const details = isRecord(result.details) ? result.details : {};
|
|
949
|
+
const nextActions = Array.isArray(details.nextActions) ? [...details.nextActions] : [];
|
|
950
|
+
if (nextActions.some((action) => action.id === "stop-pending-recording"))
|
|
951
|
+
return result;
|
|
952
|
+
const stopActions = applyNamespaceToNextActions(applySessionToNextActions([
|
|
953
|
+
buildNextToolAction({
|
|
954
|
+
args: ["record", "stop"],
|
|
955
|
+
id: "stop-pending-recording",
|
|
956
|
+
reason: "Stop the active recording so the requested video can be finalized and verified on disk.",
|
|
957
|
+
safety: "The file remains pending until record stop succeeds; verify details.artifactVerification afterward.",
|
|
958
|
+
}),
|
|
959
|
+
], reservation.sessionName), reservation.namespace);
|
|
960
|
+
appendUniqueAgentBrowserNextActions(nextActions, stopActions);
|
|
961
|
+
const cleanupNotice = "An active recording remains open. Use the exact stop-pending-recording payload in details.nextActions before leaving this session.";
|
|
962
|
+
let noticeAppended = false;
|
|
963
|
+
const content = result.content.map((item) => {
|
|
964
|
+
if (noticeAppended || item.type !== "text")
|
|
965
|
+
return item;
|
|
966
|
+
noticeAppended = true;
|
|
967
|
+
return { ...item, text: `${item.text}\n\n${cleanupNotice}` };
|
|
968
|
+
});
|
|
969
|
+
if (!noticeAppended)
|
|
970
|
+
content.push({ type: "text", text: cleanupNotice });
|
|
971
|
+
return { ...result, content, details: { ...details, nextActions } };
|
|
972
|
+
};
|
|
973
|
+
const retireRecordingSession = (sessionName, namespace, retireManifest = true) => {
|
|
974
|
+
const reservation = retireRecordingReservation(activeRecordingReservations, sessionName, namespace);
|
|
975
|
+
const previousManifest = artifactManifest;
|
|
976
|
+
if (retireManifest && artifactManifest)
|
|
977
|
+
artifactManifest = retirePendingRecordingManifestEntries(artifactManifest, sessionName, namespace);
|
|
978
|
+
if (!reservation && artifactManifest === previousManifest)
|
|
979
|
+
return;
|
|
980
|
+
const terminalReservation = reservation ?? { absolutePath: "", cwd: managedSessionCwd, namespace, path: "", sessionName };
|
|
981
|
+
const terminalKey = getAgentBrowserSessionIdentityKey(sessionName, namespace);
|
|
982
|
+
recordingSessionTombstones.set(terminalKey, terminalReservation);
|
|
983
|
+
try {
|
|
984
|
+
appendRecordingReservationTransition(pi, {
|
|
985
|
+
reservation: terminalReservation,
|
|
986
|
+
state: "closed",
|
|
987
|
+
});
|
|
988
|
+
recordingSessionTombstonesToPersist.delete(terminalKey);
|
|
989
|
+
}
|
|
990
|
+
catch {
|
|
991
|
+
recordingSessionTombstonesToPersist.set(terminalKey, terminalReservation);
|
|
992
|
+
}
|
|
993
|
+
};
|
|
994
|
+
const syncRecordingReservationsFromResult = (result) => {
|
|
995
|
+
const handledClosedSessionKeys = new Set();
|
|
996
|
+
const details = isRecord(result.details) ? result.details : undefined;
|
|
997
|
+
const batchSteps = Array.isArray(details?.batchSteps) ? details.batchSteps : undefined;
|
|
998
|
+
const resultSessionName = typeof details?.sessionName === "string" ? details.sessionName : undefined;
|
|
999
|
+
const resultNamespace = typeof details?.namespace === "string" ? details.namespace : undefined;
|
|
1000
|
+
if (!batchSteps) {
|
|
1001
|
+
appendRecordingTransitions(applyRecordingArtifactsToReservations(activeRecordingReservations, getResultFileArtifacts(result)));
|
|
1002
|
+
return handledClosedSessionKeys;
|
|
1003
|
+
}
|
|
1004
|
+
let sessionClosed = false;
|
|
1005
|
+
for (const step of batchSteps) {
|
|
1006
|
+
if (!isRecord(step))
|
|
1007
|
+
continue;
|
|
1008
|
+
const command = Array.isArray(step.command) && step.command.every((token) => typeof token === "string") ? step.command : undefined;
|
|
1009
|
+
const commandTokens = command ? extractUpstreamCommandTokens(command) : [];
|
|
1010
|
+
const commandName = commandTokens[0];
|
|
1011
|
+
if (resultSessionName && batchStepReportsNoRecordingInProgress(step)) {
|
|
1012
|
+
const sessionKey = getAgentBrowserSessionIdentityKey(resultSessionName, resultNamespace);
|
|
1013
|
+
retireRecordingSession(resultSessionName, resultNamespace, false);
|
|
1014
|
+
handledClosedSessionKeys.add(sessionKey);
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
if (step.success === true && commandName && isCloseCommand(commandName) && resultSessionName) {
|
|
1018
|
+
const sessionKey = getAgentBrowserSessionIdentityKey(resultSessionName, resultNamespace);
|
|
1019
|
+
retireRecordingSession(resultSessionName, resultNamespace, false);
|
|
1020
|
+
handledClosedSessionKeys.add(sessionKey);
|
|
1021
|
+
sessionClosed = true;
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
if (sessionClosed && commandName === "record")
|
|
1025
|
+
continue;
|
|
1026
|
+
if (step.success === true && sessionClosed)
|
|
1027
|
+
sessionClosed = false;
|
|
1028
|
+
const artifacts = Array.isArray(step.artifacts) ? step.artifacts.filter(isResultFileArtifact) : [];
|
|
1029
|
+
appendRecordingTransitions(applyRecordingArtifactsToReservations(activeRecordingReservations, artifacts));
|
|
1030
|
+
}
|
|
1031
|
+
for (const sessionKey of handledClosedSessionKeys) {
|
|
1032
|
+
if (!activeRecordingReservations.has(sessionKey) && artifactManifest && resultSessionName) {
|
|
1033
|
+
artifactManifest = retirePendingRecordingManifestEntries(artifactManifest, resultSessionName, resultNamespace);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return handledClosedSessionKeys;
|
|
1037
|
+
};
|
|
1038
|
+
const validateUpstreamVersion = async (cwd, signal) => {
|
|
1039
|
+
const processEnvironment = getAgentBrowserProcessEnvironment();
|
|
1040
|
+
const pathKey = `${cwd}\0${processEnvironment.PATH ?? processEnvironment.Path ?? ""}`;
|
|
1041
|
+
if (validatedUpstreamPathKeys.has(pathKey))
|
|
1042
|
+
return undefined;
|
|
1043
|
+
const probe = await runAgentBrowserProcess({ args: ["--version"], cwd, signal, timeoutMs: 5_000 });
|
|
1044
|
+
if (probe.spawnError?.code === "ENOENT" || probe.exitCode === 127 || probe.aborted)
|
|
1045
|
+
return undefined;
|
|
1046
|
+
let error;
|
|
1047
|
+
let observedVersion;
|
|
1048
|
+
if (probe.spawnError || probe.exitCode !== 0) {
|
|
1049
|
+
const detail = redactSensitiveText(probe.spawnError?.message ?? (probe.stderr.trim() || `exit ${probe.exitCode}`));
|
|
1050
|
+
error = `agent-browser --version could not be validated (${detail}). Run pi-agent-browser-doctor before browser-backed calls.`;
|
|
1051
|
+
}
|
|
1052
|
+
else {
|
|
1053
|
+
observedVersion = parseAgentBrowserVersionOutput(probe.stdout);
|
|
1054
|
+
error = getAgentBrowserVersionValidationError(probe.stdout);
|
|
1055
|
+
}
|
|
1056
|
+
if (!error) {
|
|
1057
|
+
validatedUpstreamPathKeys.add(pathKey);
|
|
1058
|
+
return undefined;
|
|
1059
|
+
}
|
|
1060
|
+
return {
|
|
1061
|
+
content: [{ type: "text", text: error }],
|
|
1062
|
+
details: {
|
|
1063
|
+
expectedVersion: TARGET_AGENT_BROWSER_VERSION,
|
|
1064
|
+
failureCategory: "validation-error",
|
|
1065
|
+
observedVersion,
|
|
1066
|
+
resultCategory: "failure",
|
|
1067
|
+
versionValidation: { expected: TARGET_AGENT_BROWSER_VERSION_LABEL, observed: observedVersion },
|
|
1068
|
+
},
|
|
1069
|
+
isError: true,
|
|
1070
|
+
};
|
|
1071
|
+
};
|
|
697
1072
|
const clearSessionScopedBrowserState = (sessionName, namespace) => {
|
|
698
1073
|
const key = getSessionContextKey(sessionName, namespace) ?? sessionName;
|
|
699
1074
|
allowedDomainsBySession = new Map(allowedDomainsBySession);
|
|
@@ -701,8 +1076,51 @@ export default function agentBrowserExtension(pi) {
|
|
|
701
1076
|
attachedSessionKeys.delete(key);
|
|
702
1077
|
networkRoutesBySession = new Map(networkRoutesBySession);
|
|
703
1078
|
networkRoutesBySession.delete(key);
|
|
1079
|
+
traceOwners.delete(key);
|
|
704
1080
|
sessionPageState.clearSession(key);
|
|
705
1081
|
};
|
|
1082
|
+
const closeScriptSessionLeaseWithinQueue = async (sessionName, cwd) => {
|
|
1083
|
+
const closeError = await withIsolatedAgentBrowserEnvironment(() => closeManagedSession({
|
|
1084
|
+
cwd,
|
|
1085
|
+
namespace: AGENT_BROWSER_SCRIPT_NAMESPACE,
|
|
1086
|
+
restoreState: managedSessionRestoreState,
|
|
1087
|
+
sessionName,
|
|
1088
|
+
timeoutMs: implicitSessionCloseTimeoutMs,
|
|
1089
|
+
}));
|
|
1090
|
+
if (closeError) {
|
|
1091
|
+
try {
|
|
1092
|
+
appendScriptSessionLease(pi, sessionName, "failed");
|
|
1093
|
+
}
|
|
1094
|
+
catch { }
|
|
1095
|
+
return redactSensitiveText(closeError);
|
|
1096
|
+
}
|
|
1097
|
+
try {
|
|
1098
|
+
appendScriptSessionLease(pi, sessionName, "closed");
|
|
1099
|
+
}
|
|
1100
|
+
catch {
|
|
1101
|
+
managedSessionRestoreState.disable(sessionName);
|
|
1102
|
+
return "The isolated session closed, but its durable cleanup record could not be saved.";
|
|
1103
|
+
}
|
|
1104
|
+
untrackOwnedManagedSession(ownedManagedSessions, sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
|
|
1105
|
+
managedSessionRestoreState.clear(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
|
|
1106
|
+
retireRecordingSession(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
|
|
1107
|
+
clearSessionScopedBrowserState(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
|
|
1108
|
+
return undefined;
|
|
1109
|
+
};
|
|
1110
|
+
const recoverScriptSessionLeasesWithinQueue = async (ctx) => {
|
|
1111
|
+
const pendingSessionNames = new Set([...ownedManagedSessions.values()]
|
|
1112
|
+
.map((session) => session.sessionName)
|
|
1113
|
+
.filter(isAgentBrowserScriptSessionName));
|
|
1114
|
+
for (const lease of getScriptSessionLeasesFromBranch(ctx.sessionManager.getBranch()).values()) {
|
|
1115
|
+
if (lease.cleanup !== "closed")
|
|
1116
|
+
pendingSessionNames.add(lease.sessionName);
|
|
1117
|
+
}
|
|
1118
|
+
for (const sessionName of pendingSessionNames) {
|
|
1119
|
+
trackOwnedManagedSession(ownedManagedSessions, sessionName, ctx.cwd, { branchOwned: true, namespace: AGENT_BROWSER_SCRIPT_NAMESPACE });
|
|
1120
|
+
managedSessionRestoreState.disable(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
|
|
1121
|
+
await closeScriptSessionLeaseWithinQueue(sessionName, ctx.cwd);
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
706
1124
|
const restoreBranchBackedState = (ctx, options) => {
|
|
707
1125
|
branchRestoreGeneration += 1;
|
|
708
1126
|
branchStateGeneration += 1;
|
|
@@ -750,13 +1168,29 @@ export default function agentBrowserExtension(pi) {
|
|
|
750
1168
|
sessionPageState = SessionPageState.fromBranch(branch);
|
|
751
1169
|
traceOwners = new Map();
|
|
752
1170
|
artifactManifest = restoreArtifactManifestFromBranch(branch);
|
|
1171
|
+
const restoredRecordingState = restoreRecordingReservationStateFromBranch(branch);
|
|
1172
|
+
for (const [key, reservation] of recordingSessionTombstones) {
|
|
1173
|
+
if (restoredRecordingState.terminal.has(key))
|
|
1174
|
+
recordingSessionTombstonesToPersist.delete(key);
|
|
1175
|
+
else
|
|
1176
|
+
recordingSessionTombstonesToPersist.set(key, reservation);
|
|
1177
|
+
}
|
|
1178
|
+
for (const [key, reservation] of restoredRecordingState.terminal) {
|
|
1179
|
+
if (!activeRecordingReservations.has(key))
|
|
1180
|
+
recordingSessionTombstones.set(key, reservation);
|
|
1181
|
+
}
|
|
1182
|
+
for (const key of recordingSessionTombstones.keys())
|
|
1183
|
+
restoredRecordingState.active.delete(key);
|
|
1184
|
+
for (const [key, reservation] of activeRecordingReservations)
|
|
1185
|
+
restoredRecordingState.active.set(key, reservation);
|
|
1186
|
+
activeRecordingReservations = restoredRecordingState.active;
|
|
753
1187
|
allowedDomainsBySession = restoreAllowedDomainsBySessionFromBranch(branch);
|
|
754
1188
|
attachedSessionKeys = restoreAttachedSessionKeysFromBranch(branch);
|
|
755
1189
|
networkRoutesBySession = new Map();
|
|
756
1190
|
electronLaunchRecords = restoreElectronLaunchRecordsFromBranch(branch);
|
|
757
1191
|
for (const record of getActiveElectronRecords(electronLaunchRecords)) {
|
|
758
1192
|
if (record.sessionName)
|
|
759
|
-
attachedSessionKeys.add(getSessionContextKey(record.sessionName) ?? record.sessionName);
|
|
1193
|
+
attachedSessionKeys.add(getSessionContextKey(record.sessionName, record.namespace) ?? record.sessionName);
|
|
760
1194
|
}
|
|
761
1195
|
if (options.resetRuntimeOwnership) {
|
|
762
1196
|
ownedManagedSessions.clear();
|
|
@@ -778,6 +1212,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
778
1212
|
continue;
|
|
779
1213
|
trackOwnedManagedSession(ownedManagedSessions, identity.sessionName, ctx.cwd, {
|
|
780
1214
|
branchOwned: true,
|
|
1215
|
+
compatibilityWorkaround: restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, identity.sessionName, identity.namespace),
|
|
781
1216
|
headedManagedAutosaveDisabled: restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, identity.sessionName, identity.namespace),
|
|
782
1217
|
headedManagedAutosaveInterval: restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, identity.sessionName, identity.namespace),
|
|
783
1218
|
namespace: identity.namespace,
|
|
@@ -786,6 +1221,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
786
1221
|
if (restoredState.active) {
|
|
787
1222
|
trackOwnedManagedSession(ownedManagedSessions, restoredState.sessionName, ctx.cwd, {
|
|
788
1223
|
branchOwned: true,
|
|
1224
|
+
compatibilityWorkaround: managedSessionCompatibilityWorkaround,
|
|
789
1225
|
headedManagedAutosaveDisabled: managedSessionHeadedAutosaveDisabled,
|
|
790
1226
|
headedManagedAutosaveInterval: managedSessionHeadedAutosaveInterval,
|
|
791
1227
|
namespace: restoredState.namespace,
|
|
@@ -841,17 +1277,25 @@ export default function agentBrowserExtension(pi) {
|
|
|
841
1277
|
cwd: ctx.cwd,
|
|
842
1278
|
includeProjectConfig: shouldIncludeProjectConfig(ctx),
|
|
843
1279
|
}));
|
|
1280
|
+
await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(() => recoverScriptSessionLeasesWithinQueue(ctx)));
|
|
844
1281
|
});
|
|
845
1282
|
pi.on("session_tree", async (_event, ctx) => {
|
|
846
|
-
|
|
1283
|
+
for (const controller of activeScriptControllers)
|
|
1284
|
+
controller.abort();
|
|
1285
|
+
await Promise.allSettled([...activeScriptExecutions]);
|
|
1286
|
+
await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(async () => {
|
|
847
1287
|
restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
|
|
848
|
-
|
|
1288
|
+
await recoverScriptSessionLeasesWithinQueue(ctx);
|
|
1289
|
+
}));
|
|
849
1290
|
});
|
|
850
1291
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
1292
|
+
for (const controller of activeScriptControllers)
|
|
1293
|
+
controller.abort();
|
|
1294
|
+
await Promise.allSettled([...activeScriptExecutions]);
|
|
851
1295
|
branchRestoreGeneration += 1;
|
|
852
1296
|
branchStateGeneration += 1;
|
|
853
1297
|
let preservedElectronProfileDirs = [];
|
|
854
|
-
await managedSessionExecutionQueue.run(async () => {
|
|
1298
|
+
await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(async () => {
|
|
855
1299
|
const shutdownCwd = ctx?.cwd ?? managedSessionCwd;
|
|
856
1300
|
const quitting = event?.reason === "quit";
|
|
857
1301
|
preservedElectronProfileDirs = quitting
|
|
@@ -874,21 +1318,38 @@ export default function agentBrowserExtension(pi) {
|
|
|
874
1318
|
...getCleanupResultsPreservedUserDataDirs(electronCleanupResults),
|
|
875
1319
|
])];
|
|
876
1320
|
syncElectronCleanupManagedSessions(ownedManagedSessions, electronCleanupResults);
|
|
1321
|
+
for (const identity of getCleanupResultsClosedManagedSessionIdentities(electronCleanupResults))
|
|
1322
|
+
retireRecordingSession(identity.sessionName, identity.namespace);
|
|
877
1323
|
if (quitting) {
|
|
878
|
-
await closeOwnedManagedSessions(ownedManagedSessions, managedSessionRestoreState, implicitSessionCloseTimeoutMs, attachedSessionKeys);
|
|
1324
|
+
await closeOwnedManagedSessions(ownedManagedSessions, managedSessionRestoreState, implicitSessionCloseTimeoutMs, attachedSessionKeys, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
|
|
879
1325
|
}
|
|
880
1326
|
else {
|
|
881
|
-
await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, managedSessionActive ? managedSessionNamespace : undefined);
|
|
1327
|
+
await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, managedSessionActive ? managedSessionNamespace : undefined, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
|
|
882
1328
|
}
|
|
883
|
-
});
|
|
1329
|
+
}));
|
|
884
1330
|
managedSessionActive = false;
|
|
885
1331
|
managedSessionCompatibilityWorkaround = undefined;
|
|
886
1332
|
managedSessionHeadedAutosaveDisabled = false;
|
|
887
1333
|
managedSessionHeadedAutosaveInterval = undefined;
|
|
888
1334
|
managedSessionNamespace = undefined;
|
|
1335
|
+
for (const reservation of recordingSessionTombstonesToPersist.values()) {
|
|
1336
|
+
try {
|
|
1337
|
+
appendRecordingReservationTransition(pi, { reservation, state: "closed" });
|
|
1338
|
+
}
|
|
1339
|
+
catch { }
|
|
1340
|
+
}
|
|
1341
|
+
for (const reservation of activeRecordingReservations.values()) {
|
|
1342
|
+
try {
|
|
1343
|
+
appendRecordingReservationTransition(pi, { reservation, state: "active" });
|
|
1344
|
+
}
|
|
1345
|
+
catch { }
|
|
1346
|
+
}
|
|
889
1347
|
sessionPageState.reset();
|
|
890
1348
|
traceOwners = new Map();
|
|
891
1349
|
artifactManifest = undefined;
|
|
1350
|
+
activeRecordingReservations = new Map();
|
|
1351
|
+
recordingSessionTombstones = new Map();
|
|
1352
|
+
recordingSessionTombstonesToPersist = new Map();
|
|
892
1353
|
allowedDomainsBySession = new Map();
|
|
893
1354
|
attachedSessionKeys = new Set();
|
|
894
1355
|
networkRoutesBySession = new Map();
|
|
@@ -937,16 +1398,16 @@ export default function agentBrowserExtension(pi) {
|
|
|
937
1398
|
}
|
|
938
1399
|
});
|
|
939
1400
|
pi.on("tool_result", async (event) => buildAgentBrowserToolResultPatch(event));
|
|
940
|
-
|
|
1401
|
+
const agentBrowserTool = {
|
|
941
1402
|
name: "agent_browser",
|
|
942
1403
|
label: "Agent Browser",
|
|
943
|
-
description: "Browse and interact with websites using agent-browser. Use this for web research, reading live docs, opening pages, taking snapshots or screenshots, clicking links, filling forms, extracting page content, and authenticated/profile-based browser work. Input choice: default `args` for open → snapshot -i → click/fill @refs; `semanticAction` for stable role/text/label targets; `job` or `qa` for multi-step checks; `electron` only for desktop apps; experimental `sourceLookup` / `networkSourceLookup` for candidates only.",
|
|
1404
|
+
description: "Browse and interact with websites using agent-browser. Use this for web research, reading live docs, opening pages, taking snapshots or screenshots, clicking links, filling forms, extracting page content, and authenticated/profile-based browser work. Input choice: `script` for one-shot JavaScript orchestration; default `args` for open → snapshot -i → click/fill @refs; `semanticAction` for stable role/text/label targets; `job` or `qa` for multi-step checks; `electron` only for desktop apps; experimental `sourceLookup` / `networkSourceLookup` for candidates only.",
|
|
944
1405
|
promptSnippet: "Browse websites, read live docs, click and fill pages, extract browser content, take screenshots, and automate real web workflows.",
|
|
945
1406
|
promptGuidelines: toolPromptGuidelines,
|
|
946
1407
|
parameters: AGENT_BROWSER_PARAMS,
|
|
947
1408
|
renderCall(args, theme, context) {
|
|
948
1409
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
949
|
-
text.setText(formatAgentBrowserRenderCall(args, theme));
|
|
1410
|
+
text.setText(formatAgentBrowserRenderCall(args, theme, context.expanded));
|
|
950
1411
|
return text;
|
|
951
1412
|
},
|
|
952
1413
|
renderResult(result, options, theme, context) {
|
|
@@ -956,11 +1417,11 @@ export default function agentBrowserExtension(pi) {
|
|
|
956
1417
|
component.setState(formatAgentBrowserRenderResult(result, options, theme, context.isError), options.expanded, theme);
|
|
957
1418
|
return component;
|
|
958
1419
|
},
|
|
959
|
-
async execute(
|
|
1420
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
960
1421
|
const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
|
|
961
1422
|
const outputPath = isRecord(params) && typeof params.outputPath === "string" ? params.outputPath : undefined;
|
|
962
1423
|
const resolvedInput = resolveAgentBrowserInput({
|
|
963
|
-
getBatchPreflightValidationError,
|
|
1424
|
+
getBatchPreflightValidationError: (args, stdin) => getArtifactPreflightValidationError({ args, cwd: ctx.cwd, outputPath, stdin }),
|
|
964
1425
|
managedSessionActive,
|
|
965
1426
|
params,
|
|
966
1427
|
});
|
|
@@ -971,6 +1432,126 @@ export default function agentBrowserExtension(pi) {
|
|
|
971
1432
|
if (outputPathValidationError) {
|
|
972
1433
|
return buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: outputPathValidationError });
|
|
973
1434
|
}
|
|
1435
|
+
const applyUnserializedOutputPath = async (result, preserveTextContent = false) => {
|
|
1436
|
+
if (!outputPath || result.isError === true || (isRecord(result.details) && result.details.resultCategory === "failure"))
|
|
1437
|
+
return result;
|
|
1438
|
+
return artifactExecutionQueue.run(async () => {
|
|
1439
|
+
const reservationError = getArtifactPreflightValidationError({
|
|
1440
|
+
activeRecordingReservations: activeRecordingReservations.values(),
|
|
1441
|
+
args: [],
|
|
1442
|
+
cwd: ctx.cwd,
|
|
1443
|
+
outputPath,
|
|
1444
|
+
});
|
|
1445
|
+
if (reservationError) {
|
|
1446
|
+
return buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: reservationError });
|
|
1447
|
+
}
|
|
1448
|
+
return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent, result });
|
|
1449
|
+
});
|
|
1450
|
+
};
|
|
1451
|
+
const versionCheckCommand = extractUpstreamCommandTokens(resolvedInput.toolArgs)[0];
|
|
1452
|
+
const electronHostOnlyAction = resolvedInput.kind === "electron" && ["cleanup", "list", "status"].includes(resolvedInput.compiledElectron.action);
|
|
1453
|
+
const browserBackedVersionCheck = needsManagedSession(parseArgvDescriptor(resolvedInput.toolArgs));
|
|
1454
|
+
if (!electronHostOnlyAction && browserBackedVersionCheck && !isPlainTextInspectionArgs(resolvedInput.toolArgs) && !isCloseCommand(versionCheckCommand) && signal?.aborted !== true) {
|
|
1455
|
+
const versionFailure = resolvedInput.kind === "script"
|
|
1456
|
+
? await withIsolatedAgentBrowserEnvironment(() => validateUpstreamVersion(ctx.cwd, signal))
|
|
1457
|
+
: await validateUpstreamVersion(ctx.cwd, signal);
|
|
1458
|
+
if (versionFailure)
|
|
1459
|
+
return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result: versionFailure });
|
|
1460
|
+
}
|
|
1461
|
+
if (resolvedInput.kind === "script") {
|
|
1462
|
+
if (!ctx.sessionManager.getSessionFile()) {
|
|
1463
|
+
return buildValidationFailureResult({
|
|
1464
|
+
attemptedKind: "script",
|
|
1465
|
+
kind: "invalid",
|
|
1466
|
+
redactedArgs: [],
|
|
1467
|
+
status: "invalid",
|
|
1468
|
+
toolArgs: [],
|
|
1469
|
+
validationError: "script requires a persisted Pi session so its isolated browser-session cleanup lease survives restart; relaunch Pi without --no-session.",
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
const sessionName = createAgentBrowserScriptSessionName();
|
|
1473
|
+
const innerResults = [];
|
|
1474
|
+
const scriptTimeoutMs = params.timeoutMs ?? AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS;
|
|
1475
|
+
const deadline = Date.now() + scriptTimeoutMs;
|
|
1476
|
+
let leased = false;
|
|
1477
|
+
let cleanupError;
|
|
1478
|
+
let run = {
|
|
1479
|
+
callCount: 0,
|
|
1480
|
+
emitCount: 0,
|
|
1481
|
+
error: "Script sandbox execution failed.",
|
|
1482
|
+
failureCategory: "upstream-error",
|
|
1483
|
+
ok: false,
|
|
1484
|
+
rejectedCallCount: 0,
|
|
1485
|
+
steps: [],
|
|
1486
|
+
};
|
|
1487
|
+
const scriptController = new AbortController();
|
|
1488
|
+
const abortScript = () => scriptController.abort();
|
|
1489
|
+
signal?.addEventListener("abort", abortScript, { once: true });
|
|
1490
|
+
if (signal?.aborted)
|
|
1491
|
+
scriptController.abort();
|
|
1492
|
+
activeScriptControllers.add(scriptController);
|
|
1493
|
+
let finishScriptExecution;
|
|
1494
|
+
const scriptExecution = new Promise((resolve) => {
|
|
1495
|
+
finishScriptExecution = resolve;
|
|
1496
|
+
});
|
|
1497
|
+
activeScriptExecutions.add(scriptExecution);
|
|
1498
|
+
try {
|
|
1499
|
+
const pendingRun = runAgentBrowserScript({
|
|
1500
|
+
beforeFirstCall() {
|
|
1501
|
+
appendScriptSessionLease(pi, sessionName, "active");
|
|
1502
|
+
trackOwnedManagedSession(ownedManagedSessions, sessionName, ctx.cwd, { namespace: AGENT_BROWSER_SCRIPT_NAMESPACE });
|
|
1503
|
+
managedSessionRestoreState.disable(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
|
|
1504
|
+
leased = true;
|
|
1505
|
+
},
|
|
1506
|
+
code: resolvedInput.compiledScript.code,
|
|
1507
|
+
dispatch: async (innerParams, innerSignal) => {
|
|
1508
|
+
const remainingMs = Math.max(1, deadline - Date.now());
|
|
1509
|
+
const innerTimeoutMs = Math.min(innerParams.timeoutMs ?? remainingMs, remainingMs);
|
|
1510
|
+
const innerResult = await withIsolatedAgentBrowserEnvironment(() => agentBrowserTool.execute(`${toolCallId}:script:${innerResults.length + 1}`, {
|
|
1511
|
+
args: ["--namespace", AGENT_BROWSER_SCRIPT_NAMESPACE, "--session", sessionName, ...innerParams.args],
|
|
1512
|
+
stdin: innerParams.stdin,
|
|
1513
|
+
timeoutMs: innerTimeoutMs,
|
|
1514
|
+
}, innerSignal, undefined, ctx));
|
|
1515
|
+
innerResults.push(innerResult);
|
|
1516
|
+
return await buildScriptBrowserEnvelope(innerResult, innerParams.args, sessionName);
|
|
1517
|
+
},
|
|
1518
|
+
signal: scriptController.signal,
|
|
1519
|
+
timeoutMs: scriptTimeoutMs,
|
|
1520
|
+
});
|
|
1521
|
+
run = await pendingRun;
|
|
1522
|
+
}
|
|
1523
|
+
catch { }
|
|
1524
|
+
finally {
|
|
1525
|
+
activeScriptControllers.delete(scriptController);
|
|
1526
|
+
signal?.removeEventListener("abort", abortScript);
|
|
1527
|
+
if (leased) {
|
|
1528
|
+
try {
|
|
1529
|
+
cleanupError = await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(() => closeScriptSessionLeaseWithinQueue(sessionName, ctx.cwd)));
|
|
1530
|
+
}
|
|
1531
|
+
catch {
|
|
1532
|
+
cleanupError = "The isolated script session cleanup operation failed.";
|
|
1533
|
+
try {
|
|
1534
|
+
appendScriptSessionLease(pi, sessionName, "failed");
|
|
1535
|
+
}
|
|
1536
|
+
catch { }
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
activeScriptExecutions.delete(scriptExecution);
|
|
1540
|
+
finishScriptExecution();
|
|
1541
|
+
}
|
|
1542
|
+
let scriptResult = buildScriptToolResult({ cleanupError, innerResults, run, sessionName: leased ? sessionName : undefined });
|
|
1543
|
+
if (artifactManifest) {
|
|
1544
|
+
scriptResult = {
|
|
1545
|
+
...scriptResult,
|
|
1546
|
+
details: {
|
|
1547
|
+
...(isRecord(scriptResult.details) ? scriptResult.details : {}),
|
|
1548
|
+
artifactManifest,
|
|
1549
|
+
artifactRetentionSummary: formatSessionArtifactRetentionSummary(artifactManifest),
|
|
1550
|
+
},
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
return applyUnserializedOutputPath(scriptResult);
|
|
1554
|
+
}
|
|
974
1555
|
const { toolArgs } = resolvedInput;
|
|
975
1556
|
const compiledElectron = resolvedInput.kind === "electron" ? resolvedInput.compiledElectron : undefined;
|
|
976
1557
|
const redactedCompiledElectron = resolvedInput.kind === "electron" ? resolvedInput.redactedCompiledElectron : undefined;
|
|
@@ -980,7 +1561,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
980
1561
|
compiledElectron,
|
|
981
1562
|
ownedRecords: ownedElectronLaunchRecords,
|
|
982
1563
|
});
|
|
983
|
-
|
|
1564
|
+
let electronHostResult = await handleElectronHostInput({
|
|
984
1565
|
attachedSessionKeys,
|
|
985
1566
|
compiledElectron,
|
|
986
1567
|
cwd: ctx.cwd,
|
|
@@ -1012,11 +1593,16 @@ export default function agentBrowserExtension(pi) {
|
|
|
1012
1593
|
}
|
|
1013
1594
|
replaceWithActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronHostLaunchRecords, branchOwnedElectronLaunchIds, cleanedLaunchIds);
|
|
1014
1595
|
mergeElectronCleanupRecords(electronLaunchRecords, cleanupRecords);
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1596
|
+
const cleanupNamespace = isRecord(electronHostResult.details) && typeof electronHostResult.details.namespace === "string"
|
|
1597
|
+
? electronHostResult.details.namespace
|
|
1598
|
+
: undefined;
|
|
1599
|
+
const closedSessionIdentities = getCleanupResultsClosedManagedSessionIdentities(cleanupRecords, cleanupNamespace);
|
|
1600
|
+
syncElectronCleanupManagedSessions(ownedManagedSessions, cleanupRecords, cleanupNamespace);
|
|
1601
|
+
for (const identity of closedSessionIdentities) {
|
|
1602
|
+
retireRecordingSession(identity.sessionName, identity.namespace);
|
|
1603
|
+
const closedSessionKey = getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName;
|
|
1604
|
+
clearSessionScopedBrowserState(closedSessionKey);
|
|
1605
|
+
if (closedSessionKey === (getSessionContextKey(managedSessionName, managedSessionNamespace) ?? managedSessionName)) {
|
|
1020
1606
|
managedSessionActive = false;
|
|
1021
1607
|
managedSessionCompatibilityWorkaround = undefined;
|
|
1022
1608
|
managedSessionHeadedAutosaveDisabled = false;
|
|
@@ -1026,14 +1612,39 @@ export default function agentBrowserExtension(pi) {
|
|
|
1026
1612
|
managedSessionName = createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal);
|
|
1027
1613
|
}
|
|
1028
1614
|
}
|
|
1615
|
+
if (artifactManifest) {
|
|
1616
|
+
electronHostResult = {
|
|
1617
|
+
...electronHostResult,
|
|
1618
|
+
details: {
|
|
1619
|
+
...(isRecord(electronHostResult.details) ? electronHostResult.details : {}),
|
|
1620
|
+
artifactManifest,
|
|
1621
|
+
artifactRetentionSummary: formatSessionArtifactRetentionSummary(artifactManifest),
|
|
1622
|
+
},
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1029
1625
|
}
|
|
1030
1626
|
return electronHostResult;
|
|
1031
1627
|
};
|
|
1032
|
-
const
|
|
1033
|
-
?
|
|
1034
|
-
:
|
|
1628
|
+
const runSerializedElectronHostInput = () => shouldSerializeElectronHostInput(compiledElectron)
|
|
1629
|
+
? managedSessionExecutionQueue.run(runElectronHostInput)
|
|
1630
|
+
: runElectronHostInput();
|
|
1631
|
+
const electronHostResult = compiledElectron?.action === "cleanup"
|
|
1632
|
+
? await artifactExecutionQueue.run(async () => {
|
|
1633
|
+
const reservationError = outputPath ? getArtifactPreflightValidationError({
|
|
1634
|
+
activeRecordingReservations: activeRecordingReservations.values(),
|
|
1635
|
+
args: [],
|
|
1636
|
+
cwd: ctx.cwd,
|
|
1637
|
+
outputPath,
|
|
1638
|
+
}) : undefined;
|
|
1639
|
+
if (reservationError) {
|
|
1640
|
+
return buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: reservationError });
|
|
1641
|
+
}
|
|
1642
|
+
const result = await runSerializedElectronHostInput();
|
|
1643
|
+
return result ? applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result }) : result;
|
|
1644
|
+
})
|
|
1645
|
+
: await runSerializedElectronHostInput();
|
|
1035
1646
|
if (electronHostResult) {
|
|
1036
|
-
return
|
|
1647
|
+
return compiledElectron?.action === "cleanup" ? electronHostResult : applyUnserializedOutputPath(electronHostResult);
|
|
1037
1648
|
}
|
|
1038
1649
|
const explicitSessionName = extractExplicitSessionName(toolArgs);
|
|
1039
1650
|
const explicitNamespace = extractExplicitNamespace(toolArgs);
|
|
@@ -1044,8 +1655,11 @@ export default function agentBrowserExtension(pi) {
|
|
|
1044
1655
|
ownedElectronLaunchRecords,
|
|
1045
1656
|
ownedManagedSessions,
|
|
1046
1657
|
});
|
|
1658
|
+
const callerOwnedSessionNamespace = explicitSessionName
|
|
1659
|
+
? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)
|
|
1660
|
+
: undefined;
|
|
1047
1661
|
const callerOwnedSessionQueueKey = !serializeBrowserCommand && explicitSessionName
|
|
1048
|
-
? getSessionContextKey(explicitSessionName,
|
|
1662
|
+
? getSessionContextKey(explicitSessionName, callerOwnedSessionNamespace) ?? explicitSessionName
|
|
1049
1663
|
: undefined;
|
|
1050
1664
|
const runBrowserCommand = async () => {
|
|
1051
1665
|
const branchRestoreGenerationAtStart = branchRestoreGeneration;
|
|
@@ -1103,20 +1717,30 @@ export default function agentBrowserExtension(pi) {
|
|
|
1103
1717
|
state: browserRunState,
|
|
1104
1718
|
});
|
|
1105
1719
|
const branchRestoreStillCurrent = branchRestoreGenerationAtStart === branchRestoreGeneration;
|
|
1720
|
+
const resultDetails = isRecord(result.details) ? result.details : undefined;
|
|
1721
|
+
const resultSessionName = typeof resultDetails?.sessionName === "string"
|
|
1722
|
+
? resultDetails.sessionName
|
|
1723
|
+
: extractExplicitSessionName(toolArgs);
|
|
1724
|
+
const resultNamespace = typeof resultDetails?.namespace === "string"
|
|
1725
|
+
? resultDetails.namespace
|
|
1726
|
+
: resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE);
|
|
1106
1727
|
if (branchRestoreStillCurrent) {
|
|
1107
|
-
const
|
|
1108
|
-
const resultSessionName = typeof resultDetails?.sessionName === "string"
|
|
1109
|
-
? resultDetails.sessionName
|
|
1110
|
-
: extractExplicitSessionName(toolArgs);
|
|
1111
|
-
const resultNamespace = typeof resultDetails?.namespace === "string"
|
|
1112
|
-
? resultDetails.namespace
|
|
1113
|
-
: resolveAgentBrowserNamespace(toolArgs, process.env.AGENT_BROWSER_NAMESPACE);
|
|
1728
|
+
const resultBatchCloseLifecycle = getSuccessfulBatchCloseLifecycle(resultDetails?.batchSteps);
|
|
1114
1729
|
const resultSessionKey = getSessionContextKey(resultSessionName, resultNamespace) ?? resultSessionName;
|
|
1115
1730
|
const managedSessionOutcome = isRecord(resultDetails?.managedSessionOutcome) ? resultDetails.managedSessionOutcome : undefined;
|
|
1731
|
+
const closeAllApplied = resultDetails?.closeAllApplied === true;
|
|
1116
1732
|
const attachedSessionRemainsActive = result.isError !== true
|
|
1117
|
-
|| (attachedSessionRequested && managedSessionOutcome?.activeAfter === true);
|
|
1118
|
-
const closesAttachedSession = result.isError !== true && isCloseCommand(
|
|
1119
|
-
|
|
1733
|
+
|| ((attachedSessionRequested || attachedSessionKnown) && managedSessionOutcome?.activeAfter === true);
|
|
1734
|
+
const closesAttachedSession = (result.isError !== true && isCloseCommand(extractUpstreamCommandTokens(toolArgs)[0]))
|
|
1735
|
+
|| resultBatchCloseLifecycle?.endsClosed === true;
|
|
1736
|
+
if (closeAllApplied) {
|
|
1737
|
+
deleteIdentityKeysInNamespace(attachedSessionKeys, resultNamespace);
|
|
1738
|
+
if (resultSessionKey && attachedSessionRemainsActive && (attachedSessionRequested || attachedSessionKnown) && resultBatchCloseLifecycle?.endsClosed === false) {
|
|
1739
|
+
attachedSessionKeys.add(resultSessionKey);
|
|
1740
|
+
result = { ...result, details: { ...(resultDetails ?? {}), attachedBrowserSession: true } };
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
else if (resultSessionKey && closesAttachedSession)
|
|
1120
1744
|
attachedSessionKeys.delete(resultSessionKey);
|
|
1121
1745
|
else if (resultSessionKey && attachedSessionRemainsActive && (attachedSessionRequested || attachedSessionKnown)) {
|
|
1122
1746
|
attachedSessionKeys.add(resultSessionKey);
|
|
@@ -1127,6 +1751,29 @@ export default function agentBrowserExtension(pi) {
|
|
|
1127
1751
|
allowedDomainsBySession = mergeBrowserRunMap(allowedDomainsBySession, initialAllowedDomainsBySession, browserRunState.allowedDomainsBySession);
|
|
1128
1752
|
networkRoutesBySession = mergeBrowserRunMap(networkRoutesBySession, initialNetworkRoutesBySession, browserRunState.networkRoutesBySession);
|
|
1129
1753
|
artifactManifest = mergeBrowserRunArtifactManifest(artifactManifest, initialArtifactManifest, browserRunState.artifactManifest);
|
|
1754
|
+
const handledBatchCloseKeys = syncRecordingReservationsFromResult(result);
|
|
1755
|
+
if (resultDetails?.closeAllApplied === true) {
|
|
1756
|
+
for (const [sessionKey, reservation] of [...activeRecordingReservations]) {
|
|
1757
|
+
if (isAgentBrowserSessionIdentityKeyInNamespace(sessionKey, resultNamespace)) {
|
|
1758
|
+
retireRecordingSession(reservation.sessionName, reservation.namespace);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
for (const closedSessionKey of browserRunState.closedManagedSessionNames) {
|
|
1763
|
+
if (handledBatchCloseKeys.has(closedSessionKey))
|
|
1764
|
+
continue;
|
|
1765
|
+
const reservation = activeRecordingReservations.get(closedSessionKey);
|
|
1766
|
+
if (reservation)
|
|
1767
|
+
retireRecordingSession(reservation.sessionName, reservation.namespace);
|
|
1768
|
+
}
|
|
1769
|
+
if (resultSessionName && resultReportsNoRecordingInProgress(result)) {
|
|
1770
|
+
retireRecordingSession(resultSessionName, resultNamespace);
|
|
1771
|
+
}
|
|
1772
|
+
if (resultSessionName) {
|
|
1773
|
+
const reservation = activeRecordingReservations.get(getAgentBrowserSessionIdentityKey(resultSessionName, resultNamespace));
|
|
1774
|
+
if (reservation)
|
|
1775
|
+
result = appendActiveRecordingCleanupAction(result, reservation);
|
|
1776
|
+
}
|
|
1130
1777
|
if (artifactManifest) {
|
|
1131
1778
|
result = {
|
|
1132
1779
|
...result,
|
|
@@ -1155,7 +1802,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
1155
1802
|
mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronLaunchRecords, {
|
|
1156
1803
|
branchOwnedLaunchIds: branchOwnedElectronLaunchIds,
|
|
1157
1804
|
touchedLaunchIds: !result.isError
|
|
1158
|
-
? getTouchedElectronLaunchIds(explicitSessionName ?? browserRunState.managedSessionName, electronLaunchRecords)
|
|
1805
|
+
? getTouchedElectronLaunchIds(explicitSessionName ?? browserRunState.managedSessionName, electronLaunchRecords, explicitSessionName ? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE) : browserRunState.managedSessionNamespace)
|
|
1159
1806
|
: undefined,
|
|
1160
1807
|
});
|
|
1161
1808
|
if (serializeBrowserCommand)
|
|
@@ -1163,12 +1810,47 @@ export default function agentBrowserExtension(pi) {
|
|
|
1163
1810
|
}
|
|
1164
1811
|
return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result });
|
|
1165
1812
|
};
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1813
|
+
const closesAllSessions = commandClosesAllSessions(toolArgs, resolvedInput.toolStdin);
|
|
1814
|
+
const closeAllNamespace = closesAllSessions
|
|
1815
|
+
? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)
|
|
1816
|
+
: undefined;
|
|
1817
|
+
const runWithinSessionQueue = () => {
|
|
1818
|
+
if (closesAllSessions)
|
|
1819
|
+
return managedSessionExecutionQueue.run(() => callerOwnedSessionExecutionQueues.runExclusive(closeAllNamespace, runBrowserCommand));
|
|
1820
|
+
if (serializeBrowserCommand)
|
|
1821
|
+
return managedSessionExecutionQueue.run(runBrowserCommand);
|
|
1822
|
+
return callerOwnedSessionQueueKey
|
|
1823
|
+
? callerOwnedSessionExecutionQueues.run(callerOwnedSessionQueueKey, callerOwnedSessionNamespace, runBrowserCommand)
|
|
1824
|
+
: runBrowserCommand();
|
|
1825
|
+
};
|
|
1826
|
+
if (!commandTouchesArtifactLifecycle(toolArgs, resolvedInput.toolStdin, outputPath))
|
|
1827
|
+
return runWithinSessionQueue();
|
|
1828
|
+
return artifactExecutionQueue.run(async () => {
|
|
1829
|
+
const artifactValidationError = getArtifactPreflightValidationError({
|
|
1830
|
+
activeRecordingReservations: activeRecordingReservations.values(),
|
|
1831
|
+
args: toolArgs,
|
|
1832
|
+
cwd: ctx.cwd,
|
|
1833
|
+
outputPath,
|
|
1834
|
+
stdin: resolvedInput.toolStdin,
|
|
1835
|
+
});
|
|
1836
|
+
if (!artifactValidationError)
|
|
1837
|
+
return runWithinSessionQueue();
|
|
1838
|
+
return applyAgentBrowserOutputPath({
|
|
1839
|
+
cwd: ctx.cwd,
|
|
1840
|
+
outputPath,
|
|
1841
|
+
result: buildValidationFailureResult({
|
|
1842
|
+
attemptedKind: resolvedInput.kind,
|
|
1843
|
+
kind: "invalid",
|
|
1844
|
+
redactedArgs: resolvedInput.redactedArgs,
|
|
1845
|
+
status: "invalid",
|
|
1846
|
+
toolArgs: resolvedInput.toolArgs,
|
|
1847
|
+
toolStdin: resolvedInput.toolStdin,
|
|
1848
|
+
validationError: artifactValidationError,
|
|
1849
|
+
}),
|
|
1850
|
+
});
|
|
1851
|
+
});
|
|
1171
1852
|
},
|
|
1172
|
-
}
|
|
1853
|
+
};
|
|
1854
|
+
pi.registerTool(agentBrowserTool);
|
|
1173
1855
|
registerWebSearchToolIfAvailable(agentBrowserConfig);
|
|
1174
1856
|
}
|