pi-agent-browser-native 0.2.77 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +69 -35
  3. package/dist/extensions/agent-browser/index.js +911 -121
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -8
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +48 -7
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +1 -6
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +35 -8
  9. package/dist/extensions/agent-browser/lib/config-policy.js +0 -5
  10. package/dist/extensions/agent-browser/lib/config.js +0 -6
  11. package/dist/extensions/agent-browser/lib/electron/cdp.js +6 -7
  12. package/dist/extensions/agent-browser/lib/electron/cleanup.js +0 -7
  13. package/dist/extensions/agent-browser/lib/electron/discovery.js +0 -7
  14. package/dist/extensions/agent-browser/lib/electron/launch.js +0 -7
  15. package/dist/extensions/agent-browser/lib/input-modes/electron.js +0 -5
  16. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -9
  17. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -7
  18. package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -29
  19. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  20. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -17
  21. package/dist/extensions/agent-browser/lib/input-modes/shared.js +0 -5
  22. package/dist/extensions/agent-browser/lib/input-modes/types.js +0 -5
  23. package/dist/extensions/agent-browser/lib/json-schema.js +0 -5
  24. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +12 -5
  25. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -2
  26. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +3 -4
  27. package/dist/extensions/agent-browser/lib/managed-session-restore.js +55 -48
  28. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -9
  29. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +48 -36
  30. package/dist/extensions/agent-browser/lib/managed-session-storage.js +50 -29
  31. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -5
  32. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  33. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  34. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  35. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +27 -26
  36. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +58 -33
  37. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  38. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +22 -14
  39. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +1 -1
  40. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +1 -1
  41. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +1 -1
  42. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +1 -1
  43. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +148 -34
  44. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +225 -61
  45. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +74 -30
  46. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +47 -26
  47. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +40 -19
  48. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +42 -1
  49. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  50. package/dist/extensions/agent-browser/lib/parsing.js +0 -7
  51. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +36 -11
  52. package/dist/extensions/agent-browser/lib/playbook.js +20 -24
  53. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  54. package/dist/extensions/agent-browser/lib/process-identity.js +4 -9
  55. package/dist/extensions/agent-browser/lib/process.js +131 -48
  56. package/dist/extensions/agent-browser/lib/prompt-policy.js +171 -27
  57. package/dist/extensions/agent-browser/lib/recording-reservations.js +183 -0
  58. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -13
  59. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +68 -11
  60. package/dist/extensions/agent-browser/lib/results/categories.js +7 -9
  61. package/dist/extensions/agent-browser/lib/results/confirmation.js +0 -7
  62. package/dist/extensions/agent-browser/lib/results/contracts.js +0 -7
  63. package/dist/extensions/agent-browser/lib/results/editable-ref-evidence.js +0 -7
  64. package/dist/extensions/agent-browser/lib/results/envelope.js +0 -7
  65. package/dist/extensions/agent-browser/lib/results/network.js +0 -7
  66. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -35
  67. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -44
  68. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +66 -11
  69. package/dist/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.js +0 -5
  70. package/dist/extensions/agent-browser/lib/results/presentation/common.js +18 -5
  71. package/dist/extensions/agent-browser/lib/results/presentation/content.js +0 -5
  72. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +7 -6
  73. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  74. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +0 -5
  75. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -5
  76. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +26 -16
  77. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +58 -13
  78. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -15
  79. package/dist/extensions/agent-browser/lib/results/presentation/skills.js +0 -5
  80. package/dist/extensions/agent-browser/lib/results/presentation.js +6 -8
  81. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +2 -7
  82. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +0 -7
  83. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +51 -15
  84. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -14
  85. package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +0 -7
  86. package/dist/extensions/agent-browser/lib/results/snapshot-segments.js +0 -7
  87. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +0 -7
  88. package/dist/extensions/agent-browser/lib/results/snapshot.js +0 -7
  89. package/dist/extensions/agent-browser/lib/results/text.js +0 -7
  90. package/dist/extensions/agent-browser/lib/runtime.js +119 -52
  91. package/dist/extensions/agent-browser/lib/session-page-state.js +62 -17
  92. package/dist/extensions/agent-browser/lib/string-enum-schema.js +0 -5
  93. package/dist/extensions/agent-browser/lib/temp.js +0 -7
  94. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  95. package/dist/extensions/agent-browser/lib/web-search.js +0 -5
  96. package/dist/extensions/agent-browser/script-worker.js +169 -0
  97. package/dist/scripts/agent-browser-target.mjs +3 -0
  98. package/docs/ARCHITECTURE.md +50 -31
  99. package/docs/COMMAND_REFERENCE.md +97 -41
  100. package/docs/ELECTRON.md +3 -19
  101. package/docs/RELEASE.md +8 -20
  102. package/docs/REQUIREMENTS.md +8 -6
  103. package/docs/SUPPORT_MATRIX.md +32 -25
  104. package/docs/TOOL_CONTRACT.md +107 -64
  105. package/docs/platform-smoke.md +1 -1
  106. package/package.json +7 -6
  107. package/platform-smoke.config.mjs +2 -2
  108. package/scripts/agent-browser-capability-baseline.mjs +24 -6
  109. package/scripts/agent-browser-target.mjs +3 -0
  110. package/scripts/build.mjs +41 -0
  111. package/scripts/doctor.mjs +8 -7
  112. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  113. package/scripts/platform-smoke/targets.mjs +12 -6
  114. package/dist/extensions/agent-browser/lib/electron/text.js +0 -13
  115. package/dist/extensions/agent-browser/lib/input-modes.js +0 -10
  116. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.js +0 -5
  117. package/dist/extensions/agent-browser/lib/results/artifact-state.js +0 -13
  118. package/dist/extensions/agent-browser/lib/results.js +0 -14
@@ -1,12 +1,14 @@
1
1
  import { rm } from "node:fs/promises";
2
+ import { getAgentBrowserSessionIdentityKey, VALUE_FLAGS } from "../../argv-grammar.js";
2
3
  import { runAgentBrowserProcess } from "../../process.js";
