pi-agent-browser-native 0.9.2 → 0.9.3
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 +14 -0
- package/README.md +18 -6
- package/dist/extensions/agent-browser/index.js +39 -16
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +6 -3
- package/dist/extensions/agent-browser/lib/browser-transcript.js +2 -1
- package/dist/extensions/agent-browser/lib/native-output.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +2 -8
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +12 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +58 -7
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +18 -12
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +33 -24
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +111 -53
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +32 -20
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +37 -32
- package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +26 -8
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +3 -1
- package/dist/extensions/agent-browser/lib/page-target-validation.js +10 -3
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +2 -1
- package/dist/extensions/agent-browser/lib/playbook.js +2 -2
- package/dist/extensions/agent-browser/lib/process-environment.js +12 -3
- package/dist/extensions/agent-browser/lib/process.js +6 -6
- package/dist/extensions/agent-browser/lib/read-confirmation.js +56 -16
- package/dist/extensions/agent-browser/lib/results/confirmation.js +3 -0
- package/dist/extensions/agent-browser/lib/results/envelope.js +5 -1
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +3 -0
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +34 -1
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +5 -1
- package/dist/extensions/agent-browser/lib/results/presentation.js +15 -2
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +5 -5
- package/dist/extensions/agent-browser/lib/runtime.js +5 -1
- package/dist/extensions/agent-browser/lib/session-page-state.js +35 -16
- package/docs/ARCHITECTURE.md +5 -5
- package/docs/COMMAND_REFERENCE.md +13 -9
- package/docs/RELEASE.md +6 -5
- package/docs/REQUIREMENTS.md +1 -0
- package/docs/SESSION_STORAGE.md +1 -1
- package/docs/SUPPORT_MATRIX.md +9 -3
- package/docs/TOOL_CONTRACT.md +20 -14
- package/package.json +4 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isRecord } from "../../parsing.js";
|
|
2
|
+
import { getUpstreamEffectiveBatchSteps } from "../../orchestration/batch-stdin.js";
|
|
2
3
|
import { redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
|
|
3
4
|
import { classifyNetworkRequestFailure, isApiLikeNetworkRequest, isNetworkArtifactNoiseRequest, summarizeNetworkFailures } from "../network.js";
|
|
4
5
|
import { withOptionalSessionArgs } from "../next-actions.js";
|
|
@@ -866,6 +867,8 @@ function redactStorageData(value) {
|
|
|
866
867
|
if (!isRecord(value))
|
|
867
868
|
return redactSensitiveValue(value);
|
|
868
869
|
const entries = Object.fromEntries(Object.entries(value).map(([key, entryValue]) => {
|
|
870
|
+
if (key === "data" && isRecord(entryValue))
|
|
871
|
+
return [key, Object.fromEntries(Object.entries(entryValue).map(([storageKey, storageValue]) => [storageKey, shouldRevealStorageValue(storageKey, storageValue) ? redactSensitiveValue(storageValue) : "[REDACTED]"]))];
|
|
869
872
|
if ((key === "entries" || key === "items") && Array.isArray(entryValue))
|
|
870
873
|
return [key, entryValue.map((item) => isRecord(item) ? redactStorageEntryValue(item) : redactSensitiveValue(item))];
|
|
871
874
|
if (key === "value") {
|
|
@@ -885,6 +888,9 @@ function redactStorageData(value) {
|
|
|
885
888
|
}
|
|
886
889
|
function formatStorageText(data) {
|
|
887
890
|
const type = getStringField(data, "type") ?? getStringField(data, "storage") ?? "storage";
|
|
891
|
+
if (isRecord(data.data)) {
|
|
892
|
+
return Object.entries(data.data).map(([key, value]) => `${redactModelFacingText(key)}: ${formatStorageValue(key, value)}`).join("\n") || `${type}: no entries.`;
|
|
893
|
+
}
|
|
888
894
|
const entries = getArrayField(data, "entries") ?? getArrayField(data, "items");
|
|
889
895
|
if (entries) {
|
|
890
896
|
if (entries.length === 0)
|
|
@@ -974,7 +980,34 @@ function redactStatefulValues(value, sensitiveKeys) {
|
|
|
974
980
|
sensitiveKeys.has(key.toLowerCase()) ? "[REDACTED]" : redactStatefulValues(entryValue, sensitiveKeys),
|
|
975
981
|
]));
|
|
976
982
|
}
|
|
977
|
-
|
|
983
|
+
function redactNativeContextText(commandInfo, text, stdin) {
|
|
984
|
+
if (commandInfo.command === "batch") {
|
|
985
|
+
const steps = getUpstreamEffectiveBatchSteps(commandInfo.commandTokens ?? ["batch"], stdin);
|
|
986
|
+
if (steps.some(step => step[0] === "cookies"))
|
|
987
|
+
text = redactNativeContextText({ command: "cookies" }, text);
|
|
988
|
+
if (steps.some(step => step[0] === "storage"))
|
|
989
|
+
text = redactNativeContextText({ command: "storage" }, text);
|
|
990
|
+
return text;
|
|
991
|
+
}
|
|
992
|
+
if (commandInfo.command === "cookies")
|
|
993
|
+
return text.replace(/^([^=\r\n]*)=([^\r\n]*)/gm, "$1=[REDACTED]");
|
|
994
|
+
if (commandInfo.command !== "storage")
|
|
995
|
+
return text;
|
|
996
|
+
const tokens = commandInfo.commandTokens;
|
|
997
|
+
const operation = tokens?.[2];
|
|
998
|
+
const key = operation === "get" ? tokens?.[3] : operation === "set" || operation === "clear" ? undefined : operation;
|
|
999
|
+
if (key !== undefined && text.startsWith(`${key}: `)) {
|
|
1000
|
+
const suffix = text.endsWith("\n") ? "\n" : "";
|
|
1001
|
+
const value = text.slice(key.length + 2, suffix ? -1 : undefined);
|
|
1002
|
+
return `${key}: ${formatStorageValue(key, value)}${suffix}`;
|
|
1003
|
+
}
|
|
1004
|
+
// ponytail: native all-entry/raw-batch text does not escape multiline values.
|
|
1005
|
+
// Format heuristics cannot separate lookalike entries; use structured JSON for sensitive storage.
|
|
1006
|
+
return text.replace(/^([^\r\n]*?): ([^\r\n]*)/gm, (_line, key, value) => `${key}: ${formatStorageValue(key, value)}`);
|
|
1007
|
+
}
|
|
1008
|
+
export function redactPresentationData(commandInfo, data, stdin) {
|
|
1009
|
+
if (typeof data === "string")
|
|
1010
|
+
return redactSensitiveText(redactNativeContextText(commandInfo, data, stdin));
|
|
978
1011
|
if (commandInfo.command === "cookies")
|
|
979
1012
|
return redactStatefulValues(data, new Set(["value"]));
|
|
980
1013
|
if (commandInfo.command === "storage")
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isRecord } from "../../parsing.js";
|
|
2
|
+
import { getNativeTabContinuationGuidance } from "../../read-confirmation.js";
|
|
2
3
|
import { detectConfirmationRequired } from "../confirmation.js";
|
|
3
4
|
import { formatRawSnapshotText, formatSnapshotSummary } from "../snapshot.js";
|
|
4
5
|
import { getPageSummary, omitUpstreamLifecycle, redactModelFacingText, stringifyModelFacing } from "./common.js";
|
|
@@ -225,7 +226,10 @@ export function formatPresentationSummary(commandInfo, data, compiledSemanticAct
|
|
|
225
226
|
export function formatPresentationContentText(commandInfo, data, compiledSemanticAction) {
|
|
226
227
|
const confirmationRequired = detectConfirmationRequired(data);
|
|
227
228
|
if (confirmationRequired)
|
|
228
|
-
return
|
|
229
|
+
return [
|
|
230
|
+
formatConfirmationRequiredText(confirmationRequired),
|
|
231
|
+
getNativeTabContinuationGuidance(commandInfo.commandTokens ?? [], data),
|
|
232
|
+
].filter(Boolean).join("\n\n");
|
|
229
233
|
const presenterText = commandInfo.command ? COMMAND_PRESENTERS[commandInfo.command]?.text?.(commandInfo, data) : undefined;
|
|
230
234
|
if (presenterText)
|
|
231
235
|
return presenterText;
|
|
@@ -53,6 +53,19 @@ export async function buildToolPresentation(options) {
|
|
|
53
53
|
const { args, artifactManifest, artifactRequest, commandInfo, compiledSemanticAction, cwd, envelope, errorText, namespace, networkRouteDiagnostics, networkRoutes, persistentArtifactStore, sessionName, } = options;
|
|
54
54
|
const commandInfoWithTokens = commandInfo.commandTokens || !args ? commandInfo : { ...commandInfo, commandTokens: extractUpstreamCommandTokens(args) };
|
|
55
55
|
const presentationCommandInfo = resolvePresentationCommandInfo(commandInfoWithTokens, compiledSemanticAction);
|
|
56
|
+
if (options.textOutput) {
|
|
57
|
+
const text = String(redactPresentationData(commandInfoWithTokens, typeof envelope?.data === "string" ? envelope.data : "", options.stdin));
|
|
58
|
+
const failure = errorText ? buildErrorPresentation({ args, commandInfo, errorText, sessionName }) : undefined;
|
|
59
|
+
const presentation = {
|
|
60
|
+
...failure,
|
|
61
|
+
content: options.modelVisible === false ? [] : [{ type: "text", text: [errorText, text].filter(Boolean).join("\n\n") }],
|
|
62
|
+
data: text,
|
|
63
|
+
summary: failure?.summary ?? (text.split("\n", 1)[0] || "Native text command completed."),
|
|
64
|
+
};
|
|
65
|
+
return sanitizeModelFacingPresentation(options.modelVisible === false ? presentation : await compactLargePresentationOutput({
|
|
66
|
+
artifactManifest, commandInfo, data: presentation.data, persistentArtifactStore, presentation,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
56
69
|
const recordingCommand = commandInfo.command === "record";
|
|
57
70
|
const recordingBatch = commandInfo.command === "batch" && isAgentBrowserBatchResultArray(envelope?.data)
|
|
58
71
|
&& envelope.data.some((row) => row.command?.[0] === "record");
|
|
@@ -66,6 +79,7 @@ export async function buildToolPresentation(options) {
|
|
|
66
79
|
data = { ...data, piCleanupOwnership: options.piCleanupOwnership ?? "unknown" };
|
|
67
80
|
}
|
|
68
81
|
const readConfirmation = nextReadConfirmation({ commandTokens: commandInfoWithTokens.commandTokens ?? [], data, namespace, sessionName: sessionName ?? "default", succeeded: envelope?.success !== false });
|
|
82
|
+
const confirmationRequired = detectConfirmationRequired(data);
|
|
69
83
|
const presentationData = commandInfo.command === "batch" && isAgentBrowserBatchResultArray(data)
|
|
70
84
|
? redactBatchSpillData(data)
|
|
71
85
|
: redactPresentationData(commandInfoWithTokens, data);
|
|
@@ -93,7 +107,7 @@ export async function buildToolPresentation(options) {
|
|
|
93
107
|
summary,
|
|
94
108
|
});
|
|
95
109
|
}
|
|
96
|
-
else if (options.modelVisible !== false && commandInfo.command === "snapshot" && isRecord(data)) {
|
|
110
|
+
else if (options.modelVisible !== false && commandInfo.command === "snapshot" && isRecord(data) && !confirmationRequired) {
|
|
97
111
|
presentation = await buildSnapshotPresentation(data, persistentArtifactStore, artifactManifest);
|
|
98
112
|
}
|
|
99
113
|
else {
|
|
@@ -160,7 +174,6 @@ export async function buildToolPresentation(options) {
|
|
|
160
174
|
secondaryPaths: presentationWithManifest.fullOutputPaths,
|
|
161
175
|
}));
|
|
162
176
|
presentationWithManifest.artifactVerification = buildArtifactVerificationSummary(artifacts, presentationWithManifest.artifactManifest, currentSpillPaths) ?? presentationWithManifest.artifactVerification;
|
|
163
|
-
const confirmationRequired = detectConfirmationRequired(data);
|
|
164
177
|
const missingArtifactFailureText = formatMissingArtifactFailureText(presentationWithManifest.artifacts);
|
|
165
178
|
if (!errorText && missingArtifactFailureText && hasMissingFileArtifact(presentationWithManifest.artifacts)) {
|
|
166
179
|
presentationWithManifest.resultCategory = "failure";
|
|
@@ -74,8 +74,8 @@ function getFindVisibleRefFallbackTarget(args, options = {}) {
|
|
|
74
74
|
export function getVisibleRefFallbackTarget(options) {
|
|
75
75
|
return getFindVisibleRefFallbackTarget(options.commandTokens, { allowLeadingDashFillText: true }) ?? (options.compiledSemanticAction ? getFindVisibleRefFallbackTarget(options.compiledSemanticAction.args, { allowLeadingDashFillText: true }) : undefined);
|
|
76
76
|
}
|
|
77
|
-
function getVisibleRefFallbackCandidates(target, snapshotData) {
|
|
78
|
-
const refs = getSnapshotRefRecord(snapshotData);
|
|
77
|
+
function getVisibleRefFallbackCandidates(target, snapshotData, refSnapshot) {
|
|
78
|
+
const refs = refSnapshot?.refs ?? getSnapshotRefRecord(snapshotData);
|
|
79
79
|
if (!refs)
|
|
80
80
|
return [];
|
|
81
81
|
const snapshotLineByRef = getSnapshotLineTextByRef(snapshotData);
|
|
@@ -85,7 +85,7 @@ function getVisibleRefFallbackCandidates(target, snapshotData) {
|
|
|
85
85
|
if (!/^e\d+$/.test(ref) || !isRecord(entry))
|
|
86
86
|
return [];
|
|
87
87
|
const snapshotLine = snapshotLineByRef.get(ref);
|
|
88
|
-
const editableEvidence = getEditableRefEvidence({ ref: entry, text: snapshotLine });
|
|
88
|
+
const editableEvidence = refSnapshot ? refSnapshot.refs?.[ref]?.isEditable : getEditableRefEvidence({ ref: entry, text: snapshotLine });
|
|
89
89
|
const role = getSnapshotRefRole(entry, editableEvidence);
|
|
90
90
|
const name = typeof entry.name === "string" ? entry.name : undefined;
|
|
91
91
|
if (!role || !name || !roleOrder.includes(role.toLowerCase()) || normalizeSemanticActionAccessibleName(name) !== targetName)
|
|
@@ -132,7 +132,7 @@ export function resolveVisibleRefActionFromSnapshot(options) {
|
|
|
132
132
|
const target = getFindVisibleRefFallbackTarget(options.compiledAction.args, { allowLeadingDashFillText: true });
|
|
133
133
|
if (!target)
|
|
134
134
|
return undefined;
|
|
135
|
-
const snapshot = extractRefSnapshotFromData(options.snapshotData);
|
|
135
|
+
const snapshot = options.refSnapshot ?? extractRefSnapshotFromData(options.snapshotData);
|
|
136
136
|
if (!snapshot)
|
|
137
137
|
return undefined;
|
|
138
138
|
const selectOptionValues = options.compiledAction.values && options.compiledAction.values.length > 0
|
|
@@ -141,7 +141,7 @@ export function resolveVisibleRefActionFromSnapshot(options) {
|
|
|
141
141
|
const effectiveTarget = target.action === "select" && selectOptionValues
|
|
142
142
|
? { ...target, optionValues: selectOptionValues }
|
|
143
143
|
: target;
|
|
144
|
-
const candidates = getVisibleRefFallbackCandidates(effectiveTarget, options.snapshotData);
|
|
144
|
+
const candidates = getVisibleRefFallbackCandidates(effectiveTarget, options.snapshotData, options.refSnapshot);
|
|
145
145
|
if (effectiveTarget.action === "fill") {
|
|
146
146
|
if (!options.allowFill || candidates.length !== 1 || effectiveTarget.text === undefined)
|
|
147
147
|
return undefined;
|
|
@@ -157,6 +157,10 @@ function createBalancedJsonEndFinder(text) {
|
|
|
157
157
|
};
|
|
158
158
|
}
|
|
159
159
|
function redactSerializedJson(text) {
|
|
160
|
+
if (text === "[REDACTED]")
|
|
161
|
+
return undefined;
|
|
162
|
+
if (!/^\s*[\[{"\d\-tfn]/.test(text))
|
|
163
|
+
return undefined;
|
|
160
164
|
// Validate grammar only; rebuilding parsed values loses duplicates and numeric spelling.
|
|
161
165
|
try {
|
|
162
166
|
JSON.parse(text);
|
|
@@ -878,7 +882,7 @@ export function buildExecutionPlan(args, options) {
|
|
|
878
882
|
const argvDescriptor = parseArgvDescriptor(args);
|
|
879
883
|
const commandInfo = argvDescriptor.commandInfo;
|
|
880
884
|
const commandNeedsManagedSession = !plainTextInspection && !options.browserIndependentReadConfirmation && needsManagedSession(argvDescriptor, options.stdin);
|
|
881
|
-
const effectiveArgs = plainTextInspection ? [...args] : args
|
|
885
|
+
const effectiveArgs = plainTextInspection ? [...args] : getBooleanFlagValue(args, "--json") !== undefined ? [] : ["--json"];
|
|
882
886
|
let namespace = explicitNamespacePresent ? explicitNamespace ?? "" : undefined;
|
|
883
887
|
if (plainTextInspection) {
|
|
884
888
|
return {
|
|
@@ -4,7 +4,8 @@ import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNa
|
|
|
4
4
|
import { getBrowserRecord, snapshotFromDefinition } from "./browser-transcript.js";
|
|
5
5
|
import { isCloseCommand, isOpenNavigationCommand, isReadOnlyDiagnosticSessionTargetCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWebMcpPageMutationCommand, isWindowOrDiffPageTransitionCommand } from "./command-taxonomy.js";
|
|
6
6
|
import { isRecord } from "./parsing.js";
|
|
7
|
-
import {
|
|
7
|
+
import { detectConfirmationRequired } from "./results/confirmation.js";
|
|
8
|
+
import { findReadConfirmation as findPendingReadConfirmation, isSuccessfulNativeConfirmedClose, parseReadConfirmation } from "./read-confirmation.js";
|
|
8
9
|
import { getEditableRefEvidence } from "./results/editable-ref-evidence.js";
|
|
9
10
|
import { enrichSnapshotRefEntries, getFullSnapshotData, getSnapshotRefEntries } from "./results/snapshot-refs.js";
|
|
10
11
|
import { parseSnapshotLines } from "./results/snapshot-segments.js";
|
|
@@ -81,6 +82,10 @@ function extractBatchResultCommand(item) {
|
|
|
81
82
|
}
|
|
82
83
|
export function extractSessionTabTargetFromCommandData(commandTokens, data) {
|
|
83
84
|
const [command, subcommand] = commandTokens;
|
|
85
|
+
if (command === "confirm" && isRecord(data) && data.confirmed === true && ["navigate", "url"].includes(String(data.action))
|
|
86
|
+
&& isRecord(data.result) && data.result.success === true && !detectConfirmationRequired(data)) {
|
|
87
|
+
return extractSessionTabTargetFromData(data.result.data);
|
|
88
|
+
}
|
|
84
89
|
if (command === "get" && subcommand === "url") {
|
|
85
90
|
return normalizeSessionTabTarget({ url: extractStringResultField(data, "url") ?? extractStringResultField(data, "result") });
|
|
86
91
|
}
|
|
@@ -93,7 +98,7 @@ export function extractSessionTabTargetFromBatchResults(data) {
|
|
|
93
98
|
let currentTarget;
|
|
94
99
|
let pendingTitle;
|
|
95
100
|
for (const item of data) {
|
|
96
|
-
if (!isRecord(item))
|
|
101
|
+
if (!isRecord(item) || detectConfirmationRequired(item.result))
|
|
97
102
|
continue;
|
|
98
103
|
// Only this row's native identity can describe the active tab after it ran.
|
|
99
104
|
if (currentTarget?.targetId)
|
|
@@ -108,7 +113,7 @@ export function extractSessionTabTargetFromBatchResults(data) {
|
|
|
108
113
|
if (item.success === false)
|
|
109
114
|
continue;
|
|
110
115
|
const result = item.result;
|
|
111
|
-
if (isCloseCommand(name)) {
|
|
116
|
+
if (isCloseCommand(name) || isSuccessfulNativeConfirmedClose(commandTokens, result)) {
|
|
112
117
|
currentTarget = undefined;
|
|
113
118
|
pendingTitle = undefined;
|
|
114
119
|
continue;
|
|
@@ -138,9 +143,7 @@ export function deriveSessionTabTarget(options) {
|
|
|
138
143
|
if (isCloseCommand(options.command)) {
|
|
139
144
|
return undefined;
|
|
140
145
|
}
|
|
141
|
-
const commandDataTarget =
|
|
142
|
-
? undefined
|
|
143
|
-
: extractSessionTabTargetFromData(options.data);
|
|
146
|
+
const commandDataTarget = extractSessionTabTargetFromCommandData([options.command, options.subcommand].filter((token) => token !== undefined), options.data);
|
|
144
147
|
const observedTarget = normalizeSessionTabTarget(options.navigationSummary)
|
|
145
148
|
?? extractSessionTabTargetFromBatchResults(options.data)
|
|
146
149
|
?? commandDataTarget;
|
|
@@ -163,6 +166,8 @@ function extractRefSnapshotRefs(data) {
|
|
|
163
166
|
return Object.keys(refs).length > 0 ? refs : undefined;
|
|
164
167
|
}
|
|
165
168
|
export function extractRefSnapshotFromData(value) {
|
|
169
|
+
if (detectConfirmationRequired(value))
|
|
170
|
+
return undefined;
|
|
166
171
|
const data = getFullSnapshotData(value);
|
|
167
172
|
if (!data)
|
|
168
173
|
return undefined;
|
|
@@ -214,7 +219,7 @@ export function extractLatestRefSnapshotStateFromBatchResults(data) {
|
|
|
214
219
|
continue;
|
|
215
220
|
const commandTokens = extractUpstreamCommandTokens(extractBatchResultCommand(item));
|
|
216
221
|
const [name] = commandTokens;
|
|
217
|
-
if (item.success !== false && isCloseCommand(name)) {
|
|
222
|
+
if (item.success !== false && !detectConfirmationRequired(item.result) && (isCloseCommand(name) || isSuccessfulNativeConfirmedClose(commandTokens, item.result))) {
|
|
218
223
|
latestState = undefined;
|
|
219
224
|
continue;
|
|
220
225
|
}
|
|
@@ -258,6 +263,7 @@ export function getSessionPageStateKey(sessionName, namespace) {
|
|
|
258
263
|
return sessionName ? getAgentBrowserSessionIdentityKey(sessionName, namespace) : undefined;
|
|
259
264
|
}
|
|
260
265
|
export class SessionPageState {
|
|
266
|
+
confirmActions = new Map();
|
|
261
267
|
readConfirmations = new Map();
|
|
262
268
|
refSnapshotInvalidations = new Map();
|
|
263
269
|
refSnapshots = new Map();
|
|
@@ -281,6 +287,7 @@ export class SessionPageState {
|
|
|
281
287
|
/** A command operates on its admitted state while the committed view is unavailable. */
|
|
282
288
|
fork() {
|
|
283
289
|
const state = new SessionPageState();
|
|
290
|
+
state.confirmActions = new Map(this.confirmActions);
|
|
284
291
|
state.readConfirmations = new Map(this.readConfirmations);
|
|
285
292
|
state.refSnapshotInvalidations = new Map(this.refSnapshotInvalidations);
|
|
286
293
|
state.refSnapshots = new Map(this.refSnapshots);
|
|
@@ -293,7 +300,7 @@ export class SessionPageState {
|
|
|
293
300
|
return state;
|
|
294
301
|
}
|
|
295
302
|
views() {
|
|
296
|
-
return new Map([...new Set([...this.tabTargets.keys(), ...this.tabTargetUnknownOrders.keys(), ...this.refSnapshots.keys(), ...this.refSnapshotInvalidations.keys()])].map(key => [key, this.get(key)]));
|
|
303
|
+
return new Map([...new Set([...this.confirmActions.keys(), ...this.tabTargets.keys(), ...this.tabTargetUnknownOrders.keys(), ...this.refSnapshots.keys(), ...this.refSnapshotInvalidations.keys()])].map(key => [key, this.get(key)]));
|
|
297
304
|
}
|
|
298
305
|
/** The same reducer commits live observations and replays selected journal envelopes. */
|
|
299
306
|
applyBrowserRecord({ event, snapshot }) {
|
|
@@ -301,12 +308,6 @@ export class SessionPageState {
|
|
|
301
308
|
const confirmation = parseReadConfirmation(event.state.readConfirmation);
|
|
302
309
|
for (const page of event.pages ?? []) {
|
|
303
310
|
const key = page.key;
|
|
304
|
-
if (event.phase === "begin") {
|
|
305
|
-
this.pending.set(key, { operationId: event.operationId, snapshot: this.refSnapshots.get(key) ?? this.pending.get(key)?.snapshot });
|
|
306
|
-
this.markTabTargetUnknown({ sessionName: key, update });
|
|
307
|
-
this.applyRefSnapshotInvalidation({ sessionName: key, update, invalidation: buildPageTransitionRefSnapshotInvalidation("This browser operation has no persisted finish. Inspect the current URL and take a fresh snapshot before using refs; changes may already have happened.") });
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
311
|
if (page.clear) {
|
|
311
312
|
this.clearSession(key);
|
|
312
313
|
continue;
|
|
@@ -314,6 +315,14 @@ export class SessionPageState {
|
|
|
314
315
|
const pending = this.pending.get(key);
|
|
315
316
|
if (event.phase === "finish" && pending && pending.operationId !== event.operationId)
|
|
316
317
|
continue;
|
|
318
|
+
if (page.confirmActions !== undefined)
|
|
319
|
+
this.setConfirmActions(key, page.confirmActions ?? undefined);
|
|
320
|
+
if (event.phase === "begin") {
|
|
321
|
+
this.pending.set(key, { operationId: event.operationId, snapshot: this.refSnapshots.get(key) ?? this.pending.get(key)?.snapshot });
|
|
322
|
+
this.markTabTargetUnknown({ sessionName: key, update });
|
|
323
|
+
this.applyRefSnapshotInvalidation({ sessionName: key, update, invalidation: buildPageTransitionRefSnapshotInvalidation("This browser operation has no persisted finish. Inspect the current URL and take a fresh snapshot before using refs; changes may already have happened.") });
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
317
326
|
if (page.unknown || page.refs.kind === "unknown") {
|
|
318
327
|
this.markTabTargetUnknown({ sessionName: key, update });
|
|
319
328
|
this.applyRefSnapshotInvalidation({ sessionName: key, update, invalidation: page.refs.kind === "unknown" && page.refs.invalidation
|
|
@@ -361,6 +370,7 @@ export class SessionPageState {
|
|
|
361
370
|
return this.updateOrder;
|
|
362
371
|
}
|
|
363
372
|
reset() {
|
|
373
|
+
this.confirmActions.clear();
|
|
364
374
|
this.readConfirmations.clear();
|
|
365
375
|
this.pending.clear();
|
|
366
376
|
this.nativeGenerations.clear();
|
|
@@ -375,6 +385,7 @@ export class SessionPageState {
|
|
|
375
385
|
if (!sessionName)
|
|
376
386
|
return {};
|
|
377
387
|
return {
|
|
388
|
+
...(this.confirmActions.has(sessionName) ? { confirmActions: this.confirmActions.get(sessionName) } : {}),
|
|
378
389
|
pinningReason: this.tabPinningReasons.get(sessionName),
|
|
379
390
|
...(this.tabTargets.get(sessionName)?.reopenPending !== undefined ? { tabReopenPending: this.tabTargets.get(sessionName)?.reopenPending } : {}),
|
|
380
391
|
refSnapshot: stripRefSnapshotOrder(this.refSnapshots.get(sessionName)),
|
|
@@ -383,8 +394,14 @@ export class SessionPageState {
|
|
|
383
394
|
tabTarget: this.tabTargets.get(sessionName)?.target,
|
|
384
395
|
};
|
|
385
396
|
}
|
|
386
|
-
findReadConfirmation(args, namespace) {
|
|
387
|
-
return findPendingReadConfirmation(args, [...this.readConfirmations.values()].map(entry => entry.value), namespace);
|
|
397
|
+
findReadConfirmation(args, namespace, stdin) {
|
|
398
|
+
return findPendingReadConfirmation(args, [...this.readConfirmations.values()].map(entry => entry.value), namespace, stdin);
|
|
399
|
+
}
|
|
400
|
+
setConfirmActions(sessionName, value) {
|
|
401
|
+
if (value !== undefined)
|
|
402
|
+
this.confirmActions.set(sessionName, value);
|
|
403
|
+
else
|
|
404
|
+
this.confirmActions.delete(sessionName);
|
|
388
405
|
}
|
|
389
406
|
getReadConfirmation(sessionKey) {
|
|
390
407
|
return this.readConfirmations.get(sessionKey)?.value;
|
|
@@ -455,6 +472,7 @@ export class SessionPageState {
|
|
|
455
472
|
return { ...this.get(options.sessionName), applied: true };
|
|
456
473
|
}
|
|
457
474
|
clearSession(sessionName) {
|
|
475
|
+
this.confirmActions.delete(sessionName);
|
|
458
476
|
this.pending.delete(sessionName);
|
|
459
477
|
this.nativeGenerations.delete(sessionName);
|
|
460
478
|
this.readConfirmations.delete(sessionName);
|
|
@@ -466,6 +484,7 @@ export class SessionPageState {
|
|
|
466
484
|
}
|
|
467
485
|
clearNamespace(namespace) {
|
|
468
486
|
const sessionKeys = new Set([
|
|
487
|
+
...this.confirmActions.keys(),
|
|
469
488
|
...this.readConfirmations.keys(),
|
|
470
489
|
...this.refSnapshotInvalidations.keys(),
|
|
471
490
|
...this.refSnapshots.keys(),
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -33,7 +33,7 @@ The extension should:
|
|
|
33
33
|
- resolve `agent-browser` from `PATH`
|
|
34
34
|
- invoke it with native Node `spawn` on POSIX and `cross-spawn` on Windows. For the recognized global npm CMD template, public `which`/`path-key` resolution selects the same launcher and Node invokes its exact adjacent native executable directly, preserving literal CR/LF. Recognition requires the stock template, package identity and executable; custom launchers, other layouts, ambiguous search environments and worker-thread calls retain `cross-spawn`. No later installation is substituted. Custom or unrecognized batch files can still truncate literal newlines. Caller commands and helper probes retain empty operands, literal doublequotes, command/subcommand adjacency and explicit default `--namespace ""`. Keep piped stdin/stdout/stderr and await full Windows process-tree termination with `taskkill /T /F` on timeout or abort before falling back to the direct child signal. Missing commands use the spawner's `ENOENT`
|
|
35
35
|
- bound native process-identity queries at ten seconds on Windows, including PowerShell startup, and five seconds on POSIX. These probes preserve PID/start-time comparison and fail closed when identity is unavailable; their budget is separate from lock contention and browser command watchdogs
|
|
36
|
-
- inject `--json
|
|
36
|
+
- inject `--json` only when no caller output mode is selected; explicit `--json false` retains opaque redacted native text and process outcomes without interpreting page text as trusted receipts or a session target. Optional booleans follow upstream last-wins semantics; structured helpers stay strict JSON. Nonzero exits with stderr retain the native error ahead of parse diagnostics, while zero-exit malformed machine output remains a strict failure. Empty stdin batch rows pass validation and native execution skips them without rewriting caller input. Caller-requested JSON also uses the existing `buildJsonVisibleContent` envelope for early snapshot/network filters, scroll results, and preparation failures, retaining metadata in `details` and keeping output-file notices out of visible JSON. Help/version stays native text
|
|
37
37
|
- complete each upstream invocation when the direct `agent-browser` child exits even if Node delays `"close"`: piped stdio can stay referenced by longer-lived descendant processes, so `runAgentBrowserProcess` watches `exit` and `close` together, leaves stdio intact during a short post-`exit` grace so normal `close` can still win, destroys streams only when the post-`exit` fallback fires, and prefers `close` codes then wrapper timeout (`124`) over signal-shaped `exit` codes (`watchSpawnedChildCompletion` / `resolveSpawnedChildExitCode` in `extensions/agent-browser/lib/process.ts`) so the tool cannot hang after the CLI process has already terminated
|
|
38
38
|
- support optional stdin only for `eval --stdin`, `batch`, `auth save --password-stdin`, and internally generated QA/source batch plans, rejecting other command/stdin combinations before launch; Electron manages its own input
|
|
39
39
|
- support optional top-level `outputPath` for successful browser results and failed/pending/recovered recording receipts. Recording exports retain success/error, original attempt, native data, artifacts and recovery provenance; they do not treat a written receipt as a successful video. Other successful results write `details.data` (or model-facing text when no structured data exists) to a caller-requested local file and reporting `details.outputFile`, without changing upstream argv semantics or overwriting a browser artifact when both destinations resolve to the same file. If presentation compacted direct data, a batch result row, or a whole batch, apply the relevant command-specific redactor before spilling and rehydrate each full pre-compaction value only from its matching live wrapper-manifest spill; fail without writing compact metadata when any required spill is unavailable or untrusted
|
|
@@ -136,7 +136,7 @@ The optional `pi-change-working-dir` event protocol supplies one invocation exec
|
|
|
136
136
|
|
|
137
137
|
### Default
|
|
138
138
|
|
|
139
|
-
An explicit native `session` default (user/project JSON or `AGENT_BROWSER_SESSION`) selects a caller-owned browser across Pi sessions. Per-call session/namespace flags win. `lib/orchestration/native-session-defaults.ts` reads session/namespace and contributing launch defaults from native config paths and uses sessionless native `--config <path> --json session` to validate each contributing file and resolve its session. Native config continues to own launch/profile/storage settings; the adapter does not duplicate its schema. Explicit config
|
|
139
|
+
An explicit native `session` default (user/project JSON or `AGENT_BROWSER_SESSION`) selects a caller-owned browser across Pi sessions. Per-call session/namespace flags win. `lib/orchestration/native-session-defaults.ts` reads session/namespace and contributing launch defaults from native config paths and uses sessionless native `--config <path> --json session` to validate each contributing file and resolve its session. Native config continues to own launch/profile/storage settings; the adapter does not duplicate its schema. Explicit config, per-call idle settings, `--action-policy`, `--confirm-actions`, `--debug` and `--no-auto-dialog` are scoped to helpers through `lib/process-environment.ts`. Boolean selections also supply CLI operands to helpers lacking an explicit selection, because native ORs environment/config booleans and only CLI `false` overrides true defaults. Explicit debug false removes the debug environment variable because native daemon logging tests its presence; no-auto-dialog false retains native `0`. Caller argv stays intact and the environment scope ends with the call. The effective confirmation setting is retained in existing `SessionPageState` and canonical page events, resolved inside the identity queue before daemon inspection and helpers. First launch and new explicit `--confirm-actions` / `--config` selections use native argv → environment → config precedence; bare follow-ups retain selected strings, including explicit `""`, over ambient environment/discovered-config edits. Native unset is not retained and continues native default resolution; no config freezing or ambient-change detector is needed. Successful session/namespace close and successfully closed managed replacements clear it. Cleanup scopes every probe and close to the old identity's setting; failed close retains its page/policy continuity. Off-branch live owners keep their selected strings in the same page state until cleanup; an ordered post-close relaunch retains the new daemon's setting. Resume and branch restore use the same reducer. Native still owns policy changes and pending confirmations; there is no policy hot-reload, pending-command store, persistent config cache, or new lifecycle service. Electron launch and host probes retain the same selected confirmation setting. Begin records preserve interrupted intent; a definite no-dispatch/native CLI parse/config/local-inspection rejection restores the previous setting only when no earlier browser helper could have established the request. Other policy flags remain call-scoped.
|
|
140
140
|
|
|
141
141
|
When no native session, attachment, or explicit fresh launch is selected, `sessionMode: "auto"` uses `pi-root-<sha256(root Pi ID)[0:24]>` through the existing caller-owned named-session path. Descendants receive `PI_SUBAGENT_ROOT_SESSION_ID` from pi-subagents; root processes use `ctx.sessionManager.getSessionId()`. No cwd, human fork ancestry, run registry, or process-global assignment participates. The same name is the default native restore key. Existing native daemon inspection runs inside the existing identity queue: inactive roots receive bootstrap defaults, while active roots retain native launch settings and their current restore key. Scoped subprocess environment keeps helper settings consistent; no launch-settings journal is added. Root-name explicit follow-ups retain those defaults; unrelated explicit names do not. Global/override package `browser.defaultProfile` with `policy: "always"` supplies native Chrome profile names only, plus the configured executable, only when the effective engine is Chrome. Paths and project-only profile settings stay advisory. Native config/environment and explicit launch settings win. The communal source is copied by native Chrome launch, never by the wrapper, and each root's restore writes remain separate. Native JSON restore omits IndexedDB, service workers, and page-memory credentials; application-level restart verification is required. Parent and child quit leave the root browser alive; shared execution locks coordinate updated cooperating processes without a broker. An active older/fresh managed session keeps its existing lifecycle. A configured session has the same precedence over `fresh` as a literal `--session`; code follows the selected ordinary browser lifecycle; Electron launch retains its owned lifecycle. Electron launch suppresses the native session environment default only while allocating its generated session; ordinary browser follow-ups still honor the configured native default, so use the returned Electron session explicitly.
|
|
142
142
|
|
|
@@ -151,7 +151,7 @@ Browser-independent calls to an already-owned session reuse its idle timeout, na
|
|
|
151
151
|
|
|
152
152
|
An explicit native `session info` call is the preflight: daemon activity/PID is separate from `runtime.browser` liveness, Chrome PID, exact profile, tabs and launched/attached ownership. Pi adds its own cleanup ownership from existing ownership records. Unknown native fields stay unknown; the wrapper does not infer the browser from daemon/config state or add a host process scan. Native `runtime.recording.current` / `last` receipts and protocol capabilities remain visible. Full fields require companion native support; receipts retained by the native daemon are not durable after it exits.
|
|
153
153
|
|
|
154
|
-
`lib/read-confirmation.ts` recognizes
|
|
154
|
+
`lib/read-confirmation.ts` recognizes strict native control envelopes with actual command provenance. Ordered pending/cleared `readConfirmation` markers in the existing `SessionPageState` and transcript preserve exact ID, canonical identity and compact originating command/native action across resume and branch replay; they never store argv or command blobs. For originating `tab new` commands, the same decoder and presentation validator also recognize paired `after_confirmation: ["open", <original URL>]` / nonempty `guidance` transport metadata, using the existing navigation operand reader and retaining strict control keys. It preserves conditional, redacted prose without persisting or executing continuation arguments; page/eval/HTTP data never acquires that provenance. Exact matching guarded decisions suppress only page/pinning helpers that could overwrite the native pending slot. For native batch this applies only when its first effective row is that matching decision, using native raw-argv-over-stdin selection; remaining page/ref checks stay intact. Completed browser-backed decisions still reconcile page/ref state, report inner failures and retire confirmed close normally; native ID validation is not implied. Explicit URL-read provenance remains separate: only `capabilities.readRequiresConfirmation: true` grants browser independence with native ID matching. Legacy URL reads keep normal checks; unrelated or forged/page-shaped observations gain no exemption. A per-call helper observer updates this same marker and stops later same-identity page helpers after a new pending control. Cold reopen exposes that helper decision without dispatching the original getter or replaying open. Confirmed full snapshots feed the existing ordered ref fold, while pending captures cannot replace completed refs. The existing cleared marker records one-use freshness only for a final successful complete confirmed capture; the next same-identity call consumes it in live state and replay. The semantic visible-ref resolver uses the same eligible capture for role click/check/fill and unique role/label select, preserving editable evidence and action payloads. This avoids gated semantic/freshness-snapshot retry loops without bypassing target, membership, invalidation or native generation checks. Pending snapshots use the common confirmation renderer rather than the empty-snapshot formatter. Required live URL verification replaces the call's remembered target before semantic resolution and direct-ref preflight, dropping old title/target identity when the complete URL changes. The existing stale-ref guard compares the complete approved URL, including fragments, only on one-use confirmed-capture calls; ordinary ref/tab URL comparisons remain fragment-insensitive. Caller-owned, attached and wrapper-owned managed sessions therefore reject refs from a different approved route without requiring resume, restore pinning or another gated snapshot. DOM-only renames on the same page after that sample are not resampled on this one use; ordinary checks resume afterward. Attachment replay honors realized compact close commands, including confirmed closes. There is no second confirmation store or wrapper authorization system. Click-dispatch probes keep their ordinary diagnostic window but self-clean their page listeners and marker after the per-call process timeout plus the existing two-second cleanup grace. Normal cleanup cancels that timer; expiry never needs a browser helper that could overwrite the pending slot.
|
|
155
155
|
|
|
156
156
|
### Native observation state
|
|
157
157
|
|
|
@@ -183,7 +183,7 @@ Practical policy:
|
|
|
183
183
|
- redact snapshot spill payloads before writing them, clean up process-private temp spill artifacts on shutdown, and keep persisted-session spill files in a private session-scoped artifact directory for `details.fullOutputPath` after reload/resume, with a 32 MiB per-session budget by default; `PI_AGENT_BROWSER_SESSION_ARTIFACT_MAX_BYTES=0` skips persistent-spill enumeration and eviction without changing temporary spill cleanup
|
|
184
184
|
- keep explicit screenshots, downloads, PDFs, traces, HAR captures, and recordings written to caller-chosen paths on disk after a successful upstream close command (`close`, `quit`, or `exit`); before artifact-producing commands run, create missing parent directories for requested host paths, while native download always owns the click and download, including loopback links, generated Blob exports, and redirects; never substitute an anchor-href fetch. When the internal recent artifact view has entries, successful close commands also surface `details.artifactCleanup` and a compact `Artifact lifecycle` note pointing to structured explicit paths so operators remove files with normal host tools—the native tool does not delete arbitrary user paths (`extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts`, `getArtifactCleanupGuidance`); contract in [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details), checklist `RQ-0079` in [`SUPPORT_MATRIX.md`](SUPPORT_MATRIX.md)
|
|
185
185
|
- reconstruct the current branch-visible extension-managed session, every transcript-proven still-active wrapper-owned managed identity, page-scoped refs, journal-ordered artifact-row changes, unbounded active-recording reservation events, and Electron launch records from the active transcript branch on `session_start` and `session_tree` so later default and explicit off-current calls keep following owned browsers after resume/reload or branch switching; restore also honors successful explicit `--session <wrapper-owned> close` rows, terminal nested-batch close outcomes even when aggregate artifact verification failed, and `electron.cleanup` managed-session steps so closed wrapper-owned sessions are not resurrected; a nested close invalidates the pre-close page target so a lifecycle-proven relaunch at `about:blank` is not treated as stale focus drift; explicit lifecycle evidence that a later diagnostic did not launch a browser preserves the terminal close, while any later row—including a failed row—whose lifecycle reports a browser launch keeps active/attached provenance; failed-step presentation persists only that bounded launch boolean so transcript replay reaches the same decision, missing lifecycle evidence remains conservatively active even on the first managed call, successful closes clear wrapper trace/profiler ownership before ordered later successful rows can rebuild it, namespace-scoped `close --all` clears all matching managed/attached/page/ref/route/trace/recording ownership, and recording starts after close are rejected before spawn
|
|
186
|
-
- keep active recording destination reservations separate from the bounded metadata-only artifact manifest. The same journal retains native recording IDs and start windows; native receipts, including failures, flow through existing artifact metadata and verification. A stop timeout/no-recording result gets one two-second native `session info` query matching namespace/session, ID/path or effective batch start window. Recovery needs terminal native encoder success plus a matching verified file; filesystem presence alone is insufficient, old same-path receipts cannot verify a new take, and freshness starts at capture rather than status-query time. Original attempts and unrelated batch failures remain visible. No second ledger, recorder, sidecar or runtime ffprobe is added. The process-wide map is keyed by canonical namespace/session identity, rebuilt from append-only branch events, and retained for still-live process-owned recordings across branch switches. Shutdown/reload appends both terminal tombstones and still-live reservations onto the current branch so restart cannot resurrect a cross-branch close or lose a live-daemon reservation. One artifact lifecycle/output queue makes global destination preflight and reservation updates atomic across otherwise-concurrent caller-owned session queues. Every successful direct, ordered nested-batch, managed replacement, code, Electron, or shutdown close retires its exact identity at that lifecycle point; only the newest pending recording path remains authoritative across current transition replay (including same-timestamp restart rows), and recording starts after a nested close are rejected because upstream can falsely report success. Existing and dangling symlink ancestry, hardlink inode identity, full Unicode/platform case folding, and same-call `outputPath` comparison prevent alias reuse. One shared command-token projection mirrors upstream's full-argv global cleanup before artifact, recording, and presentation parsing; wait-download detection removes only the first timeout pair, follows upstream long/short mode precedence, and accepts both `--download` and `-d` wherever download mode wins; screenshot destinations use upstream's exact-flag, selector-prefix, case-sensitive extension, slash-path, and second-positional rules, while retaining the wrapper's intentional slash-bearing hidden-workspace path normalization. Recording path/URL consumers share a command-local reader that skips complete numeric `--fps` pairs, cursor/contact-sheet switches, and contact-sheet threshold values without rewriting argv or treating them as outer globals; native owns rate, format and extra-argument validation. Current recording transitions are replayed directly; artifact manifests are not treated as reservation events
|
|
186
|
+
- keep active recording destination reservations separate from the bounded metadata-only artifact manifest. The same journal retains native recording IDs and start windows; native receipts, including failures, flow through existing artifact metadata and verification. A restart without a terminal previous-take receipt retires the prior pending artifact only when native acknowledges `restarted: true`; rejected or pending restarts retain its reservation and stop action. A stop timeout/no-recording result gets one two-second native `session info` query matching namespace/session, ID/path or effective batch start window. Recovery needs terminal native encoder success plus a matching verified file; filesystem presence alone is insufficient, old same-path receipts cannot verify a new take, and freshness starts at capture rather than status-query time. Original attempts and unrelated batch failures remain visible. No second ledger, recorder, sidecar or runtime ffprobe is added. The process-wide map is keyed by canonical namespace/session identity, rebuilt from append-only branch events, and retained for still-live process-owned recordings across branch switches. Shutdown/reload appends both terminal tombstones and still-live reservations onto the current branch so restart cannot resurrect a cross-branch close or lose a live-daemon reservation. One artifact lifecycle/output queue makes global destination preflight and reservation updates atomic across otherwise-concurrent caller-owned session queues. Every successful direct, ordered nested-batch, managed replacement, code, Electron, or shutdown close retires its exact identity at that lifecycle point; only the newest pending recording path remains authoritative across current transition replay (including same-timestamp restart rows), and recording starts after a nested close are rejected because upstream can falsely report success. Existing and dangling symlink ancestry, hardlink inode identity, full Unicode/platform case folding, and same-call `outputPath` comparison prevent alias reuse. One shared command-token projection mirrors upstream's full-argv global cleanup before artifact, recording, and presentation parsing; wait-download detection removes only the first timeout pair, follows upstream long/short mode precedence, and accepts both `--download` and `-d` wherever download mode wins; screenshot destinations use upstream's exact-flag, selector-prefix, case-sensitive extension, slash-path, and second-positional rules, while retaining the wrapper's intentional slash-bearing hidden-workspace path normalization. Recording path/URL consumers share a command-local reader that skips complete numeric `--fps` pairs, cursor/contact-sheet switches, and contact-sheet threshold values without rewriting argv or treating them as outer globals; native owns rate, format and extra-argument validation. Current recording transitions are replayed directly; artifact manifests are not treated as reservation events
|
|
187
187
|
- keep process-owned cleanup registries for extension-managed sessions and wrapper-launched Electron records separate from the current branch-visible view; `session_tree` restore and wrapper-owned browser commands are serialized with managed-session work, while caller-owned explicit-session commands use local queues and shared execution locks keyed to actual socket context and effective canonical namespace/session across prepare helpers (explicit namespace argv overrides inherited `AGENT_BROWSER_NAMESPACE`, including an explicit empty default) and main execution. macOS and Windows additionally normalize and case-fold namespace and session components to match case-insensitive daemon identity. Different caller-owned identities remain concurrent, except namespace-scoped `close --all` drains and exclusively barriers managed plus matching caller-owned work before clearing global namespace state; nested helpers never re-enter the outer queue, policy/route/artifact deltas merge across unrelated managed-state commits, and a separate branch-restore generation guard prevents stale completions from overwriting newer branch-visible state; artifact-row changes use the existing artifact queue and journal order so replay retains concurrently completed entries. Branch switches still must not drop resources the current Pi process owns and must keep fresh-session allocation monotonic
|
|
188
188
|
- record successful `connect`, `--cdp`, enabled `--auto-connect`, environment-configured CDP/auto-connect, and wrapper Electron attachment identities in branch-visible state. First-use and later content-bearing calls live-check `get url` because attached targets can drift outside Pi. Caller config, file access, launch arguments, and environment pass through unchanged; only wrapper-injected compatibility launch arguments are omitted on active attachments. A terminal successful close removes the marker; a close followed by a later step whose lifecycle reports a browser launch preserves it, while a non-launching diagnostic such as `stream status` leaves the close terminal
|
|
189
189
|
- when a successful close targets the current extension-managed session, including an explicit `--session <current> close` or an `electron.cleanup` managed-session step, clear page/ref state, mark that session inactive, untrack cleanup ownership, and rotate the next default auto call to a fresh wrapper-generated session name rather than reusing the closed name
|
|
@@ -199,7 +199,7 @@ Practical policy:
|
|
|
199
199
|
- keep a per-session `refSnapshot` aligned with the last successful `snapshot` (including refs merged from a successful `batch` by taking the last successful `snapshot` step in batch result order): restore its once-stored ancestral definition when reloading, resuming, or moving to a different Pi session-tree branch, retain full membership and accessible metadata from that capture for wrapper-side diagnostics, drop it on successful close commands (`close`, `quit`, or `exit`), replace it with a persisted `page-transition` invalidation after any upstream-executed `record start` attempt (direct or batch; older supported natives can swap the page before their already-active check, so failed starts count; the warning and persisted summary explicitly describe conservative ref invalidation, not an observed page change), a `record restart` with a URL operand, `window new`, `diff url`, or WebMCP `invoke` / `result` / `cancel` (these page-provided tools can mutate, rerender, or navigate; when a spawned `batch` yields no parseable result rows, for example after a wrapper timeout, planned transition steps still record the invalidation), or after a failed transition command (`eval`, `back`, `forward`, `reload`, `connect`, `state load`, `tab` selection), including reached batch transitions and failed batch `open` rows whose live URL re-verification probe observed the page (a failed transition can still have mutated or replaced the document before throwing, so keeping the verified URL must not keep the prior refs; transcript replay preserves the persisted invalidation summary), and refuse page-scoped `@e…` argv before spawn when the active tab URL no longer matches the snapshot URL, when a ref id was never in that snapshot, when the snapshot state is invalidated, or when a `batch` step would reuse `@e…` on a guarded getter or mutation step after an earlier invalidating step (including `record start`, URL-bearing `record restart`, and WebMCP `invoke` / `result` / `cancel`) without a later `snapshot` step in the same plan; batch steps come from the source upstream actually executes (raw batch argument strings exclusively when any exist — filtering only the exact `--bail` token like upstream — stdin only otherwise, via `getUpstreamEffectiveBatchSteps` in `extensions/agent-browser/lib/orchestration/batch-stdin.ts`); tab recovery (which leaves user argv/stdin and continue-on-error control flow unchanged), artifact/recording preflight, batch screenshot path preparation (parent directories are created for effective raw rows too, without rewriting raw strings), and stale-ref echo args use that same selection so pinning and preflights cannot act on upstream-ignored stdin, while the pre-spawn state-policy validator deliberately keeps scanning parseable stdin alongside argv as a fail-closed content superset and treats stdin parse failures as fatal only when upstream would actually read stdin (its raw-token filter also uses the exact `--bail` token only). Same-snapshot `fill @e…` rows are guarded but do not themselves set that invalidation latch, so ordinary form fills can precede a click/submit row in one batch—see [`TOOL_CONTRACT.md`](TOOL_CONTRACT.md#details) for the agent-visible contract and failure text; typed per-session tab/ref/pinning state lives in `extensions/agent-browser/lib/session-page-state.ts` and is updated from `extensions/agent-browser/index.ts` after each tool result
|
|
200
200
|
- native 0.37 owns first-document setup inheritance for new tabs; the wrapper does not copy headers, user agents or other browser setup itself. The page-summary formatter displays native positive WebMCP availability while retaining raw details; it adds no discovery probe
|
|
201
201
|
- when a direct or batched WebMCP call returns `status: "pending"`, or `result` / `cancel` fails while that target is unknown, keep its tab target unknown and discard same-call snapshot evidence instead of treating an immediate post-dispatch URL probe as stable; `webmcp result` / `cancel`, `get url`, and explicit navigation remain available while unknown; replace the generic blocked snapshot action with `verify-page-target-after-pending-webmcp` (`get url`), and let a completed `batch --bail` use that verification before `snapshot -i` to re-establish both target and refs
|
|
202
|
-
- for top-level non-Electron direct `click` commands with an eligible target, install a bounded in-page target-specific event probe before upstream runs; if upstream reports success but no trusted pointer/mouse/click event reached the resolved target, fail the tool and report `details.clickDispatch` with explicit retry/inspect next actions (the wrapper does not replay clicks in-page). The probe covers `xpath=` targets and current `@e…` / `ref=` refs whose latest stored `refSnapshot.refs` role is `button`, `checkbox`, `menuitem`, `radio`, `switch`, or `tab`; it requires a unique role/name in the saved snapshot and the live candidates instead of taking a fresh pre-click snapshot that could recycle upstream refs. Ref and XPath candidates must additionally prove identity through a per-probe temporary DOM attribute read back by native `get attr <original-selector> <marker>`. Missing or mismatched identity, including XPath/frame scope mismatches, cleans up the probe and lets native click run without dispatch verification; candidate naming heuristics alone are never proof. Duplicate-name refs pass through without a probe: their old ordinal is not target identity. The probe is intentionally skipped for CSS selector clicks, unresolved `find … click` locators, and `batch`/`qa` click steps
|
|
202
|
+
- for top-level non-Electron direct `click` commands with an eligible target, install a bounded in-page target-specific event probe before upstream runs; if upstream reports success but no trusted pointer/mouse/click event reached the resolved target, fail the tool and report `details.clickDispatch` with explicit retry/inspect next actions (the wrapper does not replay clicks in-page). The probe covers `xpath=` targets and current `@e…` / `ref=` refs whose latest stored `refSnapshot.refs` role is `button`, `checkbox`, `menuitem`, `radio`, `switch`, or `tab`; it requires a unique role/name in the saved snapshot and the live candidates instead of taking a fresh pre-click snapshot that could recycle upstream refs. Ref and XPath candidates must additionally prove identity through a per-probe temporary DOM attribute read back by native `get attr <original-selector> <marker>`. Missing or mismatched identity, including XPath/frame scope mismatches, cleans up the probe and lets native click run without dispatch verification; candidate naming heuristics alone are never proof. Duplicate-name refs pass through without a probe: their old ordinal is not target identity. The probe is intentionally skipped when the effective native confirmation list guards `evaluate`, avoiding an optional-eval approval loop without bypassing native click policy. Dynamic action-policy files stay native-owned. It is also skipped for CSS selector clicks, unresolved `find … click` locators, and `batch`/`qa` click steps
|
|
203
203
|
- skip unrelated tools and harmless Bash inspections before building prompt policy. Seed the latest raw user message on start/tree, update it on user `message_end`, and re-read its current content before parsing policy so native content replacement cannot leave stale intent. Capture that policy at queued admission without a whole-branch scan on every event. Derive narrow prompt guards only for concrete evidence invariants: explicitly requested screenshot/recording output paths block browser close until the artifact manifest verifies those paths, while bare inbound attachment paths remain inputs. The wrapper intentionally does not infer broad business/user intent from prompt text such as order/payment/post boundaries; agents must follow those instructions themselves. The artifact guard is bounded preflight policy (`details.promptGuard`, `failureCategory: "policy-blocked"`), not a reusable browser recipe layer
|
|
204
204
|
- keep the existing page/selector scroll API; explicit CSS-container movement uses `Element.scrollBy` with `behavior: "instant"` before measuring offsets so smooth-scroll CSS cannot cause a premature no-movement failure
|
|
205
205
|
- reject direct and effective batch `scrollintoview text=...` / `scrollinto text=...` before dispatch because current upstream can falsely report success without movement, while leaving help forms untouched; return only native recovery (`find text ... hover` or fresh snapshot/ref), leaving CSS, XPath, and current-ref behavior upstream-owned
|