pi-agent-browser-native 0.2.47 → 0.2.49

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 (185) hide show
  1. package/CHANGELOG.md +63 -19
  2. package/README.md +52 -19
  3. package/dist/extensions/agent-browser/index.js +785 -0
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +71 -0
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +121 -0
  6. package/dist/extensions/agent-browser/lib/bash-guard.js +190 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +85 -0
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +302 -0
  9. package/dist/extensions/agent-browser/lib/config-policy.js +686 -0
  10. package/dist/extensions/agent-browser/lib/config.js +122 -0
  11. package/dist/extensions/agent-browser/lib/electron/cdp.js +51 -0
  12. package/dist/extensions/agent-browser/lib/electron/cleanup.js +212 -0
  13. package/dist/extensions/agent-browser/lib/electron/discovery.js +633 -0
  14. package/dist/extensions/agent-browser/lib/electron/launch.js +351 -0
  15. package/{extensions/agent-browser/lib/electron/text.ts → dist/extensions/agent-browser/lib/electron/text.js} +5 -5
  16. package/dist/extensions/agent-browser/lib/executable-path.js +20 -0
  17. package/dist/extensions/agent-browser/lib/fs-utils.js +18 -0
  18. package/dist/extensions/agent-browser/lib/input-modes/electron.js +165 -0
  19. package/dist/extensions/agent-browser/lib/input-modes/job.js +519 -0
  20. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +440 -0
  21. package/dist/extensions/agent-browser/lib/input-modes/params.js +164 -0
  22. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +119 -0
  23. package/dist/extensions/agent-browser/lib/input-modes/shared.js +42 -0
  24. package/dist/extensions/agent-browser/lib/input-modes/types.js +21 -0
  25. package/dist/extensions/agent-browser/lib/input-modes.js +10 -0
  26. package/dist/extensions/agent-browser/lib/json-schema.js +58 -0
  27. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +59 -0
  28. package/dist/extensions/agent-browser/lib/navigation-policy.js +83 -0
  29. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +62 -0
  30. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +39 -0
  31. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +276 -0
  32. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +909 -0
  33. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +443 -0
  34. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +47 -0
  35. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/direct-anchor-download.js +141 -0
  36. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/network-page-filter.js +108 -0
  37. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/scroll-shims.js +112 -0
  38. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +158 -0
  39. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +54 -0
  40. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +762 -0
  41. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +491 -0
  42. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.js +40 -0
  43. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-artifacts.js +5 -0
  44. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +731 -0
  45. package/dist/extensions/agent-browser/lib/orchestration/browser-run/types.js +1 -0
  46. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +718 -0
  47. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +247 -0
  48. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +68 -0
  49. package/{extensions/agent-browser/lib/parsing.ts → dist/extensions/agent-browser/lib/parsing.js} +12 -11
  50. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +241 -0
  51. package/dist/extensions/agent-browser/lib/playbook.js +121 -0
  52. package/dist/extensions/agent-browser/lib/process.js +448 -0
  53. package/dist/extensions/agent-browser/lib/prompt-policy.js +91 -0
  54. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +220 -0
  55. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +111 -0
  56. package/{extensions/agent-browser/lib/results/artifact-state.ts → dist/extensions/agent-browser/lib/results/artifact-state.js} +4 -8
  57. package/dist/extensions/agent-browser/lib/results/categories.js +76 -0
  58. package/dist/extensions/agent-browser/lib/results/confirmation.js +63 -0
  59. package/dist/extensions/agent-browser/lib/results/contracts.js +8 -0
  60. package/dist/extensions/agent-browser/lib/results/editable-ref-evidence.js +74 -0
  61. package/dist/extensions/agent-browser/lib/results/envelope.js +166 -0
  62. package/dist/extensions/agent-browser/lib/results/network-routes.js +92 -0
  63. package/dist/extensions/agent-browser/lib/results/network.js +73 -0
  64. package/dist/extensions/agent-browser/lib/results/next-actions.js +72 -0
  65. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +515 -0
  66. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +397 -0
  67. package/dist/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.js +55 -0
  68. package/dist/extensions/agent-browser/lib/results/presentation/common.js +46 -0
  69. package/dist/extensions/agent-browser/lib/results/presentation/content.js +24 -0
  70. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +960 -0
  71. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +205 -0
  72. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +134 -0
  73. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +159 -0
  74. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +216 -0
  75. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +104 -0
  76. package/dist/extensions/agent-browser/lib/results/presentation/skills.js +152 -0
  77. package/dist/extensions/agent-browser/lib/results/presentation.js +177 -0
  78. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +107 -0
  79. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +50 -0
  80. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +225 -0
  81. package/{extensions/agent-browser/lib/results/shared.ts → dist/extensions/agent-browser/lib/results/shared.js} +0 -1
  82. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +208 -0
  83. package/dist/extensions/agent-browser/lib/results/snapshot-refs.js +78 -0
  84. package/dist/extensions/agent-browser/lib/results/snapshot-segments.js +331 -0
  85. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +40 -0
  86. package/dist/extensions/agent-browser/lib/results/snapshot.js +264 -0
  87. package/dist/extensions/agent-browser/lib/results/text.js +40 -0
  88. package/{extensions/agent-browser/lib/results.ts → dist/extensions/agent-browser/lib/results.js} +2 -32
  89. package/dist/extensions/agent-browser/lib/runtime.js +816 -0
  90. package/dist/extensions/agent-browser/lib/session-page-state.js +411 -0
  91. package/dist/extensions/agent-browser/lib/string-enum-schema.js +13 -0
  92. package/dist/extensions/agent-browser/lib/temp.js +498 -0
  93. package/dist/extensions/agent-browser/lib/web-search.js +562 -0
  94. package/docs/ARCHITECTURE.md +10 -10
  95. package/docs/COMMAND_REFERENCE.md +35 -21
  96. package/docs/ELECTRON.md +3 -3
  97. package/docs/RELEASE.md +46 -26
  98. package/docs/REQUIREMENTS.md +1 -1
  99. package/docs/SUPPORT_MATRIX.md +35 -106
  100. package/docs/TOOL_CONTRACT.md +23 -21
  101. package/package.json +12 -8
  102. package/scripts/agent-browser-capability-baseline.mjs +6 -3
  103. package/scripts/config.mjs +8 -2
  104. package/scripts/doctor.mjs +19 -17
  105. package/scripts/platform-smoke.mjs +1 -1
  106. package/extensions/agent-browser/index.ts +0 -952
  107. package/extensions/agent-browser/lib/argv-descriptor.ts +0 -90
  108. package/extensions/agent-browser/lib/argv-grammar.ts +0 -128
  109. package/extensions/agent-browser/lib/bash-guard.ts +0 -205
  110. package/extensions/agent-browser/lib/command-policy.ts +0 -71
  111. package/extensions/agent-browser/lib/command-taxonomy.ts +0 -336
  112. package/extensions/agent-browser/lib/config-policy.js +0 -690
  113. package/extensions/agent-browser/lib/config.ts +0 -209
  114. package/extensions/agent-browser/lib/electron/cdp.ts +0 -69
  115. package/extensions/agent-browser/lib/electron/cleanup.ts +0 -235
  116. package/extensions/agent-browser/lib/electron/discovery.ts +0 -710
  117. package/extensions/agent-browser/lib/electron/launch.ts +0 -499
  118. package/extensions/agent-browser/lib/executable-path.ts +0 -19
  119. package/extensions/agent-browser/lib/fs-utils.ts +0 -18
  120. package/extensions/agent-browser/lib/input-modes/electron.ts +0 -170
  121. package/extensions/agent-browser/lib/input-modes/job.ts +0 -451
  122. package/extensions/agent-browser/lib/input-modes/lookups.ts +0 -447
  123. package/extensions/agent-browser/lib/input-modes/params.ts +0 -205
  124. package/extensions/agent-browser/lib/input-modes/semantic-action.ts +0 -127
  125. package/extensions/agent-browser/lib/input-modes/shared.ts +0 -46
  126. package/extensions/agent-browser/lib/input-modes/types.ts +0 -225
  127. package/extensions/agent-browser/lib/input-modes.ts +0 -45
  128. package/extensions/agent-browser/lib/json-schema.ts +0 -73
  129. package/extensions/agent-browser/lib/launch-scoped-flags.ts +0 -67
  130. package/extensions/agent-browser/lib/navigation-policy.ts +0 -95
  131. package/extensions/agent-browser/lib/orchestration/batch-stdin.ts +0 -65
  132. package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +0 -257
  133. package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +0 -912
  134. package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +0 -512
  135. package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +0 -53
  136. package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +0 -1481
  137. package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +0 -564
  138. package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +0 -47
  139. package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +0 -868
  140. package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +0 -564
  141. package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +0 -855
  142. package/extensions/agent-browser/lib/orchestration/input-plan.ts +0 -375
  143. package/extensions/agent-browser/lib/orchestration/output-file.ts +0 -86
  144. package/extensions/agent-browser/lib/pi-tool-rendering.ts +0 -252
  145. package/extensions/agent-browser/lib/playbook.ts +0 -142
  146. package/extensions/agent-browser/lib/process.ts +0 -516
  147. package/extensions/agent-browser/lib/prompt-policy.ts +0 -105
  148. package/extensions/agent-browser/lib/results/action-recommendations.ts +0 -264
  149. package/extensions/agent-browser/lib/results/artifact-manifest.ts +0 -111
  150. package/extensions/agent-browser/lib/results/categories.ts +0 -106
  151. package/extensions/agent-browser/lib/results/confirmation.ts +0 -76
  152. package/extensions/agent-browser/lib/results/contracts.ts +0 -241
  153. package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +0 -72
  154. package/extensions/agent-browser/lib/results/envelope.ts +0 -195
  155. package/extensions/agent-browser/lib/results/network-routes.ts +0 -83
  156. package/extensions/agent-browser/lib/results/network.ts +0 -78
  157. package/extensions/agent-browser/lib/results/next-actions.ts +0 -117
  158. package/extensions/agent-browser/lib/results/presentation/artifacts.ts +0 -588
  159. package/extensions/agent-browser/lib/results/presentation/batch.ts +0 -450
  160. package/extensions/agent-browser/lib/results/presentation/browser-profile-recovery.ts +0 -67
  161. package/extensions/agent-browser/lib/results/presentation/common.ts +0 -53
  162. package/extensions/agent-browser/lib/results/presentation/content.ts +0 -36
  163. package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +0 -923
  164. package/extensions/agent-browser/lib/results/presentation/errors.ts +0 -227
  165. package/extensions/agent-browser/lib/results/presentation/large-output.ts +0 -182
  166. package/extensions/agent-browser/lib/results/presentation/navigation.ts +0 -184
  167. package/extensions/agent-browser/lib/results/presentation/registry.ts +0 -242
  168. package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +0 -131
  169. package/extensions/agent-browser/lib/results/presentation/skills.ts +0 -143
  170. package/extensions/agent-browser/lib/results/presentation.ts +0 -257
  171. package/extensions/agent-browser/lib/results/recovery-actions.ts +0 -139
  172. package/extensions/agent-browser/lib/results/recovery-next-actions.ts +0 -71
  173. package/extensions/agent-browser/lib/results/selector-recovery.ts +0 -320
  174. package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +0 -273
  175. package/extensions/agent-browser/lib/results/snapshot-refs.ts +0 -100
  176. package/extensions/agent-browser/lib/results/snapshot-segments.ts +0 -366
  177. package/extensions/agent-browser/lib/results/snapshot-spill.ts +0 -63
  178. package/extensions/agent-browser/lib/results/snapshot.ts +0 -329
  179. package/extensions/agent-browser/lib/results/text.ts +0 -40
  180. package/extensions/agent-browser/lib/runtime.ts +0 -988
  181. package/extensions/agent-browser/lib/session-page-state.ts +0 -512
  182. package/extensions/agent-browser/lib/string-enum-schema.ts +0 -20
  183. package/extensions/agent-browser/lib/temp.ts +0 -577
  184. package/extensions/agent-browser/lib/web-search.ts +0 -721
  185. /package/{extensions/agent-browser/lib/orchestration/browser-run.ts → dist/extensions/agent-browser/lib/orchestration/browser-run.js} +0 -0