3
- import { buildAgentBrowserNextActions, parseAgentBrowserEnvelope } from "../../results.js";
4
+ import { buildAgentBrowserNextActions } from "../../results/action-recommendations.js";
5
+ import { parseAgentBrowserEnvelope } from "../../results/envelope.js";
4
6
  import { buildNextToolAction, withOptionalNamespaceArgs, withOptionalSessionArgs } from "../../results/next-actions.js";
5
7
  import { getSessionPageStateKey, isAboutBlankUrl, normalizeComparableUrl, normalizeSessionTabTarget, targetsMatch, } from "../../session-page-state.js";
6
8
  import { isCloseCommand, isElectronPostCommandHealthCommand, isNavigationObservableCommandName, isRefGuardedCommand, isRefInvalidatingBatchCommand, isSessionTabPinningExcludedCommand, isSessionTabPostCommandCorrectionExcludedCommand, } from "../../command-taxonomy.js";
7
9
  import { chooseOpenResultTabCorrection } from "../../runtime.js";
8
10
  import { isRecord } from "../../parsing.js";
9
- import { parseUserBatchStdin } from "../batch-stdin.js";
11
+ import { getUpstreamEffectiveBatchSteps, parseUserBatchStdin } from "../batch-stdin.js";
10
12
  export const NAVIGATION_SUMMARY_EVAL = `({ title: document.title, url: location.href })`;
