pi-agent-browser-native 0.3.0 → 0.6.5
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 +265 -0
- package/README.md +130 -54
- package/dist/extensions/agent-browser/index.js +781 -169
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
- package/dist/extensions/agent-browser/lib/argv-grammar.js +50 -2
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
- package/dist/extensions/agent-browser/lib/command-policy.js +5 -8
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +53 -12
- package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
- package/dist/extensions/agent-browser/lib/config.js +1 -1
- package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -13
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
- package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -24
- 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 +26 -4
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +6 -139
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -116
- package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
- package/dist/extensions/agent-browser/lib/managed-session-storage.js +54 -25
- 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 +54 -48
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +71 -5
- 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 -7
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +152 -64
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +244 -102
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +63 -37
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +20 -21
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
- package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
- package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
- package/dist/extensions/agent-browser/lib/playbook.js +29 -25
- package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
- package/dist/extensions/agent-browser/lib/process-identity.js +5 -12
- package/dist/extensions/agent-browser/lib/process.js +130 -104
- package/dist/extensions/agent-browser/lib/recording-reservations.js +116 -0
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -6
- 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 +86 -18
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +38 -2
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +18 -17
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +38 -20
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +60 -15
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
- package/dist/extensions/agent-browser/lib/results/presentation.js +36 -6
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +3 -1
- package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +54 -11
- package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
- package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
- package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
- package/dist/extensions/agent-browser/lib/runtime.js +186 -108
- package/dist/extensions/agent-browser/lib/session-page-state.js +71 -10
- package/dist/extensions/agent-browser/lib/temp.js +1 -2
- package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
- package/dist/extensions/agent-browser/lib/web-search.js +108 -24
- package/dist/extensions/agent-browser/script-worker.js +169 -0
- package/dist/scripts/agent-browser-target.mjs +21 -0
- package/docs/ARCHITECTURE.md +57 -34
- package/docs/COMMAND_REFERENCE.md +255 -68
- package/docs/ELECTRON.md +2 -2
- package/docs/RELEASE.md +12 -10
- package/docs/REQUIREMENTS.md +11 -8
- package/docs/SUPPORT_MATRIX.md +36 -24
- package/docs/TOOL_CONTRACT.md +169 -95
- package/package.json +3 -1
- package/platform-smoke.config.mjs +2 -2
- package/scripts/agent-browser-capability-baseline.mjs +87 -9
- package/scripts/agent-browser-target.mjs +21 -0
- package/scripts/build.mjs +41 -0
- package/scripts/config.mjs +1 -0
- package/scripts/doctor.mjs +16 -9
- package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
- package/scripts/platform-smoke/targets.mjs +12 -6
- package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
- package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -583
- package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
- package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
|
@@ -2,26 +2,36 @@ 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";
|
|
9
|
-
import {
|
|
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";
|
|
12
|
+
import { 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 { MINIMUM_AGENT_BROWSER_VERSION, SUPPORTED_AGENT_BROWSER_VERSION_LABEL, TARGET_AGENT_BROWSER_VERSION, 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";
|
|
16
|
-
import {
|
|
22
|
+
import { AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS, AGENT_BROWSER_SCRIPT_NAMESPACE, createAgentBrowserScriptSessionName, isAgentBrowserScriptSessionName, runAgentBrowserScript, } from "./lib/input-modes/script.js";
|
|
17
23
|
import { closeManagedSession, getSessionContextKey, runAgentBrowserTool } from "./lib/orchestration/browser-run/index.js";
|
|
24
|
+
import { canonicalizeExplicitArtifactDestination, getExplicitArtifactDestination } from "./lib/orchestration/browser-run/artifact-paths.js";
|
|
18
25
|
import { findElectronLaunchRecordForSession, getActiveElectronRecords } from "./lib/orchestration/browser-run/session-state.js";
|
|
19
|
-
import {
|
|
26
|
+
import { parseBatchCommandArgument, parseUserBatchStdin } from "./lib/orchestration/batch-stdin.js";
|
|
20
27
|
import { ELECTRON_POST_COMMAND_STATUS_SETTLE_MS, ELECTRON_PROFILE_ISOLATION_DETAILS, cleanupActiveElectronHostLaunches, handleElectronHostInput, restoreElectronLaunchRecordsFromBranch, } from "./lib/orchestration/electron-host/index.js";
|
|
21
28
|
import { buildValidationFailureResult, resolveAgentBrowserInput } from "./lib/orchestration/input-plan.js";
|
|
22
|
-
import { applyAgentBrowserOutputPath,
|
|
23
|
-
import {
|
|
29
|
+
import { applyAgentBrowserOutputPath, normalizeRequestedOutputPath } from "./lib/orchestration/output-file.js";
|
|
30
|
+
import { appendScriptSessionLease, buildScriptBrowserEnvelope, buildScriptToolResult, getScriptSessionLeasesFromBranch } from "./lib/orchestration/script-mode.js";
|
|
31
|
+
import { formatSessionArtifactRetentionSummary, getSessionArtifactManifestEntryKey, isPendingRecordingCommand, isSessionArtifactManifest, mergeSessionArtifactManifest, retirePendingRecordingManifestEntries } from "./lib/results/artifact-manifest.js";
|
|
32
|
+
import { appendUniqueAgentBrowserNextActions, applyNamespaceToNextActions, applySessionToNextActions, buildNextToolAction } from "./lib/results/next-actions.js";
|
|
24
33
|
import { canRegisterWebSearchTool, loadAgentBrowserConfigSync } from "./lib/config.js";
|
|
34
|
+
import { appendRecordingReservationTransition, applyRecordingArtifactsToReservations, restoreRecordingReservationStateFromBranch, retireRecordingReservation, } from "./lib/recording-reservations.js";
|
|
25
35
|
import { createAgentBrowserWebSearchTool } from "./lib/web-search.js";
|
|
26
36
|
import { isDirectAgentBrowserBashAllowed, isHarmlessAgentBrowserInspectionCommand, looksLikeDirectAgentBrowserBash, } from "./lib/bash-guard.js";
|
|
27
37
|
import { AgentBrowserResultComponent, buildAgentBrowserToolResultPatch, formatAgentBrowserRenderCall, formatAgentBrowserRenderResult, } from "./lib/pi-tool-rendering.js";
|
|
@@ -30,22 +40,94 @@ function isBashToolCallEvent(event) {
|
|
|
30
40
|
return false;
|
|
31
41
|
return typeof event.input.command === "string";
|
|
32
42
|
}
|
|
33
|
-
function
|
|
34
|
-
const commandTokens =
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
function getArtifactCommandSteps(args, stdin) {
|
|
44
|
+
const commandTokens = extractUpstreamCommandTokens(args);
|
|
45
|
+
const batch = commandTokens[0] === "batch";
|
|
46
|
+
if (!batch)
|
|
47
|
+
return { batch, steps: commandTokens.length > 0 ? [commandTokens] : [] };
|
|
48
|
+
const steps = [];
|
|
49
|
+
for (const command of commandTokens.slice(1)) {
|
|
50
|
+
if (command === "--bail")
|
|
51
|
+
continue;
|
|
52
|
+
const parsed = parseBatchCommandArgument(command);
|
|
53
|
+
if (parsed.error || !parsed.step)
|
|
54
|
+
return { batch, error: `Unsupported batch step ${steps.length + 1}: ${parsed.error ?? "command could not be parsed safely"}`, steps };
|
|
55
|
+
steps.push(parsed.step);
|
|
37
56
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
57
|
+
// Upstream executes raw argument steps exclusively when any exist, so ignored
|
|
58
|
+
// stdin must not add artifact/lifecycle steps or fail this preflight.
|
|
59
|
+
if (steps.length > 0)
|
|
60
|
+
return { batch, steps };
|
|
61
|
+
const parsed = parseUserBatchStdin(stdin);
|
|
62
|
+
return parsed.error ? { batch, error: parsed.error, steps } : { batch, steps: parsed.steps ?? [] };
|
|
63
|
+
}
|
|
64
|
+
function getArtifactPreflightValidationError(options) {
|
|
65
|
+
const { batch, error, steps } = getArtifactCommandSteps(options.args, options.stdin);
|
|
66
|
+
if (error)
|
|
67
|
+
return error;
|
|
68
|
+
const activeRecordingDestinations = new Set();
|
|
69
|
+
const cleanupOnly = steps.length > 0 && steps.every((step) => {
|
|
70
|
+
const [command, subcommand] = extractUpstreamCommandTokens(step);
|
|
71
|
+
return isCloseCommand(command) || (command === "record" && subcommand === "stop");
|
|
72
|
+
});
|
|
73
|
+
for (const reservation of options.activeRecordingReservations ?? []) {
|
|
74
|
+
try {
|
|
75
|
+
activeRecordingDestinations.add(canonicalizeExplicitArtifactDestination(reservation.cwd, reservation.absolutePath));
|
|
76
|
+
}
|
|
77
|
+
catch (canonicalizationError) {
|
|
78
|
+
if (!cleanupOnly)
|
|
79
|
+
return canonicalizationError instanceof Error ? canonicalizationError.message : "An active recording destination could not be resolved safely.";
|
|
80
|
+
}
|
|
41
81
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
82
|
+
let canonicalOutputPath;
|
|
83
|
+
if (options.outputPath) {
|
|
84
|
+
try {
|
|
85
|
+
canonicalOutputPath = canonicalizeExplicitArtifactDestination(options.cwd, normalizeRequestedOutputPath(options.outputPath));
|
|
86
|
+
if (activeRecordingDestinations.has(canonicalOutputPath)) {
|
|
87
|
+
return `Unsupported outputPath: ${options.outputPath} is reserved by an active recording. Stop that recording first or use a distinct path.`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (canonicalizationError) {
|
|
91
|
+
return canonicalizationError instanceof Error ? canonicalizationError.message : `outputPath ${options.outputPath} could not be resolved safely.`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const artifactDestinations = new Map();
|
|
95
|
+
let sawBatchClose = false;
|
|
96
|
+
for (const [index, step] of steps.entries()) {
|
|
97
|
+
const commandStep = extractUpstreamCommandTokens(step);
|
|
98
|
+
if (batch) {
|
|
99
|
+
const stepValidationError = validateToolArgs(step, { batchStep: true });
|
|
100
|
+
if (stepValidationError)
|
|
101
|
+
return `Unsupported batch step ${index + 1}: ${stepValidationError}`;
|
|
102
|
+
if (sawBatchClose && commandStep[0] === "record" && (commandStep[1] === "start" || commandStep[1] === "restart")) {
|
|
103
|
+
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.`;
|
|
104
|
+
}
|
|
105
|
+
if (isCloseCommand(commandStep[0]))
|
|
106
|
+
sawBatchClose = true;
|
|
107
|
+
}
|
|
108
|
+
const artifactDestination = getExplicitArtifactDestination(commandStep);
|
|
109
|
+
if (artifactDestination) {
|
|
110
|
+
let canonicalDestination;
|
|
111
|
+
try {
|
|
112
|
+
canonicalDestination = canonicalizeExplicitArtifactDestination(options.cwd, artifactDestination);
|
|
113
|
+
}
|
|
114
|
+
catch (canonicalizationError) {
|
|
115
|
+
return canonicalizationError instanceof Error ? canonicalizationError.message : `Artifact destination ${artifactDestination} could not be resolved safely.`;
|
|
116
|
+
}
|
|
117
|
+
if (canonicalOutputPath === canonicalDestination) {
|
|
118
|
+
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.`;
|
|
119
|
+
}
|
|
120
|
+
if (activeRecordingDestinations.has(canonicalDestination)) {
|
|
121
|
+
const prefix = batch ? `Unsupported batch artifact destination in step ${index + 1}` : "Unsupported artifact destination";
|
|
122
|
+
return `${prefix}: ${artifactDestination} is reserved by an active recording. Stop that recording first or use a distinct path.`;
|
|
123
|
+
}
|
|
124
|
+
const priorStep = artifactDestinations.get(canonicalDestination);
|
|
125
|
+
if (priorStep !== undefined) {
|
|
126
|
+
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.`;
|
|
127
|
+
}
|
|
128
|
+
artifactDestinations.set(canonicalDestination, index);
|
|
129
|
+
}
|
|
130
|
+
if (batch && commandStep[0] === "screenshot" && step.includes("--annotate")) {
|
|
49
131
|
return [
|
|
50
132
|
`Unsupported batch screenshot annotation in step ${index + 1}: put --annotate in top-level args, not inside the batch step.`,
|
|
51
133
|
`Use: { "args": ["--annotate", "batch"], "stdin": "[[\\"screenshot\\",\\"/path/to/image.png\\"]]" }`,
|
|
@@ -54,6 +136,51 @@ function getBatchPreflightValidationError(args, stdin) {
|
|
|
54
136
|
}
|
|
55
137
|
return undefined;
|
|
56
138
|
}
|
|
139
|
+
function commandClosesAllSessions(args, stdin) {
|
|
140
|
+
const parsed = getArtifactCommandSteps(args, stdin);
|
|
141
|
+
return !parsed.error && parsed.steps.some((step) => isCloseAllCommand(extractUpstreamCommandTokens(step)));
|
|
142
|
+
}
|
|
143
|
+
function commandTouchesArtifactLifecycle(args, stdin, outputPath) {
|
|
144
|
+
if (outputPath)
|
|
145
|
+
return true;
|
|
146
|
+
const parsed = getArtifactCommandSteps(args, stdin);
|
|
147
|
+
if (parsed.error)
|
|
148
|
+
return true;
|
|
149
|
+
return parsed.steps.some((step) => {
|
|
150
|
+
const commandStep = extractUpstreamCommandTokens(step);
|
|
151
|
+
return getExplicitArtifactDestination(commandStep) !== undefined || commandStep[0] === "record" || commandStep[0] === "screenshot" || isCloseCommand(commandStep[0]);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function isResultFileArtifact(artifact) {
|
|
155
|
+
return isRecord(artifact)
|
|
156
|
+
&& typeof artifact.absolutePath === "string"
|
|
157
|
+
&& typeof artifact.kind === "string"
|
|
158
|
+
&& typeof artifact.path === "string";
|
|
159
|
+
}
|
|
160
|
+
function getResultFileArtifacts(result) {
|
|
161
|
+
const details = isRecord(result.details) ? result.details : undefined;
|
|
162
|
+
return Array.isArray(details?.artifacts) ? details.artifacts.filter(isResultFileArtifact) : [];
|
|
163
|
+
}
|
|
164
|
+
function reportsNoRecordingInProgress(value) {
|
|
165
|
+
try {
|
|
166
|
+
return /no recording in progress/i.test(JSON.stringify(value));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function batchStepReportsNoRecordingInProgress(step) {
|
|
173
|
+
if (!isRecord(step) || step.success !== false)
|
|
174
|
+
return false;
|
|
175
|
+
const command = Array.isArray(step.command) && step.command.every((token) => typeof token === "string") ? extractUpstreamCommandTokens(step.command) : [];
|
|
176
|
+
return command[0] === "record" && command[1] === "stop" && reportsNoRecordingInProgress(step);
|
|
177
|
+
}
|
|
178
|
+
function resultReportsNoRecordingInProgress(result) {
|
|
179
|
+
if (result.isError !== true)
|
|
180
|
+
return false;
|
|
181
|
+
const details = isRecord(result.details) ? result.details : undefined;
|
|
182
|
+
return details?.command === "record" && details.subcommand === "stop" && reportsNoRecordingInProgress(result.content);
|
|
183
|
+
}
|
|
57
184
|
function restoreArtifactManifestFromBranch(branch) {
|
|
58
185
|
let restoredManifest;
|
|
59
186
|
for (const entry of branch) {
|
|
@@ -69,6 +196,12 @@ function restoreArtifactManifestFromBranch(branch) {
|
|
|
69
196
|
}
|
|
70
197
|
return restoredManifest;
|
|
71
198
|
}
|
|
199
|
+
function getRecognizedCompatibilityWorkaround(value) {
|
|
200
|
+
const workaround = isRecord(value) ? value : undefined;
|
|
201
|
+
return (workaround?.id === "chatgpt-headless-user-agent" || workaround?.id === "cloudflare-headless-user-agent") && typeof workaround.reason === "string"
|
|
202
|
+
? { id: workaround.id, reason: workaround.reason }
|
|
203
|
+
: undefined;
|
|
204
|
+
}
|
|
72
205
|
function restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, sessionName, namespace) {
|
|
73
206
|
let restored;
|
|
74
207
|
const targetKey = getSessionContextKey(sessionName, namespace);
|
|
@@ -81,12 +214,9 @@ function restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, sessionN
|
|
|
81
214
|
const details = isRecord(message.details) ? message.details : undefined;
|
|
82
215
|
if (!details)
|
|
83
216
|
continue;
|
|
84
|
-
const workaround = isRecord(details.compatibilityWorkaround) ? details.compatibilityWorkaround : undefined;
|
|
85
217
|
if (getSessionContextKey(typeof details.sessionName === "string" ? details.sessionName : undefined, typeof details.namespace === "string" ? details.namespace : undefined) !== targetKey)
|
|
86
218
|
continue;
|
|
87
|
-
const recognizedWorkaround = (
|
|
88
|
-
? { id: workaround.id, reason: workaround.reason }
|
|
89
|
-
: undefined;
|
|
219
|
+
const recognizedWorkaround = getRecognizedCompatibilityWorkaround(details.compatibilityWorkaround);
|
|
90
220
|
const succeeded = getSuccessfulToolResult(details, message);
|
|
91
221
|
const outcome = getManagedSessionOutcome(details);
|
|
92
222
|
const activeAfterFailure = recognizedWorkaround
|
|
@@ -163,9 +293,21 @@ function getToolResultArgs(details) {
|
|
|
163
293
|
return details.effectiveArgs;
|
|
164
294
|
return [];
|
|
165
295
|
}
|
|
166
|
-
function
|
|
296
|
+
function detailsReportCloseAllApplied(details, succeeded) {
|
|
297
|
+
const args = getToolResultArgs(details);
|
|
298
|
+
return details.closeAllApplied === true
|
|
299
|
+
|| (succeeded && isCloseAllCommand(extractUpstreamCommandTokens(args)))
|
|
300
|
+
|| batchHasSuccessfulCloseAll(details.batchSteps);
|
|
301
|
+
}
|
|
302
|
+
function deleteIdentityKeysInNamespace(entries, namespace) {
|
|
303
|
+
for (const key of entries.keys()) {
|
|
304
|
+
if (isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
|
|
305
|
+
entries.delete(key);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function isAttachedBrowserInvocation(args, env = getAgentBrowserProcessEnvironment()) {
|
|
167
309
|
const autoConnectEnv = env.AGENT_BROWSER_AUTO_CONNECT;
|
|
168
|
-
return
|
|
310
|
+
return extractUpstreamCommandTokens(args)[0] === "connect"
|
|
169
311
|
|| hasLaunchScopedFlagToken(args, "--cdp")
|
|
170
312
|
|| hasLaunchScopedFlagToken(args, "--auto-connect")
|
|
171
313
|
|| env.AGENT_BROWSER_CDP !== undefined
|
|
@@ -184,85 +326,50 @@ function restoreAttachedSessionKeysFromBranch(branch) {
|
|
|
184
326
|
continue;
|
|
185
327
|
const managedSessionOutcome = isRecord(details.managedSessionOutcome) ? details.managedSessionOutcome : undefined;
|
|
186
328
|
const retainedFailedAttachment = details.attachedBrowserSession === true && managedSessionOutcome?.activeAfter === true;
|
|
187
|
-
|
|
188
|
-
|
|
329
|
+
const succeeded = getSuccessfulToolResult(details, message);
|
|
330
|
+
const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
|
|
331
|
+
const terminalBatchClose = batchCloseLifecycle?.endsClosed === true;
|
|
189
332
|
const args = getToolResultArgs(details);
|
|
333
|
+
const namespace = typeof details.namespace === "string" ? details.namespace : extractExplicitNamespace(args);
|
|
190
334
|
const sessionName = typeof details.sessionName === "string" ? details.sessionName : extractExplicitSessionName(args);
|
|
335
|
+
const electron = isRecord(details.electron) ? details.electron : undefined;
|
|
336
|
+
const cleanup = isRecord(electron?.cleanup) ? electron.cleanup : undefined;
|
|
337
|
+
for (const cleanupResult of Array.isArray(cleanup?.results) ? cleanup.results : []) {
|
|
338
|
+
for (const identity of getCleanupResultClosedManagedSessionIdentities(cleanupResult, namespace)) {
|
|
339
|
+
attachedSessionKeys.delete(getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (detailsReportCloseAllApplied(details, succeeded)) {
|
|
343
|
+
deleteIdentityKeysInNamespace(attachedSessionKeys, namespace);
|
|
344
|
+
if (sessionName && details.attachedBrowserSession === true && batchCloseLifecycle?.endsClosed === false) {
|
|
345
|
+
attachedSessionKeys.add(getSessionContextKey(sessionName, namespace) ?? sessionName);
|
|
346
|
+
}
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (!succeeded && !retainedFailedAttachment && !terminalBatchClose)
|
|
350
|
+
continue;
|
|
191
351
|
if (!sessionName)
|
|
192
352
|
continue;
|
|
193
|
-
const namespace = typeof details.namespace === "string" ? details.namespace : extractExplicitNamespace(args);
|
|
194
353
|
const sessionKey = getSessionContextKey(sessionName, namespace) ?? sessionName;
|
|
195
|
-
if (isCloseCommand(
|
|
354
|
+
if ((succeeded && isCloseCommand(extractUpstreamCommandTokens(args)[0])) || terminalBatchClose)
|
|
196
355
|
attachedSessionKeys.delete(sessionKey);
|
|
197
356
|
else if (details.attachedBrowserSession === true || isAttachedBrowserInvocation(args, {}))
|
|
198
357
|
attachedSessionKeys.add(sessionKey);
|
|
199
358
|
}
|
|
200
359
|
return attachedSessionKeys;
|
|
201
360
|
}
|
|
202
|
-
function restoreAllowedDomainsBySessionFromBranch(branch) {
|
|
203
|
-
const restoredPolicies = new Map();
|
|
204
|
-
for (const entry of branch) {
|
|
205
|
-
if (!isRecord(entry) || entry.type !== "message")
|
|
206
|
-
continue;
|
|
207
|
-
const message = isRecord(entry.message) ? entry.message : undefined;
|
|
208
|
-
if (!message || message.toolName !== "agent_browser")
|
|
209
|
-
continue;
|
|
210
|
-
const details = isRecord(message.details) ? message.details : undefined;
|
|
211
|
-
if (!details)
|
|
212
|
-
continue;
|
|
213
|
-
const succeeded = getSuccessfulToolResult(details, message);
|
|
214
|
-
const args = getToolResultArgs(details);
|
|
215
|
-
const command = typeof details.command === "string" ? details.command : extractCommandTokens(args)[0];
|
|
216
|
-
const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
|
|
217
|
-
const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
|
|
218
|
-
const sessionKey = getSessionContextKey(sessionName, namespace);
|
|
219
|
-
const explicitSessionName = extractExplicitSessionName(args);
|
|
220
|
-
const outcome = getManagedSessionOutcome(details);
|
|
221
|
-
const outcomeSucceeded = outcome?.succeeded === true;
|
|
222
|
-
const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
|
|
223
|
-
const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
|
|
224
|
-
const outcomeAttemptedSessionName = typeof outcome?.attemptedSessionName === "string" ? outcome.attemptedSessionName : undefined;
|
|
225
|
-
if (outcomeSucceeded && outcomeStatus === "closed") {
|
|
226
|
-
const closedSessionName = outcomeAttemptedSessionName ?? outcomeCurrentSessionName ?? sessionName;
|
|
227
|
-
if (closedSessionName)
|
|
228
|
-
restoredPolicies.delete(getSessionContextKey(closedSessionName, namespace) ?? closedSessionName);
|
|
229
|
-
}
|
|
230
|
-
if (outcomeSucceeded && outcomeStatus === "replaced") {
|
|
231
|
-
const replacedSessionName = typeof outcome.replacedSessionName === "string" ? outcome.replacedSessionName : undefined;
|
|
232
|
-
const replacedSessionNamespace = typeof outcome.replacedSessionNamespace === "string" ? outcome.replacedSessionNamespace : namespace;
|
|
233
|
-
if (replacedSessionName)
|
|
234
|
-
restoredPolicies.delete(getSessionContextKey(replacedSessionName, replacedSessionNamespace) ?? replacedSessionName);
|
|
235
|
-
}
|
|
236
|
-
if (succeeded && isCloseCommand(command)) {
|
|
237
|
-
const closedSessionName = explicitSessionName ?? sessionName ?? outcomeAttemptedSessionName ?? outcomeCurrentSessionName;
|
|
238
|
-
if (closedSessionName)
|
|
239
|
-
restoredPolicies.delete(getSessionContextKey(closedSessionName, namespace) ?? closedSessionName);
|
|
240
|
-
}
|
|
241
|
-
const electron = isRecord(details.electron) ? details.electron : undefined;
|
|
242
|
-
const cleanup = isRecord(electron?.cleanup) ? electron.cleanup : undefined;
|
|
243
|
-
const cleanupResults = Array.isArray(cleanup?.results) ? cleanup.results : [];
|
|
244
|
-
for (const cleanupResult of cleanupResults) {
|
|
245
|
-
for (const closedSessionName of getCleanupResultClosedManagedSessionNames(cleanupResult))
|
|
246
|
-
restoredPolicies.delete(closedSessionName);
|
|
247
|
-
}
|
|
248
|
-
const outcomeKeepsSessionCurrent = outcome?.activeAfter === true
|
|
249
|
-
&& (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged")
|
|
250
|
-
&& outcomeCurrentSessionName === sessionName;
|
|
251
|
-
const policy = (succeeded || outcomeKeepsSessionCurrent) && sessionKey && !isCloseCommand(command) ? parseAllowedDomainsPolicyFromArgs(args) : undefined;
|
|
252
|
-
if (policy && sessionKey)
|
|
253
|
-
restoredPolicies.set(sessionKey, policy);
|
|
254
|
-
}
|
|
255
|
-
return restoredPolicies;
|
|
256
|
-
}
|
|
257
361
|
function trackOwnedManagedSession(sessions, sessionName, cwd, options = {}) {
|
|
258
362
|
if (!sessionName)
|
|
259
363
|
return;
|
|
260
364
|
const key = getSessionContextKey(sessionName, options.namespace) ?? sessionName;
|
|
261
365
|
const existing = sessions.get(key);
|
|
262
366
|
const branchOwned = existing && !existing.branchOwned ? false : options.branchOwned === true;
|
|
367
|
+
const compatibilityWorkaround = Object.hasOwn(options, "compatibilityWorkaround")
|
|
368
|
+
? options.compatibilityWorkaround
|
|
369
|
+
: existing?.compatibilityWorkaround;
|
|
263
370
|
const headedManagedAutosaveDisabled = options.headedManagedAutosaveDisabled ?? existing?.headedManagedAutosaveDisabled;
|
|
264
371
|
const headedManagedAutosaveInterval = options.headedManagedAutosaveInterval ?? existing?.headedManagedAutosaveInterval;
|
|
265
|
-
sessions.set(key, { branchOwned, cwd, headedManagedAutosaveDisabled, headedManagedAutosaveInterval, namespace: options.namespace, sessionName });
|
|
372
|
+
sessions.set(key, { branchOwned, compatibilityWorkaround, cwd, headedManagedAutosaveDisabled, headedManagedAutosaveInterval, namespace: options.namespace, sessionName });
|
|
266
373
|
}
|
|
267
374
|
function untrackOwnedManagedSession(sessions, sessionName, namespace) {
|
|
268
375
|
if (!sessionName)
|
|
@@ -291,20 +398,21 @@ function syncOwnedManagedSessionsFromResult(sessions, result, cwd) {
|
|
|
291
398
|
const status = typeof outcome.status === "string" ? outcome.status : undefined;
|
|
292
399
|
const currentSessionName = typeof outcome.currentSessionName === "string" ? outcome.currentSessionName : undefined;
|
|
293
400
|
const attemptedSessionName = typeof outcome.attemptedSessionName === "string" ? outcome.attemptedSessionName : undefined;
|
|
401
|
+
const namespace = isRecord(details) && typeof details.namespace === "string" ? details.namespace : undefined;
|
|
294
402
|
if (outcome.activeAfter === true && (status === "created" || status === "replaced" || status === "unchanged")) {
|
|
295
|
-
const namespace = isRecord(details) && typeof details.namespace === "string" ? details.namespace : undefined;
|
|
296
403
|
trackOwnedManagedSession(sessions, currentSessionName, cwd, {
|
|
404
|
+
compatibilityWorkaround: getRecognizedCompatibilityWorkaround(details?.compatibilityWorkaround),
|
|
297
405
|
headedManagedAutosaveDisabled: details?.managedSessionHeadedAutosaveDisabled === true,
|
|
298
406
|
headedManagedAutosaveInterval: typeof details?.managedSessionHeadedAutosaveInterval === "string" ? details.managedSessionHeadedAutosaveInterval : undefined,
|
|
299
407
|
namespace,
|
|
300
408
|
});
|
|
301
409
|
}
|
|
302
410
|
if (succeeded && status === "closed") {
|
|
303
|
-
untrackOwnedManagedSession(sessions, attemptedSessionName ?? currentSessionName);
|
|
411
|
+
untrackOwnedManagedSession(sessions, attemptedSessionName ?? currentSessionName, namespace);
|
|
304
412
|
}
|
|
305
413
|
}
|
|
306
|
-
function getTouchedElectronLaunchIds(sessionName, records) {
|
|
307
|
-
const record = findElectronLaunchRecordForSession(sessionName, records);
|
|
414
|
+
function getTouchedElectronLaunchIds(sessionName, records, namespace) {
|
|
415
|
+
const record = findElectronLaunchRecordForSession(sessionName, records, namespace);
|
|
308
416
|
return record ? new Set([record.launchId]) : undefined;
|
|
309
417
|
}
|
|
310
418
|
function mergeActiveElectronLaunchRecords(target, source, options = {}) {
|
|
@@ -372,10 +480,10 @@ function getElectronHostLaunchRecordsForInput(options) {
|
|
|
372
480
|
}
|
|
373
481
|
return options.branchRecords;
|
|
374
482
|
}
|
|
375
|
-
function
|
|
483
|
+
function getCleanupResultClosedManagedSessionIdentities(result, fallbackNamespace) {
|
|
376
484
|
if (!isRecord(result) || !Array.isArray(result.steps))
|
|
377
485
|
return [];
|
|
378
|
-
const
|
|
486
|
+
const identities = new Map();
|
|
379
487
|
const record = isRecord(result.record) ? result.record : undefined;
|
|
380
488
|
for (const step of result.steps) {
|
|
381
489
|
if (!isRecord(step) || step.resource !== "managed-session")
|
|
@@ -385,24 +493,29 @@ function getCleanupResultClosedManagedSessionNames(result) {
|
|
|
385
493
|
const sessionName = typeof step.sessionName === "string"
|
|
386
494
|
? step.sessionName
|
|
387
495
|
: typeof record?.sessionName === "string" ? record.sessionName : undefined;
|
|
496
|
+
const namespace = typeof step.namespace === "string"
|
|
497
|
+
? step.namespace
|
|
498
|
+
: typeof record?.namespace === "string" ? record.namespace : fallbackNamespace;
|
|
388
499
|
if (sessionName)
|
|
389
|
-
|
|
500
|
+
identities.set(getSessionContextKey(sessionName, namespace) ?? sessionName, { namespace, sessionName });
|
|
390
501
|
}
|
|
391
|
-
return [...
|
|
502
|
+
return [...identities.values()];
|
|
392
503
|
}
|
|
393
|
-
function
|
|
394
|
-
const
|
|
504
|
+
function getCleanupResultsClosedManagedSessionIdentities(cleanupResults, fallbackNamespace) {
|
|
505
|
+
const identities = new Map();
|
|
395
506
|
for (const result of cleanupResults) {
|
|
396
|
-
for (const
|
|
397
|
-
|
|
507
|
+
for (const identity of getCleanupResultClosedManagedSessionIdentities(result, fallbackNamespace)) {
|
|
508
|
+
identities.set(getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName, identity);
|
|
509
|
+
}
|
|
398
510
|
}
|
|
399
|
-
return [...
|
|
511
|
+
return [...identities.values()];
|
|
400
512
|
}
|
|
401
513
|
function isElectronLaunchRecord(value) {
|
|
402
514
|
if (!isRecord(value))
|
|
403
515
|
return false;
|
|
404
516
|
return value.version === 1
|
|
405
517
|
&& value.launchedByWrapper === true
|
|
518
|
+
&& (value.namespace === undefined || typeof value.namespace === "string")
|
|
406
519
|
&& typeof value.launchId === "string"
|
|
407
520
|
&& typeof value.appName === "string"
|
|
408
521
|
&& typeof value.executablePath === "string"
|
|
@@ -460,18 +573,20 @@ function collectBranchManagedResourceEvents(branch) {
|
|
|
460
573
|
eventRank += 1;
|
|
461
574
|
const succeeded = getSuccessfulToolResult(details, message);
|
|
462
575
|
const args = Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string") ? details.args : [];
|
|
463
|
-
const command = typeof details.command === "string" ? details.command :
|
|
576
|
+
const command = typeof details.command === "string" ? details.command : extractUpstreamCommandTokens(args)[0];
|
|
464
577
|
const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
|
|
465
578
|
const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
|
|
466
579
|
const sessionMode = details.sessionMode === "fresh" || details.sessionMode === "auto" ? details.sessionMode : undefined;
|
|
467
580
|
const usedImplicitSession = details.usedImplicitSession === true;
|
|
468
581
|
const explicitSessionName = extractExplicitSessionName(args);
|
|
582
|
+
const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
|
|
583
|
+
const closeAllApplied = detailsReportCloseAllApplied(details, succeeded);
|
|
469
584
|
const outcome = getManagedSessionOutcome(details);
|
|
470
585
|
const outcomeSucceeded = outcome?.succeeded === true;
|
|
471
586
|
const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
|
|
472
587
|
const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
|
|
473
588
|
const outcomeAttemptedSessionName = typeof outcome?.attemptedSessionName === "string" ? outcome.attemptedSessionName : undefined;
|
|
474
|
-
if (
|
|
589
|
+
if (outcome?.activeAfter === true && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged")) {
|
|
475
590
|
setBranchManagedSessionActive(events, outcomeCurrentSessionName, namespace, eventRank);
|
|
476
591
|
}
|
|
477
592
|
if (outcomeSucceeded && outcomeStatus === "closed") {
|
|
@@ -487,6 +602,14 @@ function collectBranchManagedResourceEvents(branch) {
|
|
|
487
602
|
if (succeeded && isCloseCommand(command)) {
|
|
488
603
|
setBranchRankForString(events.managedSessionCloseRanks, getSessionContextKey(explicitSessionName ?? sessionName ?? outcomeAttemptedSessionName ?? outcomeCurrentSessionName, namespace), eventRank);
|
|
489
604
|
}
|
|
605
|
+
if (closeAllApplied) {
|
|
606
|
+
const retainedSessionKey = batchCloseLifecycle?.endsClosed === false ? getSessionContextKey(sessionName, namespace) : undefined;
|
|
607
|
+
for (const sessionKey of events.managedSessionActiveIdentities.keys()) {
|
|
608
|
+
if (sessionKey !== retainedSessionKey && isAgentBrowserSessionIdentityKeyInNamespace(sessionKey, namespace)) {
|
|
609
|
+
events.managedSessionCloseRanks.set(sessionKey, eventRank);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
490
613
|
const electron = isRecord(details.electron) ? details.electron : undefined;
|
|
491
614
|
const launch = electron && isElectronLaunchRecord(electron.launch) ? electron.launch : undefined;
|
|
492
615
|
if (launch && getActiveElectronRecords(new Map([[launch.launchId, launch]])).length > 0) {
|
|
@@ -503,8 +626,8 @@ function collectBranchManagedResourceEvents(branch) {
|
|
|
503
626
|
if (isRecord(cleanupResult) && isElectronLaunchRecord(cleanupResult.record)) {
|
|
504
627
|
events.electronLaunchCleanupRanks.set(cleanupResult.record.launchId, eventRank);
|
|
505
628
|
}
|
|
506
|
-
for (const
|
|
507
|
-
events.managedSessionCloseRanks.set(
|
|
629
|
+
for (const identity of getCleanupResultClosedManagedSessionIdentities(cleanupResult, namespace)) {
|
|
630
|
+
events.managedSessionCloseRanks.set(getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName, eventRank);
|
|
508
631
|
}
|
|
509
632
|
}
|
|
510
633
|
}
|
|
@@ -523,23 +646,25 @@ function getCleanupResultsPreservedUserDataDirs(cleanupResults) {
|
|
|
523
646
|
}
|
|
524
647
|
return [...userDataDirs];
|
|
525
648
|
}
|
|
526
|
-
function syncElectronCleanupManagedSessions(sessions, cleanupResults) {
|
|
527
|
-
for (const
|
|
528
|
-
untrackOwnedManagedSession(sessions, sessionName);
|
|
649
|
+
function syncElectronCleanupManagedSessions(sessions, cleanupResults, fallbackNamespace) {
|
|
650
|
+
for (const identity of getCleanupResultsClosedManagedSessionIdentities(cleanupResults, fallbackNamespace)) {
|
|
651
|
+
untrackOwnedManagedSession(sessions, identity.sessionName, identity.namespace);
|
|
529
652
|
}
|
|
530
653
|
}
|
|
531
|
-
async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessionName, timeoutMs, attachedSessionKeys, keepNamespace) {
|
|
654
|
+
async function closeOwnedManagedSessionsExcept(sessions, restoreState, keepSessionName, timeoutMs, attachedSessionKeys, keepNamespace, onClosed) {
|
|
532
655
|
const keepKey = getSessionContextKey(keepSessionName, keepNamespace);
|
|
533
656
|
for (const [key, owner] of [...sessions]) {
|
|
534
657
|
if (key === keepKey)
|
|
535
658
|
continue;
|
|
536
659
|
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)
|
|
660
|
+
if (!error) {
|
|
538
661
|
sessions.delete(key);
|
|
662
|
+
onClosed?.(owner);
|
|
663
|
+
}
|
|
539
664
|
}
|
|
540
665
|
}
|
|
541
|
-
async function closeOwnedManagedSessions(sessions, restoreState, timeoutMs, attachedSessionKeys) {
|
|
542
|
-
await closeOwnedManagedSessionsExcept(sessions, restoreState, undefined, timeoutMs, attachedSessionKeys);
|
|
666
|
+
async function closeOwnedManagedSessions(sessions, restoreState, timeoutMs, attachedSessionKeys, onClosed) {
|
|
667
|
+
await closeOwnedManagedSessionsExcept(sessions, restoreState, undefined, timeoutMs, attachedSessionKeys, undefined, onClosed);
|
|
543
668
|
}
|
|
544
669
|
function getOffBranchOwnedElectronLaunchRecords(ownedRecords, branchRecords) {
|
|
545
670
|
const activeBranchLaunchIds = new Set(getActiveElectronRecords(branchRecords).map((record) => record.launchId));
|
|
@@ -579,14 +704,19 @@ class AsyncExecutionQueue {
|
|
|
579
704
|
})();
|
|
580
705
|
}
|
|
581
706
|
}
|
|
582
|
-
class KeyedAsyncExecutionQueue {
|
|
707
|
+
export class KeyedAsyncExecutionQueue {
|
|
708
|
+
barriers = new Map();
|
|
583
709
|
entries = new Map();
|
|
584
|
-
async run(key, work) {
|
|
710
|
+
async run(key, namespace, work) {
|
|
585
711
|
const entry = this.entries.get(key) ?? { queue: new AsyncExecutionQueue(), users: 0 };
|
|
712
|
+
const barrier = this.barriers.get(getAgentBrowserSessionIdentityKey("", namespace)) ?? Promise.resolve();
|
|
586
713
|
entry.users += 1;
|
|
587
714
|
this.entries.set(key, entry);
|
|
588
715
|
try {
|
|
589
|
-
return await entry.queue.run(
|
|
716
|
+
return await entry.queue.run(async () => {
|
|
717
|
+
await barrier;
|
|
718
|
+
return await work();
|
|
719
|
+
});
|
|
590
720
|
}
|
|
591
721
|
finally {
|
|
592
722
|
entry.users -= 1;
|
|
@@ -594,6 +724,29 @@ class KeyedAsyncExecutionQueue {
|
|
|
594
724
|
this.entries.delete(key);
|
|
595
725
|
}
|
|
596
726
|
}
|
|
727
|
+
async runExclusive(namespace, work) {
|
|
728
|
+
const namespaceKey = getAgentBrowserSessionIdentityKey("", namespace);
|
|
729
|
+
const previous = this.barriers.get(namespaceKey) ?? Promise.resolve();
|
|
730
|
+
let release;
|
|
731
|
+
const blocked = new Promise((resolve) => {
|
|
732
|
+
release = resolve;
|
|
733
|
+
});
|
|
734
|
+
const barrier = previous.then(() => blocked);
|
|
735
|
+
this.barriers.set(namespaceKey, barrier);
|
|
736
|
+
const drains = [...this.entries]
|
|
737
|
+
.filter(([key]) => isAgentBrowserSessionIdentityKeyInNamespace(key, namespace))
|
|
738
|
+
.map(([, { queue }]) => queue.run(async () => undefined));
|
|
739
|
+
await previous;
|
|
740
|
+
await Promise.all(drains);
|
|
741
|
+
try {
|
|
742
|
+
return await work();
|
|
743
|
+
}
|
|
744
|
+
finally {
|
|
745
|
+
release();
|
|
746
|
+
if (this.barriers.get(namespaceKey) === barrier)
|
|
747
|
+
this.barriers.delete(namespaceKey);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
597
750
|
}
|
|
598
751
|
function mergeBrowserRunMap(current, initial, updated) {
|
|
599
752
|
if (updated === initial)
|
|
@@ -609,11 +762,19 @@ function mergeBrowserRunMap(current, initial, updated) {
|
|
|
609
762
|
}
|
|
610
763
|
return merged;
|
|
611
764
|
}
|
|
612
|
-
function mergeBrowserRunArtifactManifest(current, initial, updated) {
|
|
765
|
+
export function mergeBrowserRunArtifactManifest(current, initial, updated) {
|
|
613
766
|
if (!updated || updated === initial)
|
|
614
767
|
return current;
|
|
768
|
+
if (current === initial)
|
|
769
|
+
return updated;
|
|
615
770
|
const initialEntries = new Map((initial?.entries ?? []).map((entry) => [getSessionArtifactManifestEntryKey(entry), entry]));
|
|
616
|
-
const changedEntries = updated.entries
|
|
771
|
+
const changedEntries = updated.entries
|
|
772
|
+
.map((entry, index) => ({ entry, index }))
|
|
773
|
+
.filter(({ entry }) => initialEntries.get(getSessionArtifactManifestEntryKey(entry)) !== entry)
|
|
774
|
+
.sort((left, right) => left.entry.createdAtMs - right.entry.createdAtMs
|
|
775
|
+
|| Number(isPendingRecordingCommand(left.entry.command, left.entry.subcommand, left.entry.kind)) - Number(isPendingRecordingCommand(right.entry.command, right.entry.subcommand, right.entry.kind))
|
|
776
|
+
|| left.index - right.index)
|
|
777
|
+
.map(({ entry }) => entry);
|
|
617
778
|
return changedEntries.length === 0
|
|
618
779
|
? current
|
|
619
780
|
: mergeSessionArtifactManifest({
|
|
@@ -681,7 +842,9 @@ export default function agentBrowserExtension(pi) {
|
|
|
681
842
|
let sessionPageState = new SessionPageState();
|
|
682
843
|
let traceOwners = new Map();
|
|
683
844
|
let artifactManifest;
|
|
684
|
-
let
|
|
845
|
+
let activeRecordingReservations = new Map();
|
|
846
|
+
let recordingSessionTombstones = new Map();
|
|
847
|
+
let recordingSessionTombstonesToPersist = new Map();
|
|
685
848
|
let attachedSessionKeys = new Set();
|
|
686
849
|
let networkRoutesBySession = new Map();
|
|
687
850
|
let electronLaunchRecords = new Map();
|
|
@@ -691,18 +854,211 @@ export default function agentBrowserExtension(pi) {
|
|
|
691
854
|
const managedSessionRestoreState = new ManagedSessionRestoreState();
|
|
692
855
|
const ownedManagedSessions = new Map();
|
|
693
856
|
const managedSessionExecutionQueue = new AsyncExecutionQueue();
|
|
857
|
+
const artifactExecutionQueue = new AsyncExecutionQueue();
|
|
694
858
|
const callerOwnedSessionExecutionQueues = new KeyedAsyncExecutionQueue();
|
|
859
|
+
const activeScriptControllers = new Set();
|
|
860
|
+
const activeScriptExecutions = new Set();
|
|
695
861
|
let branchRestoreGeneration = 0;
|
|
696
862
|
let branchStateGeneration = 0;
|
|
863
|
+
const validatedUpstreamPathKeys = new Set();
|
|
864
|
+
const appendRecordingTransitions = (transitions) => {
|
|
865
|
+
for (const transition of transitions) {
|
|
866
|
+
const key = getAgentBrowserSessionIdentityKey(transition.reservation.sessionName, transition.reservation.namespace);
|
|
867
|
+
if (transition.state === "active") {
|
|
868
|
+
recordingSessionTombstones.delete(key);
|
|
869
|
+
recordingSessionTombstonesToPersist.delete(key);
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
recordingSessionTombstones.set(key, transition.reservation);
|
|
873
|
+
}
|
|
874
|
+
try {
|
|
875
|
+
appendRecordingReservationTransition(pi, transition);
|
|
876
|
+
recordingSessionTombstonesToPersist.delete(key);
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
if (transition.state === "closed")
|
|
880
|
+
recordingSessionTombstonesToPersist.set(key, transition.reservation);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
const appendActiveRecordingCleanupAction = (result, reservation) => {
|
|
885
|
+
if (result.isError !== true)
|
|
886
|
+
return result;
|
|
887
|
+
const details = isRecord(result.details) ? result.details : {};
|
|
888
|
+
const nextActions = Array.isArray(details.nextActions) ? [...details.nextActions] : [];
|
|
889
|
+
if (nextActions.some((action) => action.id === "stop-pending-recording"))
|
|
890
|
+
return result;
|
|
891
|
+
const stopActions = applyNamespaceToNextActions(applySessionToNextActions([
|
|
892
|
+
buildNextToolAction({
|
|
893
|
+
args: ["record", "stop"],
|
|
894
|
+
id: "stop-pending-recording",
|
|
895
|
+
reason: "Stop the active recording so the requested video can be finalized and verified on disk.",
|
|
896
|
+
safety: "The file remains pending until record stop succeeds; verify details.artifactVerification afterward.",
|
|
897
|
+
}),
|
|
898
|
+
], reservation.sessionName), reservation.namespace);
|
|
899
|
+
appendUniqueAgentBrowserNextActions(nextActions, stopActions);
|
|
900
|
+
const cleanupNotice = "An active recording remains open. Use the exact stop-pending-recording payload in details.nextActions before leaving this session.";
|
|
901
|
+
let noticeAppended = false;
|
|
902
|
+
const content = result.content.map((item) => {
|
|
903
|
+
if (noticeAppended || item.type !== "text")
|
|
904
|
+
return item;
|
|
905
|
+
noticeAppended = true;
|
|
906
|
+
return { ...item, text: `${item.text}\n\n${cleanupNotice}` };
|
|
907
|
+
});
|
|
908
|
+
if (!noticeAppended)
|
|
909
|
+
content.push({ type: "text", text: cleanupNotice });
|
|
910
|
+
return { ...result, content, details: { ...details, nextActions } };
|
|
911
|
+
};
|
|
912
|
+
const retireRecordingSession = (sessionName, namespace, retireManifest = true) => {
|
|
913
|
+
const reservation = retireRecordingReservation(activeRecordingReservations, sessionName, namespace);
|
|
914
|
+
const previousManifest = artifactManifest;
|
|
915
|
+
if (retireManifest && artifactManifest)
|
|
916
|
+
artifactManifest = retirePendingRecordingManifestEntries(artifactManifest, sessionName, namespace);
|
|
917
|
+
if (!reservation && artifactManifest === previousManifest)
|
|
918
|
+
return;
|
|
919
|
+
const terminalReservation = reservation ?? { absolutePath: "", cwd: managedSessionCwd, namespace, path: "", sessionName };
|
|
920
|
+
const terminalKey = getAgentBrowserSessionIdentityKey(sessionName, namespace);
|
|
921
|
+
recordingSessionTombstones.set(terminalKey, terminalReservation);
|
|
922
|
+
try {
|
|
923
|
+
appendRecordingReservationTransition(pi, {
|
|
924
|
+
reservation: terminalReservation,
|
|
925
|
+
state: "closed",
|
|
926
|
+
});
|
|
927
|
+
recordingSessionTombstonesToPersist.delete(terminalKey);
|
|
928
|
+
}
|
|
929
|
+
catch {
|
|
930
|
+
recordingSessionTombstonesToPersist.set(terminalKey, terminalReservation);
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
const syncRecordingReservationsFromResult = (result) => {
|
|
934
|
+
const handledClosedSessionKeys = new Set();
|
|
935
|
+
const details = isRecord(result.details) ? result.details : undefined;
|
|
936
|
+
const batchSteps = Array.isArray(details?.batchSteps) ? details.batchSteps : undefined;
|
|
937
|
+
const resultSessionName = typeof details?.sessionName === "string" ? details.sessionName : undefined;
|
|
938
|
+
const resultNamespace = typeof details?.namespace === "string" ? details.namespace : undefined;
|
|
939
|
+
if (!batchSteps) {
|
|
940
|
+
appendRecordingTransitions(applyRecordingArtifactsToReservations(activeRecordingReservations, getResultFileArtifacts(result)));
|
|
941
|
+
return handledClosedSessionKeys;
|
|
942
|
+
}
|
|
943
|
+
let sessionClosed = false;
|
|
944
|
+
for (const step of batchSteps) {
|
|
945
|
+
if (!isRecord(step))
|
|
946
|
+
continue;
|
|
947
|
+
const command = Array.isArray(step.command) && step.command.every((token) => typeof token === "string") ? step.command : undefined;
|
|
948
|
+
const commandTokens = command ? extractUpstreamCommandTokens(command) : [];
|
|
949
|
+
const commandName = commandTokens[0];
|
|
950
|
+
if (resultSessionName && batchStepReportsNoRecordingInProgress(step)) {
|
|
951
|
+
const sessionKey = getAgentBrowserSessionIdentityKey(resultSessionName, resultNamespace);
|
|
952
|
+
retireRecordingSession(resultSessionName, resultNamespace, false);
|
|
953
|
+
handledClosedSessionKeys.add(sessionKey);
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
if (step.success === true && commandName && isCloseCommand(commandName) && resultSessionName) {
|
|
957
|
+
const sessionKey = getAgentBrowserSessionIdentityKey(resultSessionName, resultNamespace);
|
|
958
|
+
retireRecordingSession(resultSessionName, resultNamespace, false);
|
|
959
|
+
handledClosedSessionKeys.add(sessionKey);
|
|
960
|
+
sessionClosed = true;
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
if (sessionClosed && commandName === "record")
|
|
964
|
+
continue;
|
|
965
|
+
if (step.success === true && sessionClosed)
|
|
966
|
+
sessionClosed = false;
|
|
967
|
+
const artifacts = Array.isArray(step.artifacts) ? step.artifacts.filter(isResultFileArtifact) : [];
|
|
968
|
+
appendRecordingTransitions(applyRecordingArtifactsToReservations(activeRecordingReservations, artifacts));
|
|
969
|
+
}
|
|
970
|
+
for (const sessionKey of handledClosedSessionKeys) {
|
|
971
|
+
if (!activeRecordingReservations.has(sessionKey) && artifactManifest && resultSessionName) {
|
|
972
|
+
artifactManifest = retirePendingRecordingManifestEntries(artifactManifest, resultSessionName, resultNamespace);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return handledClosedSessionKeys;
|
|
976
|
+
};
|
|
977
|
+
const validateUpstreamVersion = async (cwd, signal) => {
|
|
978
|
+
const processEnvironment = getAgentBrowserProcessEnvironment();
|
|
979
|
+
const pathKey = `${cwd}\0${processEnvironment.PATH ?? processEnvironment.Path ?? ""}`;
|
|
980
|
+
if (validatedUpstreamPathKeys.has(pathKey))
|
|
981
|
+
return undefined;
|
|
982
|
+
const probe = await runAgentBrowserProcess({ args: ["--version"], cwd, signal, timeoutMs: 5_000 });
|
|
983
|
+
if (probe.spawnError?.code === "ENOENT" || probe.exitCode === 127 || probe.aborted)
|
|
984
|
+
return undefined;
|
|
985
|
+
let error;
|
|
986
|
+
let observedVersion;
|
|
987
|
+
if (probe.spawnError || probe.exitCode !== 0) {
|
|
988
|
+
const detail = redactSensitiveText(probe.spawnError?.message ?? (probe.stderr.trim() || `exit ${probe.exitCode}`));
|
|
989
|
+
error = `agent-browser --version could not be validated (${detail}). Run pi-agent-browser-doctor before browser-backed calls.`;
|
|
990
|
+
}
|
|
991
|
+
else {
|
|
992
|
+
observedVersion = parseAgentBrowserVersionOutput(probe.stdout);
|
|
993
|
+
error = getAgentBrowserVersionValidationError(probe.stdout);
|
|
994
|
+
}
|
|
995
|
+
if (!error) {
|
|
996
|
+
validatedUpstreamPathKeys.add(pathKey);
|
|
997
|
+
return undefined;
|
|
998
|
+
}
|
|
999
|
+
return {
|
|
1000
|
+
content: [{ type: "text", text: error }],
|
|
1001
|
+
details: {
|
|
1002
|
+
expectedVersion: TARGET_AGENT_BROWSER_VERSION,
|
|
1003
|
+
failureCategory: "validation-error",
|
|
1004
|
+
observedVersion,
|
|
1005
|
+
resultCategory: "failure",
|
|
1006
|
+
minimumSupportedVersion: MINIMUM_AGENT_BROWSER_VERSION,
|
|
1007
|
+
versionValidation: { expected: SUPPORTED_AGENT_BROWSER_VERSION_LABEL, observed: observedVersion },
|
|
1008
|
+
},
|
|
1009
|
+
isError: true,
|
|
1010
|
+
};
|
|
1011
|
+
};
|
|
697
1012
|
const clearSessionScopedBrowserState = (sessionName, namespace) => {
|
|
698
1013
|
const key = getSessionContextKey(sessionName, namespace) ?? sessionName;
|
|
699
|
-
allowedDomainsBySession = new Map(allowedDomainsBySession);
|
|
700
|
-
allowedDomainsBySession.delete(key);
|
|
701
1014
|
attachedSessionKeys.delete(key);
|
|
702
1015
|
networkRoutesBySession = new Map(networkRoutesBySession);
|
|
703
1016
|
networkRoutesBySession.delete(key);
|
|
1017
|
+
traceOwners.delete(key);
|
|
704
1018
|
sessionPageState.clearSession(key);
|
|
705
1019
|
};
|
|
1020
|
+
const closeScriptSessionLeaseWithinQueue = async (sessionName, cwd) => {
|
|
1021
|
+
const closeError = await withIsolatedAgentBrowserEnvironment(() => closeManagedSession({
|
|
1022
|
+
cwd,
|
|
1023
|
+
namespace: AGENT_BROWSER_SCRIPT_NAMESPACE,
|
|
1024
|
+
restoreState: managedSessionRestoreState,
|
|
1025
|
+
sessionName,
|
|
1026
|
+
timeoutMs: implicitSessionCloseTimeoutMs,
|
|
1027
|
+
}));
|
|
1028
|
+
if (closeError) {
|
|
1029
|
+
try {
|
|
1030
|
+
appendScriptSessionLease(pi, sessionName, "failed");
|
|
1031
|
+
}
|
|
1032
|
+
catch { }
|
|
1033
|
+
return redactSensitiveText(closeError);
|
|
1034
|
+
}
|
|
1035
|
+
try {
|
|
1036
|
+
appendScriptSessionLease(pi, sessionName, "closed");
|
|
1037
|
+
}
|
|
1038
|
+
catch {
|
|
1039
|
+
managedSessionRestoreState.disable(sessionName);
|
|
1040
|
+
return "The isolated session closed, but its durable cleanup record could not be saved.";
|
|
1041
|
+
}
|
|
1042
|
+
untrackOwnedManagedSession(ownedManagedSessions, sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
|
|
1043
|
+
managedSessionRestoreState.clear(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
|
|
1044
|
+
retireRecordingSession(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
|
|
1045
|
+
clearSessionScopedBrowserState(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
|
|
1046
|
+
return undefined;
|
|
1047
|
+
};
|
|
1048
|
+
const recoverScriptSessionLeasesWithinQueue = async (ctx) => {
|
|
1049
|
+
const pendingSessionNames = new Set([...ownedManagedSessions.values()]
|
|
1050
|
+
.map((session) => session.sessionName)
|
|
1051
|
+
.filter(isAgentBrowserScriptSessionName));
|
|
1052
|
+
for (const lease of getScriptSessionLeasesFromBranch(ctx.sessionManager.getBranch()).values()) {
|
|
1053
|
+
if (lease.cleanup !== "closed")
|
|
1054
|
+
pendingSessionNames.add(lease.sessionName);
|
|
1055
|
+
}
|
|
1056
|
+
for (const sessionName of pendingSessionNames) {
|
|
1057
|
+
trackOwnedManagedSession(ownedManagedSessions, sessionName, ctx.cwd, { branchOwned: true, namespace: AGENT_BROWSER_SCRIPT_NAMESPACE });
|
|
1058
|
+
managedSessionRestoreState.disable(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
|
|
1059
|
+
await closeScriptSessionLeaseWithinQueue(sessionName, ctx.cwd);
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
706
1062
|
const restoreBranchBackedState = (ctx, options) => {
|
|
707
1063
|
branchRestoreGeneration += 1;
|
|
708
1064
|
branchStateGeneration += 1;
|
|
@@ -750,13 +1106,28 @@ export default function agentBrowserExtension(pi) {
|
|
|
750
1106
|
sessionPageState = SessionPageState.fromBranch(branch);
|
|
751
1107
|
traceOwners = new Map();
|
|
752
1108
|
artifactManifest = restoreArtifactManifestFromBranch(branch);
|
|
753
|
-
|
|
1109
|
+
const restoredRecordingState = restoreRecordingReservationStateFromBranch(branch);
|
|
1110
|
+
for (const [key, reservation] of recordingSessionTombstones) {
|
|
1111
|
+
if (restoredRecordingState.terminal.has(key))
|
|
1112
|
+
recordingSessionTombstonesToPersist.delete(key);
|
|
1113
|
+
else
|
|
1114
|
+
recordingSessionTombstonesToPersist.set(key, reservation);
|
|
1115
|
+
}
|
|
1116
|
+
for (const [key, reservation] of restoredRecordingState.terminal) {
|
|
1117
|
+
if (!activeRecordingReservations.has(key))
|
|
1118
|
+
recordingSessionTombstones.set(key, reservation);
|
|
1119
|
+
}
|
|
1120
|
+
for (const key of recordingSessionTombstones.keys())
|
|
1121
|
+
restoredRecordingState.active.delete(key);
|
|
1122
|
+
for (const [key, reservation] of activeRecordingReservations)
|
|
1123
|
+
restoredRecordingState.active.set(key, reservation);
|
|
1124
|
+
activeRecordingReservations = restoredRecordingState.active;
|
|
754
1125
|
attachedSessionKeys = restoreAttachedSessionKeysFromBranch(branch);
|
|
755
1126
|
networkRoutesBySession = new Map();
|
|
756
1127
|
electronLaunchRecords = restoreElectronLaunchRecordsFromBranch(branch);
|
|
757
1128
|
for (const record of getActiveElectronRecords(electronLaunchRecords)) {
|
|
758
1129
|
if (record.sessionName)
|
|
759
|
-
attachedSessionKeys.add(getSessionContextKey(record.sessionName) ?? record.sessionName);
|
|
1130
|
+
attachedSessionKeys.add(getSessionContextKey(record.sessionName, record.namespace) ?? record.sessionName);
|
|
760
1131
|
}
|
|
761
1132
|
if (options.resetRuntimeOwnership) {
|
|
762
1133
|
ownedManagedSessions.clear();
|
|
@@ -778,6 +1149,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
778
1149
|
continue;
|
|
779
1150
|
trackOwnedManagedSession(ownedManagedSessions, identity.sessionName, ctx.cwd, {
|
|
780
1151
|
branchOwned: true,
|
|
1152
|
+
compatibilityWorkaround: restoreManagedSessionCompatibilityWorkaroundFromBranch(branch, identity.sessionName, identity.namespace),
|
|
781
1153
|
headedManagedAutosaveDisabled: restoreManagedSessionHeadedAutosaveDisabledFromBranch(branch, identity.sessionName, identity.namespace),
|
|
782
1154
|
headedManagedAutosaveInterval: restoreManagedSessionHeadedAutosaveIntervalFromBranch(branch, identity.sessionName, identity.namespace),
|
|
783
1155
|
namespace: identity.namespace,
|
|
@@ -786,6 +1158,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
786
1158
|
if (restoredState.active) {
|
|
787
1159
|
trackOwnedManagedSession(ownedManagedSessions, restoredState.sessionName, ctx.cwd, {
|
|
788
1160
|
branchOwned: true,
|
|
1161
|
+
compatibilityWorkaround: managedSessionCompatibilityWorkaround,
|
|
789
1162
|
headedManagedAutosaveDisabled: managedSessionHeadedAutosaveDisabled,
|
|
790
1163
|
headedManagedAutosaveInterval: managedSessionHeadedAutosaveInterval,
|
|
791
1164
|
namespace: restoredState.namespace,
|
|
@@ -841,17 +1214,25 @@ export default function agentBrowserExtension(pi) {
|
|
|
841
1214
|
cwd: ctx.cwd,
|
|
842
1215
|
includeProjectConfig: shouldIncludeProjectConfig(ctx),
|
|
843
1216
|
}));
|
|
1217
|
+
await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(() => recoverScriptSessionLeasesWithinQueue(ctx)));
|
|
844
1218
|
});
|
|
845
1219
|
pi.on("session_tree", async (_event, ctx) => {
|
|
846
|
-
|
|
1220
|
+
for (const controller of activeScriptControllers)
|
|
1221
|
+
controller.abort();
|
|
1222
|
+
await Promise.allSettled([...activeScriptExecutions]);
|
|
1223
|
+
await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(async () => {
|
|
847
1224
|
restoreBranchBackedState(ctx, { resetRuntimeOwnership: false });
|
|
848
|
-
|
|
1225
|
+
await recoverScriptSessionLeasesWithinQueue(ctx);
|
|
1226
|
+
}));
|
|
849
1227
|
});
|
|
850
1228
|
pi.on("session_shutdown", async (event, ctx) => {
|
|
1229
|
+
for (const controller of activeScriptControllers)
|
|
1230
|
+
controller.abort();
|
|
1231
|
+
await Promise.allSettled([...activeScriptExecutions]);
|
|
851
1232
|
branchRestoreGeneration += 1;
|
|
852
1233
|
branchStateGeneration += 1;
|
|
853
1234
|
let preservedElectronProfileDirs = [];
|
|
854
|
-
await managedSessionExecutionQueue.run(async () => {
|
|
1235
|
+
await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(async () => {
|
|
855
1236
|
const shutdownCwd = ctx?.cwd ?? managedSessionCwd;
|
|
856
1237
|
const quitting = event?.reason === "quit";
|
|
857
1238
|
preservedElectronProfileDirs = quitting
|
|
@@ -874,22 +1255,38 @@ export default function agentBrowserExtension(pi) {
|
|
|
874
1255
|
...getCleanupResultsPreservedUserDataDirs(electronCleanupResults),
|
|
875
1256
|
])];
|
|
876
1257
|
syncElectronCleanupManagedSessions(ownedManagedSessions, electronCleanupResults);
|
|
1258
|
+
for (const identity of getCleanupResultsClosedManagedSessionIdentities(electronCleanupResults))
|
|
1259
|
+
retireRecordingSession(identity.sessionName, identity.namespace);
|
|
877
1260
|
if (quitting) {
|
|
878
|
-
await closeOwnedManagedSessions(ownedManagedSessions, managedSessionRestoreState, implicitSessionCloseTimeoutMs, attachedSessionKeys);
|
|
1261
|
+
await closeOwnedManagedSessions(ownedManagedSessions, managedSessionRestoreState, implicitSessionCloseTimeoutMs, attachedSessionKeys, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
|
|
879
1262
|
}
|
|
880
1263
|
else {
|
|
881
|
-
await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, managedSessionActive ? managedSessionNamespace : undefined);
|
|
1264
|
+
await closeOwnedManagedSessionsExcept(ownedManagedSessions, managedSessionRestoreState, managedSessionActive ? managedSessionName : undefined, implicitSessionCloseTimeoutMs, attachedSessionKeys, managedSessionActive ? managedSessionNamespace : undefined, (owner) => retireRecordingSession(owner.sessionName, owner.namespace));
|
|
882
1265
|
}
|
|
883
|
-
});
|
|
1266
|
+
}));
|
|
884
1267
|
managedSessionActive = false;
|
|
885
1268
|
managedSessionCompatibilityWorkaround = undefined;
|
|
886
1269
|
managedSessionHeadedAutosaveDisabled = false;
|
|
887
1270
|
managedSessionHeadedAutosaveInterval = undefined;
|
|
888
1271
|
managedSessionNamespace = undefined;
|
|
1272
|
+
for (const reservation of recordingSessionTombstonesToPersist.values()) {
|
|
1273
|
+
try {
|
|
1274
|
+
appendRecordingReservationTransition(pi, { reservation, state: "closed" });
|
|
1275
|
+
}
|
|
1276
|
+
catch { }
|
|
1277
|
+
}
|
|
1278
|
+
for (const reservation of activeRecordingReservations.values()) {
|
|
1279
|
+
try {
|
|
1280
|
+
appendRecordingReservationTransition(pi, { reservation, state: "active" });
|
|
1281
|
+
}
|
|
1282
|
+
catch { }
|
|
1283
|
+
}
|
|
889
1284
|
sessionPageState.reset();
|
|
890
1285
|
traceOwners = new Map();
|
|
891
1286
|
artifactManifest = undefined;
|
|
892
|
-
|
|
1287
|
+
activeRecordingReservations = new Map();
|
|
1288
|
+
recordingSessionTombstones = new Map();
|
|
1289
|
+
recordingSessionTombstonesToPersist = new Map();
|
|
893
1290
|
attachedSessionKeys = new Set();
|
|
894
1291
|
networkRoutesBySession = new Map();
|
|
895
1292
|
electronLaunchRecords = new Map();
|
|
@@ -897,7 +1294,6 @@ export default function agentBrowserExtension(pi) {
|
|
|
897
1294
|
branchOwnedElectronLaunchIds = new Set();
|
|
898
1295
|
electronChildProcesses = new Map();
|
|
899
1296
|
ownedManagedSessions.clear();
|
|
900
|
-
cleanupManagedSessionRestoreConfig();
|
|
901
1297
|
await cleanupSecureTempArtifacts({ preservePaths: preservedElectronProfileDirs });
|
|
902
1298
|
});
|
|
903
1299
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
@@ -937,16 +1333,16 @@ export default function agentBrowserExtension(pi) {
|
|
|
937
1333
|
}
|
|
938
1334
|
});
|
|
939
1335
|
pi.on("tool_result", async (event) => buildAgentBrowserToolResultPatch(event));
|
|
940
|
-
|
|
1336
|
+
const agentBrowserTool = {
|
|
941
1337
|
name: "agent_browser",
|
|
942
1338
|
label: "Agent Browser",
|
|
943
|
-
description: "Browse and interact with websites using agent-browser. Use this for
|
|
1339
|
+
description: "Browse and interact with websites using agent-browser. Use this for reading live pages, opening known URLs, 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
1340
|
promptSnippet: "Browse websites, read live docs, click and fill pages, extract browser content, take screenshots, and automate real web workflows.",
|
|
945
1341
|
promptGuidelines: toolPromptGuidelines,
|
|
946
1342
|
parameters: AGENT_BROWSER_PARAMS,
|
|
947
1343
|
renderCall(args, theme, context) {
|
|
948
1344
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
949
|
-
text.setText(formatAgentBrowserRenderCall(args, theme));
|
|
1345
|
+
text.setText(formatAgentBrowserRenderCall(args, theme, context.expanded));
|
|
950
1346
|
return text;
|
|
951
1347
|
},
|
|
952
1348
|
renderResult(result, options, theme, context) {
|
|
@@ -956,20 +1352,138 @@ export default function agentBrowserExtension(pi) {
|
|
|
956
1352
|
component.setState(formatAgentBrowserRenderResult(result, options, theme, context.isError), options.expanded, theme);
|
|
957
1353
|
return component;
|
|
958
1354
|
},
|
|
959
|
-
async execute(
|
|
1355
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
960
1356
|
const promptPolicy = buildPromptPolicy(getLatestUserPrompt(ctx.sessionManager.getBranch()));
|
|
961
1357
|
const outputPath = isRecord(params) && typeof params.outputPath === "string" ? params.outputPath : undefined;
|
|
962
1358
|
const resolvedInput = resolveAgentBrowserInput({
|
|
963
|
-
getBatchPreflightValidationError,
|
|
1359
|
+
getBatchPreflightValidationError: (args, stdin) => getArtifactPreflightValidationError({ args, cwd: ctx.cwd, outputPath, stdin }),
|
|
964
1360
|
managedSessionActive,
|
|
965
1361
|
params,
|
|
966
1362
|
});
|
|
967
1363
|
if (resolvedInput.status === "invalid") {
|
|
968
1364
|
return buildValidationFailureResult(resolvedInput);
|
|
969
1365
|
}
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
|
|
1366
|
+
const applyUnserializedOutputPath = async (result, preserveTextContent = false) => {
|
|
1367
|
+
if (!outputPath || result.isError === true || (isRecord(result.details) && result.details.resultCategory === "failure"))
|
|
1368
|
+
return result;
|
|
1369
|
+
return artifactExecutionQueue.run(async () => {
|
|
1370
|
+
const reservationError = getArtifactPreflightValidationError({
|
|
1371
|
+
activeRecordingReservations: activeRecordingReservations.values(),
|
|
1372
|
+
args: [],
|
|
1373
|
+
cwd: ctx.cwd,
|
|
1374
|
+
outputPath,
|
|
1375
|
+
});
|
|
1376
|
+
if (reservationError) {
|
|
1377
|
+
return buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: reservationError });
|
|
1378
|
+
}
|
|
1379
|
+
return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent, result });
|
|
1380
|
+
});
|
|
1381
|
+
};
|
|
1382
|
+
const versionCheckCommand = extractUpstreamCommandTokens(resolvedInput.toolArgs)[0];
|
|
1383
|
+
const electronHostOnlyAction = resolvedInput.kind === "electron" && ["cleanup", "list", "status"].includes(resolvedInput.compiledElectron.action);
|
|
1384
|
+
const browserBackedVersionCheck = needsManagedSession(parseArgvDescriptor(resolvedInput.toolArgs));
|
|
1385
|
+
if (resolvedInput.kind !== "script" && !electronHostOnlyAction && browserBackedVersionCheck && !isPlainTextInspectionArgs(resolvedInput.toolArgs) && !isCloseCommand(versionCheckCommand) && signal?.aborted !== true) {
|
|
1386
|
+
const versionFailure = await validateUpstreamVersion(ctx.cwd, signal);
|
|
1387
|
+
if (versionFailure)
|
|
1388
|
+
return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result: versionFailure });
|
|
1389
|
+
}
|
|
1390
|
+
if (resolvedInput.kind === "script") {
|
|
1391
|
+
if (!ctx.sessionManager.getSessionFile()) {
|
|
1392
|
+
return buildValidationFailureResult({
|
|
1393
|
+
attemptedKind: "script",
|
|
1394
|
+
kind: "invalid",
|
|
1395
|
+
redactedArgs: [],
|
|
1396
|
+
status: "invalid",
|
|
1397
|
+
toolArgs: [],
|
|
1398
|
+
validationError: "script requires a persisted Pi session so its isolated browser-session cleanup lease survives restart; relaunch Pi without --no-session.",
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
const sessionName = createAgentBrowserScriptSessionName();
|
|
1402
|
+
const innerResults = [];
|
|
1403
|
+
const scriptTimeoutMs = params.timeoutMs ?? AGENT_BROWSER_SCRIPT_DEFAULT_TIMEOUT_MS;
|
|
1404
|
+
const deadline = Date.now() + scriptTimeoutMs;
|
|
1405
|
+
let leased = false;
|
|
1406
|
+
let cleanupError;
|
|
1407
|
+
let run = {
|
|
1408
|
+
callCount: 0,
|
|
1409
|
+
emitCount: 0,
|
|
1410
|
+
error: "Script sandbox execution failed.",
|
|
1411
|
+
failureCategory: "upstream-error",
|
|
1412
|
+
ok: false,
|
|
1413
|
+
rejectedCallCount: 0,
|
|
1414
|
+
steps: [],
|
|
1415
|
+
};
|
|
1416
|
+
const scriptController = new AbortController();
|
|
1417
|
+
const abortScript = () => scriptController.abort();
|
|
1418
|
+
signal?.addEventListener("abort", abortScript, { once: true });
|
|
1419
|
+
if (signal?.aborted)
|
|
1420
|
+
scriptController.abort();
|
|
1421
|
+
activeScriptControllers.add(scriptController);
|
|
1422
|
+
let finishScriptExecution;
|
|
1423
|
+
const scriptExecution = new Promise((resolve) => {
|
|
1424
|
+
finishScriptExecution = resolve;
|
|
1425
|
+
});
|
|
1426
|
+
activeScriptExecutions.add(scriptExecution);
|
|
1427
|
+
try {
|
|
1428
|
+
// Keep preflight inside shutdown tracking so quit cannot race into starting the sandbox afterward.
|
|
1429
|
+
const versionFailure = await withIsolatedAgentBrowserEnvironment(() => validateUpstreamVersion(ctx.cwd, scriptController.signal));
|
|
1430
|
+
if (versionFailure)
|
|
1431
|
+
return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result: versionFailure });
|
|
1432
|
+
const pendingRun = runAgentBrowserScript({
|
|
1433
|
+
beforeFirstCall() {
|
|
1434
|
+
appendScriptSessionLease(pi, sessionName, "active");
|
|
1435
|
+
trackOwnedManagedSession(ownedManagedSessions, sessionName, ctx.cwd, { namespace: AGENT_BROWSER_SCRIPT_NAMESPACE });
|
|
1436
|
+
managedSessionRestoreState.disable(sessionName, AGENT_BROWSER_SCRIPT_NAMESPACE);
|
|
1437
|
+
leased = true;
|
|
1438
|
+
},
|
|
1439
|
+
code: resolvedInput.compiledScript.code,
|
|
1440
|
+
dispatch: async (innerParams, innerSignal) => {
|
|
1441
|
+
const remainingMs = Math.max(1, deadline - Date.now());
|
|
1442
|
+
const innerTimeoutMs = Math.min(innerParams.timeoutMs ?? remainingMs, remainingMs);
|
|
1443
|
+
const innerResult = await withIsolatedAgentBrowserEnvironment(() => agentBrowserTool.execute(`${toolCallId}:script:${innerResults.length + 1}`, {
|
|
1444
|
+
args: ["--namespace", AGENT_BROWSER_SCRIPT_NAMESPACE, "--session", sessionName, ...innerParams.args],
|
|
1445
|
+
stdin: innerParams.stdin,
|
|
1446
|
+
timeoutMs: innerTimeoutMs,
|
|
1447
|
+
}, innerSignal, undefined, ctx));
|
|
1448
|
+
innerResults.push(innerResult);
|
|
1449
|
+
return await buildScriptBrowserEnvelope(innerResult, innerParams.args, sessionName);
|
|
1450
|
+
},
|
|
1451
|
+
signal: scriptController.signal,
|
|
1452
|
+
timeoutMs: scriptTimeoutMs,
|
|
1453
|
+
});
|
|
1454
|
+
run = await pendingRun;
|
|
1455
|
+
}
|
|
1456
|
+
catch { }
|
|
1457
|
+
finally {
|
|
1458
|
+
activeScriptControllers.delete(scriptController);
|
|
1459
|
+
signal?.removeEventListener("abort", abortScript);
|
|
1460
|
+
if (leased) {
|
|
1461
|
+
try {
|
|
1462
|
+
cleanupError = await artifactExecutionQueue.run(() => managedSessionExecutionQueue.run(() => closeScriptSessionLeaseWithinQueue(sessionName, ctx.cwd)));
|
|
1463
|
+
}
|
|
1464
|
+
catch {
|
|
1465
|
+
cleanupError = "The isolated script session cleanup operation failed.";
|
|
1466
|
+
try {
|
|
1467
|
+
appendScriptSessionLease(pi, sessionName, "failed");
|
|
1468
|
+
}
|
|
1469
|
+
catch { }
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
activeScriptExecutions.delete(scriptExecution);
|
|
1473
|
+
finishScriptExecution();
|
|
1474
|
+
}
|
|
1475
|
+
let scriptResult = buildScriptToolResult({ cleanupError, innerResults, run, sessionName: leased ? sessionName : undefined });
|
|
1476
|
+
if (artifactManifest) {
|
|
1477
|
+
scriptResult = {
|
|
1478
|
+
...scriptResult,
|
|
1479
|
+
details: {
|
|
1480
|
+
...(isRecord(scriptResult.details) ? scriptResult.details : {}),
|
|
1481
|
+
artifactManifest,
|
|
1482
|
+
artifactRetentionSummary: formatSessionArtifactRetentionSummary(artifactManifest),
|
|
1483
|
+
},
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
return applyUnserializedOutputPath(scriptResult);
|
|
973
1487
|
}
|
|
974
1488
|
const { toolArgs } = resolvedInput;
|
|
975
1489
|
const compiledElectron = resolvedInput.kind === "electron" ? resolvedInput.compiledElectron : undefined;
|
|
@@ -980,7 +1494,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
980
1494
|
compiledElectron,
|
|
981
1495
|
ownedRecords: ownedElectronLaunchRecords,
|
|
982
1496
|
});
|
|
983
|
-
|
|
1497
|
+
let electronHostResult = await handleElectronHostInput({
|
|
984
1498
|
attachedSessionKeys,
|
|
985
1499
|
compiledElectron,
|
|
986
1500
|
cwd: ctx.cwd,
|
|
@@ -1012,11 +1526,16 @@ export default function agentBrowserExtension(pi) {
|
|
|
1012
1526
|
}
|
|
1013
1527
|
replaceWithActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronHostLaunchRecords, branchOwnedElectronLaunchIds, cleanedLaunchIds);
|
|
1014
1528
|
mergeElectronCleanupRecords(electronLaunchRecords, cleanupRecords);
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1529
|
+
const cleanupNamespace = isRecord(electronHostResult.details) && typeof electronHostResult.details.namespace === "string"
|
|
1530
|
+
? electronHostResult.details.namespace
|
|
1531
|
+
: undefined;
|
|
1532
|
+
const closedSessionIdentities = getCleanupResultsClosedManagedSessionIdentities(cleanupRecords, cleanupNamespace);
|
|
1533
|
+
syncElectronCleanupManagedSessions(ownedManagedSessions, cleanupRecords, cleanupNamespace);
|
|
1534
|
+
for (const identity of closedSessionIdentities) {
|
|
1535
|
+
retireRecordingSession(identity.sessionName, identity.namespace);
|
|
1536
|
+
const closedSessionKey = getSessionContextKey(identity.sessionName, identity.namespace) ?? identity.sessionName;
|
|
1537
|
+
clearSessionScopedBrowserState(closedSessionKey);
|
|
1538
|
+
if (closedSessionKey === (getSessionContextKey(managedSessionName, managedSessionNamespace) ?? managedSessionName)) {
|
|
1020
1539
|
managedSessionActive = false;
|
|
1021
1540
|
managedSessionCompatibilityWorkaround = undefined;
|
|
1022
1541
|
managedSessionHeadedAutosaveDisabled = false;
|
|
@@ -1026,14 +1545,39 @@ export default function agentBrowserExtension(pi) {
|
|
|
1026
1545
|
managedSessionName = createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal);
|
|
1027
1546
|
}
|
|
1028
1547
|
}
|
|
1548
|
+
if (artifactManifest) {
|
|
1549
|
+
electronHostResult = {
|
|
1550
|
+
...electronHostResult,
|
|
1551
|
+
details: {
|
|
1552
|
+
...(isRecord(electronHostResult.details) ? electronHostResult.details : {}),
|
|
1553
|
+
artifactManifest,
|
|
1554
|
+
artifactRetentionSummary: formatSessionArtifactRetentionSummary(artifactManifest),
|
|
1555
|
+
},
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1029
1558
|
}
|
|
1030
1559
|
return electronHostResult;
|
|
1031
1560
|
};
|
|
1032
|
-
const
|
|
1033
|
-
?
|
|
1034
|
-
:
|
|
1561
|
+
const runSerializedElectronHostInput = () => shouldSerializeElectronHostInput(compiledElectron)
|
|
1562
|
+
? managedSessionExecutionQueue.run(runElectronHostInput)
|
|
1563
|
+
: runElectronHostInput();
|
|
1564
|
+
const electronHostResult = compiledElectron?.action === "cleanup"
|
|
1565
|
+
? await artifactExecutionQueue.run(async () => {
|
|
1566
|
+
const reservationError = outputPath ? getArtifactPreflightValidationError({
|
|
1567
|
+
activeRecordingReservations: activeRecordingReservations.values(),
|
|
1568
|
+
args: [],
|
|
1569
|
+
cwd: ctx.cwd,
|
|
1570
|
+
outputPath,
|
|
1571
|
+
}) : undefined;
|
|
1572
|
+
if (reservationError) {
|
|
1573
|
+
return buildValidationFailureResult({ attemptedKind: resolvedInput.kind, kind: "invalid", redactedArgs: resolvedInput.redactedArgs, status: "invalid", toolArgs: resolvedInput.toolArgs, toolStdin: resolvedInput.toolStdin, validationError: reservationError });
|
|
1574
|
+
}
|
|
1575
|
+
const result = await runSerializedElectronHostInput();
|
|
1576
|
+
return result ? applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, result }) : result;
|
|
1577
|
+
})
|
|
1578
|
+
: await runSerializedElectronHostInput();
|
|
1035
1579
|
if (electronHostResult) {
|
|
1036
|
-
return
|
|
1580
|
+
return compiledElectron?.action === "cleanup" ? electronHostResult : applyUnserializedOutputPath(electronHostResult);
|
|
1037
1581
|
}
|
|
1038
1582
|
const explicitSessionName = extractExplicitSessionName(toolArgs);
|
|
1039
1583
|
const explicitNamespace = extractExplicitNamespace(toolArgs);
|
|
@@ -1044,15 +1588,17 @@ export default function agentBrowserExtension(pi) {
|
|
|
1044
1588
|
ownedElectronLaunchRecords,
|
|
1045
1589
|
ownedManagedSessions,
|
|
1046
1590
|
});
|
|
1591
|
+
const callerOwnedSessionNamespace = explicitSessionName
|
|
1592
|
+
? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)
|
|
1593
|
+
: undefined;
|
|
1047
1594
|
const callerOwnedSessionQueueKey = !serializeBrowserCommand && explicitSessionName
|
|
1048
|
-
? getSessionContextKey(explicitSessionName,
|
|
1595
|
+
? getSessionContextKey(explicitSessionName, callerOwnedSessionNamespace) ?? explicitSessionName
|
|
1049
1596
|
: undefined;
|
|
1050
1597
|
const runBrowserCommand = async () => {
|
|
1051
1598
|
const branchRestoreGenerationAtStart = branchRestoreGeneration;
|
|
1052
1599
|
const generationAtStart = branchStateGeneration;
|
|
1053
1600
|
const sessionPageStateUpdate = sessionPageState.beginUpdate();
|
|
1054
1601
|
const browserRunState = {
|
|
1055
|
-
allowedDomainsBySession,
|
|
1056
1602
|
artifactManifest,
|
|
1057
1603
|
attachedSessionKeys,
|
|
1058
1604
|
closedManagedSessionNames: new Set(),
|
|
@@ -1074,7 +1620,6 @@ export default function agentBrowserExtension(pi) {
|
|
|
1074
1620
|
sessionPageState,
|
|
1075
1621
|
traceOwners,
|
|
1076
1622
|
};
|
|
1077
|
-
const initialAllowedDomainsBySession = browserRunState.allowedDomainsBySession;
|
|
1078
1623
|
const initialArtifactManifest = browserRunState.artifactManifest;
|
|
1079
1624
|
const initialNetworkRoutesBySession = browserRunState.networkRoutesBySession;
|
|
1080
1625
|
const attachedSessionRequested = isAttachedBrowserInvocation(toolArgs)
|
|
@@ -1103,20 +1648,30 @@ export default function agentBrowserExtension(pi) {
|
|
|
1103
1648
|
state: browserRunState,
|
|
1104
1649
|
});
|
|
1105
1650
|
const branchRestoreStillCurrent = branchRestoreGenerationAtStart === branchRestoreGeneration;
|
|
1651
|
+
const resultDetails = isRecord(result.details) ? result.details : undefined;
|
|
1652
|
+
const resultSessionName = typeof resultDetails?.sessionName === "string"
|
|
1653
|
+
? resultDetails.sessionName
|
|
1654
|
+
: extractExplicitSessionName(toolArgs);
|
|
1655
|
+
const resultNamespace = typeof resultDetails?.namespace === "string"
|
|
1656
|
+
? resultDetails.namespace
|
|
1657
|
+
: resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE);
|
|
1106
1658
|
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);
|
|
1659
|
+
const resultBatchCloseLifecycle = getSuccessfulBatchCloseLifecycle(resultDetails?.batchSteps);
|
|
1114
1660
|
const resultSessionKey = getSessionContextKey(resultSessionName, resultNamespace) ?? resultSessionName;
|
|
1115
1661
|
const managedSessionOutcome = isRecord(resultDetails?.managedSessionOutcome) ? resultDetails.managedSessionOutcome : undefined;
|
|
1662
|
+
const closeAllApplied = resultDetails?.closeAllApplied === true;
|
|
1116
1663
|
const attachedSessionRemainsActive = result.isError !== true
|
|
1117
|
-
|| (attachedSessionRequested && managedSessionOutcome?.activeAfter === true);
|
|
1118
|
-
const closesAttachedSession = result.isError !== true && isCloseCommand(
|
|
1119
|
-
|
|
1664
|
+
|| ((attachedSessionRequested || attachedSessionKnown) && managedSessionOutcome?.activeAfter === true);
|
|
1665
|
+
const closesAttachedSession = (result.isError !== true && isCloseCommand(extractUpstreamCommandTokens(toolArgs)[0]))
|
|
1666
|
+
|| resultBatchCloseLifecycle?.endsClosed === true;
|
|
1667
|
+
if (closeAllApplied) {
|
|
1668
|
+
deleteIdentityKeysInNamespace(attachedSessionKeys, resultNamespace);
|
|
1669
|
+
if (resultSessionKey && attachedSessionRemainsActive && (attachedSessionRequested || attachedSessionKnown) && resultBatchCloseLifecycle?.endsClosed === false) {
|
|
1670
|
+
attachedSessionKeys.add(resultSessionKey);
|
|
1671
|
+
result = { ...result, details: { ...(resultDetails ?? {}), attachedBrowserSession: true } };
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
else if (resultSessionKey && closesAttachedSession)
|
|
1120
1675
|
attachedSessionKeys.delete(resultSessionKey);
|
|
1121
1676
|
else if (resultSessionKey && attachedSessionRemainsActive && (attachedSessionRequested || attachedSessionKnown)) {
|
|
1122
1677
|
attachedSessionKeys.add(resultSessionKey);
|
|
@@ -1124,9 +1679,31 @@ export default function agentBrowserExtension(pi) {
|
|
|
1124
1679
|
}
|
|
1125
1680
|
}
|
|
1126
1681
|
if (branchRestoreStillCurrent) {
|
|
1127
|
-
allowedDomainsBySession = mergeBrowserRunMap(allowedDomainsBySession, initialAllowedDomainsBySession, browserRunState.allowedDomainsBySession);
|
|
1128
1682
|
networkRoutesBySession = mergeBrowserRunMap(networkRoutesBySession, initialNetworkRoutesBySession, browserRunState.networkRoutesBySession);
|
|
1129
1683
|
artifactManifest = mergeBrowserRunArtifactManifest(artifactManifest, initialArtifactManifest, browserRunState.artifactManifest);
|
|
1684
|
+
const handledBatchCloseKeys = syncRecordingReservationsFromResult(result);
|
|
1685
|
+
if (resultDetails?.closeAllApplied === true) {
|
|
1686
|
+
for (const [sessionKey, reservation] of [...activeRecordingReservations]) {
|
|
1687
|
+
if (isAgentBrowserSessionIdentityKeyInNamespace(sessionKey, resultNamespace)) {
|
|
1688
|
+
retireRecordingSession(reservation.sessionName, reservation.namespace);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
for (const closedSessionKey of browserRunState.closedManagedSessionNames) {
|
|
1693
|
+
if (handledBatchCloseKeys.has(closedSessionKey))
|
|
1694
|
+
continue;
|
|
1695
|
+
const reservation = activeRecordingReservations.get(closedSessionKey);
|
|
1696
|
+
if (reservation)
|
|
1697
|
+
retireRecordingSession(reservation.sessionName, reservation.namespace);
|
|
1698
|
+
}
|
|
1699
|
+
if (resultSessionName && resultReportsNoRecordingInProgress(result)) {
|
|
1700
|
+
retireRecordingSession(resultSessionName, resultNamespace);
|
|
1701
|
+
}
|
|
1702
|
+
if (resultSessionName) {
|
|
1703
|
+
const reservation = activeRecordingReservations.get(getAgentBrowserSessionIdentityKey(resultSessionName, resultNamespace));
|
|
1704
|
+
if (reservation)
|
|
1705
|
+
result = appendActiveRecordingCleanupAction(result, reservation);
|
|
1706
|
+
}
|
|
1130
1707
|
if (artifactManifest) {
|
|
1131
1708
|
result = {
|
|
1132
1709
|
...result,
|
|
@@ -1155,7 +1732,7 @@ export default function agentBrowserExtension(pi) {
|
|
|
1155
1732
|
mergeActiveElectronLaunchRecords(ownedElectronLaunchRecords, electronLaunchRecords, {
|
|
1156
1733
|
branchOwnedLaunchIds: branchOwnedElectronLaunchIds,
|
|
1157
1734
|
touchedLaunchIds: !result.isError
|
|
1158
|
-
? getTouchedElectronLaunchIds(explicitSessionName ?? browserRunState.managedSessionName, electronLaunchRecords)
|
|
1735
|
+
? getTouchedElectronLaunchIds(explicitSessionName ?? browserRunState.managedSessionName, electronLaunchRecords, explicitSessionName ? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE) : browserRunState.managedSessionNamespace)
|
|
1159
1736
|
: undefined,
|
|
1160
1737
|
});
|
|
1161
1738
|
if (serializeBrowserCommand)
|
|
@@ -1163,12 +1740,47 @@ export default function agentBrowserExtension(pi) {
|
|
|
1163
1740
|
}
|
|
1164
1741
|
return applyAgentBrowserOutputPath({ cwd: ctx.cwd, outputPath, preserveTextContent: Array.isArray(params.args) && params.args.includes("--json"), result });
|
|
1165
1742
|
};
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1743
|
+
const closesAllSessions = commandClosesAllSessions(toolArgs, resolvedInput.toolStdin);
|
|
1744
|
+
const closeAllNamespace = closesAllSessions
|
|
1745
|
+
? resolveAgentBrowserNamespace(toolArgs, getAgentBrowserProcessEnvironment().AGENT_BROWSER_NAMESPACE)
|
|
1746
|
+
: undefined;
|
|
1747
|
+
const runWithinSessionQueue = () => {
|
|
1748
|
+
if (closesAllSessions)
|
|
1749
|
+
return managedSessionExecutionQueue.run(() => callerOwnedSessionExecutionQueues.runExclusive(closeAllNamespace, runBrowserCommand));
|
|
1750
|
+
if (serializeBrowserCommand)
|
|
1751
|
+
return managedSessionExecutionQueue.run(runBrowserCommand);
|
|
1752
|
+
return callerOwnedSessionQueueKey
|
|
1753
|
+
? callerOwnedSessionExecutionQueues.run(callerOwnedSessionQueueKey, callerOwnedSessionNamespace, runBrowserCommand)
|
|
1754
|
+
: runBrowserCommand();
|
|
1755
|
+
};
|
|
1756
|
+
if (!commandTouchesArtifactLifecycle(toolArgs, resolvedInput.toolStdin, outputPath))
|
|
1757
|
+
return runWithinSessionQueue();
|
|
1758
|
+
return artifactExecutionQueue.run(async () => {
|
|
1759
|
+
const artifactValidationError = getArtifactPreflightValidationError({
|
|
1760
|
+
activeRecordingReservations: activeRecordingReservations.values(),
|
|
1761
|
+
args: toolArgs,
|
|
1762
|
+
cwd: ctx.cwd,
|
|
1763
|
+
outputPath,
|
|
1764
|
+
stdin: resolvedInput.toolStdin,
|
|
1765
|
+
});
|
|
1766
|
+
if (!artifactValidationError)
|
|
1767
|
+
return runWithinSessionQueue();
|
|
1768
|
+
return applyAgentBrowserOutputPath({
|
|
1769
|
+
cwd: ctx.cwd,
|
|
1770
|
+
outputPath,
|
|
1771
|
+
result: buildValidationFailureResult({
|
|
1772
|
+
attemptedKind: resolvedInput.kind,
|
|
1773
|
+
kind: "invalid",
|
|
1774
|
+
redactedArgs: resolvedInput.redactedArgs,
|
|
1775
|
+
status: "invalid",
|
|
1776
|
+
toolArgs: resolvedInput.toolArgs,
|
|
1777
|
+
toolStdin: resolvedInput.toolStdin,
|
|
1778
|
+
validationError: artifactValidationError,
|
|
1779
|
+
}),
|
|
1780
|
+
});
|
|
1781
|
+
});
|
|
1171
1782
|
},
|
|
1172
|
-
}
|
|
1783
|
+
};
|
|
1784
|
+
pi.registerTool(agentBrowserTool);
|
|
1173
1785
|
registerWebSearchToolIfAvailable(agentBrowserConfig);
|
|
1174
1786
|
}
|