@@ -1,868 +0,0 @@
1
- import { rm } from "node:fs/promises";
2
-
3
- import type { ElectronLaunchStatus } from "../../electron/cleanup.js";
4
- import type { ElectronCdpTarget, ElectronLaunchRecord } from "../../electron/launch.js";
5
- import { runAgentBrowserProcess } from "../../process.js";
6
- import { buildAgentBrowserNextActions, getAgentBrowserErrorText, parseAgentBrowserEnvelope, type AgentBrowserBatchResult, type AgentBrowserEnvelope, type AgentBrowserNextAction } from "../../results.js";
7
- import { buildNextToolAction, withOptionalSessionArgs } from "../../results/next-actions.js";
8
- import {
9
- extractRefSnapshotFromData,
10
- isAboutBlankUrl,
11
- normalizeComparableUrl,
12
- normalizeSessionTabTarget,
13
- targetsMatch,
14
- type SessionRefSnapshot,
15
- type SessionRefSnapshotInvalidation,
16
- type SessionTabTarget,
17
- } from "../../session-page-state.js";
18
- import {
19
- isCloseCommand,
20
- isElectronPostCommandHealthCommand,
21
- isNavigationObservableCommandName,
22
- isRefGuardedCommand,
23
- isRefInvalidatingBatchCommand,
24
- isSessionTabPinningExcludedCommand,
25
- isSessionTabPostCommandCorrectionExcludedCommand,
26
- } from "../../command-taxonomy.js";
27
- import { chooseOpenResultTabCorrection, redactInvocationArgs, type OpenResultTabCorrection } from "../../runtime.js";
28
- import { isRecord } from "../../parsing.js";
29
- import { parseUserBatchStdin } from "../batch-stdin.js";
30
- import type {
31
- AboutBlankSessionMismatch,
32
- BatchCommandStep,
33
- BrowserRunState,
34
- BrowserRunStatePatch,
35
- ElectronManagedSessionTarget,
36
- ElectronPostCommandHealthDiagnostic,
37
- ElectronPostCommandHealthReason,
38
- ElectronRefFreshnessDiagnostic,
39
- ElectronSessionMismatch,
40
- ElectronSessionMismatchReason,
41
- ManagedSessionOutcome,
42
- NavigationSummary,
43
- PinnedBatchPlan,
44
- PinnedBatchUnwrapMode,
45
- StaleRefPreflight,
46
- TraceOwner,
47
- } from "./types.js";
48
-
49
- export const NAVIGATION_SUMMARY_EVAL = `({ title: document.title, url: location.href })`;
50
-
51
- export function applyBrowserRunStatePatch(state: BrowserRunState, patch: BrowserRunStatePatch | undefined): void {
52
- if (!patch) return;
53
- if (patch.allowedDomainsBySession) state.allowedDomainsBySession = patch.allowedDomainsBySession;
54
- if ("artifactManifest" in patch) state.artifactManifest = patch.artifactManifest;
55
- if (patch.freshSessionOrdinal !== undefined) state.freshSessionOrdinal = patch.freshSessionOrdinal;
56
- if (patch.managedSessionActive !== undefined) state.managedSessionActive = patch.managedSessionActive;
57
- if (patch.managedSessionCwd !== undefined) state.managedSessionCwd = patch.managedSessionCwd;
58
- if (patch.managedSessionName !== undefined) state.managedSessionName = patch.managedSessionName;
59
- if (patch.networkRoutesBySession) state.networkRoutesBySession = patch.networkRoutesBySession;
60
- }
61
-
62
- export function buildSessionDetailFields(sessionName: string | undefined, usedImplicitSession: boolean): Record<string, unknown> {
63
- return sessionName ? { sessionName, usedImplicitSession } : {};
64
- }
65
-
66
- export function buildManagedSessionOutcome(options: {
67
- activeAfter: boolean;
68
- activeBefore: boolean;
69
- attemptedSessionName?: string;
70
- command?: string;
71
- currentSessionName: string;
72
- previousSessionName: string;
73
- replacedSessionName?: string;
74
- sessionMode: "auto" | "fresh";
75
- succeeded: boolean;
76
- }): ManagedSessionOutcome | undefined {
77
- const { activeAfter, activeBefore, attemptedSessionName, command, currentSessionName, previousSessionName, replacedSessionName, sessionMode, succeeded } = options;
78
- if (!attemptedSessionName) return undefined;
79
- let status: ManagedSessionOutcome["status"];
80
- let summary: string;
81
- if (isCloseCommand(command)) {
82
- status = succeeded ? "closed" : activeBefore ? "preserved" : "abandoned";
83
- summary = succeeded
84
- ? `Managed session ${attemptedSessionName} was closed.`
85
- : activeBefore
86
- ? `Managed session close failed; previous managed session ${previousSessionName} remains current.`
87
- : `Managed session close failed; no managed session is active.`;
88
- } else if (succeeded) {
89
- if (replacedSessionName) {
90
- status = "replaced";
91
- summary = `Managed session ${replacedSessionName} was replaced by ${currentSessionName}.`;
92
- } else if (!activeBefore && activeAfter) {
93
- status = "created";
94
- summary = `Managed session ${currentSessionName} is now current.`;
95
- } else {
96
- status = "unchanged";
97
- summary = `Managed session ${currentSessionName} remains current.`;
98
- }
99
- } else if (activeBefore) {
100
- status = "preserved";
101
- summary = sessionMode === "fresh" && attemptedSessionName !== previousSessionName
102
- ? `Fresh managed session ${attemptedSessionName} failed before becoming current; previous managed session ${previousSessionName} was preserved.`
103
- : `Managed session call failed; previous managed session ${previousSessionName} was preserved.`;
104
- } else {
105
- status = "abandoned";
106
- summary = sessionMode === "fresh"
107
- ? `Fresh managed session ${attemptedSessionName} failed before becoming current; no previous managed session was active, so no managed session is current.`
108
- : `Managed session call failed before any managed session became current.`;
109
- }
110
- return {
111
- activeAfter,
112
- activeBefore,
113
- attemptedSessionName,
114
- currentSessionName,
115
- previousSessionName,
116
- replacedSessionName,
117
- sessionMode,
118
- status,
119
- succeeded,
120
- summary,
121
- };
122
- }
123
-
124
- function isFreshPostLaunchFailure(outcome: ManagedSessionOutcome): boolean {
125
- return !outcome.succeeded && outcome.sessionMode === "fresh" && outcome.activeAfter && !!outcome.currentSessionName && (outcome.status === "created" || outcome.status === "replaced" || outcome.status === "unchanged");
126
- }
127
-
128
- function formatManagedSessionOutcomeHeadline(outcome: ManagedSessionOutcome): string {
129
- if (outcome.status === "preserved") {
130
- return "Managed session outcome: Fresh launch failed; your previous browser session is still active.";
131
- }
132
- if (outcome.status === "abandoned") {
133
- return "Managed session outcome: Fresh launch failed; no managed browser session is current.";
134
- }
135
- if (isFreshPostLaunchFailure(outcome)) {
136
- return "Managed session outcome: Fresh launch became current, but this tool call failed after launch.";
137
- }
138
- return `Managed session outcome: ${outcome.summary}`;
139
- }
140
-
141
- function formatManagedSessionOutcomeRecoveryGuidance(outcome: ManagedSessionOutcome): string {
142
- const lines = ["Recovery:"];
143
- if (outcome.status === "preserved") {
144
- lines.push('- Continue with sessionMode "auto" on the current session, or retry the intended launch with sessionMode "fresh".');
145
- lines.push("- Run doctor to verify agent-browser install and environment when failures persist.");
146
- } else if (outcome.status === "abandoned") {
147
- lines.push('- Retry with sessionMode "fresh" (for example args: ["open", "<url>"]) after verifying agent-browser is on PATH.');
148
- lines.push("- Run doctor when install or environment issues are suspected.");
149
- } else if (isFreshPostLaunchFailure(outcome)) {
150
- lines.push('- Continue with sessionMode "auto" on the current session, or inspect failureCategory / qaPreset to fix the post-launch failure.');
151
- lines.push("- Run doctor only if later browser commands also fail.");
152
- } else {
153
- lines.push('- Retry with sessionMode "fresh" when launch-scoped flags must apply, or run doctor to verify the environment.');
154
- }
155
- lines.push("- Full session names and transition details remain in details.managedSessionOutcome.");
156
- return lines.join("\n");
157
- }
158
-
159
- export function formatManagedSessionOutcomeText(outcome: ManagedSessionOutcome | undefined): string | undefined {
160
- if (!outcome) return undefined;
161
- if (outcome.status === "closed" && outcome.succeeded) {
162
- return [
163
- "Managed session outcome: The current wrapper-managed browser session was closed.",
164
- "Next sessionMode auto call will start or attach a managed session as needed. If upstream session list still shows rows, they are separate saved/upstream sessions; use close --all only when full cleanup is intended.",
165
- "Full session names and transition details remain in details.managedSessionOutcome.",
166
- ].join("\n");
167
- }
168
- if (outcome.succeeded || outcome.sessionMode !== "fresh") return undefined;
169
- return [formatManagedSessionOutcomeHeadline(outcome), formatManagedSessionOutcomeRecoveryGuidance(outcome)].join("\n");
170
- }
171
-
172
- export function buildManagedSessionFreshFailureNextActions(outcome: ManagedSessionOutcome | undefined): AgentBrowserNextAction[] {
173
- if (!outcome || outcome.succeeded || outcome.sessionMode !== "fresh") return [];
174
- const actions: AgentBrowserNextAction[] = [];
175
- if (!isFreshPostLaunchFailure(outcome)) {
176
- actions.push(buildNextToolAction({
177
- args: ["doctor"],
178
- id: "run-agent-browser-doctor",
179
- reason: "Verify agent-browser install, PATH, and environment after a failed fresh launch.",
180
- safety: "Read-only local diagnostics; does not mutate browser state.",
181
- }));
182
- }
183
- if ((outcome.status === "preserved" || isFreshPostLaunchFailure(outcome)) && outcome.activeAfter && outcome.currentSessionName) {
184
- const sessionLabel = isFreshPostLaunchFailure(outcome) ? "current managed session" : "preserved managed session";
185
- actions.push(
186
- buildNextToolAction({
187
- args: withOptionalSessionArgs(outcome.currentSessionName, ["get", "url"]),
188
- id: "verify-current-managed-session",
189
- reason: `Confirm the ${sessionLabel} before continuing with sessionMode auto.`,
190
- safety: `Read-only URL check on the ${sessionLabel}.`,
191
- }),
192
- buildNextToolAction({
193
- args: withOptionalSessionArgs(outcome.currentSessionName, ["snapshot", "-i"]),
194
- id: "snapshot-current-managed-session",
195
- reason: `Refresh interactive refs on the ${sessionLabel} before retrying the workflow.`,
196
- safety: "Read-only snapshot; no navigation.",
197
- }),
198
- );
199
- } else {
200
- actions.push(
201
- buildNextToolAction({
202
- args: ["open", "about:blank"],
203
- id: "retry-fresh-managed-session",
204
- reason: "Start a new managed browser session after the failed fresh launch.",
205
- safety: "Replace about:blank with the intended URL from your workflow.",
206
- sessionMode: "fresh",
207
- }),
208
- );
209
- }
210
- return actions;
211
- }
212
-
213
- function getTraceOwner(command: string | undefined): TraceOwner | undefined {
214
- return command === "trace" || command === "profiler" ? command : undefined;
215
- }
216
-
217
- export function getTraceOwnerGuardMessage(options: {
218
- command: string | undefined;
219
- sessionName: string | undefined;
220
- subcommand: string | undefined;
221
- traceOwners: Map<string, TraceOwner>;
222
- }): string | undefined {
223
- const owner = getTraceOwner(options.command);
224
- if (!owner || !options.sessionName || (options.subcommand !== "start" && options.subcommand !== "stop")) {
225
- return undefined;
226
- }
227
- const activeOwner = options.traceOwners.get(options.sessionName);
228
- if (!activeOwner || activeOwner === owner) {
229
- return undefined;
230
- }
231
- return options.subcommand === "start"
232
- ? `Wrapper believes ${activeOwner} tracing is active for session ${options.sessionName}; stop ${activeOwner} before starting ${owner}.`
233
- : `Wrapper believes tracing for session ${options.sessionName} is owned by ${activeOwner}; run ${activeOwner} stop instead of ${owner} stop.`;
234
- }
235
-
236
- export function updateTraceOwnerState(options: {
237
- command: string | undefined;
238
- sessionName: string | undefined;
239
- subcommand: string | undefined;
240
- succeeded: boolean;
241
- traceOwners: Map<string, TraceOwner>;
242
- }): void {
243
- const owner = getTraceOwner(options.command);
244
- if (!owner || !options.sessionName || !options.succeeded) {
245
- return;
246
- }
247
- if (options.subcommand === "start") {
248
- options.traceOwners.set(options.sessionName, owner);
249
- }
250
- if (options.subcommand === "stop" && options.traceOwners.get(options.sessionName) === owner) {
251
- options.traceOwners.delete(options.sessionName);
252
- }
253
- }
254
-
255
- export function extractStringResultField(data: unknown, fieldName: "result" | "title" | "url" | "value"): string | undefined {
256
- if (typeof data === "string") {
257
- if (fieldName === "value") return data;
258
- const text = data.trim();
259
- return text.length > 0 ? text : undefined;
260
- }
261
- if (!isRecord(data) || typeof data[fieldName] !== "string") {
262
- return undefined;
263
- }
264
- if (fieldName === "value") return data[fieldName];
265
- const text = data[fieldName].trim();
266
- return text.length > 0 ? text : undefined;
267
- }
268
-
269
- export function extractNavigationSummaryFromData(data: unknown): NavigationSummary | undefined {
270
- const result = isRecord(data) && isRecord(data.result) ? data.result : data;
271
- const title = extractStringResultField(result, "title");
272
- const url = extractStringResultField(result, "url");
273
- return title || url ? { title, url } : undefined;
274
- }
275
-
276
- export function shouldCaptureNavigationSummary(command: string | undefined, data: unknown): boolean {
277
- return (
278
- isNavigationObservableCommandName(command) &&
279
- (!isRecord(data) || (typeof data.title !== "string" && typeof data.url !== "string"))
280
- );
281
- }
282
-
283
- export function mergeNavigationSummaryIntoData(data: unknown, navigationSummary: NavigationSummary): unknown {
284
- if (isRecord(data)) {
285
- return { ...data, navigationSummary };
286
- }
287
- return { navigationSummary, result: data };
288
- }
289
-
290
- export function buildAboutBlankRecoveryHint(): string {
291
- return "agent_browser detected that the active tab became about:blank while this session still had a prior intended tab. Run tab list for this session and re-select the intended tab, or retry with sessionMode:fresh if the tab is gone.".replace("sessionMode:fresh", "sessionMode=fresh");
292
- }
293
-
294
- export function buildAboutBlankWarning(mismatch: AboutBlankSessionMismatch): string {
295
- return `Warning: agent_browser detected that this session returned about:blank while the prior intended tab was ${mismatch.targetUrl}. ${mismatch.recoveryApplied ? "The wrapper re-selected the intended tab for the session." : "No matching tab could be re-selected; run tab list for the same session or retry with sessionMode=fresh."}`;
296
- }
297
-
298
- function extractBatchResultCommand(item: Record<string, unknown>): string[] {
299
- return Array.isArray(item.command) ? item.command.filter((token): token is string => typeof token === "string") : [];
300
- }
301
-
302
- export function getStaleRefArgs(commandTokens: string[], stdin?: string): string[] {
303
- if (commandTokens[0] !== "batch" || stdin === undefined) {
304
- return commandTokens;
305
- }
306
- const parsed = parseUserBatchStdin(stdin);
307
- if (parsed.error || parsed.steps === undefined) {
308
- return commandTokens;
309
- }
310
- return parsed.steps.flatMap((step) => step);
311
- }
312
-
313
- function collectRefsFromTokens(tokens: string[]): string[] {
314
- return tokens.filter((token) => /^@e\d+\b/.test(token)).map((token) => token.slice(1));
315
- }
316
-
317
- export function getGuardedRefUsage(commandTokens: string[], stdin?: string, options: { includeRefsAfterBatchSnapshot?: boolean } = {}): string[] {
318
- const collectFromStep = (step: string[]) => isRefGuardedCommand(step[0]) ? collectRefsFromTokens(step) : [];
319
- if (commandTokens[0] !== "batch" || stdin === undefined) {
320
- return collectFromStep(commandTokens);
321
- }
322
- const parsed = parseUserBatchStdin(stdin);
323
- if (parsed.error || parsed.steps === undefined) {
324
- return collectFromStep(commandTokens);
325
- }
326
- const refsBeforeInBatchSnapshot: string[] = [];
327
- for (const step of parsed.steps) {
328
- if (!options.includeRefsAfterBatchSnapshot && (step[0] ?? "") === "snapshot") break;
329
- refsBeforeInBatchSnapshot.push(...collectFromStep(step));
330
- }
331
- return refsBeforeInBatchSnapshot;
332
- }
333
-
334
- function getSnapshotRefRole(refSnapshot: SessionRefSnapshot | undefined, refId: string): string | undefined {
335
- return refSnapshot?.refs?.[refId]?.role?.toLowerCase();
336
- }
337
-
338
- function isSafeSameSnapshotFormBatchStep(step: string[], refSnapshot: SessionRefSnapshot | undefined): boolean {
339
- const command = step[0];
340
- const refIds = collectRefsFromTokens(step);
341
- if (refIds.length === 0 || !refSnapshot) return false;
342
- const roles = refIds.map((refId) => getSnapshotRefRole(refSnapshot, refId));
343
- if (roles.some((role) => role === undefined)) return false;
344
- if (command === "check" || command === "uncheck") return roles.every((role) => role === "checkbox" || role === "radio");
345
- if (command === "click" || command === "tap") return roles.every((role) => role === "checkbox" || role === "radio");
346
- if (command === "select") return roles.every((role) => role === "combobox");
347
- return false;
348
- }
349
-
350
- function getBatchRefInvalidationMessage(commandTokens: string[], stdin?: string, refSnapshot?: SessionRefSnapshot): string | undefined {
351
- if (commandTokens[0] !== "batch" || stdin === undefined) return undefined;
352
- const parsed = parseUserBatchStdin(stdin);
353
- if (parsed.error || parsed.steps === undefined) return undefined;
354
- let priorStepInvalidatesRefs = false;
355
- for (const step of parsed.steps) {
356
- if ((step[0] ?? "") === "snapshot") {
357
- priorStepInvalidatesRefs = false;
358
- }
359
- const refIds = collectRefsFromTokens(step);
360
- if (refIds.length > 0 && isRefGuardedCommand(step[0]) && priorStepInvalidatesRefs) {
361
- 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.`;
362
- }
363
- if (isRefInvalidatingBatchCommand(step[0]) && !isSafeSameSnapshotFormBatchStep(step, refSnapshot)) {
364
- priorStepInvalidatesRefs = true;
365
- }
366
- }
367
- return undefined;
368
- }
369
-
370
- export function buildStaleRefPreflight(options: {
371
- commandTokens: string[];
372
- currentTarget?: SessionTabTarget;
373
- refSnapshot?: SessionRefSnapshot;
374
- refSnapshotInvalidation?: SessionRefSnapshotInvalidation;
375
- stdin?: string;
376
- }): StaleRefPreflight | undefined {
377
- const guardedRefIds = [...new Set(getGuardedRefUsage(options.commandTokens, options.stdin))];
378
- const usedRefIds = options.refSnapshotInvalidation
379
- ? [...new Set(getGuardedRefUsage(options.commandTokens, options.stdin, { includeRefsAfterBatchSnapshot: true }))]
380
- : guardedRefIds;
381
- const batchInvalidationMessage = getBatchRefInvalidationMessage(options.commandTokens, options.stdin, options.refSnapshot);
382
- if (batchInvalidationMessage && guardedRefIds.length > 0) {
383
- return {
384
- message: batchInvalidationMessage,
385
- refIds: guardedRefIds,
386
- snapshot: options.refSnapshot,
387
- };
388
- }
389
- if (usedRefIds.length === 0) return undefined;
390
- if (options.refSnapshotInvalidation) {
391
- return {
392
- 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.`,
393
- refIds: usedRefIds,
394
- snapshotInvalidation: options.refSnapshotInvalidation,
395
- };
396
- }
397
- if (!options.refSnapshot) return undefined;
398
- if (!targetsMatch(options.refSnapshot.target, options.currentTarget)) {
399
- return {
400
- message: `Ref ${usedRefIds.map((refId) => `@${refId}`).join(", ")} came from a snapshot for ${options.refSnapshot.target?.url ?? "a prior page"}, but the current session target is ${options.currentTarget?.url ?? "unknown"}. Run snapshot -i again before using page-scoped refs.`,
401
- refIds: usedRefIds,
402
- snapshot: options.refSnapshot,
403
- };
404
- }
405
- const knownRefs = new Set(options.refSnapshot.refIds);
406
- const missingRefs = usedRefIds.filter((refId) => !knownRefs.has(refId));
407
- if (missingRefs.length > 0) {
408
- return {
409
- message: `Ref ${missingRefs.map((refId) => `@${refId}`).join(", ")} was not present in the latest snapshot for this session. Run snapshot -i again before using page-scoped refs.`,
410
- refIds: missingRefs,
411
- snapshot: options.refSnapshot,
412
- };
413
- }
414
- return undefined;
415
- }
416
-
417
- function supportsPinnedStdinCommand(options: { command?: string; commandTokens: string[]; stdin?: string }): boolean {
418
- if (options.command === "batch") {
419
- return options.stdin !== undefined;
420
- }
421
- if (options.stdin === undefined) {
422
- return true;
423
- }
424
- if (options.command === "eval") {
425
- return options.commandTokens.includes("--stdin");
426
- }
427
- return false;
428
- }
429
-
430
- export function shouldPinSessionTabForCommand(options: {
431
- command?: string;
432
- commandTokens: string[];
433
- pinningRequired?: boolean;
434
- sessionName?: string;
435
- stdin?: string;
436
- }): boolean {
437
- return (
438
- options.pinningRequired === true &&
439
- options.sessionName !== undefined &&
440
- options.command !== undefined &&
441
- !isSessionTabPinningExcludedCommand(options.command) &&
442
- supportsPinnedStdinCommand(options)
443
- );
444
- }
445
-
446
- export function buildPinnedBatchPlan(options: {
447
- command?: string;
448
- commandTokens: string[];
449
- selectedTab: string;
450
- stdin?: string;
451
- }): PinnedBatchPlan | { error: string } | undefined {
452
- if (options.command === "batch") {
453
- const parsed = parseUserBatchStdin(options.stdin);
454
- if (parsed.error) {
455
- return { error: parsed.error };
456
- }
457
- const tabSelectionStep: BatchCommandStep = ["tab", options.selectedTab];
458
- return {
459
- includeNavigationSummary: false,
460
- steps: [tabSelectionStep, ...(parsed.steps ?? [])],
461
- unwrapMode: "user-batch",
462
- };
463
- }
464
- if (options.commandTokens.length === 0) {
465
- return undefined;
466
- }
467
- const includeNavigationSummary = isNavigationObservableCommandName(options.command);
468
- const tabSelectionStep: BatchCommandStep = ["tab", options.selectedTab];
469
- const commandStep = options.commandTokens as BatchCommandStep;
470
- const navigationSummarySteps: BatchCommandStep[] = includeNavigationSummary ? [["eval", NAVIGATION_SUMMARY_EVAL]] : [];
471
- return {
472
- includeNavigationSummary,
473
- steps: [tabSelectionStep, commandStep, ...navigationSummarySteps],
474
- unwrapMode: "single-command",
475
- };
476
- }
477
-
478
- export function shouldCorrectSessionTabAfterCommand(options: { command?: string; pinningRequired?: boolean; sessionName?: string }): boolean {
479
- return (
480
- options.pinningRequired === true &&
481
- options.sessionName !== undefined &&
482
- options.command !== undefined &&
483
- !isSessionTabPostCommandCorrectionExcludedCommand(options.command)
484
- );
485
- }
486
-
487
- function selectSessionTargetTab(options: {
488
- tabs: Array<{ active?: boolean; index?: number; label?: string; tabId?: string; title?: string; url?: string }>;
489
- target: SessionTabTarget;
490
- }): OpenResultTabCorrection | undefined {
491
- return chooseOpenResultTabCorrection({
492
- tabs: options.tabs,
493
- targetTitle: options.target.title,
494
- targetUrl: options.target.url,
495
- });
496
- }
497
-
498
- export function unwrapPinnedSessionBatchEnvelope(options: {
499
- envelope?: AgentBrowserEnvelope;
500
- includeNavigationSummary: boolean;
501
- mode?: PinnedBatchUnwrapMode;
502
- }): { envelope?: AgentBrowserEnvelope; navigationSummary?: NavigationSummary; parseError?: string } {
503
- if (!options.envelope) {
504
- return {};
505
- }
506
- if (!Array.isArray(options.envelope.data)) {
507
- return {
508
- parseError: "agent-browser returned an unexpected response while applying the wrapper's tab-pinning batch.",
509
- };
510
- }
511
-
512
- const steps = options.envelope.data.filter(isRecord) as AgentBrowserBatchResult[];
513
- const tabSelectionStep = steps[0];
514
- const commandStep = steps[1];
515
- if (tabSelectionStep?.success === false) {
516
- return {
517
- envelope: {
518
- success: false,
519
- error: tabSelectionStep.error ?? "agent-browser could not re-select the intended tab before running the command.",
520
- },
521
- };
522
- }
523
- if (options.mode === "user-batch") {
524
- const userSteps = steps.slice(1);
525
- return {
526
- envelope: {
527
- success: userSteps.every((step) => step.success !== false),
528
- data: userSteps,
529
- error: userSteps.find((step) => step.success === false)?.error,
530
- },
531
- };
532
- }
533
- if (!commandStep) {
534
- return {
535
- envelope: {
536
- success: false,
537
- error: "agent-browser did not return the corrected command result.",
538
- },
539
- };
540
- }
541
-
542
- const navigationSummaryStep = options.includeNavigationSummary ? steps[2] : undefined;
543
- const navigationSummary = normalizeSessionTabTarget(extractNavigationSummaryFromData(navigationSummaryStep?.result));
544
- return {
545
- envelope: {
546
- success: commandStep.success !== false,
547
- data: commandStep.result,
548
- error: commandStep.success === false ? commandStep.error : undefined,
549
- },
550
- navigationSummary,
551
- };
552
- }
553
-
554
- export async function runSessionCommandData(options: {
555
- args: string[];
556
- cwd: string;
557
- sessionName?: string;
558
- signal?: AbortSignal;
559
- stdin?: string;
560
- timeoutMs?: number;
561
- }): Promise<unknown | undefined> {
562
- const { args, cwd, sessionName, signal, stdin, timeoutMs } = options;
563
- if (!sessionName) return undefined;
564
-
565
- const processResult = await runAgentBrowserProcess({
566
- args: ["--json", "--session", sessionName, ...args],
567
- cwd,
568
- signal,
569
- stdin,
570
- timeoutMs,
571
- });
572
- try {
573
- if (processResult.aborted || processResult.spawnError || processResult.exitCode !== 0) {
574
- return undefined;
575
- }
576
- const parsed = await parseAgentBrowserEnvelope({
577
- stdout: processResult.stdout,
578
- stdoutPath: processResult.stdoutSpillPath,
579
- });
580
- if (parsed.parseError || parsed.envelope?.success === false) {
581
- return undefined;
582
- }
583
- return parsed.envelope?.data;
584
- } finally {
585
- if (processResult.stdoutSpillPath) {
586
- await rm(processResult.stdoutSpillPath, { force: true }).catch(() => undefined);
587
- }
588
- }
589
- }
590
-
591
- export async function collectOpenResultTabCorrection(options: {
592
- cwd: string;
593
- sessionName?: string;
594
- signal?: AbortSignal;
595
- targetTitle?: string;
596
- targetUrl?: string;
597
- }): Promise<OpenResultTabCorrection | undefined> {
598
- const { cwd, sessionName, signal, targetTitle, targetUrl } = options;
599
- const tabData = await runSessionCommandData({ args: ["tab", "list"], cwd, sessionName, signal });
600
- if (!isRecord(tabData) || !Array.isArray(tabData.tabs)) {
601
- return undefined;
602
- }
603
- const tabs = tabData.tabs.filter(isRecord).map((tab, index) => ({
604
- active: tab.active === true,
605
- index: typeof tab.index === "number" ? tab.index : index,
606
- label: typeof tab.label === "string" ? tab.label : undefined,
607
- tabId: typeof tab.tabId === "string" ? tab.tabId : undefined,
608
- title: typeof tab.title === "string" ? tab.title : undefined,
609
- url: typeof tab.url === "string" ? tab.url : undefined,
610
- }));
611
- return chooseOpenResultTabCorrection({ tabs, targetTitle, targetUrl });
612
- }
613
-
614
- export async function collectSessionTabSelection(options: {
615
- cwd: string;
616
- sessionName?: string;
617
- signal?: AbortSignal;
618
- target: SessionTabTarget;
619
- }): Promise<OpenResultTabCorrection | undefined> {
620
- const { cwd, sessionName, signal, target } = options;
621
- const tabData = await runSessionCommandData({ args: ["tab", "list"], cwd, sessionName, signal });
622
- if (!isRecord(tabData) || !Array.isArray(tabData.tabs)) {
623
- return undefined;
624
- }
625
- const tabs = tabData.tabs.filter(isRecord).map((tab, index) => ({
626
- active: tab.active === true,
627
- index: typeof tab.index === "number" ? tab.index : index,
628
- label: typeof tab.label === "string" ? tab.label : undefined,
629
- tabId: typeof tab.tabId === "string" ? tab.tabId : undefined,
630
- title: typeof tab.title === "string" ? tab.title : undefined,
631
- url: typeof tab.url === "string" ? tab.url : undefined,
632
- }));
633
- return selectSessionTargetTab({ tabs, target });
634
- }
635
-
636
- export async function applyOpenResultTabCorrection(options: {
637
- correction: OpenResultTabCorrection;
638
- cwd: string;
639
- sessionName?: string;
640
- signal?: AbortSignal;
641
- }): Promise<OpenResultTabCorrection | undefined> {
642
- const { correction, cwd, sessionName, signal } = options;
643
- const result = await runSessionCommandData({
644
- args: ["tab", correction.selectedTab],
645
- cwd,
646
- sessionName,
647
- signal,
648
- });
649
- return result === undefined ? undefined : correction;
650
- }
651
-
652
- export function isLiveElectronRendererTarget(target: ElectronCdpTarget): boolean {
653
- const normalizedUrl = normalizeComparableUrl(target.url);
654
- if (!normalizedUrl || normalizedUrl === "about:blank" || normalizedUrl.startsWith("devtools://")) return false;
655
- return target.type === undefined || target.type === "page" || target.type === "webview";
656
- }
657
-
658
- export function getLiveElectronRendererTargets(targets: ElectronCdpTarget[]): ElectronCdpTarget[] {
659
- return targets.filter(isLiveElectronRendererTarget);
660
- }
661
-
662
- export function electronTargetLabel(target: ElectronCdpTarget | undefined): string {
663
- if (!target) return "unknown target";
664
- return [target.title, target.url, target.id].find((value) => typeof value === "string" && value.trim().length > 0) ?? "unknown target";
665
- }
666
-
667
- export function getActiveElectronRecords(records: Map<string, ElectronLaunchRecord>): ElectronLaunchRecord[] {
668
- return [...records.values()].filter((record) => record.cleanupState === "active" || record.cleanupState === "dead" || record.cleanupState === "partial" || record.cleanupState === "failed");
669
- }
670
-
671
- export function findElectronLaunchRecordForSession(sessionName: string | undefined, records: Map<string, ElectronLaunchRecord>): ElectronLaunchRecord | undefined {
672
- if (!sessionName) return undefined;
673
- return getActiveElectronRecords(records).find((record) => record.sessionName === sessionName);
674
- }
675
-
676
- function buildElectronReattachNextAction(record: ElectronLaunchRecord, liveTarget?: ElectronCdpTarget): AgentBrowserNextAction {
677
- const endpoint = liveTarget?.webSocketDebuggerUrl ?? record.webSocketDebuggerUrl ?? String(record.port);
678
- return {
679
- id: "reattach-electron-launch",
680
- params: { args: ["connect", endpoint], sessionMode: "fresh" },
681
- reason: "Attach a fresh managed session to the same wrapper-tracked Electron debug endpoint when the current session no longer matches the live renderer.",
682
- safety: "Creates a new managed browser session; it does not mutate the Electron app. Keep the launchId for later status and cleanup.",
683
- tool: "agent_browser",
684
- };
685
- }
686
-
687
- export function buildElectronMismatchNextActions(record: ElectronLaunchRecord, liveTarget?: ElectronCdpTarget): AgentBrowserNextAction[] {
688
- const baseActions = buildAgentBrowserNextActions({
689
- electron: { launchId: record.launchId, sessionName: record.sessionName, status: record.cleanupState },
690
- resultCategory: "success",
691
- successCategory: "completed",
692
- }) ?? [];
693
- const reattachAction = buildElectronReattachNextAction(record, liveTarget);
694
- const actions: AgentBrowserNextAction[] = [];
695
- for (const action of baseActions) {
696
- actions.push(action);
697
- if (action.id === "probe-electron-launch") actions.push(reattachAction);
698
- }
699
- if (!actions.some((action) => action.id === reattachAction.id)) actions.push(reattachAction);
700
- return actions;
701
- }
702
-
703
- export function buildElectronSessionMismatch(options: {
704
- managedSession: ElectronManagedSessionTarget;
705
- record: ElectronLaunchRecord;
706
- statusTargets: ElectronCdpTarget[];
707
- }): ElectronSessionMismatch | undefined {
708
- const liveTargets = getLiveElectronRendererTargets(options.statusTargets);
709
- if (liveTargets.length === 0) return undefined;
710
- const managedUrl = normalizeComparableUrl(options.managedSession.url);
711
- const matchingLiveTarget = managedUrl
712
- ? liveTargets.find((target) => normalizeComparableUrl(target.url) === managedUrl)
713
- : undefined;
714
- if (matchingLiveTarget) return undefined;
715
-
716
- const liveTarget = liveTargets[0];
717
- let reason: ElectronSessionMismatchReason | undefined;
718
- if (isAboutBlankUrl(options.managedSession.url)) {
719
- reason = "managed-session-about-blank-while-launch-target-live";
720
- } else if (options.record.sessionName && options.record.sessionName !== options.managedSession.sessionName) {
721
- reason = "launch-session-not-current";
722
- } else if (managedUrl) {
723
- reason = "managed-session-target-not-in-launch-status";
724
- }
725
- if (!reason) return undefined;
726
-
727
- const managedDescription = options.managedSession.url ?? options.managedSession.title ?? options.managedSession.sessionName;
728
- const liveDescription = electronTargetLabel(liveTarget);
729
- const summary = reason === "launch-session-not-current"
730
- ? `Electron session mismatch: current managed session ${options.managedSession.sessionName} is not the wrapper launch session ${options.record.sessionName ?? "unknown"}, while launch ${options.record.launchId} still has live target ${liveDescription}.`
731
- : `Electron session mismatch: managed session ${options.managedSession.sessionName} is on ${managedDescription}, but launch ${options.record.launchId} still has live target ${liveDescription}.`;
732
- const nextActions = buildElectronMismatchNextActions(options.record, liveTarget);
733
- return {
734
- launchId: options.record.launchId,
735
- liveTarget,
736
- managedSession: options.managedSession,
737
- nextActionIds: nextActions.map((action) => action.id),
738
- reason,
739
- sessionName: options.record.sessionName,
740
- statusTargets: options.statusTargets,
741
- summary,
742
- };
743
- }
744
-
745
- export function formatElectronSessionMismatchText(mismatch: ElectronSessionMismatch): string {
746
- return `${mismatch.summary}\nNext: run electron.status/electron.probe with launchId ${mismatch.launchId}, reattach with the reattach-electron-launch nextAction if needed, or cleanup when finished.`;
747
- }
748
-
749
- export function shouldInspectElectronPostCommandHealth(command: string | undefined): boolean {
750
- return isElectronPostCommandHealthCommand(command);
751
- }
752
-
753
- export function buildElectronLifecycleNextActions(record: ElectronLaunchRecord): AgentBrowserNextAction[] {
754
- return buildAgentBrowserNextActions({
755
- electron: { launchId: record.launchId, sessionName: record.sessionName, status: record.cleanupState },
756
- resultCategory: "success",
757
- successCategory: "completed",
758
- }) ?? [];
759
- }
760
-
761
- export function buildElectronPostCommandHealthDiagnostic(options: {
762
- command?: string;
763
- record: ElectronLaunchRecord;
764
- status: ElectronLaunchStatus;
765
- target?: SessionTabTarget;
766
- }): ElectronPostCommandHealthDiagnostic | undefined {
767
- let reason: ElectronPostCommandHealthReason | undefined;
768
- if (options.status.pidAlive === false) reason = "process-dead";
769
- else if (!options.status.portAlive) reason = "debug-port-dead";
770
- else if (isAboutBlankUrl(options.target?.url) && getLiveElectronRendererTargets(options.status.targets).length === 0) reason = "about-blank-no-live-target";
771
- if (!reason) return undefined;
772
- const nextActions = buildElectronLifecycleNextActions(options.record);
773
- const commandText = options.command ? `${options.command} command` : "command";
774
- const statusText = `${options.status.portAlive ? "debug port alive" : "debug port dead"}${options.status.pidAlive === undefined ? "" : options.status.pidAlive ? ", pid alive" : ", pid dead"}`;
775
- const summary = `Electron lifecycle warning: ${commandText} completed, but launch ${options.record.launchId} is no longer healthy (${statusText}).`;
776
- return {
777
- appName: options.record.appName,
778
- command: options.command,
779
- launchId: options.record.launchId,
780
- nextActionIds: nextActions.map((action) => action.id),
781
- reason,
782
- sessionName: options.record.sessionName,
783
- status: options.status,
784
- summary,
785
- target: options.target,
786
- };
787
- }
788
-
789
- export function formatElectronPostCommandHealthText(diagnostic: ElectronPostCommandHealthDiagnostic | undefined): string | undefined {
790
- if (!diagnostic) return undefined;
791
- const lines = [diagnostic.summary];
792
- if (diagnostic.target?.url) lines.push(`Current browser session target: ${diagnostic.target.url}.`);
793
- lines.push(`Status: ${diagnostic.status.portAlive ? "debug port alive" : "debug port dead"}${diagnostic.status.pidAlive === undefined ? "" : diagnostic.status.pidAlive ? ", pid alive" : ", pid dead"}; ${diagnostic.status.targets.length} CDP target(s).`);
794
- lines.push(`Next: run electron.status/electron.probe with launchId ${diagnostic.launchId}, cleanup the wrapper-owned launch if dead, or relaunch the app.`);
795
- return lines.join("\n");
796
- }
797
-
798
- export function buildElectronIdentifiers(record: ElectronLaunchRecord): { appName: string; launchId: string; sessionName?: string } {
799
- return { appName: record.appName, launchId: record.launchId, sessionName: record.sessionName };
800
- }
801
-
802
- export function buildElectronRefFreshnessNextActions(sessionName: string | undefined): AgentBrowserNextAction[] {
803
- return [{
804
- id: "refresh-electron-refs-after-rerender",
805
- params: { args: sessionName ? ["--session", sessionName, "snapshot", "-i"] : ["snapshot", "-i"] },
806
- reason: "Electron UIs often rerender without changing URL; refresh refs before using old @e handles again.",
807
- safety: "Read-only snapshot; avoids stale same-URL refs after quick-pick, modal, theme, or editor rerenders.",
808
- tool: "agent_browser",
809
- }];
810
- }
811
-
812
- export function buildElectronRefFreshnessDiagnostic(options: {
813
- command?: string;
814
- commandTokens: string[];
815
- record?: ElectronLaunchRecord;
816
- sessionName?: string;
817
- stdin?: string;
818
- }): ElectronRefFreshnessDiagnostic | undefined {
819
- if (!options.record || !shouldInspectElectronPostCommandHealth(options.command)) return undefined;
820
- if (getGuardedRefUsage(options.commandTokens, options.stdin).length === 0) return undefined;
821
- const nextActions = buildElectronRefFreshnessNextActions(options.sessionName);
822
- return {
823
- command: options.command,
824
- launchId: options.record.launchId,
825
- nextActionIds: nextActions.map((action) => action.id),
826
- sessionName: options.sessionName,
827
- summary: `Electron ref freshness: ${options.command ?? "mutation"} used page-scoped refs in an Electron UI. Re-run snapshot -i before reusing old @e refs, even if the URL did not change.`,
828
- };
829
- }
830
-
831
- export function formatElectronRefFreshnessText(diagnostic: ElectronRefFreshnessDiagnostic | undefined): string | undefined {
832
- return diagnostic?.summary;
833
- }
834
-
835
- export async function closeManagedSession(options: { cwd: string; sessionName: string; timeoutMs: number }): Promise<string | undefined> {
836
- const controller = new AbortController();
837
- const timer = setTimeout(() => controller.abort(), options.timeoutMs);
838
- let stdoutSpillPath: string | undefined;
839
- const closeArgs = ["--session", options.sessionName, "close"];
840
- try {
841
- const processResult = await runAgentBrowserProcess({
842
- args: closeArgs,
843
- cwd: options.cwd,
844
- signal: controller.signal,
845
- });
846
- stdoutSpillPath = processResult.stdoutSpillPath;
847
- return getAgentBrowserErrorText({
848
- aborted: processResult.aborted,
849
- command: "close",
850
- effectiveArgs: redactInvocationArgs(closeArgs),
851
- exitCode: processResult.exitCode,
852
- plainTextInspection: false,
853
- spawnError: processResult.spawnError,
854
- stderr: processResult.stderr,
855
- timedOut: processResult.timedOut,
856
- timeoutMs: processResult.timeoutMs,
857
- });
858
- } catch (error) {
859
- return error instanceof Error ? error.message : String(error);
860
- } finally {
861
- clearTimeout(timer);
862
- if (stdoutSpillPath) {
863
- await rm(stdoutSpillPath, { force: true }).catch(() => undefined);
864
- }
865
- }
866
- }
867
-
868
- export { extractBatchResultCommand };