11
13
  export function applyBrowserRunStatePatch(state, patch) {
12
14
  if (!patch)
@@ -21,6 +23,10 @@ export function applyBrowserRunStatePatch(state, patch) {
21
23
  state.managedSessionActive = patch.managedSessionActive;
22
24
  if ("managedSessionCompatibilityWorkaround" in patch)
23
25
  state.managedSessionCompatibilityWorkaround = patch.managedSessionCompatibilityWorkaround;
26
+ if (patch.managedSessionHeadedAutosaveDisabled !== undefined)
27
+ state.managedSessionHeadedAutosaveDisabled = patch.managedSessionHeadedAutosaveDisabled;
28
+ if ("managedSessionHeadedAutosaveInterval" in patch)
29
+ state.managedSessionHeadedAutosaveInterval = patch.managedSessionHeadedAutosaveInterval;
24
30
  if (patch.managedSessionCwd !== undefined)
25
31
  state.managedSessionCwd = patch.managedSessionCwd;
26
32
  if (patch.managedSessionName !== undefined)
@@ -33,7 +39,7 @@ export function applyBrowserRunStatePatch(state, patch) {
33
39
  export const getSessionContextKey = getSessionPageStateKey;
34
40
  export function buildSessionDetailFields(sessionName, usedImplicitSession, namespace, managedSessionRestoreDisabled = false) {
35
41
  return {
36
- ...(namespace ? { namespace } : {}),
42
+ ...(namespace !== undefined ? { namespace } : {}),
37
43
  ...(sessionName
38
44
  ? {
39
45
  sessionName,
@@ -136,6 +142,12 @@ function formatManagedSessionOutcomeRecoveryGuidance(outcome) {
136
142
  export function formatManagedSessionOutcomeText(outcome) {
137
143
  if (!outcome)
138
144
  return undefined;
145
+ if (outcome.replacedSessionClosed === false) {
146
+ const cleanupWarning = "Cleanup warning: Automatic close of the previous wrapper-managed session failed, so it remains wrapper-owned. Use details.managedSessionOutcome for its exact identity and close it explicitly when safe.";
147
+ return outcome.succeeded
148
+ ? ["Managed session outcome: The fresh browser became current.", cleanupWarning].join("\n")
149
+ : [formatManagedSessionOutcomeHeadline(outcome), formatManagedSessionOutcomeRecoveryGuidance(outcome), cleanupWarning].join("\n");
150
+ }
139
151
  if (outcome.status === "closed" && outcome.succeeded) {
140
152
  return [
141
153
  "Managed session outcome: The current wrapper-managed browser session was closed.",
@@ -202,10 +214,15 @@ export function getTraceOwnerGuardMessage(options) {
202
214
  : `Wrapper believes tracing for session ${options.sessionName} is owned by ${activeOwner}; run ${activeOwner} stop instead of ${owner} stop.`;
203
215
  }
204
216
  export function updateTraceOwnerState(options) {
205
- const owner = getTraceOwner(options.command);
206
- if (!owner || !options.sessionName || !options.succeeded) {
217
+ if (!options.sessionName || !options.succeeded)
218
+ return;
219
+ if (isCloseCommand(options.command)) {
220
+ options.traceOwners.delete(options.sessionName);
207
221
  return;
208
222
  }
223
+ const owner = getTraceOwner(options.command);
224
+ if (!owner)
225
+ return;
209
226
  if (options.subcommand === "start") {
210
227
  options.traceOwners.set(options.sessionName, owner);
211
228
  }
@@ -237,8 +254,6 @@ export function extractNavigationSummaryFromData(data) {
237
254
  export function shouldCaptureNavigationSummary(command, data) {
238
255
  if (command === "eval")
239
256
  return true;
240
- if (isRecord(data) && typeof data.clicked === "string" && !data.clicked.startsWith("@") && !data.clicked.startsWith("ref=") && typeof data.href !== "string")
241
- return false;
242
257
  return (isNavigationObservableCommandName(command) &&
243
258
  (!isRecord(data) || (typeof data.title !== "string" && typeof data.url !== "string")));
244
259
  }
@@ -258,29 +273,42 @@ function extractBatchResultCommand(item) {
258
273
  return Array.isArray(item.command) ? item.command.filter((token) => typeof token === "string") : [];
259
274
  }
260
275
  export function getStaleRefArgs(commandTokens, stdin) {
261
- if (commandTokens[0] !== "batch" || stdin === undefined) {
262
- return commandTokens;
263
- }
264
- const parsed = parseUserBatchStdin(stdin);
265
- if (parsed.error || parsed.steps === undefined) {
276
+ if (commandTokens[0] !== "batch") {
266
277
  return commandTokens;
267
278
  }
268
- return parsed.steps.flatMap((step) => step);
279
+ const steps = getUpstreamEffectiveBatchSteps(commandTokens, stdin);
280
+ return steps.length > 0 ? steps.flatMap((step) => step) : commandTokens;
281
+ }
282
+ // Selector-carrying flags whose values remain page-scoped refs; values of other value-taking flags
283
+ // (--text, --baseline, --name, and similar) are literal text or paths that may merely look like refs,
284
+ // per upstream parsing. Boolean flags such as --new-tab or --full do not consume the following token,
285
+ // so a ref after them is a genuine positional selector and must stay guarded.
286
+ const SELECTOR_FLAG_TOKENS = new Set(["--selector", "-s"]);
287
+ const SHORT_VALUE_FLAG_TOKENS = new Set(["-b", "-o", "-t"]);
288
+ function isNonSelectorValueFlagToken(token) {
289
+ if (token === undefined || SELECTOR_FLAG_TOKENS.has(token))
290
+ return false;
291
+ return VALUE_FLAGS.has(token) || SHORT_VALUE_FLAG_TOKENS.has(token);
269
292
  }
270
293
  function collectRefsFromTokens(tokens) {
271
- return tokens.filter((token) => /^@e\d+\b/.test(token)).map((token) => token.slice(1));
294
+ const refs = [];
295
+ for (const [index, token] of tokens.entries()) {
296
+ if (!/^@e\d+\b/.test(token))
297
+ continue;
298
+ if (isNonSelectorValueFlagToken(index > 0 ? tokens[index - 1] : undefined))
299
+ continue;
300
+ refs.push(token.slice(1));
301
+ }
302
+ return refs;
272
303
  }
273
304
  export function getGuardedRefUsage(commandTokens, stdin, options = {}) {
274
305
  const collectFromStep = (step) => isRefGuardedCommand(step[0]) ? collectRefsFromTokens(step) : [];
275
- if (commandTokens[0] !== "batch" || stdin === undefined) {
276
- return collectFromStep(commandTokens);
277
- }
278
- const parsed = parseUserBatchStdin(stdin);
279
- if (parsed.error || parsed.steps === undefined) {
306
+ if (commandTokens[0] !== "batch") {
280
307
  return collectFromStep(commandTokens);
281
308
  }
309
+ const steps = getUpstreamEffectiveBatchSteps(commandTokens, stdin);
282
310
  const refsBeforeInBatchSnapshot = [];
283
- for (const step of parsed.steps) {
311
+ for (const step of steps) {
284
312
  if (!options.includeRefsAfterBatchSnapshot && (step[0] ?? "") === "snapshot")
285
313
  break;
286
314
  refsBeforeInBatchSnapshot.push(...collectFromStep(step));
@@ -307,13 +335,10 @@ function isSafeSameSnapshotFormBatchStep(step, refSnapshot) {
307
335
  return false;
308
336
  }
309
337
  function getBatchRefInvalidationMessage(commandTokens, stdin, refSnapshot) {
310
- if (commandTokens[0] !== "batch" || stdin === undefined)
311
- return undefined;
312
- const parsed = parseUserBatchStdin(stdin);
313
- if (parsed.error || parsed.steps === undefined)
338
+ if (commandTokens[0] !== "batch")
314
339
  return undefined;
315
340
  let priorStepInvalidatesRefs = false;
316
- for (const step of parsed.steps) {
341
+ for (const step of getUpstreamEffectiveBatchSteps(commandTokens, stdin)) {
317
342
  if ((step[0] ?? "") === "snapshot") {
318
343
  priorStepInvalidatesRefs = false;
319
344
  }
@@ -321,7 +346,7 @@ function getBatchRefInvalidationMessage(commandTokens, stdin, refSnapshot) {
321
346
  if (refIds.length > 0 && isRefGuardedCommand(step[0]) && priorStepInvalidatesRefs) {
322
347
  return `Batch step ${step[0]} uses page-scoped ref ${refIds.map((refId) => `@${refId}`).join(", ")} after an earlier batch step can navigate or mutate the page. Split the batch, run snapshot -i after the page-changing step, then retry with current refs.`;
323
348
  }
324
- if (isRefInvalidatingBatchCommand(step[0]) && !isSafeSameSnapshotFormBatchStep(step, refSnapshot)) {
349
+ if (isRefInvalidatingBatchCommand(step) && !isSafeSameSnapshotFormBatchStep(step, refSnapshot)) {
325
350
  priorStepInvalidatesRefs = true;
326
351
  }
327
352
  }
@@ -344,7 +369,9 @@ export function buildStaleRefPreflight(options) {
344
369
  return undefined;
345
370
  if (options.refSnapshotInvalidation) {
346
371
  return {
347
- message: `Ref ${usedRefIds.map((refId) => `@${refId}`).join(", ")} cannot be used because the latest snapshot for this session reported No active page. Run snapshot -i successfully before using page-scoped refs.`,
372
+ message: options.refSnapshotInvalidation.reason === "page-transition"
373
+ ? `Ref ${usedRefIds.map((refId) => `@${refId}`).join(", ")} cannot be used yet. ${options.refSnapshotInvalidation.summary}`
374
+ : `Ref ${usedRefIds.map((refId) => `@${refId}`).join(", ")} cannot be used because the latest snapshot for this session reported No active page. Run snapshot -i successfully before using page-scoped refs.`,
348
375
  refIds: usedRefIds,
349
376
  snapshotInvalidation: options.refSnapshotInvalidation,
350
377
  };
@@ -390,11 +417,22 @@ export function shouldPinSessionTabForCommand(options) {
390
417
  }
391
418
  export function buildPinnedBatchPlan(options) {
392
419
  if (options.command === "batch") {
420
+ const tabSelectionStep = ["tab", options.selectedTab];
421
+ // Upstream executes raw argument steps exclusively when any exist, so the
422
+ // pinned rewrite must dispatch those same steps rather than resurrecting
423
+ // caller stdin the guard never scanned.
424
+ const argumentSteps = getUpstreamEffectiveBatchSteps(options.commandTokens, undefined);
425
+ if (argumentSteps.length > 0) {
426
+ return {
427
+ includeNavigationSummary: false,
428
+ steps: [tabSelectionStep, ...argumentSteps],
429
+ unwrapMode: "user-batch",
430
+ };
431
+ }
393
432
  const parsed = parseUserBatchStdin(options.stdin);
394
433
  if (parsed.error) {
395
434
  return { error: parsed.error };
396
435
  }
397
- const tabSelectionStep = ["tab", options.selectedTab];
398
436
  return {
399
437
  includeNavigationSummary: false,
400
438
  steps: [tabSelectionStep, ...(parsed.steps ?? [])],
@@ -602,10 +640,11 @@ export function electronTargetLabel(target) {
602
640
  export function getActiveElectronRecords(records) {
603
641
  return [...records.values()].filter((record) => record.cleanupState === "active" || record.cleanupState === "dead" || record.cleanupState === "partial" || record.cleanupState === "failed");
604
642
  }
605
- export function findElectronLaunchRecordForSession(sessionName, records) {
643
+ export function findElectronLaunchRecordForSession(sessionName, records, namespace) {
606
644
  if (!sessionName)
607
645
  return undefined;
608
- return getActiveElectronRecords(records).find((record) => record.sessionName === sessionName);
646
+ const sessionKey = getAgentBrowserSessionIdentityKey(sessionName, namespace);
647
+ return getActiveElectronRecords(records).find((record) => record.sessionName && getAgentBrowserSessionIdentityKey(record.sessionName, record.namespace) === sessionKey);
609
648
  }
610
649
  function buildElectronReattachNextAction(record, liveTarget) {
611
650
  const endpoint = liveTarget?.webSocketDebuggerUrl ?? record.webSocketDebuggerUrl ?? String(record.port);
@@ -753,3 +792,8 @@ export function formatElectronRefFreshnessText(diagnostic) {
753
792
  return diagnostic?.summary;
754
793
  }
755
794
  export { extractBatchResultCommand };
795
+ export function getPersistentSessionArtifactStore(ctx) {
796
+ const sessionDir = typeof ctx.sessionManager.getSessionDir === "function" ? ctx.sessionManager.getSessionDir() : undefined;
797
+ const sessionId = ctx.sessionManager.getSessionId();
798
+ return sessionDir && sessionId ? { sessionDir, sessionId } : undefined;
799
+ }
@@ -1,22 +1,18 @@
1
- /**
2
- * Purpose: Own wrapper-side Electron host orchestration for agent_browser structured electron input.
3
- * Responsibilities: Discover Electron apps, inspect/probe/cleanup wrapper-tracked Electron launches, and build Pi-facing Electron host results.
4
- * Scope: Electron host actions that do not spawn the main upstream browser command; generic agent_browser execution stays in browser-run.
5
- */
6
1
  import { cleanupElectronLaunchResources, inspectElectronLaunchStatus } from "../../electron/cleanup.js";
7
2
  import { discoverElectronApps } from "../../electron/discovery.js";
8
- import { boundElectronProbeString } from "../../electron/text.js";
3
+ import { boundElectronProbeString } from "../../electron/cdp.js";
9
4
  import { buildOwnedManagedSessionRestoreContext, withOwnedManagedSessionContext, } from "../../managed-session-restore.js";
10
5
  import { getManagedSessionStateAccessValidationError } from "../../managed-session-state-policy.js";
11
6
  import { isRecord } from "../../parsing.js";
12
7
  import { withAttachedBrowserSessionContext } from "../../process.js";
13
- import { buildAgentBrowserNextActions, buildAgentBrowserResultCategoryDetails } from "../../results.js";
8
+ import { buildAgentBrowserNextActions } from "../../results/action-recommendations.js";
9
+ import { buildAgentBrowserResultCategoryDetails } from "../../results/categories.js";
14
10
  import { appendUniqueAgentBrowserNextActions } from "../../results/next-actions.js";
15
11
  import { extractRefSnapshotFromData, getSessionPageStateKey, isAboutBlankUrl, normalizeSessionTabTarget } from "../../session-page-state.js";
16
12
  import { redactSensitiveText } from "../../runtime.js";
17
13
  import { collectElectronManagedSessionTarget } from "../browser-run/diagnostics.js";
18
14
  import { buildElectronHostFailureResult, formatElectronTargetLines, redactToolDetails } from "../browser-run/final-result.js";
19
- import { acquireOwnedManagedSessionDaemonPolicy, closeManagedSession } from "../browser-run/managed-session-daemon-policy.js";
15
+ import { acquireOwnedManagedSessionDaemonPolicy, closeManagedSession, getRunningHeadedAutosavePolicyChangeError } from "../browser-run/managed-session-daemon-policy.js";
20
16
  import { buildElectronIdentifiers, buildElectronMismatchNextActions, buildElectronSessionMismatch, extractStringResultField, findElectronLaunchRecordForSession, formatElectronSessionMismatchText, getActiveElectronRecords, getLiveElectronRendererTargets, runSessionCommandData, } from "../browser-run/session-state.js";
21
17
  const ELECTRON_PROFILE_ISOLATION_NOTE = "Profile note: electron.launch starts an isolated temporary profile; it does not reuse the app's normal signed-in profile or attach to an already-running authenticated app.";
22
18
  const ELECTRON_EXISTING_AUTH_GUIDANCE = "For already-authenticated desktop app content, do not stop here: if host tools are allowed and the app is not running, launch the normal app with --remote-debugging-port=<port>, verify the port, then run agent_browser connect <port>; if it is already running without a debug port, ask before relaunching it.";
@@ -118,6 +114,7 @@ function isElectronLaunchRecord(value) {
118
114
  return false;
119
115
  return value.version === 1 &&
120
116
  value.launchedByWrapper === true &&
117
+ (value.namespace === undefined || typeof value.namespace === "string") &&
121
118
  typeof value.launchId === "string" &&
122
119
  typeof value.appName === "string" &&
123
120
  typeof value.executablePath === "string" &&
@@ -137,13 +134,14 @@ export function restoreElectronLaunchRecordsFromBranch(branch) {
137
134
  const electron = isRecord(details?.electron) ? details.electron : undefined;
138
135
  if (!electron)
139
136
  continue;
137
+ const namespace = typeof details?.namespace === "string" ? details.namespace : undefined;
140
138
  const launch = isElectronLaunchRecord(electron.launch) ? electron.launch : undefined;
141
139
  if (launch)
142
- records.set(launch.launchId, launch);
140
+ records.set(launch.launchId, { ...launch, namespace: launch.namespace ?? namespace });
143
141
  const cleanupRecords = isRecord(electron.cleanup) && Array.isArray(electron.cleanup.records) ? electron.cleanup.records : [];
144
142
  for (const cleanupRecord of cleanupRecords) {
145
143
  if (isElectronLaunchRecord(cleanupRecord))
146
- records.set(cleanupRecord.launchId, cleanupRecord);
144
+ records.set(cleanupRecord.launchId, { ...cleanupRecord, namespace: cleanupRecord.namespace ?? namespace });
147
145
  }
148
146
  }
149
147
  return records;
@@ -398,9 +396,14 @@ function getElectronProbeSummary(probe) {
398
396
  class ElectronManagedSessionPolicyError extends Error {
399
397
  }
400
398
  async function withOwnedElectronManagedSessionPolicy(options, run) {
399
+ const autosavePolicyChangeError = getRunningHeadedAutosavePolicyChangeError(options.headedManagedAutosaveInterval);
400
+ if (autosavePolicyChangeError)
401
+ throw new ElectronManagedSessionPolicyError(autosavePolicyChangeError);
401
402
  const context = buildOwnedManagedSessionRestoreContext({
402
403
  args: ["--namespace", options.namespace ?? "", "--session", options.sessionName, ...options.args],
403
404
  cwd: options.cwd,
405
+ headedManagedAutosaveDisabled: options.headedManagedAutosaveDisabled,
406
+ headedManagedAutosaveInterval: options.headedManagedAutosaveInterval,
404
407
  managedSessionName: options.sessionName,
405
408
  namespace: options.namespace,
406
409
  restoreState: options.restoreState,
@@ -427,7 +430,7 @@ async function withOwnedElectronManagedSessionPolicy(options, run) {
427
430
  }
428
431
  async function collectOwnedElectronManagedSessionTarget(options) {
429
432
  try {
430
- return await withOwnedElectronManagedSessionPolicy({ ...options, args: ["get", "url"] }, async () => await collectElectronManagedSessionTarget({
433
+ const target = await withOwnedElectronManagedSessionPolicy({ ...options, args: ["get", "url"] }, async () => await collectElectronManagedSessionTarget({
431
434
  allowManagedSessionTarget: true,
432
435
  cwd: options.cwd,
433
436
  namespace: options.namespace,
@@ -435,9 +438,10 @@ async function collectOwnedElectronManagedSessionTarget(options) {
435
438
  signal: options.signal,
436
439
  timeoutMs: options.timeoutMs,
437
440
  })) ?? { sessionName: options.sessionName };
441
+ return { ...target, namespace: options.namespace };
438
442
  }
439
443
  catch (error) {
440
- return { error: error instanceof Error ? error.message : String(error), sessionName: options.sessionName };
444
+ return { error: error instanceof Error ? error.message : String(error), namespace: options.namespace, sessionName: options.sessionName };
441
445
  }
442
446
  }
443
447
  async function runElectronProbeCommandData(options) {
@@ -581,6 +585,8 @@ function buildElectronProbeResult(options) {
581
585
  },
582
586
  nextActions: nextActions.length > 0 ? nextActions : undefined,
583
587
  ...buildAgentBrowserResultCategoryDetails({ args: [], succeeded: true }),
588
+ managedSessionHeadedAutosaveDisabled: options.headedManagedAutosaveDisabled === true ? true : undefined,
589
+ managedSessionHeadedAutosaveInterval: options.headedManagedAutosaveInterval,
584
590
  namespace: options.namespace,
585
591
  refSnapshot: options.probe.refSnapshot,
586
592
  sessionName: options.probe.sessionName,
@@ -597,8 +603,10 @@ function buildElectronProbeResult(options) {
597
603
  export async function cleanupTrackedElectronHostLaunches(options) {
598
604
  const results = [];
599
605
  for (const record of options.records) {
606
+ const sessionKey = getSessionPageStateKey(record.sessionName, record.namespace) ?? record.sessionName;
607
+ const managedSessionOwner = sessionKey ? options.ownedManagedSessions.get(sessionKey) : undefined;
600
608
  const managedSessionCloseError = record.sessionName
601
- ? await closeManagedSession({ cwd: options.cwd, preserveAttachedBrowserSession: options.attachedSessionKeys.has(getSessionPageStateKey(record.sessionName) ?? record.sessionName), restoreState: options.managedSessionRestoreState, sessionName: record.sessionName, timeoutMs: options.timeoutMs })
609
+ ? await closeManagedSession({ cwd: options.cwd, headedManagedAutosaveInterval: managedSessionOwner?.headedManagedAutosaveInterval, namespace: record.namespace, preserveAttachedBrowserSession: options.attachedSessionKeys.has(sessionKey ?? record.sessionName), restoreState: options.managedSessionRestoreState, sessionName: record.sessionName, timeoutMs: options.timeoutMs })
602
610
  : undefined;
603
611
  const managedSessionStep = record.sessionName
604
612
  ? managedSessionCloseError
@@ -643,11 +651,11 @@ export async function cleanupActiveElectronHostLaunches(options) {
643
651
  export async function handleElectronHostInput(options) {
644
652
  const currentSessionKey = getSessionPageStateKey(options.managedSessionName, options.managedSessionNamespace) ?? options.managedSessionName;
645
653
  const preserveAttachedBrowserSession = options.attachedSessionKeys.has(currentSessionKey)
646
- || [...options.electronLaunchRecords.values()].some((record) => record.sessionName !== undefined && options.attachedSessionKeys.has(getSessionPageStateKey(record.sessionName) ?? record.sessionName));
654
+ || [...options.electronLaunchRecords.values()].some((record) => record.sessionName !== undefined && options.attachedSessionKeys.has(getSessionPageStateKey(record.sessionName, record.namespace) ?? record.sessionName));
647
655
  return await withAttachedBrowserSessionContext(preserveAttachedBrowserSession, () => handleElectronHostInputInContext(options));
648
656
  }
649
657
  async function handleElectronHostInputInContext(options) {
650
- const { attachedSessionKeys, compiledElectron, cwd, electronChildProcesses, electronLaunchRecords, implicitSessionCloseTimeoutMs, managedSessionActive, managedSessionName, managedSessionNamespace, managedSessionRestoreState, redactedCompiledElectron, sessionPageState, signal, } = options;
658
+ const { attachedSessionKeys, compiledElectron, cwd, electronChildProcesses, electronLaunchRecords, implicitSessionCloseTimeoutMs, managedSessionActive, managedSessionName, managedSessionNamespace, managedSessionRestoreState, ownedManagedSessions, redactedCompiledElectron, sessionPageState, signal, } = options;
651
659
  if (compiledElectron?.action === "list") {
652
660
  try {
653
661
  const discovery = await discoverElectronApps({ maxResults: compiledElectron.maxResults, query: compiledElectron.query });
@@ -665,16 +673,22 @@ async function handleElectronHostInputInContext(options) {
665
673
  const statuses = await Promise.all(records.map((record) => inspectElectronLaunchStatus(record)));
666
674
  const managedSessions = await Promise.all(records
667
675
  .filter((record) => typeof record.sessionName === "string")
668
- .map((record) => collectOwnedElectronManagedSessionTarget({
669
- cwd,
670
- restoreState: managedSessionRestoreState,
671
- sessionName: record.sessionName,
672
- signal,
673
- timeoutMs: compiledElectron.timeoutMs,
674
- })));
676
+ .map((record) => {
677
+ const sessionKey = getSessionPageStateKey(record.sessionName, record.namespace) ?? record.sessionName;
678
+ return collectOwnedElectronManagedSessionTarget({
679
+ cwd,
680
+ headedManagedAutosaveDisabled: ownedManagedSessions.get(sessionKey)?.headedManagedAutosaveDisabled,
681
+ headedManagedAutosaveInterval: ownedManagedSessions.get(sessionKey)?.headedManagedAutosaveInterval,
682
+ namespace: record.namespace,
683
+ restoreState: managedSessionRestoreState,
684
+ sessionName: record.sessionName,
685
+ signal,
686
+ timeoutMs: compiledElectron.timeoutMs,
687
+ });
688
+ }));
675
689
  const mismatches = managedSessions
676
690
  .map((managedSession) => {
677
- const record = records.find((candidate) => candidate.sessionName === managedSession.sessionName);
691
+ const record = records.find((candidate) => candidate.sessionName === managedSession.sessionName && candidate.namespace === managedSession.namespace);
678
692
  const status = record ? statuses.find((candidate) => candidate.launchId === record.launchId) : undefined;
679
693
  return record && status ? buildElectronSessionMismatch({ managedSession, record, statusTargets: status.targets }) : undefined;
680
694
  })
@@ -690,7 +704,7 @@ async function handleElectronHostInputInContext(options) {
690
704
  if (compiledElectron?.action === "probe") {
691
705
  const launchRecord = compiledElectron.launchId
692
706
  ? electronLaunchRecords.get(compiledElectron.launchId)
693
- : findElectronLaunchRecordForSession(managedSessionName, electronLaunchRecords);
707
+ : findElectronLaunchRecordForSession(managedSessionName, electronLaunchRecords, managedSessionNamespace);
694
708
  if (compiledElectron.launchId && !launchRecord) {
695
709
  return buildElectronHostFailureResult({
696
710
  compiledElectron: redactedCompiledElectron ?? compiledElectron,
@@ -722,7 +736,7 @@ async function handleElectronHostInputInContext(options) {
722
736
  }
723
737
  try {
724
738
  const status = launchRecord ? await inspectElectronLaunchStatus(launchRecord) : undefined;
725
- const probeNamespace = compiledElectron.launchId ? undefined : managedSessionNamespace;
739
+ const probeNamespace = compiledElectron.launchId ? launchRecord?.namespace : managedSessionNamespace;
726
740
  const pageStateKey = getSessionPageStateKey(probeSessionName, probeNamespace) ?? probeSessionName;
727
741
  const currentPageState = sessionPageState.get(pageStateKey);
728
742
  const fileAccessError = getManagedSessionStateAccessValidationError({
@@ -733,9 +747,14 @@ async function handleElectronHostInputInContext(options) {
733
747
  });
734
748
  if (fileAccessError)
735
749
  throw new ElectronManagedSessionPolicyError(fileAccessError);
750
+ const managedSessionOwner = ownedManagedSessions.get(pageStateKey);
751
+ const headedManagedAutosaveDisabled = managedSessionOwner?.headedManagedAutosaveDisabled === true;
752
+ const headedManagedAutosaveInterval = managedSessionOwner?.headedManagedAutosaveInterval;
736
753
  const probe = await withOwnedElectronManagedSessionPolicy({
737
754
  args: ["snapshot", "-i"],
738
755
  cwd,
756
+ headedManagedAutosaveDisabled,
757
+ headedManagedAutosaveInterval,
739
758
  namespace: probeNamespace,
740
759
  restoreState: managedSessionRestoreState,
741
760
  sessionName: probeSessionName,
@@ -779,6 +798,8 @@ async function handleElectronHostInputInContext(options) {
779
798
  }
780
799
  return buildElectronProbeResult({
781
800
  compiledElectron: redactedCompiledElectron ?? compiledElectron,
801
+ headedManagedAutosaveDisabled,
802
+ headedManagedAutosaveInterval,
782
803
  mismatch: sessionMismatch,
783
804
  namespace: probeNamespace,
784
805
  probe,
@@ -801,7 +822,7 @@ async function handleElectronHostInputInContext(options) {
801
822
  const selection = selectElectronRecords(compiledElectron, electronLaunchRecords);
802
823
  if (selection.error)
803
824
  return buildElectronHostFailureResult({ compiledElectron: redactedCompiledElectron ?? compiledElectron, errorText: selection.error, failureCategory: "validation-error" });
804
- const cleanupResults = await cleanupTrackedElectronHostLaunches({ attachedSessionKeys, cwd, electronChildProcesses, electronLaunchRecords, managedSessionRestoreState, records: selection.records ?? [], timeoutMs: compiledElectron.timeoutMs ?? implicitSessionCloseTimeoutMs });
825
+ const cleanupResults = await cleanupTrackedElectronHostLaunches({ attachedSessionKeys, cwd, electronChildProcesses, electronLaunchRecords, managedSessionRestoreState, ownedManagedSessions, records: selection.records ?? [], timeoutMs: compiledElectron.timeoutMs ?? implicitSessionCloseTimeoutMs });
805
826
  return buildElectronCleanupResult(redactedCompiledElectron ?? compiledElectron, cleanupResults);
806
827
  }
807
828
  return undefined;
@@ -1,7 +1,11 @@
1
1
  import { parseArgvDescriptor } from "../argv-descriptor.js";
2
2
  import { validateToolArgs, redactInvocationArgs, redactSensitiveText } from "../runtime.js";
3
3
  import { buildAgentBrowserResultCategoryDetails } from "../results/categories.js";
4
- import { compileAgentBrowserElectron, compileAgentBrowserJob, compileAgentBrowserNetworkSourceLookup, compileAgentBrowserQaPreset, compileAgentBrowserSemanticAction, compileAgentBrowserSourceLookup, redactNetworkSourceLookupArgs, redactNetworkSourceLookupUrl, } from "../input-modes.js";
4
+ import { compileAgentBrowserElectron } from "../input-modes/electron.js";
5
+ import { compileAgentBrowserJob, compileAgentBrowserQaPreset } from "../input-modes/job.js";
6
+ import { compileAgentBrowserNetworkSourceLookup, compileAgentBrowserSourceLookup, redactNetworkSourceLookupArgs, redactNetworkSourceLookupUrl } from "../input-modes/lookups.js";
7
+ import { compileAgentBrowserSemanticAction } from "../input-modes/semantic-action.js";
8
+ import { AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS, compileAgentBrowserScript } from "../input-modes/script.js";
5
9
  function redactCompiledElectron(compiled) {
6
10
  if (!compiled)
7
11
  return undefined;
@@ -68,6 +72,7 @@ export function resolveAgentBrowserInput(options) {
68
72
  const sourceLookupResult = params.sourceLookup === undefined ? {} : compileAgentBrowserSourceLookup(params.sourceLookup);
69
73
  const networkSourceLookupResult = params.networkSourceLookup === undefined ? {} : compileAgentBrowserNetworkSourceLookup(params.networkSourceLookup);
70
74
  const electronResult = params.electron === undefined ? {} : compileAgentBrowserElectron(params.electron);
75
+ const scriptResult = params.script === undefined ? {} : compileAgentBrowserScript(params.script);
71
76
  const hasExplicitArgs = Array.isArray(params.args);
72
77
  const explicitInputModes = [
73
78
  hasExplicitArgs,
@@ -77,19 +82,21 @@ export function resolveAgentBrowserInput(options) {
77
82
  Boolean(sourceLookupResult.compiled),
78
83
  Boolean(networkSourceLookupResult.compiled),
79
84
  Boolean(electronResult.compiled),
85
+ Boolean(scriptResult.compiled),
80
86
  ].filter(Boolean).length;
81
87
  const inputModeError = explicitInputModes !== 1
82
- ? "Provide exactly one of args, semanticAction, job, qa, sourceLookup, networkSourceLookup, or electron."
88
+ ? "Provide exactly one of script, args, semanticAction, job, qa, sourceLookup, networkSourceLookup, or electron."
83
89
  : undefined;
84
90
  const compiledSemanticAction = semanticActionResult.compiled;
85
91
  const compiledQaPreset = qaResult.compiled;
86
92
  const compiledSourceLookup = sourceLookupResult.compiled;
87
93
  const compiledNetworkSourceLookup = networkSourceLookupResult.compiled;
88
94
  const compiledElectron = electronResult.compiled;
95
+ const compiledScript = scriptResult.compiled;
89
96
  const compiledJob = jobResult.compiled ?? compiledQaPreset;
90
97
  const compiledGeneratedBatch = compiledNetworkSourceLookup ?? compiledSourceLookup ?? compiledJob;
91
98
  const normalizedExplicitArgs = normalizeExplicitEvalStdinArgs(params.args ?? [], params.stdin);
92
- const toolArgs = compiledElectron ? [] : compiledSemanticAction?.args ?? compiledGeneratedBatch?.args ?? normalizedExplicitArgs.args;
99
+ const toolArgs = compiledElectron || compiledScript ? [] : compiledSemanticAction?.args ?? compiledGeneratedBatch?.args ?? normalizedExplicitArgs.args;
93
100
  const toolStdin = compiledGeneratedBatch?.stdin ?? normalizedExplicitArgs.stdin;
94
101
  const redactedArgs = redactInvocationArgs(toolArgs);
95
102
  const generatedStdinError = params.stdin !== undefined
@@ -97,7 +104,9 @@ export function resolveAgentBrowserInput(options) {
97
104
  ? "Do not provide stdin with job, qa, sourceLookup, or networkSourceLookup; those modes generate their own batch stdin."
98
105
  : compiledElectron
99
106
  ? "Do not provide stdin with electron; electron mode is host-only or manages its own input."
100
- : undefined
107
+ : compiledScript
108
+ ? "Do not provide stdin with script; browser call stdin belongs inside browser({ args, stdin })."
109
+ : undefined
101
110
  : undefined;
102
111
  const outputPathError = params.outputPath !== undefined && (typeof params.outputPath !== "string" || params.outputPath.trim().length === 0)
103
112
  ? "outputPath must be a non-empty string when provided."
@@ -106,7 +115,12 @@ export function resolveAgentBrowserInput(options) {
106
115
  ? "timeoutMs must be a positive integer when provided."
107
116
  : compiledElectron && params.timeoutMs !== undefined
108
117
  ? "Use electron.timeoutMs for electron actions; top-level timeoutMs applies only to browser CLI subprocess calls."
109
- : undefined;
118
+ : compiledScript && params.timeoutMs !== undefined && params.timeoutMs > AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS
119
+ ? `script timeoutMs must be ${AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS} or less.`
120
+ : undefined;
121
+ const scriptSessionModeError = compiledScript && params.sessionMode !== undefined
122
+ ? "Do not provide sessionMode with script; script always uses its own isolated session."
123
+ : undefined;
110
124
  const attachedQaSessionError = compiledQaPreset?.checks.attached
111
125
  ? params.sessionMode === "fresh"
112
126
  ? "qa.attached cannot be used with sessionMode=fresh; attach or launch a session first, then run qa.attached with the current session."
@@ -120,31 +134,35 @@ export function resolveAgentBrowserInput(options) {
120
134
  ?? sourceLookupResult.error
121
135
  ?? networkSourceLookupResult.error
122
136
  ?? electronResult.error
137
+ ?? scriptResult.error
123
138
  ?? inputModeError
124
139
  ?? generatedStdinError
125
140
  ?? outputPathError
126
141
  ?? timeoutMsError
142
+ ?? scriptSessionModeError
127
143
  ?? attachedQaSessionError
128
- ?? (compiledElectron ? undefined : validateToolArgs(toolArgs) ?? getBatchPreflightValidationError(toolArgs, toolStdin));
144
+ ?? (compiledElectron || compiledScript ? undefined : validateToolArgs(toolArgs) ?? getBatchPreflightValidationError(toolArgs, toolStdin));
129
145
  const redactedCompiledJob = redactCompiledJob(compiledJob);
130
146
  const redactedCompiledSemanticAction = compiledSemanticAction
131
147
  ? { ...compiledSemanticAction, args: redactInvocationArgs(compiledSemanticAction.args) }
132
148
  : undefined;
133
149
  const attemptedKind = compiledElectron
134
150
  ? "electron"
135
- : compiledNetworkSourceLookup
136
- ? "networkSourceLookup"
137
- : compiledSourceLookup
138
- ? "sourceLookup"
139
- : compiledQaPreset
140
- ? "qa"
141
- : jobResult.compiled
142
- ? "job"
143
- : compiledSemanticAction
144
- ? "semanticAction"
145
- : hasExplicitArgs
146
- ? "args"
147
- : undefined;
151
+ : compiledScript
152
+ ? "script"
153
+ : compiledNetworkSourceLookup
154
+ ? "networkSourceLookup"
155
+ : compiledSourceLookup
156
+ ? "sourceLookup"
157
+ : compiledQaPreset
158
+ ? "qa"
159
+ : jobResult.compiled
160
+ ? "job"
161
+ : compiledSemanticAction
162
+ ? "semanticAction"
163
+ : hasExplicitArgs
164
+ ? "args"
165
+ : undefined;
148
166
  const redactedCompiledElectron = redactCompiledElectron(compiledElectron);
149
167
  const redactedCompiledNetworkSourceLookup = redactCompiledNetworkSourceLookup(compiledNetworkSourceLookup);
150
168
  const redactedCompiledQaPreset = compiledQaPreset && redactedCompiledJob ? { ...redactedCompiledJob, checks: compiledQaPreset.checks } : undefined;
@@ -175,6 +193,9 @@ export function resolveAgentBrowserInput(options) {
175
193
  if (compiledElectron && redactedCompiledElectron) {
176
194
  return { ...resolvedBase, compiledElectron, kind: "electron", redactedCompiledElectron, status: "valid" };
177
195
  }
196
+ if (compiledScript) {
197
+ return { ...resolvedBase, compiledScript, kind: "script", status: "valid" };
198
+ }
178
199
  if (compiledNetworkSourceLookup && redactedCompiledNetworkSourceLookup) {
179
200
  return {
180
201
  ...resolvedBase,
@@ -1,4 +1,4 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
1
+ import { mkdir, realpath, stat, writeFile } from "node:fs/promises";
2
2
  import { dirname, isAbsolute, resolve } from "node:path";
3
3
  import { getAgentBrowserStoragePathValidationError } from "../managed-session-state-policy.js";
4
4
  import { isRecord } from "../parsing.js";
@@ -31,6 +31,33 @@ function appendOutputFileNotice(result, message) {
31
31
  export function getAgentBrowserOutputPathValidationError(outputPath, cwd) {
32
32
  return outputPath ? getAgentBrowserStoragePathValidationError(normalizeRequestedOutputPath(outputPath), cwd) : undefined;
33
33
  }
34
+ function getArtifactPaths(result, cwd) {
35
+ const details = isRecord(result.details) ? result.details : undefined;
36
+ if (!details || !Array.isArray(details.artifacts))
37
+ return [];
38
+ return details.artifacts.flatMap((artifact) => {
39
+ if (!isRecord(artifact))
40
+ return [];
41
+ const path = typeof artifact.absolutePath === "string" ? artifact.absolutePath : typeof artifact.path === "string" ? artifact.path : undefined;
42
+ return path ? [isAbsolute(path) ? path : resolve(cwd, path)] : [];
43
+ });
44
+ }
45
+ async function pathsReferToSameFile(left, right) {
46
+ if (resolve(left) === resolve(right))
47
+ return true;
48
+ try {
49
+ if (await realpath(left) === await realpath(right))
50
+ return true;
51
+ }
52
+ catch { }
53
+ try {
54
+ const [leftStat, rightStat] = await Promise.all([stat(left), stat(right)]);
55
+ return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
34
61
  export async function applyAgentBrowserOutputPath(options) {
35
62
  if (!options.outputPath)
36
63
  return options.result;
@@ -47,6 +74,20 @@ export async function applyAgentBrowserOutputPath(options) {
47
74
  const requestedPath = normalizeRequestedOutputPath(options.outputPath);
48
75
  const absolutePath = isAbsolute(requestedPath) ? requestedPath : resolve(options.cwd, requestedPath);
49
76
  const payload = getOutputPayload(options.result);
77
+ for (const artifactPath of getArtifactPaths(options.result, options.cwd)) {
78
+ if (!await pathsReferToSameFile(absolutePath, artifactPath))
79
+ continue;
80
+ const message = "outputPath resolves to the same file as a browser artifact destination; choose a separate outputPath or omit it. The browser artifact was preserved.";
81
+ const outputFile = { absolutePath, error: message, path: requestedPath, source: payload.source, status: "failed" };
82
+ const details = isRecord(options.result.details) ? { ...options.result.details } : {};
83
+ delete details.successCategory;
84
+ return {
85
+ ...options.result,
86
+ content: appendOutputFileNotice(options.result, `Output file rejected: ${message}`),
87
+ details: { ...details, failureCategory: "validation-error", outputFile, resultCategory: "failure" },
88
+ isError: true,
89
+ };
90
+ }
50
91
  try {
51
92
  const serialized = serializeOutputPayload(payload.value);
52
93
  await mkdir(dirname(absolutePath), { recursive: true });